diff --git a/.github/workflows/app-test-build-deploy.yaml b/.github/workflows/app-test-build-deploy.yaml index bc848da6a89..8c3bd21503d 100644 --- a/.github/workflows/app-test-build-deploy.yaml +++ b/.github/workflows/app-test-build-deploy.yaml @@ -185,28 +185,50 @@ jobs: echo "both develop builds for edge" echo 'variants=["release", "internal-release"]' >> $GITHUB_OUTPUT echo 'type=develop' >> $GITHUB_OUTPUT - elif [ "${{ format('{0}', endsWith(github.ref, 'app-build-internal')) }}" = "true" ] ; then - echo "internal-release builds for app-build-internal suffixes" + elif [ "${{ format('{0}', contains(github.ref, 'app-build-internal')) }}" = "true" ] ; then + echo 'variants=["internal-release"]' >> $GITHUB_OUTPUT - echo 'type=develop' >> $GITHUB_OUTPUT - elif [ "${{ format('{0}', endsWith(github.ref, 'app-build')) }}" = "true" ] ; then - echo "release develop builds for app-build suffixes" + if [ "${{ format('{0}', contains(github.ref, 'as-release')) }}" = "true" ] ; then + echo "internal-release as-release builds for app-build-internal + as-release suffixes" + echo 'type=as-release' >> $GITHUB_OUTPUT + else + echo "internal-release develop builds for app-build-internal suffixes" + echo 'type=develop' >> $GITHUB_OUTPUT + fi + elif [ "${{ format('{0}', contains(github.ref, 'app-build')) }}" = "true" ] ; then echo 'variants=["release"]' >> $GITHUB_OUTPUT - echo 'type=develop' >> $GITHUB_OUTPUT - elif [ "${{ format('{0}', endsWith(github.ref, 'app-build-both')) }}" = "true" ] ; then - echo "Both develop builds for app-build-both suffixes" + if [ "${{ format('{0}', contains(github.ref, 'as-release')) }}" = "true" ] ; then + echo "release as-release builds for app-build + as-release suffixes" + echo 'type=as-release' >> $GITHUB_OUTPUT + else + echo "release develop builds for app-build suffixes" + echo 'type=develop' >> $GITHUB_OUTPUT + fi + elif [ "${{ format('{0}', contains(github.ref, 'app-build-both')) }}" = "true" ] ; then + echo 'variants=["release", "internal-release"]' >> $GITHUB_OUTPUT - echo 'type=develop' >> $GITHUB_OUTPUT + if [ "${{ format('{0}', contains(github.ref, 'as-release')) }}" = "true" ] ; then + echo "Both as-release builds for app-build-both + as-release suffixes" + echo 'type=as-release' >> $GITHUB_OUTPUT + else + echo "Both develop builds for app-build-both + as-release suffixes" + echo 'type=develop' >> $GITHUB_OUTPUT + fi else echo "No build for ref ${{github.ref}} and event ${{github.event_type}}" echo 'variants=[]' >> $GITHUB_OUTPUT echo 'type=develop' >> $GITHUB_OUTPUT fi + - name: set summary + run: | + echo 'Type: ${{steps.determine-build-type.outputs.type}} Variants: ${{steps.determine-build-type.outputs.variants}}' >> $GITHUB_STEP_SUMMARY + build-app: needs: [determine-build-type] if: needs.determine-build-type.outputs.variants != '[]' strategy: + fail-fast: false matrix: os: ['windows-2022', 'ubuntu-22.04', 'macos-latest'] variant: ${{fromJSON(needs.determine-build-type.outputs.variants)}} @@ -277,6 +299,34 @@ jobs: npm config set cache ${{ github.workspace }}/.npm-cache yarn config set cache-folder ${{ github.workspace }}/.yarn-cache make setup-js + + - name: 'Configure Windows code signing environment' + if: startsWith(matrix.os, 'windows') && contains(needs.determine-build-type.outputs.type, 'release') + shell: bash + run: | + echo "${{ secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /d/Certificate_pkcs12.p12 + echo "${{ secrets.WINDOWS_CSC_B64}}" | base64 --decode > /d/opentrons_labworks_inc.crt + echo "C:\Program Files (x86)\Windows Kits\10\App Certification Kit" >> $GITHUB_PATH + echo "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools" >> $GITHUB_PATH + echo "C:\Program Files\DigiCert\DigiCert Keylocker Tools" >> $GITHUB_PATH + + - name: 'Setup Windows code signing helpers' + if: startsWith(matrix.os, 'windows') && contains(needs.determine-build-type.outputs.type, 'release') + shell: cmd + env: + 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}} + 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}}" -o Keylockertools-windows-x64.msi + msiexec /i Keylockertools-windows-x64.msi /quiet /qn + smksp_registrar.exe list + smctl.exe keypair ls + C:\Windows\System32\certutil.exe -csp "DigiCert Signing Manager KSP" -key -user + smksp_cert_sync.exe + smctl.exe healthcheck --all + # build the desktop app and deploy it - name: 'build ${{matrix.variant}} app for ${{ matrix.os }}' if: matrix.target == 'desktop' @@ -284,8 +334,14 @@ jobs: env: OT_APP_MIXPANEL_ID: ${{ secrets.OT_APP_MIXPANEL_ID }} OT_APP_INTERCOM_ID: ${{ secrets.OT_APP_INTERCOM_ID }} - WIN_CSC_LINK: ${{ secrets.OT_APP_CSC_WINDOWS }} - WIN_CSC_KEY_PASSWORD: ${{ secrets.OT_APP_CSC_KEY_WINDOWS }} + WINDOWS_SIGN: ${{ format('{0}', contains(needs.determine-build-type.outputs.type, 'release')) }} + 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 }} CSC_KEY_PASSWORD: ${{ secrets.OT_APP_CSC_KEY_MACOS }} APPLE_ID: ${{ secrets.OT_APP_APPLE_ID }} diff --git a/Makefile b/Makefile index ffdbb8509c0..47191131c7b 100755 --- a/Makefile +++ b/Makefile @@ -25,7 +25,7 @@ HARDWARE_DIR := hardware USB_BRIDGE_DIR := usb-bridge NODE_USB_BRIDGE_CLIENT_DIR := usb-bridge/node-client -PYTHON_DIRS := $(API_DIR) $(UPDATE_SERVER_DIR) $(ROBOT_SERVER_DIR) $(SERVER_UTILS_DIR) $(SHARED_DATA_DIR)/python $(G_CODE_TESTING_DIR) $(HARDWARE_DIR) $(USB_BRIDGE_DIR) +PYTHON_DIRS := $(API_DIR) $(UPDATE_SERVER_DIR) $(ROBOT_SERVER_DIR) $(SERVER_UTILS_DIR) $(SHARED_DATA_DIR)/python $(SYSTEM_SERVER_DIR) $(G_CODE_TESTING_DIR) $(HARDWARE_DIR) $(USB_BRIDGE_DIR) # This may be set as an environment variable (and is by CI tasks that upload # to test pypi) to add a .dev extension to the python package versions. If diff --git a/abr-testing/abr_testing/automation/google_sheets_tool.py b/abr-testing/abr_testing/automation/google_sheets_tool.py index 5e464754273..3a6590fa03b 100644 --- a/abr-testing/abr_testing/automation/google_sheets_tool.py +++ b/abr-testing/abr_testing/automation/google_sheets_tool.py @@ -136,7 +136,6 @@ def column_letter_to_index(column_letter: str) -> int: for col_offset, col_values in enumerate(data): column_index = start_column_index + col_offset - # column_letter = index_to_column_letter(column_index) for row_offset, value in enumerate(col_values): row_index = start_row + row_offset try: @@ -163,7 +162,10 @@ def column_letter_to_index(column_letter: str) -> int: ) body = {"requests": requests} - self.spread_sheet.batch_update(body=body) + try: + self.spread_sheet.batch_update(body=body) + except gspread.exceptions.APIError as e: + print(f"ERROR MESSAGE: {e}") def update_cell( self, sheet_title: str, row: int, column: int, single_data: Any diff --git a/abr-testing/abr_testing/data_collection/abr_google_drive.py b/abr-testing/abr_testing/data_collection/abr_google_drive.py index 1827d79cec0..3bd03cf3e3d 100644 --- a/abr-testing/abr_testing/data_collection/abr_google_drive.py +++ b/abr-testing/abr_testing/data_collection/abr_google_drive.py @@ -39,6 +39,8 @@ def create_data_dictionary( """Pull data from run files and format into a dictionary.""" runs_and_robots: List[Any] = [] runs_and_lpc: List[Dict[str, Any]] = [] + headers: List[str] = [] + headers_lpc: List[str] = [] for filename in os.listdir(storage_directory): file_path = os.path.join(storage_directory, filename) if file_path.endswith(".json"): @@ -49,7 +51,14 @@ def create_data_dictionary( if not isinstance(file_results, dict): continue run_id = file_results.get("run_id", "NaN") + try: + start_time_test = file_results["startedAt"] + completed_time_test = file_results["completedAt"] + except KeyError: + print(f"Run {run_id} is incomplete. Skipping run.") + continue if run_id in runs_to_save: + print("started reading run.") robot = file_results.get("robot_name") protocol_name = file_results["protocol"]["metadata"].get("protocolName", "") software_version = file_results.get("API_Version", "") @@ -74,15 +83,15 @@ def create_data_dictionary( ) try: start_time = datetime.strptime( - file_results.get("startedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + start_time_test, "%Y-%m-%dT%H:%M:%S.%f%z" ) - adjusted_start_time = start_time - timedelta(hours=5) + adjusted_start_time = start_time - timedelta(hours=4) start_date = str(adjusted_start_time.date()) start_time_str = str(adjusted_start_time).split("+")[0] complete_time = datetime.strptime( - file_results.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + completed_time_test, "%Y-%m-%dT%H:%M:%S.%f%z" ) - adjusted_complete_time = complete_time - timedelta(hours=5) + adjusted_complete_time = complete_time - timedelta(hours=4) complete_time_str = str(adjusted_complete_time).split("+")[0] run_time = complete_time - start_time run_time_min = run_time.total_seconds() / 60 @@ -130,8 +139,7 @@ def create_data_dictionary( **pipette_dict, **plate_measure, } - headers: List[str] = list(row_2.keys()) - # runs_and_robots[run_id] = row_2 + headers = list(row_2.keys()) runs_and_robots.append(list(row_2.values())) # LPC Data Recording runs_and_lpc, headers_lpc = read_robot_logs.lpc_data( @@ -139,6 +147,8 @@ def create_data_dictionary( ) else: continue + num_of_runs_read = len(runs_and_robots) + print(f"Number of runs read: {num_of_runs_read}") transposed_runs_and_robots = list(map(list, zip(*runs_and_robots))) transposed_runs_and_lpc = list(map(list, zip(*runs_and_lpc))) return transposed_runs_and_robots, headers, transposed_runs_and_lpc, headers_lpc @@ -207,7 +217,6 @@ def create_data_dictionary( start_row = google_sheet.get_index_row() + 1 print(start_row) google_sheet.batch_update_cells(transposed_runs_and_robots, "A", start_row, "0") - # Calculate Robot Lifetimes # Add LPC to google sheet google_sheet_lpc = google_sheets_tool.google_sheet(credentials_path, "ABR-LPC", 0) @@ -216,4 +225,5 @@ def create_data_dictionary( transposed_runs_and_lpc, "A", start_row_lpc, "0" ) robots = list(set(google_sheet.get_column(1))) + # Calculate Robot Lifetimes sync_abr_sheet.determine_lifetime(google_sheet) diff --git a/abr-testing/abr_testing/data_collection/abr_robot_error.py b/abr-testing/abr_testing/data_collection/abr_robot_error.py index 9e1569f47af..a45e64cd86d 100644 --- a/abr-testing/abr_testing/data_collection/abr_robot_error.py +++ b/abr-testing/abr_testing/data_collection/abr_robot_error.py @@ -1,5 +1,5 @@ """Create ticket for robot with error.""" -from typing import List, Tuple, Any +from typing import List, Tuple, Any, Dict, Optional from abr_testing.data_collection import read_robot_logs, abr_google_drive, get_run_logs import requests import argparse @@ -7,9 +7,141 @@ import shutil import os import subprocess +from datetime import datetime, timedelta import sys import json import re +import pandas as pd +from statistics import mean, StatisticsError + + +def compare_current_trh_to_average( + robot: str, + start_time: Any, + end_time: Optional[Any], + protocol_name: str, + storage_directory: str, +) -> str: + """Get average temp/rh for errored run and compare to average.""" + # Connect to ABR ambient conditions sheet + credentials_path = os.path.join(storage_directory, "credentials.json") + temprh_data_sheet = google_sheets_tool.google_sheet( + credentials_path, "ABR Ambient Conditions", 0 + ) + headers = temprh_data_sheet.get_row(1) + all_trh_data = temprh_data_sheet.get_all_data(expected_headers=headers) + # Connect to ABR-run-data sheet + abr_data = google_sheets_tool.google_sheet(credentials_path, "ABR-run-data", 0) + headers = abr_data.get_row(1) + all_run_data = abr_data.get_all_data(expected_headers=headers) + # Find average conditions of errored time period + df_all_trh = pd.DataFrame(all_trh_data) + # Convert timestamps to datetime objects + df_all_trh["Timestamp"] = pd.to_datetime( + df_all_trh["Timestamp"], format="mixed", utc=True + ).dt.tz_localize(None) + # Ensure start_time is timezone-naive + start_time = start_time.replace(tzinfo=None) + relevant_temp_rhs = df_all_trh[ + (df_all_trh["Robot"] == robot) & (df_all_trh["Timestamp"] >= start_time) + ] + try: + avg_temp = round(mean(relevant_temp_rhs["Temp (oC)"]), 2) + avg_rh = round(mean(relevant_temp_rhs["Relative Humidity (%)"]), 2) + except StatisticsError: + # If there is one value assign it as the average. + if len(relevant_temp_rhs["Temp (oC)"]) == 1: + avg_temp = relevant_temp_rhs["Temp (oC)"][0] + avg_rh = relevant_temp_rhs["Relative Humidity (%)"][0] + else: + avg_temp = None + avg_rh = None + # Get AVG t/rh of runs w/ same robot & protocol newer than 3 wks old with no errors + weeks_ago_3 = start_time - timedelta(weeks=3) + df_all_run_data = pd.DataFrame(all_run_data) + df_all_run_data["Start_Time"] = pd.to_datetime( + df_all_run_data["Start_Time"], format="mixed", utc=True + ).dt.tz_localize(None) + df_all_run_data["Errors"] = pd.to_numeric(df_all_run_data["Errors"]) + df_all_run_data["Average Temp (oC)"] = pd.to_numeric( + df_all_run_data["Average Temp (oC)"] + ) + common_filters = ( + (df_all_run_data["Robot"] == robot) + & (df_all_run_data["Start_Time"] >= weeks_ago_3) + & (df_all_run_data["Start_Time"] <= start_time) + & (df_all_run_data["Errors"] < 1) + & (df_all_run_data["Average Temp (oC)"] > 1) + ) + + if protocol_name == "": + relevant_run_data = df_all_run_data[common_filters] + else: + relevant_run_data = df_all_run_data[ + common_filters & (df_all_run_data["Protocol_Name"] == protocol_name) + ] + # Calculate means of historical data + try: + historical_avg_temp = round( + mean(relevant_run_data["Average Temp (oC)"].astype(float)), 2 + ) + historical_avg_rh = round( + mean(relevant_run_data["Average RH(%)"].astype(float)), 2 + ) + except StatisticsError: + historical_avg_temp = None + historical_avg_rh = None + # Formats TEMP/RH message for ticket. + temp_rh_message = ( + f"{len(relevant_run_data)} runs with temp/rh data for {robot} running {protocol_name}." + f" AVG TEMP (deg C): {historical_avg_temp}. AVG RH (%): {historical_avg_rh}." + f" AVG TEMP of ERROR: {avg_temp}. AVG RH of ERROR: {avg_rh}." + ) + # Print out comparison string. + print(temp_rh_message) + return temp_rh_message + + +def compare_lpc_to_historical_data( + labware_dict: Dict[str, Any], robot: str, storage_directory: str +) -> str: + """Compare LPC data of slot error occurred in to historical relevant data.""" + # Connect to LPC Google Sheet and get data. + credentials_path = os.path.join(storage_directory, "credentials.json") + google_sheet_lpc = google_sheets_tool.google_sheet(credentials_path, "ABR-LPC", 0) + headers = google_sheet_lpc.get_row(1) + all_lpc_data = google_sheet_lpc.get_all_data(expected_headers=headers) + df_lpc_data = pd.DataFrame(all_lpc_data) + labware = labware_dict["Labware Type"] + slot = labware_dict["Slot"] + # Filter data to match to appropriate labware and slot. + # Discludes any run with an error. + relevant_lpc = df_lpc_data[ + (df_lpc_data["Slot"] == slot) + & (df_lpc_data["Labware Type"] == labware) + & (df_lpc_data["Robot"] == robot) + & (df_lpc_data["Module"] == labware_dict["Module"]) + & (df_lpc_data["Adapter"] == labware_dict["Adapter"]) + & (df_lpc_data["Errors"] < 1) + ] + # Converts coordinates to floats and finds averages. + x_float = [float(value) for value in relevant_lpc["X"]] + y_float = [float(value) for value in relevant_lpc["Y"]] + z_float = [float(value) for value in relevant_lpc["Z"]] + current_x = round(labware_dict["X"], 2) + current_y = round(labware_dict["Y"], 2) + current_z = round(labware_dict["Z"], 2) + avg_x = round(mean(x_float), 2) + avg_y = round(mean(y_float), 2) + avg_z = round(mean(z_float), 2) + + # Formats LPC message for ticket. + lpc_message = ( + f"There were {len(x_float)} LPC coords found for {labware} at {slot}. " + f"AVERAGE POSITION: ({avg_x}, {avg_y}, {avg_z}). " + f"CURRENT POSITION: ({current_x}, {current_y}, {current_z})" + ) + return lpc_message def read_each_log(folder_path: str, issue_url: str) -> None: @@ -152,6 +284,15 @@ def get_robot_state( components = ["Flex-RABR"] components = match_error_to_component("RABR", reported_string, components) print(components) + end_time = datetime.now() + print(end_time) + start_time = end_time - timedelta(hours=2) + print(start_time) + # Get current temp/rh compared to historical data + temp_rh_string = compare_current_trh_to_average( + parent, start_time, end_time, "", storage_directory + ) + description["Robot Temp and RH Comparison"] = temp_rh_string whole_description_str = ( "{" + "\n".join("{!r}: {!r},".format(k, v) for k, v in description.items()) @@ -199,11 +340,62 @@ def get_run_error_info_from_robot( description["protocol_name"] = results["protocol"]["metadata"].get( "protocolName", "" ) + # Get start and end time of run + start_time = datetime.strptime( + results.get("startedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + adjusted_start_time = start_time - timedelta(hours=4) + complete_time = datetime.strptime( + results.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + adjusted_complete_time = complete_time - timedelta(hours=4) + # Get average temp and rh of robot and protocol the error occurred on. + temp_rh_comparison = compare_current_trh_to_average( + parent, + adjusted_start_time, + adjusted_complete_time, + description["protocol_name"], + storage_directory, + ) + # Get LPC coordinates of labware of failure + lpc_dict = results["labwareOffsets"] + labware_dict = results["labware"] description["error"] = " ".join([error_code, error_type, error_instrument]) - description["protocol_step"] = list(results["commands"])[-1] + protocol_step = list(results["commands"])[-1] + errored_labware_id = protocol_step["params"].get("labwareId", "") + errored_labware_dict = {} + lpc_message = "" + # If there is labware included in the error message, its LPC coords will be extracted. + if len(errored_labware_id) > 0: + for labware in labware_dict: + if labware["id"] == errored_labware_id: + errored_labware_dict["Slot"] = labware["location"].get("slotName", "") + errored_labware_dict["Labware Type"] = labware.get("definitionUri", "") + offset_id = labware.get("offsetId", "") + for lpc in lpc_dict: + if lpc.get("id", "") == offset_id: + errored_labware_dict["X"] = lpc["vector"].get("x", "") + errored_labware_dict["Y"] = lpc["vector"].get("y", "") + errored_labware_dict["Z"] = lpc["vector"].get("z", "") + errored_labware_dict["Module"] = lpc["location"].get( + "moduleModel", "" + ) + errored_labware_dict["Adapter"] = lpc["location"].get( + "definitionUri", "" + ) + + lpc_message = compare_lpc_to_historical_data( + errored_labware_dict, parent, storage_directory + ) + + description["protocol_step"] = protocol_step description["right_mount"] = results.get("right", "No attachment") description["left_mount"] = results.get("left", "No attachment") description["gripper"] = results.get("extension", "No attachment") + if len(lpc_message) < 1: + lpc_message = "No LPC coordinates found in relation to error." + description["LPC Comparison"] = lpc_message + description["Robot Temp and RH Comparison"] = temp_rh_comparison all_modules = abr_google_drive.get_modules(results) whole_description = {**description, **all_modules} whole_description_str = ( @@ -276,6 +468,7 @@ def get_run_error_info_from_robot( email = args.email[0] board_id = args.board_id[0] reporter_id = args.reporter_id[0] + file_paths = read_robot_logs.get_logs(storage_directory, ip) ticket = jira_tool.JiraTicket(url, api_token, email) ticket.issues_on_board(board_id) users_file_path = ticket.get_jira_users(storage_directory) @@ -308,7 +501,6 @@ def get_run_error_info_from_robot( saved_file_path_calibration, calibration = read_robot_logs.get_calibration_offsets( ip, storage_directory ) - file_paths = read_robot_logs.get_logs(storage_directory, ip) print(f"Making ticket for {summary}.") # TODO: make argument or see if I can get rid of with using board_id. diff --git a/abr-testing/abr_testing/data_collection/read_robot_logs.py b/abr-testing/abr_testing/data_collection/read_robot_logs.py index ac3636ed8a7..740adbf0cb6 100644 --- a/abr-testing/abr_testing/data_collection/read_robot_logs.py +++ b/abr-testing/abr_testing/data_collection/read_robot_logs.py @@ -569,23 +569,32 @@ def get_calibration_offsets( def get_logs(storage_directory: str, ip: str) -> List[str]: """Get Robot logs.""" - log_types = ["api.log", "server.log", "serial.log", "touchscreen.log"] + log_types: List[Dict[str, Any]] = [ + {"log type": "api.log", "records": 1000}, + {"log type": "server.log", "records": 10000}, + {"log type": "serial.log", "records": 10000}, + {"log type": "touchscreen.log", "records": 1000}, + ] all_paths = [] for log_type in log_types: try: + log_type_name = log_type["log type"] + print(log_type_name) + log_records = int(log_type["records"]) + print(log_records) response = requests.get( - f"http://{ip}:31950/logs/{log_type}", - headers={"log_identifier": log_type}, - params={"records": 5000}, + f"http://{ip}:31950/logs/{log_type_name}", + headers={"log_identifier": log_type_name}, + params={"records": log_records}, ) response.raise_for_status() log_data = response.text - log_name = ip + "_" + log_type.split(".")[0] + ".log" + log_name = ip + "_" + log_type_name.split(".")[0] + ".log" file_path = os.path.join(storage_directory, log_name) with open(file_path, mode="w", encoding="utf-8") as file: file.write(log_data) except RuntimeError: - print(f"Request exception. Did not save {log_type}") + print(f"Request exception. Did not save {log_type_name}") continue all_paths.append(file_path) # Get weston.log using scp diff --git a/abr-testing/abr_testing/tools/abr_scale.py b/abr-testing/abr_testing/tools/abr_scale.py index 6d83df03f2b..d02bf0acfed 100644 --- a/abr-testing/abr_testing/tools/abr_scale.py +++ b/abr-testing/abr_testing/tools/abr_scale.py @@ -34,19 +34,29 @@ def get_protocol_step_as_int( # create an dict copying the contents of IP_N_Volumes try: ip_file = json.load(open(ip_json_file)) + try: + # grab IP and volume from the dict + tot_info = ip_file["information"] + robot_info = tot_info[robot] + IP_add = robot_info["IP"] + exp_volume = robot_info["volume"] + # sets return variables equal to those grabbed from the sheet + ip = IP_add + expected_liquid_moved = float(exp_volume) + except KeyError: + ip = input("Robot IP: ") + while True: + try: + expected_liquid_moved = float(input("Expected volume moved: ")) + if expected_liquid_moved >= 0 or expected_liquid_moved <= 0: + break + except ValueError: + print("Expected liquid moved volume should be an float.") except FileNotFoundError: print( f"Please add json file with robot IPs and expected volumes to: {storage_directory}." ) sys.exit() - # grab IP and volume from the dict - tot_info = ip_file["information"] - robot_info = tot_info[robot] - IP_add = robot_info["IP"] - exp_volume = robot_info["volume"] - # sets return variables equal to those grabbed from the sheet - ip = IP_add - expected_liquid_moved = float(exp_volume) return protocol_step, expected_liquid_moved, ip @@ -113,9 +123,25 @@ def get_most_recent_run_and_record( most_recent_run_id = run_list[-1]["id"] results = get_run_logs.get_run_data(most_recent_run_id, ip) # Save run information to local directory as .json file - read_robot_logs.save_run_log_to_json(ip, results, storage_directory) + saved_file_path = read_robot_logs.save_run_log_to_json( + ip, results, storage_directory + ) + # Check that last run is completed. + with open(saved_file_path) as file: + file_results = json.load(file) + try: + file_results["completedAt"] + except ValueError: + # no completedAt field, get run before the last run. + most_recent_run_id = run_list[-2]["id"] + results = get_run_logs.get_run_data(most_recent_run_id, ip) + # Save run information to local directory as .json file + saved_file_path = read_robot_logs.save_run_log_to_json( + ip, results, storage_directory + ) # Record run to google sheets. print(most_recent_run_id) + ( runs_and_robots, headers, diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[89a8226c4e][Flex_X_v2_16_P1000_96_TC_PartialTipPickupThermocyclerLidConflict].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[89a8226c4e][Flex_X_v2_16_P1000_96_TC_PartialTipPickupThermocyclerLidConflict].json index c30512b818b..cf0293eee21 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[89a8226c4e][Flex_X_v2_16_P1000_96_TC_PartialTipPickupThermocyclerLidConflict].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[89a8226c4e][Flex_X_v2_16_P1000_96_TC_PartialTipPickupThermocyclerLidConflict].json @@ -4889,39 +4889,6 @@ }, "startedAt": "TIMESTAMP", "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "517162d1e8d73c035348a1870a8abc8a", - "notes": [], - "params": { - "flowRate": 160.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 20.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -9.8 - }, - "origin": "top" - }, - "wellName": "A2" - }, - "result": { - "position": { - "x": 23.28, - "y": 181.18, - "z": 4.5 - }, - "volume": 20.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" } ], "config": { @@ -4935,7 +4902,7 @@ "errors": [ { "createdAt": "TIMESTAMP", - "detail": "PartialTipMovementNotAllowedError [line 26]: Error 2004 MOTION_PLANNING_FAILURE (PartialTipMovementNotAllowedError): Moving to NEST 96 Well Plate 200 µL Flat in slot A2 with A12 nozzle partial configuration will result in collision with thermocycler lid in deck slot A1.", + "detail": "PartialTipMovementNotAllowedError [line 24]: Error 2004 MOTION_PLANNING_FAILURE (PartialTipMovementNotAllowedError): Requested motion with the A12 nozzle partial configuration is outside of robot bounds for the pipette.", "errorCode": "4000", "errorInfo": {}, "errorType": "ExceptionInProtocolError", @@ -4944,7 +4911,7 @@ "wrappedErrors": [ { "createdAt": "TIMESTAMP", - "detail": "Moving to NEST 96 Well Plate 200 µL Flat in slot A2 with A12 nozzle partial configuration will result in collision with thermocycler lid in deck slot A1.", + "detail": "Requested motion with the A12 nozzle partial configuration is outside of robot bounds for the pipette.", "errorCode": "2004", "errorInfo": {}, "errorType": "PartialTipMovementNotAllowedError", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8fcfd2ced0][Flex_S_v2_16_P1000_96_TC_PartialTipPickupColumn].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8fcfd2ced0][Flex_S_v2_16_P1000_96_TC_PartialTipPickupColumn].json index 10ee86bd162..02df13c1a33 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8fcfd2ced0][Flex_S_v2_16_P1000_96_TC_PartialTipPickupColumn].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8fcfd2ced0][Flex_S_v2_16_P1000_96_TC_PartialTipPickupColumn].json @@ -3606,82 +3606,6 @@ }, "startedAt": "TIMESTAMP", "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "ddccee6754fe0092b9c66898d66b79a7", - "notes": [], - "params": { - "flowRate": 160.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 20.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -9.8 - }, - "origin": "top" - }, - "wellName": "A2" - }, - "result": { - "position": { - "x": 23.28, - "y": 181.18, - "z": 4.5 - }, - "volume": 20.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "moveToAddressableAreaForDropTip", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "5287b77e909d217f4b05e5006cf9ff25", - "notes": [], - "params": { - "addressableAreaName": "movableTrashA3", - "alternateDropLocation": true, - "forceDirect": false, - "ignoreTipConfiguration": true, - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "pipetteId": "UUID" - }, - "result": { - "position": { - "x": 466.25, - "y": 364.0, - "z": 40.0 - } - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" - }, - { - "commandType": "dropTipInPlace", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "b81364c35c04784c34f571446e64484c", - "notes": [], - "params": { - "pipetteId": "UUID" - }, - "result": {}, - "startedAt": "TIMESTAMP", - "status": "succeeded" } ], "config": { @@ -3692,7 +3616,29 @@ "protocolType": "python" }, "createdAt": "TIMESTAMP", - "errors": [], + "errors": [ + { + "createdAt": "TIMESTAMP", + "detail": "PartialTipMovementNotAllowedError [line 20]: Error 2004 MOTION_PLANNING_FAILURE (PartialTipMovementNotAllowedError): Requested motion with the A12 nozzle partial configuration is outside of robot bounds for the pipette.", + "errorCode": "4000", + "errorInfo": {}, + "errorType": "ExceptionInProtocolError", + "id": "UUID", + "isDefined": false, + "wrappedErrors": [ + { + "createdAt": "TIMESTAMP", + "detail": "Requested motion with the A12 nozzle partial configuration is outside of robot bounds for the pipette.", + "errorCode": "2004", + "errorInfo": {}, + "errorType": "PartialTipMovementNotAllowedError", + "id": "UUID", + "isDefined": false, + "wrappedErrors": [] + } + ] + } + ], "files": [ { "name": "Flex_S_v2_16_P1000_96_TC_PartialTipPickupColumn.py", @@ -3735,7 +3681,7 @@ "pipetteName": "p1000_96" } ], - "result": "ok", + "result": "not-ok", "robotType": "OT-3 Standard", "runTimeParameters": [] } diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[adc0621263][Flex_X_v2_16_P1000_96_TC_pipetteCollisionWithThermocyclerLid].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[adc0621263][Flex_X_v2_16_P1000_96_TC_pipetteCollisionWithThermocyclerLid].json index 66957b72660..a3cf2d44d05 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[adc0621263][Flex_X_v2_16_P1000_96_TC_pipetteCollisionWithThermocyclerLid].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[adc0621263][Flex_X_v2_16_P1000_96_TC_pipetteCollisionWithThermocyclerLid].json @@ -6116,39 +6116,6 @@ }, "startedAt": "TIMESTAMP", "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "e1b16944e3d0ff8ae0a964f7e638c1b3", - "notes": [], - "params": { - "flowRate": 160.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 20.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -9.8 - }, - "origin": "top" - }, - "wellName": "A2" - }, - "result": { - "position": { - "x": 23.28, - "y": 181.18, - "z": 4.5 - }, - "volume": 20.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" } ], "config": { @@ -6162,7 +6129,7 @@ "errors": [ { "createdAt": "TIMESTAMP", - "detail": "PartialTipMovementNotAllowedError [line 28]: Error 2004 MOTION_PLANNING_FAILURE (PartialTipMovementNotAllowedError): Moving to NEST 96 Well Plate 200 µL Flat in slot A2 with A12 nozzle partial configuration will result in collision with thermocycler lid in deck slot A1.", + "detail": "PartialTipMovementNotAllowedError [line 25]: Error 2004 MOTION_PLANNING_FAILURE (PartialTipMovementNotAllowedError): Requested motion with the A12 nozzle partial configuration is outside of robot bounds for the pipette.", "errorCode": "4000", "errorInfo": {}, "errorType": "ExceptionInProtocolError", @@ -6171,7 +6138,7 @@ "wrappedErrors": [ { "createdAt": "TIMESTAMP", - "detail": "Moving to NEST 96 Well Plate 200 µL Flat in slot A2 with A12 nozzle partial configuration will result in collision with thermocycler lid in deck slot A1.", + "detail": "Requested motion with the A12 nozzle partial configuration is outside of robot bounds for the pipette.", "errorCode": "2004", "errorInfo": {}, "errorType": "PartialTipMovementNotAllowedError", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b0ce7dde5d][Flex_X_v2_16_P1000_96_TC_PartialTipPickupTryToReturnTip].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b0ce7dde5d][Flex_X_v2_16_P1000_96_TC_PartialTipPickupTryToReturnTip].json index cdb9d4235a9..32e9e2f9294 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b0ce7dde5d][Flex_X_v2_16_P1000_96_TC_PartialTipPickupTryToReturnTip].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b0ce7dde5d][Flex_X_v2_16_P1000_96_TC_PartialTipPickupTryToReturnTip].json @@ -3606,39 +3606,6 @@ }, "startedAt": "TIMESTAMP", "status": "succeeded" - }, - { - "commandType": "dispense", - "completedAt": "TIMESTAMP", - "createdAt": "TIMESTAMP", - "id": "UUID", - "key": "ddccee6754fe0092b9c66898d66b79a7", - "notes": [], - "params": { - "flowRate": 160.0, - "labwareId": "UUID", - "pipetteId": "UUID", - "volume": 20.0, - "wellLocation": { - "offset": { - "x": 0.0, - "y": 0.0, - "z": -9.8 - }, - "origin": "top" - }, - "wellName": "A2" - }, - "result": { - "position": { - "x": 23.28, - "y": 181.18, - "z": 4.5 - }, - "volume": 20.0 - }, - "startedAt": "TIMESTAMP", - "status": "succeeded" } ], "config": { @@ -3652,7 +3619,7 @@ "errors": [ { "createdAt": "TIMESTAMP", - "detail": "UnexpectedProtocolError [line 22]: Error 4000 GENERAL_ERROR (UnexpectedProtocolError): Cannot return tip to a tiprack while the pipette is configured for partial tip.", + "detail": "PartialTipMovementNotAllowedError [line 21]: Error 2004 MOTION_PLANNING_FAILURE (PartialTipMovementNotAllowedError): Requested motion with the A12 nozzle partial configuration is outside of robot bounds for the pipette.", "errorCode": "4000", "errorInfo": {}, "errorType": "ExceptionInProtocolError", @@ -3661,10 +3628,10 @@ "wrappedErrors": [ { "createdAt": "TIMESTAMP", - "detail": "Cannot return tip to a tiprack while the pipette is configured for partial tip.", - "errorCode": "4000", + "detail": "Requested motion with the A12 nozzle partial configuration is outside of robot bounds for the pipette.", + "errorCode": "2004", "errorInfo": {}, - "errorType": "UnexpectedProtocolError", + "errorType": "PartialTipMovementNotAllowedError", "id": "UUID", "isDefined": false, "wrappedErrors": [] diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d8cb88b3b2][Flex_S_v2_16_P1000_96_TC_PartialTipPickupSingle].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d8cb88b3b2][Flex_S_v2_16_P1000_96_TC_PartialTipPickupSingle].json index f658c602a39..78da0891438 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d8cb88b3b2][Flex_S_v2_16_P1000_96_TC_PartialTipPickupSingle].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d8cb88b3b2][Flex_S_v2_16_P1000_96_TC_PartialTipPickupSingle].json @@ -3535,7 +3535,7 @@ "errors": [ { "createdAt": "TIMESTAMP", - "detail": "ValueError [line 16]: Nozzle layout configuration of style SINGLE is currently unsupported.", + "detail": "ValueError [line 16]: Nozzle layout configuration of style SINGLE is unsupported in API Versions lower than 2.20.", "errorCode": "4000", "errorInfo": {}, "errorType": "ExceptionInProtocolError", @@ -3544,10 +3544,10 @@ "wrappedErrors": [ { "createdAt": "TIMESTAMP", - "detail": "ValueError: Nozzle layout configuration of style SINGLE is currently unsupported.", + "detail": "ValueError: Nozzle layout configuration of style SINGLE is unsupported in API Versions lower than 2.20.", "errorCode": "4000", "errorInfo": { - "args": "('Nozzle layout configuration of style SINGLE is currently unsupported.',)", + "args": "('Nozzle layout configuration of style SINGLE is unsupported in API Versions lower than 2.20.',)", "class": "ValueError", "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line N, in exec_run\n exec(\"run(__context)\", new_globs)\n\n File \"\", line N, in \n\n File \"Flex_S_v2_16_P1000_96_TC_PartialTipPickupSingle.py\", line N, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line N, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/instrument_context.py\", line N, in configure_nozzle_layout\n raise ValueError(\n" }, diff --git a/api-client/src/dataFiles/uploadCsvFile.ts b/api-client/src/dataFiles/uploadCsvFile.ts index 051d44cc26b..8f48216379a 100644 --- a/api-client/src/dataFiles/uploadCsvFile.ts +++ b/api-client/src/dataFiles/uploadCsvFile.ts @@ -8,11 +8,17 @@ export function uploadCsvFile( config: HostConfig, data: FileData ): ResponsePromise { - return request( + const formData = new FormData() + + if (typeof data !== 'string') { + formData.append('file', data) + } else { + formData.append('filePath', data) + } + return request( POST, '/dataFiles', - null, - config, - data + formData, + config ) } diff --git a/api-client/src/protocols/createProtocol.ts b/api-client/src/protocols/createProtocol.ts index 98712031246..965f7e9e962 100644 --- a/api-client/src/protocols/createProtocol.ts +++ b/api-client/src/protocols/createProtocol.ts @@ -2,14 +2,18 @@ import { POST, request } from '../request' import type { ResponsePromise } from '../request' import type { HostConfig } from '../types' import type { Protocol } from './types' -import type { RunTimeParameterCreateData } from '../runs' +import type { + RunTimeParameterValuesCreateData, + RunTimeParameterFilesCreateData, +} from '../runs' export function createProtocol( config: HostConfig, files: File[], protocolKey?: string, protocolKind?: string, - runTimeParameterValues?: RunTimeParameterCreateData + runTimeParameterValues?: RunTimeParameterValuesCreateData, + runTimeParameterFiles?: RunTimeParameterFilesCreateData ): ResponsePromise { const formData = new FormData() files.forEach(file => { @@ -22,6 +26,11 @@ export function createProtocol( 'runTimeParameterValues', JSON.stringify(runTimeParameterValues) ) + if (runTimeParameterFiles != null) + formData.append( + 'runTimeParameterFiles', + JSON.stringify(runTimeParameterFiles) + ) return request(POST, '/protocols', formData, config) } diff --git a/api-client/src/protocols/createProtocolAnalysis.ts b/api-client/src/protocols/createProtocolAnalysis.ts index 81ab83c11af..faf10907c85 100644 --- a/api-client/src/protocols/createProtocolAnalysis.ts +++ b/api-client/src/protocols/createProtocolAnalysis.ts @@ -3,21 +3,27 @@ import { POST, request } from '../request' import type { ProtocolAnalysisSummary } from '@opentrons/shared-data' import type { ResponsePromise } from '../request' import type { HostConfig } from '../types' -import type { RunTimeParameterCreateData } from '../runs' +import type { + RunTimeParameterFilesCreateData, + RunTimeParameterValuesCreateData, +} from '../runs' interface CreateProtocolAnalysisData { - runTimeParameterValues: RunTimeParameterCreateData + runTimeParameterValues: RunTimeParameterValuesCreateData + runTimeParameterFiles: RunTimeParameterFilesCreateData forceReAnalyze: boolean } export function createProtocolAnalysis( config: HostConfig, protocolKey: string, - runTimeParameterValues?: RunTimeParameterCreateData, + runTimeParameterValues?: RunTimeParameterValuesCreateData, + runTimeParameterFiles?: RunTimeParameterFilesCreateData, forceReAnalyze?: boolean ): ResponsePromise { const data = { runTimeParameterValues: runTimeParameterValues ?? {}, + runTimeParameterFiles: runTimeParameterFiles ?? {}, forceReAnalyze: forceReAnalyze ?? false, } const response = request< diff --git a/api-client/src/protocols/getCsvFiles.ts b/api-client/src/protocols/getCsvFiles.ts index 8cb962f795a..ebfd7f19a74 100644 --- a/api-client/src/protocols/getCsvFiles.ts +++ b/api-client/src/protocols/getCsvFiles.ts @@ -1,5 +1,3 @@ -import { v4 as uuidv4 } from 'uuid' - // import { GET, request } from '../request' // import type { ResponsePromise } from '../request' @@ -25,18 +23,16 @@ export function getCsvFiles( config: HostConfig, protocolId: string ): Promise<{ data: UploadedCsvFilesResponse }> { - const fileIdOne = uuidv4() - const fileIdTwo = uuidv4() const stub = { data: { files: [ { - id: fileIdOne, + id: '1', createdAt: '2024-06-07T19:19:56.268029+00:00', name: 'rtp_mock_file1.csv', }, { - id: fileIdTwo, + id: '2', createdAt: '2024-06-17T19:19:56.268029+00:00', name: 'rtp_mock_file2.csv', }, diff --git a/api-client/src/runs/createRun.ts b/api-client/src/runs/createRun.ts index 7f0fb1ad72d..6e8cd4b7525 100644 --- a/api-client/src/runs/createRun.ts +++ b/api-client/src/runs/createRun.ts @@ -5,13 +5,15 @@ import type { HostConfig } from '../types' import type { Run, LabwareOffsetCreateData, - RunTimeParameterCreateData, + RunTimeParameterValuesCreateData, + RunTimeParameterFilesCreateData, } from './types' export interface CreateRunData { protocolId?: string labwareOffsets?: LabwareOffsetCreateData[] - runTimeParameterValues?: RunTimeParameterCreateData + runTimeParameterValues?: RunTimeParameterValuesCreateData + runTimeParameterFiles?: RunTimeParameterFilesCreateData } export function createRun( diff --git a/api-client/src/runs/types.ts b/api-client/src/runs/types.ts index b2416d7a31a..45e40f2f8b9 100644 --- a/api-client/src/runs/types.ts +++ b/api-client/src/runs/types.ts @@ -132,11 +132,12 @@ export interface LabwareOffsetCreateData { vector: VectorOffset } -type RunTimeParameterValueType = string | number | boolean | { id: string } -export type RunTimeParameterCreateData = Record< +type RunTimeParameterValuesType = string | number | boolean | { id: string } +export type RunTimeParameterValuesCreateData = Record< string, - RunTimeParameterValueType + RunTimeParameterValuesType > +export type RunTimeParameterFilesCreateData = Record export interface CommandData { data: RunTimeCommand diff --git a/api/release-notes-internal.md b/api/release-notes-internal.md index 353df2e8833..4cffb961116 100644 --- a/api/release-notes-internal.md +++ b/api/release-notes-internal.md @@ -2,6 +2,24 @@ For more details about this release, please see the full [technical change log][ [technical change log]: https://github.com/Opentrons/opentrons/releases +## Internal Release 2.0.0-alpha.1 + +This internal release, pulled from the `edge` branch, contains features being developed for 8.0.0. It's for internal testing only. Usage may require a robot factory reset to restore robot stability. + + + +## Internal Release 2.0.0-alpha.0 + +This internal release, pulled from the `edge` branch, contains features being developed for 8.0.0. It's for internal testing only. Usage may require a robot factory reset to restore robot stability. + + + +## Internal Release 1.5.0 + +This internal release is from the `edge` branch to contain rapid dev on new features for 7.3.0. This release is for internal testing purposes and if used may require a factory reset of the robot to return to a stable version. Though designated as stable, this build contains many critical bugs and should not be used in production. + + + ## Internal Release 1.5.0-alpha.1 This internal release is from the `edge` branch to contain rapid dev on new features for 7.3.0. This release is for internal testing purposes and if used may require a factory reset of the robot to return to a stable version. diff --git a/api/setup.py b/api/setup.py index 1b2a7dde508..8c1dd7cfa63 100755 --- a/api/setup.py +++ b/api/setup.py @@ -59,7 +59,7 @@ def get_version(): f"opentrons-shared-data=={VERSION}", "aionotify==0.3.1", "anyio>=3.6.1,<4.0.0", - "jsonschema>=3.0.1,<4.18.0", + "jsonschema>=3.0.1,<5", "numpy>=1.20.0,<2", "pydantic>=1.10.9,<2.0.0", "pyserial>=3.5", diff --git a/api/src/opentrons/config/defaults_ot3.py b/api/src/opentrons/config/defaults_ot3.py index e6d91cab081..b09235ce35b 100644 --- a/api/src/opentrons/config/defaults_ot3.py +++ b/api/src/opentrons/config/defaults_ot3.py @@ -23,8 +23,8 @@ DEFAULT_MODULE_OFFSET = [0.0, 0.0, 0.0] DEFAULT_LIQUID_PROBE_SETTINGS: Final[LiquidProbeSettings] = LiquidProbeSettings( - mount_speed=10, - plunger_speed=5, + mount_speed=5, + plunger_speed=20, plunger_impulse_time=0.2, sensor_threshold_pascals=15, output_option=OutputOptions.sync_buffer_to_csv, @@ -328,7 +328,7 @@ def _build_default_liquid_probe( or output_option is OutputOptions.stream_to_csv ): data_files = _build_log_files_with_default( - from_conf.get("data_files", {}), default.data_files + from_conf.get("data_files", None), default.data_files ) return LiquidProbeSettings( mount_speed=from_conf.get("mount_speed", default.mount_speed), diff --git a/api/src/opentrons/config/gripper_config.py b/api/src/opentrons/config/gripper_config.py index 0c364bc749c..1de9be1de0b 100644 --- a/api/src/opentrons/config/gripper_config.py +++ b/api/src/opentrons/config/gripper_config.py @@ -24,7 +24,12 @@ def info_num_to_model(num: str) -> GripperModel: # PVT will now be 1.2 model_map = { "0": {"0": GripperModel.v1, "1": GripperModel.v1}, - "1": {"0": GripperModel.v1, "1": GripperModel.v1_1, "2": GripperModel.v1_2}, + "1": { + "0": GripperModel.v1, + "1": GripperModel.v1_1, + "2": GripperModel.v1_2, + "3": GripperModel.v1_3, + }, } return model_map[major_model][minor_model] diff --git a/api/src/opentrons/hardware_control/backends/flex_protocol.py b/api/src/opentrons/hardware_control/backends/flex_protocol.py index 6e96f3f3485..9e7218099cc 100644 --- a/api/src/opentrons/hardware_control/backends/flex_protocol.py +++ b/api/src/opentrons/hardware_control/backends/flex_protocol.py @@ -146,6 +146,7 @@ async def liquid_probe( mount_speed: float, plunger_speed: float, threshold_pascals: float, + plunger_impulse_time: float, output_format: OutputOptions = OutputOptions.can_bus_only, data_files: Optional[Dict[InstrumentProbeType, str]] = None, probe: InstrumentProbeType = InstrumentProbeType.PRIMARY, diff --git a/api/src/opentrons/hardware_control/backends/ot3controller.py b/api/src/opentrons/hardware_control/backends/ot3controller.py index fd901955022..cd6aa9e112a 100644 --- a/api/src/opentrons/hardware_control/backends/ot3controller.py +++ b/api/src/opentrons/hardware_control/backends/ot3controller.py @@ -194,7 +194,6 @@ PipetteLiquidNotFoundError, CommunicationError, PythonException, - UnsupportedHardwareCommand, ) from .subsystem_manager import SubsystemManager @@ -644,7 +643,7 @@ async def move( origin=origin, target_list=[move_target] ) except ZeroLengthMoveError as zme: - log.warning(f"Not moving because move was zero length {str(zme)}") + log.debug(f"Not moving because move was zero length {str(zme)}") return moves = movelist[0] log.info(f"move: machine {target} from {origin} requires {moves}") @@ -1358,22 +1357,12 @@ async def liquid_probe( mount_speed: float, plunger_speed: float, threshold_pascals: float, + plunger_impulse_time: float, output_option: OutputOptions = OutputOptions.can_bus_only, data_files: Optional[Dict[InstrumentProbeType, str]] = None, probe: InstrumentProbeType = InstrumentProbeType.PRIMARY, force_both_sensors: bool = False, ) -> float: - if output_option == OutputOptions.sync_buffer_to_csv: - if ( - self._subsystem_manager.device_info[ - SubSystem.of_mount(mount) - ].revision.tertiary - != "1" - ): - raise UnsupportedHardwareCommand( - "Liquid Probe not supported on this pipette firmware" - ) - head_node = axis_to_node(Axis.by_mount(mount)) tool = sensor_node_for_pipette(OT3Mount(mount.value)) csv_output = bool(output_option.value & OutputOptions.stream_to_csv.value) @@ -1399,6 +1388,7 @@ async def liquid_probe( plunger_speed=plunger_speed, mount_speed=mount_speed, threshold_pascals=threshold_pascals, + plunger_impulse_time=plunger_impulse_time, csv_output=csv_output, sync_buffer_output=sync_buffer_output, can_bus_only_output=can_bus_only_output, @@ -1461,7 +1451,6 @@ async def capacitive_probe( tool=sensor_node_for_mount(mount), mover=axis_to_node(moving), distance=distance_mm, - plunger_speed=speed_mm_per_s, mount_speed=speed_mm_per_s, csv_output=csv_output, sync_buffer_output=sync_buffer_output, diff --git a/api/src/opentrons/hardware_control/backends/ot3simulator.py b/api/src/opentrons/hardware_control/backends/ot3simulator.py index 8e3a7f8990c..34c8fe0df68 100644 --- a/api/src/opentrons/hardware_control/backends/ot3simulator.py +++ b/api/src/opentrons/hardware_control/backends/ot3simulator.py @@ -345,6 +345,7 @@ async def liquid_probe( mount_speed: float, plunger_speed: float, threshold_pascals: float, + plunger_impulse_time: float, output_format: OutputOptions = OutputOptions.can_bus_only, data_files: Optional[Dict[InstrumentProbeType, str]] = None, probe: InstrumentProbeType = InstrumentProbeType.PRIMARY, diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index 1c03b49fc6c..cdc95bdd7de 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -1932,6 +1932,37 @@ async def _move_to_plunger_bottom( acquire_lock=acquire_lock, ) + async def _move_to_plunger_top_for_liquid_probe( + self, + mount: OT3Mount, + rate: float, + acquire_lock: bool = True, + ) -> None: + """ + Move an instrument's plunger to the top, to prepare for a following + liquid probe action. + + The plunger backlash distance (mm) is used to ensure the plunger is pre-loaded + in the downward direction. This means that the final position will not be + the plunger's configured "top" position, but "top" plus the "backlashDistance". + """ + max_speeds = self.config.motion_settings.default_max_speed + speed = max_speeds[self.gantry_load][OT3AxisKind.P] + instrument = self._pipette_handler.get_pipette(mount) + top_plunger_pos = target_position_from_plunger( + OT3Mount.from_mount(mount), + instrument.plunger_positions.top, + self._current_position, + ) + target_pos = top_plunger_pos.copy() + target_pos[Axis.of_main_tool_actuator(mount)] += instrument.backlash_distance + await self._move(top_plunger_pos, speed=speed * rate, acquire_lock=acquire_lock) + # NOTE: This should ALWAYS be moving DOWN. + # There should never be a time that this function is called and + # the plunger doesn't physically move DOWN. + # This is to make sure we are always engaged at the beginning of liquid-probe. + await self._move(target_pos, speed=speed * rate, acquire_lock=acquire_lock) + async def configure_for_volume( self, mount: Union[top_types.Mount, OT3Mount], volume: float ) -> None: @@ -2568,6 +2599,21 @@ def add_gripper_probe(self, probe: GripperProbe) -> None: def remove_gripper_probe(self) -> None: self._gripper_handler.remove_probe() + @staticmethod + def liquid_probe_non_responsive_z_distance(z_speed: float) -> float: + """Calculate the Z distance travelled where the LLD pass will be unresponsive.""" + # NOTE: (sigler) Here lye some magic numbers. + # The Z axis probing motion uses the first 20 samples to calculate + # a baseline for all following samples, making the very beginning of + # that Z motion unable to detect liquid. The sensor is configured for + # 4ms sample readings, and so we then assume it takes ~80ms to complete. + # If the Z is moving at 5mm/sec, then ~80ms equates to ~0.4 + baseline_during_z_sample_num = 20 # FIXME: (sigler) shouldn't be defined here? + sample_time_sec = 0.004 # FIXME: (sigler) shouldn't be defined here? + baseline_duration_sec = baseline_during_z_sample_num * sample_time_sec + non_responsive_z_mm = baseline_duration_sec * z_speed + return non_responsive_z_mm + async def _liquid_probe_pass( self, mount: OT3Mount, @@ -2583,6 +2629,7 @@ async def _liquid_probe_pass( probe_settings.mount_speed, (probe_settings.plunger_speed * plunger_direction), probe_settings.sensor_threshold_pascals, + probe_settings.plunger_impulse_time, probe_settings.output_option, probe_settings.data_files, probe=probe, @@ -2626,27 +2673,62 @@ async def liquid_probe( probe_start_pos = await self.gantry_position(checked_mount, refresh=True) - p_travel = ( + # plunger travel distance is from TOP->BOTTOM (minus the backlash distance + impulse) + # FIXME: logic for how plunger moves is divided between here and tool_sensors.py + p_impulse_mm = ( + probe_settings.plunger_impulse_time * probe_settings.plunger_speed + ) + p_total_mm = ( instrument.plunger_positions.bottom - instrument.plunger_positions.top ) - max_speeds = self.config.motion_settings.default_max_speed - p_prep_speed = max_speeds[self.gantry_load][OT3AxisKind.P] + + # We need to significatly slow down the 96 channel liquid probe + if self.gantry_load == GantryLoad.HIGH_THROUGHPUT: + max_plunger_speed = self.config.motion_settings.max_speed_discontinuity[ + GantryLoad.HIGH_THROUGHPUT + ][OT3AxisKind.P] + probe_settings.plunger_speed = min( + max_plunger_speed, probe_settings.plunger_speed + ) + + p_working_mm = p_total_mm - (instrument.backlash_distance + p_impulse_mm) + + # height where probe action will begin + # TODO: (sigler) add this to pipette's liquid def (per tip) + probe_pass_overlap_mm = 0.1 + non_responsive_z_mm = OT3API.liquid_probe_non_responsive_z_distance( + probe_settings.mount_speed + ) + probe_pass_z_offset_mm = non_responsive_z_mm + probe_pass_overlap_mm + + # height that is considered safe to reset the plunger without disturbing liquid + # this usually needs to at least 1-2mm from liquid, to avoid splashes from air + # TODO: (sigler) add this to pipette's liquid def (per tip) + probe_safe_reset_mm = max(2.0, probe_pass_z_offset_mm) error: Optional[PipetteLiquidNotFoundError] = None pos = await self.gantry_position(checked_mount, refresh=True) while (probe_start_pos.z - pos.z) < max_z_dist: # safe distance so we don't accidentally aspirate liquid if we're already close to liquid - safe_plunger_pos = pos._replace(z=(pos.z + 2)) + safe_plunger_pos = top_types.Point( + pos.x, pos.y, pos.z + probe_safe_reset_mm + ) # overlap amount we want to use between passes - pass_start_pos = pos._replace(z=(pos.z + 0.5)) - + pass_start_pos = top_types.Point( + pos.x, pos.y, pos.z + probe_pass_z_offset_mm + ) + max_z_time = ( + max_z_dist - (probe_start_pos.z - safe_plunger_pos.z) + ) / probe_settings.mount_speed + p_travel_required_for_z = max_z_time * probe_settings.plunger_speed + p_pass_travel = min(p_travel_required_for_z, p_working_mm) # Prep the plunger await self.move_to(checked_mount, safe_plunger_pos) if probe_settings.aspirate_while_sensing: # TODO(cm, 7/8/24): remove p_prep_speed from the rate at some point - await self._move_to_plunger_bottom(checked_mount, rate=p_prep_speed) + await self._move_to_plunger_bottom(checked_mount, rate=1) else: - await self._move_to_plunger_top(checked_mount, rate=p_prep_speed) + await self._move_to_plunger_top_for_liquid_probe(checked_mount, rate=1) try: # move to where we want to start a pass and run a pass @@ -2655,7 +2737,7 @@ async def liquid_probe( checked_mount, probe_settings, probe if probe else InstrumentProbeType.PRIMARY, - p_travel, + p_pass_travel + p_impulse_mm, ) # if we made it here without an error we found the liquid error = None @@ -2663,26 +2745,14 @@ async def liquid_probe( except PipetteLiquidNotFoundError as lnfe: error = lnfe pos = await self.gantry_position(checked_mount, refresh=True) + await self.move_to(checked_mount, probe_start_pos + top_types.Point(z=2)) + await self.prepare_for_aspirate(checked_mount) await self.move_to(checked_mount, probe_start_pos) if error is not None: # if we never found liquid raise an error raise error return height - async def _move_to_plunger_top( - self, - mount: OT3Mount, - rate: float, - acquire_lock: bool = True, - ) -> None: - instrument = self._pipette_handler.get_pipette(mount) - target_pos = target_position_from_plunger( - OT3Mount.from_mount(mount), - instrument.plunger_positions.top, - self._current_position, - ) - await self._move(target_pos, speed=rate, acquire_lock=acquire_lock) - async def capacitive_probe( self, mount: OT3Mount, diff --git a/api/src/opentrons/protocol_api/__init__.py b/api/src/opentrons/protocol_api/__init__.py index 3f82aa41303..3bf263d6b76 100644 --- a/api/src/opentrons/protocol_api/__init__.py +++ b/api/src/opentrons/protocol_api/__init__.py @@ -31,6 +31,9 @@ from ._types import OFF_DECK from ._nozzle_layout import ( COLUMN, + PARTIAL_COLUMN, + SINGLE, + ROW, ALL, ) from ._parameters import Parameters @@ -63,6 +66,9 @@ "Liquid", "Parameters", "COLUMN", + "PARTIAL_COLUMN", + "SINGLE", + "ROW", "ALL", "OFF_DECK", "RuntimeParameterRequiredError", diff --git a/api/src/opentrons/protocol_api/_nozzle_layout.py b/api/src/opentrons/protocol_api/_nozzle_layout.py index 8e8cdf99521..06da3fc111e 100644 --- a/api/src/opentrons/protocol_api/_nozzle_layout.py +++ b/api/src/opentrons/protocol_api/_nozzle_layout.py @@ -4,6 +4,7 @@ class NozzleLayout(enum.Enum): COLUMN = "COLUMN" + PARTIAL_COLUMN = "PARTIAL_COLUMN" SINGLE = "SINGLE" ROW = "ROW" QUADRANT = "QUADRANT" @@ -11,6 +12,9 @@ class NozzleLayout(enum.Enum): COLUMN: Final = NozzleLayout.COLUMN +PARTIAL_COLUMN: Final = NozzleLayout.PARTIAL_COLUMN +SINGLE: Final = NozzleLayout.SINGLE +ROW: Final = NozzleLayout.ROW ALL: Final = NozzleLayout.ALL # Set __doc__ manually as a workaround. When this docstring is written the normal way, right after diff --git a/api/src/opentrons/protocol_api/core/engine/deck_conflict.py b/api/src/opentrons/protocol_api/core/engine/deck_conflict.py index 2a50964e757..405aa2256a7 100644 --- a/api/src/opentrons/protocol_api/core/engine/deck_conflict.py +++ b/api/src/opentrons/protocol_api/core/engine/deck_conflict.py @@ -16,7 +16,6 @@ from opentrons_shared_data.errors.exceptions import MotionPlanningFailureError from opentrons_shared_data.module import FLEX_TC_LID_COLLISION_ZONE -from opentrons.hardware_control.nozzle_manager import NozzleConfigurationType from opentrons.hardware_control.modules.types import ModuleType from opentrons.motion_planning import deck_conflict as wrapped_deck_conflict from opentrons.motion_planning import adjacent_slots_getters @@ -63,21 +62,6 @@ def __init__(self, message: str) -> None: _log = logging.getLogger(__name__) -# TODO (spp, 2023-12-06): move this to a location like motion planning where we can -# derive these values from geometry definitions -# Also, verify y-axis extents values for the nozzle columns. -# Bounding box measurements -A12_column_front_left_bound = Point(x=-11.03, y=2) -A12_column_back_right_bound = Point(x=526.77, y=506.2) - -_NOZZLE_PITCH = 9 -A1_column_front_left_bound = Point( - x=A12_column_front_left_bound.x - _NOZZLE_PITCH * 11, y=2 -) -A1_column_back_right_bound = Point( - x=A12_column_back_right_bound.x - _NOZZLE_PITCH * 11, y=506.2 -) - _FLEX_TC_LID_BACK_LEFT_PT = Point( x=FLEX_TC_LID_COLLISION_ZONE["back_left"]["x"], y=FLEX_TC_LID_COLLISION_ZONE["back_left"]["y"], @@ -244,8 +228,15 @@ def check_safe_for_pipette_movement( ) primary_nozzle = engine_state.pipettes.get_primary_nozzle(pipette_id) + pipette_bounds_at_well_location = ( + engine_state.pipettes.get_pipette_bounds_at_specified_move_to_position( + pipette_id=pipette_id, destination_position=well_location_point + ) + ) if not _is_within_pipette_extents( - engine_state=engine_state, pipette_id=pipette_id, location=well_location_point + engine_state=engine_state, + pipette_id=pipette_id, + pipette_bounding_box_at_loc=pipette_bounds_at_well_location, ): raise PartialTipMovementNotAllowedError( f"Requested motion with the {primary_nozzle} nozzle partial configuration" @@ -253,11 +244,7 @@ def check_safe_for_pipette_movement( ) labware_slot = engine_state.geometry.get_ancestor_slot_name(labware_id) - pipette_bounds_at_well_location = ( - engine_state.pipettes.get_pipette_bounds_at_specified_move_to_position( - pipette_id=pipette_id, destination_position=well_location_point - ) - ) + surrounding_slots = adjacent_slots_getters.get_surrounding_slots( slot=labware_slot.as_int(), robot_type=engine_state.config.robot_type ) @@ -423,42 +410,30 @@ def check_safe_for_tip_pickup_and_return( ) -# TODO (spp, 2023-02-06): update the extents check to use all nozzle bounds instead of -# just position of primary nozzle when checking if the pipette is out-of-bounds def _is_within_pipette_extents( engine_state: StateView, pipette_id: str, - location: Point, + pipette_bounding_box_at_loc: Tuple[Point, Point, Point, Point], ) -> bool: """Whether a given point is within the extents of a configured pipette on the specified robot.""" - robot_type = engine_state.config.robot_type - pipette_channels = engine_state.pipettes.get_channels(pipette_id) - nozzle_config = engine_state.pipettes.get_nozzle_layout_type(pipette_id) - primary_nozzle = engine_state.pipettes.get_primary_nozzle(pipette_id) - if robot_type == "OT-3 Standard": - if pipette_channels == 96 and nozzle_config == NozzleConfigurationType.COLUMN: - # TODO (spp, 2023-12-18): change this eventually to use column mappings in - # the pipette geometry definitions. - if primary_nozzle == "A12": - return ( - A12_column_front_left_bound.x - <= location.x - <= A12_column_back_right_bound.x - and A12_column_front_left_bound.y - <= location.y - <= A12_column_back_right_bound.y - ) - elif primary_nozzle == "A1": - return ( - A1_column_front_left_bound.x - <= location.x - <= A1_column_back_right_bound.x - and A1_column_front_left_bound.y - <= location.y - <= A1_column_back_right_bound.y - ) - # TODO (spp, 2023-11-07): check for 8-channel nozzle A1 & H1 extents on Flex & OT2 - return True + mount = engine_state.pipettes.get_mount(pipette_id) + robot_extent_per_mount = engine_state.geometry.absolute_deck_extents + pip_back_left_bound, pip_front_right_bound, _, _ = pipette_bounding_box_at_loc + pipette_bounds_offsets = engine_state.pipettes.get_pipette_bounding_box(pipette_id) + from_back_right = ( + robot_extent_per_mount.back_right[mount] + + pipette_bounds_offsets.back_right_corner + ) + from_front_left = ( + robot_extent_per_mount.front_left[mount] + + pipette_bounds_offsets.front_left_corner + ) + return ( + from_back_right.x >= pip_back_left_bound.x >= from_front_left.x + and from_back_right.y >= pip_back_left_bound.y >= from_front_left.y + and from_back_right.x >= pip_front_right_bound.x >= from_front_left.x + and from_back_right.y >= pip_front_right_bound.y >= from_front_left.y + ) def _map_labware( diff --git a/api/src/opentrons/protocol_api/core/engine/instrument.py b/api/src/opentrons/protocol_api/core/engine/instrument.py index 7b6400bc561..d89e946dadc 100644 --- a/api/src/opentrons/protocol_api/core/engine/instrument.py +++ b/api/src/opentrons/protocol_api/core/engine/instrument.py @@ -2,7 +2,6 @@ from __future__ import annotations from typing import Optional, TYPE_CHECKING, cast, Union -from opentrons.protocol_engine.commands.liquid_probe import LiquidProbeResult from opentrons.protocols.api_support.types import APIVersion from opentrons.types import Location, Mount @@ -754,20 +753,13 @@ def get_liquid_presence_detection(self) -> bool: return self._liquid_presence_detection def is_tip_tracking_available(self) -> bool: - primary_nozzle = self._engine_client.state.pipettes.get_primary_nozzle( - self._pipette_id - ) if self.get_nozzle_configuration() == NozzleConfigurationType.FULL: return True else: if self.get_channels() == 96: return True if self.get_channels() == 8: - # TODO: (cb, 03/06/24): Enable automatic tip tracking on the 8 channel pipettes once PAPI support exists - return ( - self.get_nozzle_configuration() == NozzleConfigurationType.SINGLE - and primary_nozzle == "H1" - ) + return True return False def set_flow_rate( @@ -810,6 +802,7 @@ def configure_nozzle_layout( style: NozzleLayout, primary_nozzle: Optional[str], front_right_nozzle: Optional[str], + back_left_nozzle: Optional[str], ) -> None: if style == NozzleLayout.COLUMN: configuration_model: NozzleLayoutConfigurationType = ( @@ -821,11 +814,11 @@ def configure_nozzle_layout( configuration_model = RowNozzleLayoutConfiguration( primaryNozzle=cast(PRIMARY_NOZZLE_LITERAL, primary_nozzle) ) - elif style == NozzleLayout.QUADRANT: - assert front_right_nozzle is not None + elif style == NozzleLayout.QUADRANT or style == NozzleLayout.PARTIAL_COLUMN: configuration_model = QuadrantNozzleLayoutConfiguration( primaryNozzle=cast(PRIMARY_NOZZLE_LITERAL, primary_nozzle), frontRightNozzle=front_right_nozzle, + backLeftNozzle=back_left_nozzle, ) elif style == NozzleLayout.SINGLE: configuration_model = SingleNozzleLayoutConfiguration( @@ -844,13 +837,50 @@ def retract(self) -> None: z_axis = self._engine_client.state.pipettes.get_z_axis(self._pipette_id) self._engine_client.execute_command(cmd.HomeParams(axes=[z_axis])) - def liquid_probe_with_recovery(self, well_core: WellCore) -> None: + def detect_liquid_presence(self, well_core: WellCore, loc: Location) -> bool: labware_id = well_core.labware_id well_name = well_core.get_name() well_location = WellLocation( origin=WellOrigin.TOP, offset=WellOffset(x=0, y=0, z=0) ) + # The error handling here is a bit nuanced and also a bit broken: + # + # - If the hardware detects liquid, the `tryLiquidProbe` engine command will + # succeed and return a height, which we'll convert to a `True` return. + # Okay so far. + # + # - If the hardware detects no liquid, the `tryLiquidProbe` engine command will + # succeed and return `None`, which we'll convert to a `False` return. + # Still okay so far. + # + # - If there is any other error within the `tryLiquidProbe` command, things get + # messy. It may kick the run into recovery mode. At that point, all bets are + # off--we lose our guarantee of having a `tryLiquidProbe` command whose + # `result` we can inspect. We don't know how to deal with that here, so we + # currently propagate the exception up, which will quickly kill the protocol, + # after a potential split second of recovery mode. It's unclear what would + # be good user-facing behavior here, but it's unfortunate to kill the protocol + # for an error that the engine thinks should be recoverable. + result = self._engine_client.execute_command_without_recovery( + cmd.TryLiquidProbeParams( + labwareId=labware_id, + wellName=well_name, + wellLocation=well_location, + pipetteId=self.pipette_id, + ) + ) + + self._protocol_core.set_last_location(location=loc, mount=self.get_mount()) + + return result.z_position is not None + + def liquid_probe_with_recovery(self, well_core: WellCore, loc: Location) -> None: + labware_id = well_core.labware_id + well_name = well_core.get_name() + well_location = WellLocation( + origin=WellOrigin.TOP, offset=WellOffset(x=0, y=0, z=2) + ) self._engine_client.execute_command( cmd.LiquidProbeParams( labwareId=labware_id, @@ -860,13 +890,16 @@ def liquid_probe_with_recovery(self, well_core: WellCore) -> None: ) ) - def liquid_probe_without_recovery(self, well_core: WellCore) -> float: + self._protocol_core.set_last_location(location=loc, mount=self.get_mount()) + + def liquid_probe_without_recovery( + self, well_core: WellCore, loc: Location + ) -> float: labware_id = well_core.labware_id well_name = well_core.get_name() well_location = WellLocation( - origin=WellOrigin.TOP, offset=WellOffset(x=0, y=0, z=0) + origin=WellOrigin.TOP, offset=WellOffset(x=0, y=0, z=2) ) - result = self._engine_client.execute_command_without_recovery( cmd.LiquidProbeParams( labwareId=labware_id, @@ -876,5 +909,6 @@ def liquid_probe_without_recovery(self, well_core: WellCore) -> float: ) ) - if result is not None and isinstance(result, LiquidProbeResult): - return result.z_position + self._protocol_core.set_last_location(location=loc, mount=self.get_mount()) + + return result.z_position diff --git a/api/src/opentrons/protocol_api/core/engine/overlap_versions.py b/api/src/opentrons/protocol_api/core/engine/overlap_versions.py index ed14859ecd3..896ba5bb774 100644 --- a/api/src/opentrons/protocol_api/core/engine/overlap_versions.py +++ b/api/src/opentrons/protocol_api/core/engine/overlap_versions.py @@ -3,7 +3,11 @@ from typing_extensions import Final from opentrons.protocols.api_support.types import APIVersion -_OVERLAP_VERSION_MAP: Final = {APIVersion(2, 0): "v0", APIVersion(2, 19): "v1"} +_OVERLAP_VERSION_MAP: Final = { + APIVersion(2, 0): "v0", + APIVersion(2, 19): "v1", + APIVersion(2, 20): "v3", +} @lru_cache(1) diff --git a/api/src/opentrons/protocol_api/core/instrument.py b/api/src/opentrons/protocol_api/core/instrument.py index 2ad70c7274b..1695f96e5db 100644 --- a/api/src/opentrons/protocol_api/core/instrument.py +++ b/api/src/opentrons/protocol_api/core/instrument.py @@ -284,6 +284,7 @@ def configure_nozzle_layout( style: NozzleLayout, primary_nozzle: Optional[str], front_right_nozzle: Optional[str], + back_left_nozzle: Optional[str], ) -> None: """Configure the pipette to a specific nozzle layout. @@ -291,6 +292,7 @@ def configure_nozzle_layout( style: The type of configuration you wish to build. primary_nozzle: The nozzle that will determine a pipette's critical point. front_right_nozzle: The front right most nozzle in the requested layout. + back_left_nozzle: The back left most nozzle in the requested layout. """ ... @@ -304,12 +306,22 @@ def retract(self) -> None: ... @abstractmethod - def liquid_probe_with_recovery(self, well_core: WellCoreType) -> None: + def detect_liquid_presence( + self, well_core: WellCoreType, loc: types.Location + ) -> bool: + """Do a liquid probe to detect whether there is liquid in the well.""" + + @abstractmethod + def liquid_probe_with_recovery( + self, well_core: WellCoreType, loc: types.Location + ) -> None: """Do a liquid probe to detect the presence of liquid in the well.""" ... @abstractmethod - def liquid_probe_without_recovery(self, well_core: WellCoreType) -> float: + def liquid_probe_without_recovery( + self, well_core: WellCoreType, loc: types.Location + ) -> float: """Do a liquid probe to find the level of the liquid in the well.""" ... 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 6090c62a083..a831a9113f2 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 @@ -545,6 +545,7 @@ def configure_nozzle_layout( style: NozzleLayout, primary_nozzle: Optional[str], front_right_nozzle: Optional[str], + back_left_nozzle: Optional[str], ) -> None: """This will never be called because it was added in API 2.16.""" pass @@ -565,10 +566,18 @@ def retract(self) -> None: """Retract this instrument to the top of the gantry.""" self._protocol_interface.get_hardware.retract(self._mount) # type: ignore [attr-defined] - def liquid_probe_with_recovery(self, well_core: WellCore) -> None: + def detect_liquid_presence(self, well_core: WellCore, loc: types.Location) -> bool: + """This will never be called because it was added in API 2.20.""" + assert False, "detect_liquid_presence only supported in API 2.20 & later" + + def liquid_probe_with_recovery( + self, well_core: WellCore, loc: types.Location + ) -> None: """This will never be called because it was added in API 2.20.""" assert False, "liquid_probe_with_recovery only supported in API 2.20 & later" - def liquid_probe_without_recovery(self, well_core: WellCore) -> float: + def liquid_probe_without_recovery( + self, well_core: WellCore, loc: types.Location + ) -> float: """This will never be called because it was added in API 2.20.""" assert False, "liquid_probe_without_recovery only supported in API 2.20 & later" diff --git a/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py b/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py index 6d02252ceb5..1471af79fe8 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 @@ -463,6 +463,7 @@ def configure_nozzle_layout( style: NozzleLayout, primary_nozzle: Optional[str], front_right_nozzle: Optional[str], + back_left_nozzle: Optional[str], ) -> None: """This will never be called because it was added in API 2.15.""" pass @@ -483,10 +484,18 @@ def retract(self) -> None: """Retract this instrument to the top of the gantry.""" self._protocol_interface.get_hardware.retract(self._mount) # type: ignore [attr-defined] - def liquid_probe_with_recovery(self, well_core: WellCore) -> None: + def detect_liquid_presence(self, well_core: WellCore, loc: types.Location) -> bool: + """This will never be called because it was added in API 2.20.""" + assert False, "detect_liquid_presence only supported in API 2.20 & later" + + def liquid_probe_with_recovery( + self, well_core: WellCore, loc: types.Location + ) -> None: """This will never be called because it was added in API 2.20.""" assert False, "liquid_probe_with_recovery only supported in API 2.20 & later" - def liquid_probe_without_recovery(self, well_core: WellCore) -> float: + def liquid_probe_without_recovery( + self, well_core: WellCore, loc: types.Location + ) -> float: """This will never be called because it was added in API 2.20.""" assert False, "liquid_probe_without_recovery only supported in API 2.20 & later" diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index 7f2f86463ea..05a8ecdc80c 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -2,14 +2,11 @@ import logging from contextlib import ExitStack from typing import Any, List, Optional, Sequence, Union, cast, Dict -from opentrons.protocol_engine.commands.pipetting_common import LiquidNotFoundError -from opentrons.protocol_engine.errors.error_occurrence import ProtocolCommandFailedError from opentrons_shared_data.errors.exceptions import ( CommandPreconditionViolated, CommandParameterLimitViolated, UnexpectedTipRemovalError, ) -from opentrons.protocol_engine.errors.exceptions import WellDoesNotExistError from opentrons.legacy_broker import LegacyBroker from opentrons.hardware_control.dev_types import PipetteDict from opentrons import types @@ -65,6 +62,8 @@ """The version after which automatic tip tracking supported partially configured nozzle layouts.""" _DISPOSAL_LOCATION_OFFSET_ADDED_IN = APIVersion(2, 18) """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.""" class InstrumentContext(publisher.CommandPublisher): @@ -224,7 +223,6 @@ def aspirate( well: Optional[labware.Well] = None move_to_location: types.Location - last_location = self._get_last_location_by_api_version() try: target = validation.validate_location( @@ -262,6 +260,14 @@ def aspirate( c_vol = self._core.get_available_volume() if not volume else volume flow_rate = self._core.get_aspirate_flow_rate(rate) + if ( + self.api_version >= APIVersion(2, 20) + and well is not None + and self.liquid_presence_detection + ): + self.require_liquid_presence(well=well) + self.prepare_to_aspirate() + with publisher.publish_context( broker=self.broker, command=cmds.aspirate( @@ -1678,7 +1684,7 @@ def tip_racks(self, racks: List[labware.Labware]) -> None: @property @requires_version(2, 20) - def liquid_detection(self) -> bool: + def liquid_presence_detection(self) -> bool: """ Gets the global setting for liquid level detection. @@ -1689,9 +1695,9 @@ def liquid_detection(self) -> bool: """ return self._core.get_liquid_presence_detection() - @liquid_detection.setter + @liquid_presence_detection.setter @requires_version(2, 20) - def liquid_detection(self, enable: bool) -> None: + def liquid_presence_detection(self, enable: bool) -> None: self._core.set_liquid_presence_detection(enable) @property @@ -1969,14 +1975,16 @@ def prepare_to_aspirate(self) -> None: self._core.prepare_to_aspirate() @requires_version(2, 16) - def configure_nozzle_layout( + def configure_nozzle_layout( # noqa: C901 self, style: NozzleLayout, start: Optional[str] = None, + end: Optional[str] = None, front_right: Optional[str] = None, + back_left: Optional[str] = None, tip_racks: Optional[List[labware.Labware]] = None, ) -> None: - """Configure how many tips the 96-channel pipette will pick up. + """Configure how many tips the 8-channel or 96-channel pipette will pick up. Changing the nozzle layout will affect gantry movement for all subsequent pipetting actions that the pipette performs. It also alters the pipette's @@ -1990,13 +1998,18 @@ def configure_nozzle_layout( :param style: The shape of the nozzle layout. + - ``SINGLE`` sets the pipette to use 1 nozzle. This corresponds to a single of well on labware. - ``COLUMN`` sets the pipette to use 8 nozzles, aligned from front to back with respect to the deck. This corresponds to a column of wells on labware. + - ``PARTIAL_COLUMN`` sets the pipette to use 2-7 nozzles, aligned from front to back + with respect to the deck. + - ``ROW`` sets the pipette to use 12 nozzles, aligned from left to right + with respect to the deck. This corresponds to a row of wells on labware. - ``ALL`` resets the pipette to use all of its nozzles. Calling ``configure_nozzle_layout`` with no arguments also resets the pipette. :type style: ``NozzleLayout`` or ``None`` - :param start: The nozzle at the back left of the layout, which the robot uses + :param start: The primary nozzle of the layout, which the robot uses to determine how it will move to different locations on the deck. The string should be of the same format used when identifying wells by name. Required unless setting ``style=ALL``. @@ -2006,6 +2019,16 @@ def configure_nozzle_layout( tips *from the same rack*. Doing so can affect positional accuracy. :type start: str or ``None`` + :param end: The nozzle at the end of a linear layout, which is used + to determine how many tips will be picked up by a pipette. The string + should be of the same format used when identifying wells by name. + Required when setting ``style=PARTIAL_COLUMN``. + + .. note:: + Nozzle layouts numbering between 2-7 nozzles, account for the distance from + ``start``. For example, 4 nozzles would require ``start="H1"`` and ``end="E1"``. + + :type end: str or ``None`` :param tip_racks: Behaves the same as setting the ``tip_racks`` parameter of :py:meth:`.load_instrument`. If not specified, the new configuration resets :py:obj:`.InstrumentContext.tip_racks` and you must specify the location @@ -2021,9 +2044,8 @@ def configure_nozzle_layout( # NOTE: Disabled layouts error case can be removed once desired map configurations # have appropriate data regarding tip-type to map current values added to the # pipette definitions. + disabled_layouts = [ - NozzleLayout.ROW, - NozzleLayout.SINGLE, NozzleLayout.QUADRANT, ] if style in disabled_layouts: @@ -2031,6 +2053,15 @@ def configure_nozzle_layout( f"Nozzle layout configuration of style {style.value} is currently unsupported." ) + original_enabled_layouts = [NozzleLayout.COLUMN, NozzleLayout.ALL] + if ( + self._api_version + < _PARTIAL_NOZZLE_CONFIGURATION_SINGLE_ROW_PARTIAL_COLUMN_ADDED_IN + ) and (style not in original_enabled_layouts): + raise ValueError( + f"Nozzle layout configuration of style {style.value} is unsupported in API Versions lower than {_PARTIAL_NOZZLE_CONFIGURATION_SINGLE_ROW_PARTIAL_COLUMN_ADDED_IN}." + ) + if style != NozzleLayout.ALL: if start is None: raise ValueError( @@ -2041,16 +2072,35 @@ def configure_nozzle_layout( f"Starting nozzle specified is not one of {types.ALLOWED_PRIMARY_NOZZLES}" ) if style == NozzleLayout.QUADRANT: - if front_right is None: + if front_right is None and back_left is None: raise ValueError( - "Cannot configure a QUADRANT layout without a front right nozzle." + "Cannot configure a QUADRANT layout without a front right or back left nozzle." ) + elif not (front_right is None and back_left is None): + raise ValueError( + f"Parameters 'front_right' and 'back_left' cannot be used with {style.value} Nozzle Configuration Layout." + ) + + front_right_resolved = front_right + back_left_resolved = back_left + if style == NozzleLayout.PARTIAL_COLUMN: + if end is None: + raise ValueError( + "Parameter 'end' is required for Partial Column Nozzle Configuration Layout." + ) + + # Determine if 'end' will be configured as front_right or back_left + if start == "H1" or start == "H12": + back_left_resolved = end + elif start == "A1" or start == "A12": + front_right_resolved = end + self._core.configure_nozzle_layout( style, primary_nozzle=start, - front_right_nozzle=front_right, + front_right_nozzle=front_right_resolved, + back_left_nozzle=back_left_resolved, ) - # TODO (spp, 2023-12-05): verify that tipracks are on adapters for only full 96 channel config self._tip_racks = tip_racks or [] @requires_version(2, 20) @@ -2059,16 +2109,8 @@ def detect_liquid_presence(self, well: labware.Well) -> bool: :returns: A boolean. """ - if not isinstance(well, labware.Well): - raise WellDoesNotExistError("You must provide a valid well to check.") - try: - self._core.liquid_probe_without_recovery(well._core) - except ProtocolCommandFailedError as e: - if isinstance(e.original_error, LiquidNotFoundError): - return False - raise e - else: - return True + loc = well.top() + return self._core.detect_liquid_presence(well._core, loc) @requires_version(2, 20) def require_liquid_presence(self, well: labware.Well) -> None: @@ -2076,10 +2118,8 @@ def require_liquid_presence(self, well: labware.Well) -> None: :returns: None. """ - if not isinstance(well, labware.Well): - raise WellDoesNotExistError("You must provide a valid well to check.") - - self._core.liquid_probe_with_recovery(well._core) + loc = well.top() + self._core.liquid_probe_with_recovery(well._core, loc) @requires_version(2, 20) def measure_liquid_height(self, well: labware.Well) -> float: @@ -2091,8 +2131,6 @@ def measure_liquid_height(self, well: labware.Well) -> float: This is intended for Opentrons internal use only and is not a guaranteed API. """ - if not isinstance(well, labware.Well): - raise WellDoesNotExistError("You must provide a valid well to check.") - - height = self._core.liquid_probe_without_recovery(well._core) + loc = well.top() + height = self._core.liquid_probe_without_recovery(well._core, loc) return height diff --git a/api/src/opentrons/protocol_api/protocol_context.py b/api/src/opentrons/protocol_api/protocol_context.py index ad96e0c3156..57a04d664a6 100644 --- a/api/src/opentrons/protocol_api/protocol_context.py +++ b/api/src/opentrons/protocol_api/protocol_context.py @@ -246,10 +246,6 @@ def cleanup(self) -> None: self._unsubscribe_commands() self._unsubscribe_commands = None - def __del__(self) -> None: - if getattr(self, "_unsubscribe_commands", None): - self._unsubscribe_commands() # type: ignore - @property @requires_version(2, 0) def max_speeds(self) -> AxisMaxSpeeds: diff --git a/api/src/opentrons/protocol_engine/clients/sync_client.py b/api/src/opentrons/protocol_engine/clients/sync_client.py index f772d81cea8..5750ba72d21 100644 --- a/api/src/opentrons/protocol_engine/clients/sync_client.py +++ b/api/src/opentrons/protocol_engine/clients/sync_client.py @@ -83,6 +83,12 @@ def execute_command_without_recovery( ) -> commands.LiquidProbeResult: pass + @overload + def execute_command_without_recovery( + self, params: commands.TryLiquidProbeParams + ) -> commands.TryLiquidProbeResult: + pass + def execute_command_without_recovery( self, params: commands.CommandParams ) -> commands.CommandResult: diff --git a/api/src/opentrons/protocol_engine/commands/__init__.py b/api/src/opentrons/protocol_engine/commands/__init__.py index 09ad591277e..75904ab00a3 100644 --- a/api/src/opentrons/protocol_engine/commands/__init__.py +++ b/api/src/opentrons/protocol_engine/commands/__init__.py @@ -331,6 +331,11 @@ LiquidProbeCreate, LiquidProbeResult, LiquidProbeCommandType, + TryLiquidProbe, + TryLiquidProbeParams, + TryLiquidProbeCreate, + TryLiquidProbeResult, + TryLiquidProbeCommandType, ) __all__ = [ @@ -580,4 +585,9 @@ "LiquidProbeCreate", "LiquidProbeResult", "LiquidProbeCommandType", + "TryLiquidProbe", + "TryLiquidProbeParams", + "TryLiquidProbeCreate", + "TryLiquidProbeResult", + "TryLiquidProbeCommandType", ] diff --git a/api/src/opentrons/protocol_engine/commands/command_unions.py b/api/src/opentrons/protocol_engine/commands/command_unions.py index 68e59d5e3c5..d20b64f363b 100644 --- a/api/src/opentrons/protocol_engine/commands/command_unions.py +++ b/api/src/opentrons/protocol_engine/commands/command_unions.py @@ -1,6 +1,7 @@ """Union types of concrete command definitions.""" -from typing import Annotated, Iterable, Type, Union, get_type_hints +from collections.abc import Collection +from typing import Annotated, Type, Union, get_type_hints from pydantic import Field @@ -313,6 +314,11 @@ LiquidProbeCreate, LiquidProbeResult, LiquidProbeCommandType, + TryLiquidProbe, + TryLiquidProbeParams, + TryLiquidProbeCreate, + TryLiquidProbeResult, + TryLiquidProbeCommandType, ) Command = Annotated[ @@ -353,6 +359,7 @@ VerifyTipPresence, GetTipPresence, LiquidProbe, + TryLiquidProbe, heater_shaker.WaitForTemperature, heater_shaker.SetTargetTemperature, heater_shaker.DeactivateHeater, @@ -421,6 +428,7 @@ VerifyTipPresenceParams, GetTipPresenceParams, LiquidProbeParams, + TryLiquidProbeParams, heater_shaker.WaitForTemperatureParams, heater_shaker.SetTargetTemperatureParams, heater_shaker.DeactivateHeaterParams, @@ -487,6 +495,7 @@ VerifyTipPresenceCommandType, GetTipPresenceCommandType, LiquidProbeCommandType, + TryLiquidProbeCommandType, heater_shaker.WaitForTemperatureCommandType, heater_shaker.SetTargetTemperatureCommandType, heater_shaker.DeactivateHeaterCommandType, @@ -554,6 +563,7 @@ VerifyTipPresenceCreate, GetTipPresenceCreate, LiquidProbeCreate, + TryLiquidProbeCreate, heater_shaker.WaitForTemperatureCreate, heater_shaker.SetTargetTemperatureCreate, heater_shaker.DeactivateHeaterCreate, @@ -622,6 +632,7 @@ VerifyTipPresenceResult, GetTipPresenceResult, LiquidProbeResult, + TryLiquidProbeResult, heater_shaker.WaitForTemperatureResult, heater_shaker.SetTargetTemperatureResult, heater_shaker.DeactivateHeaterResult, @@ -671,12 +682,20 @@ def _map_create_types_by_params_type( - create_types: Iterable[Type[CommandCreate]], + create_types: Collection[Type[CommandCreate]], ) -> dict[Type[CommandParams], Type[CommandCreate]]: def get_params_type(create_type: Type[CommandCreate]) -> Type[CommandParams]: return get_type_hints(create_type)["params"] # type: ignore[no-any-return] - return {get_params_type(create_type): create_type for create_type in create_types} + result = {get_params_type(create_type): create_type for create_type in create_types} + + # This isn't an inherent requirement of opentrons.protocol_engine, + # but this mapping is only useful to higher-level code if this holds true. + assert len(result) == len( + create_types + ), "Param models should map to create models 1:1." + + return result CREATE_TYPES_BY_PARAMS_TYPE = _map_create_types_by_params_type( diff --git a/api/src/opentrons/protocol_engine/commands/configure_nozzle_layout.py b/api/src/opentrons/protocol_engine/commands/configure_nozzle_layout.py index ace59d49fde..74681098ab9 100644 --- a/api/src/opentrons/protocol_engine/commands/configure_nozzle_layout.py +++ b/api/src/opentrons/protocol_engine/commands/configure_nozzle_layout.py @@ -71,11 +71,13 @@ async def execute( """Check that requested pipette can support the requested nozzle layout.""" primary_nozzle = params.configurationParams.dict().get("primaryNozzle") front_right_nozzle = params.configurationParams.dict().get("frontRightNozzle") + back_left_nozzle = params.configurationParams.dict().get("backLeftNozzle") nozzle_params = await self._tip_handler.available_for_nozzle_layout( pipette_id=params.pipetteId, style=params.configurationParams.style, primary_nozzle=primary_nozzle, front_right_nozzle=front_right_nozzle, + back_left_nozzle=back_left_nozzle, ) nozzle_map = await self._equipment.configure_nozzle_layout( diff --git a/api/src/opentrons/protocol_engine/commands/liquid_probe.py b/api/src/opentrons/protocol_engine/commands/liquid_probe.py index 46606415792..ecf932a3470 100644 --- a/api/src/opentrons/protocol_engine/commands/liquid_probe.py +++ b/api/src/opentrons/protocol_engine/commands/liquid_probe.py @@ -1,4 +1,5 @@ -"""Liquid-probe command for OT3 hardware. request, result, and implementation models.""" +"""The liquidProbe and tryLiquidProbe commands.""" + from __future__ import annotations from typing import TYPE_CHECKING, Optional, Type, Union from opentrons.protocol_engine.errors.exceptions import MustHomeError, TipNotEmptyError @@ -10,7 +11,7 @@ from pydantic import Field -from ..types import CurrentWell, DeckPoint +from ..types import DeckPoint from .pipetting_common import ( LiquidNotFoundError, LiquidNotFoundErrorInternalData, @@ -34,16 +35,30 @@ LiquidProbeCommandType = Literal["liquidProbe"] +TryLiquidProbeCommandType = Literal["tryLiquidProbe"] + + +# Both command variants should have identical parameters. +# But we need two separate parameter model classes because +# `command_unions.CREATE_TYPES_BY_PARAMS_TYPE` needs to be a 1:1 mapping. +class _CommonParams(PipetteIdMixin, WellLocationMixin): + pass + +class LiquidProbeParams(_CommonParams): + """Parameters required for a `liquidProbe` command.""" -class LiquidProbeParams(PipetteIdMixin, WellLocationMixin): - """Parameters required to liquid probe a specific well.""" + pass + + +class TryLiquidProbeParams(_CommonParams): + """Parameters required for a `tryLiquidProbe` command.""" pass class LiquidProbeResult(DestinationPositionResult): - """Result data from the execution of a liquid-probe command.""" + """Result data from the execution of a `liquidProbe` command.""" z_position: float = Field( ..., description="The Z coordinate, in mm, of the found liquid in deck space." @@ -51,13 +66,28 @@ class LiquidProbeResult(DestinationPositionResult): # New fields should use camelCase. z_position is snake_case for historical reasons. -_ExecuteReturn = Union[ +class TryLiquidProbeResult(DestinationPositionResult): + """Result data from the execution of a `tryLiquidProbe` command.""" + + z_position: Optional[float] = Field( + ..., + description=( + "The Z coordinate, in mm, of the found liquid in deck space." + " If no liquid was found, `null` or omitted." + ), + ) + + +_LiquidProbeExecuteReturn = Union[ SuccessData[LiquidProbeResult, None], DefinedErrorData[LiquidNotFoundError, LiquidNotFoundErrorInternalData], ] +_TryLiquidProbeExecuteReturn = SuccessData[TryLiquidProbeResult, None] -class LiquidProbeImplementation(AbstractCommandImpl[LiquidProbeParams, _ExecuteReturn]): +class LiquidProbeImplementation( + AbstractCommandImpl[LiquidProbeParams, _LiquidProbeExecuteReturn] +): """The implementation of a `liquidProbe` command.""" def __init__( @@ -71,15 +101,19 @@ def __init__( self._pipetting = pipetting self._model_utils = model_utils - async def execute(self, params: LiquidProbeParams) -> _ExecuteReturn: + async def execute(self, params: _CommonParams) -> _LiquidProbeExecuteReturn: """Move to and liquid probe the requested well. Return the z-position of the found liquid. + If no liquid is found, return a LiquidNotFoundError as a defined error. Raises: - TipNotAttachedError: if there is not tip attached to the pipette - MustHomeError: if the plunger is not in a valid position - LiquidNotFoundError: if liquid is not found during the probe process. + TipNotAttachedError: as an undefined error, if there is not tip attached to + the pipette. + TipNotEmptyError: as an undefined error, if the tip starts with liquid + in it. + MustHomeError: as an undefined error, if the plunger is not in a valid + position. """ pipette_id = params.pipetteId labware_id = params.labwareId @@ -99,24 +133,20 @@ async def execute(self, params: LiquidProbeParams) -> _ExecuteReturn: message="Current position of pipette is invalid. Please home." ) - current_well = CurrentWell( - pipette_id=pipette_id, - labware_id=labware_id, - well_name=well_name, - ) - # liquid_probe process start position position = await self._movement.move_to_well( pipette_id=pipette_id, labware_id=labware_id, well_name=well_name, well_location=params.wellLocation, - current_well=current_well, ) try: z_pos = await self._pipetting.liquid_probe_in_place( - pipette_id=pipette_id, labware_id=labware_id, well_name=well_name + pipette_id=pipette_id, + labware_id=labware_id, + well_name=well_name, + well_location=params.wellLocation, ) except PipetteLiquidNotFoundError as e: return DefinedErrorData( @@ -145,8 +175,68 @@ async def execute(self, params: LiquidProbeParams) -> _ExecuteReturn: ) -class LiquidProbe(BaseCommand[LiquidProbeParams, LiquidProbeResult, ErrorOccurrence]): - """LiquidProbe command model.""" +class TryLiquidProbeImplementation( + AbstractCommandImpl[TryLiquidProbeParams, _TryLiquidProbeExecuteReturn] +): + """The implementation of a `tryLiquidProbe` command.""" + + def __init__( + self, + movement: MovementHandler, + pipetting: PipettingHandler, + model_utils: ModelUtils, + **kwargs: object, + ) -> None: + self._movement = movement + self._pipetting = pipetting + self._model_utils = model_utils + + async def execute(self, params: _CommonParams) -> _TryLiquidProbeExecuteReturn: + """Execute a `tryLiquidProbe` command. + + `tryLiquidProbe` is identical to `liquidProbe`, except that if no liquid is + found, `tryLiquidProbe` returns a success result with `z_position=null` instead + of a defined error. + """ + # We defer to the `liquidProbe` implementation. If it returns a defined + # `liquidNotFound` error, we remap that to a success result. + # Otherwise, we return the result or propagate the exception unchanged. + + original_impl = LiquidProbeImplementation( + movement=self._movement, + pipetting=self._pipetting, + model_utils=self._model_utils, + ) + original_result = await original_impl.execute(params) + + match original_result: + case DefinedErrorData( + public=LiquidNotFoundError(), + private=LiquidNotFoundErrorInternalData() as original_private, + ): + return SuccessData( + public=TryLiquidProbeResult( + z_position=None, + position=original_private.position, + ), + private=None, + ) + case SuccessData( + public=LiquidProbeResult() as original_public, private=None + ): + return SuccessData( + public=TryLiquidProbeResult( + position=original_public.position, + z_position=original_public.z_position, + ), + private=None, + ) + + +class LiquidProbe( + BaseCommand[LiquidProbeParams, LiquidProbeResult, LiquidNotFoundError] +): + """The model for a full `liquidProbe` command.""" commandType: LiquidProbeCommandType = "liquidProbe" params: LiquidProbeParams @@ -155,10 +245,33 @@ class LiquidProbe(BaseCommand[LiquidProbeParams, LiquidProbeResult, ErrorOccurre _ImplementationCls: Type[LiquidProbeImplementation] = LiquidProbeImplementation +class TryLiquidProbe( + BaseCommand[TryLiquidProbeParams, TryLiquidProbeResult, ErrorOccurrence] +): + """The model for a full `tryLiquidProbe` command.""" + + commandType: TryLiquidProbeCommandType = "tryLiquidProbe" + params: TryLiquidProbeParams + result: Optional[TryLiquidProbeResult] + + _ImplementationCls: Type[ + TryLiquidProbeImplementation + ] = TryLiquidProbeImplementation + + class LiquidProbeCreate(BaseCommandCreate[LiquidProbeParams]): - """Create LiquidProbe command request model.""" + """The request model for a `liquidProbe` command.""" commandType: LiquidProbeCommandType = "liquidProbe" params: LiquidProbeParams _CommandCls: Type[LiquidProbe] = LiquidProbe + + +class TryLiquidProbeCreate(BaseCommandCreate[TryLiquidProbeParams]): + """The request model for a `tryLiquidProbe` command.""" + + commandType: TryLiquidProbeCommandType = "tryLiquidProbe" + params: TryLiquidProbeParams + + _CommandCls: Type[TryLiquidProbe] = TryLiquidProbe diff --git a/api/src/opentrons/protocol_engine/create_protocol_engine.py b/api/src/opentrons/protocol_engine/create_protocol_engine.py index fd7b1b8bd5f..8a6a4355fd7 100644 --- a/api/src/opentrons/protocol_engine/create_protocol_engine.py +++ b/api/src/opentrons/protocol_engine/create_protocol_engine.py @@ -7,6 +7,7 @@ from opentrons.hardware_control.types import DoorState from opentrons.protocol_engine.error_recovery_policy import ErrorRecoveryPolicy from opentrons.util.async_helpers import async_context_manager_in_thread +from opentrons_shared_data.robot import load as load_robot from .protocol_engine import ProtocolEngine from .resources import DeckDataProvider, ModuleDataProvider @@ -45,11 +46,12 @@ async def create_protocol_engine( else [] ) module_calibration_offsets = ModuleDataProvider.load_module_calibrations() - + robot_definition = load_robot(config.robot_type) state_store = StateStore( config=config, deck_definition=deck_definition, deck_fixed_labware=deck_fixed_labware, + robot_definition=robot_definition, is_door_open=hardware_api.door_state is DoorState.OPEN, module_calibration_offsets=module_calibration_offsets, deck_configuration=deck_configuration, diff --git a/api/src/opentrons/protocol_engine/execution/pipetting.py b/api/src/opentrons/protocol_engine/execution/pipetting.py index ccf515e1226..c3e606849ff 100644 --- a/api/src/opentrons/protocol_engine/execution/pipetting.py +++ b/api/src/opentrons/protocol_engine/execution/pipetting.py @@ -13,7 +13,7 @@ InvalidPushOutVolumeError, InvalidDispenseVolumeError, ) - +from opentrons.protocol_engine.types import WellLocation # 1e-9 µL (1 femtoliter!) is a good value because: # * It's large relative to rounding errors that occur in practice in protocols. For @@ -30,7 +30,7 @@ class PipettingHandler(TypingProtocol): """Liquid handling commands.""" def get_is_empty(self, pipette_id: str) -> bool: - """Get whether a pipette has a working volume equal to 0.""" + """Get whether a pipette has an aspirated volume equal to 0.""" def get_is_ready_to_aspirate(self, pipette_id: str) -> bool: """Get whether a pipette is ready to aspirate.""" @@ -68,6 +68,7 @@ async def liquid_probe_in_place( pipette_id: str, labware_id: str, well_name: str, + well_location: WellLocation, ) -> float: """Detect liquid level.""" @@ -81,8 +82,8 @@ def __init__(self, state_view: StateView, hardware_api: HardwareControlAPI) -> N self._hardware_api = hardware_api def get_is_empty(self, pipette_id: str) -> bool: - """Get whether a pipette has a working volume equal to 0.""" - return self._state_view.pipettes.get_working_volume(pipette_id) == 0 + """Get whether a pipette has an aspirated volume equal to 0.""" + return self._state_view.pipettes.get_aspirated_volume(pipette_id) == 0 def get_is_ready_to_aspirate(self, pipette_id: str) -> bool: """Get whether a pipette is ready to aspirate.""" @@ -176,6 +177,7 @@ async def liquid_probe_in_place( pipette_id: str, labware_id: str, well_name: str, + well_location: WellLocation, ) -> float: """Detect liquid level.""" hw_pipette = self._state_view.pipettes.get_hardware_pipette( @@ -188,7 +190,8 @@ async def liquid_probe_in_place( pipette_id=pipette_id ) z_pos = await self._hardware_api.liquid_probe( - mount=hw_pipette.mount, max_z_dist=well_depth - lld_min_height + mount=hw_pipette.mount, + max_z_dist=well_depth - lld_min_height + well_location.offset.z, ) return float(z_pos) @@ -234,8 +237,8 @@ def __init__( self._state_view = state_view def get_is_empty(self, pipette_id: str) -> bool: - """Get whether a pipette has a working volume equal to 0.""" - return self._state_view.pipettes.get_working_volume(pipette_id) == 0 + """Get whether a pipette has an aspirated volume equal to 0.""" + return self._state_view.pipettes.get_aspirated_volume(pipette_id) == 0 def get_is_ready_to_aspirate(self, pipette_id: str) -> bool: """Get whether a pipette is ready to aspirate.""" @@ -290,6 +293,7 @@ async def liquid_probe_in_place( pipette_id: str, labware_id: str, well_name: str, + well_location: WellLocation, ) -> float: """Detect liquid level.""" # TODO (pm, 6-18-24): return a value of worth if needed diff --git a/api/src/opentrons/protocol_engine/execution/tip_handler.py b/api/src/opentrons/protocol_engine/execution/tip_handler.py index 6638d216095..7acfae1e3ef 100644 --- a/api/src/opentrons/protocol_engine/execution/tip_handler.py +++ b/api/src/opentrons/protocol_engine/execution/tip_handler.py @@ -28,6 +28,13 @@ "H12": {"COLUMN": "A12", "ROW": "H1"}, } +PRIMARY_NOZZLE_TO_BACK_LEFT_NOZZLE_MAP = { + "A1": {"COLUMN": "A1", "ROW": "A1"}, + "H1": {"COLUMN": "A1", "ROW": "H1"}, + "A12": {"COLUMN": "A12", "ROW": "A1"}, + "H12": {"COLUMN": "A12", "ROW": "H1"}, +} + class TipHandler(TypingProtocol): """Pick up and drop tips.""" @@ -38,6 +45,7 @@ async def available_for_nozzle_layout( style: str, primary_nozzle: Optional[str] = None, front_right_nozzle: Optional[str] = None, + back_left_nozzle: Optional[str] = None, ) -> Dict[str, str]: """Check nozzle layout is compatible with the pipette. @@ -82,11 +90,12 @@ async def verify_tip_presence( """Verify the expected tip presence status.""" -async def _available_for_nozzle_layout( +async def _available_for_nozzle_layout( # noqa: C901 channels: int, style: str, primary_nozzle: Optional[str], front_right_nozzle: Optional[str], + back_left_nozzle: Optional[str], ) -> Dict[str, str]: """Check nozzle layout is compatible with the pipette. @@ -106,20 +115,60 @@ async def _available_for_nozzle_layout( limit_statement="RowNozzleLayout is incompatible with {channels} channel pipettes.", actual_value=str(primary_nozzle), ) + if style == "PARTIAL_COLUM" and channels == 96: + raise CommandParameterLimitViolated( + command_name="configure_nozzle_layout", + parameter_name="PartialColumnNozzleLayout", + limit_statement="PartialColumnNozzleLayout is incompatible with {channels} channel pipettes.", + actual_value=str(primary_nozzle), + ) if not primary_nozzle: return {"primary_nozzle": "A1"} if style == "SINGLE": return {"primary_nozzle": primary_nozzle} - if not front_right_nozzle: + if style == "QUADRANT" and front_right_nozzle and not back_left_nozzle: + return { + "primary_nozzle": primary_nozzle, + "front_right_nozzle": front_right_nozzle, + "back_left_nozzle": primary_nozzle, + } + if style == "QUADRANT" and back_left_nozzle and not front_right_nozzle: + return { + "primary_nozzle": primary_nozzle, + "front_right_nozzle": primary_nozzle, + "back_left_nozzle": back_left_nozzle, + } + if not front_right_nozzle and back_left_nozzle: return { "primary_nozzle": primary_nozzle, "front_right_nozzle": PRIMARY_NOZZLE_TO_ENDING_NOZZLE_MAP[primary_nozzle][ style ], + "back_left_nozzle": back_left_nozzle, } + if front_right_nozzle and not back_left_nozzle: + return { + "primary_nozzle": primary_nozzle, + "front_right_nozzle": front_right_nozzle, + "back_left_nozzle": PRIMARY_NOZZLE_TO_BACK_LEFT_NOZZLE_MAP[primary_nozzle][ + style + ], + } + if front_right_nozzle and back_left_nozzle: + return { + "primary_nozzle": primary_nozzle, + "front_right_nozzle": front_right_nozzle, + "back_left_nozzle": back_left_nozzle, + } + return { "primary_nozzle": primary_nozzle, - "front_right_nozzle": front_right_nozzle, + "front_right_nozzle": PRIMARY_NOZZLE_TO_ENDING_NOZZLE_MAP[primary_nozzle][ + style + ], + "back_left_nozzle": PRIMARY_NOZZLE_TO_BACK_LEFT_NOZZLE_MAP[primary_nozzle][ + style + ], } @@ -142,6 +191,7 @@ async def available_for_nozzle_layout( style: str, primary_nozzle: Optional[str] = None, front_right_nozzle: Optional[str] = None, + back_left_nozzle: Optional[str] = None, ) -> Dict[str, str]: """Returns configuration for nozzle layout to pass to configure_nozzle_layout.""" if self._state_view.pipettes.get_attached_tip(pipette_id): @@ -150,7 +200,7 @@ async def available_for_nozzle_layout( ) channels = self._state_view.pipettes.get_channels(pipette_id) return await _available_for_nozzle_layout( - channels, style, primary_nozzle, front_right_nozzle + channels, style, primary_nozzle, front_right_nozzle, back_left_nozzle ) async def pick_up_tip( @@ -307,6 +357,7 @@ async def available_for_nozzle_layout( style: str, primary_nozzle: Optional[str] = None, front_right_nozzle: Optional[str] = None, + back_left_nozzle: Optional[str] = None, ) -> Dict[str, str]: """Returns configuration for nozzle layout to pass to configure_nozzle_layout.""" if self._state_view.pipettes.get_attached_tip(pipette_id): @@ -315,7 +366,7 @@ async def available_for_nozzle_layout( ) channels = self._state_view.pipettes.get_channels(pipette_id) return await _available_for_nozzle_layout( - channels, style, primary_nozzle, front_right_nozzle + channels, style, primary_nozzle, front_right_nozzle, back_left_nozzle ) async def drop_tip( diff --git a/api/src/opentrons/protocol_engine/state/addressable_areas.py b/api/src/opentrons/protocol_engine/state/addressable_areas.py index 85c61bfa917..7e3a0325ed4 100644 --- a/api/src/opentrons/protocol_engine/state/addressable_areas.py +++ b/api/src/opentrons/protocol_engine/state/addressable_areas.py @@ -1,8 +1,9 @@ """Basic addressable area data state and store.""" from dataclasses import dataclass +from functools import cached_property from typing import Dict, List, Optional, Set, Union -from opentrons_shared_data.robot.dev_types import RobotType +from opentrons_shared_data.robot.dev_types import RobotType, RobotDefinition from opentrons_shared_data.deck.dev_types import ( DeckDefinitionV5, SlotDefV3, @@ -77,6 +78,9 @@ class AddressableAreaState: use_simulated_deck_config: bool """See `Config.use_simulated_deck_config`.""" + """Information about the current robot model.""" + robot_definition: RobotDefinition + _OT2_ORDERED_SLOTS = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"] _FLEX_ORDERED_SLOTS = [ @@ -164,6 +168,7 @@ def __init__( deck_configuration: DeckConfigurationType, config: Config, deck_definition: DeckDefinitionV5, + robot_definition: RobotDefinition, ) -> None: """Initialize an addressable area store and its state.""" if config.use_simulated_deck_config: @@ -183,6 +188,7 @@ def __init__( deck_definition=deck_definition, robot_type=config.robot_type, use_simulated_deck_config=config.use_simulated_deck_config, + robot_definition=robot_definition, ) def handle_action(self, action: Action) -> None: @@ -330,6 +336,22 @@ def __init__(self, state: AddressableAreaState) -> None: """ self._state = state + @cached_property + def deck_extents(self) -> Point: + """The maximum space on the deck.""" + extents = self._state.robot_definition["extents"] + return Point(x=extents[0], y=extents[1], z=extents[2]) + + @cached_property + def mount_offsets(self) -> Dict[str, Point]: + """The left and right mount offsets of the robot.""" + left_offset = self.state.robot_definition["mountOffsets"]["left"] + right_offset = self.state.robot_definition["mountOffsets"]["right"] + return { + "left": Point(x=left_offset[0], y=left_offset[1], z=left_offset[2]), + "right": Point(x=right_offset[0], y=right_offset[1], z=right_offset[2]), + } + def get_addressable_area(self, addressable_area_name: str) -> AddressableArea: """Get addressable area.""" if not self._state.use_simulated_deck_config: diff --git a/api/src/opentrons/protocol_engine/state/commands.py b/api/src/opentrons/protocol_engine/state/commands.py index 209cb5872a1..a558210cbff 100644 --- a/api/src/opentrons/protocol_engine/state/commands.py +++ b/api/src/opentrons/protocol_engine/state/commands.py @@ -232,221 +232,229 @@ def __init__( stopped_by_estop=False, ) - def handle_action(self, action: Action) -> None: # noqa: C901 + def handle_action(self, action: Action) -> None: """Modify state in reaction to an action.""" - if isinstance(action, QueueCommandAction): - # TODO(mc, 2021-06-22): mypy has trouble with this automatic - # request > command mapping, figure out how to type precisely - # (or wait for a future mypy version that can figure it out). - queued_command = action.request._CommandCls.construct( - id=action.command_id, - key=( - action.request.key - if action.request.key is not None - else (action.request_hash or action.command_id) - ), - createdAt=action.created_at, - params=action.request.params, # type: ignore[arg-type] - intent=action.request.intent, - status=CommandStatus.QUEUED, - failedCommandId=action.failed_command_id, - ) + match action: + case QueueCommandAction(): + self._handle_queue_command_action(action) + case RunCommandAction(): + self._handle_run_command_action(action) + case SucceedCommandAction(): + self._handle_succeed_command_action(action) + case FailCommandAction(): + self._handle_fail_command_action(action) + case PlayAction(): + self._handle_play_action(action) + case PauseAction(): + self._handle_pause_action(action) + case ResumeFromRecoveryAction(): + self._handle_resume_from_recovery_action(action) + case StopAction(): + self._handle_stop_action(action) + case FinishAction(): + self._handle_finish_action(action) + case HardwareStoppedAction(): + self._handle_hardware_stopped_action(action) + case DoorChangeAction(): + self._handle_door_change_action(action) + case _: + pass + + def _handle_queue_command_action(self, action: QueueCommandAction) -> None: + # TODO(mc, 2021-06-22): mypy has trouble with this automatic + # request > command mapping, figure out how to type precisely + # (or wait for a future mypy version that can figure it out). + queued_command = action.request._CommandCls.construct( + id=action.command_id, + key=( + action.request.key + if action.request.key is not None + else (action.request_hash or action.command_id) + ), + createdAt=action.created_at, + params=action.request.params, # type: ignore[arg-type] + intent=action.request.intent, + status=CommandStatus.QUEUED, + failedCommandId=action.failed_command_id, + ) - self._state.command_history.append_queued_command(queued_command) + self._state.command_history.append_queued_command(queued_command) - if action.request_hash is not None: - self._state.latest_protocol_command_hash = action.request_hash + if action.request_hash is not None: + self._state.latest_protocol_command_hash = action.request_hash - elif isinstance(action, RunCommandAction): - prev_entry = self._state.command_history.get(action.command_id) + def _handle_run_command_action(self, action: RunCommandAction) -> None: + prev_entry = self._state.command_history.get(action.command_id) - running_command = prev_entry.command.copy( - update={ - "status": CommandStatus.RUNNING, - "startedAt": action.started_at, - } - ) + running_command = prev_entry.command.copy( + update={ + "status": CommandStatus.RUNNING, + "startedAt": action.started_at, + } + ) + + self._state.command_history.set_command_running(running_command) - self._state.command_history.set_command_running(running_command) + def _handle_succeed_command_action(self, action: SucceedCommandAction) -> None: + succeeded_command = action.command + self._state.command_history.set_command_succeeded(succeeded_command) - elif isinstance(action, SucceedCommandAction): - succeeded_command = action.command - self._state.command_history.set_command_succeeded(succeeded_command) + def _handle_fail_command_action(self, action: FailCommandAction) -> None: + prev_entry = self.state.command_history.get(action.command_id) - elif isinstance(action, FailCommandAction): - if isinstance(action.error, EnumeratedError): - public_error_occurrence = ErrorOccurrence.from_failed( - id=action.error_id, - createdAt=action.failed_at, - error=action.error, + if isinstance(action.error, EnumeratedError): + public_error_occurrence = ErrorOccurrence.from_failed( + id=action.error_id, + createdAt=action.failed_at, + error=action.error, + ) + else: + public_error_occurrence = action.error.public + + self._update_to_failed( + command_id=action.command_id, + failed_at=action.failed_at, + error_occurrence=public_error_occurrence, + error_recovery_type=action.type, + notes=action.notes, + ) + self._state.failed_command = self._state.command_history.get(action.command_id) + + other_command_ids_to_fail: List[str] + if prev_entry.command.intent == CommandIntent.SETUP: + other_command_ids_to_fail = list( + self._state.command_history.get_setup_queue_ids() + ) + elif prev_entry.command.intent == CommandIntent.FIXIT: + other_command_ids_to_fail = list( + self._state.command_history.get_fixit_queue_ids() + ) + elif ( + prev_entry.command.intent == CommandIntent.PROTOCOL + or prev_entry.command.intent is None + ): + if action.type == ErrorRecoveryType.FAIL_RUN: + other_command_ids_to_fail = list( + self._state.command_history.get_queue_ids() ) + elif action.type == ErrorRecoveryType.WAIT_FOR_RECOVERY: + other_command_ids_to_fail = [] else: - public_error_occurrence = action.error.public - - prev_entry = self.state.command_history.get(action.command_id) + assert_never(action.type) + else: + assert_never(prev_entry.command.intent) + for command_id in other_command_ids_to_fail: + # TODO(mc, 2022-06-06): add new "cancelled" status or similar self._update_to_failed( - command_id=action.command_id, + command_id=command_id, failed_at=action.failed_at, - error_occurrence=public_error_occurrence, - error_recovery_type=action.type, - notes=action.notes, + error_occurrence=None, + error_recovery_type=None, + notes=None, ) - self._state.failed_command = self._state.command_history.get( - action.command_id - ) + if ( + prev_entry.command.intent in (CommandIntent.PROTOCOL, None) + and action.type == ErrorRecoveryType.WAIT_FOR_RECOVERY + ): + self._state.queue_status = QueueStatus.AWAITING_RECOVERY + self._state.recovery_target_command_id = action.command_id - if prev_entry.command.intent == CommandIntent.SETUP: - other_command_ids_to_fail = list( - # Copy to avoid it mutating as we remove elements below. - self._state.command_history.get_setup_queue_ids() - ) - for command_id in other_command_ids_to_fail: - # TODO(mc, 2022-06-06): add new "cancelled" status or similar - self._update_to_failed( - command_id=command_id, - failed_at=action.failed_at, - error_occurrence=None, - error_recovery_type=None, - notes=None, + def _handle_play_action(self, action: PlayAction) -> None: + if not self._state.run_result: + self._state.run_started_at = ( + self._state.run_started_at or action.requested_at + ) + match self._state.queue_status: + case QueueStatus.SETUP: + self._state.queue_status = ( + QueueStatus.PAUSED + if self._state.is_door_blocking + else QueueStatus.RUNNING ) - elif ( - prev_entry.command.intent == CommandIntent.PROTOCOL - or prev_entry.command.intent is None - ): - if action.type == ErrorRecoveryType.WAIT_FOR_RECOVERY: + case QueueStatus.AWAITING_RECOVERY_PAUSED: self._state.queue_status = QueueStatus.AWAITING_RECOVERY - self._state.recovery_target_command_id = action.command_id - elif action.type == ErrorRecoveryType.FAIL_RUN: - other_command_ids_to_fail = list( - # Copy to avoid it mutating as we remove elements below. - self._state.command_history.get_queue_ids() - ) - for command_id in other_command_ids_to_fail: - # TODO(mc, 2022-06-06): add new "cancelled" status or similar - self._update_to_failed( - command_id=command_id, - failed_at=action.failed_at, - error_occurrence=None, - error_recovery_type=None, - notes=None, - ) - else: - assert_never(action.type) - elif prev_entry.command.intent == CommandIntent.FIXIT: - other_command_ids_to_fail = list( - # Copy to avoid it mutating as we remove elements below. - self._state.command_history.get_fixit_queue_ids() - ) - for command_id in other_command_ids_to_fail: - # TODO(mc, 2022-06-06): add new "cancelled" status or similar - self._update_to_failed( - command_id=command_id, - failed_at=action.failed_at, - error_occurrence=None, - error_recovery_type=None, - notes=None, - ) - else: - assert_never(prev_entry.command.intent) + case QueueStatus.PAUSED: + self._state.queue_status = QueueStatus.RUNNING + case QueueStatus.RUNNING | QueueStatus.AWAITING_RECOVERY: + # Nothing for the play action to do. No-op. + pass - elif isinstance(action, PlayAction): - if not self._state.run_result: - self._state.run_started_at = ( - self._state.run_started_at or action.requested_at - ) - match self._state.queue_status: - case QueueStatus.SETUP: - self._state.queue_status = ( - QueueStatus.PAUSED - if self._state.is_door_blocking - else QueueStatus.RUNNING - ) - case QueueStatus.AWAITING_RECOVERY_PAUSED: - self._state.queue_status = QueueStatus.AWAITING_RECOVERY - case QueueStatus.PAUSED: - self._state.queue_status = QueueStatus.RUNNING - case QueueStatus.RUNNING | QueueStatus.AWAITING_RECOVERY: - # Nothing for the play action to do. No-op. - pass + def _handle_pause_action(self, action: PauseAction) -> None: + self._state.queue_status = QueueStatus.PAUSED - elif isinstance(action, PauseAction): - self._state.queue_status = QueueStatus.PAUSED + def _handle_resume_from_recovery_action( + self, action: ResumeFromRecoveryAction + ) -> None: + self._state.queue_status = QueueStatus.RUNNING + self._state.recovery_target_command_id = None - elif isinstance(action, ResumeFromRecoveryAction): - self._state.queue_status = QueueStatus.RUNNING + def _handle_stop_action(self, action: StopAction) -> None: + if not self._state.run_result: self._state.recovery_target_command_id = None - elif isinstance(action, StopAction): - if not self._state.run_result: - self._state.recovery_target_command_id = None - - self._state.queue_status = QueueStatus.PAUSED - if action.from_estop: - self._state.stopped_by_estop = True - self._state.run_result = RunResult.FAILED - else: - self._state.run_result = RunResult.STOPPED - - elif isinstance(action, FinishAction): - if not self._state.run_result: - self._state.queue_status = QueueStatus.PAUSED - if action.set_run_status: - self._state.run_result = ( - RunResult.SUCCEEDED - if not action.error_details - else RunResult.FAILED - ) - else: - self._state.run_result = RunResult.STOPPED - - if not self._state.run_error and action.error_details: - self._state.run_error = self._map_run_exception_to_error_occurrence( - action.error_details.error_id, - action.error_details.created_at, - action.error_details.error, - ) + self._state.queue_status = QueueStatus.PAUSED + if action.from_estop: + self._state.stopped_by_estop = True + self._state.run_result = RunResult.FAILED else: - # HACK(sf): There needs to be a better way to set - # an estop error than this else clause - if self._state.stopped_by_estop and action.error_details: - self._state.run_error = self._map_run_exception_to_error_occurrence( - action.error_details.error_id, - action.error_details.created_at, - action.error_details.error, - ) + self._state.run_result = RunResult.STOPPED - elif isinstance(action, HardwareStoppedAction): + def _handle_finish_action(self, action: FinishAction) -> None: + if not self._state.run_result: self._state.queue_status = QueueStatus.PAUSED - self._state.run_result = self._state.run_result or RunResult.STOPPED - self._state.run_completed_at = ( - self._state.run_completed_at or action.completed_at - ) + if action.set_run_status: + self._state.run_result = ( + RunResult.SUCCEEDED + if not action.error_details + else RunResult.FAILED + ) + else: + self._state.run_result = RunResult.STOPPED - if action.finish_error_details: - self._state.finish_error = ( - self._map_finish_exception_to_error_occurrence( - action.finish_error_details.error_id, - action.finish_error_details.created_at, - action.finish_error_details.error, - ) + if not self._state.run_error and action.error_details: + self._state.run_error = self._map_run_exception_to_error_occurrence( + action.error_details.error_id, + action.error_details.created_at, + action.error_details.error, + ) + else: + # HACK(sf): There needs to be a better way to set + # an estop error than this else clause + if self._state.stopped_by_estop and action.error_details: + self._state.run_error = self._map_run_exception_to_error_occurrence( + action.error_details.error_id, + action.error_details.created_at, + action.error_details.error, ) - elif isinstance(action, DoorChangeAction): - if self._config.block_on_door_open: - if action.door_state == DoorState.OPEN: - self._state.is_door_blocking = True - match self._state.queue_status: - case QueueStatus.SETUP: - pass - case QueueStatus.RUNNING | QueueStatus.PAUSED: - self._state.queue_status = QueueStatus.PAUSED - case QueueStatus.AWAITING_RECOVERY | QueueStatus.AWAITING_RECOVERY_PAUSED: - self._state.queue_status = ( - QueueStatus.AWAITING_RECOVERY_PAUSED - ) - elif action.door_state == DoorState.CLOSED: - self._state.is_door_blocking = False + def _handle_hardware_stopped_action(self, action: HardwareStoppedAction) -> None: + self._state.queue_status = QueueStatus.PAUSED + self._state.run_result = self._state.run_result or RunResult.STOPPED + self._state.run_completed_at = ( + self._state.run_completed_at or action.completed_at + ) + + if action.finish_error_details: + self._state.finish_error = self._map_finish_exception_to_error_occurrence( + action.finish_error_details.error_id, + action.finish_error_details.created_at, + action.finish_error_details.error, + ) + + def _handle_door_change_action(self, action: DoorChangeAction) -> None: + if self._config.block_on_door_open: + if action.door_state == DoorState.OPEN: + self._state.is_door_blocking = True + match self._state.queue_status: + case QueueStatus.SETUP: + pass + case QueueStatus.RUNNING | QueueStatus.PAUSED: + self._state.queue_status = QueueStatus.PAUSED + case QueueStatus.AWAITING_RECOVERY | QueueStatus.AWAITING_RECOVERY_PAUSED: + self._state.queue_status = QueueStatus.AWAITING_RECOVERY_PAUSED + elif action.door_state == DoorState.CLOSED: + self._state.is_door_blocking = False def _update_to_failed( self, diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index 112d7d60ef4..904e0c470b2 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -3,6 +3,8 @@ from numpy import array, dot, double as npdouble from numpy.typing import NDArray from typing import Optional, List, Tuple, Union, cast, TypeVar, Dict +from dataclasses import dataclass +from functools import cached_property from opentrons.types import Point, DeckSlotName, StagingSlotName, MountType @@ -71,6 +73,12 @@ class _GripperMoveType(enum.Enum): DROP_LABWARE = enum.auto() +@dataclass +class _AbsoluteRobotExtents: + front_left: Dict[MountType, Point] + back_right: Dict[MountType, Point] + + _LabwareLocation = TypeVar("_LabwareLocation", bound=LabwareLocation) @@ -95,6 +103,24 @@ def __init__( self._addressable_areas = addressable_area_view self._last_drop_tip_location_spot: Dict[str, _TipDropSection] = {} + @cached_property + def absolute_deck_extents(self) -> _AbsoluteRobotExtents: + """The absolute deck extents for a given robot deck.""" + left_offset = self._addressable_areas.mount_offsets["left"] + right_offset = self._addressable_areas.mount_offsets["right"] + + front_left_abs = { + MountType.LEFT: Point(left_offset.x, -1 * left_offset.y, left_offset.z), + MountType.RIGHT: Point(right_offset.x, -1 * right_offset.y, right_offset.z), + } + back_right_abs = { + MountType.LEFT: self._addressable_areas.deck_extents + left_offset, + MountType.RIGHT: self._addressable_areas.deck_extents + right_offset, + } + return _AbsoluteRobotExtents( + front_left=front_left_abs, back_right=back_right_abs + ) + def get_labware_highest_z(self, labware_id: str) -> float: """Get the highest Z-point of a labware.""" labware_data = self._labware.get(labware_id) diff --git a/api/src/opentrons/protocol_engine/state/pipettes.py b/api/src/opentrons/protocol_engine/state/pipettes.py index 748786d9bda..92344dd9600 100644 --- a/api/src/opentrons/protocol_engine/state/pipettes.py +++ b/api/src/opentrons/protocol_engine/state/pipettes.py @@ -97,6 +97,8 @@ class PipetteBoundingBoxOffsets: back_left_corner: Point front_right_corner: Point + back_right_corner: Point + front_left_corner: Point @dataclass(frozen=True) @@ -194,6 +196,16 @@ def _handle_command( # noqa: C901 pipette_bounding_box_offsets=PipetteBoundingBoxOffsets( back_left_corner=config.back_left_corner_offset, front_right_corner=config.front_right_corner_offset, + back_right_corner=Point( + config.front_right_corner_offset.x, + config.back_left_corner_offset.y, + config.back_left_corner_offset.z, + ), + front_left_corner=Point( + config.back_left_corner_offset.x, + config.front_right_corner_offset.y, + config.back_left_corner_offset.z, + ), ), bounding_nozzle_offsets=BoundingNozzlesOffsets( back_left_offset=config.nozzle_map.back_left_nozzle_offset, @@ -637,7 +649,7 @@ def get_current_tip_lld_settings(self, pipette_id: str) -> float: if attached_tip is None or attached_tip.volume is None: return 0 lld_settings = self.get_pipette_lld_settings(pipette_id) - tipVolume = str(attached_tip.volume) + tipVolume = "t" + str(int(attached_tip.volume)) if ( lld_settings is None or lld_settings[tipVolume] is None @@ -788,6 +800,10 @@ def get_pipette_bounding_nozzle_offsets( """Get the nozzle offsets of the pipette's bounding nozzles.""" return self.get_config(pipette_id).bounding_nozzle_offsets + def get_pipette_bounding_box(self, pipette_id: str) -> PipetteBoundingBoxOffsets: + """Get the bounding box of the pipette.""" + return self.get_config(pipette_id).pipette_bounding_box_offsets + def get_pipette_bounds_at_specified_move_to_position( self, pipette_id: str, @@ -796,6 +812,7 @@ def get_pipette_bounds_at_specified_move_to_position( """Get the pipette's bounding offsets when primary nozzle is at the given position.""" primary_nozzle_offset = self.get_primary_nozzle_offset(pipette_id) tip = self.get_attached_tip(pipette_id) + # TODO update this for pipette robot stackup # Primary nozzle position at destination, in deck coordinates primary_nozzle_position = destination_position + Point( x=0, y=0, z=tip.length if tip else 0 diff --git a/api/src/opentrons/protocol_engine/state/state.py b/api/src/opentrons/protocol_engine/state/state.py index aa54383b379..e343a4dfde1 100644 --- a/api/src/opentrons/protocol_engine/state/state.py +++ b/api/src/opentrons/protocol_engine/state/state.py @@ -6,6 +6,7 @@ from typing_extensions import ParamSpec from opentrons_shared_data.deck.dev_types import DeckDefinitionV5 +from opentrons_shared_data.robot.dev_types import RobotDefinition from opentrons.protocol_engine.types import ModuleOffsetData from opentrons.util.change_notifier import ChangeNotifier @@ -144,6 +145,7 @@ def __init__( config: Config, deck_definition: DeckDefinitionV5, deck_fixed_labware: Sequence[DeckFixedLabware], + robot_definition: RobotDefinition, is_door_open: bool, change_notifier: Optional[ChangeNotifier] = None, module_calibration_offsets: Optional[Dict[str, ModuleOffsetData]] = None, @@ -162,6 +164,7 @@ def __init__( change_notifier: Internal state change notifier. module_calibration_offsets: Module offsets to preload. deck_configuration: The initial deck configuration the addressable area store will be instantiated with. + robot_definition: Static information about the robot type being used. notify_publishers: Notifies robot server publishers of internal state change. """ self._command_store = CommandStore(config=config, is_door_open=is_door_open) @@ -172,6 +175,7 @@ def __init__( deck_configuration=deck_configuration, config=config, deck_definition=deck_definition, + robot_definition=robot_definition, ) self._labware_store = LabwareStore( deck_fixed_labware=deck_fixed_labware, diff --git a/api/src/opentrons/protocol_engine/state/tips.py b/api/src/opentrons/protocol_engine/state/tips.py index 5af1e19a31f..85d437888fb 100644 --- a/api/src/opentrons/protocol_engine/state/tips.py +++ b/api/src/opentrons/protocol_engine/state/tips.py @@ -317,8 +317,8 @@ def _cluster_search_A1(active_columns: int, active_rows: int) -> Optional[str]: return result elif isinstance(result, int) and result == -1: return None - if critical_row + active_rows < len(columns[0]): - critical_row = critical_row + active_rows + if critical_row + 1 < len(columns[0]): + critical_row = critical_row + 1 else: critical_column += 1 critical_row = active_rows - 1 @@ -341,8 +341,8 @@ def _cluster_search_A12(active_columns: int, active_rows: int) -> Optional[str]: return result elif isinstance(result, int) and result == -1: return None - if critical_row + active_rows < len(columns[0]): - critical_row = critical_row + active_rows + if critical_row + 1 < len(columns[0]): + critical_row = critical_row + 1 else: critical_column -= 1 critical_row = active_rows - 1 @@ -365,8 +365,8 @@ def _cluster_search_H1(active_columns: int, active_rows: int) -> Optional[str]: return result elif isinstance(result, int) and result == -1: return None - if critical_row - active_rows >= 0: - critical_row = critical_row - active_rows + if critical_row - 1 >= 0: + critical_row = critical_row - 1 else: critical_column += 1 if critical_column >= len(columns): @@ -391,8 +391,8 @@ def _cluster_search_H12(active_columns: int, active_rows: int) -> Optional[str]: return result elif isinstance(result, int) and result == -1: return None - if critical_row - active_rows >= 0: - critical_row = critical_row - active_rows + if critical_row - 1 >= 0: + critical_row = critical_row - 1 else: critical_column -= 1 if critical_column < 0: diff --git a/api/src/opentrons/protocol_engine/types.py b/api/src/opentrons/protocol_engine/types.py index e596d2314fe..17a18a8ae4f 100644 --- a/api/src/opentrons/protocol_engine/types.py +++ b/api/src/opentrons/protocol_engine/types.py @@ -876,11 +876,16 @@ class QuadrantNozzleLayoutConfiguration(BaseModel): ..., description="The primary nozzle to use in the layout configuration. This nozzle will update the critical point of the current pipette. For now, this is also the back left corner of your rectangle.", ) - frontRightNozzle: str = Field( + frontRightNozzle: Optional[str] = Field( ..., regex=NOZZLE_NAME_REGEX, description="The front right nozzle in your configuration.", ) + backLeftNozzle: Optional[str] = Field( + ..., + regex=NOZZLE_NAME_REGEX, + description="The back left nozzle in your configuration.", + ) NozzleLayoutConfigurationType = Union[ diff --git a/api/src/opentrons/protocols/advanced_control/transfers.py b/api/src/opentrons/protocols/advanced_control/transfers.py index df1c6961be6..41b69306805 100644 --- a/api/src/opentrons/protocols/advanced_control/transfers.py +++ b/api/src/opentrons/protocols/advanced_control/transfers.py @@ -16,12 +16,16 @@ from opentrons.protocol_api.labware import Labware, Well from opentrons import types from opentrons.protocols.api_support.types import APIVersion +from opentrons.hardware_control.nozzle_manager import NozzleConfigurationType if TYPE_CHECKING: from opentrons.protocol_api import InstrumentContext from opentrons.protocols.execution.dev_types import Dictable +_PARTIAL_TIP_SUPPORT_ADDED = APIVersion(2, 18) +"""The version after which partial tip support and nozzle maps were made available.""" + class MixStrategy(enum.Enum): BOTH = enum.auto() @@ -409,7 +413,15 @@ def __init__( # then avoid iterating through its Wells. # ii. if using single channel pipettes, flatten a multi-dimensional # list of Wells into a 1 dimensional list of Wells - if self._instr.channels > 1: + pipette_configuration_type = NozzleConfigurationType.FULL + if self._api_version >= _PARTIAL_TIP_SUPPORT_ADDED: + pipette_configuration_type = ( + self._instr._core.get_nozzle_map().configuration + ) + if ( + self._instr.channels > 1 + and pipette_configuration_type == NozzleConfigurationType.FULL + ): sources, dests = self._multichannel_transfer(sources, dests) else: if isinstance(sources, List) and isinstance(sources[0], List): diff --git a/api/src/opentrons/protocols/parameters/csv_parameter_definition.py b/api/src/opentrons/protocols/parameters/csv_parameter_definition.py index 6806e03b2d7..35e0d4f2345 100644 --- a/api/src/opentrons/protocols/parameters/csv_parameter_definition.py +++ b/api/src/opentrons/protocols/parameters/csv_parameter_definition.py @@ -8,7 +8,6 @@ ) from . import validation -from .exceptions import ParameterDefinitionError from .parameter_definition import AbstractParameterDefinition from .types import CSVParameter @@ -44,10 +43,6 @@ def value(self) -> Optional[TextIO]: @value.setter def value(self, new_file: TextIO) -> None: - if not new_file.name.endswith(".csv"): - raise ParameterDefinitionError( - f"CSV parameter {self._variable_name} was given non csv file {new_file.name}" - ) self._value = new_file @property diff --git a/api/src/opentrons/protocols/parameters/types.py b/api/src/opentrons/protocols/parameters/types.py index f61a35457ad..46b47a04282 100644 --- a/api/src/opentrons/protocols/parameters/types.py +++ b/api/src/opentrons/protocols/parameters/types.py @@ -1,32 +1,62 @@ import csv -from typing import TypeVar, Union, TypedDict, TextIO, Optional, List +from typing import TypeVar, Union, TypedDict, TextIO, Optional, List, Any -from .exceptions import RuntimeParameterRequired +from .exceptions import RuntimeParameterRequired, ParameterValueError class CSVParameter: def __init__(self, csv_file: Optional[TextIO]) -> None: self._file = csv_file - self._rows = [] - if self._file is not None: - for row in csv.reader(self._file): - self._rows.append(row) - self._file.seek(0) + self._contents: Optional[str] = None @property def file(self) -> TextIO: + """Returns the file handler for the CSV file.""" if self._file is None: raise RuntimeParameterRequired( "CSV parameter needs to be set to a file for full analysis or run." ) return self._file - def rows(self) -> List[List[str]]: - if self._file is None: - raise RuntimeParameterRequired( - "CSV parameter needs to be set to a file for full analysis or run." - ) - return self._rows + @property + def contents(self) -> str: + """Returns the full contents of the CSV file as a single string.""" + if self._contents is None: + self.file.seek(0) + self._contents = self.file.read() + return self._contents + + def parse_as_csv( + self, detect_dialect: bool = True, **kwargs: Any + ) -> List[List[str]]: + """Returns a list of rows with each row represented as a list of column elements. + + If there is a header for the CSV that will be the first row in the list (i.e. `.rows()[0]`). + All elements will be represented as strings, even if they are numeric in nature. + """ + rows: List[List[str]] = [] + if detect_dialect: + try: + self.file.seek(0) + dialect = csv.Sniffer().sniff(self.file.read(1024)) + self.file.seek(0) + reader = csv.reader(self.file, dialect, **kwargs) + except (UnicodeDecodeError, csv.Error): + raise ParameterValueError( + "Cannot parse dialect or contents from provided CSV file." + ) + else: + try: + reader = csv.reader(self.file, **kwargs) + except (UnicodeDecodeError, csv.Error): + raise ParameterValueError("Cannot parse provided CSV file.") + try: + for row in reader: + rows.append(row) + except (UnicodeDecodeError, csv.Error): + raise ParameterValueError("Cannot parse provided CSV file.") + self.file.seek(0) + return rows PrimitiveAllowedTypes = Union[str, int, float, bool] diff --git a/api/src/opentrons/system/log_control.py b/api/src/opentrons/system/log_control.py index bd44af3c7c2..0b992b31658 100644 --- a/api/src/opentrons/system/log_control.py +++ b/api/src/opentrons/system/log_control.py @@ -15,7 +15,11 @@ MAX_RECORDS = 100000 DEFAULT_RECORDS = 50000 -UNIT_SELECTORS = ["opentrons-robot-server", "opentrons-robot-app"] +UNIT_SELECTORS = [ + "opentrons-robot-server", + "opentrons-update-server", + "opentrons-robot-app", +] SERIAL_SPECIAL = "ALL_SERIAL" SERIAL_SELECTORS = [ "opentrons-api-serial", diff --git a/api/tests/opentrons/hardware_control/backends/test_ot3_controller.py b/api/tests/opentrons/hardware_control/backends/test_ot3_controller.py index ae5385ff1f9..fa57c4347ff 100644 --- a/api/tests/opentrons/hardware_control/backends/test_ot3_controller.py +++ b/api/tests/opentrons/hardware_control/backends/test_ot3_controller.py @@ -719,6 +719,7 @@ async def test_liquid_probe( mount_speed=fake_liquid_settings.mount_speed, plunger_speed=fake_liquid_settings.plunger_speed, threshold_pascals=fake_liquid_settings.sensor_threshold_pascals, + plunger_impulse_time=fake_liquid_settings.plunger_impulse_time, output_option=fake_liquid_settings.output_option, ) except PipetteLiquidNotFoundError: diff --git a/api/tests/opentrons/hardware_control/test_ot3_api.py b/api/tests/opentrons/hardware_control/test_ot3_api.py index fe9f4085436..0c1fff849c0 100644 --- a/api/tests/opentrons/hardware_control/test_ot3_api.py +++ b/api/tests/opentrons/hardware_control/test_ot3_api.py @@ -116,8 +116,8 @@ def fake_settings() -> CapacitivePassSettings: @pytest.fixture def fake_liquid_settings() -> LiquidProbeSettings: return LiquidProbeSettings( - mount_speed=40, - plunger_speed=10, + mount_speed=5, + plunger_speed=20, plunger_impulse_time=0.2, sensor_threshold_pascals=15, output_option=OutputOptions.can_bus_only, @@ -806,9 +806,6 @@ async def test_liquid_probe( ) await ot3_hardware.cache_pipette(mount, instr_data, None) pipette = ot3_hardware.hardware_pipettes[mount.to_mount()] - plunger_positions = ot3_hardware._pipette_handler.get_pipette( - mount - ).plunger_positions assert pipette await ot3_hardware.add_tip(mount, 100) @@ -828,8 +825,8 @@ async def test_liquid_probe( # make sure aspirate while sensing reverses direction mock_liquid_probe.return_value = return_dict fake_settings_aspirate = LiquidProbeSettings( - mount_speed=40, - plunger_speed=10, + mount_speed=5, + plunger_speed=20, plunger_impulse_time=0.2, sensor_threshold_pascals=15, output_option=OutputOptions.can_bus_only, @@ -838,13 +835,14 @@ async def test_liquid_probe( ) fake_max_z_dist = 10.0 await ot3_hardware.liquid_probe(mount, fake_max_z_dist, fake_settings_aspirate) - mock_move_to_plunger_bottom.assert_called_once() + mock_move_to_plunger_bottom.call_count == 2 mock_liquid_probe.assert_called_once_with( mount, - plunger_positions.bottom - plunger_positions.top, + 52, fake_settings_aspirate.mount_speed, (fake_settings_aspirate.plunger_speed * -1), fake_settings_aspirate.sensor_threshold_pascals, + fake_settings_aspirate.plunger_impulse_time, fake_settings_aspirate.output_option, fake_settings_aspirate.data_files, probe=InstrumentProbeType.PRIMARY, @@ -913,13 +911,14 @@ async def test_multi_liquid_probe( await ot3_hardware.liquid_probe( OT3Mount.LEFT, fake_max_z_dist, fake_settings_aspirate ) - assert mock_move_to_plunger_bottom.call_count == 3 + assert mock_move_to_plunger_bottom.call_count == 4 mock_liquid_probe.assert_called_with( OT3Mount.LEFT, - plunger_positions.bottom - plunger_positions.top, + plunger_positions.bottom - plunger_positions.top - 0.1, fake_settings_aspirate.mount_speed, (fake_settings_aspirate.plunger_speed * -1), fake_settings_aspirate.sensor_threshold_pascals, + fake_settings_aspirate.plunger_impulse_time, fake_settings_aspirate.output_option, fake_settings_aspirate.data_files, probe=InstrumentProbeType.PRIMARY, @@ -957,6 +956,7 @@ async def _fake_pos_update_and_raise( mount_speed: float, plunger_speed: float, threshold_pascals: float, + plunger_impulse_time: float, output_format: OutputOptions = OutputOptions.can_bus_only, data_files: Optional[Dict[InstrumentProbeType, str]] = None, probe: InstrumentProbeType = InstrumentProbeType.PRIMARY, @@ -989,8 +989,8 @@ async def _fake_pos_update_and_raise( await ot3_hardware.liquid_probe( OT3Mount.LEFT, fake_max_z_dist, fake_settings_aspirate ) - # assert that it went through 3 passes - assert mock_move_to_plunger_bottom.call_count == 3 + # assert that it went through 4 passes and then prepared to aspirate + assert mock_move_to_plunger_bottom.call_count == 5 @pytest.mark.parametrize( diff --git a/api/tests/opentrons/protocol_api/core/engine/test_deck_conflict.py b/api/tests/opentrons/protocol_api/core/engine/test_deck_conflict.py index 82ce80695d3..c50ffe4687e 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_deck_conflict.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_deck_conflict.py @@ -25,9 +25,12 @@ ModuleModel, StateView, ) +from opentrons.protocol_engine.state.geometry import _AbsoluteRobotExtents +from opentrons.protocol_engine.state.pipettes import PipetteBoundingBoxOffsets + from opentrons.protocol_engine.clients import SyncClient from opentrons.protocol_engine.errors import LabwareNotLoadedOnModuleError -from opentrons.types import DeckSlotName, Point, StagingSlotName +from opentrons.types import DeckSlotName, Point, StagingSlotName, MountType from opentrons.protocol_engine.types import ( DeckType, @@ -416,7 +419,7 @@ def test_maps_trash_bins( [("OT-3 Standard", DeckType.OT3_STANDARD)], ) @pytest.mark.parametrize( - ["pipette_bounds", "expected_raise"], + ["pipette_bounds", "expected_raise", "y_value"], [ ( # nozzles above highest Z ( @@ -426,6 +429,7 @@ def test_maps_trash_bins( Point(x=50, y=50, z=60), ), does_not_raise(), + 0, ), # X, Y, Z collisions ( @@ -439,6 +443,7 @@ def test_maps_trash_bins( deck_conflict.PartialTipMovementNotAllowedError, match="collision with items in deck slot D1", ), + 0, ), ( ( @@ -451,6 +456,7 @@ def test_maps_trash_bins( deck_conflict.PartialTipMovementNotAllowedError, match="collision with items in deck slot D2", ), + 0, ), ( # Collision with staging slot ( @@ -461,8 +467,9 @@ def test_maps_trash_bins( ), pytest.raises( deck_conflict.PartialTipMovementNotAllowedError, - match="collision with items in staging slot C4", + match="will result in collision with items in staging slot C4.", ), + 170, ), ], ) @@ -471,6 +478,7 @@ def test_deck_conflict_raises_for_bad_pipette_move( mock_state_view: StateView, pipette_bounds: Tuple[Point, Point, Point, Point], expected_raise: ContextManager[Any], + y_value: float, ) -> None: """It should raise errors when moving to locations with restrictions for partial pipette movement. @@ -485,7 +493,36 @@ def test_deck_conflict_raises_for_bad_pipette_move( in order to preserve readability of the test. That means the test does actual slot overlap checks. """ - destination_well_point = Point(x=123, y=123, z=123) + destination_well_point = Point(x=123, y=y_value, z=123) + decoy.when( + mock_state_view.pipettes.get_is_partially_configured("pipette-id") + ).then_return(True) + decoy.when(mock_state_view.pipettes.get_mount("pipette-id")).then_return( + MountType.LEFT + ) + decoy.when(mock_state_view.geometry.absolute_deck_extents).then_return( + _AbsoluteRobotExtents( + front_left={ + MountType.LEFT: Point(13.5, -60.5, 0.0), + MountType.RIGHT: Point(-40.5, -60.5, 0.0), + }, + back_right={ + MountType.LEFT: Point(463.7, 433.3, 0.0), + MountType.RIGHT: Point(517.7, 433.3), + }, + ) + ) + decoy.when( + mock_state_view.pipettes.get_pipette_bounding_box("pipette-id") + ).then_return( + # 96 chan outer bounds + PipetteBoundingBoxOffsets( + back_left_corner=Point(-36.0, -25.5, -259.15), + front_right_corner=Point(63.0, -88.5, -259.15), + front_left_corner=Point(-36.0, -88.5, -259.15), + back_right_corner=Point(63.0, -25.5, -259.15), + ) + ) decoy.when( mock_state_view.pipettes.get_is_partially_configured("pipette-id") ).then_return(True) @@ -589,7 +626,7 @@ def test_deck_conflict_raises_for_collision_with_tc_lid( destination_well_point = Point(x=123, y=123, z=123) pipette_bounds_at_destination = ( Point(x=50, y=350, z=204.5), - Point(x=150, y=450, z=204.5), + Point(x=150, y=429, z=204.5), Point(x=150, y=400, z=204.5), Point(x=50, y=300, z=204.5), ) @@ -616,6 +653,32 @@ def test_deck_conflict_raises_for_collision_with_tc_lid( pipette_id="pipette-id", destination_position=destination_well_point ) ).then_return(pipette_bounds_at_destination) + decoy.when(mock_state_view.pipettes.get_mount("pipette-id")).then_return( + MountType.LEFT + ) + decoy.when( + mock_state_view.pipettes.get_pipette_bounding_box("pipette-id") + ).then_return( + # 96 chan outer bounds + PipetteBoundingBoxOffsets( + back_left_corner=Point(-67.0, -3.5, -259.15), + front_right_corner=Point(94.0, -113.0, -259.15), + front_left_corner=Point(-67.0, -113.0, -259.15), + back_right_corner=Point(94.0, -3.5, -259.15), + ) + ) + decoy.when(mock_state_view.geometry.absolute_deck_extents).then_return( + _AbsoluteRobotExtents( + front_left={ + MountType.LEFT: Point(13.5, 60.5, 0.0), + MountType.RIGHT: Point(-40.5, 60.5, 0.0), + }, + back_right={ + MountType.LEFT: Point(463.7, 433.3, 0.0), + MountType.RIGHT: Point(517.7, 433.3), + }, + ) + ) decoy.when( adjacent_slots_getters.get_surrounding_slots(5, robot_type="OT-3 Standard") diff --git a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py index 3ca12bc004f..c3adca3f5a8 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py @@ -1,10 +1,10 @@ """Test for the ProtocolEngine-based instrument API core.""" from typing import cast, Optional, Union -from opentrons.protocol_engine.commands.liquid_probe import LiquidProbeResult from opentrons_shared_data.errors.exceptions import PipetteLiquidNotFoundError import pytest from decoy import Decoy +from decoy import errors from opentrons_shared_data.pipette.dev_types import PipetteNameType @@ -1166,24 +1166,33 @@ def test_liquid_presence_detection( @pytest.mark.parametrize( - argnames=["style", "primary_nozzle", "front_right_nozzle", "expected_model"], + argnames=[ + "style", + "primary_nozzle", + "front_right_nozzle", + "back_left_nozzle", + "expected_model", + ], argvalues=[ [ NozzleLayout.COLUMN, "A1", "H1", + None, ColumnNozzleLayoutConfiguration(primaryNozzle="A1"), ], [ NozzleLayout.SINGLE, "H12", None, + None, SingleNozzleLayoutConfiguration(primaryNozzle="H12"), ], [ NozzleLayout.ROW, "A12", None, + None, RowNozzleLayoutConfiguration(primaryNozzle="A12"), ], ], @@ -1195,10 +1204,13 @@ def test_configure_nozzle_layout( style: NozzleLayout, primary_nozzle: Optional[str], front_right_nozzle: Optional[str], + back_left_nozzle: Optional[str], expected_model: NozzleLayoutConfigurationType, ) -> None: """The correct model is passed to the engine client.""" - subject.configure_nozzle_layout(style, primary_nozzle, front_right_nozzle) + subject.configure_nozzle_layout( + style, primary_nozzle, front_right_nozzle, back_left_nozzle + ) decoy.verify( mock_engine_client.execute_command( @@ -1222,7 +1234,7 @@ def test_configure_nozzle_layout( (8, NozzleConfigurationType.FULL, "A1", True), (8, NozzleConfigurationType.FULL, None, True), (8, NozzleConfigurationType.SINGLE, "H1", True), - (8, NozzleConfigurationType.SINGLE, "A1", False), + (8, NozzleConfigurationType.SINGLE, "A1", True), (1, NozzleConfigurationType.FULL, None, True), ], ) @@ -1281,32 +1293,51 @@ def test_configure_for_volume_post_219( """Configure_for_volume should specify overlap version.""" decoy.when(mock_protocol_core.api_version).then_return(version) subject.configure_for_volume(123.0) - decoy.verify( - mock_engine_client.execute_command( - cmd.ConfigureForVolumeParams( - pipetteId=subject.pipette_id, - volume=123.0, - tipOverlapNotAfterVersion="v1", + try: + decoy.verify( + mock_engine_client.execute_command( + cmd.ConfigureForVolumeParams( + pipetteId=subject.pipette_id, + volume=123.0, + tipOverlapNotAfterVersion="v1", + ) + ) + ) + except errors.VerifyError: + decoy.verify( + mock_engine_client.execute_command( + cmd.ConfigureForVolumeParams( + pipetteId=subject.pipette_id, + volume=123.0, + tipOverlapNotAfterVersion="v3", + ) ) ) - ) -@pytest.mark.parametrize("version", versions_at_or_above(APIVersion(2, 20))) -def test_liquid_probe_without_recovery( +@pytest.mark.parametrize( + ("returned_from_engine", "expected_return_from_core"), + [ + (None, False), + (0, True), + (1, True), + ], +) +def test_detect_liquid_presence( + returned_from_engine: Optional[float], + expected_return_from_core: bool, decoy: Decoy, - mock_engine_client: EngineClient, mock_protocol_core: ProtocolCore, + mock_engine_client: EngineClient, subject: InstrumentCore, - version: APIVersion, ) -> None: - """It should raise an exception on an empty well and return a float on a valid well.""" + """It should convert a height result from the engine to True/False.""" well_core = WellCore( name="my cool well", labware_id="123abc", engine_client=mock_engine_client ) decoy.when( mock_engine_client.execute_command_without_recovery( - cmd.LiquidProbeParams( + cmd.TryLiquidProbeParams( pipetteId=subject.pipette_id, wellLocation=WellLocation( origin=WellOrigin.TOP, offset=WellOffset(x=0, y=0, z=0) @@ -1315,51 +1346,63 @@ def test_liquid_probe_without_recovery( labwareId=well_core.labware_id, ) ) - ).then_raise(PipetteLiquidNotFoundError()) - try: - subject.liquid_probe_without_recovery(well_core=well_core) - except PipetteLiquidNotFoundError: - assert True - else: - assert False + ).then_return( + cmd.TryLiquidProbeResult.construct( + z_position=returned_from_engine, + position=object(), # type: ignore[arg-type] + ) + ) + loc = Location(Point(0, 0, 0), None) + + result = subject.detect_liquid_presence(well_core=well_core, loc=loc) + assert result == expected_return_from_core - decoy.reset() + decoy.verify(mock_protocol_core.set_last_location(loc, mount=subject.get_mount())) - lpr = LiquidProbeResult(z_position=5.0) + +def test_liquid_probe_without_recovery( + decoy: Decoy, + mock_engine_client: EngineClient, + subject: InstrumentCore, +) -> None: + """It should raise an exception on an empty well and return a float on a valid well.""" + well_core = WellCore( + name="my cool well", labware_id="123abc", engine_client=mock_engine_client + ) decoy.when( mock_engine_client.execute_command_without_recovery( cmd.LiquidProbeParams( pipetteId=subject.pipette_id, wellLocation=WellLocation( - origin=WellOrigin.TOP, offset=WellOffset(x=0, y=0, z=0) + origin=WellOrigin.TOP, offset=WellOffset(x=0, y=0, z=2) ), wellName=well_core.get_name(), labwareId=well_core.labware_id, ) ) - ).then_return(lpr) - assert subject.liquid_probe_without_recovery(well_core=well_core) == 5.0 + ).then_raise(PipetteLiquidNotFoundError()) + loc = Location(Point(0, 0, 0), None) + with pytest.raises(PipetteLiquidNotFoundError): + subject.liquid_probe_without_recovery(well_core=well_core, loc=loc) -@pytest.mark.parametrize("version", versions_at_or_above(APIVersion(2, 20))) def test_liquid_probe_with_recovery( decoy: Decoy, mock_engine_client: EngineClient, - mock_protocol_core: ProtocolCore, subject: InstrumentCore, - version: APIVersion, ) -> None: """It should not raise an exception on an empty well.""" well_core = WellCore( name="my cool well", labware_id="123abc", engine_client=mock_engine_client ) - subject.liquid_probe_with_recovery(well_core=well_core) + loc = Location(Point(0, 0, 0), None) + subject.liquid_probe_with_recovery(well_core=well_core, loc=loc) decoy.verify( mock_engine_client.execute_command( cmd.LiquidProbeParams( pipetteId=subject.pipette_id, wellLocation=WellLocation( - origin=WellOrigin.TOP, offset=WellOffset(x=0, y=0, z=0) + origin=WellOrigin.TOP, offset=WellOffset(x=0, y=0, z=2.0) ), wellName=well_core.get_name(), labwareId=well_core.labware_id, diff --git a/api/tests/opentrons/protocol_api/core/engine/test_overlap_versions.py b/api/tests/opentrons/protocol_api/core/engine/test_overlap_versions.py index 9d41a431026..41ce048dd43 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_overlap_versions.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_overlap_versions.py @@ -16,11 +16,18 @@ def test_all_below_219_use_v0(api_version: APIVersion) -> None: @pytest.mark.parametrize("api_version", versions_at_or_above(APIVersion(2, 19))) -def test_all_above_219_use_v1(api_version: APIVersion) -> None: - """Versions above 2.19 should use v1.""" - assert overlap_for_api_version(api_version) == "v1" +def test_above_219_below_220_use_v1(api_version: APIVersion) -> None: + """Versions above 2.19 and below 2.20 should use v1.""" + if api_version in versions_below(APIVersion(2, 20), flex_only=False): + assert overlap_for_api_version(api_version) == "v1" -def test_future_api_version_uses_v1() -> None: - """Future versions should use v1.""" - assert overlap_for_api_version(APIVersion(2, 99)) == "v1" +@pytest.mark.parametrize("api_version", versions_at_or_above(APIVersion(2, 20))) +def test_above_220_use_v3(api_version: APIVersion) -> None: + """Versions above 2.20 should use v3.""" + assert overlap_for_api_version(api_version) == "v3" + + +def test_future_api_version_uses_v3() -> None: + """Future versions should use v3.""" + assert overlap_for_api_version(APIVersion(2, 99)) == "v3" 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 dc10b9bbad8..18286397a76 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 @@ -277,8 +277,8 @@ def test_load_instrument_pre_219( assert result.pipette_id == "cool-pipette" -@pytest.mark.parametrize("api_version", versions_at_or_above(APIVersion(2, 19))) -def test_load_instrument_post_219( +@pytest.mark.parametrize("api_version", versions_at_or_above(APIVersion(2, 20))) +def test_load_instrument_post_220( decoy: Decoy, mock_sync_hardware_api: SyncHardwareAPI, mock_engine_client: EngineClient, @@ -290,7 +290,7 @@ def test_load_instrument_post_219( cmd.LoadPipetteParams( pipetteName=PipetteNameType.P300_SINGLE, mount=MountType.LEFT, - tipOverlapNotAfterVersion="v1", + tipOverlapNotAfterVersion="v3", liquidPresenceDetection=False, ) ) diff --git a/api/tests/opentrons/protocol_api/test_instrument_context.py b/api/tests/opentrons/protocol_api/test_instrument_context.py index 1e3af474497..d98b99a9a6d 100644 --- a/api/tests/opentrons/protocol_api/test_instrument_context.py +++ b/api/tests/opentrons/protocol_api/test_instrument_context.py @@ -1,18 +1,21 @@ """Tests for the InstrumentContext public interface.""" +import inspect +import pytest from collections import OrderedDict +from contextlib import nullcontext as does_not_raise from datetime import datetime -import inspect +from typing import ContextManager, Optional +from unittest.mock import sentinel + +from decoy import Decoy +from pytest_lazyfixture import lazy_fixture # type: ignore[import-untyped] + from opentrons.protocol_engine.commands.pipetting_common import LiquidNotFoundError from opentrons.protocol_engine.errors.error_occurrence import ( ProtocolCommandFailedError, ) -import pytest -from pytest_lazyfixture import lazy_fixture # type: ignore[import-untyped] -from decoy import Decoy from opentrons.legacy_broker import LegacyBroker -from typing import ContextManager, Optional -from contextlib import nullcontext as does_not_raise from opentrons.protocols.api_support import instrument as mock_instrument_support from opentrons.protocols.api_support.types import APIVersion @@ -1069,8 +1072,8 @@ def test_liquid_presence_detection( ) -> None: """It should have a default liquid presence detection boolean set to False.""" decoy.when(mock_instrument_core.get_liquid_presence_detection()).then_return(False) - assert subject.liquid_detection is False - subject.liquid_detection = True + assert subject.liquid_presence_detection is False + subject.liquid_presence_detection = True decoy.verify(mock_instrument_core.set_liquid_presence_detection(True), times=1) @@ -1119,11 +1122,13 @@ def test_prepare_to_aspirate_checks_volume( @pytest.mark.parametrize( - argnames=["style", "primary_nozzle", "front_right_nozzle", "exception"], + argnames=["style", "primary_nozzle", "front_right_nozzle", "end", "exception"], argvalues=[ - [NozzleLayout.COLUMN, "A1", "H1", does_not_raise()], - [NozzleLayout.SINGLE, None, None, pytest.raises(ValueError)], - [NozzleLayout.ROW, "E1", None, pytest.raises(ValueError)], + [NozzleLayout.COLUMN, "A1", None, None, does_not_raise()], + [NozzleLayout.SINGLE, None, None, None, pytest.raises(ValueError)], + [NozzleLayout.ROW, "E1", None, None, pytest.raises(ValueError)], + [NozzleLayout.PARTIAL_COLUMN, "H1", None, "G1", does_not_raise()], + [NozzleLayout.PARTIAL_COLUMN, "H1", "H1", "G1", pytest.raises(ValueError)], ], ) def test_configure_nozzle_layout( @@ -1131,11 +1136,14 @@ def test_configure_nozzle_layout( style: NozzleLayout, primary_nozzle: Optional[str], front_right_nozzle: Optional[str], + end: Optional[str], exception: ContextManager[None], ) -> None: """The correct model is passed to the engine client.""" with exception: - subject.configure_nozzle_layout(style, primary_nozzle, front_right_nozzle) + subject.configure_nozzle_layout( + style=style, start=primary_nozzle, end=end, front_right=front_right_nozzle + ) @pytest.mark.parametrize("api_version", [APIVersion(2, 15)]) @@ -1284,17 +1292,11 @@ def test_detect_liquid_presence( ) -> None: """It should only return booleans. Not raise an exception.""" mock_well = decoy.mock(cls=Well) - lnfe = LiquidNotFoundError(id="1234", createdAt=datetime.now()) - errorToRaise = ProtocolCommandFailedError( - original_error=lnfe, - message=f"{lnfe.errorType}: {lnfe.detail}", - ) decoy.when( - mock_instrument_core.liquid_probe_without_recovery(mock_well._core) - ).then_raise(errorToRaise) - result = subject.detect_liquid_presence(mock_well) - assert isinstance(result, bool) - assert not result + mock_instrument_core.detect_liquid_presence(mock_well._core, mock_well.top()) + ).then_return(sentinel.inner_result) + outer_result = subject.detect_liquid_presence(mock_well) + assert outer_result is sentinel.inner_result @pytest.mark.parametrize("api_version", [APIVersion(2, 20)]) @@ -1311,10 +1313,16 @@ def test_require_liquid_presence( original_error=lnfe, message=f"{lnfe.errorType}: {lnfe.detail}", ) - decoy.when(mock_instrument_core.liquid_probe_with_recovery(mock_well._core)) + decoy.when( + mock_instrument_core.liquid_probe_with_recovery( + mock_well._core, mock_well.top() + ) + ) subject.require_liquid_presence(mock_well) decoy.when( - mock_instrument_core.liquid_probe_with_recovery(mock_well._core) + mock_instrument_core.liquid_probe_with_recovery( + mock_well._core, mock_well.top() + ) ).then_raise(errorToRaise) with pytest.raises(ProtocolCommandFailedError) as pcfe: subject.require_liquid_presence(mock_well) @@ -1336,7 +1344,9 @@ def test_measure_liquid_height( message=f"{lnfe.errorType}: {lnfe.detail}", ) decoy.when( - mock_instrument_core.liquid_probe_without_recovery(mock_well._core) + mock_instrument_core.liquid_probe_without_recovery( + mock_well._core, mock_well.top() + ) ).then_raise(errorToRaise) with pytest.raises(ProtocolCommandFailedError) as pcfe: subject.measure_liquid_height(mock_well) diff --git a/api/tests/opentrons/protocol_api_integration/test_pipette_movement_deck_conflicts.py b/api/tests/opentrons/protocol_api_integration/test_pipette_movement_deck_conflicts.py index 33e92086edb..59523fd2c91 100644 --- a/api/tests/opentrons/protocol_api_integration/test_pipette_movement_deck_conflicts.py +++ b/api/tests/opentrons/protocol_api_integration/test_pipette_movement_deck_conflicts.py @@ -3,7 +3,7 @@ import pytest from opentrons import simulate -from opentrons.protocol_api import COLUMN, ALL +from opentrons.protocol_api import COLUMN, ALL, SINGLE from opentrons.protocol_api.core.engine.deck_conflict import ( PartialTipMovementNotAllowedError, ) @@ -61,8 +61,14 @@ def test_deck_conflicts_for_96_ch_a12_column_configuration() -> None: ): instrument.pick_up_tip(badly_placed_tiprack.wells_by_name()["A1"]) - # No error since no tall item in west slot of destination slot - instrument.pick_up_tip(well_placed_tiprack.wells_by_name()["A1"]) + with pytest.raises( + PartialTipMovementNotAllowedError, match="outside of robot bounds" + ): + # Picking up from A1 in an east-most slot using a configuration with column 12 would + # result in a collision with the side of the robot. + instrument.pick_up_tip(well_placed_tiprack.wells_by_name()["A1"]) + + instrument.pick_up_tip(well_placed_tiprack.wells_by_name()["A12"]) instrument.aspirate(50, well_placed_labware.wells_by_name()["A4"]) with pytest.raises( @@ -75,14 +81,19 @@ def test_deck_conflicts_for_96_ch_a12_column_configuration() -> None: ): instrument.dispense(10, tc_adjacent_plate.wells_by_name()["A1"]) + instrument.dispense(10, tc_adjacent_plate.wells_by_name()["H2"]) + # No error cuz dispensing from high above plate, so it clears tuberack in west slot instrument.dispense(15, badly_placed_labware.wells_by_name()["A1"].top(150)) thermocycler.open_lid() # type: ignore[union-attr] - # Will NOT raise error since first column of TC labware is accessible - # (it is just a few mm away from the left bound) - instrument.dispense(25, accessible_plate.wells_by_name()["A1"]) + with pytest.raises( + PartialTipMovementNotAllowedError, match="outside of robot bounds" + ): + # Dispensing to A1 in an east-most slot using a configuration with column 12 would + # result in a collision with the side of the robot. + instrument.dispense(25, accessible_plate.wells_by_name()["A1"]) instrument.drop_tip() @@ -102,7 +113,7 @@ def test_deck_conflicts_for_96_ch_a12_column_configuration() -> None: @pytest.mark.ot3_only def test_close_shave_deck_conflicts_for_96_ch_a12_column_configuration() -> None: """Shouldn't raise errors for "almost collision"s.""" - protocol_context = simulate.get_protocol_api(version="2.16", robot_type="Flex") + protocol_context = simulate.get_protocol_api(version="2.20", robot_type="Flex") res12 = protocol_context.load_labware("nest_12_reservoir_15ml", "C3") # Mag block and tiprack adapter are very close to the destination reservoir labware @@ -113,18 +124,19 @@ def test_close_shave_deck_conflicts_for_96_ch_a12_column_configuration() -> None adapter="opentrons_flex_96_tiprack_adapter", ) tiprack_8 = protocol_context.load_labware("opentrons_flex_96_tiprack_200ul", "B2") - hs = protocol_context.load_module("heaterShakerModuleV1", "D1") + hs = protocol_context.load_module("heaterShakerModuleV1", "C1") hs_adapter = hs.load_adapter("opentrons_96_deep_well_adapter") deepwell = hs_adapter.load_labware("nest_96_wellplate_2ml_deep") protocol_context.load_trash_bin("A3") p1000_96 = protocol_context.load_instrument("flex_96channel_1000") - p1000_96.configure_nozzle_layout(style=COLUMN, start="A12", tip_racks=[tiprack_8]) + p1000_96.configure_nozzle_layout(style=SINGLE, start="A12", tip_racks=[tiprack_8]) hs.close_labware_latch() # type: ignore[union-attr] + # Note p1000_96.distribute( 15, - res12.wells()[0], - deepwell.rows()[0], + res12["A6"], + deepwell.columns()[6], disposal_vol=0, ) @@ -180,8 +192,15 @@ def test_deck_conflicts_for_96_ch_a1_column_configuration() -> None: with pytest.raises( PartialTipMovementNotAllowedError, match="outside of robot bounds" ): + # Moving the 96 channel in column configuration with column 1 + # is incompatible with moving to a plate in B3 in the right most + # column. instrument.aspirate(25, well_placed_plate.wells_by_name()["A11"]) + # No error because we're moving to column 1 of the plate with + # column 1 of the 96 channel. + instrument.aspirate(25, well_placed_plate.wells_by_name()["A1"]) + # No error cuz no taller labware on the right instrument.aspirate(10, my_tuberack.wells_by_name()["A1"]) diff --git a/api/tests/opentrons/protocol_engine/commands/test_configure_nozzle_layout.py b/api/tests/opentrons/protocol_engine/commands/test_configure_nozzle_layout.py index 2159d5efb9c..2f318b147ac 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_configure_nozzle_layout.py +++ b/api/tests/opentrons/protocol_engine/commands/test_configure_nozzle_layout.py @@ -66,7 +66,7 @@ ], [ QuadrantNozzleLayoutConfiguration( - primaryNozzle="A1", frontRightNozzle="E1" + primaryNozzle="A1", frontRightNozzle="E1", backLeftNozzle="A1" ), NozzleMap.build( physical_nozzles=NINETY_SIX_MAP, @@ -115,6 +115,11 @@ async def test_configure_nozzle_layout_implementation( if isinstance(request_model, QuadrantNozzleLayoutConfiguration) else None ) + back_left_nozzle = ( + request_model.backLeftNozzle + if isinstance(request_model, QuadrantNozzleLayoutConfiguration) + else None + ) decoy.when( await tip_handler.available_for_nozzle_layout( @@ -122,6 +127,7 @@ async def test_configure_nozzle_layout_implementation( style=request_model.style, primary_nozzle=primary_nozzle, front_right_nozzle=front_right_nozzle, + back_left_nozzle=back_left_nozzle, ) ).then_return(nozzle_params) diff --git a/api/tests/opentrons/protocol_engine/commands/test_liquid_probe.py b/api/tests/opentrons/protocol_engine/commands/test_liquid_probe.py index a6cea0ab40b..61f4339360d 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_liquid_probe.py +++ b/api/tests/opentrons/protocol_engine/commands/test_liquid_probe.py @@ -1,5 +1,6 @@ """Test LiquidProbe commands.""" from datetime import datetime +from typing import Type, Union from opentrons.protocol_engine.errors.exceptions import ( MustHomeError, @@ -23,6 +24,9 @@ LiquidProbeParams, LiquidProbeResult, LiquidProbeImplementation, + TryLiquidProbeParams, + TryLiquidProbeResult, + TryLiquidProbeImplementation, ) from opentrons.protocol_engine.commands.command import DefinedErrorData, SuccessData @@ -33,17 +37,59 @@ PipettingHandler, ) from opentrons.protocol_engine.resources.model_utils import ModelUtils -from opentrons.protocol_engine.types import CurrentWell, LoadedPipette +from opentrons.protocol_engine.types import LoadedPipette + + +EitherImplementationType = Union[ + Type[LiquidProbeImplementation], Type[TryLiquidProbeImplementation] +] +EitherImplementation = Union[LiquidProbeImplementation, TryLiquidProbeImplementation] +EitherParamsType = Union[Type[LiquidProbeParams], Type[TryLiquidProbeParams]] +EitherResultType = Union[Type[LiquidProbeResult], Type[TryLiquidProbeResult]] + + +@pytest.fixture( + params=[ + (LiquidProbeImplementation, LiquidProbeParams, LiquidProbeResult), + (TryLiquidProbeImplementation, TryLiquidProbeParams, TryLiquidProbeResult), + ] +) +def types( + request: pytest.FixtureRequest, +) -> tuple[EitherImplementationType, EitherParamsType, EitherResultType]: + """Return a tuple of types associated with a single variant of the command.""" + return request.param # type: ignore[no-any-return] + + +@pytest.fixture +def implementation_type( + types: tuple[EitherImplementationType, object, object] +) -> EitherImplementationType: + """Return an implementation type. Kept in sync with the params and result types.""" + return types[0] + + +@pytest.fixture +def params_type(types: tuple[object, EitherParamsType, object]) -> EitherParamsType: + """Return a params type. Kept in sync with the implementation and result types.""" + return types[1] + + +@pytest.fixture +def result_type(types: tuple[object, object, EitherResultType]) -> EitherResultType: + """Return a result type. Kept in sync with the implementation and params types.""" + return types[2] @pytest.fixture def subject( + implementation_type: EitherImplementationType, movement: MovementHandler, pipetting: PipettingHandler, model_utils: ModelUtils, -) -> LiquidProbeImplementation: +) -> Union[LiquidProbeImplementation, TryLiquidProbeImplementation]: """Get the implementation subject.""" - return LiquidProbeImplementation( + return implementation_type( pipetting=pipetting, movement=movement, model_utils=model_utils, @@ -54,13 +100,14 @@ async def test_liquid_probe_implementation_no_prep( decoy: Decoy, movement: MovementHandler, pipetting: PipettingHandler, - subject: LiquidProbeImplementation, + subject: EitherImplementation, + params_type: EitherParamsType, + result_type: EitherResultType, ) -> None: """A Liquid Probe should have an execution implementation without preparing to aspirate.""" location = WellLocation(origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1)) - current_well = CurrentWell(pipette_id="abc", labware_id="123", well_name="A3") - data = LiquidProbeParams( + data = params_type( pipetteId="abc", labwareId="123", wellName="A3", @@ -75,7 +122,6 @@ async def test_liquid_probe_implementation_no_prep( labware_id="123", well_name="A3", well_location=location, - current_well=current_well, ), ).then_return(Point(x=1, y=2, z=3)) @@ -84,13 +130,15 @@ async def test_liquid_probe_implementation_no_prep( pipette_id="abc", labware_id="123", well_name="A3", + well_location=location, ), ).then_return(15.0) result = await subject.execute(data) + assert type(result.public) is result_type # Pydantic v1 only compares the fields. assert result == SuccessData( - public=LiquidProbeResult(z_position=15.0, position=DeckPoint(x=1, y=2, z=3)), + public=result_type(z_position=15.0, position=DeckPoint(x=1, y=2, z=3)), private=None, ) @@ -100,13 +148,14 @@ async def test_liquid_probe_implementation_with_prep( state_view: StateView, movement: MovementHandler, pipetting: PipettingHandler, - subject: LiquidProbeImplementation, + subject: EitherImplementation, + params_type: EitherParamsType, + result_type: EitherResultType, ) -> None: """A Liquid Probe should have an execution implementation with preparing to aspirate.""" - location = WellLocation(origin=WellOrigin.TOP, offset=WellOffset(x=0, y=0, z=0)) - current_well = CurrentWell(pipette_id="abc", labware_id="123", well_name="A3") + location = WellLocation(origin=WellOrigin.TOP, offset=WellOffset(x=0, y=0, z=2)) - data = LiquidProbeParams( + data = params_type( pipetteId="abc", labwareId="123", wellName="A3", @@ -122,11 +171,7 @@ async def test_liquid_probe_implementation_with_prep( ) decoy.when( await movement.move_to_well( - pipette_id="abc", - labware_id="123", - well_name="A3", - well_location=location, - current_well=current_well, + pipette_id="abc", labware_id="123", well_name="A3", well_location=location ), ).then_return(Point(x=1, y=2, z=3)) @@ -135,13 +180,15 @@ async def test_liquid_probe_implementation_with_prep( pipette_id="abc", labware_id="123", well_name="A3", + well_location=location, ), ).then_return(15.0) result = await subject.execute(data) + assert type(result.public) is result_type # Pydantic v1 only compares the fields. assert result == SuccessData( - public=LiquidProbeResult(z_position=15.0, position=DeckPoint(x=1, y=2, z=3)), + public=result_type(z_position=15.0, position=DeckPoint(x=1, y=2, z=3)), private=None, ) @@ -150,8 +197,9 @@ async def test_liquid_probe_implementation_with_prep( pipette_id="abc", labware_id="123", well_name="A3", - well_location=WellLocation(origin=WellOrigin.TOP), - current_well=current_well, + well_location=WellLocation( + origin=WellOrigin.TOP, offset=WellOffset(x=0, y=0, z=2) + ), ), ) @@ -160,7 +208,8 @@ async def test_liquid_not_found_error( decoy: Decoy, movement: MovementHandler, pipetting: PipettingHandler, - subject: LiquidProbeImplementation, + subject: EitherImplementation, + params_type: EitherParamsType, model_utils: ModelUtils, ) -> None: """It should return a liquid not found error if the hardware API indicates that.""" @@ -170,16 +219,13 @@ async def test_liquid_not_found_error( well_location = WellLocation( origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1) ) - current_well = CurrentWell( - pipette_id=pipette_id, labware_id=labware_id, well_name=well_name - ) position = Point(x=1, y=2, z=3) error_id = "error-id" error_timestamp = datetime(year=2020, month=1, day=2) - data = LiquidProbeParams( + data = params_type( pipetteId=pipette_id, labwareId=labware_id, wellName=well_name, @@ -196,7 +242,6 @@ async def test_liquid_not_found_error( labware_id=labware_id, well_name=well_name, well_location=well_location, - current_well=current_well, ), ).then_return(position) @@ -205,6 +250,7 @@ async def test_liquid_not_found_error( pipette_id=pipette_id, labware_id=labware_id, well_name=well_name, + well_location=well_location, ), ).then_raise(PipetteLiquidNotFoundError()) @@ -213,22 +259,32 @@ async def test_liquid_not_found_error( result = await subject.execute(data) - assert result == DefinedErrorData( - public=LiquidNotFoundError.construct( - id=error_id, createdAt=error_timestamp, wrappedErrors=[matchers.Anything()] - ), - private=LiquidNotFoundErrorInternalData( - position=DeckPoint(x=position.x, y=position.y, z=position.z) - ), - ) + if isinstance(subject, LiquidProbeImplementation): + assert result == DefinedErrorData( + public=LiquidNotFoundError.construct( + id=error_id, + createdAt=error_timestamp, + wrappedErrors=[matchers.Anything()], + ), + private=LiquidNotFoundErrorInternalData( + position=DeckPoint(x=position.x, y=position.y, z=position.z) + ), + ) + else: + assert result == SuccessData( + public=TryLiquidProbeResult( + z_position=None, + position=DeckPoint(x=position.x, y=position.y, z=position.z), + ), + private=None, + ) async def test_liquid_probe_tip_checking( decoy: Decoy, - movement: MovementHandler, pipetting: PipettingHandler, - subject: LiquidProbeImplementation, - model_utils: ModelUtils, + subject: EitherImplementation, + params_type: EitherParamsType, ) -> None: """It should return a TipNotAttached error if the hardware API indicates that.""" pipette_id = "pipette-id" @@ -238,7 +294,7 @@ async def test_liquid_probe_tip_checking( origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1) ) - data = LiquidProbeParams( + data = params_type( pipetteId=pipette_id, labwareId=labware_id, wellName=well_name, @@ -250,21 +306,15 @@ async def test_liquid_probe_tip_checking( pipette_id=pipette_id, ), ).then_raise(TipNotAttachedError()) - try: + with pytest.raises(TipNotAttachedError): await subject.execute(data) - assert False - except TipNotAttachedError: - assert True - except Exception: - assert False async def test_liquid_probe_volume_checking( decoy: Decoy, - movement: MovementHandler, pipetting: PipettingHandler, - subject: LiquidProbeImplementation, - model_utils: ModelUtils, + subject: EitherImplementation, + params_type: EitherParamsType, ) -> None: """It should return a TipNotEmptyError if the hardware API indicates that.""" pipette_id = "pipette-id" @@ -274,7 +324,7 @@ async def test_liquid_probe_volume_checking( origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1) ) - data = LiquidProbeParams( + data = params_type( pipetteId=pipette_id, labwareId=labware_id, wellName=well_name, @@ -283,21 +333,15 @@ async def test_liquid_probe_volume_checking( decoy.when( pipetting.get_is_empty(pipette_id=pipette_id), ).then_return(False) - try: + with pytest.raises(TipNotEmptyError): await subject.execute(data) - assert False - except TipNotEmptyError: - assert True - except Exception: - assert False async def test_liquid_probe_location_checking( decoy: Decoy, movement: MovementHandler, - pipetting: PipettingHandler, - subject: LiquidProbeImplementation, - model_utils: ModelUtils, + subject: EitherImplementation, + params_type: EitherParamsType, ) -> None: """It should return a PositionUnkownError if the hardware API indicates that.""" pipette_id = "pipette-id" @@ -307,7 +351,7 @@ async def test_liquid_probe_location_checking( origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1) ) - data = LiquidProbeParams( + data = params_type( pipetteId=pipette_id, labwareId=labware_id, wellName=well_name, @@ -318,10 +362,5 @@ async def test_liquid_probe_location_checking( mount=MountType.LEFT, ), ).then_return(False) - try: + with pytest.raises(MustHomeError): await subject.execute(data) - assert False - except MustHomeError: - assert True - except Exception: - assert False diff --git a/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py b/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py index 2e5205bdc66..dfd02e9dfd5 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py +++ b/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py @@ -275,6 +275,7 @@ async def test_add_tip( "style", "primary_nozzle", "front_nozzle", + "back_nozzle", "exception", "expected_result", "tip_result", @@ -285,8 +286,13 @@ async def test_add_tip( "COLUMN", "A1", None, + None, does_not_raise(), - {"primary_nozzle": "A1", "front_right_nozzle": "H1"}, + { + "primary_nozzle": "A1", + "front_right_nozzle": "H1", + "back_left_nozzle": "A1", + }, None, ], [ @@ -294,16 +300,27 @@ async def test_add_tip( "ROW", "A1", None, + None, pytest.raises(CommandParameterLimitViolated), None, None, ], - [8, "SINGLE", "A1", None, does_not_raise(), {"primary_nozzle": "A1"}, None], + [ + 8, + "SINGLE", + "A1", + None, + None, + does_not_raise(), + {"primary_nozzle": "A1"}, + None, + ], [ 1, "SINGLE", "A1", None, + None, pytest.raises(CommandPreconditionViolated), None, None, @@ -313,6 +330,7 @@ async def test_add_tip( "COLUMN", "A1", None, + None, pytest.raises(CommandPreconditionViolated), None, TipGeometry(length=50, diameter=5, volume=300), @@ -328,6 +346,7 @@ async def test_available_nozzle_layout( style: str, primary_nozzle: Optional[str], front_nozzle: Optional[str], + back_nozzle: Optional[str], exception: ContextManager[None], expected_result: Optional[Dict[str, str]], tip_result: Optional[TipGeometry], @@ -348,12 +367,11 @@ async def test_available_nozzle_layout( with exception: hw_result = await hw_subject.available_for_nozzle_layout( - "pipette-id", style, primary_nozzle, front_nozzle + "pipette-id", style, primary_nozzle, front_nozzle, back_nozzle ) virtual_result = await virtual_subject.available_for_nozzle_layout( - "pipette-id", style, primary_nozzle, front_nozzle + "pipette-id", style, primary_nozzle, front_nozzle, back_nozzle ) - assert hw_result == virtual_result == expected_result diff --git a/api/tests/opentrons/protocol_engine/state/test_addressable_area_state.py b/api/tests/opentrons/protocol_engine/state/test_addressable_area_state.py index 7209e78bb90..66fa692fe25 100644 --- a/api/tests/opentrons/protocol_engine/state/test_addressable_area_state.py +++ b/api/tests/opentrons/protocol_engine/state/test_addressable_area_state.py @@ -28,6 +28,17 @@ def test_deck_configuration_setting( deck_type=DeckType.OT3_STANDARD, ), deck_definition=ot3_standard_deck_def, + robot_definition={ + "displayName": "OT-3", + "robotType": "OT-3 Standard", + "models": ["OT-3 Standard"], + "extents": [477.2, 493.8, 0.0], + "mountOffsets": { + "left": [-13.5, -60.5, 255.675], + "right": [40.5, -60.5, 255.675], + "gripper": [84.55, -12.75, 93.85], + }, + }, ) subject_view = AddressableAreaView(subject.state) diff --git a/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py b/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py index c3d52028647..fcadb43940e 100644 --- a/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py +++ b/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py @@ -69,6 +69,17 @@ def simulated_subject( deck_type=DeckType.OT3_STANDARD, ), deck_definition=ot3_standard_deck_def, + robot_definition={ + "displayName": "OT-3", + "robotType": "OT-3 Standard", + "models": ["OT-3 Standard"], + "extents": [477.2, 493.8, 0.0], + "mountOffsets": { + "left": [-13.5, -60.5, 255.675], + "right": [40.5, -60.5, 255.675], + "gripper": [84.55, -12.75, 93.85], + }, + }, ) @@ -85,6 +96,17 @@ def subject( deck_type=DeckType.OT3_STANDARD, ), deck_definition=ot3_standard_deck_def, + robot_definition={ + "displayName": "OT-3", + "robotType": "OT-3 Standard", + "models": ["OT-3 Standard"], + "extents": [477.2, 493.8, 0.0], + "mountOffsets": { + "left": [-13.5, -60.5, 255.675], + "right": [40.5, -60.5, 255.675], + "gripper": [84.55, -12.75, 93.85], + }, + }, ) @@ -100,6 +122,17 @@ def test_initial_state_simulated( deck_configuration=[], robot_type="OT-3 Standard", use_simulated_deck_config=True, + robot_definition={ + "displayName": "OT-3", + "robotType": "OT-3 Standard", + "models": ["OT-3 Standard"], + "extents": [477.2, 493.8, 0.0], + "mountOffsets": { + "left": [-13.5, -60.5, 255.675], + "right": [40.5, -60.5, 255.675], + "gripper": [84.55, -12.75, 93.85], + }, + }, ) diff --git a/api/tests/opentrons/protocol_engine/state/test_addressable_area_view.py b/api/tests/opentrons/protocol_engine/state/test_addressable_area_view.py index 30ebe0d0341..3d1cbe9be1a 100644 --- a/api/tests/opentrons/protocol_engine/state/test_addressable_area_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_addressable_area_view.py @@ -64,6 +64,17 @@ def get_addressable_area_view( potential_cutout_fixtures_by_cutout_id=potential_cutout_fixtures_by_cutout_id or {}, deck_definition=deck_definition or cast(DeckDefinitionV5, {"otId": "fake"}), + robot_definition={ + "displayName": "OT-3", + "robotType": "OT-3 Standard", + "models": ["OT-3 Standard"], + "extents": [477.2, 493.8, 0.0], + "mountOffsets": { + "left": [-13.5, -60.5, 255.675], + "right": [40.5, -60.5, 255.675], + "gripper": [84.55, -12.75, 93.85], + }, + }, deck_configuration=deck_configuration or [], robot_type=robot_type, use_simulated_deck_config=use_simulated_deck_config, diff --git a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py index 58a4a49940e..9887a4ef76c 100644 --- a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py @@ -174,7 +174,20 @@ def addressable_area_store( ) -> AddressableAreaStore: """Get an addressable area store that can accept actions.""" return AddressableAreaStore( - deck_configuration=[], config=state_config, deck_definition=deck_definition + deck_configuration=[], + config=state_config, + deck_definition=deck_definition, + robot_definition={ + "displayName": "OT-3", + "robotType": "OT-3 Standard", + "models": ["OT-3 Standard"], + "extents": [477.2, 493.8, 0.0], + "mountOffsets": { + "left": [-13.5, -60.5, 255.675], + "right": [40.5, -60.5, 255.675], + "gripper": [84.55, -12.75, 93.85], + }, + }, ) @@ -2077,6 +2090,8 @@ def test_get_next_drop_tip_location( pipette_bounding_box_offsets=PipetteBoundingBoxOffsets( back_left_corner=Point(x=10, y=20, z=30), front_right_corner=Point(x=40, y=50, z=60), + front_left_corner=Point(x=10, y=50, z=60), + back_right_corner=Point(x=40, y=20, z=60), ), lld_settings={}, ) diff --git a/api/tests/opentrons/protocol_engine/state/test_module_store.py b/api/tests/opentrons/protocol_engine/state/test_module_store.py index e6de0a96ac0..0dabf508483 100644 --- a/api/tests/opentrons/protocol_engine/state/test_module_store.py +++ b/api/tests/opentrons/protocol_engine/state/test_module_store.py @@ -74,6 +74,17 @@ def get_addressable_area_view( or {}, deck_definition=deck_definition or cast(DeckDefinitionV5, {"otId": "fake"}), deck_configuration=deck_configuration or [], + robot_definition={ + "displayName": "OT-3", + "robotType": "OT-3 Standard", + "models": ["OT-3 Standard"], + "extents": [477.2, 493.8, 0.0], + "mountOffsets": { + "left": [-13.5, -60.5, 255.675], + "right": [40.5, -60.5, 255.675], + "gripper": [84.55, -12.75, 93.85], + }, + }, robot_type=robot_type, use_simulated_deck_config=use_simulated_deck_config, ) diff --git a/api/tests/opentrons/protocol_engine/state/test_module_view.py b/api/tests/opentrons/protocol_engine/state/test_module_view.py index b840673f2e8..e308c09407d 100644 --- a/api/tests/opentrons/protocol_engine/state/test_module_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_module_view.py @@ -87,6 +87,17 @@ def get_addressable_area_view( or {}, deck_definition=deck_definition or cast(DeckDefinitionV5, {"otId": "fake"}), deck_configuration=deck_configuration or [], + robot_definition={ + "displayName": "OT-3", + "robotType": "OT-3 Standard", + "models": ["OT-3 Standard"], + "extents": [477.2, 493.8, 0.0], + "mountOffsets": { + "left": [-13.5, -60.5, 255.675], + "right": [40.5, -60.5, 255.675], + "gripper": [84.55, -12.75, 93.85], + }, + }, robot_type=robot_type, use_simulated_deck_config=use_simulated_deck_config, ) 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 a99ac90e9e2..8ccfc06fd07 100644 --- a/api/tests/opentrons/protocol_engine/state/test_pipette_store.py +++ b/api/tests/opentrons/protocol_engine/state/test_pipette_store.py @@ -775,6 +775,8 @@ def test_add_pipette_config( pipette_bounding_box_offsets=PipetteBoundingBoxOffsets( back_left_corner=Point(x=1, y=2, z=3), front_right_corner=Point(x=4, y=5, z=6), + front_left_corner=Point(x=1, y=5, z=3), + back_right_corner=Point(x=4, y=2, z=3), ), lld_settings={}, ) 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 e15c8401699..1942a9a04e1 100644 --- a/api/tests/opentrons/protocol_engine/state/test_pipette_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_pipette_view.py @@ -46,7 +46,10 @@ back_left_offset=Point(x=10, y=20, z=30), front_right_offset=Point(x=40, y=50, z=60) ) _SAMPLE_PIPETTE_BOUNDING_BOX_OFFSETS = PipetteBoundingBoxOffsets( - back_left_corner=Point(x=10, y=20, z=30), front_right_corner=Point(x=40, y=50, z=60) + back_left_corner=Point(x=10, y=20, z=30), + front_right_corner=Point(x=40, y=50, z=60), + front_left_corner=Point(x=10, y=50, z=60), + back_right_corner=Point(x=40, y=20, z=60), ) @@ -594,6 +597,8 @@ class _PipetteSpecs(NamedTuple): bounding_box_offsets=PipetteBoundingBoxOffsets( back_left_corner=Point(0.0, 31.5, 35.52), front_right_corner=Point(0.0, -31.5, 35.52), + front_left_corner=Point(0.0, -31.5, 35.52), + back_right_corner=Point(0.0, 31.5, 35.52), ), nozzle_map=NozzleMap.build( physical_nozzles=EIGHT_CHANNEL_MAP, @@ -620,6 +625,8 @@ class _PipetteSpecs(NamedTuple): bounding_box_offsets=PipetteBoundingBoxOffsets( back_left_corner=Point(0.0, 31.5, 35.52), front_right_corner=Point(0.0, -31.5, 35.52), + front_left_corner=Point(0.0, -31.5, 35.52), + back_right_corner=Point(0.0, 31.5, 35.52), ), nozzle_map=NozzleMap.build( physical_nozzles=EIGHT_CHANNEL_MAP, @@ -646,6 +653,8 @@ class _PipetteSpecs(NamedTuple): bounding_box_offsets=PipetteBoundingBoxOffsets( back_left_corner=Point(-36.0, -25.5, -259.15), front_right_corner=Point(63.0, -88.5, -259.15), + front_left_corner=Point(-36.0, -88.5, -259.15), + back_right_corner=Point(63.0, -25.5, -259.15), ), nozzle_map=NozzleMap.build( physical_nozzles=NINETY_SIX_MAP, @@ -688,6 +697,8 @@ class _PipetteSpecs(NamedTuple): bounding_box_offsets=PipetteBoundingBoxOffsets( back_left_corner=Point(-36.0, -25.5, -259.15), front_right_corner=Point(63.0, -88.5, -259.15), + front_left_corner=Point(-36.0, -88.5, -259.15), + back_right_corner=Point(63.0, -25.5, -259.15), ), nozzle_map=NozzleMap.build( physical_nozzles=NINETY_SIX_MAP, @@ -712,6 +723,8 @@ class _PipetteSpecs(NamedTuple): bounding_box_offsets=PipetteBoundingBoxOffsets( back_left_corner=Point(-36.0, -25.5, -259.15), front_right_corner=Point(63.0, -88.5, -259.15), + front_left_corner=Point(-36.0, -88.5, -259.15), + back_right_corner=Point(63.0, -25.5, -259.15), ), nozzle_map=NozzleMap.build( physical_nozzles=NINETY_SIX_MAP, @@ -736,6 +749,8 @@ class _PipetteSpecs(NamedTuple): bounding_box_offsets=PipetteBoundingBoxOffsets( back_left_corner=Point(-36.0, -25.5, -259.15), front_right_corner=Point(63.0, -88.5, -259.15), + front_left_corner=Point(-36.0, -88.5, -259.15), + back_right_corner=Point(63.0, -25.5, -259.15), ), nozzle_map=NozzleMap.build( physical_nozzles=NINETY_SIX_MAP, diff --git a/api/tests/opentrons/protocol_engine/state/test_state_store.py b/api/tests/opentrons/protocol_engine/state/test_state_store.py index d69784c6834..26f50515317 100644 --- a/api/tests/opentrons/protocol_engine/state/test_state_store.py +++ b/api/tests/opentrons/protocol_engine/state/test_state_store.py @@ -39,6 +39,13 @@ def subject( return StateStore( config=engine_config, deck_definition=ot2_standard_deck_def, + robot_definition={ + "displayName": "OT-2", + "robotType": "OT-2 Standard", + "models": ["OT-2 Standard", "OT-2 Refresh"], + "extents": [446.75, 347.5, 0.0], + "mountOffsets": {"left": [-34.0, 0.0, 0.0], "right": [0.0, 0.0, 0.0]}, + }, deck_fixed_labware=[], change_notifier=change_notifier, is_door_open=False, diff --git a/api/tests/opentrons/protocols/parameters/test_csv_parameter_definition.py b/api/tests/opentrons/protocols/parameters/test_csv_parameter_definition.py index 9344da7daa1..70933cc326c 100644 --- a/api/tests/opentrons/protocols/parameters/test_csv_parameter_definition.py +++ b/api/tests/opentrons/protocols/parameters/test_csv_parameter_definition.py @@ -12,10 +12,7 @@ create_csv_parameter, CSVParameterDefinition, ) -from opentrons.protocols.parameters.exceptions import ( - ParameterDefinitionError, - RuntimeParameterRequired, -) +from opentrons.protocols.parameters.exceptions import RuntimeParameterRequired @pytest.fixture(autouse=True) @@ -66,17 +63,6 @@ def test_set_csv_value( assert csv_parameter_subject.value is mock_file -def test_set_csv_value_raises( - decoy: Decoy, csv_parameter_subject: CSVParameterDefinition -) -> None: - """It should raise if the file set to does not end in '.csv'.""" - mock_file = decoy.mock(cls=TextIOWrapper) - decoy.when(mock_file.name).then_return("mock.txt") - - with pytest.raises(ParameterDefinitionError): - csv_parameter_subject.value = mock_file - - def test_csv_parameter_as_protocol_engine_type( csv_parameter_subject: CSVParameterDefinition, ) -> None: diff --git a/api/tests/opentrons/protocols/parameters/test_csv_parameter_interface.py b/api/tests/opentrons/protocols/parameters/test_csv_parameter_interface.py new file mode 100644 index 00000000000..be46b61845d --- /dev/null +++ b/api/tests/opentrons/protocols/parameters/test_csv_parameter_interface.py @@ -0,0 +1,101 @@ +import pytest +from pytest_lazyfixture import lazy_fixture # type: ignore[import-untyped] + +import tempfile +from typing import TextIO + +from opentrons.protocols.parameters.types import CSVParameter + + +@pytest.fixture +def csv_file_basic() -> TextIO: + temp_file = tempfile.TemporaryFile("r+") + contents = '"x","y","z"\n"a",1,2\n"b",3,4\n"c",5,6' + temp_file.write(contents) + temp_file.seek(0) + return temp_file + + +@pytest.fixture +def csv_file_no_quotes() -> TextIO: + temp_file = tempfile.TemporaryFile("r+") + contents = "x,y,z\na,1,2\nb,3,4\nc,5,6" + temp_file.write(contents) + temp_file.seek(0) + return temp_file + + +@pytest.fixture +def csv_file_preceding_spaces() -> TextIO: + temp_file = tempfile.TemporaryFile("r+") + contents = '"x", "y", "z"\n"a", 1, 2\n"b", 3, 4\n"c", 5, 6' + temp_file.write(contents) + temp_file.seek(0) + return temp_file + + +@pytest.fixture +def csv_file_mixed_quotes() -> TextIO: + temp_file = tempfile.TemporaryFile("r+") + contents = 'head,er\n"a,b,c",def\n"""ghi""","jkl"' + temp_file.write(contents) + temp_file.seek(0) + return temp_file + + +@pytest.fixture +def csv_file_different_delimiter() -> TextIO: + temp_file = tempfile.TemporaryFile("r+") + contents = "x:y:z\na,:1,:2\nb,:3,:4\nc,:5,:6" + temp_file.write(contents) + temp_file.seek(0) + return temp_file + + +def test_csv_parameter(csv_file_basic: TextIO) -> None: + """It should load the CSV parameter and provide access to the file, contents, and rows.""" + subject = CSVParameter(csv_file_basic) + assert subject.file is csv_file_basic + assert subject.contents == '"x","y","z"\n"a",1,2\n"b",3,4\n"c",5,6' + + +@pytest.mark.parametrize( + "csv_file", + [ + lazy_fixture("csv_file_basic"), + lazy_fixture("csv_file_no_quotes"), + lazy_fixture("csv_file_preceding_spaces"), + ], +) +def test_csv_parameter_rows(csv_file: TextIO) -> None: + """It should load the rows as all strings even with no quotes or leading spaces.""" + subject = CSVParameter(csv_file) + assert len(subject.parse_as_csv()) == 4 + assert subject.parse_as_csv()[0] == ["x", "y", "z"] + assert subject.parse_as_csv()[1] == ["a", "1", "2"] + + +def test_csv_parameter_mixed_quotes(csv_file_mixed_quotes: TextIO) -> None: + """It should load the rows with no quotes, quotes and escaped quotes with double quotes.""" + subject = CSVParameter(csv_file_mixed_quotes) + assert len(subject.parse_as_csv()) == 3 + assert subject.parse_as_csv()[0] == ["head", "er"] + assert subject.parse_as_csv()[1] == ["a,b,c", "def"] + assert subject.parse_as_csv()[2] == ['"ghi"', "jkl"] + + +def test_csv_parameter_additional_kwargs(csv_file_different_delimiter: TextIO) -> None: + """It should load the rows with a different delimiter.""" + subject = CSVParameter(csv_file_different_delimiter) + rows = subject.parse_as_csv(delimiter=":") + assert len(rows) == 4 + assert rows[0] == ["x", "y", "z"] + assert rows[1] == ["a,", "1,", "2"] + + +def test_csv_parameter_dont_detect_dialect(csv_file_preceding_spaces: TextIO) -> None: + """It should load the rows without trying to detect the dialect.""" + subject = CSVParameter(csv_file_preceding_spaces) + rows = subject.parse_as_csv(detect_dialect=False) + assert rows[0] == ["x", ' "y"', ' "z"'] + assert rows[1] == ["a", " 1", " 2"] diff --git a/app-shell/build/license_en.txt b/app-shell/build/license_en.txt index f16605697b0..cf847badf81 100644 --- a/app-shell/build/license_en.txt +++ b/app-shell/build/license_en.txt @@ -1,6 +1,6 @@ Opentrons End-User License Agreement -Last updated: June 27, 2024 +Last updated: July 10, 2024 THIS END-USER LICENSE AGREEMENT (“EULA”) is a legal agreement between you (“User”), either as an individual or on behalf of an entity, and Opentrons Labworks Inc. (“Opentrons”) regarding your use of Opentrons robots, modules, software, and associated documentation (“Opentrons Products”) including, but not limited to, the Opentrons OT-2 robot and associated modules, the Opentrons Flex robot and associated modules, the Opentrons App, the Opentrons API, the Opentrons Protocol Designer and Protocol Library, the Opentrons Labware Library, and the Opentrons Website. By installing or using the Opentrons Products, you agree to be bound by the terms and conditions of this EULA. If you do not agree to the terms of this EULA, you must immediately cease use of the Opentrons Products. @@ -9,7 +9,7 @@ Use of Opentrons Products. Permitted Use. User shall use the Opentrons Products strictly in accordance with the terms of the EULA and Related Agreements. User shall use Opentrons Product software only in conjunction with Opentrons Product hardware. Restrictions on Use. Unless otherwise specified in a separate agreement entered into between Opentrons and User, User may not, and may not permit others to: reverse engineer, decompile or otherwise derive source code from the Opentrons Products; -disassemble the Opentrons Products, except as instructed by Opentrons employees or Opentrons technical product manuals; +disassemble or bypass protection on Opentrons Products to exceed authorized access to Opentrons systems, or to analyze or modify components of the Opentrons Products for the purpose of gaining unauthorized access to confidential Opentrons or Opentrons Product information; copy, modify, or create derivative works of the Opentrons Products for the purpose of competing with Opentrons; remove or alter any proprietary notices or marks on the Opentrons Products; use the Opentrons Products in any manner that does not comply with the applicable laws in the jurisdiction(s) in which such use takes place; diff --git a/app-shell/build/release-notes-internal.md b/app-shell/build/release-notes-internal.md index e6925397157..c2193890ead 100644 --- a/app-shell/build/release-notes-internal.md +++ b/app-shell/build/release-notes-internal.md @@ -1,6 +1,24 @@ For more details about this release, please see the full [technical changelog][]. [technical change log]: https://github.com/Opentrons/opentrons/releases +## Internal Release 2.0.0-alpha.1 + +This internal release, pulled from the `edge` branch, contains features being developed for 8.0.0. It's for internal testing only. Usage may require a robot factory reset to restore robot stability. + + + +## Internal Release 2.0.0-alpha.0 + +This internal release, pulled from the `edge` branch, contains features being developed for 8.0.0. It's for internal testing only. Usage may require a robot factory reset to restore robot stability. + + + +## Internal Release 1.5.0 + +This internal release is from the `edge` branch to contain rapid dev on new features for 7.3.0. This release is for internal testing purposes and if used may require a factory reset of the robot to return to a stable version. Though designated as stable, this build contains many critical bugs and should not be used in production. + + + ## Internal Release 1.5.0-alpha.1 This internal release is from the `edge` branch to contain rapid dev on new features for 7.3.0. This release is for internal testing purposes and if used may require a factory reset of the robot to return to a stable version. diff --git a/app-shell/electron-builder.config.js b/app-shell/electron-builder.config.js index 49d58f9fcfa..1b048915255 100644 --- a/app-shell/electron-builder.config.js +++ b/app-shell/electron-builder.config.js @@ -8,6 +8,7 @@ const { } = process.env const DEV_MODE = process.env.NODE_ENV !== 'production' const USE_PYTHON = process.env.NO_PYTHON !== 'true' +const WINDOWS_SIGN = process.env.WINDOWS_SIGN === 'true' const project = process.env.OPENTRONS_PROJECT ?? 'robot-stack' // this will generate either @@ -72,6 +73,11 @@ module.exports = async () => ({ target: ['nsis'], publisherName: 'Opentrons Labworks Inc.', icon: project === 'robot-stack' ? 'build/icon.ico' : 'build/three.ico', + forceCodeSigning: WINDOWS_SIGN, + rfc3161TimeStampServer: 'http://timestamp.digicert.com', + sign: 'scripts/windows-custom-sign.js', + signDlls: true, + signingHashAlgorithms: ['sha256'], }, nsis: { oneClick: false, diff --git a/app-shell/scripts/windows-custom-sign.js b/app-shell/scripts/windows-custom-sign.js new file mode 100644 index 00000000000..90d7927ab6a --- /dev/null +++ b/app-shell/scripts/windows-custom-sign.js @@ -0,0 +1,62 @@ +// from https://github.com/electron-userland/electron-builder/issues/7605 + +'use strict' + +const { execSync } = require('node:child_process') + +exports.default = async configuration => { + const signCmd = `smctl sign --keypair-alias="${String( + process.env.SM_KEYPAIR_ALIAS + )}" --input "${String(configuration.path)}" --certificate="${String( + process.env.WINDOWS_CSC_FILEPATH + )}" --exit-non-zero-on-fail --failfast --verbose` + console.log(signCmd) + try { + const signProcess = execSync(signCmd, { + stdio: 'pipe', + }) + console.log(`Sign success!`) + console.log( + `Sign stdout: ${signProcess?.stdout?.toString() ?? ''}` + ) + console.log( + `Sign stderr: ${signProcess?.stderr?.toString() ?? ''}` + ) + console.log(`Sign code: ${signProcess.code}`) + } catch (err) { + console.error(`Exception running sign: ${err.status}! +Process stdout: + ${err?.stdout?.toString() ?? ''} +------------- +Process stderr: +${err?.stdout?.toString() ?? ''} +------------- +`) + throw err + } + const verifyCmd = `smctl sign verify --fingerprint="${String( + process.env.SM_CODE_SIGNING_CERT_SHA1_HASH + )}" --input="${String(configuration.path)}" --verbose` + console.log(verifyCmd) + try { + const verifyProcess = execSync(verifyCmd, { stdio: 'pipe' }) + console.log(`Verify success!`) + console.log( + `Verify stdout: ${verifyProcess?.stdout?.toString() ?? ''}` + ) + console.log( + `Verify stderr: ${verifyProcess?.stderr?.toString() ?? ''}` + ) + } catch (err) { + console.error(` +Exception running verification: ${err.status}! +Process stdout: + ${err?.stdout?.toString() ?? ''} +-------------- +Process stderr: + ${err?.stderr?.toString() ?? ''} +-------------- +`) + throw err + } +} diff --git a/app/package.json b/app/package.json index 2108a9dff65..43a1869b186 100644 --- a/app/package.json +++ b/app/package.json @@ -53,7 +53,7 @@ "react-intersection-observer": "^8.33.1", "react-markdown": "9.0.1", "react-redux": "8.1.2", - "react-router-dom": "5.3.4", + "react-router-dom": "6.24.1", "react-select": "5.4.0", "react-simple-keyboard": "^3.7.0", "react-viewport-list": "6.3.0", diff --git a/app/src/App/DesktopApp.tsx b/app/src/App/DesktopApp.tsx index 5576d995a1f..ab0b91f7c9c 100644 --- a/app/src/App/DesktopApp.tsx +++ b/app/src/App/DesktopApp.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Redirect, Route, Switch, useRouteMatch } from 'react-router-dom' +import { Navigate, Route, Routes, useMatch } from 'react-router-dom' import { ErrorBoundary } from 'react-error-boundary' import { I18nextProvider } from 'react-i18next' @@ -48,20 +48,17 @@ export const DesktopApp = (): JSX.Element => { const desktopRoutes: RouteProps[] = [ { Component: ProtocolsLanding, - exact: true, name: 'Protocols', navLinkTo: '/protocols', path: '/protocols', }, { Component: ProtocolDetails, - exact: true, name: 'Protocol Details', path: '/protocols/:protocolKey', }, { Component: ProtocolTimeline, - exact: true, name: 'Protocol Timeline', path: '/protocols/:protocolKey/timeline', }, @@ -73,26 +70,22 @@ export const DesktopApp = (): JSX.Element => { }, { Component: DevicesLanding, - exact: true, name: 'Devices', navLinkTo: '/devices', path: '/devices', }, { Component: DeviceDetails, - exact: true, name: 'Device', path: '/devices/:robotName', }, { Component: RobotSettings, - exact: true, name: 'Robot Settings', path: '/devices/:robotName/robot-settings/:robotSettingsTab?', }, { Component: CalibrationDashboard, - exact: true, name: 'Calibration Dashboard', path: '/devices/:robotName/robot-settings/calibration/dashboard', }, @@ -103,7 +96,6 @@ export const DesktopApp = (): JSX.Element => { }, { Component: AppSettings, - exact: true, name: 'App Settings', path: '/app-settings/:appSettingsTab?', }, @@ -123,28 +115,37 @@ export const DesktopApp = (): JSX.Element => { > - - {desktopRoutes.map( - ({ Component, exact, path }: RouteProps) => { - return ( - - - - - - - - ) - } - )} - - + + {desktopRoutes.map(({ Component, path }: RouteProps) => { + return ( + + + + + + + + + + } + path={path} + /> + ) + })} + } /> + @@ -157,7 +158,7 @@ export const DesktopApp = (): JSX.Element => { } function RobotControlTakeover(): JSX.Element | null { - const deviceRouteMatch = useRouteMatch({ path: '/devices/:robotName' }) + const deviceRouteMatch = useMatch('/devices/:robotName') const params = deviceRouteMatch?.params as DesktopRouteParams const robotName = params?.robotName const robot = useRobot(robotName) diff --git a/app/src/App/DesktopAppFallback.tsx b/app/src/App/DesktopAppFallback.tsx index 03c5c367b3c..30525606b23 100644 --- a/app/src/App/DesktopAppFallback.tsx +++ b/app/src/App/DesktopAppFallback.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useDispatch } from 'react-redux' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { useTrackEvent, ANALYTICS_DESKTOP_APP_ERROR } from '../redux/analytics' @@ -26,14 +26,14 @@ export function DesktopAppFallback({ error }: FallbackProps): JSX.Element { const { t } = useTranslation('app_settings') const trackEvent = useTrackEvent() const dispatch = useDispatch() - const history = useHistory() + const navigate = useNavigate() const handleReloadClick = (): void => { trackEvent({ name: ANALYTICS_DESKTOP_APP_ERROR, properties: { errorMessage: error.message }, }) // route to the root page and initiate an electron browser window reload via app-shell - history.push('/') + navigate('/', { replace: true }) dispatch(reloadUi(error.message as string)) } diff --git a/app/src/App/OnDeviceDisplayApp.tsx b/app/src/App/OnDeviceDisplayApp.tsx index 61c039b06b8..ae0e0a1d933 100644 --- a/app/src/App/OnDeviceDisplayApp.tsx +++ b/app/src/App/OnDeviceDisplayApp.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useDispatch, useSelector } from 'react-redux' -import { Switch, Route, Redirect } from 'react-router-dom' +import { Routes, Route, Navigate } from 'react-router-dom' import { css } from 'styled-components' import { ErrorBoundary } from 'react-error-boundary' @@ -252,23 +252,31 @@ export function OnDeviceDisplayAppRoutes(): JSX.Element { ` return ( - + {ON_DEVICE_DISPLAY_PATHS.map(path => ( - - - - {getPathComponent(path)} - - + + + {getPathComponent(path)} + + } + /> ))} - {targetPath != null && } - + {targetPath != null && ( + } /> + )} + ) } function TopLevelRedirects(): JSX.Element | null { const currentRunRoute = useCurrentRunRoute() - return currentRunRoute != null ? : null + return currentRunRoute != null ? ( + } /> + ) : null } function ProtocolReceiptToasts(): null { diff --git a/app/src/App/__tests__/Navbar.test.tsx b/app/src/App/__tests__/Navbar.test.tsx index c5ec4661226..db7eedc744b 100644 --- a/app/src/App/__tests__/Navbar.test.tsx +++ b/app/src/App/__tests__/Navbar.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { describe, it } from 'vitest' import { screen, render } from '@testing-library/react' -import { StaticRouter } from 'react-router-dom' +import { MemoryRouter } from 'react-router-dom' import { Navbar } from '../Navbar' @@ -16,9 +16,9 @@ const ROUTE_PROPS: RouteProps[] = [ describe('Navbar', () => { it('should render a NavbarLink for every nav location', () => { render( - + - + ) screen.getByRole('link', { name: 'foo' }) screen.getByRole('link', { name: 'bar' }) @@ -26,9 +26,9 @@ describe('Navbar', () => { }) it('should render logo, settings, and help', () => { render( - + - + ) screen.getByRole('img', { name: 'opentrons logo' }) screen.getByTestId('Navbar_settingsLink') diff --git a/app/src/App/types.ts b/app/src/App/types.ts index 7bec62d1409..87d8f77d4a1 100644 --- a/app/src/App/types.ts +++ b/app/src/App/types.ts @@ -6,7 +6,6 @@ export interface RouteProps { * drop developed components into slots held by placeholder div components */ Component: React.FC - exact?: boolean /** * a route/page name to render in the nav bar */ diff --git a/app/src/assets/images/on-device-display/odd-abstract-6.png b/app/src/assets/images/on-device-display/odd-abstract-6.png new file mode 100644 index 00000000000..d1456885836 Binary files /dev/null and b/app/src/assets/images/on-device-display/odd-abstract-6.png differ diff --git a/app/src/assets/localization/en/anonymous.json b/app/src/assets/localization/en/anonymous.json index 6d4a6d9b563..12e57d595fa 100644 --- a/app/src/assets/localization/en/anonymous.json +++ b/app/src/assets/localization/en/anonymous.json @@ -2,6 +2,8 @@ "a_robot_software_update_is_available": "A robot software update is required to run protocols with this version of the desktop app. Go to Robot", "about_flex_gripper": "About Gripper", "alternative_security_types_description": "The robot supports connecting to various enterprise access points. Connect via USB and finish setup in the desktop app.", + "attach_a_pipette": "Attach a pipette to your robot", + "attach_a_pipette_for_quick_transfer": "To create a quick transfer, you need to attach a pipette to your robot.", "calibration_block_description": "This block is a specially made tool that fits perfectly on your deck and helps with calibration.If you do not have a Calibration Block, please email support so we can send you one. In your message, be sure to include your name, company or institution name, and shipping address. While you wait for the block to arrive, you can use the flat surface on the trash bin of your robot instead.", "calibration_on_opentrons_tips_is_important": "It’s extremely important to perform this calibration using the tips and tip racks specified above, as the robot determines accuracy based on the known measurements of these tips.", "choose_what_data_to_share": "Choose what robot data to share.", @@ -63,6 +65,7 @@ "share_logs_with_opentrons_description": "Help improve this product by automatically sending anonymous robot logs. These logs are used to troubleshoot robot issues and spot error trends.", "show_labware_offset_snippets_description": "Only for users who need to apply labware offset data outside of the app. When enabled, code snippets for Jupyter Notebook and SSH are available during protocol setup.", "something_seems_wrong": "There may be a problem with your pipette. Exit setup and contact support for assistance.", + "storage_limit_reached_description": "Your robot has reached the limit of quick transfers that it can store. You must delete an existing quick transfer before creating a new one.", "these_are_advanced_settings": "These are advanced settings. Please do not attempt to adjust without assistance from support. Changing these settings may affect the lifespan of your pipette.These settings do not override any pipette settings defined in protocols.", "update_requires_restarting_app": "Updating requires restarting the app.", "update_robot_software_description": "Bypass the auto-update process and update the robot software manually.", diff --git a/app/src/assets/localization/en/branded.json b/app/src/assets/localization/en/branded.json index 334671e69e9..6a65184183f 100644 --- a/app/src/assets/localization/en/branded.json +++ b/app/src/assets/localization/en/branded.json @@ -1,5 +1,7 @@ { "a_robot_software_update_is_available": "A robot software update is required to run protocols with this version of the Opentrons App. Go to Robot", + "attach_a_pipette": "Attach a pipette to your Flex", + "attach_a_pipette_for_quick_transfer": "To create a quick transfer, you need to attach a pipette to your Opentrons Flex.", "about_flex_gripper": "About Flex Gripper", "alternative_security_types_description": "The Opentrons App supports connecting Flex to various enterprise access points. Connect via USB and finish setup in the app.", "calibration_block_description": "This block is a specially made tool that fits perfectly on your deck and helps with calibration.If you do not have a Calibration Block, please email support@opentrons.com so we can send you one. In your message, be sure to include your name, company or institution name, and shipping address. While you wait for the block to arrive, you can use the flat surface on the trash bin of your robot instead.", @@ -63,6 +65,7 @@ "share_logs_with_opentrons_description": "Help Opentrons improve its products and services by automatically sending anonymous robot logs. Opentrons uses these logs to troubleshoot robot issues and spot error trends.", "show_labware_offset_snippets_description": "Only for users who need to apply labware offset data outside of the Opentrons App. When enabled, code snippets for Jupyter Notebook and SSH are available during protocol setup.", "something_seems_wrong": "There may be a problem with your pipette. Exit setup and contact Opentrons Support for assistance.", + "storage_limit_reached_description": "Your Opentrons Flex has reached the limit of quick transfers that it can store. You must delete an existing quick transfer before creating a new one.", "these_are_advanced_settings": "These are advanced settings. Please do not attempt to adjust without assistance from Opentrons Support. Changing these settings may affect the lifespan of your pipette.These settings do not override any pipette settings defined in protocols.", "update_requires_restarting_app": "Updating requires restarting the Opentrons App.", "update_robot_software_description": "Bypass the Opentrons App auto-update process and update the robot software manually.", diff --git a/app/src/assets/localization/en/devices_landing.json b/app/src/assets/localization/en/devices_landing.json index b0a3307ace1..dfd92d23030 100644 --- a/app/src/assets/localization/en/devices_landing.json +++ b/app/src/assets/localization/en/devices_landing.json @@ -11,7 +11,7 @@ "forget_unavailable_robot": "Forget unavailable robot", "go_to_run": "Go to Run", "home_gantry": "Home gantry", - "how_to_setup_a_robot": "How to setup a new robot", + "how_to_setup_a_robot": "How to set up a new robot", "idle": "Idle", "if_connecting_via_usb": "If connecting via USB", "if_connecting_wirelessly": "If connecting wirelessly", @@ -23,11 +23,14 @@ "lights_on": "lights on", "loading": "loading", "looking_for_robots": "Looking for robots", - "ninety_six_mount": "Left + Right Mount", "make_sure_robot_is_connected": "Make sure the robot is connected to this computer", "modules": "Modules", + "new_robot_instructions": "When setting up a new Flex, follow the instructions on the touchscreen. For more information, consult the Quickstart Guide for your robot.", + "ninety_six_mount": "Left + Right Mount", "no_robots_found": "No robots found", "not_available": "Not available ({{count}})", + "opentrons_flex_quickstart_guide": "Opentrons Flex Quickstart Guide", + "ot2_quickstart_guide": "OT-2 Quickstart Guide", "refresh": "Refresh", "restart_the_app": "Restart the app", "restart_the_robot": "Restart the robot", diff --git a/app/src/assets/localization/en/error_recovery.json b/app/src/assets/localization/en/error_recovery.json index 3a6003fe638..eeaf8980cc8 100644 --- a/app/src/assets/localization/en/error_recovery.json +++ b/app/src/assets/localization/en/error_recovery.json @@ -10,6 +10,7 @@ "change_location": "Change location", "change_tip_pickup_location": "Change tip pick-up location", "choose_a_recovery_action": "Choose a recovery action", + "close_the_robot_door": "Close the robot door, and then resume the recovery action.", "confirm": "Confirm", "continue": "Continue", "continue_run_now": "Continue run now", @@ -23,8 +24,6 @@ "if_tips_are_attached": "If tips are attached, you can choose to blow out any aspirated liquid and drop tips before the run is terminated.", "ignore_all_errors_of_this_type": "Ignore all errors of this type", "ignore_error_and_skip": "Ignore error and skip to next step", - "skipping_to_step_succeeded": "Skipping to step {{step}} succeeded", - "retrying_step_succeeded": "Retrying step {{step}} succeeded", "ignore_only_this_error": "Ignore only this error", "ignore_similar_errors_later_in_run": "Ignore similar errors later in the run?", "launch_recovery_mode": "Launch Recovery Mode", @@ -42,14 +41,17 @@ "recovery_mode": "Recovery Mode", "recovery_mode_explanation": "Recovery Mode provides you with guided and manual controls for handling errors at runtime.
You can make changes to ensure the step in progress when the error occurred can be completed or choose to cancel the protocol. When changes are made and no subsequent errors are detected, the method completes. Depending on the conditions that caused the error, you will only be provided with appropriate options.", "replace_tips_and_select_location": "It's best to replace tips and select the last location used for tip pickup.", - "replace_used_tips_in_rack_location": "Replace used tips in rack location {{location}}", - "replace_with_new_tip_rack": "Replace with new tip rack", + "replace_used_tips_in_rack_location": "Replace used tips in rack location {{location}} in slot {{slot}}", + "replace_with_new_tip_rack": "Replace with new tip rack in slot {{slot}}", + "resume": "Resume", "retry_now": "Retry now", "retry_step": "Retry step", "retry_with_new_tips": "Retry with new tips", "retry_with_same_tips": "Retry with same tips", + "retrying_step_succeeded": "Retrying step {{step}} succeeded.", "return_to_menu": "Return to menu", "return_to_the_menu": "Return to the menu to choose how to proceed.", + "robot_door_is_open": "Robot door is open", "robot_will_not_check_for_liquid": "The robot will not check for liquid again. The run will continue from the next step.Close the robot door before proceeding.", "robot_will_retry_with_new_tips": "The robot will retry the failed step with the new tips.Close the robot door before proceeding.", "robot_will_retry_with_same_tips": "The robot will retry the failed step with the same tips.Close the robot door before proceeding.", @@ -60,6 +62,7 @@ "skip_to_next_step": "Skip to next step", "skip_to_next_step_new_tips": "Skip to next step with new tips", "skip_to_next_step_same_tips": "Skip to next step with same tips", + "skipping_to_step_succeeded": "Skipping to step {{step}} succeeded.", "stand_back": "Stand back, robot is in motion", "stand_back_picking_up_tips": "Stand back, picking up tips", "stand_back_resuming": "Stand back, resuming current step", diff --git a/app/src/assets/localization/en/protocol_command_text.json b/app/src/assets/localization/en/protocol_command_text.json index 069f6e13886..484ec67124a 100644 --- a/app/src/assets/localization/en/protocol_command_text.json +++ b/app/src/assets/localization/en/protocol_command_text.json @@ -16,37 +16,49 @@ "deactivating_tc_block": "Deactivating Thermocycler block", "deactivating_tc_lid": "Deactivating Thermocycler lid", "degrees_c": "{{temp}}°C", + "detect_liquid_presence": "Detecting liquid presence in well {{well_name}} of {{labware}} in {{labware_location}}", "disengaging_magnetic_module": "Disengaging Magnetic Module", - "dispense_push_out": "Dispensing {{volume}} µL into well {{well_name}} of {{labware}} in {{labware_location}} at {{flow_rate}} µL/sec and pushing out {{push_out_volume}} µL", "dispense": "Dispensing {{volume}} µL into well {{well_name}} of {{labware}} in {{labware_location}} at {{flow_rate}} µL/sec", "dispense_in_place": "Dispensing {{volume}} µL in place at {{flow_rate}} µL/sec", + "dispense_push_out": "Dispensing {{volume}} µL into well {{well_name}} of {{labware}} in {{labware_location}} at {{flow_rate}} µL/sec and pushing out {{push_out_volume}} µL", "drop_tip": "Dropping tip in {{well_name}} of {{labware}}", "drop_tip_in_place": "Dropping tip in place", "engaging_magnetic_module": "Engaging Magnetic Module", "fixed_trash": "Fixed Trash", "home_gantry": "Homing all gantry, pipette, and plunger axes", "latching_hs_latch": "Latching labware on Heater-Shaker", - "module_in_slot_plural": "{{module}}", + "left": "Left", + "load_labware_info_protocol_setup": "Load {{labware}} in {{module_name}} in Slot {{slot_name}}", + "load_labware_info_protocol_setup_adapter": "Load {{labware}} in {{adapter_name}} in Slot {{slot_name}}", + "load_labware_info_protocol_setup_adapter_module": "Load {{labware}} in {{adapter_name}} in {{module_name}} in Slot {{slot_name}}", + "load_labware_info_protocol_setup_adapter_off_deck": "Load {{labware}} in {{adapter_name}} off deck", + "load_labware_info_protocol_setup_no_module": "Load {{labware}} in Slot {{slot_name}}", + "load_labware_info_protocol_setup_off_deck": "Load {{labware}} off deck", + "load_liquids_info_protocol_setup": "Load {{liquid}} into {{labware}}", + "load_module_protocol_setup": "Load {{module}} in Slot {{slot_name}}", + "load_pipette_protocol_setup": "Load {{pipette_name}} in {{mount_name}} Mount", "module_in_slot": "{{module}} in Slot {{slot_name}}", + "module_in_slot_plural": "{{module}}", + "move_labware": "Move Labware", "move_labware_manually": "Manually move {{labware}} from {{old_location}} to {{new_location}}", "move_labware_on": "Move labware on {{robot_name}}", "move_labware_using_gripper": "Moving {{labware}} using gripper from {{old_location}} to {{new_location}}", - "move_labware": "Move Labware", "move_relative": "Moving {{distance}} mm along {{axis}} axis", + "move_to_addressable_area": "Moving to {{addressable_area}}", + "move_to_addressable_area_drop_tip": "Moving to {{addressable_area}}", "move_to_coordinates": "Moving to (X: {{x}}, Y: {{y}}, Z: {{z}})", "move_to_slot": "Moving to Slot {{slot_name}}", "move_to_well": "Moving to well {{well_name}} of {{labware}} in {{labware_location}}", - "move_to_addressable_area": "Moving to {{addressable_area}}", - "move_to_addressable_area_drop_tip": "Moving to {{addressable_area}}", "notes": "notes", "off_deck": "off deck", "offdeck": "offdeck", "opening_tc_lid": "Opening Thermocycler lid", - "pause_on": "Pause on {{robot_name}}", "pause": "Pause", + "pause_on": "Pause on {{robot_name}}", "pickup_tip": "Picking up tip(s) from {{well_range}} of {{labware}} in {{labware_location}}", "prepare_to_aspirate": "Preparing {{pipette}} to aspirate", "return_tip": "Returning tip to {{well_name}} of {{labware}} in {{labware_location}}", + "right": "Right", "save_position": "Saving position", "set_and_await_hs_shake": "Setting Heater-Shaker to shake at {{rpm}} rpm and waiting until reached", "setting_hs_temp": "Setting Target Temperature of Heater-Shaker to {{temp}}", @@ -58,8 +70,8 @@ "tc_awaiting_for_duration": "Waiting for Thermocycler profile to complete", "tc_run_profile_steps": "temperature: {{celsius}}°C, seconds: {{seconds}}", "tc_starting_profile": "Thermocycler starting {{repetitions}} repetitions of cycle composed of the following steps:", - "trash_bin_in_slot": "Trash Bin in {{slot_name}}", "touch_tip": "Touching tip", + "trash_bin_in_slot": "Trash Bin in {{slot_name}}", "unlatching_hs_latch": "Unlatching labware on Heater-Shaker", "wait_for_duration": "Pausing for {{seconds}} seconds. {{message}}", "wait_for_resume": "Pausing protocol", diff --git a/app/src/assets/localization/en/protocol_setup.json b/app/src/assets/localization/en/protocol_setup.json index 6ac20b18775..8b495eae864 100644 --- a/app/src/assets/localization/en/protocol_setup.json +++ b/app/src/assets/localization/en/protocol_setup.json @@ -269,6 +269,7 @@ "update_deck": "Update deck", "updated": "Updated", "usb_connected_no_port_info": "USB Port Connected", + "usb_drive_notification": "Leave USB drive attached until run starts", "usb_port_connected": "USB Port {{port}}", "usb_port_number": "USB-{{port}}", "value_out_of_range_generic": "Value must be in range", diff --git a/app/src/assets/localization/en/quick_transfer.json b/app/src/assets/localization/en/quick_transfer.json index 436fb81247f..f40298d6ae1 100644 --- a/app/src/assets/localization/en/quick_transfer.json +++ b/app/src/assets/localization/en/quick_transfer.json @@ -1,14 +1,33 @@ { - "add_or_remove_columns": "add or remove columns", + "a_way_to_move_liquid": "A way to move a single liquid from one labware to another.", "add_or_remove": "add or remove", + "add_or_remove_columns": "add or remove columns", + "advanced_setting_disabled": "Advanced setting disabled for this transfer", "advanced_settings": "Advanced settings", + "air_gap": "Air gap", + "air_gap_before_aspirating": "Air gap before aspirating", + "air_gap_before_dispensing": "Air gap before dispensing", + "air_gap_value": "{{volume}} µL", + "air_gap_volume_µL": "Air gap volume (µL)", "all": "All labware", "always": "Before every aspirate", - "aspirate_volume": "Aspirate volume per well", - "aspirate_volume_µL": "Aspirate volume per well (µL)", "aspirate_flow_rate": "Aspirate flow rate", "aspirate_flow_rate_µL": "Aspirate flow rate (µL/s)", - "flow_rate_value": "{{flow_rate}} µL/s", + "aspirate_settings": "Aspirate Settings", + "aspirate_tip_position": "Aspirate tip position", + "aspirate_volume": "Aspirate volume per well", + "aspirate_volume_µL": "Aspirate volume per well (µL)", + "attach_pipette": "Attach pipette", + "blow_out": "Blowout", + "blow_out_after_dispensing": "Blowout after dispensing", + "blow_out_destination_well": "Destination well", + "blow_out_into_destination_well": "into destination well", + "blow_out_into_source_well": "into source well", + "blow_out_into_trash_bin": "into trash bin", + "blow_out_into_waste_chute": "into waste chute", + "blow_out_source_well": "Source well", + "blow_out_trash_bin": "Trash bin", + "blow_out_waste_chute": "Waste chute", "both_mounts": "Left + Right Mount", "change_tip": "Change tip", "character_limit_error": "Character limit exceeded", @@ -16,30 +35,51 @@ "columns": "columns", "create_new_transfer": "Create new quick transfer", "create_transfer": "Create transfer", + "delay": "Delay", + "delay_before_aspirating": "Delay before aspirating", + "delay_before_dispensing": "Delay before dispensing", + "delay_duration_s": "Delay duration (seconds)", + "delay_position_mm": "Delay position from bottom of well (mm)", + "delay_value": "{{delay}}s, {{position}} mm from bottom", "create_to_get_started": "Create a new quick transfer to get started.", "delete_this_transfer": "Delete this quick transfer?", "delete_transfer": "Delete quick transfer", "deleted_transfer": "Deleted quick transfer", "destination": "Destination", "destination_labware": "Destination labware", - "dispense_volume": "Dispense volume per well", - "dispense_volume_µL": "Dispense volume per well (µL)", "dispense_flow_rate": "Dispense flow rate", "dispense_flow_rate_µL": "Dispense flow rate (µL/s)", + "dispense_settings": "Dispense Settings", + "dispense_tip_position": "Dispense tip position", + "dispense_volume": "Dispense volume per well", + "dispense_volume_µL": "Dispense volume per well (µL)", + "disposal_volume_µL": "Disposal volume (µL)", + "distance_bottom_of_well_mm": "Distance from bottom of well (mm)", "enter_characters": "Enter up to 60 characters", "error_analyzing": "An error occurred while attempting to analyze {{transferName}}.", "exit_quick_transfer": "Exit quick transfer?", + "flow_rate_value": "{{flow_rate}} µL/s", "failed_analysis": "failed analysis", + "got_it": "Got it", "grid": "grid", "grids": "grids", + "labware": "Labware", "learn_more": "Learn more", "left_mount": "Left Mount", "lose_all_progress": "You will lose all progress on this quick transfer.", + "mix": "Mix", + "mix_before_aspirating": "Mix before aspirating", + "mix_before_dispensing": "Mix before dispensing", + "mix_repetitions": "Mix repetitions", + "mix_value": "{{volume}} µL, {{reps}} times", + "mix_volume_µL": "Mix volume (µL)", "name_your_transfer": "Name your quick transfer", "none_to_show": "No quick transfers to show!", "number_wells_selected_error_learn_more": "Quick transfers with multiple source {{selectionUnits}} can either be one-to-one (select {{wellCount}} destination {{selectionUnits}} for this transfer) or consolidate (select 1 destination {{selectionUnit}}).", "number_wells_selected_error_message": "Select 1 or {{wellCount}} {{selectionUnits}} to make this transfer.", "once": "Once at the start of the transfer", + "option_disabled": "Disabled", + "option_enabled": "Enabled", "overview": "Overview", "perDest": "Per destination well", "perSource": "Per source well", @@ -47,21 +87,22 @@ "pinned_transfer": "Pinned quick transfer", "pinned_transfers": "Pinned Quick Transfers", "pipette": "Pipette", + "pipette_currently_attached": "Quick transfer options depend on the pipettes currently attached to your robot.", "pipette_path": "Pipette path", - "pipette_path_single": "Single transfers", "pipette_path_multi_aspirate": "Multi-aspirate", - "pipette_path_multi_dispense": "Multi-dispense", + "pipette_path_multi_dispense": "Multi-dispense, {{volume}} disposal volume, blowout into {{blowOutLocation}}", + "pipette_path_single": "Single transfers", + "pre_wet_tip": "Pre-wet tip", "quick_transfer": "Quick transfer", "quick_transfer_volume": "Quick Transfer {{volume}}µL", - "right_mount": "Right Mount", "reservoir": "Reservoirs", + "right_mount": "Right Mount", "run_now": "Run now", "run_transfer": "Run quick transfer", "run_quick_transfer_now": "Do you want to run your quick transfer now?", "save": "Save", - "save_to_run_later": "Save your quick transfer to run it in the future.", "save_for_later": "Save for later", - "source": "Source", + "save_to_run_later": "Save your quick transfer to run it in the future.", "select_attached_pipette": "Select attached pipette", "select_by": "select by", "select_dest_labware": "Select destination labware", @@ -72,13 +113,23 @@ "set_aspirate_volume": "Set aspirate volume", "set_dispense_volume": "Set dispense volume", "set_transfer_volume": "Set transfer volume", + "source": "Source", "source_labware": "Source labware", "source_labware_d2": "Source labware in D2", "starting_well": "starting well", - "use_deck_slots": "Quick transfers use deck slots B2-D2. These slots hold a tip rack, a source labware, and a destination labware.Make sure that your deck configuration is up to date to avoid collisions.", + "storage_limit_reached": "Storage limit reached", "tip_drop_location": "Tip drop location", "tip_management": "Tip management", + "tip_position": "Tip position", + "tip_position_value": "{{position}} mm from the bottom", "tip_rack": "Tip rack", + "touch_tip": "Touch tip", + "touch_tip_before_aspirating": "Touch tip before aspirating", + "touch_tip_before_dispensing": "Touch tip before dispensing", + "touch_tip_position_mm": "Touch tip position from bottom of well (mm)", + "touch_tip_value": "{{position}} mm from bottom", + "use_deck_slots": "Quick transfers use deck slots B2-D2. These slots hold a tip rack, a source labware, and a destination labware.Make sure that your deck configuration is up to date to avoid collisions.", + "value_out_of_range": "Value must be between {{min}}-{{max}}", "too_many_pins_body": "Remove a quick transfer in order to add more transfers to your pinned list.", "too_many_pins_header": "You've hit your max!", "transfer_analysis_failed": "quick transfer analysis failed", @@ -90,15 +141,13 @@ "unpinned_transfer": "Unpinned quick transfer", "volume_per_well": "Volume per well", "volume_per_well_µL": "Volume per well (µL)", - "value_out_of_range": "Value must be between {{min}}-{{max}}", - "labware": "Labware", - "pipette_currently_attached": "Quick transfer options depend on the pipettes currently attached to your robot.", "wasteChute": "Waste chute", "wasteChute_location": "Waste chute in {{slotName}}", + "welcome_to_quick_transfer": "Welcome to quick transfer!", + "well": "well", "wellPlate": "Well plates", - "well_selection": "Well selection", "well_ratio": "Quick transfers with multiple source wells can either be one-to-one (select {{wells}} for this transfer) or consolidate (select 1 destination well).", - "well": "well", + "well_selection": "Well selection", "wells": "wells", "will_be_deleted": " will be permanently deleted." } diff --git a/app/src/assets/localization/en/run_details.json b/app/src/assets/localization/en/run_details.json index 18127568031..9209e9e5fc2 100644 --- a/app/src/assets/localization/en/run_details.json +++ b/app/src/assets/localization/en/run_details.json @@ -1,10 +1,11 @@ { "analysis_failure_on_robot": "An error occurred while attempting to analyze {{protocolName}} on {{robotName}}. Fix the following error and try running this protocol again.", "analyzing_on_robot": "Analyzing on robot", - "anticipated_step": "Anticipated steps", "anticipated": "Anticipated steps", + "anticipated_step": "Anticipated steps", "apply_stored_data": "Apply stored data", "apply_stored_labware_offset_data": "Apply stored Labware Offset data?", + "cancel_run": "Cancel run", "cancel_run_alert_info_flex": "Doing so will terminate this run and home your robot.", "cancel_run_alert_info_ot2": "Doing so will terminate this run, drop any attached tips in the trash container, and home your robot.", "cancel_run_and_restart": "Cancel the run and restart setup to edit", @@ -12,20 +13,19 @@ "cancel_run_modal_confirm": "Yes, cancel run", "cancel_run_modal_heading": "Are you sure you want to cancel?", "cancel_run_module_info": "Additionally, any hardware modules used within the protocol will remain active and maintain their current states until deactivated.", - "cancel_run": "Cancel run", - "canceling_run_dot": "canceling run...", "canceling_run": "Canceling Run", - "clear_protocol_to_make_available": "Clear protocol from robot to make it available.", + "canceling_run_dot": "canceling run...", "clear_protocol": "Clear protocol", - "close_door_to_resume": "Close robot door to resume run", + "clear_protocol_to_make_available": "Clear protocol from robot to make it available.", "close_door": "Close robot door", + "close_door_to_resume": "Close robot door to resume run", "closing_protocol": "Closing Protocol", - "comment_step": "Comment", "comment": "Comment", + "comment_step": "Comment", "complete_protocol_to_download": "Complete the protocol to download the run log", - "current_step_pause_timer": "Timer", - "current_step_pause": "Current Step - Paused by User", "current_step": "Current Step", + "current_step_pause": "Current Step - Paused by User", + "current_step_pause_timer": "Timer", "current_temperature": "Current: {{temperature}} °C", "custom_values": "Custom values", "data_out_of_date": "This data is likely out of date", @@ -37,31 +37,22 @@ "downloading_run_log": "Downloading run log", "drop_tip": "Dropping tip in {{well_name}} of {{labware}} in {{labware_location}}", "duration": "Duration", + "end": "End", "end_of_protocol": "End of protocol", "end_step_time": "End", - "end": "End", "error_info": "Error {{errorCode}}: {{errorType}}", "error_type": "Error: {{errorType}}", "failed_step": "Failed step", "final_step": "Final Step", "ignore_stored_data": "Ignore stored data", - "labware_offset_data": "labware offset data", "labware": "labware", + "labware_offset_data": "labware offset data", "left": "Left", "listed_values": "Listed values are view-only", - "load_labware_info_protocol_setup_adapter_module": "Load {{labware}} in {{adapter_name}} in {{module_name}} in Slot {{slot_name}}", - "load_labware_info_protocol_setup_adapter_off_deck": "Load {{labware}} in {{adapter_name}} off deck", - "load_labware_info_protocol_setup_adapter": "Load {{labware}} in {{adapter_name}} in Slot {{slot_name}}", - "load_labware_info_protocol_setup_no_module": "Load {{labware}} in Slot {{slot_name}}", - "load_labware_info_protocol_setup_off_deck": "Load {{labware}} off deck", "load_labware_info_protocol_setup_plural": "Load {{labware}} in {{module_name}}", - "load_labware_info_protocol_setup": "Load {{labware}} in {{module_name}} in Slot {{slot_name}}", - "load_liquids_info_protocol_setup": "Load {{liquid}} into {{labware}}", "load_module_protocol_setup_plural": "Load {{module}}", - "load_module_protocol_setup": "Load {{module}} in Slot {{slot_name}}", - "load_pipette_protocol_setup": "Load {{pipette_name}} in {{mount_name}} Mount", - "loading_protocol": "Loading Protocol", "loading_data": "Loading data...", + "loading_protocol": "Loading Protocol", "location": "location", "module_controls": "Module Controls", "module_slot_number": "Slot {{slot_number}}", @@ -74,9 +65,9 @@ "not_started_yet": "Not started yet", "off_deck": "Off deck", "parameters": "Parameters", + "pause": "Pause", "pause_protocol": "Pause protocol", "pause_run": "Pause run", - "pause": "Pause", "paused_for": "Paused For", "pickup_tip": "Picking up tip from {{well_name}} of {{labware}} in {{labware_location}}", "plus_more": "+{{count}} more", @@ -99,32 +90,35 @@ "right": "Right", "robot_has_previous_offsets": "This robot has stored Labware Offset data from previous protocol runs. Do you want to apply that data to this protocol run? You can still adjust any offsets with Labware Position Check.", "robot_was_recalibrated": "This robot was recalibrated after this Labware Offset data was stored.", + "run": "Run", "run_again": "Run again", - "run_canceled_splash": "Run canceled", "run_canceled": "Run canceled.", - "run_complete_splash": "Run completed", + "run_canceled_splash": "Run canceled", "run_complete": "Run completed", + "run_complete_splash": "Run completed", "run_completed": "Run completed.", "run_cta_disabled": "Complete required steps on Protocol tab before starting the run", + "run_failed": "Run failed.", "run_failed_modal_body": "Error occurred when protocol was {{command}}", "run_failed_modal_header": "{{errorName}}: {{errorCode}} at protocol step {{count}}", "run_failed_modal_title": "Run failed", "run_failed_splash": "Run failed", - "run_failed": "Run failed.", "run_has_diverged_from_predicted": "Run has diverged from predicted state. Cannot anticipate new steps.", "run_preview": "Run Preview", "run_protocol": "Run Protocol", "run_status": "Status: {{status}}", "run_time": "Run Time", - "run": "Run", - "setup_incomplete": "Complete required steps in Setup tab", "setup": "Setup", + "setup_incomplete": "Complete required steps in Setup tab", "slot": "Slot {{slotName}}", + "start": "Start", "start_run": "Start run", "start_step_time": "Start", "start_time": "Start Time", - "start": "Start", + "status": "Status", "status_awaiting-recovery": "Awaiting recovery", + "status_awaiting-recovery-blocked-by-open-door": "Paused - door open", + "status_awaiting-recovery-paused": "Paused", "status_blocked-by-open-door": "Paused - door open", "status_failed": "Failed", "status_finishing": "Finishing", @@ -134,7 +128,6 @@ "status_stop-requested": "Stop requested", "status_stopped": "Canceled", "status_succeeded": "Completed", - "status": "Status", "step": "Step", "step_failed": "Step failed", "step_number": "Step {{step_number}}:", @@ -144,11 +137,11 @@ "temperature_not_available": "{{temperature_type}}: n/a", "thermocycler_error_tooltip": "Module encountered an anomaly, please contact support", "total_elapsed_time": "Total elapsed time", - "total_step_count_plural": "{{count}} steps total", "total_step_count": "{{count}} step total", + "total_step_count_plural": "{{count}} steps total", "unable_to_determine_steps": "Unable to determine steps", "view_analysis_error_details": "View error details", "view_current_step": "View current step", - "view_error_details": "View error details", - "view_error": "View error" + "view_error": "View error", + "view_error_details": "View error details" } diff --git a/app/src/atoms/Toast/index.tsx b/app/src/atoms/Toast/index.tsx index 018f2942429..2b81514641d 100644 --- a/app/src/atoms/Toast/index.tsx +++ b/app/src/atoms/Toast/index.tsx @@ -356,7 +356,7 @@ export function Toast(props: ToastProps): JSX.Element { fontWeight={ showODDStyle ? TYPOGRAPHY.fontWeightBold - : TYPOGRAPHY.fontWeightRegular + : TYPOGRAPHY.fontWeightSemiBold } lineHeight={ showODDStyle ? TYPOGRAPHY.lineHeight28 : TYPOGRAPHY.lineHeight20 diff --git a/app/src/atoms/buttons/BackButton.tsx b/app/src/atoms/buttons/BackButton.tsx index 1395c03ab8f..05df2fd800b 100644 --- a/app/src/atoms/buttons/BackButton.tsx +++ b/app/src/atoms/buttons/BackButton.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { ALIGN_CENTER, @@ -16,7 +16,7 @@ export function BackButton({ onClick, children, }: React.HTMLProps): JSX.Element { - const history = useHistory() + const navigate = useNavigate() const { t } = useTranslation('shared') return ( @@ -28,7 +28,7 @@ export function BackButton({ onClick != null ? onClick : () => { - history.goBack() + navigate(-1) } } > diff --git a/app/src/atoms/buttons/RadioButton.tsx b/app/src/atoms/buttons/RadioButton.tsx index b176e330378..f22c19ef6e7 100644 --- a/app/src/atoms/buttons/RadioButton.tsx +++ b/app/src/atoms/buttons/RadioButton.tsx @@ -6,7 +6,7 @@ import { Flex, RESPONSIVENESS, SPACING, - LegacyStyledText, + StyledText, TYPOGRAPHY, } from '@opentrons/components' @@ -65,16 +65,17 @@ export function RadioButton(props: RadioButtonProps): JSX.Element { // TODO: (ew, 2023-04-21): button is not tabbable, so focus state // is not possible on ODD. It's testable in storybook but not in real life. const SettingButtonLabel = styled.label` - border-radius: ${BORDERS.borderRadius16}; - cursor: pointer; - padding: ${isLarge ? SPACING.spacing24 : SPACING.spacing20}; - width: 100%; + border-radius: ${BORDERS.borderRadius16}; + cursor: pointer; + padding: ${isLarge ? SPACING.spacing24 : SPACING.spacing20}; + width: 100%; - ${isSelected ? SELECTED_BUTTON_STYLE : AVAILABLE_BUTTON_STYLE} - ${disabled && DISABLED_BUTTON_STYLE} + ${isSelected ? SELECTED_BUTTON_STYLE : AVAILABLE_BUTTON_STYLE} + ${disabled && DISABLED_BUTTON_STYLE} @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { - cursor: default; + cursor: default; + } } ` @@ -89,19 +90,19 @@ export function RadioButton(props: RadioButtonProps): JSX.Element { value={buttonValue} /> - {buttonLabel} - + {subButtonLabel != null ? ( - + {subButtonLabel} - + ) : null} diff --git a/app/src/atoms/buttons/SmallButton.tsx b/app/src/atoms/buttons/SmallButton.tsx index 3402d59843a..25a494a1488 100644 --- a/app/src/atoms/buttons/SmallButton.tsx +++ b/app/src/atoms/buttons/SmallButton.tsx @@ -10,7 +10,7 @@ import { Icon, JUSTIFY_CENTER, SPACING, - LegacyStyledText, + StyledText, TYPOGRAPHY, } from '@opentrons/components' import { ODD_FOCUS_VISIBLE } from './constants' @@ -180,13 +180,12 @@ export function SmallButton(props: SmallButtonProps): JSX.Element { ) : null} - {buttonText} - + {iconPlacement === 'endIcon' && iconName != null ? ( void + buttonText: React.ReactNode + disabled?: boolean +} + +export function TextOnlyButton({ + onClick, + buttonText, + disabled = false, + ...styleProps +}: TextOnlyButtonProps): JSX.Element { + return ( + + + {buttonText} + + + ) +} diff --git a/app/src/atoms/buttons/__tests__/BackButton.test.tsx b/app/src/atoms/buttons/__tests__/BackButton.test.tsx index e1c2ae3e68f..7b1595d0b83 100644 --- a/app/src/atoms/buttons/__tests__/BackButton.test.tsx +++ b/app/src/atoms/buttons/__tests__/BackButton.test.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, vi } from 'vitest' import '@testing-library/jest-dom/vitest' -import { MemoryRouter, Route, Switch } from 'react-router-dom' +import { MemoryRouter, Route, Routes } from 'react-router-dom' import { renderWithProviders } from '../../../__testing-utils__' @@ -16,14 +16,10 @@ const render = (props?: React.HTMLProps) => { initialIndex={1} > - - - this is the current page - - - this is the previous page - - + + this is the current page} /> + this is the previous page} /> + , { i18nInstance: i18n } )[0] @@ -49,7 +45,7 @@ describe('BackButton', () => { expect(screen.queryByText('this is the previous page')).toBeNull() }) - it('goes back one page in history on click if no on click handler provided', () => { + it('goes back one page in navigate on click if no on click handler provided', () => { render() screen.getByText('this is the current page') diff --git a/app/src/atoms/buttons/buttons.stories.tsx b/app/src/atoms/buttons/buttons.stories.tsx index bc1c30a3614..ac730f2cd86 100644 --- a/app/src/atoms/buttons/buttons.stories.tsx +++ b/app/src/atoms/buttons/buttons.stories.tsx @@ -13,6 +13,7 @@ import { QuaternaryButton, SubmitPrimaryButton, ToggleButton, + TextOnlyButton, } from './index' import type { Story, Meta } from '@storybook/react' @@ -132,3 +133,19 @@ export const LongPress = LongPressButtonTemplate.bind({}) LongPress.args = { children: 'long press - 2sec / tap', } + +const TextOnlyButtonTemplate: Story< + React.ComponentProps +> = () => { + const [count, setCount] = React.useState(0) + return ( + { + setCount(prev => prev + 1) + }} + buttonText={`You clicked me ${count} times`} + /> + ) +} + +export const TextOnly = TextOnlyButtonTemplate.bind({}) diff --git a/app/src/atoms/buttons/index.ts b/app/src/atoms/buttons/index.ts index 00c18fc07b7..e5d63c0c767 100644 --- a/app/src/atoms/buttons/index.ts +++ b/app/src/atoms/buttons/index.ts @@ -8,3 +8,4 @@ export { SmallButton } from './SmallButton' export { SubmitPrimaryButton } from './SubmitPrimaryButton' export { TertiaryButton } from './TertiaryButton' export { ToggleButton } from './ToggleButton' +export { TextOnlyButton } from './TextOnlyButton' diff --git a/app/src/index.tsx b/app/src/index.tsx index e37435c9aba..b8fe832abdc 100644 --- a/app/src/index.tsx +++ b/app/src/index.tsx @@ -2,15 +2,13 @@ import React from 'react' import ReactDom from 'react-dom/client' import { Provider } from 'react-redux' - -import { ConnectedRouter } from 'connected-react-router' +import { BrowserRouter } from 'react-router-dom' import { ApiClientProvider } from '@opentrons/react-api-client' import { createLogger } from './logger' import { uiInitialized } from './redux/shell' -import { history } from './redux/reducer' import { store } from './redux/store' import '../src/atoms/SoftwareKeyboard/AlphanumericKeyboard' @@ -34,10 +32,10 @@ if (container == null) throw new Error('Failed to find the root element') const root = ReactDom.createRoot(container) root.render( - + - + ) diff --git a/app/src/molecules/BackgroundOverlay/index.tsx b/app/src/molecules/BackgroundOverlay/index.tsx index fcc8956e423..ccfdb273fc4 100644 --- a/app/src/molecules/BackgroundOverlay/index.tsx +++ b/app/src/molecules/BackgroundOverlay/index.tsx @@ -6,7 +6,7 @@ import { COLORS, Flex, POSITION_FIXED } from '@opentrons/components' const BACKGROUND_OVERLAY_STYLE = css` position: ${POSITION_FIXED}; inset: 0; - z-index: 3; + z-index: 4; background-color: ${COLORS.black90}${COLORS.opacity60HexCode}; ` diff --git a/app/src/molecules/CardButton/__tests__/CardButton.test.tsx b/app/src/molecules/CardButton/__tests__/CardButton.test.tsx index 80d17c32d08..04a841ccc23 100644 --- a/app/src/molecules/CardButton/__tests__/CardButton.test.tsx +++ b/app/src/molecules/CardButton/__tests__/CardButton.test.tsx @@ -7,15 +7,15 @@ import { COLORS } from '@opentrons/components' import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' import { CardButton } from '..' -import type * as ReactRouterDom from 'react-router-dom' +import type { NavigateFunction } from 'react-router-dom' -const mockPush = vi.fn() +const mockNavigate = vi.fn() vi.mock('react-router-dom', async importOriginal => { - const reactRouterDom = await importOriginal() + const reactRouterDom = await importOriginal() return { ...reactRouterDom, - useHistory: () => ({ push: mockPush } as any), + useNavigate: () => mockNavigate, } }) @@ -65,6 +65,6 @@ describe('CardButton', () => { render(props) const button = screen.getByRole('button') fireEvent.click(button) - expect(mockPush).toHaveBeenCalledWith('/mockPath') + expect(mockNavigate).toHaveBeenCalledWith('/mockPath') }) }) diff --git a/app/src/molecules/CardButton/index.tsx b/app/src/molecules/CardButton/index.tsx index a07753c240d..1181985f772 100644 --- a/app/src/molecules/CardButton/index.tsx +++ b/app/src/molecules/CardButton/index.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { css } from 'styled-components' import { ALIGN_CENTER, @@ -76,12 +76,12 @@ interface CardButtonProps { export function CardButton(props: CardButtonProps): JSX.Element { const { title, iconName, description, destinationPath, disabled } = props - const history = useHistory() + const navigate = useNavigate() return ( { - history.push(destinationPath) + navigate(destinationPath) }} width="100%" css={CARD_BUTTON_STYLE} diff --git a/app/src/molecules/Command/Command.tsx b/app/src/molecules/Command/Command.tsx index b1c2c935eb7..fb8452f2a92 100644 --- a/app/src/molecules/Command/Command.tsx +++ b/app/src/molecules/Command/Command.tsx @@ -164,15 +164,11 @@ export function CenteredCommand( @@ -224,15 +220,11 @@ export function LeftAlignedCommand( @@ -242,14 +234,17 @@ export function LeftAlignedCommand( const TEXT_CLIP_STYLE = ` display: -webkit-box; - -webkit-box-orient: vertical; - overflow: hidden; - text-overflow: ellipsis; - word-wrap: break-word; - -webkit-line-clamp: 2; -} + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + word-wrap: break-word; + -webkit-line-clamp: 2; ` const ODD_ONLY_TEXT_CLIP_STYLE = ` + @media not (${RESPONSIVENESS.touchscreenMediaQuerySpecs}) { + max-height: 240px; + overflow: auto; + } @media (${RESPONSIVENESS.touchscreenMediaQuerySpecs}) { ${TEXT_CLIP_STYLE} } diff --git a/app/src/molecules/Command/CommandText.tsx b/app/src/molecules/Command/CommandText.tsx index a227b1bf74f..75b12733eca 100644 --- a/app/src/molecules/Command/CommandText.tsx +++ b/app/src/molecules/Command/CommandText.tsx @@ -1,6 +1,6 @@ import * as React from 'react' -import { useTranslation } from 'react-i18next' import { pick } from 'lodash' + import { ALIGN_CENTER, DIRECTION_COLUMN, @@ -9,47 +9,13 @@ import { LegacyStyledText, StyledText, RESPONSIVENESS, - styleProps, } from '@opentrons/components' -import { getPipetteNameSpecs } from '@opentrons/shared-data' -import { - getAddressableAreaDisplayName, - getLabwareName, - getLabwareDisplayLocation, - getFinalLabwareLocation, -} from './utils' -import { LoadCommandText } from './LoadCommandText' -import { PipettingCommandText } from './PipettingCommandText' -import { TemperatureCommandText } from './TemperatureCommandText' -import { MoveLabwareCommandText } from './MoveLabwareCommandText' + +import { useCommandTextString } from './hooks' import type { RobotType, RunTimeCommand } from '@opentrons/shared-data' import type { StyleProps } from '@opentrons/components' import type { CommandTextData } from './types' -import type { TFunction } from 'i18next' - -const SIMPLE_TRANSLATION_KEY_BY_COMMAND_TYPE: { - [commandType in RunTimeCommand['commandType']]?: string -} = { - home: 'home_gantry', - savePosition: 'save_position', - touchTip: 'touch_tip', - 'magneticModule/engage': 'engaging_magnetic_module', - 'magneticModule/disengage': 'disengaging_magnetic_module', - 'temperatureModule/deactivate': 'deactivate_temperature_module', - 'thermocycler/waitForBlockTemperature': 'waiting_for_tc_block_to_reach', - 'thermocycler/waitForLidTemperature': 'waiting_for_tc_lid_to_reach', - 'thermocycler/openLid': 'opening_tc_lid', - 'thermocycler/closeLid': 'closing_tc_lid', - 'thermocycler/deactivateBlock': 'deactivating_tc_block', - 'thermocycler/deactivateLid': 'deactivating_tc_lid', - 'thermocycler/awaitProfileComplete': 'tc_awaiting_for_duration', - 'heaterShaker/deactivateHeater': 'deactivating_hs_heater', - 'heaterShaker/openLabwareLatch': 'unlatching_hs_latch', - 'heaterShaker/closeLabwareLatch': 'latching_hs_latch', - 'heaterShaker/deactivateShaker': 'deactivate_hs_shake', - 'heaterShaker/waitForTemperature': 'waiting_for_hs_to_reach', -} interface LegacySTProps { as?: React.ComponentProps['as'] @@ -64,7 +30,7 @@ interface ModernSTProps { type STProps = LegacySTProps | ModernSTProps -interface Props extends StyleProps { +interface BaseProps extends StyleProps { command: RunTimeCommand commandTextData: CommandTextData robotType: RobotType @@ -72,372 +38,23 @@ interface Props extends StyleProps { propagateCenter?: boolean propagateTextLimit?: boolean } -export function CommandText(props: Props & STProps): JSX.Element | null { - const { - command, - commandTextData, - robotType, - propagateCenter = false, - propagateTextLimit = false, - ...styleProps - } = props - const { t } = useTranslation('protocol_command_text') - const shouldPropagateCenter = props.isOnDevice === true || propagateCenter - const shouldPropagateTextLimit = - props.isOnDevice === true || propagateTextLimit +export function CommandText(props: BaseProps & STProps): JSX.Element | null { + const { commandText, stepTexts } = useCommandTextString({ + ...props, + }) - switch (command.commandType) { - case 'aspirate': - case 'aspirateInPlace': - case 'dispense': - case 'dispenseInPlace': - case 'blowout': - case 'blowOutInPlace': - case 'dropTip': - case 'dropTipInPlace': - case 'pickUpTip': { - return ( - - - - ) - } - case 'loadLabware': - case 'loadPipette': - case 'loadModule': - case 'loadLiquid': { - return ( - - - - ) - } - case 'temperatureModule/setTargetTemperature': - case 'temperatureModule/waitForTemperature': - case 'thermocycler/setTargetBlockTemperature': - case 'thermocycler/setTargetLidTemperature': - case 'heaterShaker/setTargetTemperature': { - return ( - - - - ) - } + switch (props.command.commandType) { case 'thermocycler/runProfile': { - const { profile } = command.params - const steps = profile.map( - ({ holdSeconds, celsius }: { holdSeconds: number; celsius: number }) => - t('tc_run_profile_steps', { - celsius, - seconds: holdSeconds, - }).trim() - ) - return ( - // TODO(sfoster): Command sometimes wraps this in a cascaded display: -webkit-box - // to achieve multiline text clipping with an automatically inserted ellipsis, which works - // everywhere except for here where it overrides this property in the flex since this is - // the only place where CommandText uses a flex. - // The right way to handle this is probably to take the css that's in Command and make it - // live here instead, but that should be done in a followup since it would touch everything. - // See also the margin-left on the
  • s, which is needed to prevent their bullets from - // clipping if a container set overflow: hidden. - - - {t('tc_starting_profile', { - repetitions: Object.keys(steps).length, - })} - - -
      - {shouldPropagateTextLimit ? ( -
    • - {steps[0]} -
    • - ) : ( - steps.map((step: string, index: number) => ( -
    • - {' '} - {step} -
    • - )) - )} -
    -
    -
    - ) - } - case 'heaterShaker/setAndWaitForShakeSpeed': { - const { rpm } = command.params - return ( - - {t('set_and_await_hs_shake', { rpm })} - - ) - } - case 'moveToSlot': { - const { slotName } = command.params - return ( - - {t('move_to_slot', { slot_name: slotName })} - - ) - } - case 'moveRelative': { - const { axis, distance } = command.params - return ( - - {t('move_relative', { axis, distance })} - - ) - } - case 'moveToCoordinates': { - const { coordinates } = command.params - return ( - - {t('move_to_coordinates', coordinates)} - - ) - } - case 'moveToWell': { - const { wellName, labwareId } = command.params - const allPreviousCommands = commandTextData.commands.slice( - 0, - commandTextData.commands.findIndex(c => c.id === command.id) - ) - const labwareLocation = getFinalLabwareLocation( - labwareId, - allPreviousCommands - ) - const displayLocation = - labwareLocation != null - ? getLabwareDisplayLocation( - commandTextData, - labwareLocation, - t as TFunction, - robotType - ) - : '' - return ( - - {t('move_to_well', { - well_name: wellName, - labware: getLabwareName(commandTextData, labwareId), - labware_location: displayLocation, - })} - - ) - } - case 'moveLabware': { - return ( - - - - ) - } - case 'configureForVolume': { - const { volume, pipetteId } = command.params - const pipetteName = commandTextData.pipettes.find( - pip => pip.id === pipetteId - )?.pipetteName - - return ( - - {t('configure_for_volume', { - volume, - pipette: - pipetteName != null - ? getPipetteNameSpecs(pipetteName)?.displayName - : '', - })} - - ) - } - case 'configureNozzleLayout': { - const { configurationParams, pipetteId } = command.params - const pipetteName = commandTextData.pipettes.find( - pip => pip.id === pipetteId - )?.pipetteName - - // TODO (sb, 11/9/23): Add support for other configurations when needed - return ( - - {t('configure_nozzle_layout', { - amount: configurationParams.style === 'COLUMN' ? '8' : 'all', - pipette: - pipetteName != null - ? getPipetteNameSpecs(pipetteName)?.displayName - : '', - })} - - ) - } - case 'prepareToAspirate': { - const { pipetteId } = command.params - const pipetteName = commandTextData.pipettes.find( - pip => pip.id === pipetteId - )?.pipetteName - - return ( - - {t('prepare_to_aspirate', { - pipette: - pipetteName != null - ? getPipetteNameSpecs(pipetteName)?.displayName - : '', - })} - - ) - } - case 'moveToAddressableArea': { - const addressableAreaDisplayName = getAddressableAreaDisplayName( - commandTextData, - command.id, - t as TFunction - ) - - return ( - - {t('move_to_addressable_area', { - addressable_area: addressableAreaDisplayName, - })} - - ) - } - case 'moveToAddressableAreaForDropTip': { - const addressableAreaDisplayName = getAddressableAreaDisplayName( - commandTextData, - command.id, - t as TFunction - ) - return ( - - {t('move_to_addressable_area_drop_tip', { - addressable_area: addressableAreaDisplayName, - })} - - ) - } - case 'touchTip': - case 'home': - case 'savePosition': - case 'magneticModule/engage': - case 'magneticModule/disengage': - case 'temperatureModule/deactivate': - case 'thermocycler/waitForBlockTemperature': - case 'thermocycler/waitForLidTemperature': - case 'thermocycler/openLid': - case 'thermocycler/closeLid': - case 'thermocycler/deactivateBlock': - case 'thermocycler/deactivateLid': - case 'thermocycler/awaitProfileComplete': - case 'heaterShaker/deactivateHeater': - case 'heaterShaker/openLabwareLatch': - case 'heaterShaker/closeLabwareLatch': - case 'heaterShaker/deactivateShaker': - case 'heaterShaker/waitForTemperature': { - const simpleTKey = - SIMPLE_TRANSLATION_KEY_BY_COMMAND_TYPE[command.commandType] - return ( - - {simpleTKey != null ? t(simpleTKey) : null} - - ) - } - case 'waitForDuration': { - const { seconds, message } = command.params - return ( - - {t('wait_for_duration', { seconds, message: message ?? '' })} - - ) - } - case 'pause': // legacy pause command - case 'waitForResume': { - return ( - - {command.params?.message != null && command.params.message !== '' - ? command.params.message - : t('wait_for_resume')} - - ) - } - case 'delay': { - // legacy delay command - const { message = '' } = command.params - if ('waitForResume' in command.params) { - return ( - - {command.params?.message != null && command.params.message !== '' - ? command.params.message - : t('wait_for_resume')} - - ) - } else { - return ( - - {t('wait_for_duration', { - seconds: command.params.seconds, - message, - })} - - ) - } - } - case 'comment': { - const { message } = command.params - return {message} - } - case 'custom': { - const { legacyCommandText } = command.params ?? {} - const sanitizedCommandText = - typeof legacyCommandText === 'object' - ? JSON.stringify(legacyCommandText) - : String(legacyCommandText) return ( - - {legacyCommandText != null - ? sanitizedCommandText - : `${command.commandType}: ${JSON.stringify(command.params)}`} - + ) } default: { - console.warn( - 'CommandText encountered a command with an unrecognized commandType: ', - command - ) - return ( - - {JSON.stringify(command)} - - ) + return {commandText} } } } @@ -460,16 +77,95 @@ function CommandStyledText( {props.children} ) } else { return ( - + {props.children} ) } } + +type ThermocyclerRunProfileProps = BaseProps & + STProps & { + commandText: string + stepTexts?: string[] + } + +function ThermocyclerRunProfile( + props: ThermocyclerRunProfileProps +): JSX.Element { + const { + isOnDevice, + propagateCenter = false, + propagateTextLimit = false, + commandText, + stepTexts, + ...styleProps + } = props + + const shouldPropagateCenter = isOnDevice === true || propagateCenter + const shouldPropagateTextLimit = isOnDevice === true || propagateTextLimit + + // TODO(sfoster): Command sometimes wraps this in a cascaded display: -webkit-box + // to achieve multiline text clipping with an automatically inserted ellipsis, which works + // everywhere except for here where it overrides this property in the flex since this is + // the only place where CommandText uses a flex. + // The right way to handle this is probably to take the css that's in Command and make it + // live here instead, but that should be done in a followup since it would touch everything. + // See also the margin-left on the
  • s, which is needed to prevent their bullets from + // clipping if a container set overflow: hidden. + return ( + + + {commandText} + + +
      + {shouldPropagateTextLimit ? ( +
    • + {stepTexts?.[0]} +
    • + ) : ( + stepTexts?.map((step: string, index: number) => ( +
    • + {' '} + {step} +
    • + )) + )} +
    +
    +
    + ) +} diff --git a/app/src/molecules/Command/MoveLabwareCommandText.tsx b/app/src/molecules/Command/MoveLabwareCommandText.tsx deleted file mode 100644 index 030177fddfc..00000000000 --- a/app/src/molecules/Command/MoveLabwareCommandText.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { useTranslation } from 'react-i18next' -import { GRIPPER_WASTE_CHUTE_ADDRESSABLE_AREA } from '@opentrons/shared-data' -import { getLabwareName } from './utils' -import { getLabwareDisplayLocation } from './utils/getLabwareDisplayLocation' -import { getFinalLabwareLocation } from './utils/getFinalLabwareLocation' -import type { - MoveLabwareRunTimeCommand, - RobotType, -} from '@opentrons/shared-data' -import type { CommandTextData } from './types' -import type { TFunction } from 'i18next' - -interface MoveLabwareCommandTextProps { - command: MoveLabwareRunTimeCommand - commandTextData: CommandTextData - robotType: RobotType -} -export function MoveLabwareCommandText( - props: MoveLabwareCommandTextProps -): JSX.Element { - const { t } = useTranslation('protocol_command_text') - const { command, commandTextData, robotType } = props - const { labwareId, newLocation, strategy } = command.params - - const allPreviousCommands = commandTextData.commands.slice( - 0, - commandTextData.commands.findIndex(c => c.id === command.id) - ) - const oldLocation = getFinalLabwareLocation(labwareId, allPreviousCommands) - const newDisplayLocation = getLabwareDisplayLocation( - commandTextData, - newLocation, - t as TFunction, - robotType - ) - - const location = newDisplayLocation.includes( - GRIPPER_WASTE_CHUTE_ADDRESSABLE_AREA - ) - ? 'Waste Chute' - : newDisplayLocation - - return strategy === 'usingGripper' - ? t('move_labware_using_gripper', { - labware: getLabwareName(commandTextData, labwareId), - old_location: - oldLocation != null - ? getLabwareDisplayLocation( - commandTextData, - oldLocation, - t as TFunction, - robotType - ) - : '', - new_location: location, - }) - : t('move_labware_manually', { - labware: getLabwareName(commandTextData, labwareId), - old_location: - oldLocation != null - ? getLabwareDisplayLocation( - commandTextData, - oldLocation, - t as TFunction, - robotType - ) - : '', - new_location: location, - }) -} diff --git a/app/src/molecules/Command/__fixtures__/mockRobotSideAnalysis.json b/app/src/molecules/Command/__fixtures__/mockRobotSideAnalysis.json index 0f084ae89b6..b4a041b36f9 100644 --- a/app/src/molecules/Command/__fixtures__/mockRobotSideAnalysis.json +++ b/app/src/molecules/Command/__fixtures__/mockRobotSideAnalysis.json @@ -6391,6 +6391,46 @@ "addressableAreaName": "D3", "offset": { "x": 0, "y": 0, "z": 0 } } + }, + { + "id": "84f7af1d-c097-4d4b-9819-ad56479bbbb8", + "createdAt": "2023-01-31T21:53:04.965216+00:00", + "commandType": "liquidProbe", + "key": "1248111104", + "status": "succeeded", + "params": { + "labwareId": "b2a40c9d-31b0-4f27-ad4a-c92ced91204d", + "wellName": "A1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": 0 + } + }, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + } + }, + { + "id": "84f7af1d-c097-4d4b-9819-ad56479bbbb8", + "createdAt": "2023-01-31T21:53:04.965216+00:00", + "commandType": "tryLiquidProbe", + "key": "1248111104", + "status": "succeeded", + "params": { + "labwareId": "b2a40c9d-31b0-4f27-ad4a-c92ced91204d", + "wellName": "A1", + "wellLocation": { + "origin": "top", + "offset": { + "x": 0, + "y": 0, + "z": 0 + } + }, + "pipetteId": "f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb" + } } ], "errors": [], diff --git a/app/src/molecules/Command/__tests__/CommandText.test.tsx b/app/src/molecules/Command/__tests__/CommandText.test.tsx index 132105ecc44..9226f258830 100644 --- a/app/src/molecules/Command/__tests__/CommandText.test.tsx +++ b/app/src/molecules/Command/__tests__/CommandText.test.tsx @@ -1401,4 +1401,44 @@ describe('CommandText', () => { 'Moving NEST 96 Well Plate 100 µL PCR Full Skirt (1) using gripper from Magnetic Module GEN2 in Slot 1 to Magnetic Module GEN2 in Slot 1' ) }) + + it('renders correct text for liquidProbe', () => { + const command = mockCommandTextData.commands.find( + c => c.commandType === 'liquidProbe' + ) + expect(command).not.toBeUndefined() + if (command != null) { + renderWithProviders( + , + { i18nInstance: i18n } + ) + screen.getByText( + 'Detecting liquid presence in well A1 of Opentrons 96 Tip Rack 300 µL in Slot 9' + ) + } + }) + + it('renders correct text for tryLiquidProbe', () => { + const command = mockCommandTextData.commands.find( + c => c.commandType === 'tryLiquidProbe' + ) + expect(command).not.toBeUndefined() + if (command != null) { + renderWithProviders( + , + { i18nInstance: i18n } + ) + screen.getByText( + 'Detecting liquid presence in well A1 of Opentrons 96 Tip Rack 300 µL in Slot 9' + ) + } + }) }) diff --git a/app/src/molecules/Command/hooks/index.ts b/app/src/molecules/Command/hooks/index.ts new file mode 100644 index 00000000000..6b6545c7689 --- /dev/null +++ b/app/src/molecules/Command/hooks/index.ts @@ -0,0 +1,7 @@ +export { useCommandTextString } from './useCommandTextString' + +export type { + UseCommandTextStringParams, + GetCommandText, + GetCommandTextResult, +} from './useCommandTextString' diff --git a/app/src/molecules/Command/hooks/useCommandTextString/index.tsx b/app/src/molecules/Command/hooks/useCommandTextString/index.tsx new file mode 100644 index 00000000000..34df2f33c7f --- /dev/null +++ b/app/src/molecules/Command/hooks/useCommandTextString/index.tsx @@ -0,0 +1,231 @@ +import { useTranslation } from 'react-i18next' +import * as utils from './utils' + +import type { TFunction } from 'i18next' +import type { RunTimeCommand, RobotType } from '@opentrons/shared-data' +import type { CommandTextData } from '../../types' +import type { GetDirectTranslationCommandText } from './utils/getDirectTranslationCommandText' + +export interface UseCommandTextStringParams { + command: RunTimeCommand | null + commandTextData: CommandTextData | null + robotType: RobotType +} + +export type GetCommandText = UseCommandTextStringParams & { t: TFunction } +export interface GetCommandTextResult { + /* The actual command text. Ex "Homing all gantry, pipette, and plunger axes" */ + commandText: string + /* The TC run profile steps. */ + stepTexts?: string[] +} + +// TODO(jh, 07-18-24): Move the testing that covers this from CommandText to a new file, and verify that all commands are +// properly tested. + +// Get the full user-facing command text string from a given command. +export function useCommandTextString( + params: UseCommandTextStringParams +): GetCommandTextResult { + const { command } = params + const { t } = useTranslation('protocol_command_text') + + const fullParams = { ...params, t } + + switch (command?.commandType) { + case 'touchTip': + case 'home': + case 'savePosition': + case 'magneticModule/engage': + case 'magneticModule/disengage': + case 'temperatureModule/deactivate': + case 'thermocycler/waitForBlockTemperature': + case 'thermocycler/waitForLidTemperature': + case 'thermocycler/openLid': + case 'thermocycler/closeLid': + case 'thermocycler/deactivateBlock': + case 'thermocycler/deactivateLid': + case 'thermocycler/awaitProfileComplete': + case 'heaterShaker/deactivateHeater': + case 'heaterShaker/openLabwareLatch': + case 'heaterShaker/closeLabwareLatch': + case 'heaterShaker/deactivateShaker': + case 'heaterShaker/waitForTemperature': + return { + commandText: utils.getDirectTranslationCommandText( + fullParams as GetDirectTranslationCommandText + ), + } + + case 'aspirate': + case 'aspirateInPlace': + case 'dispense': + case 'dispenseInPlace': + case 'blowout': + case 'blowOutInPlace': + case 'dropTip': + case 'dropTipInPlace': + case 'pickUpTip': + return { + commandText: utils.getPipettingCommandText(fullParams), + } + + case 'loadLabware': + case 'loadPipette': + case 'loadModule': + case 'loadLiquid': + return { + commandText: utils.getLoadCommandText(fullParams), + } + + case 'liquidProbe': + case 'tryLiquidProbe': + return { + commandText: utils.getLiquidProbeCommandText({ + ...fullParams, + command, + }), + } + + case 'temperatureModule/setTargetTemperature': + case 'temperatureModule/waitForTemperature': + case 'thermocycler/setTargetBlockTemperature': + case 'thermocycler/setTargetLidTemperature': + case 'heaterShaker/setTargetTemperature': + return { + commandText: utils.getTemperatureCommandText({ + ...fullParams, + command, + }), + } + + case 'thermocycler/runProfile': + return utils.getTCRunProfileCommandText({ ...fullParams, command }) + + case 'heaterShaker/setAndWaitForShakeSpeed': + return { + commandText: utils.getHSShakeSpeedCommandText({ + ...fullParams, + command, + }), + } + + case 'moveToSlot': + return { + commandText: utils.getMoveToSlotCommandText({ ...fullParams, command }), + } + + case 'moveRelative': + return { + commandText: utils.getMoveRelativeCommandText({ + ...fullParams, + command, + }), + } + + case 'moveToCoordinates': + return { + commandText: utils.getMoveToCoordinatesCommandText({ + ...fullParams, + command, + }), + } + + case 'moveToWell': + return { + commandText: utils.getMoveToWellCommandText({ ...fullParams, command }), + } + + case 'moveLabware': + return { + commandText: utils.getMoveLabwareCommandText({ + ...fullParams, + command, + }), + } + + case 'configureForVolume': + return { + commandText: utils.getConfigureForVolumeCommandText({ + ...fullParams, + command, + }), + } + + case 'configureNozzleLayout': + return { + commandText: utils.getConfigureNozzleLayoutCommandText({ + ...fullParams, + command, + }), + } + + case 'prepareToAspirate': + return { + commandText: utils.getPrepareToAspirateCommandText({ + ...fullParams, + command, + }), + } + + case 'moveToAddressableArea': + return { + commandText: utils.getMoveToAddressableAreaCommandText({ + ...fullParams, + command, + }), + } + + case 'moveToAddressableAreaForDropTip': + return { + commandText: utils.getMoveToAddressableAreaForDropTipCommandText({ + ...fullParams, + command, + }), + } + + case 'waitForDuration': + return { + commandText: utils.getWaitForDurationCommandText({ + ...fullParams, + command, + }), + } + + case 'pause': // legacy pause command + case 'waitForResume': + return { + commandText: utils.getWaitForResumeCommandText({ + ...fullParams, + command, + }), + } + + case 'delay': + return { + commandText: utils.getDelayCommandText({ ...fullParams, command }), + } + + case 'comment': + return { + commandText: utils.getCommentCommandText({ ...fullParams, command }), + } + + case 'custom': + return { + commandText: utils.getCustomCommandText({ ...fullParams, command }), + } + + case null: + return { commandText: '' } + + default: + console.warn( + 'CommandText encountered a command with an unrecognized commandType: ', + command + ) + return { + commandText: utils.getUnknownCommandText({ ...fullParams, command }), + } + } +} diff --git a/app/src/molecules/Command/hooks/useCommandTextString/utils/getCommentCommandText.ts b/app/src/molecules/Command/hooks/useCommandTextString/utils/getCommentCommandText.ts new file mode 100644 index 00000000000..3a1b7ce7e8a --- /dev/null +++ b/app/src/molecules/Command/hooks/useCommandTextString/utils/getCommentCommandText.ts @@ -0,0 +1,10 @@ +import type { CommentRunTimeCommand } from '@opentrons/shared-data/command' +import type { HandlesCommands } from './types' + +export function getCommentCommandText({ + command, +}: HandlesCommands): string { + const { message } = command.params + + return message +} diff --git a/app/src/molecules/Command/hooks/useCommandTextString/utils/getConfigureForVolumeCommandText.ts b/app/src/molecules/Command/hooks/useCommandTextString/utils/getConfigureForVolumeCommandText.ts new file mode 100644 index 00000000000..1a4ee2e7c0e --- /dev/null +++ b/app/src/molecules/Command/hooks/useCommandTextString/utils/getConfigureForVolumeCommandText.ts @@ -0,0 +1,21 @@ +import { getPipetteSpecsV2 } from '@opentrons/shared-data' + +import type { ConfigureForVolumeRunTimeCommand } from '@opentrons/shared-data/command' +import type { HandlesCommands } from './types' + +export function getConfigureForVolumeCommandText({ + command, + commandTextData, + t, +}: HandlesCommands): string { + const { volume, pipetteId } = command.params + const pipetteName = commandTextData?.pipettes.find( + pip => pip.id === pipetteId + )?.pipetteName + + return t('configure_for_volume', { + volume, + pipette: + pipetteName != null ? getPipetteSpecsV2(pipetteName)?.displayName : '', + }) +} diff --git a/app/src/molecules/Command/hooks/useCommandTextString/utils/getConfigureNozzleLayoutCommandText.ts b/app/src/molecules/Command/hooks/useCommandTextString/utils/getConfigureNozzleLayoutCommandText.ts new file mode 100644 index 00000000000..e6693a4b937 --- /dev/null +++ b/app/src/molecules/Command/hooks/useCommandTextString/utils/getConfigureNozzleLayoutCommandText.ts @@ -0,0 +1,21 @@ +import { getPipetteSpecsV2 } from '@opentrons/shared-data' + +import type { ConfigureNozzleLayoutRunTimeCommand } from '@opentrons/shared-data' +import type { HandlesCommands } from './types' + +export function getConfigureNozzleLayoutCommandText({ + command, + commandTextData, + t, +}: HandlesCommands): string { + const { configurationParams, pipetteId } = command.params + const pipetteName = commandTextData?.pipettes.find( + pip => pip.id === pipetteId + )?.pipetteName + + return t('configure_nozzle_layout', { + amount: configurationParams.style === 'COLUMN' ? '8' : 'all', + pipette: + pipetteName != null ? getPipetteSpecsV2(pipetteName)?.displayName : '', + }) +} diff --git a/app/src/molecules/Command/hooks/useCommandTextString/utils/getCustomCommandText.ts b/app/src/molecules/Command/hooks/useCommandTextString/utils/getCustomCommandText.ts new file mode 100644 index 00000000000..da6d5a1d506 --- /dev/null +++ b/app/src/molecules/Command/hooks/useCommandTextString/utils/getCustomCommandText.ts @@ -0,0 +1,16 @@ +import type { CustomRunTimeCommand } from '@opentrons/shared-data/command' +import type { HandlesCommands } from './types' + +export function getCustomCommandText({ + command, +}: HandlesCommands): string { + const { legacyCommandText } = command.params ?? {} + const sanitizedCommandText = + typeof legacyCommandText === 'object' + ? JSON.stringify(legacyCommandText) + : String(legacyCommandText) + + return legacyCommandText != null + ? sanitizedCommandText + : `${command.commandType}: ${JSON.stringify(command.params)}` +} diff --git a/app/src/molecules/Command/hooks/useCommandTextString/utils/getDelayCommandText.ts b/app/src/molecules/Command/hooks/useCommandTextString/utils/getDelayCommandText.ts new file mode 100644 index 00000000000..8bb24d99661 --- /dev/null +++ b/app/src/molecules/Command/hooks/useCommandTextString/utils/getDelayCommandText.ts @@ -0,0 +1,20 @@ +import type { DeprecatedDelayRunTimeCommand } from '@opentrons/shared-data/command' +import type { HandlesCommands } from './types' + +export function getDelayCommandText({ + command, + t, +}: HandlesCommands): string { + const { message = '' } = command.params + + if ('waitForResume' in command.params) { + return command.params?.message != null && command.params.message !== '' + ? command.params.message + : t('wait_for_resume') + } else { + return t('wait_for_duration', { + seconds: command.params.seconds, + message, + }) + } +} diff --git a/app/src/molecules/Command/hooks/useCommandTextString/utils/getDirectTranslationCommandText.ts b/app/src/molecules/Command/hooks/useCommandTextString/utils/getDirectTranslationCommandText.ts new file mode 100644 index 00000000000..fd586136e90 --- /dev/null +++ b/app/src/molecules/Command/hooks/useCommandTextString/utils/getDirectTranslationCommandText.ts @@ -0,0 +1,44 @@ +import type { RunTimeCommand } from '@opentrons/shared-data/command' +import type { HandlesCommands } from './types' + +const SIMPLE_TRANSLATION_KEY_BY_COMMAND_TYPE: { + [commandType in RunTimeCommand['commandType']]?: string +} = { + home: 'home_gantry', + savePosition: 'save_position', + touchTip: 'touch_tip', + 'magneticModule/engage': 'engaging_magnetic_module', + 'magneticModule/disengage': 'disengaging_magnetic_module', + 'temperatureModule/deactivate': 'deactivate_temperature_module', + 'thermocycler/waitForBlockTemperature': 'waiting_for_tc_block_to_reach', + 'thermocycler/waitForLidTemperature': 'waiting_for_tc_lid_to_reach', + 'thermocycler/openLid': 'opening_tc_lid', + 'thermocycler/closeLid': 'closing_tc_lid', + 'thermocycler/deactivateBlock': 'deactivating_tc_block', + 'thermocycler/deactivateLid': 'deactivating_tc_lid', + 'thermocycler/awaitProfileComplete': 'tc_awaiting_for_duration', + 'heaterShaker/deactivateHeater': 'deactivating_hs_heater', + 'heaterShaker/openLabwareLatch': 'unlatching_hs_latch', + 'heaterShaker/closeLabwareLatch': 'latching_hs_latch', + 'heaterShaker/deactivateShaker': 'deactivate_hs_shake', + 'heaterShaker/waitForTemperature': 'waiting_for_hs_to_reach', +} + +type HandledCommands = Extract< + RunTimeCommand, + { commandType: keyof typeof SIMPLE_TRANSLATION_KEY_BY_COMMAND_TYPE } +> + +export type GetDirectTranslationCommandText = HandlesCommands + +export function getDirectTranslationCommandText({ + command, + t, +}: GetDirectTranslationCommandText): string { + const simpleTKey = + command != null + ? SIMPLE_TRANSLATION_KEY_BY_COMMAND_TYPE[command.commandType] + : null + + return simpleTKey != null ? t(simpleTKey) : '' +} diff --git a/app/src/molecules/Command/hooks/useCommandTextString/utils/getHSShakeSpeedCommandText.ts b/app/src/molecules/Command/hooks/useCommandTextString/utils/getHSShakeSpeedCommandText.ts new file mode 100644 index 00000000000..3710e7f0930 --- /dev/null +++ b/app/src/molecules/Command/hooks/useCommandTextString/utils/getHSShakeSpeedCommandText.ts @@ -0,0 +1,11 @@ +import type { HeaterShakerSetAndWaitForShakeSpeedRunTimeCommand } from '@opentrons/shared-data/command' +import type { HandlesCommands } from './types' + +export function getHSShakeSpeedCommandText({ + command, + t, +}: HandlesCommands): string { + const { rpm } = command.params + + return t('set_and_await_hs_shake', { rpm }) +} diff --git a/app/src/molecules/Command/hooks/useCommandTextString/utils/getLiquidProbeCommandText.ts b/app/src/molecules/Command/hooks/useCommandTextString/utils/getLiquidProbeCommandText.ts new file mode 100644 index 00000000000..a61a4bdf2a3 --- /dev/null +++ b/app/src/molecules/Command/hooks/useCommandTextString/utils/getLiquidProbeCommandText.ts @@ -0,0 +1,60 @@ +import { + getFinalLabwareLocation, + getLabwareDisplayLocation, + getLabwareName, +} from '../../../utils' + +import type { + LiquidProbeRunTimeCommand, + RunTimeCommand, + TryLiquidProbeRunTimeCommand, +} from '@opentrons/shared-data' +import type { HandlesCommands } from './types' +import type { TFunction } from 'i18next' + +type LiquidProbeRunTimeCommands = + | LiquidProbeRunTimeCommand + | TryLiquidProbeRunTimeCommand + +export function getLiquidProbeCommandText({ + command, + commandTextData, + t, + robotType, +}: HandlesCommands): string { + const { wellName, labwareId } = command.params + + const allPreviousCommands = commandTextData?.commands.slice( + 0, + commandTextData.commands.findIndex(c => c.id === command?.id) + ) + + const labwareLocation = + allPreviousCommands != null + ? getFinalLabwareLocation( + labwareId as string, + allPreviousCommands as RunTimeCommand[] + ) + : null + + const displayLocation = + labwareLocation != null && commandTextData != null + ? getLabwareDisplayLocation( + commandTextData, + labwareLocation, + t as TFunction, + robotType + ) + : '' + + const labware = + commandTextData != null + ? getLabwareName(commandTextData, labwareId as string) + : null + + return t('detect_liquid_presence', { + labware, + labware_location: displayLocation, + well_name: wellName, + }) +} diff --git a/app/src/molecules/Command/LoadCommandText.tsx b/app/src/molecules/Command/hooks/useCommandTextString/utils/getLoadCommandText.ts similarity index 70% rename from app/src/molecules/Command/LoadCommandText.tsx rename to app/src/molecules/Command/hooks/useCommandTextString/utils/getLoadCommandText.ts index 251d0f3d38d..b2da948d58d 100644 --- a/app/src/molecules/Command/LoadCommandText.tsx +++ b/app/src/molecules/Command/hooks/useCommandTextString/utils/getLoadCommandText.ts @@ -1,48 +1,37 @@ -import { useTranslation } from 'react-i18next' import { getModuleDisplayName, getModuleType, getOccludedSlotCountForModule, - getPipetteNameSpecs, + getPipetteSpecsV2, } from '@opentrons/shared-data' + import { getLabwareName, getPipetteNameOnMount, getModuleModel, getModuleDisplayLocation, getLiquidDisplayName, -} from './utils' - -import type { - RunTimeCommand, - RobotType, - LoadLabwareRunTimeCommand, -} from '@opentrons/shared-data' -import type { CommandTextData } from './types' +} from '../../../utils' -interface LoadCommandTextProps { - command: RunTimeCommand - commandTextData: CommandTextData - robotType: RobotType -} +import type { LoadLabwareRunTimeCommand } from '@opentrons/shared-data' +import type { GetCommandText } from '..' -export const LoadCommandText = ({ +export const getLoadCommandText = ({ command, commandTextData, robotType, -}: LoadCommandTextProps): JSX.Element | null => { - const { t } = useTranslation('run_details') - - switch (command.commandType) { + t, +}: GetCommandText): string => { + switch (command?.commandType) { case 'loadPipette': { - const pipetteModel = getPipetteNameOnMount( - commandTextData, - command.params.mount - ) + const pipetteModel = + commandTextData != null + ? getPipetteNameOnMount(commandTextData, command.params.mount) + : null return t('load_pipette_protocol_setup', { pipette_name: pipetteModel != null - ? getPipetteNameSpecs(pipetteModel)?.displayName ?? '' + ? getPipetteSpecsV2(pipetteModel)?.displayName ?? '' : '', mount_name: command.params.mount === 'left' ? t('left') : t('right'), }) @@ -63,10 +52,10 @@ export const LoadCommandText = ({ command.params.location !== 'offDeck' && 'moduleId' in command.params.location ) { - const moduleModel = getModuleModel( - commandTextData, - command.params.location.moduleId - ) + const moduleModel = + commandTextData != null + ? getModuleModel(commandTextData, command.params.location.moduleId) + : null const moduleName = moduleModel != null ? getModuleDisplayName(moduleModel) : '' @@ -79,10 +68,13 @@ export const LoadCommandText = ({ ) : 1, labware: command.result?.definition.metadata.displayName, - slot_name: getModuleDisplayLocation( - commandTextData, - command.params.location.moduleId - ), + slot_name: + commandTextData != null + ? getModuleDisplayLocation( + commandTextData, + command.params.location.moduleId + ) + : null, module_name: moduleName, }) } else if ( @@ -91,7 +83,7 @@ export const LoadCommandText = ({ ) { const labwareId = command.params.location.labwareId const labwareName = command.result?.definition.metadata.displayName - const matchingAdapter = commandTextData.commands.find( + const matchingAdapter = commandTextData?.commands.find( (command): command is LoadLabwareRunTimeCommand => command.commandType === 'loadLabware' && command.result?.labwareId === labwareId @@ -111,24 +103,27 @@ export const LoadCommandText = ({ slot_name: adapterLoc?.slotName, }) } else if (adapterLoc != null && 'moduleId' in adapterLoc) { - const moduleModel = getModuleModel( - commandTextData, - adapterLoc?.moduleId ?? '' - ) + const moduleModel = + commandTextData != null + ? getModuleModel(commandTextData, adapterLoc?.moduleId ?? '') + : null const moduleName = moduleModel != null ? getModuleDisplayName(moduleModel) : '' return t('load_labware_info_protocol_setup_adapter_module', { labware: labwareName, adapter_name: adapterName, module_name: moduleName, - slot_name: getModuleDisplayLocation( - commandTextData, - adapterLoc?.moduleId ?? '' - ), + slot_name: + commandTextData != null + ? getModuleDisplayLocation( + commandTextData, + adapterLoc?.moduleId ?? '' + ) + : null, }) } else { // shouldn't reach here, adapter shouldn't have location type labwareId - return null + return '' } } else { const labware = @@ -148,8 +143,14 @@ export const LoadCommandText = ({ case 'loadLiquid': { const { liquidId, labwareId } = command.params return t('load_liquids_info_protocol_setup', { - liquid: getLiquidDisplayName(commandTextData, liquidId), - labware: getLabwareName(commandTextData, labwareId), + liquid: + commandTextData != null + ? getLiquidDisplayName(commandTextData, liquidId) + : null, + labware: + commandTextData != null + ? getLabwareName(commandTextData, labwareId) + : null, }) } default: { @@ -157,7 +158,7 @@ export const LoadCommandText = ({ 'LoadCommandText encountered a command with an unrecognized commandType: ', command ) - return null + return '' } } } diff --git a/app/src/molecules/Command/hooks/useCommandTextString/utils/getMoveLabwareCommandText.ts b/app/src/molecules/Command/hooks/useCommandTextString/utils/getMoveLabwareCommandText.ts new file mode 100644 index 00000000000..71a0ac3e7d6 --- /dev/null +++ b/app/src/molecules/Command/hooks/useCommandTextString/utils/getMoveLabwareCommandText.ts @@ -0,0 +1,72 @@ +import { GRIPPER_WASTE_CHUTE_ADDRESSABLE_AREA } from '@opentrons/shared-data' + +import { + getLabwareName, + getLabwareDisplayLocation, + getFinalLabwareLocation, +} from '../../../utils' + +import type { MoveLabwareRunTimeCommand } from '@opentrons/shared-data' +import type { HandlesCommands } from './types' + +export function getMoveLabwareCommandText({ + command, + t, + commandTextData, + robotType, +}: HandlesCommands): string { + const { labwareId, newLocation, strategy } = command.params + + const allPreviousCommands = commandTextData?.commands.slice( + 0, + commandTextData.commands.findIndex(c => c.id === command.id) + ) + const oldLocation = + allPreviousCommands != null + ? getFinalLabwareLocation(labwareId, allPreviousCommands) + : null + const newDisplayLocation = + commandTextData != null + ? getLabwareDisplayLocation(commandTextData, newLocation, t, robotType) + : null + + const location = newDisplayLocation?.includes( + GRIPPER_WASTE_CHUTE_ADDRESSABLE_AREA + ) + ? 'Waste Chute' + : newDisplayLocation + + return strategy === 'usingGripper' + ? t('move_labware_using_gripper', { + labware: + commandTextData != null + ? getLabwareName(commandTextData, labwareId) + : null, + old_location: + oldLocation != null && commandTextData != null + ? getLabwareDisplayLocation( + commandTextData, + oldLocation, + t, + robotType + ) + : '', + new_location: location, + }) + : t('move_labware_manually', { + labware: + commandTextData != null + ? getLabwareName(commandTextData, labwareId) + : null, + old_location: + oldLocation != null && commandTextData != null + ? getLabwareDisplayLocation( + commandTextData, + oldLocation, + t, + robotType + ) + : '', + new_location: location, + }) +} diff --git a/app/src/molecules/Command/hooks/useCommandTextString/utils/getMoveRelativeCommandText.ts b/app/src/molecules/Command/hooks/useCommandTextString/utils/getMoveRelativeCommandText.ts new file mode 100644 index 00000000000..7f3f8bf0aaa --- /dev/null +++ b/app/src/molecules/Command/hooks/useCommandTextString/utils/getMoveRelativeCommandText.ts @@ -0,0 +1,11 @@ +import type { MoveRelativeRunTimeCommand } from '@opentrons/shared-data/command' +import type { HandlesCommands } from './types' + +export function getMoveRelativeCommandText({ + command, + t, +}: HandlesCommands): string { + const { axis, distance } = command.params + + return t('move_relative', { axis, distance }) +} diff --git a/app/src/molecules/Command/hooks/useCommandTextString/utils/getMoveToAddressabelAreaForDropTipCommandText.ts b/app/src/molecules/Command/hooks/useCommandTextString/utils/getMoveToAddressabelAreaForDropTipCommandText.ts new file mode 100644 index 00000000000..5788fbbdf62 --- /dev/null +++ b/app/src/molecules/Command/hooks/useCommandTextString/utils/getMoveToAddressabelAreaForDropTipCommandText.ts @@ -0,0 +1,19 @@ +import { getAddressableAreaDisplayName } from '../../../utils' + +import type { MoveToAddressableAreaForDropTipRunTimeCommand } from '@opentrons/shared-data/command' +import type { HandlesCommands } from './types' + +export function getMoveToAddressableAreaForDropTipCommandText({ + command, + commandTextData, + t, +}: HandlesCommands): string { + const addressableAreaDisplayName = + commandTextData != null + ? getAddressableAreaDisplayName(commandTextData, command.id, t) + : null + + return t('move_to_addressable_area_drop_tip', { + addressable_area: addressableAreaDisplayName, + }) +} diff --git a/app/src/molecules/Command/hooks/useCommandTextString/utils/getMoveToAddressableAreaCommandText.ts b/app/src/molecules/Command/hooks/useCommandTextString/utils/getMoveToAddressableAreaCommandText.ts new file mode 100644 index 00000000000..e8366120a23 --- /dev/null +++ b/app/src/molecules/Command/hooks/useCommandTextString/utils/getMoveToAddressableAreaCommandText.ts @@ -0,0 +1,19 @@ +import { getAddressableAreaDisplayName } from '../../../utils' + +import type { MoveToAddressableAreaRunTimeCommand } from '@opentrons/shared-data/command' +import type { HandlesCommands } from './types' + +export function getMoveToAddressableAreaCommandText({ + command, + commandTextData, + t, +}: HandlesCommands): string { + const addressableAreaDisplayName = + commandTextData != null + ? getAddressableAreaDisplayName(commandTextData, command.id, t) + : null + + return t('move_to_addressable_area', { + addressable_area: addressableAreaDisplayName, + }) +} diff --git a/app/src/molecules/Command/hooks/useCommandTextString/utils/getMoveToCoordinatesCommandText.ts b/app/src/molecules/Command/hooks/useCommandTextString/utils/getMoveToCoordinatesCommandText.ts new file mode 100644 index 00000000000..a3dc5ace9fe --- /dev/null +++ b/app/src/molecules/Command/hooks/useCommandTextString/utils/getMoveToCoordinatesCommandText.ts @@ -0,0 +1,11 @@ +import type { MoveToCoordinatesRunTimeCommand } from '@opentrons/shared-data/command' +import type { HandlesCommands } from './types' + +export function getMoveToCoordinatesCommandText({ + command, + t, +}: HandlesCommands): string { + const { coordinates } = command.params + + return t('move_to_coordinates', coordinates) +} diff --git a/app/src/molecules/Command/hooks/useCommandTextString/utils/getMoveToSlotCommandText.ts b/app/src/molecules/Command/hooks/useCommandTextString/utils/getMoveToSlotCommandText.ts new file mode 100644 index 00000000000..b66f5d78513 --- /dev/null +++ b/app/src/molecules/Command/hooks/useCommandTextString/utils/getMoveToSlotCommandText.ts @@ -0,0 +1,11 @@ +import type { MoveToSlotRunTimeCommand } from '@opentrons/shared-data/command' +import type { HandlesCommands } from './types' + +export function getMoveToSlotCommandText({ + command, + t, +}: HandlesCommands): string { + const { slotName } = command.params + + return t('move_to_slot', { slot_name: slotName }) +} diff --git a/app/src/molecules/Command/hooks/useCommandTextString/utils/getMoveToWellCommandText.ts b/app/src/molecules/Command/hooks/useCommandTextString/utils/getMoveToWellCommandText.ts new file mode 100644 index 00000000000..8c191f34b40 --- /dev/null +++ b/app/src/molecules/Command/hooks/useCommandTextString/utils/getMoveToWellCommandText.ts @@ -0,0 +1,44 @@ +import { + getFinalLabwareLocation, + getLabwareDisplayLocation, + getLabwareName, +} from '../../../utils' + +import type { TFunction } from 'i18next' +import type { MoveToWellRunTimeCommand } from '@opentrons/shared-data/command' +import type { HandlesCommands } from './types' + +export function getMoveToWellCommandText({ + command, + t, + commandTextData, + robotType, +}: HandlesCommands): string { + const { wellName, labwareId } = command.params + const allPreviousCommands = commandTextData?.commands.slice( + 0, + commandTextData.commands.findIndex(c => c.id === command.id) + ) + const labwareLocation = + allPreviousCommands != null + ? getFinalLabwareLocation(labwareId, allPreviousCommands) + : null + const displayLocation = + labwareLocation != null && commandTextData != null + ? getLabwareDisplayLocation( + commandTextData, + labwareLocation, + t as TFunction, + robotType + ) + : '' + + return t('move_to_well', { + well_name: wellName, + labware: + commandTextData != null + ? getLabwareName(commandTextData, labwareId) + : null, + labware_location: displayLocation, + }) +} diff --git a/app/src/molecules/Command/PipettingCommandText.tsx b/app/src/molecules/Command/hooks/useCommandTextString/utils/getPipettingCommandText.ts similarity index 51% rename from app/src/molecules/Command/PipettingCommandText.tsx rename to app/src/molecules/Command/hooks/useCommandTextString/utils/getPipettingCommandText.ts index 3037ce3e2c3..00c9eba08dd 100644 --- a/app/src/molecules/Command/PipettingCommandText.tsx +++ b/app/src/molecules/Command/hooks/useCommandTextString/utils/getPipettingCommandText.ts @@ -1,50 +1,46 @@ -import { useTranslation } from 'react-i18next' - import { getLabwareDefURI } from '@opentrons/shared-data' -import { getLoadedLabware } from './utils/accessors' +import { getLoadedLabware } from '../../../utils/accessors' import { getLabwareName, getLabwareDisplayLocation, getFinalLabwareLocation, getWellRange, getLabwareDefinitionsFromCommands, -} from './utils' -import type { - PipetteName, - PipettingRunTimeCommand, - RobotType, -} from '@opentrons/shared-data' -import type { CommandTextData } from './types' -import type { TFunction } from 'i18next' +} from '../../../utils' -interface PipettingCommandTextProps { - command: PipettingRunTimeCommand - commandTextData: CommandTextData - robotType: RobotType -} +import type { PipetteName, RunTimeCommand } from '@opentrons/shared-data' +import type { TFunction } from 'i18next' +import type { GetCommandText } from '..' -export const PipettingCommandText = ({ +export const getPipettingCommandText = ({ command, commandTextData, robotType, -}: PipettingCommandTextProps): JSX.Element | null => { - const { t } = useTranslation('protocol_command_text') - + t, +}: GetCommandText): string => { const labwareId = - 'labwareId' in command.params ? command.params.labwareId : '' - const wellName = 'wellName' in command.params ? command.params.wellName : '' + command != null && 'labwareId' in command.params + ? (command.params.labwareId as string) + : '' + const wellName = + command != null && 'wellName' in command.params + ? command.params.wellName + : '' - const allPreviousCommands = commandTextData.commands.slice( + const allPreviousCommands = commandTextData?.commands.slice( 0, - commandTextData.commands.findIndex(c => c.id === command.id) - ) - const labwareLocation = getFinalLabwareLocation( - labwareId, - allPreviousCommands + commandTextData.commands.findIndex(c => c.id === command?.id) ) + const labwareLocation = + allPreviousCommands != null + ? getFinalLabwareLocation( + labwareId, + allPreviousCommands as RunTimeCommand[] + ) + : null const displayLocation = - labwareLocation != null + labwareLocation != null && commandTextData != null ? getLabwareDisplayLocation( commandTextData, labwareLocation, @@ -52,12 +48,15 @@ export const PipettingCommandText = ({ robotType ) : '' - switch (command.commandType) { + switch (command?.commandType) { case 'aspirate': { const { volume, flowRate } = command.params return t('aspirate', { well_name: wellName, - labware: getLabwareName(commandTextData, labwareId), + labware: + commandTextData != null + ? getLabwareName(commandTextData, labwareId) + : null, labware_location: displayLocation, volume, flow_rate: flowRate, @@ -68,7 +67,10 @@ export const PipettingCommandText = ({ return pushOut != null ? t('dispense_push_out', { well_name: wellName, - labware: getLabwareName(commandTextData, labwareId), + labware: + commandTextData != null + ? getLabwareName(commandTextData, labwareId) + : null, labware_location: displayLocation, volume, flow_rate: flowRate, @@ -76,7 +78,10 @@ export const PipettingCommandText = ({ }) : t('dispense', { well_name: wellName, - labware: getLabwareName(commandTextData, labwareId), + labware: + commandTextData != null + ? getLabwareName(commandTextData, labwareId) + : null, labware_location: displayLocation, volume, flow_rate: flowRate, @@ -86,46 +91,67 @@ export const PipettingCommandText = ({ const { flowRate } = command.params return t('blowout', { well_name: wellName, - labware: getLabwareName(commandTextData, labwareId), + labware: + commandTextData != null + ? getLabwareName(commandTextData, labwareId) + : null, labware_location: displayLocation, flow_rate: flowRate, }) } case 'dropTip': { - const loadedLabware = getLoadedLabware(commandTextData, labwareId) - const labwareDefinitions = getLabwareDefinitionsFromCommands( - commandTextData.commands - ) - const labwareDef = labwareDefinitions.find( + const loadedLabware = + commandTextData != null + ? getLoadedLabware(commandTextData, labwareId) + : null + const labwareDefinitions = + commandTextData != null + ? getLabwareDefinitionsFromCommands( + commandTextData.commands as RunTimeCommand[] + ) + : null + const labwareDef = labwareDefinitions?.find( lw => getLabwareDefURI(lw) === loadedLabware?.definitionUri ) return Boolean(labwareDef?.parameters.isTiprack) ? t('return_tip', { well_name: wellName, - labware: getLabwareName(commandTextData, labwareId), + labware: + commandTextData != null + ? getLabwareName(commandTextData, labwareId) + : null, labware_location: displayLocation, }) : t('drop_tip', { well_name: wellName, - labware: getLabwareName(commandTextData, labwareId), + labware: + commandTextData != null + ? getLabwareName(commandTextData, labwareId) + : null, }) } case 'pickUpTip': { const pipetteId = command.params.pipetteId const pipetteName: | PipetteName - | undefined = commandTextData.pipettes.find( + | undefined = commandTextData?.pipettes.find( pipette => pipette.id === pipetteId )?.pipetteName return t('pickup_tip', { - well_range: getWellRange( - pipetteId, - allPreviousCommands, - wellName, - pipetteName - ), - labware: getLabwareName(commandTextData, labwareId), + well_range: + allPreviousCommands != null + ? getWellRange( + pipetteId, + allPreviousCommands as RunTimeCommand[], + wellName as string, + pipetteName + ) + : null, + labware: + commandTextData != null + ? getLabwareName(commandTextData, labwareId) + : null, labware_location: displayLocation, }) } @@ -149,7 +175,7 @@ export const PipettingCommandText = ({ 'PipettingCommandText encountered a command with an unrecognized commandType: ', command ) - return null + return '' } } } diff --git a/app/src/molecules/Command/hooks/useCommandTextString/utils/getPrepareToAspirateCommandText.ts b/app/src/molecules/Command/hooks/useCommandTextString/utils/getPrepareToAspirateCommandText.ts new file mode 100644 index 00000000000..13d32b6b7d6 --- /dev/null +++ b/app/src/molecules/Command/hooks/useCommandTextString/utils/getPrepareToAspirateCommandText.ts @@ -0,0 +1,20 @@ +import { getPipetteSpecsV2 } from '@opentrons/shared-data' + +import type { PrepareToAspirateRunTimeCommand } from '@opentrons/shared-data/command' +import type { HandlesCommands } from './types' + +export function getPrepareToAspirateCommandText({ + command, + commandTextData, + t, +}: HandlesCommands): string { + const { pipetteId } = command.params + const pipetteName = commandTextData?.pipettes.find( + pip => pip.id === pipetteId + )?.pipetteName + + return t('prepare_to_aspirate', { + pipette: + pipetteName != null ? getPipetteSpecsV2(pipetteName)?.displayName : '', + }) +} diff --git a/app/src/molecules/Command/hooks/useCommandTextString/utils/getTCRunProfileCommandText.ts b/app/src/molecules/Command/hooks/useCommandTextString/utils/getTCRunProfileCommandText.ts new file mode 100644 index 00000000000..2d279fca850 --- /dev/null +++ b/app/src/molecules/Command/hooks/useCommandTextString/utils/getTCRunProfileCommandText.ts @@ -0,0 +1,24 @@ +import type { TCRunProfileRunTimeCommand } from '@opentrons/shared-data/command' +import type { GetCommandTextResult } from '..' +import type { HandlesCommands } from './types' + +export function getTCRunProfileCommandText({ + command, + t, +}: HandlesCommands): GetCommandTextResult { + const { profile } = command.params + + const stepTexts = profile.map( + ({ holdSeconds, celsius }: { holdSeconds: number; celsius: number }) => + t('tc_run_profile_steps', { + celsius, + seconds: holdSeconds, + }).trim() + ) + + const startingProfileText = t('tc_starting_profile', { + repetitions: Object.keys(stepTexts).length, + }) + + return { commandText: startingProfileText, stepTexts } +} diff --git a/app/src/molecules/Command/TemperatureCommandText.tsx b/app/src/molecules/Command/hooks/useCommandTextString/utils/getTemperatureCommandText.ts similarity index 78% rename from app/src/molecules/Command/TemperatureCommandText.tsx rename to app/src/molecules/Command/hooks/useCommandTextString/utils/getTemperatureCommandText.ts index 2d09926add2..ee60a76c289 100644 --- a/app/src/molecules/Command/TemperatureCommandText.tsx +++ b/app/src/molecules/Command/hooks/useCommandTextString/utils/getTemperatureCommandText.ts @@ -1,23 +1,20 @@ -import { useTranslation } from 'react-i18next' import type { TemperatureModuleAwaitTemperatureCreateCommand, TemperatureModuleSetTargetTemperatureCreateCommand, TCSetTargetBlockTemperatureCreateCommand, TCSetTargetLidTemperatureCreateCommand, HeaterShakerSetTargetTemperatureCreateCommand, + RunTimeCommand, } from '@opentrons/shared-data' +import type { HandlesCommands } from './types' -type TemperatureCreateCommand = +export type TemperatureCreateCommand = | TemperatureModuleSetTargetTemperatureCreateCommand | TemperatureModuleAwaitTemperatureCreateCommand | TCSetTargetBlockTemperatureCreateCommand | TCSetTargetLidTemperatureCreateCommand | HeaterShakerSetTargetTemperatureCreateCommand -interface TemperatureCommandTextProps { - command: TemperatureCreateCommand -} - const T_KEYS_BY_COMMAND_TYPE: { [commandType in TemperatureCreateCommand['commandType']]: string } = { @@ -28,11 +25,17 @@ const T_KEYS_BY_COMMAND_TYPE: { 'heaterShaker/setTargetTemperature': 'setting_hs_temp', } -export const TemperatureCommandText = ({ - command, -}: TemperatureCommandTextProps): JSX.Element | null => { - const { t } = useTranslation('protocol_command_text') +type HandledCommands = Extract< + RunTimeCommand, + { commandType: keyof typeof T_KEYS_BY_COMMAND_TYPE } +> +type GetTemperatureCommandText = HandlesCommands + +export const getTemperatureCommandText = ({ + command, + t, +}: GetTemperatureCommandText): string => { return t(T_KEYS_BY_COMMAND_TYPE[command.commandType], { temp: command.params?.celsius != null diff --git a/app/src/molecules/Command/hooks/useCommandTextString/utils/getUnknownCommandText.ts b/app/src/molecules/Command/hooks/useCommandTextString/utils/getUnknownCommandText.ts new file mode 100644 index 00000000000..4f2346c7c01 --- /dev/null +++ b/app/src/molecules/Command/hooks/useCommandTextString/utils/getUnknownCommandText.ts @@ -0,0 +1,5 @@ +import type { GetCommandText } from '..' + +export function getUnknownCommandText({ command }: GetCommandText): string { + return JSON.stringify(command) +} diff --git a/app/src/molecules/Command/hooks/useCommandTextString/utils/getWaitForDurationCommandText.ts b/app/src/molecules/Command/hooks/useCommandTextString/utils/getWaitForDurationCommandText.ts new file mode 100644 index 00000000000..d3b3136be1f --- /dev/null +++ b/app/src/molecules/Command/hooks/useCommandTextString/utils/getWaitForDurationCommandText.ts @@ -0,0 +1,11 @@ +import type { WaitForDurationRunTimeCommand } from '@opentrons/shared-data/command' +import type { HandlesCommands } from './types' + +export function getWaitForDurationCommandText({ + command, + t, +}: HandlesCommands): string { + const { seconds, message } = command.params + + return t('wait_for_duration', { seconds, message: message ?? '' }) +} diff --git a/app/src/molecules/Command/hooks/useCommandTextString/utils/getWaitForResumeCommandText.ts b/app/src/molecules/Command/hooks/useCommandTextString/utils/getWaitForResumeCommandText.ts new file mode 100644 index 00000000000..f1c7b7fcef6 --- /dev/null +++ b/app/src/molecules/Command/hooks/useCommandTextString/utils/getWaitForResumeCommandText.ts @@ -0,0 +1,11 @@ +import type { WaitForResumeRunTimeCommand } from '@opentrons/shared-data/command' +import type { HandlesCommands } from './types' + +export function getWaitForResumeCommandText({ + command, + t, +}: HandlesCommands): string { + return command.params?.message != null && command.params.message !== '' + ? command.params.message + : t('wait_for_resume') +} diff --git a/app/src/molecules/Command/hooks/useCommandTextString/utils/index.ts b/app/src/molecules/Command/hooks/useCommandTextString/utils/index.ts new file mode 100644 index 00000000000..f7946ff1e47 --- /dev/null +++ b/app/src/molecules/Command/hooks/useCommandTextString/utils/index.ts @@ -0,0 +1,23 @@ +export { getLoadCommandText } from './getLoadCommandText' +export { getTemperatureCommandText } from './getTemperatureCommandText' +export { getTCRunProfileCommandText } from './getTCRunProfileCommandText' +export { getHSShakeSpeedCommandText } from './getHSShakeSpeedCommandText' +export { getMoveToSlotCommandText } from './getMoveToSlotCommandText' +export { getMoveRelativeCommandText } from './getMoveRelativeCommandText' +export { getMoveToCoordinatesCommandText } from './getMoveToCoordinatesCommandText' +export { getMoveToWellCommandText } from './getMoveToWellCommandText' +export { getMoveLabwareCommandText } from './getMoveLabwareCommandText' +export { getConfigureForVolumeCommandText } from './getConfigureForVolumeCommandText' +export { getConfigureNozzleLayoutCommandText } from './getConfigureNozzleLayoutCommandText' +export { getPrepareToAspirateCommandText } from './getPrepareToAspirateCommandText' +export { getMoveToAddressableAreaCommandText } from './getMoveToAddressableAreaCommandText' +export { getMoveToAddressableAreaForDropTipCommandText } from './getMoveToAddressabelAreaForDropTipCommandText' +export { getDirectTranslationCommandText } from './getDirectTranslationCommandText' +export { getWaitForDurationCommandText } from './getWaitForDurationCommandText' +export { getWaitForResumeCommandText } from './getWaitForResumeCommandText' +export { getDelayCommandText } from './getDelayCommandText' +export { getCommentCommandText } from './getCommentCommandText' +export { getCustomCommandText } from './getCustomCommandText' +export { getUnknownCommandText } from './getUnknownCommandText' +export { getPipettingCommandText } from './getPipettingCommandText' +export { getLiquidProbeCommandText } from './getLiquidProbeCommandText' diff --git a/app/src/molecules/Command/hooks/useCommandTextString/utils/types.ts b/app/src/molecules/Command/hooks/useCommandTextString/utils/types.ts new file mode 100644 index 00000000000..37dde8c783a --- /dev/null +++ b/app/src/molecules/Command/hooks/useCommandTextString/utils/types.ts @@ -0,0 +1,7 @@ +import type { RunTimeCommand } from '@opentrons/shared-data' +import type { GetCommandText } from '..' + +export type HandlesCommands = Omit< + GetCommandText, + 'command' +> & { command: T } diff --git a/app/src/molecules/Command/index.ts b/app/src/molecules/Command/index.ts index 357acd6b85b..b4223d82beb 100644 --- a/app/src/molecules/Command/index.ts +++ b/app/src/molecules/Command/index.ts @@ -4,3 +4,4 @@ export * from './CommandIcon' export * from './CommandIndex' export * from './utils' export * from './types' +export * from './hooks' diff --git a/app/src/molecules/GenericWizardTile/index.tsx b/app/src/molecules/GenericWizardTile/index.tsx index bbeccd13192..1d231931e96 100644 --- a/app/src/molecules/GenericWizardTile/index.tsx +++ b/app/src/molecules/GenericWizardTile/index.tsx @@ -5,8 +5,6 @@ import { useTranslation } from 'react-i18next' import { ALIGN_CENTER, ALIGN_FLEX_END, - Btn, - COLORS, DIRECTION_COLUMN, DIRECTION_ROW, DISPLAY_INLINE_BLOCK, @@ -18,14 +16,13 @@ import { PrimaryButton, RESPONSIVENESS, SPACING, - LegacyStyledText, TYPOGRAPHY, useHoverTooltip, } from '@opentrons/components' import { getIsOnDevice } from '../../redux/config' import { Tooltip } from '../../atoms/Tooltip' import { NeedHelpLink } from '../../organisms/CalibrationPanels' -import { SmallButton } from '../../atoms/buttons' +import { SmallButton, TextOnlyButton } from '../../atoms/buttons' const ALIGN_BUTTONS = css` align-items: ${ALIGN_FLEX_END}; @@ -40,29 +37,7 @@ const CAPITALIZE_FIRST_LETTER_STYLE = css` text-transform: ${TYPOGRAPHY.textTransformCapitalize}; } ` -const GO_BACK_BUTTON_STYLE = css` - ${TYPOGRAPHY.pSemiBold}; - color: ${COLORS.grey50}; - &:hover { - opacity: 70%; - } - - @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { - font-weight: ${TYPOGRAPHY.fontWeightSemiBold}; - font-size: ${TYPOGRAPHY.fontSize22}; - &:hover { - opacity: 100%; - } - &:active { - opacity: 70%; - } - } -` -const GO_BACK_BUTTON_DISABLED_STYLE = css` - ${TYPOGRAPHY.pSemiBold}; - color: ${COLORS.grey60}; -` const Title = styled.h1` ${TYPOGRAPHY.h1Default}; @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { @@ -148,17 +123,12 @@ export function GenericWizardTile(props: GenericWizardTileProps): JSX.Element { {back != null ? ( - - - {t('go_back')} - - + ) : null} {getHelp != null ? : null} {proceed != null && proceedButton == null ? ( diff --git a/app/src/molecules/InProgressModal/InProgressModal.tsx b/app/src/molecules/InProgressModal/InProgressModal.tsx index 57d91940658..63ed2e61365 100644 --- a/app/src/molecules/InProgressModal/InProgressModal.tsx +++ b/app/src/molecules/InProgressModal/InProgressModal.tsx @@ -55,7 +55,8 @@ const MODAL_STYLE = css` padding: ${SPACING.spacing32}; height: 24.625rem; @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { - height: 29.5rem; + max-height: 29.5rem; + height: 100%; } ` const SPINNER_STYLE = css` diff --git a/app/src/molecules/InterventionModal/InterventionContent/InterventionContent.stories.tsx b/app/src/molecules/InterventionModal/InterventionContent/InterventionContent.stories.tsx new file mode 100644 index 00000000000..d910847c2d3 --- /dev/null +++ b/app/src/molecules/InterventionModal/InterventionContent/InterventionContent.stories.tsx @@ -0,0 +1,126 @@ +import * as React from 'react' +import { ICON_DATA_BY_NAME } from '@opentrons/components' +import { InterventionContent } from '.' +import { TwoColumn } from '../TwoColumn' +import { StandInContent } from '../story-utils/StandIn' +import { VisibleContainer } from '../story-utils/VisibleContainer' + +import type { Meta, StoryObj } from '@storybook/react' + +const meta: Meta = { + title: + 'App/Molecules/InterventionModal/InterventionContent/InterventionContent', + component: InterventionContent, + argTypes: { + headline: { + control: { + type: 'text', + }, + }, + infoProps: { + control: { + type: 'object', + }, + type: { + control: { + type: 'select', + }, + options: [ + 'location', + 'location-arrow-location', + 'location-colon-location', + ], + }, + labwareName: { + control: 'text', + }, + currentLocationProps: { + control: { + type: 'object', + }, + slotName: { + control: 'text', + }, + iconName: { + control: { + type: 'select', + }, + options: Object.keys(ICON_DATA_BY_NAME), + }, + }, + newLocationProps: { + control: { + type: 'object', + }, + slotName: { + control: 'text', + }, + iconName: { + control: { + type: 'select', + }, + options: Object.keys(ICON_DATA_BY_NAME), + }, + }, + labwareNickname: { + control: { + type: 'text', + }, + }, + }, + notificationProps: { + control: { + type: 'object', + }, + type: { + control: { + type: 'select', + }, + options: ['alert', 'error', 'neutral', 'success'], + }, + heading: { + control: { + type: 'text', + }, + }, + message: { + control: { + type: 'text', + }, + }, + }, + }, + decorators: [ + Story => ( + + + + + + + ), + ], +} + +export default meta + +type Story = StoryObj + +export const InterventionContentStory: Story = { + args: { + headline: 'You have something to do', + infoProps: { + type: 'location', + labwareName: 'Biorad Plate 200ML', + labwareNickname: 'The biggest plate I have', + currentLocationProps: { + slotName: 'C2', + }, + }, + notificationProps: { + type: 'alert', + heading: 'An alert', + message: 'Oh no', + }, + }, +} diff --git a/app/src/molecules/InterventionModal/InterventionStep/Move.stories.tsx b/app/src/molecules/InterventionModal/InterventionContent/InterventionInfo.stories.tsx similarity index 62% rename from app/src/molecules/InterventionModal/InterventionStep/Move.stories.tsx rename to app/src/molecules/InterventionModal/InterventionContent/InterventionInfo.stories.tsx index 2bbb12d3e14..caac9a06d5c 100644 --- a/app/src/molecules/InterventionModal/InterventionStep/Move.stories.tsx +++ b/app/src/molecules/InterventionModal/InterventionContent/InterventionInfo.stories.tsx @@ -2,19 +2,23 @@ import * as React from 'react' import { Box, ICON_DATA_BY_NAME } from '@opentrons/components' -import { Move } from './Move' +import { InterventionInfo } from './InterventionInfo' import type { Meta, StoryObj } from '@storybook/react' -const meta: Meta = { - title: 'App/Organisms/InterventionModal/InterventionStep/Move', - component: Move, +const meta: Meta = { + title: 'App/Molecules/InterventionModal/InterventionContent/InterventionInfo', + component: InterventionInfo, argTypes: { type: { control: { type: 'select', - options: ['move', 'refill', 'select'], }, + options: [ + 'location', + 'location-arrow-location', + 'location-colon-location', + ], }, labwareName: { control: 'text', @@ -29,8 +33,8 @@ const meta: Meta = { iconName: { control: { type: 'select', - options: Object.keys(ICON_DATA_BY_NAME), }, + options: Object.keys(ICON_DATA_BY_NAME), }, }, newLocationProps: { @@ -43,20 +47,32 @@ const meta: Meta = { iconName: { control: { type: 'select', - options: Object.keys(ICON_DATA_BY_NAME), }, + options: Object.keys(ICON_DATA_BY_NAME), + }, + }, + labwareNickname: { + control: { + type: 'text', }, }, }, + decorators: [ + Story => ( + + + + ), + ], } export default meta -type Story = StoryObj +type Story = StoryObj export const MoveBetweenSlots: Story = { args: { - type: 'move', + type: 'location-arrow-location', labwareName: 'Plate', currentLocationProps: { slotName: 'A1', @@ -65,31 +81,21 @@ export const MoveBetweenSlots: Story = { slotName: 'B2', }, }, - render: args => ( - - - - ), } export const Refill: Story = { args: { - type: 'refill', + type: 'location', labwareName: 'Tip Rack', currentLocationProps: { slotName: 'A1', }, }, - render: args => ( - - - - ), } export const Select: Story = { args: { - type: 'select', + type: 'location-colon-location', labwareName: 'Well', currentLocationProps: { slotName: 'A1', @@ -98,9 +104,4 @@ export const Select: Story = { slotName: 'B1', }, }, - render: args => ( - - - - ), } diff --git a/app/src/molecules/InterventionModal/InterventionContent/InterventionInfo.tsx b/app/src/molecules/InterventionModal/InterventionContent/InterventionInfo.tsx new file mode 100644 index 00000000000..aa6ad81c97e --- /dev/null +++ b/app/src/molecules/InterventionModal/InterventionContent/InterventionInfo.tsx @@ -0,0 +1,169 @@ +import * as React from 'react' +import { css } from 'styled-components' + +import { + LocationIcon, + Flex, + Icon, + COLORS, + BORDERS, + SPACING, + DIRECTION_COLUMN, + StyledText, + ALIGN_CENTER, + RESPONSIVENESS, +} from '@opentrons/components' +import { Divider } from '../../../atoms/structure/Divider' + +import type { LocationIconProps } from '@opentrons/components' + +export interface InterventionInfoProps { + type: 'location-arrow-location' | 'location-colon-location' | 'location' + labwareName: string + labwareNickname?: string + currentLocationProps: LocationIconProps + newLocationProps?: LocationIconProps +} + +export function InterventionInfo(props: InterventionInfoProps): JSX.Element { + const content = buildContent(props) + + return ( + + + + {props.labwareName} + + {props.labwareNickname != null ? ( + + {props.labwareNickname}{' '} + + ) : null} + + + {content} + + ) +} + +const buildContent = (props: InterventionInfoProps): JSX.Element => { + switch (props.type) { + case 'location-arrow-location': + return buildLocArrowLoc(props) + case 'location-colon-location': + return buildLocColonLoc(props) + case 'location': + return buildLoc(props) + } +} + +const buildLocArrowLoc = (props: InterventionInfoProps): JSX.Element => { + const { currentLocationProps, newLocationProps } = props + + if (newLocationProps != null) { + return ( + + + + + + ) + } else { + return buildLoc(props) + } +} + +const buildLoc = ({ + currentLocationProps, +}: InterventionInfoProps): JSX.Element => { + return ( + + + + ) +} + +const buildLocColonLoc = (props: InterventionInfoProps): JSX.Element => { + const { currentLocationProps, newLocationProps } = props + + if (newLocationProps != null) { + return ( + + + + + + ) + } else { + return buildLoc(props) + } +} + +const ICON_STYLE = css` + width: ${SPACING.spacing24}; + height: ${SPACING.spacing24}; + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + width: ${SPACING.spacing40}; + height: ${SPACING.spacing40}; + } +` + +const CARD_STYLE = css` + background-color: ${COLORS.grey20}; + border-radius: ${BORDERS.borderRadius4}; + gap: ${SPACING.spacing8}; + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + background-color: ${COLORS.grey35}; + border-radius: ${BORDERS.borderRadius8}; + } +` + +const LINE_CLAMP_STYLE = css` + display: -webkit-box; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + word-wrap: break-word; + -webkit-line-clamp: 2; +` diff --git a/app/src/molecules/InterventionModal/InterventionContent/index.tsx b/app/src/molecules/InterventionModal/InterventionContent/index.tsx new file mode 100644 index 00000000000..cc52255e4f9 --- /dev/null +++ b/app/src/molecules/InterventionModal/InterventionContent/index.tsx @@ -0,0 +1,61 @@ +import * as React from 'react' +import { + Flex, + StyledText, + DIRECTION_COLUMN, + SPACING, + RESPONSIVENESS, +} from '@opentrons/components' +import { InlineNotification } from '../../../atoms/InlineNotification' + +import { InterventionInfo } from './InterventionInfo' +export type { InterventionInfoProps } from './InterventionInfo' +export { InterventionInfo } + +export interface InterventionContentProps { + headline: string + infoProps: React.ComponentProps + notificationProps?: React.ComponentProps +} + +export function InterventionContent({ + headline, + infoProps, + notificationProps, +}: InterventionContentProps): JSX.Element { + return ( + + + {headline} + + + + {notificationProps ? ( + + ) : null} + + + ) +} diff --git a/app/src/molecules/InterventionModal/InterventionStep/Move.tsx b/app/src/molecules/InterventionModal/InterventionStep/Move.tsx deleted file mode 100644 index 156753d7bc8..00000000000 --- a/app/src/molecules/InterventionModal/InterventionStep/Move.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import * as React from 'react' -import { css } from 'styled-components' - -import { - LocationIcon, - Flex, - Icon, - COLORS, - BORDERS, - SPACING, - DIRECTION_COLUMN, - LegacyStyledText, - ALIGN_CENTER, -} from '@opentrons/components' - -import type { LocationIconProps } from '@opentrons/components' - -export interface MoveProps { - type: 'move' | 'refill' | 'select' - labwareName: string - currentLocationProps: LocationIconProps - newLocationProps?: LocationIconProps -} - -export function Move(props: MoveProps): JSX.Element { - const content = buildContent(props) - - return ( - - {props.labwareName} - {content} - - ) -} - -const buildContent = (props: MoveProps): JSX.Element => { - switch (props.type) { - case 'move': - return buildMove(props) - case 'refill': - return buildRefill(props) - case 'select': - return buildSelect(props) - } -} - -const buildMove = (props: MoveProps): JSX.Element => { - const { currentLocationProps, newLocationProps } = props - - if (newLocationProps != null) { - return ( - - - - - - ) - } else { - return buildRefill(props) - } -} - -const buildRefill = ({ currentLocationProps }: MoveProps): JSX.Element => { - return ( - - - - ) -} - -const buildSelect = (props: MoveProps): JSX.Element => { - const { currentLocationProps, newLocationProps } = props - - if (newLocationProps != null) { - return ( - - - - - - ) - } else { - return buildRefill(props) - } -} - -const ICON_STYLE = css` - width: ${SPACING.spacing40}; - height: ${SPACING.spacing40}; -` - -const CARD_STYLE = css` - flex-direction: ${DIRECTION_COLUMN}; - background-color: ${COLORS.grey35}; - padding: ${SPACING.spacing16}; - grid-gap: ${SPACING.spacing8}; - border-radius: ${BORDERS.borderRadius8}; -` diff --git a/app/src/molecules/InterventionModal/InterventionStep/index.tsx b/app/src/molecules/InterventionModal/InterventionStep/index.tsx deleted file mode 100644 index a6f80de51bf..00000000000 --- a/app/src/molecules/InterventionModal/InterventionStep/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export { Move } from './Move' - -export type { MoveProps } from './Move' diff --git a/app/src/molecules/InterventionModal/OneColumn.stories.tsx b/app/src/molecules/InterventionModal/OneColumn.stories.tsx index 60e4efa03b8..ef4e8a6a02f 100644 --- a/app/src/molecules/InterventionModal/OneColumn.stories.tsx +++ b/app/src/molecules/InterventionModal/OneColumn.stories.tsx @@ -1,39 +1,13 @@ import * as React from 'react' -import { - LegacyStyledText, - Box, - Flex, - BORDERS, - RESPONSIVENESS, - SPACING, - ALIGN_CENTER, - JUSTIFY_CENTER, -} from '@opentrons/components' +import { Box, RESPONSIVENESS } from '@opentrons/components' import { OneColumn as OneColumnComponent } from './' +import { StandInContent } from './story-utils/StandIn' import type { Meta, StoryObj } from '@storybook/react' -function StandInContent(): JSX.Element { - return ( - - - This is a standin for some other component - - - ) -} - -const meta: Meta> = { +const meta: Meta> = { title: 'App/Molecules/InterventionModal/OneColumn', component: OneColumnComponent, render: args => ( @@ -46,7 +20,7 @@ const meta: Meta> = { `} > - + This is a standin for another component ), @@ -54,6 +28,6 @@ const meta: Meta> = { export default meta -export type Story = StoryObj +export type Story = StoryObj export const ExampleOneColumn: Story = { args: {} } diff --git a/app/src/molecules/InterventionModal/OneColumn.tsx b/app/src/molecules/InterventionModal/OneColumn.tsx index 0c36b6ecac7..e92f3ffd51e 100644 --- a/app/src/molecules/InterventionModal/OneColumn.tsx +++ b/app/src/molecules/InterventionModal/OneColumn.tsx @@ -1,11 +1,28 @@ import * as React from 'react' -import { Box } from '@opentrons/components' +import { + Flex, + DIRECTION_COLUMN, + JUSTIFY_SPACE_BETWEEN, +} from '@opentrons/components' +import type { StyleProps } from '@opentrons/components' -export interface OneColumnProps { +export interface OneColumnProps extends StyleProps { children: React.ReactNode } -export function OneColumn({ children }: OneColumnProps): JSX.Element { - return {children} +export function OneColumn({ + children, + ...styleProps +}: OneColumnProps): JSX.Element { + return ( + + {children} + + ) } diff --git a/app/src/molecules/InterventionModal/OneColumnOrTwoColumn.stories.tsx b/app/src/molecules/InterventionModal/OneColumnOrTwoColumn.stories.tsx new file mode 100644 index 00000000000..791edcbdb83 --- /dev/null +++ b/app/src/molecules/InterventionModal/OneColumnOrTwoColumn.stories.tsx @@ -0,0 +1,68 @@ +import * as React from 'react' + +import { OneColumnOrTwoColumn } from './' + +import { StandInContent } from './story-utils/StandIn' +import { VisibleContainer } from './story-utils/VisibleContainer' +import { css } from 'styled-components' +import { + RESPONSIVENESS, + Flex, + ALIGN_CENTER, + JUSTIFY_SPACE_AROUND, + DIRECTION_COLUMN, +} from '@opentrons/components' + +import type { Meta, StoryObj } from '@storybook/react' + +function Wrapper(props: {}): JSX.Element { + return ( + + + + This component is the only one shown on the ODD. + + + + + This component is shown in the right column on desktop. + + + + ) +} + +const meta: Meta> = { + title: 'App/Molecules/InterventionModal/OneColumnOrTwoColumn', + component: Wrapper, + decorators: [ + Story => ( + + + + ), + ], +} + +export default meta + +type Story = StoryObj + +export const OneOrTwoColumn: Story = {} diff --git a/app/src/molecules/InterventionModal/OneColumnOrTwoColumn.tsx b/app/src/molecules/InterventionModal/OneColumnOrTwoColumn.tsx new file mode 100644 index 00000000000..8a6455d67e3 --- /dev/null +++ b/app/src/molecules/InterventionModal/OneColumnOrTwoColumn.tsx @@ -0,0 +1,55 @@ +import * as React from 'react' + +import { css } from 'styled-components' +import { + Flex, + Box, + DIRECTION_ROW, + SPACING, + WRAP, + RESPONSIVENESS, +} from '@opentrons/components' +import type { StyleProps } from '@opentrons/components' +import { TWO_COLUMN_ELEMENT_MIN_WIDTH } from './constants' + +export interface OneColumnOrTwoColumnProps extends StyleProps { + children: [React.ReactNode, React.ReactNode] +} + +export function OneColumnOrTwoColumn({ + children: [leftOrSingleElement, optionallyDisplayedRightElement], + ...styleProps +}: OneColumnOrTwoColumnProps): JSX.Element { + return ( + + + {leftOrSingleElement} + + + {optionallyDisplayedRightElement} + + + ) +} diff --git a/app/src/molecules/InterventionModal/TwoColumn.tsx b/app/src/molecules/InterventionModal/TwoColumn.tsx index 8e87a2d62b5..f0ed10ebf2a 100644 --- a/app/src/molecules/InterventionModal/TwoColumn.tsx +++ b/app/src/molecules/InterventionModal/TwoColumn.tsx @@ -1,20 +1,28 @@ import * as React from 'react' import { Flex, Box, DIRECTION_ROW, SPACING, WRAP } from '@opentrons/components' +import type { StyleProps } from '@opentrons/components' +import { TWO_COLUMN_ELEMENT_MIN_WIDTH } from './constants' -export interface TwoColumnProps { +export interface TwoColumnProps extends StyleProps { children: [React.ReactNode, React.ReactNode] } export function TwoColumn({ children: [leftElement, rightElement], + ...styleProps }: TwoColumnProps): JSX.Element { return ( - - + + {leftElement} - + {rightElement} diff --git a/app/src/molecules/InterventionModal/constants.ts b/app/src/molecules/InterventionModal/constants.ts new file mode 100644 index 00000000000..c5f1fbea4d0 --- /dev/null +++ b/app/src/molecules/InterventionModal/constants.ts @@ -0,0 +1 @@ +export const TWO_COLUMN_ELEMENT_MIN_WIDTH = '17.1875rem' as const diff --git a/app/src/molecules/InterventionModal/index.tsx b/app/src/molecules/InterventionModal/index.tsx index 20c298c54f7..3faa3b34f2c 100644 --- a/app/src/molecules/InterventionModal/index.tsx +++ b/app/src/molecules/InterventionModal/index.tsx @@ -1,10 +1,10 @@ import * as React from 'react' import { useSelector } from 'react-redux' +import { css } from 'styled-components' import { ALIGN_CENTER, BORDERS, - Box, COLORS, Flex, Icon, @@ -15,6 +15,8 @@ import { POSITION_RELATIVE, POSITION_STICKY, SPACING, + DIRECTION_COLUMN, + RESPONSIVENESS, } from '@opentrons/components' import { getIsOnDevice } from '../../redux/config' @@ -23,6 +25,7 @@ import type { IconName } from '@opentrons/components' import { ModalContentOneColSimpleButtons } from './ModalContentOneColSimpleButtons' import { TwoColumn } from './TwoColumn' import { OneColumn } from './OneColumn' +import { OneColumnOrTwoColumn } from './OneColumnOrTwoColumn' import { ModalContentMixed } from './ModalContentMixed' import { DescriptionContent } from './DescriptionContent' import { DeckMapContent } from './DeckMapContent' @@ -31,6 +34,7 @@ export { ModalContentOneColSimpleButtons, TwoColumn, OneColumn, + OneColumnOrTwoColumn, ModalContentMixed, DescriptionContent, DeckMapContent, @@ -143,8 +147,9 @@ export function InterventionModal({ return ( - { e.stopPropagation() @@ -154,19 +159,29 @@ export function InterventionModal({ {...headerStyle} backgroundColor={headerColor} justifyContent={headerJustifyContent} - onClick={iconHeadingOnClick} > {titleHeading} - + {iconName != null ? ( - + ) : null} {iconHeading != null ? iconHeading : null} {children} - + ) } + +const ICON_STYLE = css` + width: ${SPACING.spacing16}; + height: ${SPACING.spacing16}; + margin: ${SPACING.spacing4}; + @media (${RESPONSIVENESS.touchscreenMediaQuerySpecs}) { + width: ${SPACING.spacing32}; + height: ${SPACING.spacing32}; + margin: ${SPACING.spacing12}; + } +` diff --git a/app/src/molecules/InterventionModal/story-utils/StandIn.tsx b/app/src/molecules/InterventionModal/story-utils/StandIn.tsx index f6ac9e7dd78..0fb46f44b8c 100644 --- a/app/src/molecules/InterventionModal/story-utils/StandIn.tsx +++ b/app/src/molecules/InterventionModal/story-utils/StandIn.tsx @@ -1,14 +1,19 @@ import * as React from 'react' -import { Box, BORDERS, SPACING } from '@opentrons/components' +import { Box, BORDERS } from '@opentrons/components' -export function StandInContent(): JSX.Element { +export function StandInContent({ + children, +}: { + children?: React.ReactNode +}): JSX.Element { return ( + > + {children} + ) } diff --git a/app/src/molecules/InterventionModal/story-utils/VisibleContainer.tsx b/app/src/molecules/InterventionModal/story-utils/VisibleContainer.tsx new file mode 100644 index 00000000000..b716b3335ee --- /dev/null +++ b/app/src/molecules/InterventionModal/story-utils/VisibleContainer.tsx @@ -0,0 +1,28 @@ +import * as React from 'react' + +import { Box, BORDERS, SPACING } from '@opentrons/components' +import type { StyleProps } from '@opentrons/components' + +export interface VisibleContainerProps extends StyleProps { + children: JSX.Element | JSX.Element[] +} + +export function VisibleContainer({ + children, + ...styleProps +}: VisibleContainerProps): JSX.Element { + return ( + + {children} + + ) +} diff --git a/app/src/molecules/LegacyModal/LegacyModalShell.tsx b/app/src/molecules/LegacyModal/LegacyModalShell.tsx index 431b607c815..7f9378433dc 100644 --- a/app/src/molecules/LegacyModal/LegacyModalShell.tsx +++ b/app/src/molecules/LegacyModal/LegacyModalShell.tsx @@ -1,17 +1,17 @@ import * as React from 'react' import styled from 'styled-components' import { - COLORS, - POSITION_ABSOLUTE, ALIGN_CENTER, + BORDERS, + COLORS, JUSTIFY_CENTER, - POSITION_RELATIVE, OVERFLOW_AUTO, + POSITION_ABSOLUTE, + POSITION_RELATIVE, POSITION_STICKY, - BORDERS, RESPONSIVENESS, - styleProps, SPACING, + styleProps, } from '@opentrons/components' import type { StyleProps } from '@opentrons/components' export interface LegacyModalShellProps extends StyleProps { diff --git a/app/src/organisms/Breadcrumbs/__tests__/Breadcrumbs.test.tsx b/app/src/organisms/Breadcrumbs/__tests__/Breadcrumbs.test.tsx index 688a3d8a9f1..bd06ebcd9df 100644 --- a/app/src/organisms/Breadcrumbs/__tests__/Breadcrumbs.test.tsx +++ b/app/src/organisms/Breadcrumbs/__tests__/Breadcrumbs.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { MemoryRouter, Route, Switch } from 'react-router-dom' +import { MemoryRouter, Route, Routes } from 'react-router-dom' import { when } from 'vitest-when' import { describe, it, expect, beforeEach, vi } from 'vitest' @@ -34,20 +34,37 @@ const PROTOCOL_NAME = 'a protocol for otie' const render = (path = '/') => { return renderWithProviders( - - - -
    device details path matched
    -
    - - -
    protocol run details path matched
    -
    - - -
    protocol details path matched
    -
    -
    + + + +
    device details path matched
    + + } + /> + + + +
    protocol run details path matched
    + + } + /> + + + +
    protocol details path matched
    + + } + /> +
    , { i18nInstance: i18n, diff --git a/app/src/organisms/Breadcrumbs/index.tsx b/app/src/organisms/Breadcrumbs/index.tsx index f7f43ae3745..e760cf8beb4 100644 --- a/app/src/organisms/Breadcrumbs/index.tsx +++ b/app/src/organisms/Breadcrumbs/index.tsx @@ -71,8 +71,9 @@ const CrumbLinkInactive = styled(Flex)` function BreadcrumbsComponent(): JSX.Element | null { const { t } = useTranslation('top_navigation') const isOnDevice = useSelector(getIsOnDevice) - const { protocolKey, robotName, runId } = useParams() - + const { protocolKey, robotName, runId } = useParams< + keyof DesktopRouteParams + >() as DesktopRouteParams const runCreatedAtTimestamp = useRunCreatedAtTimestamp(runId) const storedProtocol = useSelector((state: State) => @@ -148,7 +149,9 @@ function BreadcrumbsComponent(): JSX.Element | null { } export function Breadcrumbs(): JSX.Element | null { - const { robotName } = useParams() + const { robotName } = useParams< + keyof DesktopRouteParams + >() as DesktopRouteParams const robot = useRobot(robotName) return ( diff --git a/app/src/organisms/CalibrationTaskList/__tests__/CalibrationTaskList.test.tsx b/app/src/organisms/CalibrationTaskList/__tests__/CalibrationTaskList.test.tsx index 8ad8fe61ce6..2c3e27b43df 100644 --- a/app/src/organisms/CalibrationTaskList/__tests__/CalibrationTaskList.test.tsx +++ b/app/src/organisms/CalibrationTaskList/__tests__/CalibrationTaskList.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { StaticRouter } from 'react-router-dom' +import { MemoryRouter } from 'react-router-dom' import { vi, it, describe, expect, beforeEach, afterEach } from 'vitest' import { fireEvent, screen } from '@testing-library/react' @@ -30,7 +30,7 @@ vi.mock('../../ProtocolUpload/hooks') const render = (robotName: string = 'otie') => { return renderWithProviders( - + { deckCalLauncher={mockDeckCalLauncher} exitBeforeDeckConfigCompletion={false} /> - , + , { i18nInstance: i18n, } @@ -82,7 +82,7 @@ describe('CalibrationTaskList', () => { // Complete screen will only render if a wizard has been launched fireEvent.click(screen.getByText('Calibrate')) rerender( - + { deckCalLauncher={mockDeckCalLauncher} exitBeforeDeckConfigCompletion={false} /> - + ) expect(screen.getByText('Calibrations complete!')).toBeTruthy() }) @@ -108,7 +108,7 @@ describe('CalibrationTaskList', () => { screen.getByText('Right Mount') fireEvent.click(screen.getByText('Calibrate')) rerender( - + { deckCalLauncher={mockDeckCalLauncher} exitBeforeDeckConfigCompletion={false} /> - + ) expect(screen.getByText('Calibrations complete!')).toBeTruthy() }) @@ -133,7 +133,7 @@ describe('CalibrationTaskList', () => { screen.getByText('Right Mount') fireEvent.click(screen.getByText('Calibrate')) rerender( - + { deckCalLauncher={mockDeckCalLauncher} exitBeforeDeckConfigCompletion={false} /> - + ) expect(screen.getByText('Calibrations complete!')).toBeTruthy() }) @@ -158,7 +158,7 @@ describe('CalibrationTaskList', () => { screen.getByText('Right Mount') fireEvent.click(screen.getByText('Calibrate')) rerender( - + { deckCalLauncher={mockDeckCalLauncher} exitBeforeDeckConfigCompletion={false} /> - + ) expect(screen.getByText('Calibrations complete!')).toBeTruthy() }) @@ -182,7 +182,7 @@ describe('CalibrationTaskList', () => { expect(recalibrateLinks).toHaveLength(3) fireEvent.click(recalibrateLinks[2]) rerender( - + { deckCalLauncher={mockDeckCalLauncher} exitBeforeDeckConfigCompletion={false} /> - + ) expect(screen.getByText('Calibrations complete!')).toBeTruthy() }) @@ -206,7 +206,7 @@ describe('CalibrationTaskList', () => { expect(recalibrateLinks).toHaveLength(3) fireEvent.click(recalibrateLinks[0]) rerender( - + { deckCalLauncher={mockDeckCalLauncher} exitBeforeDeckConfigCompletion={false} /> - + ) expect(screen.getByText('Calibrations complete!')).toBeTruthy() }) @@ -228,7 +228,7 @@ describe('CalibrationTaskList', () => { const recalibrateLink = screen.getByText('Recalibrate') fireEvent.click(recalibrateLink) rerender( - + { deckCalLauncher={mockDeckCalLauncher} exitBeforeDeckConfigCompletion={true} /> - + ) expect(screen.getByText('Using current calibrations.')).toBeTruthy() }) @@ -253,7 +253,7 @@ describe('CalibrationTaskList', () => { fireEvent.click(calibrateButtons[0]) expect(mockDeckCalLauncher).not.toHaveBeenCalled() rerender( - + { deckCalLauncher={mockDeckCalLauncher} exitBeforeDeckConfigCompletion={false} /> - + ) const recalibrateLinks = screen.getAllByText('Recalibrate') expect(recalibrateLinks).toHaveLength(1) // only deck's recalibration link should be shown @@ -281,7 +281,7 @@ describe('CalibrationTaskList', () => { fireEvent.click(calibrateButtons[0]) expect(mockTipLengthCalLauncher).not.toHaveBeenCalled() rerender( - + { deckCalLauncher={mockDeckCalLauncher} exitBeforeDeckConfigCompletion={false} /> - + ) fireEvent.click(screen.getByText('Left Mount')) const recalibrateLinks = screen.getAllByText('Recalibrate') diff --git a/app/src/organisms/CalibrationTaskList/index.tsx b/app/src/organisms/CalibrationTaskList/index.tsx index d3011a8d573..301a6d1e2b8 100644 --- a/app/src/organisms/CalibrationTaskList/index.tsx +++ b/app/src/organisms/CalibrationTaskList/index.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { css } from 'styled-components' import { useTranslation } from 'react-i18next' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { ALIGN_CENTER, @@ -55,7 +55,7 @@ export function CalibrationTaskList({ setShowCompletionScreen, ] = React.useState(false) const { t } = useTranslation(['robot_calibration', 'device_settings']) - const history = useHistory() + const navigate = useNavigate() const { activeIndex, taskList, taskListStatus } = useCalibrationTaskList( pipOffsetCalLauncher, tipLengthCalLauncher, @@ -111,7 +111,7 @@ export function CalibrationTaskList({ { - history.push(`/devices/${robotName}/robot-settings/calibration`) + navigate(`/devices/${robotName}/robot-settings/calibration`) }} fullPage backgroundColor={COLORS.grey10} @@ -145,7 +145,7 @@ export function CalibrationTaskList({ { - history.push(`/devices/${robotName}/robot-settings/calibration`) + navigate(`/devices/${robotName}/robot-settings/calibration`) }} > {t('device_settings:done')} diff --git a/app/src/organisms/ChangePipette/__tests__/ChangePipette.test.tsx b/app/src/organisms/ChangePipette/__tests__/ChangePipette.test.tsx index 47277d64b76..40b2174194b 100644 --- a/app/src/organisms/ChangePipette/__tests__/ChangePipette.test.tsx +++ b/app/src/organisms/ChangePipette/__tests__/ChangePipette.test.tsx @@ -21,18 +21,18 @@ import { ExitModal } from '../ExitModal' import { ConfirmPipette } from '../ConfirmPipette' import { ChangePipette } from '..' -import type { useHistory } from 'react-router-dom' +import type { NavigateFunction } from 'react-router-dom' import type { PipetteNameSpecs } from '@opentrons/shared-data' import type { AttachedPipette } from '../../../redux/pipettes/types' import type { DispatchApiRequestType } from '../../../redux/robot-api' -const mockPush = vi.fn() +const mockNavigate = vi.fn() vi.mock('react-router-dom', async importOriginal => { - const actual = await importOriginal() + const actual = await importOriginal() return { ...actual, - useHistory: () => ({ push: mockPush }), + useNavigate: () => mockNavigate, } }) diff --git a/app/src/organisms/ChangePipette/index.tsx b/app/src/organisms/ChangePipette/index.tsx index de19bf2f353..a241ed2dd8c 100644 --- a/app/src/organisms/ChangePipette/index.tsx +++ b/app/src/organisms/ChangePipette/index.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import capitalize from 'lodash/capitalize' import { useSelector, useDispatch } from 'react-redux' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { getPipetteNameSpecs } from '@opentrons/shared-data' import { SPACING, TYPOGRAPHY, LegacyStyledText } from '@opentrons/components' @@ -57,7 +57,7 @@ interface Props { export function ChangePipette(props: Props): JSX.Element | null { const { robotName, mount, closeModal } = props const { t } = useTranslation(['change_pipette', 'shared']) - const history = useHistory() + const navigate = useNavigate() const dispatch = useDispatch() const finalRequestId = React.useRef(null) const [dispatchApiRequests] = useDispatchApiRequests(dispatchedAction => { @@ -267,7 +267,7 @@ export function ChangePipette(props: Props): JSX.Element | null { const toCalDashboard = (): void => { dispatchApiRequests(home(robotName, ROBOT)) closeModal() - history.push(`/devices/${robotName}/robot-settings/calibration/dashboard`) + navigate(`/devices/${robotName}/robot-settings/calibration/dashboard`) } exitWizardHeader = diff --git a/app/src/organisms/ChildNavigation/ChildNavigation.stories.tsx b/app/src/organisms/ChildNavigation/ChildNavigation.stories.tsx index ef72b506ed0..ce1e4af098e 100644 --- a/app/src/organisms/ChildNavigation/ChildNavigation.stories.tsx +++ b/app/src/organisms/ChildNavigation/ChildNavigation.stories.tsx @@ -87,3 +87,14 @@ export const TitleWithTwoButtonsDisabled: Story = { ariaDisabled: true, }, } + +export const TitleWithInlineNotification: Story = { + args: { + header: 'Header', + onClickBack: () => {}, + inlineNotification: { + type: 'neutral', + heading: 'Inline notification', + }, + }, +} diff --git a/app/src/organisms/ChooseProtocolSlideout/__tests__/ChooseProtocolSlideout.test.tsx b/app/src/organisms/ChooseProtocolSlideout/__tests__/ChooseProtocolSlideout.test.tsx index 0b812ae060c..af2ce216663 100644 --- a/app/src/organisms/ChooseProtocolSlideout/__tests__/ChooseProtocolSlideout.test.tsx +++ b/app/src/organisms/ChooseProtocolSlideout/__tests__/ChooseProtocolSlideout.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { vi, it, describe, expect, beforeEach } from 'vitest' -import { StaticRouter } from 'react-router-dom' +import { MemoryRouter } from 'react-router-dom' import { fireEvent, screen, waitFor } from '@testing-library/react' import { simpleAnalysisFileFixture } from '@opentrons/api-client' @@ -27,9 +27,9 @@ vi.mock('../../../resources/useNotifyDataReady') const render = (props: React.ComponentProps) => { return renderWithProviders( - + - , + , { i18nInstance: i18n, } diff --git a/app/src/organisms/ChooseProtocolSlideout/index.tsx b/app/src/organisms/ChooseProtocolSlideout/index.tsx index 624064744fc..cd82b1a48ba 100644 --- a/app/src/organisms/ChooseProtocolSlideout/index.tsx +++ b/app/src/organisms/ChooseProtocolSlideout/index.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import first from 'lodash/first' import { Trans, useTranslation } from 'react-i18next' -import { Link, NavLink, useHistory } from 'react-router-dom' +import { Link, NavLink, useNavigate } from 'react-router-dom' import { useSelector } from 'react-redux' import { css } from 'styled-components' @@ -52,7 +52,10 @@ import { useCreateRunFromProtocol } from '../ChooseRobotToRunProtocolSlideout/us import { ApplyHistoricOffsets } from '../ApplyHistoricOffsets' import { useOffsetCandidatesForAnalysis } from '../ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' import { FileCard } from '../ChooseRobotSlideout/FileCard' -import { getRunTimeParameterValuesForRun } from '../Devices/utils' +import { + getRunTimeParameterFilesForRun, + getRunTimeParameterValuesForRun, +} from '../Devices/utils' import { getAnalysisStatus } from '../ProtocolsLanding/utils' import type { DropdownOption } from '@opentrons/components' @@ -86,7 +89,7 @@ export function ChooseProtocolSlideoutComponent( props: ChooseProtocolSlideoutProps ): JSX.Element | null { const { t } = useTranslation(['device_details', 'shared']) - const history = useHistory() + const navigate = useNavigate() const logger = useLogger(new URL('', import.meta.url).pathname) const [targetProps, tooltipProps] = useTooltip() const [targetPropsHover, tooltipPropsHover] = useHoverTooltip() @@ -187,7 +190,7 @@ export function ChooseProtocolSlideoutComponent( name: 'createProtocolRecordResponse', properties: { success: true }, }) - history.push(`/devices/${name}/protocol-runs/${runData.id}`) + navigate(`/devices/${name}/protocol-runs/${runData.id}`) }, onError: (error: Error) => { trackCreateProtocolRunEvent({ @@ -230,14 +233,26 @@ export function ChooseProtocolSlideoutComponent( return { ...acc, [variableName]: uploadedFileResponse.data.id } }, {}) const runTimeParameterValues = getRunTimeParameterValuesForRun( + runTimeParametersOverrides + ) + const runTimeParameterFiles = getRunTimeParameterFilesForRun( runTimeParametersOverrides, mappedResolvedCsvVariableToFileId ) - createRunFromProtocolSource({ - files: srcFileObjects, - protocolKey: selectedProtocol.protocolKey, - runTimeParameterValues, - }) + if (enableCsvFile) { + createRunFromProtocolSource({ + files: srcFileObjects, + protocolKey: selectedProtocol.protocolKey, + runTimeParameterValues, + runTimeParameterFiles, + }) + } else { + createRunFromProtocolSource({ + files: srcFileObjects, + protocolKey: selectedProtocol.protocolKey, + runTimeParameterValues, + }) + } }) } else { logger.warn('failed to create protocol, no protocol selected') @@ -285,7 +300,7 @@ export function ChooseProtocolSlideoutComponent( } return parameter }) - setRunTimeParametersOverrides?.(clone) + setRunTimeParametersOverrides?.(clone as RunTimeParameter[]) }} title={runtimeParam.displayName} width="100%" @@ -431,6 +446,7 @@ export function ChooseProtocolSlideoutComponent( flexDirection={DIRECTION_COLUMN} alignItems={ALIGN_CENTER} gridgap={SPACING.spacing8} + key={runtimeParam.variableName} > ) => { return renderWithProviders( - + - , + , { i18nInstance: i18n, } diff --git a/app/src/organisms/ChooseRobotSlideout/__tests__/FileCard.test.tsx b/app/src/organisms/ChooseRobotSlideout/__tests__/FileCard.test.tsx index b9649386090..0370fc68e09 100644 --- a/app/src/organisms/ChooseRobotSlideout/__tests__/FileCard.test.tsx +++ b/app/src/organisms/ChooseRobotSlideout/__tests__/FileCard.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { vi, it, describe, expect } from 'vitest' -import { StaticRouter } from 'react-router-dom' +import { MemoryRouter } from 'react-router-dom' import { fireEvent, screen } from '@testing-library/react' import { renderWithProviders } from '../../../__testing-utils__' @@ -16,9 +16,9 @@ vi.mock('../../../resources/useNotifyDataReady') vi.mock('../../../redux/config') const render = (props: React.ComponentProps) => { return renderWithProviders( - + - , + , { i18nInstance: i18n, } diff --git a/app/src/organisms/ChooseRobotSlideout/index.tsx b/app/src/organisms/ChooseRobotSlideout/index.tsx index 95e1754f3b8..092c90e0eb9 100644 --- a/app/src/organisms/ChooseRobotSlideout/index.tsx +++ b/app/src/organisms/ChooseRobotSlideout/index.tsx @@ -392,7 +392,7 @@ export function ChooseRobotSlideout( } return parameter }) - setRunTimeParametersOverrides?.(clone) + setRunTimeParametersOverrides?.(clone as RunTimeParameter[]) }} title={runtimeParam.displayName} width="100%" diff --git a/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx b/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx index 6085ff5636d..6216e7d1ae9 100644 --- a/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx +++ b/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { vi, it, describe, expect, beforeEach, afterEach } from 'vitest' -import { StaticRouter } from 'react-router-dom' +import { MemoryRouter } from 'react-router-dom' import { fireEvent, screen, waitFor } from '@testing-library/react' import { when } from 'vitest-when' @@ -51,9 +51,9 @@ const render = ( props: React.ComponentProps ) => { return renderWithProviders( - + - , + , { i18nInstance: i18n, } diff --git a/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx b/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx index 08a809e9306..49cbb7d8217 100644 --- a/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx +++ b/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import first from 'lodash/first' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { Icon, @@ -22,7 +22,10 @@ import { useFeatureFlag } from '../../redux/config' import { OPENTRONS_USB } from '../../redux/discovery' import { appShellRequestor } from '../../redux/shell/remote' import { useTrackCreateProtocolRunEvent } from '../Devices/hooks' -import { getRunTimeParameterValuesForRun } from '../Devices/utils' +import { + getRunTimeParameterFilesForRun, + getRunTimeParameterValuesForRun, +} from '../Devices/utils' import { ApplyHistoricOffsets } from '../ApplyHistoricOffsets' import { useOffsetCandidatesForAnalysis } from '../ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' import { ChooseRobotSlideout } from '../ChooseRobotSlideout' @@ -47,7 +50,7 @@ export function ChooseRobotToRunProtocolSlideoutComponent( ): JSX.Element | null { const { t } = useTranslation(['protocol_details', 'shared', 'app_settings']) const { storedProtocolData, showSlideout, onCloseClick } = props - const history = useHistory() + const navigate = useNavigate() const [shouldApplyOffsets, setShouldApplyOffsets] = React.useState( true ) @@ -109,9 +112,7 @@ export function ChooseRobotToRunProtocolSlideoutComponent( name: 'createProtocolRecordResponse', properties: { success: true }, }) - history.push( - `/devices/${selectedRobot.name}/protocol-runs/${runData.id}` - ) + navigate(`/devices/${selectedRobot.name}/protocol-runs/${runData.id}`) } }, onError: (error: Error) => { @@ -161,6 +162,9 @@ export function ChooseRobotToRunProtocolSlideoutComponent( return { ...acc, [variableName]: uploadedFileResponse.data.id } }, {}) const runTimeParameterValues = getRunTimeParameterValuesForRun( + runTimeParametersOverrides + ) + const runTimeParameterFiles = getRunTimeParameterFilesForRun( runTimeParametersOverrides, mappedResolvedCsvVariableToFileId ) @@ -168,6 +172,7 @@ export function ChooseRobotToRunProtocolSlideoutComponent( files: srcFileObjects, protocolKey, runTimeParameterValues, + runTimeParameterFiles, }) }) } else { diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx index 38627396e08..756ca93acb7 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx @@ -11,8 +11,8 @@ import { DIRECTION_ROW, Flex, JUSTIFY_SPACE_BETWEEN, - SPACING, LegacyStyledText, + SPACING, TYPOGRAPHY, } from '@opentrons/components' import { diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/DeckConfigurationDiscardChangesModal.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/DeckConfigurationDiscardChangesModal.tsx index 1d58f2b6f4f..03fa0608e38 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/DeckConfigurationDiscardChangesModal.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/DeckConfigurationDiscardChangesModal.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { DIRECTION_COLUMN, @@ -23,14 +23,14 @@ export function DeckConfigurationDiscardChangesModal({ setShowConfirmationModal, }: DeckConfigurationDiscardChangesModalProps): JSX.Element { const { t } = useTranslation('device_details') - const history = useHistory() + const navigate = useNavigate() const modalHeader: ModalHeaderBaseProps = { title: t('changes_will_be_lost'), } const handleDiscard = (): void => { setShowConfirmationModal(false) - history.goBack() + navigate(-1) } return ( diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/DeckConfigurationDiscardChangesModal.test.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/DeckConfigurationDiscardChangesModal.test.tsx index 0461f81496b..8b2d6c409d4 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/DeckConfigurationDiscardChangesModal.test.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/DeckConfigurationDiscardChangesModal.test.tsx @@ -5,16 +5,16 @@ import { describe, it, beforeEach, vi, expect } from 'vitest' import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' import { DeckConfigurationDiscardChangesModal } from '../DeckConfigurationDiscardChangesModal' -import type { useHistory } from 'react-router-dom' +import type { NavigateFunction } from 'react-router-dom' const mockFunc = vi.fn() -const mockGoBack = vi.fn() +const mockNavigate = vi.fn() vi.mock('react-router-dom', async importOriginal => { - const actual = await importOriginal() + const actual = await importOriginal() return { ...actual, - useHistory: () => ({ goBack: mockGoBack }), + useNavigate: () => mockNavigate, } }) @@ -51,7 +51,7 @@ describe('DeckConfigurationDiscardChangesModal', () => { render(props) fireEvent.click(screen.getByText('Discard changes')) expect(mockFunc).toHaveBeenCalledWith(false) - expect(mockGoBack).toHaveBeenCalled() + expect(mockNavigate).toHaveBeenCalled() }) it('should call a mock function when tapping continue editing button', () => { diff --git a/app/src/organisms/Devices/HistoricalProtocolRunOverflowMenu.tsx b/app/src/organisms/Devices/HistoricalProtocolRunOverflowMenu.tsx index 49eab67c57f..b72b70c8bcf 100644 --- a/app/src/organisms/Devices/HistoricalProtocolRunOverflowMenu.tsx +++ b/app/src/organisms/Devices/HistoricalProtocolRunOverflowMenu.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' -import { NavLink, useHistory } from 'react-router-dom' +import { NavLink, useNavigate } from 'react-router-dom' import { Flex, @@ -103,7 +103,7 @@ interface MenuDropdownProps extends HistoricalProtocolRunOverflowMenuProps { } function MenuDropdown(props: MenuDropdownProps): JSX.Element { const { t } = useTranslation('device_details') - const history = useHistory() + const navigate = useNavigate() const { runId, @@ -121,7 +121,7 @@ function MenuDropdown(props: MenuDropdownProps): JSX.Element { ) const [targetProps, tooltipProps] = useHoverTooltip() const onResetSuccess = (createRunResponse: Run): void => { - history.push( + navigate( `/devices/${robotName}/protocol-runs/${createRunResponse.data.id}/run-preview` ) } diff --git a/app/src/organisms/Devices/ProtocolRun/EmptySetupStep.tsx b/app/src/organisms/Devices/ProtocolRun/EmptySetupStep.tsx index 7f17d80c7f2..8a4d5a9c2bc 100644 --- a/app/src/organisms/Devices/ProtocolRun/EmptySetupStep.tsx +++ b/app/src/organisms/Devices/ProtocolRun/EmptySetupStep.tsx @@ -11,19 +11,12 @@ import { interface EmptySetupStepProps { title: React.ReactNode description: string - label: string } export function EmptySetupStep(props: EmptySetupStepProps): JSX.Element { - const { title, description, label } = props + const { title, description } = props return ( - - {label} - { if (protocolData != null && !isRobotViewable) { - history.push(`/devices`) + navigate('/devices') } - }, [protocolData, isRobotViewable, history]) + }, [protocolData, isRobotViewable, navigate]) // Side effects dependent on the current run state. React.useEffect(() => { @@ -254,7 +254,7 @@ export function ProtocolRunHeader({ // redirect to new run after successful reset const onResetSuccess = (createRunResponse: Run): void => { - history.push( + navigate( `/devices/${robotName}/protocol-runs/${createRunResponse.data.id}/run-preview` ) } @@ -298,6 +298,7 @@ export function ProtocolRunHeader({ {isERActive ? ( 0 && ( )} - {runStatus === RUN_STATUS_BLOCKED_BY_OPEN_DOOR || - runStatus === RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR ? ( + {runStatus === RUN_STATUS_BLOCKED_BY_OPEN_DOOR ? ( {t('close_door_to_resume')} @@ -545,11 +545,16 @@ const RUN_AGAIN_STATUSES: RunStatus[] = [ RUN_STATUS_FAILED, RUN_STATUS_SUCCEEDED, ] +const RECOVERY_STATUSES: RunStatus[] = [ + RUN_STATUS_AWAITING_RECOVERY, + RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR, + RUN_STATUS_AWAITING_RECOVERY_PAUSED, +] const DISABLED_STATUSES: RunStatus[] = [ RUN_STATUS_FINISHING, RUN_STATUS_STOP_REQUESTED, RUN_STATUS_BLOCKED_BY_OPEN_DOOR, - RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR, + ...RECOVERY_STATUSES, ] interface ActionButtonProps { runId: string @@ -572,7 +577,7 @@ function ActionButton(props: ActionButtonProps): JSX.Element { isFixtureMismatch, isResetRunLoadingRef, } = props - const history = useHistory() + const navigate = useNavigate() const { t } = useTranslation(['run_details', 'shared']) const attachedModules = useModulesQuery({ @@ -592,7 +597,7 @@ function ActionButton(props: ActionButtonProps): JSX.Element { } = useRunControls(runId, (createRunResponse: Run): void => // redirect to new run after successful reset { - history.push( + navigate( `/devices/${robotName}/protocol-runs/${createRunResponse.data.id}/run-preview` ) } @@ -633,7 +638,6 @@ function ActionButton(props: ActionButtonProps): JSX.Element { // For before running a protocol, "close door to begin". (isDoorOpen && runStatus !== RUN_STATUS_BLOCKED_BY_OPEN_DOOR && - runStatus !== RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR && runStatus != null && CANCELLABLE_STATUSES.includes(runStatus)) const robot = useRobot(robotName) @@ -694,7 +698,10 @@ function ActionButton(props: ActionButtonProps): JSX.Element { if (isProtocolAnalyzing) { buttonIconName = 'ot-spinner' buttonText = t('analyzing_on_robot') - } else if (runStatus === RUN_STATUS_RUNNING) { + } else if ( + runStatus === RUN_STATUS_RUNNING || + (runStatus != null && RECOVERY_STATUSES.includes(runStatus)) + ) { buttonIconName = 'pause' buttonText = t('pause_run') handleButtonClick = (): void => { @@ -719,7 +726,7 @@ function ActionButton(props: ActionButtonProps): JSX.Element { confirmAttachment() } else { play() - history.push(`/devices/${robotName}/protocol-runs/${runId}/run-preview`) + navigate(`/devices/${robotName}/protocol-runs/${runId}/run-preview`) trackProtocolRunEvent({ name: runStatus === RUN_STATUS_IDLE diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx index a882e90745b..ef3740625fc 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx @@ -209,7 +209,8 @@ const StyledTableRowComponent = ( {parameter.type === 'csv_file' - ? parameter.file?.file?.name ?? '' + ? // TODO (nd, 07/17/2024): retrieve filename from parameter once backend is wired up + parameter.file?.file?.name ?? '' : formatRunTimeParameterValue(parameter, t)} {parameter.type === 'csv_file' || diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunSetup.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunSetup.tsx index 0e0669213c5..19c29827c15 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunSetup.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunSetup.tsx @@ -282,12 +282,10 @@ export function ProtocolRunSetup({ ) : ( { diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabware/LabwareListItem.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabware/LabwareListItem.tsx index eabf0f12cea..80141c4f1e5 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabware/LabwareListItem.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabware/LabwareListItem.tsx @@ -6,6 +6,7 @@ import { ALIGN_CENTER, BORDERS, Btn, + LocationIcon, COLORS, DIRECTION_COLUMN, DIRECTION_ROW, @@ -14,10 +15,11 @@ import { Icon, JUSTIFY_CENTER, JUSTIFY_SPACE_BETWEEN, + MODULE_ICON_NAME_BY_TYPE, LabwareRender, SIZE_AUTO, SPACING, - LegacyStyledText, + StyledText, TYPOGRAPHY, WELL_LABEL_OPTIONS, } from '@opentrons/components' @@ -35,6 +37,7 @@ import { } from '@opentrons/shared-data' import { ToggleButton } from '../../../../atoms/buttons' +import { Divider } from '../../../../atoms/structure' import { SecureLabwareModal } from './SecureLabwareModal' import type { @@ -58,7 +61,10 @@ const LabwareRow = styled.div` border-width: 1px; border-color: ${COLORS.grey30}; border-radius: ${BORDERS.borderRadius4}; - padding: ${SPACING.spacing16}; + padding: ${(SPACING.spacing12, + SPACING.spacing16, + SPACING.spacing12, + SPACING.spacing24)}; ` interface LabwareListItemProps extends LabwareSetupItem { @@ -67,6 +73,7 @@ interface LabwareListItemProps extends LabwareSetupItem { isFlex: boolean commands: RunTimeCommand[] nestedLabwareInfo: NestedLabwareInfo | null + showLabwareSVG?: boolean } export function LabwareListItem( @@ -83,8 +90,9 @@ export function LabwareListItem( isFlex, commands, nestedLabwareInfo, + showLabwareSVG, } = props - const { t } = useTranslation('protocol_setup') + const { i18n, t } = useTranslation('protocol_setup') const [ secureLabwareModalType, setSecureLabwareModalType, @@ -103,10 +111,14 @@ export function LabwareListItem( 'addressableAreaName' in initialLocation ) { slotInfo = initialLocation.addressableAreaName + } else if (initialLocation === 'offDeck') { + slotInfo = i18n.format(t('off_deck'), 'upperCase') } let moduleDisplayName: string | null = null + let moduleType: ModuleType | null = null let extraAttentionText: JSX.Element | null = null + let secureLabwareInstructions: JSX.Element | null = null let isCorrectHeaterShakerAttached: boolean = false let isHeaterShakerInProtocol: boolean = false let latchCommand: @@ -144,7 +156,7 @@ export function LabwareListItem( moduleModel != null ) { const moduleName = getModuleDisplayName(moduleModel) - const moduleType = getModuleType(moduleModel) + moduleType = getModuleType(moduleModel) const moduleTypeNeedsAttention = extraAttentionModules.find( extraAttentionModType => extraAttentionModType === moduleType ) @@ -158,7 +170,7 @@ export function LabwareListItem( case MAGNETIC_MODULE_TYPE: case THERMOCYCLER_MODULE_TYPE: if (moduleModel !== THERMOCYCLER_MODULE_V2) { - extraAttentionText = ( + secureLabwareInstructions = ( - + - {t('secure_labware_instructions')} - + ) @@ -192,9 +206,9 @@ export function LabwareListItem( case HEATERSHAKER_MODULE_TYPE: isHeaterShakerInProtocol = true extraAttentionText = ( - + {t('heater_shaker_labware_list_view')} - + ) const matchingHeaterShaker = attachedModuleInfo != null && @@ -256,96 +270,128 @@ export function LabwareListItem( return ( - - - {slotInfo} - + + {slotInfo != null && isFlex ? ( + + ) : ( + + {slotInfo} + + )} + {nestedLabwareInfo != null || moduleDisplayName != null ? ( + + ) : null} - + - + {showLabwareSVG && } - + {labwareDisplayName} - - + + {nickName} - + {nestedLabwareInfo != null && nestedLabwareInfo?.sharedSlotId === slotInfo ? ( - - {nestedLabwareInfo.nestedLabwareDefinition != null ? ( - - ) : null} - - + + + - {nestedLabwareInfo.nestedLabwareDisplayName} - - - {nestedLabwareInfo.nestedLabwareNickName} - + + {nestedLabwareInfo.nestedLabwareDisplayName} + + + {nestedLabwareInfo.nestedLabwareNickName} + + - + ) : null} - - - - - {moduleDisplayName != null - ? moduleDisplayName - : t(initialLocation === 'offDeck' ? 'off_deck' : 'on_deck')} - - {extraAttentionText != null ? extraAttentionText : null} - - - {isHeaterShakerInProtocol ? ( - - - {t('labware_latch')} - + {moduleDisplayName != null ? ( + <> + - - - {hsLatchText} - + + {moduleType != null ? ( + + ) : null} + + + {moduleDisplayName} + + {extraAttentionText} + + + {secureLabwareInstructions} + {isHeaterShakerInProtocol ? ( + + + {t('labware_latch')} + + + + + {hsLatchText} + + + + ) : null} - + ) : null} {secureLabwareModalType != null && ( diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabware/OffDeckLabwareList.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabware/OffDeckLabwareList.tsx index 47c6df4bbb7..121f4588691 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabware/OffDeckLabwareList.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabware/OffDeckLabwareList.tsx @@ -35,6 +35,7 @@ export function OffDeckLabwareList( isFlex={isFlex} commands={commands} nestedLabwareInfo={null} + showLabwareSVG /> ))} diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareList.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareList.tsx index ebaac5f8410..da69fb7169a 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareList.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareList.tsx @@ -5,23 +5,24 @@ import { DIRECTION_COLUMN, Flex, SPACING, - LegacyStyledText, - TYPOGRAPHY, + StyledText, + COLORS, } from '@opentrons/components' import { getLabwareSetupItemGroups } from '../../../../pages/Protocols/utils' import { LabwareListItem } from './LabwareListItem' -import { OffDeckLabwareList } from './OffDeckLabwareList' import { getNestedLabwareInfo } from './getNestedLabwareInfo' import type { RunTimeCommand } from '@opentrons/shared-data' import type { ModuleRenderInfoForProtocol } from '../../hooks' import type { ModuleTypesThatRequireExtraAttention } from '../utils/getModuleTypesThatRequireExtraAttention' +import type { LabwareSetupItem } from '../../../../pages/Protocols/utils' const HeaderRow = styled.div` display: grid; grid-template-columns: 1fr 5.2fr 5.3fr; - grid-gap: ${SPACING.spacing8}; - padding: ${SPACING.spacing8}; + grid-gap: ${SPACING.spacing16}; + padding-left: ${SPACING.spacing24}; + padding-top: ${SPACING.spacing20}; ` interface SetupLabwareListProps { attachedModuleInfo: { [moduleId: string]: ModuleRenderInfoForProtocol } @@ -35,6 +36,9 @@ export function SetupLabwareList( const { attachedModuleInfo, commands, extraAttentionModules, isFlex } = props const { t } = useTranslation('protocol_setup') const { offDeckItems, onDeckItems } = getLabwareSetupItemGroups(commands) + const allItems: LabwareSetupItem[] = [] + allItems.push.apply(allItems, onDeckItems) + allItems.push.apply(allItems, offDeckItems) return ( - + {t('location')} - - + + {t('labware_name')} - - - {t('placement')} - + - {onDeckItems.map((labwareItem, index) => { - const labwareOnAdapter = onDeckItems.find( + {allItems.map((labwareItem, index) => { + const labwareOnAdapter = allItems.find( item => labwareItem.initialLocation !== 'offDeck' && 'labwareId' in labwareItem.initialLocation && @@ -72,11 +73,6 @@ export function SetupLabwareList( /> ) })} - ) } diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/LabwareListItem.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/LabwareListItem.test.tsx index 267e27cc20e..108439c1262 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/LabwareListItem.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/LabwareListItem.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, beforeEach, vi, expect } from 'vitest' -import { StaticRouter } from 'react-router-dom' +import { MemoryRouter } from 'react-router-dom' import { opentrons96PcrAdapterV1 } from '@opentrons/shared-data' import { useCreateLiveCommandMutation } from '@opentrons/react-api-client' @@ -68,9 +68,9 @@ const mockNickName = 'nickName' const render = (props: React.ComponentProps) => { return renderWithProviders( - + - , + , { i18nInstance: i18n, } @@ -172,6 +172,7 @@ describe('LabwareListItem', () => { }) screen.getByText('Mock Labware Definition') screen.getByTestId('slot_info_7') + screen.getByTestId('LocationIcon_stacked') screen.getByText('Magnetic Module GEN1') const button = screen.getByText('Secure labware instructions') fireEvent.click(button) @@ -206,6 +207,7 @@ describe('LabwareListItem', () => { }) screen.getByText('Mock Labware Definition') screen.getByTestId('slot_info_7') + screen.getByTestId('LocationIcon_stacked') screen.getByText('Temperature Module GEN1') screen.getByText('nickName') }) @@ -314,7 +316,6 @@ describe('LabwareListItem', () => { screen.getByText('mock nested display name') screen.getByText('nestedLabwareNickName') screen.getByText('nickName') - screen.getByText('On deck') }) it('renders the correct info for a labware on top of a heater shaker', () => { @@ -375,6 +376,6 @@ describe('LabwareListItem', () => { nestedLabwareInfo: null, }) screen.getByText('Mock Labware Definition') - screen.getByText('Off deck') + screen.getByTestId('slot_info_OFF DECK') }) }) diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/OffDeckLabwareList.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/OffDeckLabwareList.test.tsx index 0fbe91a3265..59487246732 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/OffDeckLabwareList.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/OffDeckLabwareList.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { StaticRouter } from 'react-router-dom' +import { MemoryRouter } from 'react-router-dom' import { screen } from '@testing-library/react' import { describe, it, beforeEach, vi, expect } from 'vitest' @@ -13,9 +13,9 @@ vi.mock('../LabwareListItem') const render = (props: React.ComponentProps) => { return renderWithProviders( - + - , + , { i18nInstance: i18n, } diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabware.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabware.test.tsx index 0e19191306d..d6a6ab4b05e 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabware.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabware.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { StaticRouter } from 'react-router-dom' +import { MemoryRouter } from 'react-router-dom' import { fireEvent, screen } from '@testing-library/react' import { describe, it, beforeEach, vi, afterEach, expect } from 'vitest' import { when } from 'vitest-when' @@ -36,7 +36,7 @@ const RUN_ID = '1' const render = () => { return renderWithProviders( - + { expandStep={vi.fn()} nextStep={'liquid_setup_step'} /> - , + , { i18nInstance: i18n, } diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabwareList.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabwareList.test.tsx index 34b8412f536..4228c517134 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabwareList.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabwareList.test.tsx @@ -1,19 +1,15 @@ import * as React from 'react' -import { StaticRouter } from 'react-router-dom' -import { describe, it, beforeEach, vi, expect } from 'vitest' +import { MemoryRouter } from 'react-router-dom' +import { describe, it, beforeEach, vi } from 'vitest' import { screen } from '@testing-library/react' import { multiple_tipacks_with_tc } from '@opentrons/shared-data' import { renderWithProviders } from '../../../../../__testing-utils__' import { i18n } from '../../../../../i18n' -import { mockDefinition } from '../../../../../redux/custom-labware/__fixtures__' import { SetupLabwareList } from '../SetupLabwareList' import { LabwareListItem } from '../LabwareListItem' -import type { - CompletedProtocolAnalysis, - RunTimeCommand, -} from '@opentrons/shared-data' +import type { CompletedProtocolAnalysis } from '@opentrons/shared-data' vi.mock('../LabwareListItem') @@ -21,148 +17,15 @@ const protocolWithTC = (multiple_tipacks_with_tc as unknown) as CompletedProtoco const render = (props: React.ComponentProps) => { return renderWithProviders( - + - , + , { i18nInstance: i18n, } )[0] } -const mockOffDeckCommands = ([ - { - id: '0abc1', - commandType: 'loadPipette', - params: { - pipetteId: 'pipetteId', - mount: 'left', - }, - }, - { - id: '0abc2', - commandType: 'loadLabware', - params: { - labwareId: 'fixedTrash', - location: { - slotName: '12', - }, - }, - result: { - labwareId: 'fixedTrash', - definition: { - ordering: [['A1']], - metadata: { - displayCategory: 'trash', - displayName: 'Opentrons Fixed Trash', - }, - }, - }, - }, - { - id: '0abc3', - commandType: 'loadLabware', - params: { - labwareId: 'tiprackId', - location: { - slotName: '1', - }, - }, - result: { - labwareId: 'labwareId', - definition: mockDefinition, - }, - }, - { - id: '0abc4', - commandType: 'loadLabware', - params: { - labwareId: 'sourcePlateId', - location: { - slotName: '2', - }, - }, - result: { - labwareId: 'labwareId', - definition: mockDefinition, - }, - }, - { - id: '0abc4', - commandType: 'loadLabware', - params: { - labwareId: 'destPlateId', - location: { - slotName: '3', - }, - }, - result: { - labwareId: 'labwareId', - definition: mockDefinition, - }, - }, - { - id: '0', - commandType: 'pickUpTip', - params: { - pipetteId: 'pipetteId', - labwareId: 'tiprackId', - wellName: 'B1', - }, - }, - { - id: '1', - commandType: 'aspirate', - params: { - pipetteId: 'pipetteId', - labwareId: 'sourcePlateId', - wellName: 'A1', - volume: 5, - flowRate: 3, - wellLocation: { - origin: 'bottom', - offset: { x: 0, y: 0, z: 2 }, - }, - }, - }, - { - id: '2', - commandType: 'dispense', - params: { - pipetteId: 'pipetteId', - labwareId: 'destPlateId', - wellName: 'B1', - volume: 4.5, - flowRate: 2.5, - wellLocation: { - origin: 'bottom', - offset: { x: 0, y: 0, z: 1 }, - }, - }, - }, - { - id: '3', - commandType: 'dropTip', - params: { - pipetteId: 'pipetteId', - labwareId: 'fixedTrash', - wellName: 'A1', - }, - }, - { - id: '4', - commandType: 'loadLabware', - params: { - labwareId: 'fixedTrash', - location: 'offDeck', - }, - result: { - labwareId: 'labwareId', - definition: mockDefinition, - }, - }, -] as any) as RunTimeCommand[] - describe('SetupLabwareList', () => { beforeEach(() => { vi.mocked(LabwareListItem).mockReturnValue( @@ -186,34 +49,5 @@ describe('SetupLabwareList', () => { screen.getAllByText('mock labware list item') screen.getByText('Labware name') screen.getByText('Location') - screen.getByText('Placement') - }) - it('renders null for the offdeck labware list when there are none', () => { - render({ - commands: protocolWithTC.commands, - extraAttentionModules: [], - attachedModuleInfo: { - x: 1, - y: 2, - z: 3, - attachedModuleMatch: null, - moduleId: 'moduleId', - } as any, - isFlex: false, - }) - expect( - screen.queryByText('Additional Off-Deck Labware') - ).not.toBeInTheDocument() - }) - - it('renders offdeck labware list when there are additional offdeck labwares', () => { - render({ - commands: mockOffDeckCommands, - extraAttentionModules: [], - attachedModuleInfo: {} as any, - isFlex: false, - }) - screen.getByText('Additional Off-Deck Labware') - screen.getAllByText('mock labware list item') }) }) diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabwareMap.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabwareMap.test.tsx index a9fe5d6d1fc..2f305f90dab 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabwareMap.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabwareMap.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { when } from 'vitest-when' -import { StaticRouter } from 'react-router-dom' +import { MemoryRouter } from 'react-router-dom' import { describe, it, beforeEach, vi, afterEach, expect } from 'vitest' import { screen } from '@testing-library/react' @@ -86,9 +86,9 @@ const mockTCModule = { const render = (props: React.ComponentProps) => { return renderWithProviders( - + - , + , { i18nInstance: i18n, } diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/__tests__/SetupLabwarePositionCheck.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/__tests__/SetupLabwarePositionCheck.test.tsx index 98bfe60da4a..0bf4aaebbfc 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/__tests__/SetupLabwarePositionCheck.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/__tests__/SetupLabwarePositionCheck.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { when } from 'vitest-when' -import { StaticRouter } from 'react-router-dom' +import { MemoryRouter } from 'react-router-dom' import { screen, fireEvent } from '@testing-library/react' import { describe, it, beforeEach, vi, expect, afterEach } from 'vitest' @@ -43,13 +43,13 @@ const RUN_ID = '1' const render = () => { return renderWithProviders( - + - , + , { i18nInstance: i18n, } diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/ChooseModuleToConfigureModal.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/ChooseModuleToConfigureModal.tsx index 10eaee47a41..79066ce7398 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/ChooseModuleToConfigureModal.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/ChooseModuleToConfigureModal.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { useModulesQuery } from '@opentrons/react-api-client' import { ALIGN_CENTER, @@ -193,13 +193,13 @@ function NoUnconfiguredModules(props: NoUnconfiguredModulesProps): JSX.Element { robotName, } = props const { t } = useTranslation('protocol_setup') - const history = useHistory() + const navigate = useNavigate() const { closeCurrentRun } = useCloseCurrentRun() const handleCancelRun = (): void => { closeCurrentRun() } const handleNavigateToDeviceDetails = (): void => { - history.push(`/devices/${robotName}`) + navigate(`/devices/${robotName}`) } const exitButton = isOnDevice ? ( ) => { return renderWithProviders( - + - , + , { i18nInstance: i18n, } diff --git a/app/src/organisms/Devices/ProtocolRun/SetupStep.tsx b/app/src/organisms/Devices/ProtocolRun/SetupStep.tsx index 9289320b87d..9ddb1daf9af 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupStep.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupStep.tsx @@ -12,7 +12,7 @@ import { Icon, JUSTIFY_SPACE_BETWEEN, SPACING, - LegacyStyledText, + StyledText, TYPOGRAPHY, } from '@opentrons/components' @@ -23,8 +23,6 @@ interface SetupStepProps { title: React.ReactNode /** always shown text that provides a one sentence explanation of the contents */ description: string - /** always shown text that sits above title of step (used for step number) */ - label: string /** callback that should toggle the expanded state (managed by parent) */ toggleExpanded: () => void /** contents to be shown only when expanded */ @@ -58,7 +56,6 @@ export function SetupStep({ expanded, title, description, - label, toggleExpanded, children, rightElement, @@ -78,29 +75,21 @@ export function SetupStep({ gridGap={SPACING.spacing40} > - - {label} - - {title} - - + {description} - + {rightElement} diff --git a/app/src/organisms/Devices/ProtocolRun/__tests__/BackToTopButton.test.tsx b/app/src/organisms/Devices/ProtocolRun/__tests__/BackToTopButton.test.tsx index 4113d2c131c..65c5f106f1a 100644 --- a/app/src/organisms/Devices/ProtocolRun/__tests__/BackToTopButton.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/__tests__/BackToTopButton.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { when } from 'vitest-when' -import { StaticRouter } from 'react-router-dom' +import { MemoryRouter } from 'react-router-dom' import { describe, it, beforeEach, vi, afterEach, expect } from 'vitest' import { renderWithProviders } from '../../../../__testing-utils__' @@ -25,14 +25,14 @@ const ROBOT_SERIAL_NUMBER = 'OT123' const render = () => { return renderWithProviders( - + - , + , { i18nInstance: i18n, } diff --git a/app/src/organisms/Devices/ProtocolRun/__tests__/EmptySetupStep.test.tsx b/app/src/organisms/Devices/ProtocolRun/__tests__/EmptySetupStep.test.tsx index ffba66a5754..3c84e76468c 100644 --- a/app/src/organisms/Devices/ProtocolRun/__tests__/EmptySetupStep.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/__tests__/EmptySetupStep.test.tsx @@ -18,7 +18,6 @@ describe('EmptySetupStep', () => { props = { title: 'mockTitle', description: 'mockDescription', - label: 'mockLabel', } }) @@ -26,6 +25,5 @@ describe('EmptySetupStep', () => { render(props) screen.getByText('mockTitle') screen.getByText('mockDescription') - screen.getByText('mockLabel') }) }) diff --git a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunHeader.test.tsx b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunHeader.test.tsx index 0ae4fd544bb..b090b284a34 100644 --- a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunHeader.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunHeader.test.tsx @@ -98,19 +98,19 @@ import { } from '../../../ErrorRecoveryFlows' import type { UseQueryResult } from 'react-query' -import type * as ReactRouterDom from 'react-router-dom' +import type { NavigateFunction } from 'react-router-dom' import type { Mock } from 'vitest' import type * as OpentronsSharedData from '@opentrons/shared-data' import type * as OpentronsComponents from '@opentrons/components' import type * as OpentronsApiClient from '@opentrons/api-client' -const mockPush = vi.fn() +const mockNavigate = vi.fn() vi.mock('react-router-dom', async importOriginal => { - const reactRouterDom = await importOriginal() + const reactRouterDom = await importOriginal() return { ...reactRouterDom, - useHistory: () => ({ push: mockPush } as any), + useNavigate: () => mockNavigate, } }) @@ -945,7 +945,7 @@ describe('ProtocolRunHeader', () => { vi.mocked(useIsRobotViewable).mockReturnValue(false) render() await waitFor(() => { - expect(mockPush).toHaveBeenCalledWith('/devices') + expect(mockNavigate).toHaveBeenCalledWith('/devices') }) }) diff --git a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx index 0db69d94416..89238cbaa01 100644 --- a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx @@ -279,7 +279,6 @@ describe('ProtocolRunSetup', () => { .thenReturn({ complete: false }) render() - screen.getByText('STEP 2') screen.getByText('Deck hardware') screen.getByText('Calibration needed') }) @@ -304,7 +303,6 @@ describe('ProtocolRunSetup', () => { .thenReturn({ complete: false }) render() - screen.getByText('STEP 2') screen.getByText('Deck hardware') screen.getByText('Action needed') }) @@ -338,7 +336,6 @@ describe('ProtocolRunSetup', () => { .thenReturn({ complete: false }) render() - screen.getByText('STEP 2') screen.getByText('Deck hardware') screen.getByText('Action needed') }) @@ -353,16 +350,13 @@ describe('ProtocolRunSetup', () => { it('renders correct text contents for multiple modules', () => { render() - screen.getByText('STEP 1') screen.getByText('Instruments') screen.getByText( 'Review required pipettes and tip length calibrations for this protocol.' ) - screen.getByText('STEP 2') screen.getByText('Deck hardware') screen.getByText('Install the required modules.') - screen.getByText('STEP 3') screen.getByText('Labware') screen.getByText( @@ -389,16 +383,13 @@ describe('ProtocolRunSetup', () => { ]) render() - screen.getByText('STEP 1') screen.getByText('Instruments') screen.getByText( 'Review required pipettes and tip length calibrations for this protocol.' ) - screen.getByText('STEP 2') screen.getByText('Deck hardware') screen.getByText('Install the required module.') - screen.getByText('STEP 3') screen.getByText('Labware') screen.getByText( 'Gather the following labware and full tip racks. To run your protocol without Labware Position Check, place and secure labware in their initial locations.' @@ -425,7 +416,6 @@ describe('ProtocolRunSetup', () => { ]) render() - screen.getByText('STEP 2') screen.getByText('Deck hardware') screen.getByText( 'Install and calibrate the required modules. Install the required fixtures.' diff --git a/app/src/organisms/Devices/ProtocolRun/__tests__/SetupStep.test.tsx b/app/src/organisms/Devices/ProtocolRun/__tests__/SetupStep.test.tsx index 9d37054705d..74b5ee7fb8e 100644 --- a/app/src/organisms/Devices/ProtocolRun/__tests__/SetupStep.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/__tests__/SetupStep.test.tsx @@ -13,7 +13,6 @@ describe('SetupStep', () => { expanded = true, title = 'stub title', description = 'stub description', - label = 'stub label', toggleExpanded = toggleExpandedMock, children = , rightElement =
    right element
    , @@ -24,7 +23,6 @@ describe('SetupStep', () => { expanded, title, description, - label, toggleExpanded, children, rightElement, @@ -54,7 +52,6 @@ describe('SetupStep', () => { }) it('renders text nodes with prop contents', () => { render({ expanded: false }) - screen.getByText('stub label') screen.getByText('stub title') screen.queryAllByText('stub description') screen.queryAllByText('right element') diff --git a/app/src/organisms/Devices/RobotCard.tsx b/app/src/organisms/Devices/RobotCard.tsx index 5910ea8e8cc..d92ea790435 100644 --- a/app/src/organisms/Devices/RobotCard.tsx +++ b/app/src/organisms/Devices/RobotCard.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { ALIGN_START, @@ -54,7 +54,7 @@ interface RobotCardProps { export function RobotCard(props: RobotCardProps): JSX.Element | null { const { robot } = props const { name: robotName, local } = robot - const history = useHistory() + const navigate = useNavigate() const robotModel = useSelector((state: State) => getRobotModelByName(state, robotName) ) @@ -71,7 +71,7 @@ export function RobotCard(props: RobotCardProps): JSX.Element | null { padding={SPACING.spacing16} position={POSITION_RELATIVE} onClick={() => { - history.push(`/devices/${robotName}`) + navigate(`/devices/${robotName}`) }} > { - history.push(`/devices/${robot.name}/robot-settings`) + navigate(`/devices/${robot.name}/robot-settings`) }} disabled={ robot == null || diff --git a/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/DeviceResetModal.tsx b/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/DeviceResetModal.tsx index e2e429d2507..25386016168 100644 --- a/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/DeviceResetModal.tsx +++ b/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/DeviceResetModal.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' import last from 'lodash/last' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { AlertPrimaryButton, @@ -46,7 +46,7 @@ export function DeviceResetModal({ resetOptions, }: DeviceResetModalProps): JSX.Element { const { t } = useTranslation(['device_settings', 'shared', 'branded']) - const history = useHistory() + const navigate = useNavigate() const [dispatchRequest, requestIds] = useDispatchApiRequest() const isFlex = useIsFlex(robotName) const resetRequestStatus = useSelector((state: State) => { @@ -79,7 +79,7 @@ export function DeviceResetModal({ } } dispatchRequest(resetConfig(robotName, resetOptions)) - history.push(`/devices/`) + navigate('/devices/') } } diff --git a/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/RenameRobotSlideout.tsx b/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/RenameRobotSlideout.tsx index 5c68bdceabd..4774249d1f7 100644 --- a/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/RenameRobotSlideout.tsx +++ b/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/RenameRobotSlideout.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useSelector, useDispatch } from 'react-redux' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { useForm, Controller } from 'react-hook-form' import { useTranslation } from 'react-i18next' import { @@ -57,7 +57,7 @@ export function RenameRobotSlideout({ ) const isFlex = useIsFlex(robotName) const trackEvent = useTrackEvent() - const history = useHistory() + const navigate = useNavigate() const dispatch = useDispatch() const connectableRobots = useSelector((state: State) => getConnectableRobots(state) @@ -136,10 +136,10 @@ export function RenameRobotSlideout({ const { updateRobotName } = useUpdateRobotNameMutation({ onSuccess: (data: UpdatedRobotName) => { // TODO: 6/10/2022 kj for the robot name, we need to use GET: /server/name - // data.name != null && history.push(`/devices/${data.name}/robot-settings`) + // data.name != null && navigate(`/devices/${data.name}/robot-settings`) // TODO 6/9/2022 kj this is a temporary fix to avoid the issue // https://github.com/Opentrons/opentrons/issues/10709 - data.name != null && history.push(`/devices`) + data.name != null && navigate('/devices') dispatch(removeRobot(previousRobotName)) }, onError: (error: Error) => { diff --git a/app/src/organisms/Devices/RobotStatusHeader.tsx b/app/src/organisms/Devices/RobotStatusHeader.tsx index 32ef7c4da90..6516c7910c7 100644 --- a/app/src/organisms/Devices/RobotStatusHeader.tsx +++ b/app/src/organisms/Devices/RobotStatusHeader.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import { useSelector, useDispatch } from 'react-redux' -import { Link, useHistory } from 'react-router-dom' +import { Link, useNavigate } from 'react-router-dom' import styled from 'styled-components' import { useProtocolQuery } from '@opentrons/react-api-client' @@ -58,7 +58,7 @@ export function RobotStatusHeader(props: RobotStatusHeaderProps): JSX.Element { 'device_settings', 'run_details', ]) - const history = useHistory() + const navigate = useNavigate() const [targetProps, tooltipProps] = useHoverTooltip() const dispatch = useDispatch() @@ -187,7 +187,7 @@ export function RobotStatusHeader(props: RobotStatusHeaderProps): JSX.Element { {...targetProps} marginRight={SPACING.spacing8} onClick={() => { - history.push(`/devices/${name}/robot-settings/networking`) + navigate(`/devices/${name}/robot-settings/networking`) }} > { + const { variableName } = param + if (param.type !== 'csv_file' && param.value !== param.default) { + return { ...acc, [variableName]: param.value } + } + return acc + }, {}) +} + +/** + * prepares object to send to endpoints requiring RunTimeParameterFilesCreateData + * @param {RunTimeParameter[]} runTimeParameters array of updated RunTimeParameter overrides + * @param {Record} [fileIdMap] mapping of variable name to file ID created and returned by robot server + * @returns {RunTimeParameterFilesCreateData} object mapping variable name to file ID + */ +export function getRunTimeParameterFilesForRun( runTimeParameters: RunTimeParameter[], fileIdMap?: Record -): RunTimeParameterCreateData { +): RunTimeParameterFilesCreateData { return runTimeParameters.reduce((acc, param) => { const { variableName } = param if (param.type === 'csv_file' && param.file?.id != null) { - return { ...acc, [variableName]: { id: param.file.id } } + return { ...acc, [variableName]: param.file.id } } else if ( param.type === 'csv_file' && fileIdMap != null && variableName in fileIdMap ) { - return { ...acc, [variableName]: { id: fileIdMap[variableName] } } - } else if (param.type !== 'csv_file' && param.value !== param.default) { - return { ...acc, [variableName]: param.value } + return { ...acc, [variableName]: fileIdMap[variableName] } } return acc }, {}) diff --git a/app/src/organisms/ErrorRecoveryFlows/BeforeBeginning.tsx b/app/src/organisms/ErrorRecoveryFlows/BeforeBeginning.tsx deleted file mode 100644 index 48a360901d9..00000000000 --- a/app/src/organisms/ErrorRecoveryFlows/BeforeBeginning.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import * as React from 'react' -import { Trans, useTranslation } from 'react-i18next' - -import { - DIRECTION_COLUMN, - Flex, - JUSTIFY_CENTER, - LegacyStyledText, -} from '@opentrons/components' - -import { SmallButton } from '../../atoms/buttons' -import { BODY_TEXT_STYLE, ODD_SECTION_TITLE_STYLE } from './constants' -import { RecoveryContentWrapper } from './shared' - -import type { RecoveryContentProps } from './types' - -export function BeforeBeginning({ - isOnDevice, - routeUpdateActions, -}: RecoveryContentProps): JSX.Element | null { - const { t } = useTranslation('error_recovery') - const { proceedNextStep } = routeUpdateActions - - if (isOnDevice) { - return ( - - - - {t('before_you_begin')} - - , - }} - /> - - - - ) - } else { - return null - } -} diff --git a/app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx b/app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx index 5a40b280139..73b7a1a5ea1 100644 --- a/app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx @@ -1,10 +1,10 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' -import { LegacyStyledText } from '@opentrons/components' +import { StyledText } from '@opentrons/components' -import { BeforeBeginning } from './BeforeBeginning' import { RecoveryError } from './RecoveryError' +import { RecoveryDoorOpen } from './RecoveryDoorOpen' import { SelectRecoveryOption, RetryStep, @@ -28,11 +28,7 @@ import { RECOVERY_MAP } from './constants' import type { RobotType } from '@opentrons/shared-data' import type { RecoveryContentProps } from './types' -import type { - useRouteUpdateActions, - useRecoveryCommands, - ERUtilsResults, -} from './hooks' +import type { ERUtilsResults } from './hooks' import type { ErrorRecoveryFlowsProps } from '.' interface UseERWizardResult { @@ -61,6 +57,7 @@ export type ErrorRecoveryWizardProps = ErrorRecoveryFlowsProps & ERUtilsResults & { robotType: RobotType isOnDevice: boolean + isDoorOpen: boolean } export function ErrorRecoveryWizard( @@ -86,24 +83,40 @@ export function ErrorRecoveryWizard( export function ErrorRecoveryComponent( props: RecoveryContentProps ): JSX.Element { - const { route, step } = props.recoveryMap + const { recoveryMap, hasLaunchedRecovery, isDoorOpen, isOnDevice } = props + const { route, step } = recoveryMap const { t } = useTranslation('error_recovery') const { showModal, toggleModal } = useErrorDetailsModal() const buildTitleHeading = (): JSX.Element => { - const titleText = props.hasLaunchedRecovery - ? t('recovery_mode') - : t('cancel_run') - return {titleText} + const titleText = hasLaunchedRecovery ? t('recovery_mode') : t('cancel_run') + return ( + + {titleText} + + ) } const buildIconHeading = (): JSX.Element => ( - + {t('view_error_details')} - + ) + // TODO(jh, 07-16-24): Revisit making RecoveryDoorOpen a route. + const buildInterventionContent = (): JSX.Element => { + if (isDoorOpen) { + return + } else { + return + } + } + const isLargeDesktopStyle = + !isDoorOpen && route === RECOVERY_MAP.DROP_TIP_FLOWS.ROUTE && step !== RECOVERY_MAP.DROP_TIP_FLOWS.STEPS.BEGIN_REMOVAL @@ -114,20 +127,17 @@ export function ErrorRecoveryComponent( iconHeadingOnClick={toggleModal} iconName="information" desktopType={isLargeDesktopStyle ? 'desktop-large' : 'desktop-small'} + isOnDevice={isOnDevice} > {showModal ? ( ) : null} - + {buildInterventionContent()} ) } export function ErrorRecoveryContent(props: RecoveryContentProps): JSX.Element { - const buildBeforeBeginning = (): JSX.Element => { - return - } - const buildSelectRecoveryOption = (): JSX.Element => { return } @@ -177,8 +187,6 @@ export function ErrorRecoveryContent(props: RecoveryContentProps): JSX.Element { } switch (props.recoveryMap.route) { - case RECOVERY_MAP.BEFORE_BEGINNING.ROUTE: - return buildBeforeBeginning() case RECOVERY_MAP.OPTION_SELECTION.ROUTE: return buildSelectRecoveryOption() case RECOVERY_MAP.ERROR_WHILE_RECOVERING.ROUTE: @@ -213,9 +221,9 @@ export function ErrorRecoveryContent(props: RecoveryContentProps): JSX.Element { } } interface UseInitialPipetteHomeParams { - hasLaunchedRecovery: boolean - recoveryCommands: ReturnType - routeUpdateActions: ReturnType + hasLaunchedRecovery: ErrorRecoveryWizardProps['hasLaunchedRecovery'] + recoveryCommands: ErrorRecoveryWizardProps['recoveryCommands'] + routeUpdateActions: ErrorRecoveryWizardProps['routeUpdateActions'] } // Home the Z-axis of all attached pipettes on Error Recovery launch. export function useInitialPipetteHome({ diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryDoorOpen.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryDoorOpen.tsx new file mode 100644 index 00000000000..17bf1cf0379 --- /dev/null +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryDoorOpen.tsx @@ -0,0 +1,100 @@ +import * as React from 'react' +import { css } from 'styled-components' +import { useTranslation } from 'react-i18next' + +import { + COLORS, + DIRECTION_COLUMN, + Flex, + Icon, + StyledText, + SPACING, + ALIGN_CENTER, + JUSTIFY_END, + RESPONSIVENESS, + TEXT_ALIGN_CENTER, +} from '@opentrons/components' +import { RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR } from '@opentrons/api-client' + +import { + RecoverySingleColumnContentWrapper, + RecoveryFooterButtons, +} from './shared' + +import type { RecoveryContentProps } from './types' + +export function RecoveryDoorOpen({ + recoveryActionMutationUtils, + runStatus, +}: RecoveryContentProps): JSX.Element { + const { + resumeRecovery, + isResumeRecoveryLoading, + } = recoveryActionMutationUtils + const { t } = useTranslation('error_recovery') + + return ( + + + + + + {t('robot_door_is_open')} + + + {t('close_the_robot_door')} + + + + + + + + ) +} + +const TEXT_STYLE = css` + flex-direction: ${DIRECTION_COLUMN}; + grid-gap: ${SPACING.spacing8}; + align-items: ${ALIGN_CENTER}; + text-align: ${TEXT_ALIGN_CENTER}; + + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + grid-gap: ${SPACING.spacing4}; + } +` + +const ICON_STYLE = css` + height: ${SPACING.spacing40}; + width: ${SPACING.spacing40}; + color: ${COLORS.yellow50}; + + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + height: ${SPACING.spacing60}; + width: ${SPACING.spacing60}; + } +` diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryError.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryError.tsx index 5e7ba51c0bd..e38647927db 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryError.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryError.tsx @@ -13,7 +13,7 @@ import { } from '@opentrons/components' import { RECOVERY_MAP } from './constants' -import { RecoveryContentWrapper } from './shared' +import { RecoverySingleColumnContentWrapper } from './shared' import type { RecoveryContentProps } from './types' import { SmallButton } from '../../atoms/buttons' @@ -167,39 +167,35 @@ export function ErrorContent({ btnText: string btnOnClick: () => void }): JSX.Element | null { - if (isOnDevice) { - return ( - + return ( + + + - - - {title} - {subTitle} - - - - + {title} + {subTitle} - - ) - } else { - return null - } +
    + + + + + ) } diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryInProgress.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryInProgress.tsx index 4453598de18..4a0d64f7661 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryInProgress.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryInProgress.tsx @@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next' import { InProgressModal } from '../../molecules/InProgressModal/InProgressModal' import { RECOVERY_MAP } from './constants' +import { Flex, ALIGN_CENTER, JUSTIFY_CENTER } from '@opentrons/components' import type { RobotMovingRoute, RecoveryContentProps } from './types' @@ -41,5 +42,13 @@ export function RecoveryInProgress({ const description = buildDescription() - return + return ( + + + + ) } diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/CancelRun.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/CancelRun.tsx index edd64990ea5..154e1600cb0 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/CancelRun.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/CancelRun.tsx @@ -1,5 +1,6 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' +import { css } from 'styled-components' import { ALIGN_CENTER, @@ -8,11 +9,15 @@ import { Flex, Icon, SPACING, - LegacyStyledText, + StyledText, + RESPONSIVENESS, } from '@opentrons/components' import { RECOVERY_MAP } from '../constants' -import { RecoveryFooterButtons, RecoveryContentWrapper } from '../shared' +import { + RecoveryFooterButtons, + RecoverySingleColumnContentWrapper, +} from '../shared' import { SelectRecoveryOption } from './SelectRecoveryOption' import type { RecoveryContentProps } from '../types' @@ -41,7 +46,6 @@ export function CancelRun(props: RecoveryContentProps): JSX.Element { } function CancelRunConfirmation({ - isOnDevice, routeUpdateActions, recoveryCommands, tipStatusUtils, @@ -56,48 +60,44 @@ function CancelRunConfirmation({ tipStatusUtils, }) - if (isOnDevice) { - return ( - + - - - - {t('are_you_sure_you_want_to_cancel')} - - - {t('if_tips_are_attached')} - - - - - ) - } else { - return null - } + + {t('are_you_sure_you_want_to_cancel')} + + + {t('if_tips_are_attached')} + +
    + + + ) } interface OnCancelRunProps { @@ -146,3 +146,19 @@ export function useOnCancelRun({ return { showBtnLoadingState, handleCancelRunClick } } + +const FLEX_WIDTH = css` + width: 41.625rem; + @media (${RESPONSIVENESS.touchscreenMediaQuerySpecs}) { + width: 53rem; + } +` + +const ICON_SIZE = css` + width: ${SPACING.spacing40}; + height: ${SPACING.spacing40}; + @media (${RESPONSIVENESS.touchscreenMediaQuerySpecs}) { + width: ${SPACING.spacing60}; + height: ${SPACING.spacing60}; + } +` diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/FillWellAndSkip.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/FillWellAndSkip.tsx index 583d0428ee0..fab5d36f8eb 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/FillWellAndSkip.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/FillWellAndSkip.tsx @@ -12,7 +12,7 @@ import { RECOVERY_MAP } from '../constants' import { CancelRun } from './CancelRun' import { RecoveryFooterButtons, - RecoveryContentWrapper, + RecoverySingleColumnContentWrapper, LeftColumnLabwareInfo, TwoColTextAndFailedStepNextStep, } from '../shared' @@ -44,42 +44,32 @@ export function FillWellAndSkip(props: RecoveryContentProps): JSX.Element { } export function FillWell(props: RecoveryContentProps): JSX.Element | null { - const { - isOnDevice, - routeUpdateActions, - failedLabwareUtils, - deckMapUtils, - } = props + const { routeUpdateActions, failedLabwareUtils, deckMapUtils } = props const { t } = useTranslation('error_recovery') const { goBackPrevStep, proceedNextStep } = routeUpdateActions - if (isOnDevice) { - return ( - - - - - - - - - - - - ) - } else { - return null - } + return ( + + + + + + + + + + + + ) } export function SkipToNextStep( @@ -96,7 +86,7 @@ export function SkipToNextStep( proceedToRouteAndStep, } = routeUpdateActions const { selectedRecoveryOption } = currentRecoveryOptionUtils - const { skipFailedCommand, resumeRun } = recoveryCommands + const { skipFailedCommand } = recoveryCommands const { ROBOT_SKIPPING_STEP, IGNORE_AND_SKIP } = RECOVERY_MAP const { t } = useTranslation('error_recovery') @@ -110,11 +100,9 @@ export function SkipToNextStep( } const primaryBtnOnClick = (): Promise => { - return setRobotInMotion(true, ROBOT_SKIPPING_STEP.ROUTE) - .then(() => skipFailedCommand()) - .then(() => { - resumeRun() - }) + return setRobotInMotion(true, ROBOT_SKIPPING_STEP.ROUTE).then(() => { + skipFailedCommand() + }) } const buildBodyText = (): JSX.Element => { diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/IgnoreErrorSkipStep.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/IgnoreErrorSkipStep.tsx index 1808729876d..c5ecf84a61b 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/IgnoreErrorSkipStep.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/IgnoreErrorSkipStep.tsx @@ -11,7 +11,10 @@ import { import { ODD_SECTION_TITLE_STYLE, RECOVERY_MAP } from '../constants' import { SelectRecoveryOption } from './SelectRecoveryOption' -import { RecoveryFooterButtons, RecoveryContentWrapper } from '../shared' +import { + RecoveryFooterButtons, + RecoverySingleColumnContentWrapper, +} from '../shared' import { RadioButton } from '../../../atoms/buttons' import type { RecoveryContentProps } from '../types' @@ -35,7 +38,6 @@ export function IgnoreErrorSkipStep(props: RecoveryContentProps): JSX.Element { } export function IgnoreErrorStepHome({ - isOnDevice, recoveryCommands, routeUpdateActions, }: RecoveryContentProps): JSX.Element | null { @@ -78,29 +80,24 @@ export function IgnoreErrorStepHome({ } } - if (isOnDevice) { - return ( - - - {t('ignore_similar_errors_later_in_run')} - - - - - + + {t('ignore_similar_errors_later_in_run')} + + + - - ) - } else { - return null - } +
    + + + ) } interface IgnoreOptionsProps { diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx index a761d881f95..a80b777d4ba 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx @@ -1,18 +1,29 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import head from 'lodash/head' +import { css } from 'styled-components' import { DIRECTION_COLUMN, SPACING, Flex, - LegacyStyledText, + StyledText, + RESPONSIVENESS, } from '@opentrons/components' import { FLEX_ROBOT_TYPE, OT2_ROBOT_TYPE } from '@opentrons/shared-data' import { RadioButton } from '../../../atoms/buttons' -import { ODD_SECTION_TITLE_STYLE, RECOVERY_MAP } from '../constants' -import { RecoveryFooterButtons, RecoveryContentWrapper } from '../shared' +import { + ODD_SECTION_TITLE_STYLE, + RECOVERY_MAP, + ODD_ONLY, + DESKTOP_ONLY, +} from '../constants' +import { + RecoveryFooterButtons, + RecoverySingleColumnContentWrapper, + RecoveryRadioGroup, +} from '../shared' import { DropTipWizardFlows } from '../../DropTipWizardFlows' import { DT_ROUTES } from '../../DropTipWizardFlows/constants' import { SelectRecoveryOption } from './SelectRecoveryOption' @@ -48,7 +59,6 @@ export function ManageTips(props: RecoveryContentProps): JSX.Element { type RemovalOptions = 'begin-removal' | 'skip' export function BeginRemoval({ - isOnDevice, tipStatusUtils, routeUpdateActions, recoveryCommands, @@ -87,39 +97,91 @@ export function BeginRemoval({ } } - if (isOnDevice) { - return ( - - - {t('you_may_want_to_remove', { mount })} - - - { - setSelected('begin-removal') - }} - isSelected={selected === 'begin-removal'} - /> - { - setSelected('skip') - }} - isSelected={selected === 'skip'} - /> - - + + {t('you_may_want_to_remove', { mount })} + + + { + setSelected('begin-removal') + }} + isSelected={selected === 'begin-removal'} /> - - ) - } else { - return null - } + { + setSelected('skip') + }} + isSelected={selected === 'skip'} + /> +
    + + ) => { + setSelected(e.currentTarget.value as RemovalOptions) + }} + options={[ + { + value: t('begin_removal'), + children: ( + + {t('begin_removal')} + + ), + }, + { + value: t('skip'), + children: ( + + {t('skip')} + + ), + }, + ]} + /> + + + + ) } function DropTipFlowsContainer( @@ -131,7 +193,6 @@ function DropTipFlowsContainer( recoveryCommands, isFlex, currentRecoveryOptionUtils, - isOnDevice, } = props const { DROP_TIP_FLOWS, ROBOT_CANCELING, RETRY_NEW_TIPS } = RECOVERY_MAP const { proceedToRouteAndStep, setRobotInMotion } = routeUpdateActions @@ -166,21 +227,17 @@ function DropTipFlowsContainer( const fixitCommandTypeUtils = useDropTipFlowUtils(props) - if (isOnDevice) { - return ( - - - - ) - } else { - return null - } + return ( + + + + ) } // Builds the overrides injected into DT Wiz. diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SelectRecoveryOption.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SelectRecoveryOption.tsx index 25b71b64d0d..a33f2fb7abc 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SelectRecoveryOption.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SelectRecoveryOption.tsx @@ -6,16 +6,22 @@ import { DIRECTION_COLUMN, Flex, SPACING, - LegacyStyledText, + StyledText, } from '@opentrons/components' import { RECOVERY_MAP, ERROR_KINDS, ODD_SECTION_TITLE_STYLE, + ODD_ONLY, + DESKTOP_ONLY, } from '../constants' import { RadioButton } from '../../../atoms/buttons' -import { RecoveryFooterButtons, RecoveryContentWrapper } from '../shared' +import { + RecoveryODDOneDesktopTwoColumnContentWrapper, + RecoveryRadioGroup, + FailedStepNextStep, +} from '../shared' import type { ErrorKind, RecoveryContentProps, RecoveryRoute } from '../types' import type { PipetteWithTip } from '../../DropTipWizardFlows' @@ -40,12 +46,12 @@ export function SelectRecoveryOption(props: RecoveryContentProps): JSX.Element { } export function SelectRecoveryOptionHome({ - isOnDevice, errorKind, routeUpdateActions, tipStatusUtils, currentRecoveryOptionUtils, getRecoveryOptionCopy, + ...rest }: RecoveryContentProps): JSX.Element | null { const { t } = useTranslation('error_recovery') const { proceedToRouteAndStep } = routeUpdateActions @@ -58,32 +64,43 @@ export function SelectRecoveryOptionHome({ useCurrentTipStatus(determineTipStatus) - if (isOnDevice) { - return ( - - + return ( + { + setSelectedRecoveryOption(selectedRoute) + void proceedToRouteAndStep(selectedRoute as RecoveryRoute) + }, + }} + > + + {t('choose_a_recovery_action')} - - - + + - { - setSelectedRecoveryOption(selectedRoute) - void proceedToRouteAndStep(selectedRoute as RecoveryRoute) - }} - /> - - ) - } else { - return null - } + + + +
    + + + ) } interface RecoveryOptionsProps { @@ -93,29 +110,66 @@ interface RecoveryOptionsProps { selectedRoute?: RecoveryRoute } // For ODD use only. -export function RecoveryOptions({ +export function ODDRecoveryOptions({ validRecoveryOptions, selectedRoute, setSelectedRoute, getRecoveryOptionCopy, -}: RecoveryOptionsProps): JSX.Element[] { - return validRecoveryOptions.map((recoveryOption: RecoveryRoute) => { - const optionName = getRecoveryOptionCopy(recoveryOption) - - return ( - { - setSelectedRoute(recoveryOption) - }} - isSelected={recoveryOption === selectedRoute} - /> - ) - }) +}: RecoveryOptionsProps): JSX.Element { + return ( + + {validRecoveryOptions.map((recoveryOption: RecoveryRoute) => { + const optionName = getRecoveryOptionCopy(recoveryOption) + return ( + { + setSelectedRoute(recoveryOption) + }} + isSelected={recoveryOption === selectedRoute} + /> + ) + })} + + ) } +export function DesktopRecoveryOptions({ + validRecoveryOptions, + selectedRoute, + setSelectedRoute, + getRecoveryOptionCopy, +}: RecoveryOptionsProps): JSX.Element { + return ( + { + setSelectedRoute(e.currentTarget.value) + }} + value={selectedRoute} + options={validRecoveryOptions.map( + (option: RecoveryRoute) => + ({ + value: option, + children: ( + + {getRecoveryOptionCopy(option)} + + ), + } as const) + )} + /> + ) +} // Pre-fetch tip attachment status. Users are not blocked from proceeding at this step. export function useCurrentTipStatus( determineTipStatus: () => Promise diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SkipStepNewTips.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SkipStepNewTips.tsx index d8c837b33bc..33c0f199cd8 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SkipStepNewTips.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SkipStepNewTips.tsx @@ -48,17 +48,15 @@ export function SkipStepNewTips( export function SkipStepWithNewTips(props: RecoveryContentProps): JSX.Element { const { recoveryCommands, routeUpdateActions } = props - const { skipFailedCommand, resumeRun } = recoveryCommands + const { skipFailedCommand } = recoveryCommands const { setRobotInMotion } = routeUpdateActions const { ROBOT_SKIPPING_STEP } = RECOVERY_MAP const { t } = useTranslation('error_recovery') const primaryBtnOnClick = (): Promise => { - return setRobotInMotion(true, ROBOT_SKIPPING_STEP.ROUTE) - .then(() => skipFailedCommand()) - .then(() => { - resumeRun() - }) + return setRobotInMotion(true, ROBOT_SKIPPING_STEP.ROUTE).then(() => { + skipFailedCommand() + }) } const buildBodyText = (): JSX.Element => { diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SkipStepSameTips.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SkipStepSameTips.tsx index 9a679ba8d17..aed84372ccf 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SkipStepSameTips.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SkipStepSameTips.tsx @@ -29,17 +29,15 @@ export function SkipStepSameTips(props: RecoveryContentProps): JSX.Element { export function SkipStepSameTipsInfo(props: RecoveryContentProps): JSX.Element { const { routeUpdateActions, recoveryCommands } = props - const { skipFailedCommand, resumeRun } = recoveryCommands + const { skipFailedCommand } = recoveryCommands const { setRobotInMotion } = routeUpdateActions const { ROBOT_SKIPPING_STEP } = RECOVERY_MAP const { t } = useTranslation('error_recovery') const primaryBtnOnClick = (): Promise => { - return setRobotInMotion(true, ROBOT_SKIPPING_STEP.ROUTE) - .then(() => skipFailedCommand()) - .then(() => { - resumeRun() - }) + return setRobotInMotion(true, ROBOT_SKIPPING_STEP.ROUTE).then(() => { + skipFailedCommand() + }) } const buildBodyText = (): JSX.Element => { diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/CancelRun.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/CancelRun.test.tsx index 251c6fa43bd..31e991deef9 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/CancelRun.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/CancelRun.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { vi, describe, it, expect, beforeEach } from 'vitest' -import { screen, fireEvent, waitFor } from '@testing-library/react' +import { screen, waitFor } from '@testing-library/react' import { renderWithProviders } from '../../../../__testing-utils__' import { i18n } from '../../../../i18n' @@ -8,7 +8,7 @@ import { mockRecoveryContentProps } from '../../__fixtures__' import { CancelRun } from '../CancelRun' import { RECOVERY_MAP } from '../../constants' import { SelectRecoveryOption } from '../SelectRecoveryOption' - +import { clickButtonLabeled } from '../../__tests__/util' import type { Mock } from 'vitest' vi.mock('../SelectRecoveryOption') @@ -70,9 +70,7 @@ describe('RecoveryFooterButtons', () => { 'If tips are attached, you can choose to blowout any aspirated liquid and drop tips before the run is terminated.' ) - const secondaryBtn = screen.getByRole('button', { name: 'Go back' }) - - fireEvent.click(secondaryBtn) + clickButtonLabeled('Go back') expect(mockGoBackPrevStep).toHaveBeenCalled() }) @@ -95,8 +93,7 @@ describe('RecoveryFooterButtons', () => { routeUpdateActions: mockRouteUpdateActions, }) - const primaryBtn = screen.getByRole('button', { name: 'Confirm' }) - fireEvent.click(primaryBtn) + clickButtonLabeled('Confirm') await waitFor(() => { expect(setRobotInMotionMock).toHaveBeenCalledTimes(1) @@ -127,9 +124,7 @@ describe('RecoveryFooterButtons', () => { render(props) - const primaryBtn = screen.getByRole('button', { name: 'Confirm' }) - - fireEvent.click(primaryBtn) + clickButtonLabeled('Confirm') expect(mockProceedToRouteAndStep).toHaveBeenCalledWith(DROP_TIP_FLOWS.ROUTE) }) @@ -144,9 +139,7 @@ describe('RecoveryFooterButtons', () => { render(props) - const primaryBtn = screen.getByRole('button', { name: 'Confirm' }) - - fireEvent.click(primaryBtn) + clickButtonLabeled('Confirm') expect(mockProceedToRouteAndStep).not.toHaveBeenCalled() expect(mockSetRobotInMotion).not.toHaveBeenCalled() }) @@ -162,9 +155,7 @@ describe('RecoveryFooterButtons', () => { render(props) - const primaryBtn = screen.getByRole('button', { name: 'Confirm' }) - - fireEvent.click(primaryBtn) + clickButtonLabeled('Confirm') expect(mockProceedToRouteAndStep).not.toHaveBeenCalled() expect(mockSetRobotInMotion).toHaveBeenCalled() }) diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/FillWellAndSkip.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/FillWellAndSkip.test.tsx index 47e8d604dcf..123bbb33626 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/FillWellAndSkip.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/FillWellAndSkip.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { describe, it, vi, expect, beforeEach } from 'vitest' -import { screen, fireEvent, waitFor } from '@testing-library/react' +import { screen, waitFor } from '@testing-library/react' import { mockRecoveryContentProps } from '../../__fixtures__' import { renderWithProviders } from '../../../../__testing-utils__' @@ -9,6 +9,7 @@ import { FillWellAndSkip, FillWell, SkipToNextStep } from '../FillWellAndSkip' import { RECOVERY_MAP } from '../../constants' import { CancelRun } from '../CancelRun' import { SelectRecoveryOption } from '../SelectRecoveryOption' +import { clickButtonLabeled } from '../../__tests__/util' import type { Mock } from 'vitest' @@ -140,14 +141,12 @@ describe('SkipToNextStep', () => { let mockGoBackPrevStep: Mock let mockProceedToRouteAndStep: Mock let mockSkipFailedCommand: Mock - let mockResumeRun: Mock beforeEach(() => { mockSetRobotInMotion = vi.fn(() => Promise.resolve()) mockGoBackPrevStep = vi.fn() mockProceedToRouteAndStep = vi.fn() mockSkipFailedCommand = vi.fn(() => Promise.resolve()) - mockResumeRun = vi.fn() props = { ...mockRecoveryContentProps, @@ -158,7 +157,6 @@ describe('SkipToNextStep', () => { } as any, recoveryCommands: { skipFailedCommand: mockSkipFailedCommand, - resumeRun: mockResumeRun, } as any, } }) @@ -172,7 +170,7 @@ describe('SkipToNextStep', () => { }, } renderSkipToNextStep(props) - fireEvent.click(screen.getByRole('button', { name: 'Go back' })) + clickButtonLabeled('Go back') expect(mockProceedToRouteAndStep).toHaveBeenCalledWith( RECOVERY_MAP.IGNORE_AND_SKIP.ROUTE ) @@ -180,14 +178,13 @@ describe('SkipToNextStep', () => { it('calls goBackPrevStep when selectedRecoveryOption is not IGNORE_AND_SKIP and secondary button is clicked', () => { renderSkipToNextStep(props) - fireEvent.click(screen.getByRole('button', { name: 'Go back' })) + clickButtonLabeled('Go back') expect(mockGoBackPrevStep).toHaveBeenCalled() }) it('calls the correct routeUpdateActions and recoveryCommands in the correct order when the primary button is clicked', async () => { renderSkipToNextStep(props) - fireEvent.click(screen.getByRole('button', { name: 'Continue run now' })) - + clickButtonLabeled('Continue run now') await waitFor(() => { expect(mockSetRobotInMotion).toHaveBeenCalledWith( true, @@ -197,15 +194,9 @@ describe('SkipToNextStep', () => { await waitFor(() => { expect(mockSkipFailedCommand).toHaveBeenCalled() }) - await waitFor(() => { - expect(mockResumeRun).toHaveBeenCalled() - }) expect(mockSetRobotInMotion.mock.invocationCallOrder[0]).toBeLessThan( mockSkipFailedCommand.mock.invocationCallOrder[0] ) - expect(mockSkipFailedCommand.mock.invocationCallOrder[0]).toBeLessThan( - mockResumeRun.mock.invocationCallOrder[0] - ) }) }) diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/IgnoreErrorSkipStep.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/IgnoreErrorSkipStep.test.tsx index 68b16d11fd3..d6241b7dcd9 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/IgnoreErrorSkipStep.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/IgnoreErrorSkipStep.test.tsx @@ -12,6 +12,7 @@ import { } from '../IgnoreErrorSkipStep' import { RECOVERY_MAP } from '../../constants' import { SelectRecoveryOption } from '../SelectRecoveryOption' +import { clickButtonLabeled } from '../../__tests__/util' import type { Mock } from 'vitest' @@ -19,7 +20,9 @@ vi.mock('../shared', async () => { const actual = await vi.importActual('../shared') return { ...actual, - RecoveryContentWrapper: vi.fn(({ children }) =>
    {children}
    ), + RecoverySingleColumnContentWrapper: vi.fn(({ children }) => ( +
    {children}
    + )), } }) vi.mock('../SelectRecoveryOption') @@ -103,7 +106,7 @@ describe('IgnoreErrorStepHome', () => { it('calls ignoreOnce when "ignore_only_this_error" is selected and primary button is clicked', async () => { renderIgnoreErrorStepHome(props) fireEvent.click(screen.getByText('Ignore only this error')) - fireEvent.click(screen.getByRole('button', { name: 'Continue' })) + clickButtonLabeled('Continue') await waitFor(() => { expect(mockProceedToRouteAndStep).toHaveBeenCalledWith( RECOVERY_MAP.FILL_MANUALLY_AND_SKIP.ROUTE, @@ -115,7 +118,7 @@ describe('IgnoreErrorStepHome', () => { it('calls ignoreAlways when "ignore_all_errors_of_this_type" is selected and primary button is clicked', async () => { renderIgnoreErrorStepHome(props) fireEvent.click(screen.getByText('Ignore all errors of this type')) - fireEvent.click(screen.getByRole('button', { name: 'Continue' })) + clickButtonLabeled('Continue') await waitFor(() => { expect(mockIgnoreErrorKindThisRun).toHaveBeenCalled() }) @@ -129,7 +132,7 @@ describe('IgnoreErrorStepHome', () => { it('calls goBackPrevStep when secondary button is clicked', () => { renderIgnoreErrorStepHome(props) - fireEvent.click(screen.getByRole('button', { name: 'Go back' })) + clickButtonLabeled('Go back') expect(mockGoBackPrevStep).toHaveBeenCalled() }) }) diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/ManageTips.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/ManageTips.test.tsx index bc54129c614..8e9327dd45f 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/ManageTips.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/ManageTips.test.tsx @@ -16,6 +16,7 @@ import { RECOVERY_MAP } from '../../constants' import { DropTipWizardFlows } from '../../../DropTipWizardFlows' import { DT_ROUTES } from '../../../DropTipWizardFlows/constants' import { SelectRecoveryOption } from '../SelectRecoveryOption' +import { clickButtonLabeled } from '../../__tests__/util' import type { Mock } from 'vitest' import type { PipetteModelSpecs } from '@opentrons/shared-data' @@ -92,25 +93,24 @@ describe('ManageTips', () => { screen.getByText( 'You may want to remove the tips from the left pipette before using it again in a protocol' ) - screen.getByText('Begin removal') - screen.getByText('Skip') - screen.getByText('Continue') + screen.queryAllByText('Begin removal') + screen.queryAllByText('Skip') + expect(screen.getAllByText('Continue').length).toBe(2) }) it('routes correctly when continuing on BeginRemoval', () => { render(props) - const beginRemovalBtn = screen.getByText('Begin removal') - const skipBtn = screen.getByText('Skip') - const continueBtn = screen.getByRole('button', { name: 'Continue' }) + const beginRemovalBtn = screen.queryAllByText('Begin removal')[0] + const skipBtn = screen.queryAllByText('Skip')[0] fireEvent.click(beginRemovalBtn) - fireEvent.click(continueBtn) + clickButtonLabeled('Continue') expect(mockProceedNextStep).toHaveBeenCalled() fireEvent.click(skipBtn) - fireEvent.click(continueBtn) + clickButtonLabeled('Continue') expect(mockSetRobotInMotion).toHaveBeenCalled() }) @@ -124,11 +124,10 @@ describe('ManageTips', () => { } render(props) - const skipBtn = screen.getByText('Skip') - const continueBtn = screen.getByRole('button', { name: 'Continue' }) + const skipBtn = screen.queryAllByText('Skip')[0] fireEvent.click(skipBtn) - fireEvent.click(continueBtn) + clickButtonLabeled('Continue') expect(mockProceedToRouteAndStep).toHaveBeenCalledWith( RETRY_NEW_TIPS.ROUTE, diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/RetryNewTips.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/RetryNewTips.test.tsx index 8e79d193f8f..708c847cd70 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/RetryNewTips.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/RetryNewTips.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { describe, it, vi, expect, beforeEach, afterEach } from 'vitest' -import { screen, fireEvent, waitFor } from '@testing-library/react' +import { screen, waitFor } from '@testing-library/react' import { mockRecoveryContentProps } from '../../__fixtures__' import { renderWithProviders } from '../../../../__testing-utils__' @@ -8,6 +8,7 @@ import { i18n } from '../../../../i18n' import { RetryNewTips, RetryWithNewTips } from '../RetryNewTips' import { RECOVERY_MAP } from '../../constants' import { SelectRecoveryOption } from '../SelectRecoveryOption' +import { clickButtonLabeled } from '../../__tests__/util' import type { Mock } from 'vitest' @@ -155,7 +156,7 @@ describe('RetryWithNewTips', () => { it('calls the correct routeUpdateActions and recoveryCommands in the correct order when the primary button is clicked', async () => { renderRetryWithNewTips(props) - fireEvent.click(screen.getByRole('button', { name: 'Retry now' })) + clickButtonLabeled('Retry now') await waitFor(() => { expect(mockSetRobotInMotion).toHaveBeenCalledWith( diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/RetrySameTips.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/RetrySameTips.test.tsx index f2e3cbd2b48..da7e85fcadc 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/RetrySameTips.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/RetrySameTips.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { describe, it, vi, expect, beforeEach, afterEach } from 'vitest' -import { screen, fireEvent, waitFor } from '@testing-library/react' +import { screen, waitFor } from '@testing-library/react' import { mockRecoveryContentProps } from '../../__fixtures__' import { renderWithProviders } from '../../../../__testing-utils__' @@ -8,6 +8,7 @@ import { i18n } from '../../../../i18n' import { RetrySameTips, RetrySameTipsInfo } from '../RetrySameTips' import { RECOVERY_MAP } from '../../constants' import { SelectRecoveryOption } from '../SelectRecoveryOption' +import { clickButtonLabeled } from '../../__tests__/util' import type { Mock } from 'vitest' @@ -106,7 +107,8 @@ describe('RetrySameTipsInfo', () => { it('calls the correct routeUpdateActions and recoveryCommands in the correct order when the primary button is clicked', async () => { renderRetrySameTipsInfo(props) - fireEvent.click(screen.getByRole('button', { name: 'Retry now' })) + + clickButtonLabeled('Retry now') await waitFor(() => { expect(mockSetRobotInMotion).toHaveBeenCalledWith( diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/RetryStep.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/RetryStep.test.tsx index bbd0b6a742e..40a7095b51d 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/RetryStep.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/RetryStep.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { describe, it, vi, expect, beforeEach, afterEach } from 'vitest' -import { screen, fireEvent, waitFor } from '@testing-library/react' +import { screen, waitFor } from '@testing-library/react' import { mockRecoveryContentProps } from '../../__fixtures__' import { renderWithProviders } from '../../../../__testing-utils__' @@ -9,6 +9,8 @@ import { RetryStep, RetryStepInfo } from '../RetryStep' import { RECOVERY_MAP } from '../../constants' import { SelectRecoveryOption } from '../SelectRecoveryOption' +import { clickButtonLabeled } from '../../__tests__/util' + import type { Mock } from 'vitest' vi.mock('../../../../molecules/Command') @@ -108,7 +110,7 @@ describe('RetryStepInfo', () => { it('calls the correct routeUpdateActions and recoveryCommands in the correct order when the primary button is clicked', async () => { renderRetryStepInfo(props) - fireEvent.click(screen.getByRole('button', { name: 'Retry now' })) + clickButtonLabeled('Retry now') await waitFor(() => { expect(mockSetRobotInMotion).toHaveBeenCalledWith( diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SelectRecoveryOptions.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SelectRecoveryOptions.test.tsx index c73afd73539..a70e66d662e 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SelectRecoveryOptions.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SelectRecoveryOptions.test.tsx @@ -8,7 +8,8 @@ import { i18n } from '../../../../i18n' import { mockRecoveryContentProps } from '../../__fixtures__' import { SelectRecoveryOption, - RecoveryOptions, + ODDRecoveryOptions, + DesktopRecoveryOptions, getRecoveryOptions, GENERAL_ERROR_OPTIONS, OVERPRESSURE_WHILE_ASPIRATING_OPTIONS, @@ -17,21 +18,32 @@ import { NO_LIQUID_DETECTED_OPTIONS, } from '../SelectRecoveryOption' import { RECOVERY_MAP, ERROR_KINDS } from '../../constants' +import { clickButtonLabeled } from '../../__tests__/util' import type { Mock } from 'vitest' const renderSelectRecoveryOption = ( props: React.ComponentProps ) => { - return renderWithProviders(, { + return renderWithProviders( + , + { + i18nInstance: i18n, + } + )[0] +} + +const renderODDRecoveryOptions = ( + props: React.ComponentProps +) => { + return renderWithProviders(, { i18nInstance: i18n, })[0] } - -const renderRecoveryOptions = ( - props: React.ComponentProps +const renderDesktopRecoveryOptions = ( + props: React.ComponentProps ) => { - return renderWithProviders(, { + return renderWithProviders(, { i18nInstance: i18n, })[0] } @@ -88,8 +100,7 @@ describe('SelectRecoveryOption', () => { it('sets the selected recovery option when clicking continue', () => { renderSelectRecoveryOption(props) - const continueBtn = screen.getByRole('button', { name: 'Continue' }) - fireEvent.click(continueBtn) + clickButtonLabeled('Continue') expect(mockSetSelectedRecoveryOption).toHaveBeenCalledWith( RETRY_FAILED_COMMAND.ROUTE @@ -101,14 +112,14 @@ describe('SelectRecoveryOption', () => { screen.getByText('Choose a recovery action') - const retryStepOption = screen.getByRole('label', { name: 'Retry step' }) - const continueBtn = screen.getByRole('button', { name: 'Continue' }) + const retryStepOption = screen.getAllByRole('label', { name: 'Retry step' }) + clickButtonLabeled('Continue') expect( screen.queryByRole('button', { name: 'Go back' }) ).not.toBeInTheDocument() - fireEvent.click(retryStepOption) - fireEvent.click(continueBtn) + fireEvent.click(retryStepOption[0]) + clickButtonLabeled('Continue') expect(mockProceedToRouteAndStep).toHaveBeenCalledWith( RETRY_FAILED_COMMAND.ROUTE @@ -125,16 +136,15 @@ describe('SelectRecoveryOption', () => { screen.getByText('Choose a recovery action') - const retryNewTips = screen.getByRole('label', { + const retryNewTips = screen.getAllByRole('label', { name: 'Retry with new tips', }) - const continueBtn = screen.getByRole('button', { name: 'Continue' }) expect( screen.queryByRole('button', { name: 'Go back' }) ).not.toBeInTheDocument() - fireEvent.click(retryNewTips) - fireEvent.click(continueBtn) + fireEvent.click(retryNewTips[0]) + clickButtonLabeled('Continue') expect(mockProceedToRouteAndStep).toHaveBeenCalledWith(RETRY_NEW_TIPS.ROUTE) }) @@ -149,13 +159,12 @@ describe('SelectRecoveryOption', () => { screen.getByText('Choose a recovery action') - const fillManuallyAndSkip = screen.getByRole('label', { + const fillManuallyAndSkip = screen.getAllByRole('label', { name: 'Manually fill well and skip to next step', }) - const continueBtn = screen.getByRole('button', { name: 'Continue' }) - fireEvent.click(fillManuallyAndSkip) - fireEvent.click(continueBtn) + fireEvent.click(fillManuallyAndSkip[0]) + clickButtonLabeled('Continue') expect(mockProceedToRouteAndStep).toHaveBeenCalledWith( RECOVERY_MAP.FILL_MANUALLY_AND_SKIP.ROUTE @@ -172,13 +181,12 @@ describe('SelectRecoveryOption', () => { screen.getByText('Choose a recovery action') - const retrySameTips = screen.getByRole('label', { + const retrySameTips = screen.getAllByRole('label', { name: 'Retry with same tips', }) - const continueBtn = screen.getByRole('button', { name: 'Continue' }) - fireEvent.click(retrySameTips) - fireEvent.click(continueBtn) + fireEvent.click(retrySameTips[0]) + clickButtonLabeled('Continue') expect(mockProceedToRouteAndStep).toHaveBeenCalledWith( RECOVERY_MAP.RETRY_SAME_TIPS.ROUTE @@ -195,130 +203,135 @@ describe('SelectRecoveryOption', () => { screen.getByText('Choose a recovery action') - const skipStepWithSameTips = screen.getByRole('label', { + const skipStepWithSameTips = screen.getAllByRole('label', { name: 'Skip to next step with same tips', }) - const continueBtn = screen.getByRole('button', { name: 'Continue' }) - fireEvent.click(skipStepWithSameTips) - fireEvent.click(continueBtn) + fireEvent.click(skipStepWithSameTips[0]) + clickButtonLabeled('Continue') expect(mockProceedToRouteAndStep).toHaveBeenCalledWith( RECOVERY_MAP.SKIP_STEP_WITH_SAME_TIPS.ROUTE ) }) }) +;([ + ['desktop', renderDesktopRecoveryOptions] as const, + ['odd', renderODDRecoveryOptions] as const, +] as const).forEach(([target, renderer]) => { + describe(`RecoveryOptions on ${target}`, () => { + let props: React.ComponentProps + let mockSetSelectedRoute: Mock + let mockGetRecoveryOptionCopy: Mock + + beforeEach(() => { + mockSetSelectedRoute = vi.fn() + mockGetRecoveryOptionCopy = vi.fn() + const generalRecoveryOptions = getRecoveryOptions( + ERROR_KINDS.GENERAL_ERROR + ) + + props = { + validRecoveryOptions: generalRecoveryOptions, + setSelectedRoute: mockSetSelectedRoute, + getRecoveryOptionCopy: mockGetRecoveryOptionCopy, + } + + when(mockGetRecoveryOptionCopy) + .calledWith(RECOVERY_MAP.RETRY_FAILED_COMMAND.ROUTE) + .thenReturn('Retry step') + when(mockGetRecoveryOptionCopy) + .calledWith(RECOVERY_MAP.CANCEL_RUN.ROUTE) + .thenReturn('Cancel run') + when(mockGetRecoveryOptionCopy) + .calledWith(RECOVERY_MAP.RETRY_NEW_TIPS.ROUTE) + .thenReturn('Retry with new tips') + when(mockGetRecoveryOptionCopy) + .calledWith(RECOVERY_MAP.FILL_MANUALLY_AND_SKIP.ROUTE) + .thenReturn('Manually fill well and skip to next step') + when(mockGetRecoveryOptionCopy) + .calledWith(RECOVERY_MAP.RETRY_SAME_TIPS.ROUTE) + .thenReturn('Retry with same tips') + when(mockGetRecoveryOptionCopy) + .calledWith(RECOVERY_MAP.SKIP_STEP_WITH_SAME_TIPS.ROUTE) + .thenReturn('Skip to next step with same tips') + when(mockGetRecoveryOptionCopy) + .calledWith(RECOVERY_MAP.SKIP_STEP_WITH_NEW_TIPS.ROUTE) + .thenReturn('Skip to next step with new tips') + when(mockGetRecoveryOptionCopy) + .calledWith(RECOVERY_MAP.IGNORE_AND_SKIP.ROUTE) + .thenReturn('Ignore error and skip to next step') + }) -describe('RecoveryOptions', () => { - let props: React.ComponentProps - let mockSetSelectedRoute: Mock - let mockGetRecoveryOptionCopy: Mock - - beforeEach(() => { - mockSetSelectedRoute = vi.fn() - mockGetRecoveryOptionCopy = vi.fn() - const generalRecoveryOptions = getRecoveryOptions(ERROR_KINDS.GENERAL_ERROR) - - props = { - validRecoveryOptions: generalRecoveryOptions, - setSelectedRoute: mockSetSelectedRoute, - getRecoveryOptionCopy: mockGetRecoveryOptionCopy, - } - - when(mockGetRecoveryOptionCopy) - .calledWith(RECOVERY_MAP.RETRY_FAILED_COMMAND.ROUTE) - .thenReturn('Retry step') - when(mockGetRecoveryOptionCopy) - .calledWith(RECOVERY_MAP.CANCEL_RUN.ROUTE) - .thenReturn('Cancel run') - when(mockGetRecoveryOptionCopy) - .calledWith(RECOVERY_MAP.RETRY_NEW_TIPS.ROUTE) - .thenReturn('Retry with new tips') - when(mockGetRecoveryOptionCopy) - .calledWith(RECOVERY_MAP.FILL_MANUALLY_AND_SKIP.ROUTE) - .thenReturn('Manually fill well and skip to next step') - when(mockGetRecoveryOptionCopy) - .calledWith(RECOVERY_MAP.RETRY_SAME_TIPS.ROUTE) - .thenReturn('Retry with same tips') - when(mockGetRecoveryOptionCopy) - .calledWith(RECOVERY_MAP.SKIP_STEP_WITH_SAME_TIPS.ROUTE) - .thenReturn('Skip to next step with same tips') - when(mockGetRecoveryOptionCopy) - .calledWith(RECOVERY_MAP.SKIP_STEP_WITH_NEW_TIPS.ROUTE) - .thenReturn('Skip to next step with new tips') - when(mockGetRecoveryOptionCopy) - .calledWith(RECOVERY_MAP.IGNORE_AND_SKIP.ROUTE) - .thenReturn('Ignore error and skip to next step') - }) - - it('renders valid recovery options for a general error errorKind', () => { - renderRecoveryOptions(props) + it('renders valid recovery options for a general error errorKind', () => { + renderer(props) - screen.getByRole('label', { name: 'Retry step' }) - screen.getByRole('label', { name: 'Cancel run' }) - }) + screen.getByRole('label', { name: 'Retry step' }) + screen.getByRole('label', { name: 'Cancel run' }) + }) - it(`renders valid recovery options for a ${ERROR_KINDS.OVERPRESSURE_WHILE_ASPIRATING} errorKind`, () => { - props = { - ...props, - validRecoveryOptions: OVERPRESSURE_WHILE_ASPIRATING_OPTIONS, - } + it(`renders valid recovery options for a ${ERROR_KINDS.OVERPRESSURE_WHILE_ASPIRATING} errorKind`, () => { + props = { + ...props, + validRecoveryOptions: OVERPRESSURE_WHILE_ASPIRATING_OPTIONS, + } - renderRecoveryOptions(props) + renderer(props) - screen.getByRole('label', { name: 'Retry with new tips' }) - screen.getByRole('label', { name: 'Cancel run' }) - }) + screen.getByRole('label', { name: 'Retry with new tips' }) + screen.getByRole('label', { name: 'Cancel run' }) + }) - it('updates the selectedRoute when a new option is selected', () => { - renderRecoveryOptions(props) + it('updates the selectedRoute when a new option is selected', () => { + renderer(props) - fireEvent.click(screen.getByRole('label', { name: 'Cancel run' })) + fireEvent.click(screen.getByRole('label', { name: 'Cancel run' })) - expect(mockSetSelectedRoute).toHaveBeenCalledWith( - RECOVERY_MAP.CANCEL_RUN.ROUTE - ) - }) + expect(mockSetSelectedRoute).toHaveBeenCalledWith( + RECOVERY_MAP.CANCEL_RUN.ROUTE + ) + }) - it(`renders valid recovery options for a ${ERROR_KINDS.NO_LIQUID_DETECTED} errorKind`, () => { - props = { - ...props, - validRecoveryOptions: NO_LIQUID_DETECTED_OPTIONS, - } + it(`renders valid recovery options for a ${ERROR_KINDS.NO_LIQUID_DETECTED} errorKind`, () => { + props = { + ...props, + validRecoveryOptions: NO_LIQUID_DETECTED_OPTIONS, + } - renderRecoveryOptions(props) + renderer(props) - screen.getByRole('label', { - name: 'Manually fill well and skip to next step', + screen.getByRole('label', { + name: 'Manually fill well and skip to next step', + }) + screen.getByRole('label', { name: 'Ignore error and skip to next step' }) + screen.getByRole('label', { name: 'Cancel run' }) }) - screen.getByRole('label', { name: 'Ignore error and skip to next step' }) - screen.getByRole('label', { name: 'Cancel run' }) - }) - it(`renders valid recovery options for a ${ERROR_KINDS.OVERPRESSURE_PREPARE_TO_ASPIRATE} errorKind`, () => { - props = { - ...props, - validRecoveryOptions: OVERPRESSURE_PREPARE_TO_ASPIRATE, - } + it(`renders valid recovery options for a ${ERROR_KINDS.OVERPRESSURE_PREPARE_TO_ASPIRATE} errorKind`, () => { + props = { + ...props, + validRecoveryOptions: OVERPRESSURE_PREPARE_TO_ASPIRATE, + } - renderRecoveryOptions(props) + renderer(props) - screen.getByRole('label', { name: 'Retry with new tips' }) - screen.getByRole('label', { name: 'Retry with same tips' }) - screen.getByRole('label', { name: 'Cancel run' }) - }) + screen.getByRole('label', { name: 'Retry with new tips' }) + screen.getByRole('label', { name: 'Retry with same tips' }) + screen.getByRole('label', { name: 'Cancel run' }) + }) - it(`renders valid recovery options for a ${ERROR_KINDS.OVERPRESSURE_WHILE_DISPENSING} errorKind`, () => { - props = { - ...props, - validRecoveryOptions: OVERPRESSURE_WHILE_DISPENSING_OPTIONS, - } + it(`renders valid recovery options for a ${ERROR_KINDS.OVERPRESSURE_WHILE_DISPENSING} errorKind`, () => { + props = { + ...props, + validRecoveryOptions: OVERPRESSURE_WHILE_DISPENSING_OPTIONS, + } - renderRecoveryOptions(props) + renderer(props) - screen.getByRole('label', { name: 'Skip to next step with same tips' }) - screen.getByRole('label', { name: 'Skip to next step with new tips' }) - screen.getByRole('label', { name: 'Cancel run' }) + screen.getByRole('label', { name: 'Skip to next step with same tips' }) + screen.getByRole('label', { name: 'Skip to next step with new tips' }) + screen.getByRole('label', { name: 'Cancel run' }) + }) }) }) diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SkipStepNewTips.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SkipStepNewTips.test.tsx index 25ea34dfbf3..afb8dfdee48 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SkipStepNewTips.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SkipStepNewTips.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { describe, it, vi, expect, beforeEach, afterEach } from 'vitest' -import { screen, fireEvent, waitFor } from '@testing-library/react' +import { screen, waitFor } from '@testing-library/react' import { mockRecoveryContentProps } from '../../__fixtures__' import { renderWithProviders } from '../../../../__testing-utils__' @@ -8,6 +8,7 @@ import { i18n } from '../../../../i18n' import { SkipStepNewTips, SkipStepWithNewTips } from '../SkipStepNewTips' import { RECOVERY_MAP } from '../../constants' import { SelectRecoveryOption } from '../SelectRecoveryOption' +import { clickButtonLabeled } from '../../__tests__/util' import type { Mock } from 'vitest' @@ -123,12 +124,10 @@ describe('SkipStepWithNewTips', () => { let props: React.ComponentProps let mockSetRobotInMotion: Mock let mockSkipFailedCommand: Mock - let mockResumeRun: Mock beforeEach(() => { mockSetRobotInMotion = vi.fn(() => Promise.resolve()) mockSkipFailedCommand = vi.fn(() => Promise.resolve()) - mockResumeRun = vi.fn() props = { ...mockRecoveryContentProps, @@ -137,7 +136,6 @@ describe('SkipStepWithNewTips', () => { } as any, recoveryCommands: { skipFailedCommand: mockSkipFailedCommand, - resumeRun: mockResumeRun, } as any, } }) @@ -157,7 +155,7 @@ describe('SkipStepWithNewTips', () => { it('calls the correct routeUpdateActions and recoveryCommands in the correct order when the primary button is clicked', async () => { renderSkipStepWithNewTips(props) - fireEvent.click(screen.getByRole('button', { name: 'Continue run now' })) + clickButtonLabeled('Continue run now') await waitFor(() => { expect(mockSetRobotInMotion).toHaveBeenCalledWith( @@ -168,15 +166,9 @@ describe('SkipStepWithNewTips', () => { await waitFor(() => { expect(mockSkipFailedCommand).toHaveBeenCalled() }) - await waitFor(() => { - expect(mockResumeRun).toHaveBeenCalled() - }) expect(mockSetRobotInMotion.mock.invocationCallOrder[0]).toBeLessThan( mockSkipFailedCommand.mock.invocationCallOrder[0] ) - expect(mockSkipFailedCommand.mock.invocationCallOrder[0]).toBeLessThan( - mockResumeRun.mock.invocationCallOrder[0] - ) }) }) diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SkipStepSameTips.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SkipStepSameTips.test.tsx index 6c8c3ec4c54..3459ce305dd 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SkipStepSameTips.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SkipStepSameTips.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { describe, it, vi, expect, beforeEach, afterEach } from 'vitest' -import { screen, fireEvent, waitFor } from '@testing-library/react' +import { screen, waitFor } from '@testing-library/react' import { mockRecoveryContentProps } from '../../__fixtures__' import { renderWithProviders } from '../../../../__testing-utils__' @@ -9,6 +9,8 @@ import { SkipStepSameTips, SkipStepSameTipsInfo } from '../SkipStepSameTips' import { RECOVERY_MAP } from '../../constants' import { SelectRecoveryOption } from '../SelectRecoveryOption' +import { clickButtonLabeled } from '../../__tests__/util' + import type { Mock } from 'vitest' vi.mock('../../../../molecules/Command') @@ -70,12 +72,10 @@ describe('SkipStepSameTipsInfo', () => { let props: React.ComponentProps let mockSetRobotInMotion: Mock let mockSkipFailedCommand: Mock - let mockResumeRun: Mock beforeEach(() => { mockSetRobotInMotion = vi.fn(() => Promise.resolve()) mockSkipFailedCommand = vi.fn(() => Promise.resolve()) - mockResumeRun = vi.fn() props = { ...mockRecoveryContentProps, @@ -84,7 +84,6 @@ describe('SkipStepSameTipsInfo', () => { } as any, recoveryCommands: { skipFailedCommand: mockSkipFailedCommand, - resumeRun: mockResumeRun, } as any, } }) @@ -104,7 +103,7 @@ describe('SkipStepSameTipsInfo', () => { it('calls the correct routeUpdateActions and recoveryCommands in the correct order when the primary button is clicked', async () => { renderSkipStepSameTipsInfo(props) - fireEvent.click(screen.getByRole('button', { name: 'Continue run now' })) + clickButtonLabeled('Continue run now') await waitFor(() => { expect(mockSetRobotInMotion).toHaveBeenCalledWith( @@ -112,18 +111,13 @@ describe('SkipStepSameTipsInfo', () => { RECOVERY_MAP.ROBOT_SKIPPING_STEP.ROUTE ) }) + await waitFor(() => { expect(mockSkipFailedCommand).toHaveBeenCalled() }) - await waitFor(() => { - expect(mockResumeRun).toHaveBeenCalled() - }) expect(mockSetRobotInMotion.mock.invocationCallOrder[0]).toBeLessThan( mockSkipFailedCommand.mock.invocationCallOrder[0] ) - expect(mockSkipFailedCommand.mock.invocationCallOrder[0]).toBeLessThan( - mockResumeRun.mock.invocationCallOrder[0] - ) }) }) diff --git a/app/src/organisms/ErrorRecoveryFlows/RunPausedSplash.tsx b/app/src/organisms/ErrorRecoveryFlows/RunPausedSplash.tsx index 9b12e75e173..f9d253719ed 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RunPausedSplash.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RunPausedSplash.tsx @@ -30,7 +30,7 @@ import { LargeButton } from '../../atoms/buttons' import { RECOVERY_MAP } from './constants' import { RecoveryInterventionModal, - RecoveryContentWrapper, + RecoverySingleColumnContentWrapper, StepInfo, } from './shared' @@ -39,9 +39,13 @@ import type { ErrorRecoveryFlowsProps } from '.' import type { ERUtilsResults } from './hooks' import { useHost } from '@opentrons/react-api-client' -export function useRunPausedSplash(showERWizard: boolean): boolean { - // Don't show the splash when the ER wizard is active. - return !showERWizard +export function useRunPausedSplash( + isOnDevice: boolean, + showERWizard: boolean +): boolean { + // Don't show the splash when desktop ER wizard is active, + // but always show it on the ODD (with or without the wizard rendered above it). + return !(!isOnDevice && showERWizard) } type RunPausedSplashProps = ERUtilsResults & { @@ -54,7 +58,7 @@ type RunPausedSplashProps = ERUtilsResults & { export function RunPausedSplash( props: RunPausedSplashProps ): JSX.Element | null { - const { toggleERWiz, routeUpdateActions, failedCommand } = props + const { isOnDevice, toggleERWiz, routeUpdateActions, failedCommand } = props const { t } = useTranslation('error_recovery') const errorKind = getErrorKind(failedCommand) const title = useErrorName(errorKind) @@ -88,7 +92,7 @@ export function RunPausedSplash( // TODO(jh 06-18-24): Instead of passing stepCount internally, we probably want to // pass it in as a prop to ErrorRecoveryFlows to ameliorate blippy "step = ? -> step = 24" behavior. - if (props.isOnDevice) { + if (isOnDevice) { return ( - +
    - + ) } diff --git a/app/src/organisms/ErrorRecoveryFlows/__fixtures__/index.ts b/app/src/organisms/ErrorRecoveryFlows/__fixtures__/index.ts index 183feec96f5..7666d7beebf 100644 --- a/app/src/organisms/ErrorRecoveryFlows/__fixtures__/index.ts +++ b/app/src/organisms/ErrorRecoveryFlows/__fixtures__/index.ts @@ -3,6 +3,7 @@ import { getLabwareDefURI, opentrons96PcrAdapterV1, } from '@opentrons/shared-data' +import { RUN_STATUS_AWAITING_RECOVERY } from '@opentrons/api-client' import { RECOVERY_MAP } from '../constants' import { mockRobotSideAnalysis } from '../../../molecules/Command/__fixtures__' @@ -56,8 +57,10 @@ export const mockRecoveryContentProps: RecoveryContentProps = { errorKind: 'GENERAL_ERROR', robotType: FLEX_ROBOT_TYPE, runId: 'MOCK_RUN_ID', + isDoorOpen: false, isFlex: true, isOnDevice: true, + runStatus: RUN_STATUS_AWAITING_RECOVERY, recoveryMap: { route: RECOVERY_MAP.OPTION_SELECTION.ROUTE, step: RECOVERY_MAP.OPTION_SELECTION.STEPS.SELECT, @@ -70,7 +73,7 @@ export const mockRecoveryContentProps: RecoveryContentProps = { failedPipetteInfo: {} as any, deckMapUtils: { setSelectedLocation: () => {} } as any, stepCounts: {} as any, - protocolAnalysis: { commands: [mockFailedCommand] } as any, + protocolAnalysis: mockRobotSideAnalysis, trackExternalMap: () => null, hasLaunchedRecovery: true, getRecoveryOptionCopy: () => 'MOCK_COPY', @@ -78,4 +81,5 @@ export const mockRecoveryContentProps: RecoveryContentProps = { mockRobotSideAnalysis.commands[mockRobotSideAnalysis.commands.length - 2], mockRobotSideAnalysis.commands[mockRobotSideAnalysis.commands.length - 1], ], + recoveryActionMutationUtils: {} as any, } diff --git a/app/src/organisms/ErrorRecoveryFlows/__tests__/BeforeBeginning.test.tsx b/app/src/organisms/ErrorRecoveryFlows/__tests__/BeforeBeginning.test.tsx deleted file mode 100644 index 676fad63a88..00000000000 --- a/app/src/organisms/ErrorRecoveryFlows/__tests__/BeforeBeginning.test.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import * as React from 'react' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { fireEvent, screen } from '@testing-library/react' - -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' -import { mockRecoveryContentProps } from '../__fixtures__' -import { BeforeBeginning } from '../BeforeBeginning' -import { RECOVERY_MAP } from '../constants' - -import type { Mock } from 'vitest' - -const render = (props: React.ComponentProps) => { - return renderWithProviders(, { - i18nInstance: i18n, - })[0] -} - -describe('BeforeBeginning', () => { - const { BEFORE_BEGINNING } = RECOVERY_MAP - let props: React.ComponentProps - let mockProceedNextStep: Mock - - beforeEach(() => { - mockProceedNextStep = vi.fn() - const mockRouteUpdateActions = { - proceedNextStep: mockProceedNextStep, - } as any - - props = { - ...mockRecoveryContentProps, - routeUpdateActions: mockRouteUpdateActions, - recoveryMap: { - route: BEFORE_BEGINNING.ROUTE, - step: BEFORE_BEGINNING.STEPS.RECOVERY_DESCRIPTION, - }, - } - }) - - it('renders appropriate copy and click behavior', () => { - render(props) - - screen.getByText('Before you begin') - screen.queryByText( - 'Recovery Mode provides you with guided and manual controls for handling errors at runtime.' - ) - - const primaryBtn = screen.getByRole('button', { - name: 'View recovery options', - }) - - fireEvent.click(primaryBtn) - - expect(mockProceedNextStep).toHaveBeenCalled() - }) -}) diff --git a/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryFlows.test.tsx b/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryFlows.test.tsx index ba592e78191..427bb8d00cd 100644 --- a/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryFlows.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryFlows.test.tsx @@ -12,7 +12,11 @@ import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' import { mockFailedCommand } from '../__fixtures__' import { ErrorRecoveryFlows, useErrorRecoveryFlows } from '..' -import { useCurrentlyRecoveringFrom, useERUtils } from '../hooks' +import { + useCurrentlyRecoveringFrom, + useERUtils, + useShowDoorInfo, +} from '../hooks' import { useFeatureFlag } from '../../../redux/config' import { useERWizard, ErrorRecoveryWizard } from '../ErrorRecoveryWizard' import { useRunPausedSplash, RunPausedSplash } from '../RunPausedSplash' @@ -122,6 +126,7 @@ describe('ErrorRecovery', () => { beforeEach(() => { props = { + runStatus: RUN_STATUS_AWAITING_RECOVERY, failedCommand: mockFailedCommand, runId: 'MOCK_RUN_ID', isFlex: true, @@ -139,6 +144,7 @@ describe('ErrorRecovery', () => { }) vi.mocked(useRunPausedSplash).mockReturnValue(true) vi.mocked(useERUtils).mockReturnValue({ routeUpdateActions: {} } as any) + vi.mocked(useShowDoorInfo).mockReturnValue(false) }) it('renders the wizard when the wizard is toggled on', () => { @@ -146,6 +152,18 @@ describe('ErrorRecovery', () => { screen.getByText('MOCK WIZARD') }) + it('renders the wizard when isDoorOpen is true', () => { + vi.mocked(useShowDoorInfo).mockReturnValue(true) + vi.mocked(useERWizard).mockReturnValue({ + hasLaunchedRecovery: false, + toggleERWizard: () => Promise.resolve(), + showERWizard: false, + }) + + render(props) + screen.getByText('MOCK WIZARD') + }) + it('does not render the wizard when the wizard is toggled off', () => { vi.mocked(useERWizard).mockReturnValue({ hasLaunchedRecovery: true, diff --git a/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryWizard.test.tsx b/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryWizard.test.tsx index a3656163f20..3c5174bf54d 100644 --- a/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryWizard.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryWizard.test.tsx @@ -9,9 +9,9 @@ import { ErrorRecoveryContent, useInitialPipetteHome, useERWizard, + ErrorRecoveryComponent, } from '../ErrorRecoveryWizard' import { RECOVERY_MAP } from '../constants' -import { BeforeBeginning } from '../BeforeBeginning' import { SelectRecoveryOption, RetryStep, @@ -26,15 +26,23 @@ import { } from '../RecoveryOptions' import { RecoveryInProgress } from '../RecoveryInProgress' import { RecoveryError } from '../RecoveryError' +import { RecoveryDoorOpen } from '../RecoveryDoorOpen' +import { useErrorDetailsModal, ErrorDetailsModal } from '../shared' import type { Mock } from 'vitest' -vi.mock('../BeforeBeginning') vi.mock('../RecoveryOptions') vi.mock('../RecoveryInProgress') vi.mock('../RecoveryError') -vi.mock('../shared') - +vi.mock('../RecoveryDoorOpen') +vi.mock('../shared', async importOriginal => { + const actual = await importOriginal() + return { + ...actual, + useErrorDetailsModal: vi.fn(), + ErrorDetailsModal: vi.fn(), + } +}) describe('useERWizard', () => { it('has correct initial values', () => { const { result } = renderHook(() => useERWizard()) @@ -61,6 +69,65 @@ describe('useERWizard', () => { }) }) +const renderRecoveryComponent = ( + props: React.ComponentProps +) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +describe('ErrorRecoveryComponent', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = mockRecoveryContentProps + + vi.mocked(RecoveryDoorOpen).mockReturnValue( +
    MOCK_RECOVERY_DOOR_OPEN
    + ) + vi.mocked(ErrorDetailsModal).mockReturnValue(
    ERROR_DETAILS_MODAL
    ) + vi.mocked(useErrorDetailsModal).mockReturnValue({ + toggleModal: vi.fn(), + showModal: false, + }) + vi.mocked(SelectRecoveryOption).mockReturnValue( +
    MOCK_SELECT_RECOVERY_OPTION
    + ) + }) + + it('renders appropriate header copy', () => { + renderRecoveryComponent(props) + + screen.getByText('View error details') + }) + + it('renders the error details modal when there is an error', () => { + vi.mocked(useErrorDetailsModal).mockReturnValue({ + toggleModal: vi.fn(), + showModal: true, + }) + + renderRecoveryComponent(props) + + screen.getByText('ERROR_DETAILS_MODAL') + }) + + it('renders the recovery door modal when isDoorOpen is true', () => { + props = { ...props, isDoorOpen: true } + + renderRecoveryComponent(props) + + screen.getByText('MOCK_RECOVERY_DOOR_OPEN') + }) + + it('renders recovery content when isDoorOpen is false', () => { + renderRecoveryComponent(props) + + screen.getByText('MOCK_SELECT_RECOVERY_OPTION') + }) +}) + const renderRecoveryContent = ( props: React.ComponentProps ) => { @@ -72,7 +139,6 @@ const renderRecoveryContent = ( describe('ErrorRecoveryContent', () => { const { OPTION_SELECTION, - BEFORE_BEGINNING, RETRY_FAILED_COMMAND, ROBOT_CANCELING, ROBOT_RESUMING, @@ -99,7 +165,6 @@ describe('ErrorRecoveryContent', () => { vi.mocked(SelectRecoveryOption).mockReturnValue(
    MOCK_SELECT_RECOVERY_OPTION
    ) - vi.mocked(BeforeBeginning).mockReturnValue(
    MOCK_BEFORE_BEGINNING
    ) vi.mocked(RetryStep).mockReturnValue(
    MOCK_RESUME_RUN
    ) vi.mocked(RecoveryInProgress).mockReturnValue(
    MOCK_IN_PROGRESS
    ) vi.mocked(CancelRun).mockReturnValue(
    MOCK_CANCEL_RUN
    ) @@ -125,19 +190,6 @@ describe('ErrorRecoveryContent', () => { screen.getByText('MOCK_SELECT_RECOVERY_OPTION') }) - it(`returns BeforeBeginning when the route is ${BEFORE_BEGINNING.ROUTE}`, () => { - props = { - ...props, - recoveryMap: { - ...props.recoveryMap, - route: BEFORE_BEGINNING.ROUTE, - }, - } - renderRecoveryContent(props) - - screen.getByText('MOCK_BEFORE_BEGINNING') - }) - it(`returns ResumeRun when the route is ${RETRY_FAILED_COMMAND.ROUTE}`, () => { props = { ...props, @@ -416,4 +468,3 @@ describe('useInitialPipetteHome', () => { }) }) }) -it.todo('add test for ErrorRecoveryComponent.') diff --git a/app/src/organisms/ErrorRecoveryFlows/__tests__/RecoveryDoorOpen.test.tsx b/app/src/organisms/ErrorRecoveryFlows/__tests__/RecoveryDoorOpen.test.tsx new file mode 100644 index 00000000000..e224d9cb33b --- /dev/null +++ b/app/src/organisms/ErrorRecoveryFlows/__tests__/RecoveryDoorOpen.test.tsx @@ -0,0 +1,53 @@ +import * as React from 'react' +import { describe, it, beforeEach, vi, expect } from 'vitest' +import { screen } from '@testing-library/react' +import { RUN_STATUS_AWAITING_RECOVERY_PAUSED } from '@opentrons/api-client' + +import { renderWithProviders } from '../../../__testing-utils__' +import { mockRecoveryContentProps } from '../__fixtures__' +import { i18n } from '../../../i18n' +import { RecoveryDoorOpen } from '../RecoveryDoorOpen' + +import type { Mock } from 'vitest' +import { clickButtonLabeled } from './util' + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +describe('RecoveryDoorOpen', () => { + let props: React.ComponentProps + let mockResumeRecovery: Mock + + beforeEach(() => { + mockResumeRecovery = vi.fn() + props = { + ...mockRecoveryContentProps, + recoveryActionMutationUtils: { + resumeRecovery: mockResumeRecovery, + isResumeRecoveryLoading: false, + }, + runStatus: RUN_STATUS_AWAITING_RECOVERY_PAUSED, + } + }) + + it('renders the correct content', () => { + render(props) + + screen.getByTestId('recovery_door_alert_icon') + screen.getByText('Robot door is open') + screen.getByText( + 'Close the robot door, and then resume the recovery action.' + ) + }) + + it(`calls resumeRecovery when the primary button is clicked and the run status is ${RUN_STATUS_AWAITING_RECOVERY_PAUSED}`, () => { + render(props) + + clickButtonLabeled('Resume') + + expect(mockResumeRecovery).toHaveBeenCalledTimes(1) + }) +}) diff --git a/app/src/organisms/ErrorRecoveryFlows/__tests__/RunPausedSplash.test.tsx b/app/src/organisms/ErrorRecoveryFlows/__tests__/RunPausedSplash.test.tsx index 57f228b4830..ff3c8d1cc34 100644 --- a/app/src/organisms/ErrorRecoveryFlows/__tests__/RunPausedSplash.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/__tests__/RunPausedSplash.test.tsx @@ -34,13 +34,24 @@ describe('useRunPausedSplash', () => { ) }) - const IS_WIZARD_SHOWN = [false, true] - IS_WIZARD_SHOWN.forEach(val => { - it(`returns ${!val} if showERWizard is ${val}`, () => { - const { result } = renderHook(() => useRunPausedSplash(val), { - wrapper, + const TEST_CASES = [ + { isOnDevice: true, showERWizard: true, expected: true }, + { isOnDevice: true, showERWizard: false, expected: true }, + { isOnDevice: false, showERWizard: true, expected: false }, + { isOnDevice: false, showERWizard: false, expected: true }, + ] + + describe('useRunPausedSplash', () => { + TEST_CASES.forEach(({ isOnDevice, showERWizard, expected }) => { + it(`returns ${expected} when isOnDevice is ${isOnDevice} and showERWizard is ${showERWizard}`, () => { + const { result } = renderHook( + () => useRunPausedSplash(isOnDevice, showERWizard), + { + wrapper, + } + ) + expect(result.current).toEqual(expected) }) - expect(result.current).toEqual(!val) }) }) }) diff --git a/app/src/organisms/ErrorRecoveryFlows/__tests__/util.ts b/app/src/organisms/ErrorRecoveryFlows/__tests__/util.ts new file mode 100644 index 00000000000..2e4f673f02b --- /dev/null +++ b/app/src/organisms/ErrorRecoveryFlows/__tests__/util.ts @@ -0,0 +1,6 @@ +import { screen, fireEvent } from '@testing-library/react' + +export function clickButtonLabeled(label: string): void { + const buttons = screen.getAllByRole('button', { name: label }) + fireEvent.click(buttons[0]) +} diff --git a/app/src/organisms/ErrorRecoveryFlows/constants.ts b/app/src/organisms/ErrorRecoveryFlows/constants.ts index c551af98b20..846f7e2efc0 100644 --- a/app/src/organisms/ErrorRecoveryFlows/constants.ts +++ b/app/src/organisms/ErrorRecoveryFlows/constants.ts @@ -1,6 +1,6 @@ import { css } from 'styled-components' -import { SPACING, TYPOGRAPHY } from '@opentrons/components' +import { SPACING, TYPOGRAPHY, RESPONSIVENESS } from '@opentrons/components' import type { StepOrder } from './types' @@ -24,12 +24,6 @@ export const ERROR_KINDS = { // TODO(jh, 06-14-24): Consolidate motion routes to a single route with several steps. // Valid recovery routes and steps. export const RECOVERY_MAP = { - BEFORE_BEGINNING: { - ROUTE: 'before-beginning', - STEPS: { - RECOVERY_DESCRIPTION: 'recovery-description', - }, - }, DROP_TIP_FLOWS: { ROUTE: 'drop-tip', STEPS: { @@ -139,7 +133,6 @@ export const RECOVERY_MAP = { } as const const { - BEFORE_BEGINNING, OPTION_SELECTION, RETRY_FAILED_COMMAND, ROBOT_CANCELING, @@ -162,7 +155,6 @@ const { // The deterministic ordering of steps for a given route. export const STEP_ORDER: StepOrder = { - [BEFORE_BEGINNING.ROUTE]: [BEFORE_BEGINNING.STEPS.RECOVERY_DESCRIPTION], [OPTION_SELECTION.ROUTE]: [OPTION_SELECTION.STEPS.SELECT], [RETRY_FAILED_COMMAND.ROUTE]: [RETRY_FAILED_COMMAND.STEPS.CONFIRM_RETRY], [RETRY_NEW_TIPS.ROUTE]: [ @@ -219,3 +211,14 @@ export const BODY_TEXT_STYLE = css` export const ODD_SECTION_TITLE_STYLE = css` margin-bottom: ${SPACING.spacing16}; ` + +export const ODD_ONLY = css` + @media not (${RESPONSIVENESS.touchscreenMediaQuerySpecs}) { + display: none; + } +` +export const DESKTOP_ONLY = css` + @media (${RESPONSIVENESS.touchscreenMediaQuerySpecs}) { + display: none; + } +` diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryActionMutation.test.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryActionMutation.test.ts new file mode 100644 index 00000000000..029f7d5e239 --- /dev/null +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryActionMutation.test.ts @@ -0,0 +1,55 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { renderHook } from '@testing-library/react' + +import { useRunActionMutations } from '@opentrons/react-api-client' + +import { useRecoveryActionMutation } from '../useRecoveryActionMutation' + +import type { Mock } from 'vitest' + +vi.mock('@opentrons/react-api-client', () => ({ + useRunActionMutations: vi.fn(), +})) + +describe('useRecoveryActionMutation', () => { + const mockRunId = 'MOCK_ID' + let mockPlayRun: Mock + let mockIsPlayRunActionLoading: boolean + + beforeEach(() => { + mockPlayRun = vi.fn() + mockIsPlayRunActionLoading = false + + vi.mocked(useRunActionMutations).mockReturnValue({ + playRun: mockPlayRun, + isPlayRunActionLoading: mockIsPlayRunActionLoading, + } as any) + }) + + it('should return resumeRecovery and isResumeRecoveryLoading', () => { + const { result } = renderHook(() => useRecoveryActionMutation(mockRunId)) + + expect(result.current).toEqual({ + resumeRecovery: mockPlayRun, + isResumeRecoveryLoading: mockIsPlayRunActionLoading, + }) + }) + + it('should return updated isResumeRecoveryLoading when it changes', () => { + const { result, rerender } = renderHook(() => + useRecoveryActionMutation(mockRunId) + ) + + expect(result.current.isResumeRecoveryLoading).toBe(false) + + mockIsPlayRunActionLoading = true + vi.mocked(useRunActionMutations).mockReturnValue({ + playRun: mockPlayRun, + isPlayRunActionLoading: mockIsPlayRunActionLoading, + } as any) + + rerender() + + expect(result.current.isResumeRecoveryLoading).toBe(true) + }) +}) diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryCommands.test.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryCommands.test.ts index c6a0764e796..d75387a99d4 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryCommands.test.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryCommands.test.ts @@ -33,13 +33,16 @@ const mockRouteUpdateActions = { } as any describe('useRecoveryCommands', () => { - const mockResumeRunFromRecovery = vi.fn() + const mockMakeSuccessToast = vi.fn() + const mockResumeRunFromRecovery = vi.fn(() => + Promise.resolve(mockMakeSuccessToast()) + ) const mockStopRun = vi.fn() const mockChainRunCommands = vi.fn().mockResolvedValue([]) beforeEach(() => { vi.mocked(useResumeRunFromRecoveryMutation).mockReturnValue({ - resumeRunFromRecovery: mockResumeRunFromRecovery, + mutateAsync: mockResumeRunFromRecovery, } as any) vi.mocked(useStopRunMutation).mockReturnValue({ stopRun: mockStopRun, @@ -56,6 +59,7 @@ describe('useRecoveryCommands', () => { failedCommand: mockFailedCommand, failedLabwareUtils: mockFailedLabwareUtils, routeUpdateActions: mockRouteUpdateActions, + recoveryToastUtils: {} as any, }) ) @@ -81,6 +85,7 @@ describe('useRecoveryCommands', () => { failedCommand: mockFailedCommand, failedLabwareUtils: mockFailedLabwareUtils, routeUpdateActions: mockRouteUpdateActions, + recoveryToastUtils: {} as any, }) ) @@ -107,6 +112,7 @@ describe('useRecoveryCommands', () => { failedCommand: mockFailedCommand, failedLabwareUtils: mockFailedLabwareUtils, routeUpdateActions: mockRouteUpdateActions, + recoveryToastUtils: {} as any, }) ) @@ -120,19 +126,23 @@ describe('useRecoveryCommands', () => { ) }) - it('should call resumeRun with runId', () => { + it('should call resumeRun with runId and show success toast on success', async () => { const { result } = renderHook(() => useRecoveryCommands({ runId: mockRunId, failedCommand: mockFailedCommand, failedLabwareUtils: mockFailedLabwareUtils, routeUpdateActions: mockRouteUpdateActions, + recoveryToastUtils: { makeSuccessToast: mockMakeSuccessToast } as any, }) ) - result.current.resumeRun() + await act(async () => { + await result.current.resumeRun() + }) expect(mockResumeRunFromRecovery).toHaveBeenCalledWith(mockRunId) + expect(mockMakeSuccessToast).toHaveBeenCalled() }) it('should call cancelRun with runId', () => { @@ -142,6 +152,7 @@ describe('useRecoveryCommands', () => { failedCommand: mockFailedCommand, failedLabwareUtils: mockFailedLabwareUtils, routeUpdateActions: mockRouteUpdateActions, + recoveryToastUtils: {} as any, }) ) @@ -157,6 +168,7 @@ describe('useRecoveryCommands', () => { failedCommand: mockFailedCommand, failedLabwareUtils: mockFailedLabwareUtils, routeUpdateActions: mockRouteUpdateActions, + recoveryToastUtils: {} as any, }) ) @@ -195,6 +207,7 @@ describe('useRecoveryCommands', () => { failedLabware: mockFailedLabware, }, routeUpdateActions: mockRouteUpdateActions, + recoveryToastUtils: {} as any, }) ) @@ -208,24 +221,23 @@ describe('useRecoveryCommands', () => { ) }) - it('should call skipFailedCommand and resolve after a timeout', async () => { + it('should call skipFailedCommand and show success toast on success', async () => { const { result } = renderHook(() => useRecoveryCommands({ runId: mockRunId, failedCommand: mockFailedCommand, failedLabwareUtils: mockFailedLabwareUtils, routeUpdateActions: mockRouteUpdateActions, + recoveryToastUtils: { makeSuccessToast: mockMakeSuccessToast } as any, }) ) - const consoleSpy = vi.spyOn(console, 'log') - await act(async () => { await result.current.skipFailedCommand() }) - expect(consoleSpy).toHaveBeenCalledWith('SKIPPING TO NEXT STEP') - expect(result.current.skipFailedCommand()).resolves.toBeUndefined() + expect(mockResumeRunFromRecovery).toHaveBeenCalledWith(mockRunId) + expect(mockMakeSuccessToast).toHaveBeenCalled() }) it('should call ignoreErrorKindThisRun and resolve immediately', async () => { @@ -235,6 +247,7 @@ describe('useRecoveryCommands', () => { failedCommand: mockFailedCommand, failedLabwareUtils: mockFailedLabwareUtils, routeUpdateActions: mockRouteUpdateActions, + recoveryToastUtils: {} as any, }) ) diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryRouting.test.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryRouting.test.ts index 5e5543ca268..0bc23d79ee7 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryRouting.test.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryRouting.test.ts @@ -20,8 +20,8 @@ describe('useRecoveryRouting', () => { const { result } = renderHook(() => useRecoveryRouting()) const newRecoveryMap = { - route: RECOVERY_MAP.BEFORE_BEGINNING.ROUTE, - step: RECOVERY_MAP.BEFORE_BEGINNING.STEPS.RECOVERY_DESCRIPTION, + route: RECOVERY_MAP.ERROR_WHILE_RECOVERING.ROUTE, + step: RECOVERY_MAP.ERROR_WHILE_RECOVERING.STEPS.RECOVERY_ACTION_FAILED, } as IRecoveryMap act(() => { diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryToasts.test.tsx b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryToasts.test.tsx index 9514931cf54..8766fc83590 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryToasts.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryToasts.test.tsx @@ -3,34 +3,54 @@ import { vi, describe, it, expect, beforeEach } from 'vitest' import { I18nextProvider } from 'react-i18next' import { i18n } from '../../../../i18n' import { renderHook, render, screen } from '@testing-library/react' + +import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' + import { useRecoveryToasts, - useToastText, + useRecoveryToastText, getStepNumber, + useRecoveryFullCommandText, } from '../useRecoveryToasts' import { RECOVERY_MAP } from '../../constants' import { useToaster } from '../../../ToasterOven' +import { useCommandTextString } from '../../../../molecules/Command' import type { Mock } from 'vitest' +import type { BuildToast } from '../useRecoveryToasts' vi.mock('../../../ToasterOven') +vi.mock('../../../../molecules/Command') + +const TEST_COMMAND = 'test command' +const TC_COMMAND = 'tc command cycle some more text' let mockMakeToast: Mock +const DEFAULT_PROPS: BuildToast = { + isOnDevice: false, + currentStepCount: 1, + selectedRecoveryOption: RECOVERY_MAP.RETRY_SAME_TIPS.ROUTE, + commandTextData: { commands: [] } as any, + robotType: FLEX_ROBOT_TYPE, +} + +// Utility function for rendering with I18nextProvider +const renderWithI18n = (component: React.ReactElement) => { + return render({component}) +} + describe('useRecoveryToasts', () => { beforeEach(() => { mockMakeToast = vi.fn() vi.mocked(useToaster).mockReturnValue({ makeToast: mockMakeToast } as any) + vi.mocked(useCommandTextString).mockReturnValue({ + commandText: TEST_COMMAND, + }) }) it('should return makeSuccessToast function', () => { - const { result } = renderHook(() => - useRecoveryToasts({ - isOnDevice: false, - currentStepCount: 1, - selectedRecoveryOption: RECOVERY_MAP.RETRY_SAME_TIPS.ROUTE, - }) - ) + const { result } = renderHook(() => useRecoveryToasts(DEFAULT_PROPS)) expect(result.current.makeSuccessToast).toBeInstanceOf(Function) }) @@ -38,67 +58,90 @@ describe('useRecoveryToasts', () => { it(`should not make toast for ${RECOVERY_MAP.CANCEL_RUN.ROUTE} option`, () => { const { result } = renderHook(() => useRecoveryToasts({ - isOnDevice: false, - currentStepCount: 1, + ...DEFAULT_PROPS, selectedRecoveryOption: RECOVERY_MAP.CANCEL_RUN.ROUTE, }) ) - const mockMakeToast = vi.fn() - vi.mocked(useToaster).mockReturnValue({ makeToast: mockMakeToast } as any) - result.current.makeSuccessToast() expect(mockMakeToast).not.toHaveBeenCalled() }) -}) -describe('useToastText', () => { - it(`should return correct text for ${RECOVERY_MAP.RETRY_SAME_TIPS.ROUTE} option`, () => { + it('should make toast with correct parameters for desktop', () => { + vi.mocked(useCommandTextString).mockReturnValue({ + commandText: TEST_COMMAND, + }) + const { result } = renderHook(() => - useToastText({ - currentStepCount: 2, - selectedRecoveryOption: RECOVERY_MAP.RETRY_SAME_TIPS.ROUTE, + useRecoveryToasts({ + ...DEFAULT_PROPS, + commandTextData: { commands: [TEST_COMMAND] } as any, }) ) - render( - -
    {result.current}
    -
    + vi.mocked(useCommandTextString).mockReturnValue({ + commandText: TEST_COMMAND, + stepTexts: undefined, + }) + + result.current.makeSuccessToast() + expect(mockMakeToast).toHaveBeenCalledWith( + TEST_COMMAND, + 'success', + expect.objectContaining({ + closeButton: true, + disableTimeout: true, + displayType: 'desktop', + heading: expect.any(String), + }) ) - screen.getByText('Retrying step 2 succeeded') }) - it(`should return correct text for ${RECOVERY_MAP.SKIP_STEP_WITH_SAME_TIPS.ROUTE} option`, () => { + it('should make toast with correct parameters for ODD', () => { const { result } = renderHook(() => - useToastText({ - currentStepCount: 2, - selectedRecoveryOption: RECOVERY_MAP.SKIP_STEP_WITH_SAME_TIPS.ROUTE, + useRecoveryToasts({ + ...DEFAULT_PROPS, + isOnDevice: true, }) ) - render( - -
    {result.current}
    -
    + result.current.makeSuccessToast() + expect(mockMakeToast).toHaveBeenCalledWith( + expect.any(String), + 'success', + expect.objectContaining({ + closeButton: true, + disableTimeout: true, + displayType: 'odd', + heading: undefined, + }) ) - screen.getByText('Skipping to step 3 succeeded') }) +}) - it('should handle a falsy currentStepCount', () => { +describe('useRecoveryToastText', () => { + it(`should return correct text for ${RECOVERY_MAP.RETRY_SAME_TIPS.ROUTE} option`, () => { const { result } = renderHook(() => - useToastText({ - currentStepCount: null, + useRecoveryToastText({ + stepNumber: 2, selectedRecoveryOption: RECOVERY_MAP.RETRY_SAME_TIPS.ROUTE, }) ) - render( - -
    {result.current}
    -
    + renderWithI18n(
    {result.current}
    ) + screen.getByText('Retrying step 2 succeeded.') + }) + + it(`should return correct text for ${RECOVERY_MAP.SKIP_STEP_WITH_SAME_TIPS.ROUTE} option`, () => { + const { result } = renderHook(() => + useRecoveryToastText({ + stepNumber: 3, + selectedRecoveryOption: RECOVERY_MAP.SKIP_STEP_WITH_SAME_TIPS.ROUTE, + }) ) - screen.getByText('Retrying step ? succeeded') + + renderWithI18n(
    {result.current}
    ) + screen.getByText('Skipping to step 3 succeeded.') }) }) @@ -123,3 +166,53 @@ describe('getStepNumber', () => { ) }) }) + +describe('useRecoveryFullCommandText', () => { + it('should return the correct command text', () => { + vi.mocked(useCommandTextString).mockReturnValue({ + commandText: TEST_COMMAND, + stepTexts: undefined, + }) + + const { result } = renderHook(() => + useRecoveryFullCommandText({ + robotType: FLEX_ROBOT_TYPE, + stepNumber: 1, + commandTextData: { commands: [TEST_COMMAND] } as any, + }) + ) + + expect(result.current).toBe(TEST_COMMAND) + }) + + it('should return stepNumber if it is a string', () => { + const { result } = renderHook(() => + useRecoveryFullCommandText({ + robotType: FLEX_ROBOT_TYPE, + stepNumber: '?', + commandTextData: { commands: [] } as any, + }) + ) + + expect(result.current).toBe('?') + }) + + it('should truncate TC command', () => { + vi.mocked(useCommandTextString).mockReturnValue({ + commandText: TC_COMMAND, + stepTexts: ['step'], + }) + + const { result } = renderHook(() => + useRecoveryFullCommandText({ + robotType: FLEX_ROBOT_TYPE, + stepNumber: 1, + commandTextData: { + commands: [TC_COMMAND], + } as any, + }) + ) + + expect(result.current).toBe('tc command cycle') + }) +}) diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useShowDoorInfo.test.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useShowDoorInfo.test.ts new file mode 100644 index 00000000000..226ca7de023 --- /dev/null +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useShowDoorInfo.test.ts @@ -0,0 +1,51 @@ +import { describe, it, expect, beforeEach } from 'vitest' +import { renderHook, act } from '@testing-library/react' +import { useShowDoorInfo } from '../useShowDoorInfo' +import { + RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR, + RUN_STATUS_AWAITING_RECOVERY_PAUSED, + RUN_STATUS_AWAITING_RECOVERY, +} from '@opentrons/api-client' + +describe('useShowDoorInfo', () => { + let initialProps: Parameters[0] + + beforeEach(() => { + initialProps = RUN_STATUS_AWAITING_RECOVERY + }) + + it('should return false initially', () => { + const { result } = renderHook(() => useShowDoorInfo(initialProps)) + expect(result.current).toBe(false) + }) + + it(`should return true when runStatus is ${RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR}`, () => { + const props = RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR + + const { result } = renderHook(() => useShowDoorInfo(props)) + expect(result.current).toBe(true) + }) + + it(`should return true when runStatus is ${RUN_STATUS_AWAITING_RECOVERY_PAUSED}`, () => { + const props = RUN_STATUS_AWAITING_RECOVERY_PAUSED + + const { result } = renderHook(() => useShowDoorInfo(props)) + expect(result.current).toBe(true) + }) + + it(`should keep returning true when runStatus changes from ${RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR} to ${RUN_STATUS_AWAITING_RECOVERY_PAUSED}`, () => { + const { result, rerender } = renderHook(props => useShowDoorInfo(props), { + initialProps, + }) + + act(() => { + rerender(RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR) + }) + expect(result.current).toBe(true) + + act(() => { + rerender(RUN_STATUS_AWAITING_RECOVERY_PAUSED) + }) + expect(result.current).toBe(true) + }) +}) diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/index.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/index.ts index 8bb482cb1aa..31d9ebb4367 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/index.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/index.ts @@ -1,6 +1,7 @@ export { useCurrentlyRecoveringFrom } from './useCurrentlyRecoveringFrom' export { useErrorMessage } from './useErrorMessage' export { useErrorName } from './useErrorName' +export { useShowDoorInfo } from './useShowDoorInfo' export { useRecoveryCommands } from './useRecoveryCommands' export { useRouteUpdateActions } from './useRouteUpdateActions' export { useERUtils } from './useERUtils' diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useERUtils.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useERUtils.ts index 1881b4b829c..ff05642ff18 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useERUtils.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useERUtils.ts @@ -12,9 +12,12 @@ import { useNotifyRunQuery, } from '../../../resources/runs' import { useRecoveryOptionCopy } from './useRecoveryOptionCopy' +import { useRecoveryActionMutation } from './useRecoveryActionMutation' import { useRunningStepCounts } from '../../../resources/protocols/hooks' +import { useRecoveryToasts } from './useRecoveryToasts' import type { PipetteData } from '@opentrons/api-client' +import type { RobotType } from '@opentrons/shared-data' import type { IRecoveryMap } from '../types' import type { ErrorRecoveryFlowsProps } from '..' import type { UseRouteUpdateActionsResult } from './useRouteUpdateActions' @@ -23,11 +26,14 @@ import type { RecoveryTipStatusUtils } from './useRecoveryTipStatus' import type { UseFailedLabwareUtilsResult } from './useFailedLabwareUtils' import type { UseDeckMapUtilsResult } from './useDeckMapUtils' import type { CurrentRecoveryOptionUtils } from './useRecoveryRouting' +import type { RecoveryActionMutationResult } from './useRecoveryActionMutation' import type { StepCounts } from '../../../resources/protocols/hooks' type ERUtilsProps = ErrorRecoveryFlowsProps & { toggleERWizard: (launchER: boolean) => Promise hasLaunchedRecovery: boolean + isOnDevice: boolean + robotType: RobotType } export interface ERUtilsResults { @@ -39,6 +45,7 @@ export interface ERUtilsResults { failedLabwareUtils: UseFailedLabwareUtilsResult deckMapUtils: UseDeckMapUtilsResult getRecoveryOptionCopy: ReturnType + recoveryActionMutationUtils: RecoveryActionMutationResult failedPipetteInfo: PipetteData | null hasLaunchedRecovery: boolean trackExternalMap: (map: Record) => void @@ -55,6 +62,8 @@ export function useERUtils({ toggleERWizard, hasLaunchedRecovery, protocolAnalysis, + isOnDevice, + robotType, }: ERUtilsProps): ERUtilsResults { const { data: attachedInstruments } = useInstrumentsQuery() const { data: runRecord } = useNotifyRunQuery(runId) @@ -68,6 +77,8 @@ export function useERUtils({ pageLength: 999, }) + const stepCounts = useRunningStepCounts(runId, runCommands) + const { recoveryMap, setRM, @@ -75,6 +86,14 @@ export function useERUtils({ currentRecoveryOptionUtils, } = useRecoveryRouting() + const recoveryToastUtils = useRecoveryToasts({ + currentStepCount: stepCounts.currentStepNumber, + selectedRecoveryOption: currentRecoveryOptionUtils.selectedRecoveryOption, + isOnDevice, + commandTextData: protocolAnalysis, + robotType, + }) + const tipStatusUtils = useRecoveryTipStatus({ runId, isFlex, @@ -108,6 +127,7 @@ export function useERUtils({ failedCommand, failedLabwareUtils, routeUpdateActions, + recoveryToastUtils, }) const deckMapUtils = useDeckMapUtils({ @@ -117,7 +137,7 @@ export function useERUtils({ failedLabwareUtils, }) - const stepCounts = useRunningStepCounts(runId, runCommands) + const recoveryActionMutationUtils = useRecoveryActionMutation(runId) // TODO(jh, 06-14-24): Ensure other string build utilities that are internal to ErrorRecoveryFlows are exported under // one utility object in useERUtils. @@ -131,6 +151,7 @@ export function useERUtils({ recoveryMap, trackExternalMap, currentRecoveryOptionUtils, + recoveryActionMutationUtils, routeUpdateActions, recoveryCommands, hasLaunchedRecovery, diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useFailedLabwareUtils.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useFailedLabwareUtils.ts index 0ff11b7d00a..5de95fe90da 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useFailedLabwareUtils.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useFailedLabwareUtils.ts @@ -20,6 +20,7 @@ import type { DispenseRunTimeCommand, LiquidProbeRunTimeCommand, } from '@opentrons/shared-data' +import { getLoadedLabware } from '../../../molecules/Command/utils/accessors' import type { ErrorRecoveryFlowsProps } from '..' interface UseFailedLabwareUtilsProps { @@ -37,6 +38,8 @@ export type UseFailedLabwareUtilsResult = UseTipSelectionUtilsResult & { failedLabware: LoadedLabware | null /* The name of the well(s) or tip location(s), if any. */ relevantWellName: string | null + /* The user-content nickname of the failed labware, if any */ + failedLabwareNickname: string | null } /** Utils for labware relating to the failedCommand. @@ -59,7 +62,7 @@ export function useFailedLabwareUtils({ const tipSelectionUtils = useTipSelectionUtils(recentRelevantFailedLabwareCmd) - const failedLabwareName = React.useMemo( + const failedLabwareDetails = React.useMemo( () => getFailedCmdRelevantLabware( protocolAnalysis, @@ -81,9 +84,10 @@ export function useFailedLabwareUtils({ return { ...tipSelectionUtils, - failedLabwareName, + failedLabwareName: failedLabwareDetails?.name ?? null, failedLabware, relevantWellName, + failedLabwareNickname: failedLabwareDetails?.nickname ?? null, } } @@ -238,15 +242,25 @@ export function getFailedCmdRelevantLabware( protocolAnalysis: ErrorRecoveryFlowsProps['protocolAnalysis'], recentRelevantFailedLabwareCmd: FailedCommandRelevantLabware, runRecord?: Run -): string | null { +): { name: string; nickname: string | null } | null { const lwDefsByURI = getLoadedLabwareDefinitionsByUri( protocolAnalysis?.commands ?? [] ) + const labwareNickname = + protocolAnalysis != null + ? getLoadedLabware( + protocolAnalysis, + recentRelevantFailedLabwareCmd?.params.labwareId || '' + )?.displayName ?? null + : null const failedLWURI = runRecord?.data.labware.find( labware => labware.id === recentRelevantFailedLabwareCmd?.params.labwareId )?.definitionUri if (failedLWURI != null) { - return getLabwareDisplayName(lwDefsByURI[failedLWURI]) + return { + name: getLabwareDisplayName(lwDefsByURI[failedLWURI]), + nickname: labwareNickname, + } } else { return null } diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryActionMutation.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryActionMutation.ts new file mode 100644 index 00000000000..5f33df7941d --- /dev/null +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryActionMutation.ts @@ -0,0 +1,20 @@ +import type { ErrorRecoveryFlowsProps } from '..' +import { useRunActionMutations } from '@opentrons/react-api-client' + +export interface RecoveryActionMutationResult { + resumeRecovery: ReturnType['playRun'] + isResumeRecoveryLoading: ReturnType< + typeof useRunActionMutations + >['isPlayRunActionLoading'] +} + +export function useRecoveryActionMutation( + runId: ErrorRecoveryFlowsProps['runId'] +): RecoveryActionMutationResult { + const { + playRun: resumeRecovery, + isPlayRunActionLoading: isResumeRecoveryLoading, + } = useRunActionMutations(runId) + + return { resumeRecovery, isResumeRecoveryLoading } +} diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryCommands.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryCommands.ts index a1cc6a946fd..3ef8f5b3809 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryCommands.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryCommands.ts @@ -15,20 +15,22 @@ import type { WellGroup } from '@opentrons/components' import type { FailedCommand } from '../types' import type { UseFailedLabwareUtilsResult } from './useFailedLabwareUtils' import type { UseRouteUpdateActionsResult } from './useRouteUpdateActions' +import type { RecoveryToasts } from './useRecoveryToasts' interface UseRecoveryCommandsParams { runId: string failedCommand: FailedCommand | null failedLabwareUtils: UseFailedLabwareUtilsResult routeUpdateActions: UseRouteUpdateActionsResult + recoveryToastUtils: RecoveryToasts } export interface UseRecoveryCommandsResult { /* A terminal recovery command that causes ER to exit as the run status becomes "running" */ resumeRun: () => void /* A terminal recovery command that causes ER to exit as the run status becomes "stop-requested" */ cancelRun: () => void - /* A non-terminal recovery command, but should generally be chained with a resumeRun. */ - skipFailedCommand: () => Promise + /* A terminal recovery command, that causes ER to exit as the run status becomes "running" */ + skipFailedCommand: () => void /* A non-terminal recovery command. Ignore this errorKind for the rest of this run. */ ignoreErrorKindThisRun: () => Promise /* A non-terminal recovery command */ @@ -44,11 +46,15 @@ export function useRecoveryCommands({ failedCommand, failedLabwareUtils, routeUpdateActions, + recoveryToastUtils, }: UseRecoveryCommandsParams): UseRecoveryCommandsResult { const { proceedToRouteAndStep } = routeUpdateActions const { chainRunCommands } = useChainRunCommands(runId, failedCommand?.id) - const { resumeRunFromRecovery } = useResumeRunFromRecoveryMutation() + const { + mutateAsync: resumeRunFromRecovery, + } = useResumeRunFromRecoveryMutation() const { stopRun } = useStopRunMutation() + const { makeSuccessToast } = recoveryToastUtils const chainRunRecoveryCommands = React.useCallback( ( @@ -96,23 +102,20 @@ export function useRecoveryCommands({ }, [chainRunRecoveryCommands, failedCommand, failedLabwareUtils]) const resumeRun = React.useCallback((): void => { - resumeRunFromRecovery(runId) - }, [runId, resumeRunFromRecovery]) + void resumeRunFromRecovery(runId).then(() => { + makeSuccessToast() + }) + }, [runId, resumeRunFromRecovery, makeSuccessToast]) const cancelRun = React.useCallback((): void => { stopRun(runId) }, [runId]) - // TODO(jh, 06-18-24): If this command is actually terminal for error recovery, remove the resumeRun currently promise - // chained where this is used. Also update docstring in iface. - const skipFailedCommand = React.useCallback((): Promise => { - console.log('SKIPPING TO NEXT STEP') - return new Promise(resolve => { - setTimeout(() => { - resolve() - }, 2000) + const skipFailedCommand = React.useCallback((): void => { + void resumeRunFromRecovery(runId).then(() => { + makeSuccessToast() }) - }, []) + }, [runId, resumeRunFromRecovery, makeSuccessToast]) const ignoreErrorKindThisRun = React.useCallback((): Promise => { console.log('IGNORING ALL ERRORS OF THIS KIND THIS RUN') diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryToasts.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryToasts.ts index a22b2701f2b..632846329b5 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryToasts.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryToasts.ts @@ -1,10 +1,14 @@ +import { useTranslation } from 'react-i18next' + import { useToaster } from '../../ToasterOven' import { RECOVERY_MAP } from '../constants' -import type { CurrentRecoveryOptionUtils } from './useRecoveryRouting' -import { useTranslation } from 'react-i18next' +import { useCommandTextString } from '../../../molecules/Command' + import type { StepCounts } from '../../../resources/protocols/hooks' +import type { CurrentRecoveryOptionUtils } from './useRecoveryRouting' +import type { UseCommandTextStringParams } from '../../../molecules/Command' -interface BuildToast { +export type BuildToast = Omit & { isOnDevice: boolean currentStepCount: StepCounts['currentStepNumber'] selectedRecoveryOption: CurrentRecoveryOptionUtils['selectedRecoveryOption'] @@ -20,17 +24,35 @@ export function useRecoveryToasts({ currentStepCount, isOnDevice, selectedRecoveryOption, + ...rest }: BuildToast): RecoveryToasts { const { makeToast } = useToaster() + const displayType = isOnDevice ? 'odd' : 'desktop' + + const stepNumber = getStepNumber(selectedRecoveryOption, currentStepCount) + + const desktopFullCommandText = useRecoveryFullCommandText({ + ...rest, + stepNumber, + }) + const recoveryToastText = useRecoveryToastText({ + stepNumber, + selectedRecoveryOption, + }) - const toastText = useToastText({ currentStepCount, selectedRecoveryOption }) + // The "body" of the toast message. On ODD, this is the recovery-specific text. On desktop, this is the full command text. + const bodyText = + displayType === 'desktop' ? desktopFullCommandText : recoveryToastText + // The "heading" of the toast message. Currently, this text is only present on the desktop toasts. + const headingText = displayType === 'desktop' ? recoveryToastText : undefined const makeSuccessToast = (): void => { if (selectedRecoveryOption !== RECOVERY_MAP.CANCEL_RUN.ROUTE) { - makeToast(toastText, 'success', { + makeToast(bodyText, 'success', { closeButton: true, disableTimeout: true, - displayType: isOnDevice ? 'odd' : 'desktop', + displayType, + heading: headingText, }) } } @@ -39,14 +61,16 @@ export function useRecoveryToasts({ } // Return i18n toast text for the corresponding user selected recovery option. -export function useToastText({ - currentStepCount, +// Ex: "Skip to step <###> succeeded." +export function useRecoveryToastText({ + stepNumber, selectedRecoveryOption, -}: Omit): string { +}: { + stepNumber: ReturnType + selectedRecoveryOption: CurrentRecoveryOptionUtils['selectedRecoveryOption'] +}): string { const { t } = useTranslation('error_recovery') - const stepNumber = getStepNumber(selectedRecoveryOption, currentStepCount) - const currentStepReturnVal = t('retrying_step_succeeded', { step: stepNumber, }) as string @@ -63,6 +87,35 @@ export function useToastText({ return toastText } +type UseRecoveryFullCommandTextParams = Omit< + UseCommandTextStringParams, + 'command' +> & { + stepNumber: ReturnType +} + +// Return the full command text of the recovery command that is "retried" or "skipped". +export function useRecoveryFullCommandText( + props: UseRecoveryFullCommandTextParams +): string { + const { commandTextData, stepNumber } = props + + const relevantCmdIdx = typeof stepNumber === 'number' ? stepNumber : -1 + const relevantCmd = commandTextData?.commands[relevantCmdIdx] ?? null + + const { commandText, stepTexts } = useCommandTextString({ + ...props, + command: relevantCmd, + }) + + if (typeof stepNumber === 'string') { + return stepNumber + } else { + return truncateIfTCCommand(commandText, stepTexts != null) + } +} + +// Return the user-facing step number. If the step number cannot be determined, return '?'. export function getStepNumber( selectedRecoveryOption: BuildToast['selectedRecoveryOption'], currentStepCount: BuildToast['currentStepCount'] @@ -101,3 +154,20 @@ function handleRecoveryOptionAction( return 'HANDLE RECOVERY TOAST OPTION EXPLICITLY.' } } + +// Special case the TC text, so it make sense in a success toast. +function truncateIfTCCommand(commandText: string, isTCText: boolean): string { + if (isTCText) { + const indexOfCycle = commandText.indexOf('cycle') + + if (indexOfCycle === -1) { + console.warn( + 'TC cycle text has changed. Update Error Recovery TC text utility.' + ) + } + + return commandText.slice(0, indexOfCycle + 5) // +5 to include "cycle" + } else { + return commandText + } +} diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useShowDoorInfo.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useShowDoorInfo.ts new file mode 100644 index 00000000000..f73f1a22ae0 --- /dev/null +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useShowDoorInfo.ts @@ -0,0 +1,38 @@ +import * as React from 'react' + +import { + RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR, + RUN_STATUS_AWAITING_RECOVERY_PAUSED, +} from '@opentrons/api-client' + +import type { RunStatus } from '@opentrons/api-client' +import type { ErrorRecoveryFlowsProps } from '../index' + +const DOOR_OPEN_STATUSES: RunStatus[] = [ + RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR, + RUN_STATUS_AWAITING_RECOVERY_PAUSED, +] + +// Whether the door is open or the user has not yet resumed the run after a door open event. +export function useShowDoorInfo( + runStatus: ErrorRecoveryFlowsProps['runStatus'] +): boolean { + const [showDoorModal, setShowDoorModal] = React.useState(false) + + React.useEffect(() => { + // TODO(jh, 07-16-24): "recovery paused" is only used for door status and therefore + // a valid way to ensure all apps show the door open prompt, however this could be problematic in the future. + // Consider restructuring this check once the takeover modals are added. + if (runStatus != null && DOOR_OPEN_STATUSES.includes(runStatus)) { + setShowDoorModal(true) + } else if ( + showDoorModal && + runStatus != null && + !DOOR_OPEN_STATUSES.includes(runStatus) + ) { + setShowDoorModal(false) + } + }, [runStatus, showDoorModal]) + + return showDoorModal +} diff --git a/app/src/organisms/ErrorRecoveryFlows/index.tsx b/app/src/organisms/ErrorRecoveryFlows/index.tsx index 3f64b644785..6e4e2bf1fd3 100644 --- a/app/src/organisms/ErrorRecoveryFlows/index.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/index.tsx @@ -18,7 +18,11 @@ import { OT2_ROBOT_TYPE } from '@opentrons/shared-data' import { getIsOnDevice } from '../../redux/config' import { ErrorRecoveryWizard, useERWizard } from './ErrorRecoveryWizard' import { RunPausedSplash, useRunPausedSplash } from './RunPausedSplash' -import { useCurrentlyRecoveringFrom, useERUtils } from './hooks' +import { + useCurrentlyRecoveringFrom, + useERUtils, + useShowDoorInfo, +} from './hooks' import type { RunStatus } from '@opentrons/api-client' import type { CompletedProtocolAnalysis } from '@opentrons/shared-data' @@ -100,6 +104,7 @@ export function useErrorRecoveryFlows( export interface ErrorRecoveryFlowsProps { runId: string + runStatus: RunStatus | null failedCommand: FailedCommand | null isFlex: boolean protocolAnalysis: CompletedProtocolAnalysis | null @@ -108,30 +113,33 @@ export interface ErrorRecoveryFlowsProps { export function ErrorRecoveryFlows( props: ErrorRecoveryFlowsProps ): JSX.Element | null { + const { protocolAnalysis, runStatus } = props + const { hasLaunchedRecovery, toggleERWizard, showERWizard } = useERWizard() + const isOnDevice = useSelector(getIsOnDevice) + const robotType = protocolAnalysis?.robotType ?? OT2_ROBOT_TYPE + const showSplash = useRunPausedSplash(isOnDevice, showERWizard) + + const isDoorOpen = useShowDoorInfo(runStatus) + const recoveryUtils = useERUtils({ ...props, hasLaunchedRecovery, toggleERWizard, + isOnDevice, + robotType, }) - // if (!enableRunNotes) { - // return null - // } - - const { protocolAnalysis } = props - const robotType = protocolAnalysis?.robotType ?? OT2_ROBOT_TYPE - const isOnDevice = useSelector(getIsOnDevice) - const showSplash = useRunPausedSplash(showERWizard) return ( <> - {showERWizard ? ( + {showERWizard || isDoorOpen ? ( ) : null} {showSplash ? ( diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/ErrorDetailsModal.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/ErrorDetailsModal.tsx index 475ad3d02a0..de4829d937f 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/ErrorDetailsModal.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/ErrorDetailsModal.tsx @@ -43,20 +43,7 @@ type ErrorDetailsModalProps = ErrorRecoveryFlowsProps & robotType: RobotType } -export function ErrorDetailsModal( - props: ErrorDetailsModalProps -): JSX.Element | null { - if (props.isOnDevice) { - return - } else { - return null - } -} - -// For ODD use only. -export function ErrorDetailsModalODD( - props: ErrorDetailsModalProps -): JSX.Element { +export function ErrorDetailsModal(props: ErrorDetailsModalProps): JSX.Element { const { failedCommand, toggleModal, isOnDevice } = props const errorKind = getErrorKind(failedCommand) const errorName = useErrorName(errorKind) diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/FailedStepNextStep.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/FailedStepNextStep.tsx new file mode 100644 index 00000000000..b29ade0d2eb --- /dev/null +++ b/app/src/organisms/ErrorRecoveryFlows/shared/FailedStepNextStep.tsx @@ -0,0 +1,62 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { CategorizedStepContent } from '../../../molecules/InterventionModal' +import type { RecoveryContentProps } from '../types' + +export function FailedStepNextStep({ + stepCounts, + failedCommand, + commandsAfterFailedCommand, + protocolAnalysis, + robotType, +}: Pick< + RecoveryContentProps, + | 'stepCounts' + | 'failedCommand' + | 'commandsAfterFailedCommand' + | 'protocolAnalysis' + | 'robotType' +>): JSX.Element { + const { t } = useTranslation('error_recovery') + + const nthStepAfter = (n: number): number | undefined => + stepCounts.currentStepNumber == null + ? undefined + : stepCounts.currentStepNumber + n + const nthCommand = (n: number): typeof failedCommand => + commandsAfterFailedCommand != null + ? n < commandsAfterFailedCommand.length + ? commandsAfterFailedCommand[n] + : null + : null + + const commandsAfter = [nthCommand(0), nthCommand(1)] as const + + const indexedCommandsAfter = [ + commandsAfter[0] != null + ? { command: commandsAfter[0], index: nthStepAfter(1) } + : null, + commandsAfter[1] != null + ? { command: commandsAfter[1], index: nthStepAfter(2) } + : null, + ] as const + return ( + + ) +} diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/LeftColumnLabwareInfo.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/LeftColumnLabwareInfo.tsx index 259f7666a65..2c38ec645c6 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/LeftColumnLabwareInfo.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/LeftColumnLabwareInfo.tsx @@ -1,20 +1,12 @@ import * as React from 'react' -import { - DIRECTION_COLUMN, - Flex, - SPACING, - LegacyStyledText, -} from '@opentrons/components' - -import { Move } from '../../../molecules/InterventionModal/InterventionStep' -import { InlineNotification } from '../../../atoms/InlineNotification' +import { InterventionContent } from '../../../molecules/InterventionModal/InterventionContent' import type { RecoveryContentProps } from '../types' type LeftColumnLabwareInfoProps = RecoveryContentProps & { title: string - moveType: React.ComponentProps['type'] + type: React.ComponentProps['infoProps']['type'] /* Renders a warning InlineNotification if provided. */ bannerText?: string } @@ -23,11 +15,14 @@ type LeftColumnLabwareInfoProps = RecoveryContentProps & { export function LeftColumnLabwareInfo({ title, failedLabwareUtils, - isOnDevice, - moveType, + type, bannerText, }: LeftColumnLabwareInfoProps): JSX.Element | null { - const { failedLabwareName, failedLabware } = failedLabwareUtils + const { + failedLabwareName, + failedLabware, + failedLabwareNickname, + } = failedLabwareUtils const buildLabwareLocationSlotName = (): string => { const location = failedLabware?.location @@ -42,26 +37,18 @@ export function LeftColumnLabwareInfo({ } } - if (isOnDevice) { - return ( - - - {title} - - - {bannerText != null ? ( - - ) : null} - - ) - } else { - return null - } + return ( + + ) } diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryContentWrapper.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryContentWrapper.tsx index f4a8fdbbaad..b9acdcc8cae 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryContentWrapper.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryContentWrapper.tsx @@ -10,37 +10,102 @@ import { Flex, RESPONSIVENESS, } from '@opentrons/components' - import type { StyleProps } from '@opentrons/components' +import { + OneColumn, + TwoColumn, + OneColumnOrTwoColumn, +} from '../../../molecules/InterventionModal' +import { RecoveryFooterButtons } from './RecoveryFooterButtons' -interface SingleColumnContentWrapperProps extends StyleProps { +interface SingleColumnContentWrapperProps { children: React.ReactNode + footerDetails?: React.ComponentProps +} + +interface TwoColumnContentWrapperProps { + children: [React.ReactNode, React.ReactNode] + footerDetails?: React.ComponentProps +} + +interface OneOrTwoColumnContentWrapperProps { + children: [React.ReactNode, React.ReactNode] + footerDetails?: React.ComponentProps } // For flex-direction: column recovery content with one column only. -// -// For ODD use only. -export function RecoveryContentWrapper({ +export function RecoverySingleColumnContentWrapper({ children, + footerDetails, ...styleProps -}: SingleColumnContentWrapperProps): JSX.Element { +}: SingleColumnContentWrapperProps & StyleProps): JSX.Element { return ( - {children} + + {children} + + {footerDetails != null ? ( + + ) : null} + + ) +} + +// For two-column recovery content +export function RecoveryTwoColumnContentWrapper({ + children, + footerDetails, +}: TwoColumnContentWrapperProps): JSX.Element { + const [leftChild, rightChild] = children + return ( + + + {leftChild} + {rightChild} + + {footerDetails != null ? ( + + ) : null} + + ) +} + +// For recovery content with one column on ODD and two columns on desktop +export function RecoveryODDOneDesktopTwoColumnContentWrapper({ + children: [leftOrSingleElement, optionallyShownRightElement], + footerDetails, +}: OneOrTwoColumnContentWrapperProps): JSX.Element { + return ( + + + {leftOrSingleElement} + {optionallyShownRightElement} + + {footerDetails != null ? ( + + ) : null} ) } const STYLE = css` - padding: ${SPACING.spacing32}; gap: ${SPACING.spacing24}; + width: 100%; + height: 100%; @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { gap: none; - height: 29.25rem; } ` diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryFooterButtons.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryFooterButtons.tsx index faf862c5095..ea78376da4e 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryFooterButtons.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryFooterButtons.tsx @@ -3,21 +3,27 @@ import { useTranslation } from 'react-i18next' import { css } from 'styled-components' import { + ALIGN_FLEX_END, ALIGN_CENTER, + Icon, + Box, Flex, JUSTIFY_SPACE_BETWEEN, SPACING, COLORS, + SecondaryButton, + PrimaryButton, + RESPONSIVENESS, } from '@opentrons/components' -import { SmallButton } from '../../../atoms/buttons' +import { SmallButton, TextOnlyButton } from '../../../atoms/buttons' interface RecoveryFooterButtonProps { - isOnDevice: boolean primaryBtnOnClick: () => void /* The "Go back" button */ secondaryBtnOnClick?: () => void primaryBtnTextOverride?: string + primaryBtnDisabled?: boolean /* If true, render pressed state and a spinner icon for the primary button. */ isLoadingPrimaryBtnAction?: boolean /* To the left of the primary button. */ @@ -28,61 +34,50 @@ interface RecoveryFooterButtonProps { export function RecoveryFooterButtons( props: RecoveryFooterButtonProps ): JSX.Element | null { - const { isOnDevice, secondaryBtnOnClick } = props - const { t } = useTranslation('error_recovery') + return ( + + + + + ) +} +function RecoveryGoBackButton({ + secondaryBtnOnClick, +}: RecoveryFooterButtonProps): JSX.Element | null { const showGoBackBtn = secondaryBtnOnClick != null - - if (isOnDevice) { - return ( - - - {showGoBackBtn ? ( - - ) : null} - - - - ) - } else { - return null - } + const { t } = useTranslation('error_recovery') + return showGoBackBtn ? ( + + + + ) : ( + + ) } function PrimaryButtonGroup(props: RecoveryFooterButtonProps): JSX.Element { - const { tertiaryBtnDisabled, tertiaryBtnOnClick, tertiaryBtnText } = props + const { tertiaryBtnOnClick, tertiaryBtnText } = props const renderTertiaryBtn = tertiaryBtnOnClick != null || tertiaryBtnText != null - const tertiaryBtnDefaultOnClick = (): null => null - if (!renderTertiaryBtn) { return ( - + ) } else { return ( - - + + ) @@ -92,20 +87,74 @@ function PrimaryButtonGroup(props: RecoveryFooterButtonProps): JSX.Element { function RecoveryPrimaryBtn({ isLoadingPrimaryBtnAction, primaryBtnOnClick, + primaryBtnDisabled, primaryBtnTextOverride, }: RecoveryFooterButtonProps): JSX.Element { const { t } = useTranslation('error_recovery') return ( - + <> + + + + {isLoadingPrimaryBtnAction && ( + + )} + {primaryBtnTextOverride ?? t('continue')} + + + + ) +} + +function RecoveryTertiaryBtn({ + tertiaryBtnOnClick, + tertiaryBtnText, + tertiaryBtnDisabled, +}: RecoveryFooterButtonProps): JSX.Element { + const tertiaryBtnDefaultOnClick = (): null => null + + return ( + <> + + + {tertiaryBtnText} + + ) } @@ -124,3 +173,15 @@ const PRESSED_LOADING_STATE = css` background-color: ${COLORS.blue60}; } ` + +const ODD_ONLY_BUTTON = css` + @media not (${RESPONSIVENESS.touchscreenMediaQuerySpecs}) { + display: none; + } +` + +const DESKTOP_ONLY_BUTTON = css` + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + display: none; + } +` diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryInterventionModal.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryInterventionModal.tsx index 5855357b4a3..9332ab8766d 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryInterventionModal.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryInterventionModal.tsx @@ -2,10 +2,10 @@ import * as React from 'react' import { createPortal } from 'react-dom' import { css } from 'styled-components' -import { Flex, RESPONSIVENESS } from '@opentrons/components' +import { Flex, RESPONSIVENESS, SPACING } from '@opentrons/components' import { InterventionModal } from '../../../molecules/InterventionModal' -import { getModalPortalEl } from '../../../App/portal' +import { getModalPortalEl, getTopPortalEl } from '../../../App/portal' import type { ModalType } from '../../../molecules/InterventionModal' @@ -15,12 +15,14 @@ export type RecoveryInterventionModalProps = Omit< > & { /* If on desktop, specifies the hard-coded dimensions height of the modal. */ desktopType: 'desktop-small' | 'desktop-large' + isOnDevice: boolean } // A wrapper around InterventionModal with Error-Recovery specific props and styling. export function RecoveryInterventionModal({ children, desktopType, + isOnDevice, ...rest }: RecoveryInterventionModalProps): JSX.Element { const restProps = { @@ -36,11 +38,12 @@ export function RecoveryInterventionModal({ ? SMALL_MODAL_STYLE : LARGE_MODAL_STYLE } + padding={SPACING.spacing32} > {children} , - getModalPortalEl() + isOnDevice ? getTopPortalEl() : getModalPortalEl() ) } @@ -51,12 +54,10 @@ const ODD_STYLE = ` ` const SMALL_MODAL_STYLE = css` - height: 25.25rem; - + height: 22rem; ${ODD_STYLE} ` const LARGE_MODAL_STYLE = css` - height: 30rem; - + height: 26.75rem; ${ODD_STYLE} ` diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryRadioGroup.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryRadioGroup.tsx new file mode 100644 index 00000000000..571f0b0333a --- /dev/null +++ b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryRadioGroup.tsx @@ -0,0 +1,42 @@ +import * as React from 'react' + +import type { ChangeEventHandler } from 'react' +import { RadioGroup, SPACING, Flex } from '@opentrons/components' + +// note: this typescript stuff is so that e.currentTarget.value in the ChangeEventHandler +// is deduced to a union of the values of the options passed to the radiogroup rather than +// just string +export interface Target extends Omit { + value: T +} + +export type Options = Array<{ + value: T + children: React.ReactNode +}> + +export interface RecoveryRadioGroupProps + extends Omit< + React.ComponentProps, + 'labelTextClassName' | 'options' | 'onchange' + > { + options: Options + onChange: ChangeEventHandler> +} + +export function RecoveryRadioGroup( + props: RecoveryRadioGroupProps +): JSX.Element { + return ( + ({ + name: '', + value: radioOption.value, + children: ( + {radioOption.children} + ), + }))} + /> + ) +} diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/ReplaceTips.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/ReplaceTips.tsx index 48ddf992e2c..9d7f8adfcd7 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/ReplaceTips.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/ReplaceTips.tsx @@ -3,60 +3,54 @@ import * as React from 'react' import { Flex } from '@opentrons/components' import { useTranslation } from 'react-i18next' -import { RecoveryContentWrapper } from './RecoveryContentWrapper' +import { RecoverySingleColumnContentWrapper } from './RecoveryContentWrapper' import { TwoColumn, DeckMapContent } from '../../../molecules/InterventionModal' import { RecoveryFooterButtons } from './RecoveryFooterButtons' import { LeftColumnLabwareInfo } from './LeftColumnLabwareInfo' +import { getSlotNameAndLwLocFrom } from '../hooks/useDeckMapUtils' import type { RecoveryContentProps } from '../types' export function ReplaceTips(props: RecoveryContentProps): JSX.Element | null { const { - isOnDevice, routeUpdateActions, failedPipetteInfo, failedLabwareUtils, deckMapUtils, } = props - const { relevantWellName } = failedLabwareUtils + const { relevantWellName, failedLabware } = failedLabwareUtils const { proceedNextStep } = routeUpdateActions const { t } = useTranslation('error_recovery') const primaryOnClick = (): void => { void proceedNextStep() } - + const [slot] = getSlotNameAndLwLocFrom(failedLabware?.location ?? null, false) const buildTitle = (): string => { if (failedPipetteInfo?.data.channels === 96) { - return t('replace_with_new_tip_rack') + return t('replace_with_new_tip_rack', { slot }) } else { return t('replace_used_tips_in_rack_location', { location: relevantWellName, + slot, }) } } - if (isOnDevice) { - return ( - - - - - - - - + + - - ) - } else { - return null - } + + + + + + + ) } diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/SelectTips.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/SelectTips.tsx index 72210a09b61..0b6f66aa484 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/SelectTips.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/SelectTips.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import { RECOVERY_MAP } from '../constants' -import { RecoveryContentWrapper } from './RecoveryContentWrapper' +import { RecoverySingleColumnContentWrapper } from './RecoveryContentWrapper' import { TwoColumn } from '../../../molecules/InterventionModal' import { RecoveryFooterButtons } from './RecoveryFooterButtons' import { LeftColumnLabwareInfo } from './LeftColumnLabwareInfo' @@ -12,12 +12,7 @@ import { TipSelection } from './TipSelection' import type { RecoveryContentProps } from '../types' export function SelectTips(props: RecoveryContentProps): JSX.Element | null { - const { - failedPipetteInfo, - isOnDevice, - routeUpdateActions, - recoveryCommands, - } = props + const { failedPipetteInfo, routeUpdateActions, recoveryCommands } = props const { ROBOT_PICKING_UP_TIPS } = RECOVERY_MAP const { pickUpTips } = recoveryCommands const { @@ -38,39 +33,34 @@ export function SelectTips(props: RecoveryContentProps): JSX.Element | null { setShowTipSelectModal(!showTipSelectModal) } - if (isOnDevice) { - return ( - <> - {showTipSelectModal && ( - + {showTipSelectModal && ( + + )} + + + - )} - - - - - - - - - ) - } else { - return null - } + + + + + + ) } diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/TipSelection.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/TipSelection.tsx index 30640465f5e..2b9084cb0f4 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/TipSelection.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/TipSelection.tsx @@ -9,7 +9,6 @@ export type TipSelectionProps = RecoveryContentProps & { allowTipSelection: boolean } -// TODO(jh, 06-13-24): EXEC-535. export function TipSelection(props: TipSelectionProps): JSX.Element { const { failedLabwareUtils, failedPipetteInfo, allowTipSelection } = props diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/TwoColTextAndFailedStepNextStep.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/TwoColTextAndFailedStepNextStep.tsx index a6185768294..4ed62e8ff8d 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/TwoColTextAndFailedStepNextStep.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/TwoColTextAndFailedStepNextStep.tsx @@ -1,5 +1,4 @@ import * as React from 'react' -import { useTranslation } from 'react-i18next' import { css } from 'styled-components' import { DIRECTION_COLUMN, @@ -9,12 +8,10 @@ import { RESPONSIVENESS, } from '@opentrons/components' -import { RecoveryContentWrapper } from './RecoveryContentWrapper' -import { - TwoColumn, - CategorizedStepContent, -} from '../../../molecules/InterventionModal' +import { RecoverySingleColumnContentWrapper } from './RecoveryContentWrapper' +import { TwoColumn } from '../../../molecules/InterventionModal' import { RecoveryFooterButtons } from './RecoveryFooterButtons' +import { FailedStepNextStep } from './FailedStepNextStep' import type { RecoveryContentProps } from '../types' @@ -24,61 +21,34 @@ type TwoColTextAndFailedStepNextStepProps = RecoveryContentProps & { primaryBtnCopy: string primaryBtnOnClick: () => void secondaryBtnOnClickOverride?: () => void - secondaryBtnOnClickCopyOverride?: string } /** * Left Column: Title + body text * Right column: FailedStepNextStep */ -export function TwoColTextAndFailedStepNextStep({ - leftColBodyText, - leftColTitle, - primaryBtnCopy, - primaryBtnOnClick, - secondaryBtnOnClickOverride, - secondaryBtnOnClickCopyOverride, - isOnDevice, - routeUpdateActions, - failedCommand, - stepCounts, - commandsAfterFailedCommand, - protocolAnalysis, - robotType, -}: TwoColTextAndFailedStepNextStepProps): JSX.Element | null { +export function TwoColTextAndFailedStepNextStep( + props: TwoColTextAndFailedStepNextStepProps +): JSX.Element | null { + const { + leftColBodyText, + leftColTitle, + primaryBtnCopy, + primaryBtnOnClick, + secondaryBtnOnClickOverride, + routeUpdateActions, + } = props const { goBackPrevStep } = routeUpdateActions - const { t } = useTranslation('error_recovery') - const nthStepAfter = (n: number): number | undefined => - stepCounts.currentStepNumber == null - ? undefined - : stepCounts.currentStepNumber + n - const nthCommand = (n: number): typeof failedCommand => - commandsAfterFailedCommand != null - ? n < commandsAfterFailedCommand.length - ? commandsAfterFailedCommand[n] - : null - : null - - const commandsAfter = [nthCommand(0), nthCommand(1)] as const - - const indexedCommandsAfter = [ - commandsAfter[0] != null - ? { command: commandsAfter[0], index: nthStepAfter(1) } - : null, - commandsAfter[1] != null - ? { command: commandsAfter[1], index: nthStepAfter(2) } - : null, - ] as const return ( - + @@ -95,30 +65,13 @@ export function TwoColTextAndFailedStepNextStep({ {leftColBodyText} - + - + ) } diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/ErrorDetailsModal.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/ErrorDetailsModal.test.tsx index 80ebae71884..3eb590f1a35 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/ErrorDetailsModal.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/ErrorDetailsModal.test.tsx @@ -11,7 +11,6 @@ import { Modal } from '../../../../molecules/Modal' import { useErrorDetailsModal, ErrorDetailsModal, - ErrorDetailsModalODD, OverpressureBanner, } from '../ErrorDetailsModal' @@ -58,7 +57,7 @@ describe('ErrorDetailsModal', () => { vi.mocked(StepInfo).mockReturnValue(
    MOCK_STEP_INFO
    ) }) - it('renders ErrorDetailsModalODD', () => { + it('renders ErrorDetailsModal', () => { renderWithProviders(, { i18nInstance: i18n, }) @@ -66,14 +65,14 @@ describe('ErrorDetailsModal', () => { }) }) -const render = (props: React.ComponentProps) => { - return renderWithProviders(, { +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { i18nInstance: i18n, })[0] } -describe('ErrorDetailsModalODD', () => { - let props: React.ComponentProps +describe('ErrorDetailsModal', () => { + let props: React.ComponentProps beforeEach(() => { props = { diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/LeftColumnLabwareInfo.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/LeftColumnLabwareInfo.test.tsx index 4ea8cee5ac4..0edf9b95236 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/LeftColumnLabwareInfo.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/LeftColumnLabwareInfo.test.tsx @@ -6,10 +6,12 @@ import { renderWithProviders } from '../../../../__testing-utils__' import { mockRecoveryContentProps } from '../../__fixtures__' import { i18n } from '../../../../i18n' import { LeftColumnLabwareInfo } from '../LeftColumnLabwareInfo' -import { Move } from '../../../../molecules/InterventionModal/InterventionStep' +import { InterventionInfo } from '../../../../molecules/InterventionModal/InterventionContent/InterventionInfo' import { InlineNotification } from '../../../../atoms/InlineNotification' -vi.mock('../../../../molecules/InterventionModal/InterventionStep') +vi.mock( + '../../../../molecules/InterventionModal/InterventionContent/InterventionInfo' +) vi.mock('../../../../atoms/InlineNotification') const render = (props: React.ComponentProps) => { @@ -31,24 +33,24 @@ describe('LeftColumnLabwareInfo', () => { location: { slotName: 'A1' }, }, } as any, - moveType: 'refill', + type: 'location', bannerText: 'MOCK_BANNER_TEXT', } - vi.mocked(Move).mockReturnValue(
    MOCK_MOVE
    ) + vi.mocked(InterventionInfo).mockReturnValue(
    MOCK_MOVE
    ) vi.mocked(InlineNotification).mockReturnValue(
    MOCK_INLINE_NOTIFICATION
    ) }) - it('renders the title, Move component, and InlineNotification when bannerText is provided', () => { + it('renders the title, InterventionInfo component, and InlineNotification when bannerText is provided', () => { render(props) screen.getByText('MOCK_TITLE') screen.getByText('MOCK_MOVE') - expect(vi.mocked(Move)).toHaveBeenCalledWith( + expect(vi.mocked(InterventionInfo)).toHaveBeenCalledWith( expect.objectContaining({ - type: 'refill', + type: 'location', labwareName: 'MOCK_LW_NAME', currentLocationProps: { slotName: 'A1' }, }), @@ -78,7 +80,7 @@ describe('LeftColumnLabwareInfo', () => { props.failedLabwareUtils.failedLabware.location = 'offDeck' render(props) - expect(vi.mocked(Move)).toHaveBeenCalledWith( + expect(vi.mocked(InterventionInfo)).toHaveBeenCalledWith( expect.objectContaining({ currentLocationProps: { slotName: '' }, }), diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/RecoveryFooterButtons.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/RecoveryFooterButtons.test.tsx index 94586b231da..b4e2b260715 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/RecoveryFooterButtons.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/RecoveryFooterButtons.test.tsx @@ -27,7 +27,6 @@ describe('RecoveryFooterButtons', () => { mockSecondaryBtnOnClick = vi.fn() mockTertiaryBtnOnClick = vi.fn() props = { - isOnDevice: true, primaryBtnOnClick: mockPrimaryBtnOnClick, secondaryBtnOnClick: mockSecondaryBtnOnClick, } @@ -36,21 +35,48 @@ describe('RecoveryFooterButtons', () => { it('renders default button copy and click behavior', () => { render(props) - const primaryBtn = screen.getByRole('button', { name: 'Continue' }) - const secondaryBtn = screen.getByRole('button', { name: 'Go back' }) + const primaryBtns = screen.getAllByRole('button', { name: 'Continue' }) + const secondaryBtns = screen.getAllByRole('button', { name: 'Go back' }) + expect(primaryBtns.length).toBe(2) + expect(secondaryBtns.length).toBe(1) - fireEvent.click(primaryBtn) - fireEvent.click(secondaryBtn) + primaryBtns.forEach(btn => { + mockPrimaryBtnOnClick.mockReset() + fireEvent.click(btn) + expect(mockPrimaryBtnOnClick).toHaveBeenCalled() + }) - expect(mockPrimaryBtnOnClick).toHaveBeenCalled() - expect(mockSecondaryBtnOnClick).toHaveBeenCalled() + secondaryBtns.forEach(btn => { + mockSecondaryBtnOnClick.mockReset() + fireEvent.click(btn) + expect(mockSecondaryBtnOnClick).toHaveBeenCalled() + }) }) it('renders alternative button text when supplied', () => { props = { ...props, primaryBtnTextOverride: 'MOCK_OVERRIDE_TEXT' } render(props) - screen.getByRole('button', { name: 'MOCK_OVERRIDE_TEXT' }) + const secondaries = screen.getAllByRole('button', { + name: 'MOCK_OVERRIDE_TEXT', + }) + expect(secondaries.length).toBe(2) + }) + + it('renders the primary button as disabled when primaryBtnDisabled is true', () => { + props = { + ...props, + primaryBtnOnClick: mockPrimaryBtnOnClick, + primaryBtnDisabled: true, + primaryBtnTextOverride: 'Hi', + } + render(props) + + const primaryBtns = screen.getAllByRole('button', { name: 'Hi' }) + + primaryBtns.forEach(btn => { + expect(btn).toBeDisabled() + }) }) it('does not render the secondary button if no on click handler is supplied', () => { @@ -66,30 +92,36 @@ describe('RecoveryFooterButtons', () => { props = { ...props, isLoadingPrimaryBtnAction: true } render(props) - const primaryBtn = screen.getByRole('button', { + const primaryBtns = screen.getAllByRole('button', { name: 'loading indicator Continue', }) screen.getByLabelText('loading indicator') - expect(primaryBtn).toHaveStyle(`background-color: ${COLORS.blue60}`) + primaryBtns.forEach(btn => { + expect(btn).toHaveStyle(`background-color: ${COLORS.blue60}`) + }) }) it('renders the tertiary button when tertiaryBtnOnClick is provided', () => { props = { ...props, tertiaryBtnOnClick: mockTertiaryBtnOnClick } render(props) - const tertiaryBtn = screen.getByRole('button', { name: '' }) - - fireEvent.click(tertiaryBtn) + const tertiaryBtns = screen.getAllByRole('button', { name: '' }) + expect(tertiaryBtns.length).toBe(2) - expect(mockTertiaryBtnOnClick).toHaveBeenCalled() + tertiaryBtns.forEach(btn => { + mockTertiaryBtnOnClick.mockReset() + fireEvent.click(btn) + expect(mockTertiaryBtnOnClick).toHaveBeenCalled() + }) }) it('renders the tertiary button with custom text when tertiaryBtnText is provided', () => { props = { ...props, tertiaryBtnText: 'Hey' } render(props) - screen.getByRole('button', { name: 'Hey' }) + const tertiaryBtns = screen.getAllByRole('button', { name: 'Hey' }) + expect(tertiaryBtns.length).toBe(2) }) it('renders the tertiary button as disabled when tertiaryBtnDisabled is true', () => { @@ -101,8 +133,10 @@ describe('RecoveryFooterButtons', () => { } render(props) - const tertiaryBtn = screen.getByRole('button', { name: 'Hi' }) + const tertiaryBtns = screen.getAllByRole('button', { name: 'Hi' }) - expect(tertiaryBtn).toBeDisabled() + tertiaryBtns.forEach(btn => { + expect(btn).toBeDisabled() + }) }) }) diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/SelectTips.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/SelectTips.test.tsx index 36e486e2504..15afe841639 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/SelectTips.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/SelectTips.test.tsx @@ -59,7 +59,7 @@ describe('SelectTips', () => { it('renders the TipSelectionModal when showTipSelectModal is true', () => { render(props) - fireEvent.click(screen.getByText('Change location')) + fireEvent.click(screen.getAllByText('Change location')[0]) expect(screen.getByText('MOCK TIP SELECTION MODAL')).toBeInTheDocument() }) @@ -84,7 +84,7 @@ describe('SelectTips', () => { routeUpdateActions: mockRouteUpdateActions, }) - const primaryBtn = screen.getByText('Pick up tips') + const primaryBtn = screen.getAllByText('Pick up tips')[0] fireEvent.click(primaryBtn) await waitFor(() => { @@ -117,7 +117,7 @@ describe('SelectTips', () => { it('calls goBackPrevStep when the secondary button is clicked', () => { render(props) - fireEvent.click(screen.getByText('Go back')) + fireEvent.click(screen.getAllByText('Go back')[0]) expect(mockGoBackPrevStep).toHaveBeenCalled() }) @@ -133,7 +133,9 @@ describe('SelectTips', () => { } render(props) - const tertiaryBtn = screen.getByRole('button', { name: 'Change location' }) - expect(tertiaryBtn).toBeDisabled() + const tertiaryBtn = screen.getAllByRole('button', { + name: 'Change location', + }) + expect(tertiaryBtn[0]).toBeDisabled() }) }) diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/StepInfo.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/StepInfo.test.tsx index 54e579daf93..4e7e8b393fa 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/StepInfo.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/StepInfo.test.tsx @@ -3,7 +3,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest' import { screen } from '@testing-library/react' import { renderWithProviders } from '../../../../__testing-utils__' -import { mockRecoveryContentProps } from '../../__fixtures__' +import { mockRecoveryContentProps, mockFailedCommand } from '../../__fixtures__' import { i18n } from '../../../../i18n' import { StepInfo } from '../StepInfo' import { CommandText } from '../../../../molecules/Command' @@ -21,7 +21,10 @@ describe('StepInfo', () => { beforeEach(() => { props = { - ...mockRecoveryContentProps, + ...{ + ...mockRecoveryContentProps, + protocolAnalysis: { commands: [mockFailedCommand] } as any, + }, textStyle: 'h4', stepCounts: { currentStepNumber: 5, diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/index.ts b/app/src/organisms/ErrorRecoveryFlows/shared/index.ts index 33c9299db44..955058e5311 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/index.ts +++ b/app/src/organisms/ErrorRecoveryFlows/shared/index.ts @@ -1,5 +1,9 @@ export { RecoveryFooterButtons } from './RecoveryFooterButtons' -export { RecoveryContentWrapper } from './RecoveryContentWrapper' +export { + RecoverySingleColumnContentWrapper, + RecoveryTwoColumnContentWrapper, + RecoveryODDOneDesktopTwoColumnContentWrapper, +} from './RecoveryContentWrapper' export { ReplaceTips } from './ReplaceTips' export { SelectTips } from './SelectTips' export { TwoColTextAndFailedStepNextStep } from './TwoColTextAndFailedStepNextStep' @@ -9,5 +13,7 @@ export { TipSelectionModal } from './TipSelectionModal' export { StepInfo } from './StepInfo' export { useErrorDetailsModal, ErrorDetailsModal } from './ErrorDetailsModal' export { RecoveryInterventionModal } from './RecoveryInterventionModal' +export { FailedStepNextStep } from './FailedStepNextStep' +export { RecoveryRadioGroup } from './RecoveryRadioGroup' export type { RecoveryInterventionModalProps } from './RecoveryInterventionModal' diff --git a/app/src/organisms/InstrumentInfo/__tests__/InstrumentInfo.test.tsx b/app/src/organisms/InstrumentInfo/__tests__/InstrumentInfo.test.tsx index 35bd692a589..adccba35529 100644 --- a/app/src/organisms/InstrumentInfo/__tests__/InstrumentInfo.test.tsx +++ b/app/src/organisms/InstrumentInfo/__tests__/InstrumentInfo.test.tsx @@ -11,7 +11,7 @@ import { InstrumentInfo } from '..' import type { GripperData } from '@opentrons/api-client' import type * as Dom from 'react-router-dom' -const mockPush = vi.fn() +const mockNavigate = vi.fn() vi.mock('../../PipetteWizardFlows') vi.mock('../../GripperWizardFlows') @@ -19,7 +19,7 @@ vi.mock('react-router-dom', async importOriginal => { const reactRouterDom = await importOriginal() return { ...reactRouterDom, - useHistory: () => ({ push: mockPush } as any), + useNavigate: () => mockNavigate, } }) diff --git a/app/src/organisms/InstrumentInfo/index.tsx b/app/src/organisms/InstrumentInfo/index.tsx index d87491a21bf..ea7599dde76 100644 --- a/app/src/organisms/InstrumentInfo/index.tsx +++ b/app/src/organisms/InstrumentInfo/index.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { BORDERS, COLORS, @@ -35,7 +35,7 @@ interface InstrumentInfoProps { export const InstrumentInfo = (props: InstrumentInfoProps): JSX.Element => { const { t, i18n } = useTranslation('instruments_dashboard') const { instrument } = props - const history = useHistory() + const navigate = useNavigate() const [wizardProps, setWizardProps] = React.useState< | React.ComponentProps | React.ComponentProps @@ -66,7 +66,7 @@ export const InstrumentInfo = (props: InstrumentInfoProps): JSX.Element => { ...sharedGripperWizardProps, flowType: GRIPPER_FLOW_TYPES.DETACH, onComplete: () => { - history.goBack() + navigate(-1) }, } : { @@ -74,7 +74,7 @@ export const InstrumentInfo = (props: InstrumentInfoProps): JSX.Element => { setWizardProps(null) }, onComplete: () => { - history.goBack() + navigate(-1) }, mount: instrument.mount as PipetteMount, selectedPipette: is96Channel diff --git a/app/src/organisms/InstrumentMountItem/AttachedInstrumentMountItem.tsx b/app/src/organisms/InstrumentMountItem/AttachedInstrumentMountItem.tsx index 1811fbaf706..d56a1af86ae 100644 --- a/app/src/organisms/InstrumentMountItem/AttachedInstrumentMountItem.tsx +++ b/app/src/organisms/InstrumentMountItem/AttachedInstrumentMountItem.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { SINGLE_MOUNT_PIPETTES } from '@opentrons/shared-data' @@ -33,8 +33,7 @@ interface AttachedInstrumentMountItemProps { export function AttachedInstrumentMountItem( props: AttachedInstrumentMountItemProps ): JSX.Element { - const history = useHistory() - console.log(history) + const navigate = useNavigate() const { mount, attachedInstrument, setWizardProps } = props const [showChoosePipetteModal, setShowChoosePipetteModal] = React.useState( @@ -53,8 +52,8 @@ export function AttachedInstrumentMountItem( flowType: GRIPPER_FLOW_TYPES.ATTACH, attachedGripper: attachedInstrument, onComplete: () => { - history.push( - attachedInstrument == null ? `/instruments` : `/instrument/${mount}` + navigate( + attachedInstrument == null ? '/instruments' : `/instrument/${mount}` ) }, closeFlow: () => { @@ -62,7 +61,7 @@ export function AttachedInstrumentMountItem( }, }) } else { - history.push(`/instruments/${mount}`) + navigate(`/instruments/${mount}`) } } @@ -101,7 +100,7 @@ export function AttachedInstrumentMountItem( setShowChoosePipetteModal(false) }, onComplete: () => { - history.push( + navigate( attachedInstrument == null ? `/instruments` : `/instrument/${mount}` diff --git a/app/src/organisms/ModuleCard/__tests__/ModuleCard.test.tsx b/app/src/organisms/ModuleCard/__tests__/ModuleCard.test.tsx index 8c6dbcfd025..4dc583d6c9d 100644 --- a/app/src/organisms/ModuleCard/__tests__/ModuleCard.test.tsx +++ b/app/src/organisms/ModuleCard/__tests__/ModuleCard.test.tsx @@ -37,6 +37,7 @@ import { FirmwareUpdateFailedModal } from '../FirmwareUpdateFailedModal' import { ErrorInfo } from '../ErrorInfo' import { ModuleCard } from '..' +import type { NavigateFunction } from 'react-router-dom' import type { HeaterShakerModule, MagneticModule, @@ -57,6 +58,13 @@ vi.mock('../../../redux/robot-api') vi.mock('../../../organisms/ToasterOven') vi.mock('../../../organisms/Devices/hooks') vi.mock('../../../resources/devices/hooks/useIsEstopNotDisengaged') +vi.mock('react-router-dom', async importOriginal => { + const actual = await importOriginal() + return { + ...actual, + useNavigate: () => vi.fn(), + } +}) const mockMagneticModuleHub = { id: 'magdeck_id', diff --git a/app/src/organisms/ModuleCard/index.tsx b/app/src/organisms/ModuleCard/index.tsx index c6f9b27ee9f..c3d78ddf730 100644 --- a/app/src/organisms/ModuleCard/index.tsx +++ b/app/src/organisms/ModuleCard/index.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { Trans, useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { ALIGN_START, @@ -126,11 +126,11 @@ export const ModuleCard = (props: ModuleCardProps): JSX.Element | null => { const [showCalModal, setShowCalModal] = React.useState(false) const [targetProps, tooltipProps] = useHoverTooltip() - const history = useHistory() + const navigate = useNavigate() const runStatus = useCurrentRunStatus({ onSettled: data => { if (data == null) { - history.push('/upload') + navigate('/upload') } }, }) diff --git a/app/src/organisms/Navigation/NavigationMenu.tsx b/app/src/organisms/Navigation/NavigationMenu.tsx index 9c933f31a38..3b4f10752e8 100644 --- a/app/src/organisms/Navigation/NavigationMenu.tsx +++ b/app/src/organisms/Navigation/NavigationMenu.tsx @@ -1,15 +1,16 @@ import * as React from 'react' +import { createPortal } from 'react-dom' import { useDispatch } from 'react-redux' import { useTranslation } from 'react-i18next' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { ALIGN_CENTER, COLORS, Flex, Icon, - SPACING, LegacyStyledText, + SPACING, TYPOGRAPHY, } from '@opentrons/components' @@ -17,6 +18,7 @@ import { MenuList } from '../../atoms/MenuList' import { MenuItem } from '../../atoms/MenuList/MenuItem' import { home, ROBOT } from '../../redux/robot-controls' import { useLights } from '../Devices/hooks' +import { getTopPortalEl } from '../../App/portal' import { RestartRobotConfirmationModal } from './RestartRobotConfirmationModal' import type { Dispatch } from '../../redux/types' @@ -37,7 +39,7 @@ export function NavigationMenu(props: NavigationMenuProps): JSX.Element { setShowRestartRobotConfirmationModal, ] = React.useState(false) - const history = useHistory() + const navigate = useNavigate() const handleRestart = (): void => { setShowRestartRobotConfirmationModal(true) @@ -48,9 +50,7 @@ export function NavigationMenu(props: NavigationMenuProps): JSX.Element { setShowNavMenu(false) } - // ToDo (kk:10/02/2023) - // Need to update a function for onClick - return ( + return createPortal( <> {showRestartRobotConfirmationModal ? ( { - history.push('/deck-configuration') + navigate('/deck-configuration') }} > @@ -132,6 +132,7 @@ export function NavigationMenu(props: NavigationMenuProps): JSX.Element { - + , + getTopPortalEl() ) } diff --git a/app/src/organisms/Navigation/__tests__/NavigationMenu.test.tsx b/app/src/organisms/Navigation/__tests__/NavigationMenu.test.tsx index 1c85aa47861..52f18c32306 100644 --- a/app/src/organisms/Navigation/__tests__/NavigationMenu.test.tsx +++ b/app/src/organisms/Navigation/__tests__/NavigationMenu.test.tsx @@ -9,19 +9,19 @@ import { useLights } from '../../Devices/hooks' import { RestartRobotConfirmationModal } from '../RestartRobotConfirmationModal' import { NavigationMenu } from '../NavigationMenu' -import type { useHistory } from 'react-router-dom' +import type { NavigateFunction } from 'react-router-dom' vi.mock('../../../redux/robot-admin') vi.mock('../../../redux/robot-controls') vi.mock('../../Devices/hooks') vi.mock('../RestartRobotConfirmationModal') -const mockPush = vi.fn() +const mockNavigate = vi.fn() vi.mock('react-router-dom', async importOriginal => { - const actual = await importOriginal() + const actual = await importOriginal() return { ...actual, - useHistory: () => ({ push: mockPush } as any), + useNavigate: () => mockNavigate, } }) @@ -97,6 +97,6 @@ describe('NavigationMenu', () => { it('should call a mock function when tapping deck configuration', () => { render(props) fireEvent.click(screen.getByText('Deck configuration')) - expect(mockPush).toHaveBeenCalledWith('/deck-configuration') + expect(mockNavigate).toHaveBeenCalledWith('/deck-configuration') }) }) diff --git a/app/src/organisms/NetworkSettings/AlternativeSecurityTypeModal.tsx b/app/src/organisms/NetworkSettings/AlternativeSecurityTypeModal.tsx index 50a9a05b074..b4a532f095b 100644 --- a/app/src/organisms/NetworkSettings/AlternativeSecurityTypeModal.tsx +++ b/app/src/organisms/NetworkSettings/AlternativeSecurityTypeModal.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { COLORS, @@ -27,7 +27,7 @@ export function AlternativeSecurityTypeModal({ setShowAlternativeSecurityTypeModal, }: AlternativeSecurityTypeModalProps): JSX.Element { const { t } = useTranslation(['device_settings', 'branded']) - const history = useHistory() + const navigate = useNavigate() const modalHeader: ModalHeaderBaseProps = { title: t('alternative_security_types'), hasExitIcon: true, @@ -37,7 +37,7 @@ export function AlternativeSecurityTypeModal({ } const handleClick = (): void => { setShowAlternativeSecurityTypeModal(false) - history.push('/network-setup/usb') + navigate('/network-setup/usb') } return ( diff --git a/app/src/organisms/NetworkSettings/DisplayWifiList.tsx b/app/src/organisms/NetworkSettings/DisplayWifiList.tsx index 137391618c5..2925a47f392 100644 --- a/app/src/organisms/NetworkSettings/DisplayWifiList.tsx +++ b/app/src/organisms/NetworkSettings/DisplayWifiList.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { css } from 'styled-components' import { @@ -73,7 +73,7 @@ export function DisplayWifiList({ isHeader = false, }: DisplayWifiListProps): JSX.Element { const { t } = useTranslation('device_settings') - const history = useHistory() + const navigate = useNavigate() return ( <> @@ -81,7 +81,7 @@ export function DisplayWifiList({ { - history.push('/network-setup') + navigate('/network-setup') }} /> ) : null} diff --git a/app/src/organisms/NetworkSettings/WifiConnectionDetails.tsx b/app/src/organisms/NetworkSettings/WifiConnectionDetails.tsx index 97f264dc556..ec95615c4e5 100644 --- a/app/src/organisms/NetworkSettings/WifiConnectionDetails.tsx +++ b/app/src/organisms/NetworkSettings/WifiConnectionDetails.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { ALIGN_CENTER, @@ -37,7 +37,7 @@ export function WifiConnectionDetails({ authType, }: WifiConnectionDetailsProps): JSX.Element { const { i18n, t } = useTranslation(['device_settings', 'shared']) - const history = useHistory() + const navigate = useNavigate() const localRobot = useSelector(getLocalRobot) const robotName = localRobot?.name != null ? localRobot.name : 'no name' const dispatch = useDispatch() @@ -95,7 +95,7 @@ export function WifiConnectionDetails({ flex="1" buttonText={i18n.format(t('continue'), 'capitalize')} onClick={() => { - history.push('/robot-settings/update-robot-during-onboarding') + navigate('/robot-settings/update-robot-during-onboarding') }} />
    diff --git a/app/src/organisms/NetworkSettings/__tests__/AlternativeSecurityTypeModal.test.tsx b/app/src/organisms/NetworkSettings/__tests__/AlternativeSecurityTypeModal.test.tsx index d838c397942..ee23afbee84 100644 --- a/app/src/organisms/NetworkSettings/__tests__/AlternativeSecurityTypeModal.test.tsx +++ b/app/src/organisms/NetworkSettings/__tests__/AlternativeSecurityTypeModal.test.tsx @@ -6,15 +6,15 @@ import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' import { AlternativeSecurityTypeModal } from '../AlternativeSecurityTypeModal' -import type { useHistory } from 'react-router-dom' +import type { NavigateFunction } from 'react-router-dom' const mockFunc = vi.fn() -const mockPush = vi.fn() +const mockNavigate = vi.fn() vi.mock('react-router-dom', async importOriginal => { - const actual = await importOriginal() + const actual = await importOriginal() return { ...actual, - useHistory: () => ({ push: mockPush } as any), + useNavigate: () => mockNavigate, } }) @@ -55,6 +55,6 @@ describe('AlternativeSecurityTypeModal', () => { const button = screen.getByText('Connect via USB') fireEvent.click(button) expect(mockFunc).toHaveBeenCalled() - expect(mockPush).toHaveBeenCalledWith('/network-setup/usb') + expect(mockNavigate).toHaveBeenCalledWith('/network-setup/usb') }) }) diff --git a/app/src/organisms/NetworkSettings/__tests__/DisplayWifiList.test.tsx b/app/src/organisms/NetworkSettings/__tests__/DisplayWifiList.test.tsx index 04920134dee..2a901ee1850 100644 --- a/app/src/organisms/NetworkSettings/__tests__/DisplayWifiList.test.tsx +++ b/app/src/organisms/NetworkSettings/__tests__/DisplayWifiList.test.tsx @@ -8,9 +8,9 @@ import * as Fixtures from '../../../redux/networking/__fixtures__' import { DisplaySearchNetwork } from '../DisplaySearchNetwork' import { DisplayWifiList } from '../DisplayWifiList' -import type { useHistory } from 'react-router-dom' +import type { NavigateFunction } from 'react-router-dom' -const mockPush = vi.fn() +const mockNavigate = vi.fn() const mockWifiList = [ { ...Fixtures.mockWifiNetwork, ssid: 'foo', active: true }, { ...Fixtures.mockWifiNetwork, ssid: 'bar' }, @@ -24,10 +24,10 @@ vi.mock('../../../redux/networking/selectors') vi.mock('../../../redux/discovery/selectors') vi.mock('../DisplaySearchNetwork') vi.mock('react-router-dom', async importOriginal => { - const actual = await importOriginal() + const actual = await importOriginal() return { ...actual, - useHistory: () => ({ push: mockPush } as any), + useNavigate: () => mockNavigate, } }) @@ -76,7 +76,7 @@ describe('DisplayWifiList', () => { render(props) const button = screen.getByLabelText('back-button') fireEvent.click(button) - expect(mockPush).toHaveBeenCalledWith('/network-setup') + expect(mockNavigate).toHaveBeenCalledWith('/network-setup') }) it('should call mock function when tapping tapping a ssid', () => { diff --git a/app/src/organisms/NetworkSettings/__tests__/SelectAuthenticationType.test.tsx b/app/src/organisms/NetworkSettings/__tests__/SelectAuthenticationType.test.tsx index 2028e100991..d014f2b5316 100644 --- a/app/src/organisms/NetworkSettings/__tests__/SelectAuthenticationType.test.tsx +++ b/app/src/organisms/NetworkSettings/__tests__/SelectAuthenticationType.test.tsx @@ -11,9 +11,9 @@ import { AlternativeSecurityTypeModal } from '../AlternativeSecurityTypeModal' import { SelectAuthenticationType } from '../SelectAuthenticationType' import { SetWifiCred } from '../SetWifiCred' -import type { useHistory } from 'react-router-dom' +import type { NavigateFunction } from 'react-router-dom' -const mockPush = vi.fn() +const mockNavigate = vi.fn() const mockSetSelectedAuthType = vi.fn() vi.mock('../SetWifiCred') @@ -22,10 +22,10 @@ vi.mock('../../../redux/discovery/selectors') vi.mock('../AlternativeSecurityTypeModal') vi.mock('../../RobotSettingsDashboard/NetworkSettings/hooks') vi.mock('react-router-dom', async importOriginal => { - const actual = await importOriginal() + const actual = await importOriginal() return { ...actual, - useHistory: () => ({ push: mockPush } as any), + useNavigate: () => mockNavigate, } }) diff --git a/app/src/organisms/NetworkSettings/__tests__/WifiConnectionDetails.test.tsx b/app/src/organisms/NetworkSettings/__tests__/WifiConnectionDetails.test.tsx index 479f8c65ab0..3c5427d3426 100644 --- a/app/src/organisms/NetworkSettings/__tests__/WifiConnectionDetails.test.tsx +++ b/app/src/organisms/NetworkSettings/__tests__/WifiConnectionDetails.test.tsx @@ -11,19 +11,19 @@ import * as Fixtures from '../../../redux/networking/__fixtures__' import { NetworkDetailsModal } from '../../RobotSettingsDashboard/NetworkSettings/NetworkDetailsModal' import { WifiConnectionDetails } from '../WifiConnectionDetails' -import type { useHistory } from 'react-router-dom' +import type { NavigateFunction } from 'react-router-dom' vi.mock('../../../resources/networking/hooks') vi.mock('../../../redux/networking') vi.mock('../../../redux/discovery/selectors') vi.mock('../../RobotSettingsDashboard/NetworkSettings/NetworkDetailsModal') -const mockPush = vi.fn() +const mockNavigate = vi.fn() vi.mock('react-router-dom', async importOriginal => { - const actual = await importOriginal() + const actual = await importOriginal() return { ...actual, - useHistory: () => ({ push: mockPush } as any), + useNavigate: () => mockNavigate, } }) @@ -88,7 +88,7 @@ describe('WifiConnectionDetails', () => { it('when clicking Check for updates button, should call mock function', () => { render(props) fireEvent.click(screen.getByText('Continue')) - expect(mockPush).toHaveBeenCalledWith( + expect(mockNavigate).toHaveBeenCalledWith( '/robot-settings/update-robot-during-onboarding' ) }) diff --git a/app/src/organisms/OnDeviceDisplay/NameRobot/ConfirmRobotName.tsx b/app/src/organisms/OnDeviceDisplay/NameRobot/ConfirmRobotName.tsx index 449d31105f0..ebcb54f1f69 100644 --- a/app/src/organisms/OnDeviceDisplay/NameRobot/ConfirmRobotName.tsx +++ b/app/src/organisms/OnDeviceDisplay/NameRobot/ConfirmRobotName.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { @@ -27,10 +27,10 @@ export function ConfirmRobotName({ robotName, }: ConfirmRobotNameProps): JSX.Element { const { t } = useTranslation('device_settings') - const history = useHistory() + const navigate = useNavigate() const handleClick = (): void => { - history.push('/dashboard') + navigate('/dashboard') } return ( <> diff --git a/app/src/organisms/OnDeviceDisplay/NameRobot/__tests__/ConfirmRobotName.test.tsx b/app/src/organisms/OnDeviceDisplay/NameRobot/__tests__/ConfirmRobotName.test.tsx index 5b6966ff0a6..d33230a6424 100644 --- a/app/src/organisms/OnDeviceDisplay/NameRobot/__tests__/ConfirmRobotName.test.tsx +++ b/app/src/organisms/OnDeviceDisplay/NameRobot/__tests__/ConfirmRobotName.test.tsx @@ -7,15 +7,15 @@ import { renderWithProviders } from '../../../../__testing-utils__' import { i18n } from '../../../../i18n' import { ConfirmRobotName } from '../ConfirmRobotName' -import type { useHistory } from 'react-router-dom' +import type { NavigateFunction } from 'react-router-dom' -const mockPush = vi.fn() +const mockNavigate = vi.fn() vi.mock('react-router-dom', async importOriginal => { - const actual = await importOriginal() + const actual = await importOriginal() return { ...actual, - useHistory: () => ({ push: mockPush } as any), + useNavigate: () => mockNavigate, } }) @@ -49,6 +49,6 @@ describe('ConfirmRobotName', () => { render(props) const button = screen.getByText('Finish setup') fireEvent.click(button) - expect(mockPush).toBeCalledWith('/dashboard') + expect(mockNavigate).toBeCalledWith('/dashboard') }) }) diff --git a/app/src/organisms/OnDeviceDisplay/RobotDashboard/RecentRunProtocolCard.tsx b/app/src/organisms/OnDeviceDisplay/RobotDashboard/RecentRunProtocolCard.tsx index 29ad4a159cd..f3c218a8392 100644 --- a/app/src/organisms/OnDeviceDisplay/RobotDashboard/RecentRunProtocolCard.tsx +++ b/app/src/organisms/OnDeviceDisplay/RobotDashboard/RecentRunProtocolCard.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { css } from 'styled-components' import { useTranslation } from 'react-i18next' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { formatDistance } from 'date-fns' import last from 'lodash/last' @@ -81,7 +81,7 @@ export function ProtocolWithLastRun({ isLoading: isLookingForHardware, conflictedSlots, } = useMissingProtocolHardware(protocolData.id) - const history = useHistory() + const navigate = useNavigate() const isOk = 'ok' in runData ? !(runData?.ok === false) : true const isReadyToBeReRun = isOk && missingProtocolHardware.length === 0 const chipText = useRerunnableStatusText( @@ -93,7 +93,7 @@ export function ProtocolWithLastRun({ // TODO(BC, 08/29/23): reintroduce this analytics event when we refactor the hook to fetch data lazily (performance concern) // const { trackProtocolRunEvent } = useTrackProtocolRunEvent(runData.id) const onResetSuccess = (createRunResponse: Run): void => { - history.push(`runs/${createRunResponse.data.id}/setup`) + navigate(`runs/${createRunResponse.data.id}/setup`) } const { cloneRun } = useCloneRun(runData.id, onResetSuccess) const robotInitStatus = useRobotInitializationStatus() @@ -147,7 +147,7 @@ export function ProtocolWithLastRun({ const handleCardClick = (): void => { setShowSpinner(true) if (hasRunTimeParameters) { - history.push(`/protocols/${protocolId}`) + navigate(`/protocols/${protocolId}`) } else { cloneRun() trackEvent({ diff --git a/app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCard.test.tsx b/app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCard.test.tsx index 25e040583a7..6d82103955b 100644 --- a/app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCard.test.tsx +++ b/app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCard.test.tsx @@ -33,16 +33,16 @@ import { INIT_STATUS, } from '../../../../resources/health/hooks' -import type { useHistory } from 'react-router-dom' +import type { NavigateFunction } from 'react-router-dom' import type { ProtocolHardware } from '../../../../pages/Protocols/hooks' -const mockPush = vi.fn() +const mockNavigate = vi.fn() vi.mock('react-router-dom', async importOriginal => { - const actual = await importOriginal() + const actual = await importOriginal() return { ...actual, - useHistory: () => ({ push: mockPush } as any), + useNavigate: () => mockNavigate, } }) @@ -292,6 +292,6 @@ describe('RecentRunProtocolCard', () => { render(props) const button = screen.getByLabelText('RecentRunProtocolCard') fireEvent.click(button) - expect(mockPush).toBeCalledWith('/protocols/mockProtocolId') + expect(mockNavigate).toBeCalledWith('/protocols/mockProtocolId') }) }) diff --git a/app/src/organisms/OnDeviceDisplay/RunningProtocol/ConfirmCancelRunModal.tsx b/app/src/organisms/OnDeviceDisplay/RunningProtocol/ConfirmCancelRunModal.tsx index 8bd583f7a74..76efff81b8b 100644 --- a/app/src/organisms/OnDeviceDisplay/RunningProtocol/ConfirmCancelRunModal.tsx +++ b/app/src/organisms/OnDeviceDisplay/RunningProtocol/ConfirmCancelRunModal.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { useSelector } from 'react-redux' import { RUN_STATUS_STOPPED } from '@opentrons/api-client' @@ -50,7 +50,7 @@ export function ConfirmCancelRunModal({ const localRobot = useSelector(getLocalRobot) const robotName = localRobot?.name ?? '' const { trackProtocolRunEvent } = useTrackProtocolRunEvent(runId, robotName) - const history = useHistory() + const navigate = useNavigate() const [isCanceling, setIsCanceling] = React.useState(false) const modalHeader: ModalHeaderBaseProps = { @@ -75,9 +75,9 @@ export function ConfirmCancelRunModal({ dismissCurrentRun(runId) if (!isActiveRun) { if (protocolId != null) { - history.push(`/protocols/${protocolId}`) + navigate(`/protocols/${protocolId}`) } else { - history.push(`/protocols`) + navigate('/protocols') } } } diff --git a/app/src/organisms/OnDeviceDisplay/RunningProtocol/RunFailedModal.tsx b/app/src/organisms/OnDeviceDisplay/RunningProtocol/RunFailedModal.tsx index d4e0b056b5a..c4db8a35360 100644 --- a/app/src/organisms/OnDeviceDisplay/RunningProtocol/RunFailedModal.tsx +++ b/app/src/organisms/OnDeviceDisplay/RunningProtocol/RunFailedModal.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { css } from 'styled-components' import { @@ -34,7 +34,7 @@ export function RunFailedModal({ errors, }: RunFailedModalProps): JSX.Element | null { const { t, i18n } = useTranslation(['run_details', 'shared', 'branded']) - const history = useHistory() + const navigate = useNavigate() const { stopRun } = useStopRunMutation() const [isCanceling, setIsCanceling] = React.useState(false) @@ -53,7 +53,7 @@ export function RunFailedModal({ // ToDo do we need to track this event? // If need, runCancel or runFailure something // trackProtocolRunEvent({ name: 'runCancel' }) - history.push('/dashboard') + navigate('/dashboard') }, onError: () => { setIsCanceling(false) diff --git a/app/src/organisms/OnDeviceDisplay/RunningProtocol/RunningProtocolCommandList.tsx b/app/src/organisms/OnDeviceDisplay/RunningProtocol/RunningProtocolCommandList.tsx index f20cafd8875..9bfe2ca73fc 100644 --- a/app/src/organisms/OnDeviceDisplay/RunningProtocol/RunningProtocolCommandList.tsx +++ b/app/src/organisms/OnDeviceDisplay/RunningProtocol/RunningProtocolCommandList.tsx @@ -18,6 +18,7 @@ import { SPACING, LegacyStyledText, TYPOGRAPHY, + StyledText, } from '@opentrons/components' import { RUN_STATUS_RUNNING, RUN_STATUS_IDLE } from '@opentrons/api-client' @@ -221,8 +222,15 @@ export function RunningProtocolCommandList({ + + {index + 1} + { - const actual = await importOriginal() + const actual = await importOriginal() return { ...actual, - useHistory: () => ({ push: mockPush } as any), + useNavigate: () => mockNavigate, } }) @@ -150,6 +150,6 @@ describe('ConfirmCancelRunModal', () => { expect(mockDismissCurrentRun).toHaveBeenCalled() expect(mockTrackProtocolRunEvent).toHaveBeenCalled() - expect(mockPush).toHaveBeenCalledWith('/protocols') + expect(mockNavigate).toHaveBeenCalledWith('/protocols') }) }) diff --git a/app/src/organisms/OnDeviceDisplay/RunningProtocol/__tests__/RunFailedModal.test.tsx b/app/src/organisms/OnDeviceDisplay/RunningProtocol/__tests__/RunFailedModal.test.tsx index 9634879ee44..b57a09cc8aa 100644 --- a/app/src/organisms/OnDeviceDisplay/RunningProtocol/__tests__/RunFailedModal.test.tsx +++ b/app/src/organisms/OnDeviceDisplay/RunningProtocol/__tests__/RunFailedModal.test.tsx @@ -9,13 +9,13 @@ import { renderWithProviders } from '../../../../__testing-utils__' import { i18n } from '../../../../i18n' import { RunFailedModal } from '../RunFailedModal' -import type { useHistory } from 'react-router-dom' +import type { NavigateFunction } from 'react-router-dom' vi.mock('@opentrons/react-api-client') const RUN_ID = 'mock_runID' const mockFn = vi.fn() -const mockPush = vi.fn() +const mockNavigate = vi.fn() const mockErrors = [ { id: 'd0245210-dfb9-4f1c-8ad0-3416b603a7ba', @@ -74,10 +74,10 @@ const mockErrors = [ const mockStopRun = vi.fn((_runId, opts) => opts.onSuccess()) vi.mock('react-router-dom', async importOriginal => { - const actual = await importOriginal() + const actual = await importOriginal() return { ...actual, - useHistory: () => ({ push: mockPush } as any), + useNavigate: () => mockNavigate, } }) @@ -123,6 +123,6 @@ describe('RunFailedModal', () => { const button = screen.getByText('Close') fireEvent.click(button) expect(mockStopRun).toHaveBeenCalled() - expect(mockPush).toHaveBeenCalledWith('/dashboard') + expect(mockNavigate).toHaveBeenCalledWith('/dashboard') }) }) diff --git a/app/src/organisms/OnDeviceDisplay/RunningProtocol/__tests__/RunningProtocolCommandList.test.tsx b/app/src/organisms/OnDeviceDisplay/RunningProtocol/__tests__/RunningProtocolCommandList.test.tsx index 225ded3adff..eb21f242817 100644 --- a/app/src/organisms/OnDeviceDisplay/RunningProtocol/__tests__/RunningProtocolCommandList.test.tsx +++ b/app/src/organisms/OnDeviceDisplay/RunningProtocol/__tests__/RunningProtocolCommandList.test.tsx @@ -63,6 +63,11 @@ describe('RunningProtocolCommandList', () => { expect(mockShowModal).toHaveBeenCalled() }) + it("it displays the run's current action number", () => { + render({ ...props, currentRunCommandIndex: 11 }) + screen.getByText(12) + }) + // ToDo (kj:04/10/2023) once we fix the track event stuff, we can implement tests it.todo('when tapping play button, track event mock function is called') }) diff --git a/app/src/organisms/ProtocolAnalysisFailure/__tests__/ProtocolAnalysisFailure.test.tsx b/app/src/organisms/ProtocolAnalysisFailure/__tests__/ProtocolAnalysisFailure.test.tsx index a2b0d712d54..86f0feb7373 100644 --- a/app/src/organisms/ProtocolAnalysisFailure/__tests__/ProtocolAnalysisFailure.test.tsx +++ b/app/src/organisms/ProtocolAnalysisFailure/__tests__/ProtocolAnalysisFailure.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { StaticRouter } from 'react-router-dom' +import { MemoryRouter } from 'react-router-dom' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect } from 'vitest' @@ -13,7 +13,7 @@ const render = ( props: Partial> = {} ) => { return renderWithProviders( - + - , + , { i18nInstance: i18n, } diff --git a/app/src/organisms/ProtocolDetails/__tests__/ProtocolDetails.test.tsx b/app/src/organisms/ProtocolDetails/__tests__/ProtocolDetails.test.tsx index 130c3ceedbd..140cf3f6373 100644 --- a/app/src/organisms/ProtocolDetails/__tests__/ProtocolDetails.test.tsx +++ b/app/src/organisms/ProtocolDetails/__tests__/ProtocolDetails.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { act, screen, waitFor } from '@testing-library/react' -import { StaticRouter } from 'react-router-dom' +import { MemoryRouter } from 'react-router-dom' import { describe, it, beforeEach, vi, expect, afterEach } from 'vitest' import { renderWithProviders } from '../../../__testing-utils__' @@ -40,9 +40,9 @@ const render = ( props: Partial> = {} ) => { return renderWithProviders( - + - , + , { i18nInstance: i18n, } diff --git a/app/src/organisms/ProtocolSetupLabware/__tests__/LabwareMapViewModal.test.tsx b/app/src/organisms/ProtocolSetupLabware/__tests__/LabwareMapViewModal.test.tsx index 8a76e5703fe..04b4df83d09 100644 --- a/app/src/organisms/ProtocolSetupLabware/__tests__/LabwareMapViewModal.test.tsx +++ b/app/src/organisms/ProtocolSetupLabware/__tests__/LabwareMapViewModal.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { StaticRouter } from 'react-router-dom' +import { MemoryRouter } from 'react-router-dom' import { when } from 'vitest-when' import { fireEvent, screen } from '@testing-library/react' import { describe, it, vi, beforeEach, afterEach, expect } from 'vitest' @@ -53,9 +53,9 @@ vi.mock('@opentrons/components', async importOriginal => { const render = (props: React.ComponentProps) => { return renderWithProviders( - + - , + , { i18nInstance: i18n, } diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/index.tsx b/app/src/organisms/ProtocolSetupModulesAndDeck/index.tsx index 6e4986ae0bf..349faeff2b6 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/index.tsx +++ b/app/src/organisms/ProtocolSetupModulesAndDeck/index.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' - +import { useNavigate } from 'react-router-dom' import { COLORS, DIRECTION_COLUMN, @@ -14,6 +14,7 @@ import { FLEX_ROBOT_TYPE, getDeckDefFromRobotType, } from '@opentrons/shared-data' +import { RUN_STATUS_STOPPED } from '@opentrons/api-client' import { getTopPortalEl } from '../../App/portal' import { FloatingActionButton } from '../../atoms/buttons' @@ -22,6 +23,7 @@ import { ChildNavigation } from '../../organisms/ChildNavigation' import { useAttachedModules } from '../../organisms/Devices/hooks' import { getProtocolModulesInfo } from '../../organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo' import { useMostRecentCompletedAnalysis } from '../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' +import { useRunStatus } from '../RunTimeControl/hooks' import { getAttachedProtocolModuleMatches, getUnmatchedModulesForProtocol, @@ -34,9 +36,6 @@ import { useNotifyDeckConfigurationQuery } from '../../resources/deck_configurat import type { CutoutId, CutoutFixtureId } from '@opentrons/shared-data' import type { SetupScreens } from '../../pages/ProtocolSetup' -import { useRunStatus } from '../RunTimeControl/hooks' -import { RUN_STATUS_STOPPED } from '@opentrons/api-client' -import { useHistory } from 'react-router-dom' const ATTACHED_MODULE_POLL_MS = 5000 const DECK_CONFIG_POLL_MS = 5000 @@ -58,13 +57,13 @@ export function ProtocolSetupModulesAndDeck({ setProvidedFixtureOptions, }: ProtocolSetupModulesAndDeckProps): JSX.Element { const { i18n, t } = useTranslation('protocol_setup') - const history = useHistory() + const navigate = useNavigate() const runStatus = useRunStatus(runId) React.useEffect(() => { if (runStatus === RUN_STATUS_STOPPED) { - history.push('/protocols') + navigate('/protocols') } - }, [runStatus, history]) + }, [runStatus, navigate]) const [ showSetupInstructionsModal, setShowSetupInstructionsModal, diff --git a/app/src/organisms/ProtocolSetupParameters/AnalysisFailedModal.tsx b/app/src/organisms/ProtocolSetupParameters/AnalysisFailedModal.tsx index 82195197903..3fba85759a6 100644 --- a/app/src/organisms/ProtocolSetupParameters/AnalysisFailedModal.tsx +++ b/app/src/organisms/ProtocolSetupParameters/AnalysisFailedModal.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { BORDERS, COLORS, @@ -27,7 +27,7 @@ export function AnalysisFailedModal({ setShowAnalysisFailedModal, }: AnalysisFailedModalProps): JSX.Element { const { t } = useTranslation('protocol_setup') - const history = useHistory() + const navigate = useNavigate() const modalHeader: ModalHeaderBaseProps = { title: t('protocol_analysis_failed'), iconName: 'information', @@ -36,7 +36,7 @@ export function AnalysisFailedModal({ } const handleRestartSetup = (): void => { - history.push(protocolId != null ? `/protocols/${protocolId}` : '/protocols') + navigate(protocolId != null ? `/protocols/${protocolId}` : '/protocols') } return ( diff --git a/app/src/organisms/ProtocolSetupParameters/ChooseCsvFile.tsx b/app/src/organisms/ProtocolSetupParameters/ChooseCsvFile.tsx index 23c0a7352a3..5c0d5202c28 100644 --- a/app/src/organisms/ProtocolSetupParameters/ChooseCsvFile.tsx +++ b/app/src/organisms/ProtocolSetupParameters/ChooseCsvFile.tsx @@ -2,6 +2,7 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' import { css } from 'styled-components' +import isEqual from 'lodash/isEqual' import last from 'lodash/last' import { @@ -20,17 +21,20 @@ import { getShellUpdateDataFiles } from '../../redux/shell' import { ChildNavigation } from '../ChildNavigation' import { EmptyFile } from './EmptyFile' -import type { CsvFileParameter } from '@opentrons/shared-data' +import type { + CsvFileParameter, + CsvFileParameterFileData, +} from '@opentrons/shared-data' import type { CsvFileData } from '@opentrons/api-client' interface ChooseCsvFileProps { protocolId: string handleGoBack: () => void - // ToDo (kk:06/18/2024) null will be removed when implemented required part - parameter: CsvFileParameter | null - setParameter: (value: boolean | string | number, variableName: string) => void - csvFileInfo: string - setCsvFileInfo: (fileInfo: string) => void + parameter: CsvFileParameter + setParameter: ( + value: boolean | string | number | CsvFileParameterFileData, + variableName: string + ) => void } export function ChooseCsvFile({ @@ -38,31 +42,41 @@ export function ChooseCsvFile({ handleGoBack, parameter, setParameter, - csvFileInfo, - setCsvFileInfo, }: ChooseCsvFileProps): JSX.Element { const { t } = useTranslation('protocol_setup') + const csvFilesOnUSB = useSelector(getShellUpdateDataFiles) ?? [] const csvFilesOnRobot = useAllCsvFilesQuery(protocolId).data?.data.files ?? [] - // ToDo (06/20/2024) this will removed when working on AUTH-521 - // const handleOnChange = (newValue: string | number | boolean): void => { - // setParameter(newValue, parameter?.variableName ?? 'csvFileId') - // } + const initialFileObject: CsvFileParameterFileData = parameter.file ?? {} + const [ + csvFileSelected, + setCsvFileSelected, + ] = React.useState(initialFileObject) - const handleConfirmSelection = (): void => { - // ToDo (kk:06/18/2024) wire up later + const handleBackButton = (): void => { + if (!isEqual(csvFileSelected, initialFileObject)) { + setParameter(csvFileSelected, parameter.variableName) + } + handleGoBack() } + React.useEffect(() => { + if (csvFilesOnUSB.length === 0) { + setCsvFileSelected({}) + } + }, [csvFilesOnUSB]) + return ( <> {}} + buttonValue={`${csv.id}`} + onChange={() => { + setCsvFileSelected({ id: csv.id, fileName: csv.name }) + }} + isSelected={csvFileSelected?.id === csv.id} /> )) ) : ( @@ -99,23 +116,27 @@ export function ChooseCsvFile({ {csvFilesOnUSB.length !== 0 ? ( - csvFilesOnUSB.map(csv => ( - <> - {csv.length !== 0 && last(csv.split('/')) !== undefined ? ( - { - // ToDO this will be implemented AUTH-521 - // handleOnChange(option.value) - setCsvFileInfo(csv) - }} - /> - ) : null} - - )) + csvFilesOnUSB.map(csvFilePath => { + const fileName = last(csvFilePath.split('/')) + return ( + + {csvFilePath.length !== 0 && fileName !== undefined ? ( + { + setCsvFileSelected({ + filePath: csvFilePath, + fileName: fileName, + }) + }} + isSelected={csvFileSelected?.filePath === csvFilePath} + /> + ) : null} + + ) + }) ) : ( )} diff --git a/app/src/organisms/ProtocolSetupParameters/__tests__/AnalysisFailedModal.test.tsx b/app/src/organisms/ProtocolSetupParameters/__tests__/AnalysisFailedModal.test.tsx index 143f6d92feb..2f0be95f26d 100644 --- a/app/src/organisms/ProtocolSetupParameters/__tests__/AnalysisFailedModal.test.tsx +++ b/app/src/organisms/ProtocolSetupParameters/__tests__/AnalysisFailedModal.test.tsx @@ -5,17 +5,17 @@ import { fireEvent, screen } from '@testing-library/react' import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' import { AnalysisFailedModal } from '../AnalysisFailedModal' -import type * as ReactRouterDom from 'react-router-dom' +import type { NavigateFunction } from 'react-router-dom' -const mockPush = vi.fn() +const mockNavigate = vi.fn() const PROTOCOL_ID = 'mockId' const mockSetShowAnalysisFailedModal = vi.fn() vi.mock('react-router-dom', async importOriginal => { - const reactRouterDom = await importOriginal() + const reactRouterDom = await importOriginal() return { ...reactRouterDom, - useHistory: () => ({ push: mockPush } as any), + useNavigate: () => mockNavigate, } }) @@ -58,12 +58,12 @@ describe('AnalysisFailedModal', () => { it('should call a mock function when tapping restart setup button', () => { render(props) fireEvent.click(screen.getByText('Restart setup')) - expect(mockPush).toHaveBeenCalledWith(`/protocols/${PROTOCOL_ID}`) + expect(mockNavigate).toHaveBeenCalledWith(`/protocols/${PROTOCOL_ID}`) }) it('should push to protocols dashboard when tapping restart setup button and protocol ID is null', () => { render({ ...props, protocolId: null }) fireEvent.click(screen.getByText('Restart setup')) - expect(mockPush).toHaveBeenCalledWith('/protocols') + expect(mockNavigate).toHaveBeenCalledWith('/protocols') }) }) diff --git a/app/src/organisms/ProtocolSetupParameters/__tests__/ChooseCsvFile.test.tsx b/app/src/organisms/ProtocolSetupParameters/__tests__/ChooseCsvFile.test.tsx index 05c85dfc207..918f5838084 100644 --- a/app/src/organisms/ProtocolSetupParameters/__tests__/ChooseCsvFile.test.tsx +++ b/app/src/organisms/ProtocolSetupParameters/__tests__/ChooseCsvFile.test.tsx @@ -23,7 +23,6 @@ vi.mock('../EmptyFile') const mockHandleGoBack = vi.fn() const mockSetParameter = vi.fn() const mockParameter: CsvFileParameter = {} as any -const mockSetFileInfo = vi.fn() const PROTOCOL_ID = 'fake_protocol_id' const mockUsbData = [ '/media/mock-usb-drive/mock-file1.csv', @@ -64,8 +63,6 @@ describe('ChooseCsvFile', () => { handleGoBack: mockHandleGoBack, parameter: mockParameter, setParameter: mockSetParameter, - csvFileInfo: 'mockFileId', - setCsvFileInfo: mockSetFileInfo, } vi.mocked(getLocalRobot).mockReturnValue(mockConnectedRobot) vi.mocked(EmptyFile).mockReturnValue(
    mock EmptyFile
    ) @@ -80,7 +77,7 @@ describe('ChooseCsvFile', () => { screen.getByText('Choose CSV file') screen.getByText('CSV files on robot') screen.getByText('CSV files on USB') - screen.getByText('Confirm selection') + screen.getByText('Leave USB drive attached until run starts') }) it('should render csv file names', () => { @@ -95,13 +92,71 @@ describe('ChooseCsvFile', () => { screen.getByText('mock-file3.csv') }) - it('should call a mock function when tapping back button', () => { + it('should call a mock function when tapping back button + without selecting a csv file', () => { render(props) + + fireEvent.click(screen.getAllByRole('button')[0]) + expect(props.setParameter).not.toHaveBeenCalled() + expect(mockHandleGoBack).toHaveBeenCalled() + }) + + it('should render a selected radio button in Robot side when tapped', () => { + when(useAllCsvFilesQuery) + .calledWith(PROTOCOL_ID) + .thenReturn(mockDataOnRobot as any) + render(props) + + const selectedCsvFileOnRobot = screen.getByLabelText('rtp_mock_file2.csv') + fireEvent.click(selectedCsvFileOnRobot) + expect(selectedCsvFileOnRobot).toBeChecked() + }) + + it('should render a selected radio button in USB side when tapped', () => { + render(props) + + const selectCsvFileOnUsb = screen.getByLabelText('mock-file2.csv') + fireEvent.click(selectCsvFileOnUsb) + expect(selectCsvFileOnUsb).toBeChecked() + }) + + it('call mock function (setParameter) with fileId + fileName when the selected file is a csv on Robot + tapping back button', () => { + when(useAllCsvFilesQuery) + .calledWith(PROTOCOL_ID) + .thenReturn(mockDataOnRobot as any) + render(props) + + const csvFileOnRobot = screen.getByRole('label', { + name: 'rtp_mock_file2.csv', + }) + + fireEvent.click(csvFileOnRobot) fireEvent.click(screen.getAllByRole('button')[0]) + expect(props.setParameter).toHaveBeenCalledWith( + { + id: '2', + fileName: 'rtp_mock_file2.csv', + }, + props.parameter.variableName + ) expect(mockHandleGoBack).toHaveBeenCalled() }) - it.todo('should call a mock function when tapping a csv file') + it('call mock function (setParameter) with filePath + fileName when the selected file is a csv on USB + tapping back button', () => { + render(props) + + const csvFileOnUsb = screen.getByRole('label', { name: 'mock-file1.csv' }) + + fireEvent.click(csvFileOnUsb) + fireEvent.click(screen.getAllByRole('button')[0]) + expect(props.setParameter).toHaveBeenCalledWith( + { + filePath: '/media/mock-usb-drive/mock-file1.csv', + fileName: 'mock-file1.csv', + }, + props.parameter.variableName + ) + expect(mockHandleGoBack).toHaveBeenCalled() + }) it('should render mock empty file component when there is no csv file', () => { vi.mocked(getShellUpdateDataFiles).mockReturnValue([]) diff --git a/app/src/organisms/ProtocolSetupParameters/__tests__/ProtocolSetupParameters.test.tsx b/app/src/organisms/ProtocolSetupParameters/__tests__/ProtocolSetupParameters.test.tsx index 8a660fcf818..a80d8565748 100644 --- a/app/src/organisms/ProtocolSetupParameters/__tests__/ProtocolSetupParameters.test.tsx +++ b/app/src/organisms/ProtocolSetupParameters/__tests__/ProtocolSetupParameters.test.tsx @@ -6,6 +6,7 @@ import { useCreateProtocolAnalysisMutation, useCreateRunMutation, useHost, + useUploadCsvFileMutation, } from '@opentrons/react-api-client' import { COLORS } from '@opentrons/components' @@ -19,11 +20,11 @@ import { useToaster } from '../../ToasterOven' import { useFeatureFlag } from '../../../redux/config' import { ProtocolSetupParameters } from '..' -import type * as ReactRouterDom from 'react-router-dom' +import type { NavigateFunction } from 'react-router-dom' import type { HostConfig } from '@opentrons/api-client' import type { CompletedProtocolAnalysis } from '@opentrons/shared-data' -const mockGoBack = vi.fn() +const mockNavigate = vi.fn() vi.mock('../ChooseEnum') vi.mock('../ChooseNumber') @@ -33,16 +34,17 @@ vi.mock('../../ToasterOven') vi.mock('@opentrons/react-api-client') vi.mock('../../LabwarePositionCheck/useMostRecentCompletedAnalysis') vi.mock('react-router-dom', async importOriginal => { - const reactRouterDom = await importOriginal() + const reactRouterDom = await importOriginal() return { ...reactRouterDom, - useHistory: () => ({ goBack: mockGoBack } as any), + useNavigate: () => mockNavigate, } }) vi.mock('../../../redux/config') const MOCK_HOST_CONFIG: HostConfig = { hostname: 'MOCK_HOST' } const mockCreateProtocolAnalysis = vi.fn() +const mockUploadCsvFile = vi.fn() const mockCreateRun = vi.fn() const mockMostRecentAnalysis = ({ commands: [], @@ -79,6 +81,9 @@ describe('ProtocolSetupParameters', () => { when(vi.mocked(useCreateRunMutation)) .calledWith(expect.anything()) .thenReturn({ createRun: mockCreateRun } as any) + when(vi.mocked(useUploadCsvFileMutation)) + .calledWith(expect.anything(), expect.anything()) + .thenReturn({ uploadCsvFile: mockUploadCsvFile } as any) when(vi.mocked(useFeatureFlag)) .calledWith('enableCsvFile') .thenReturn(false) @@ -136,17 +141,16 @@ describe('ProtocolSetupParameters', () => { screen.getByText('EtoH Volume') }) - // ToDo (kk:06/18/2024) comment-out will be removed in a following PR. - // it('renders the other setting when csv param', () => { - // vi.mocked(useFeatureFlag).mockReturnValue(true) - // render(props) - // screen.getByText('CSV File') - // }) + it('renders the other setting when csv param', () => { + vi.mocked(useFeatureFlag).mockReturnValue(true) + render(props) + screen.getByText('CSV File') + }) - it('renders the back icon and calls useHistory', () => { + it('renders the back icon and calls useNavigate', () => { render(props) fireEvent.click(screen.getAllByRole('button')[0]) - expect(mockGoBack).toHaveBeenCalled() + expect(mockNavigate).toHaveBeenCalled() }) it('renders the confirm values button and clicking on it creates a run', () => { diff --git a/app/src/organisms/ProtocolSetupParameters/index.tsx b/app/src/organisms/ProtocolSetupParameters/index.tsx index 239406650a6..66cad283b6c 100644 --- a/app/src/organisms/ProtocolSetupParameters/index.tsx +++ b/app/src/organisms/ProtocolSetupParameters/index.tsx @@ -1,10 +1,11 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { useCreateProtocolAnalysisMutation, useCreateRunMutation, useHost, + useUploadCsvFileMutation, } from '@opentrons/react-api-client' import { useQueryClient } from 'react-query' import { @@ -18,8 +19,10 @@ import { sortRuntimeParameters, } from '@opentrons/shared-data' -import { ProtocolSetupStep } from '../../pages/ProtocolSetup' -import { getRunTimeParameterValuesForRun } from '../Devices/utils' +import { + getRunTimeParameterFilesForRun, + getRunTimeParameterValuesForRun, +} from '../Devices/utils' import { ChildNavigation } from '../ChildNavigation' import { ResetValuesModal } from './ResetValuesModal' import { ChooseEnum } from './ChooseEnum' @@ -27,7 +30,7 @@ import { ChooseNumber } from './ChooseNumber' import { ChooseCsvFile } from './ChooseCsvFile' import { useFeatureFlag } from '../../redux/config' import { useToaster } from '../ToasterOven' - +import { ProtocolSetupStep } from '../../pages/ProtocolSetup' import type { CompletedProtocolAnalysis, ChoiceParameter, @@ -35,8 +38,10 @@ import type { NumberParameter, RunTimeParameter, ValueRunTimeParameter, + CsvFileParameterFileData, } from '@opentrons/shared-data' -import type { LabwareOffsetCreateData } from '@opentrons/api-client' +import type { ProtocolSetupStepStatus } from '../../pages/ProtocolSetup' +import type { FileData, LabwareOffsetCreateData } from '@opentrons/api-client' interface ProtocolSetupParametersProps { protocolId: string @@ -53,7 +58,7 @@ export function ProtocolSetupParameters({ }: ProtocolSetupParametersProps): JSX.Element { const { t } = useTranslation('protocol_setup') const enableCsvFile = useFeatureFlag('enableCsvFile') - const history = useHistory() + const navigate = useNavigate() const host = useHost() const queryClient = useQueryClient() const [ @@ -84,21 +89,35 @@ export function ProtocolSetupParameters({ ({ ...parameter, value: parameter.default } as ValueRunTimeParameter) ) ) - const { makeSnackbar } = useToaster() - const csvFileParameter = runTimeParameters.find( - (param): param is CsvFileParameter => param.type === 'csv_file' - ) - const initialFileId: string = csvFileParameter?.file?.id ?? '' - const [csvFileInfo, setCSVFileInfo] = React.useState(initialFileId) + const hasMissingFileParam = + runTimeParametersOverrides?.some((parameter): boolean => { + if (parameter.type !== 'csv_file') { + return false + } + + if (parameter.file == null) { + return true + } + + return ( + parameter.file.id == null && + parameter.file.file == null && + parameter.file.filePath == null + ) + }) ?? false + + const { makeSnackbar } = useToaster() const updateParameters = ( - value: boolean | string | number, + value: boolean | string | number | CsvFileParameterFileData, variableName: string ): void => { const updatedParameters = runTimeParametersOverrides.map(parameter => { if (parameter.variableName === variableName) { - return { ...parameter, value } + return parameter.type === 'csv_file' + ? { ...parameter, file: value } + : { ...parameter, value } } return parameter }) @@ -143,6 +162,8 @@ export function ProtocolSetupParameters({ host ) + const { uploadCsvFile } = useUploadCsvFileMutation({}, host) + const { createRun, isLoading } = useCreateRunMutation({ onSuccess: data => { queryClient.invalidateQueries([host, 'runs']).catch((e: Error) => { @@ -151,11 +172,61 @@ export function ProtocolSetupParameters({ }, }) const handleConfirmValues = (): void => { - if ( - enableCsvFile && - mostRecentAnalysis?.result === 'parameter-value-required' - ) { - makeSnackbar(t('protocol_requires_csv') as string) + if (enableCsvFile) { + if (hasMissingFileParam) { + makeSnackbar(t('protocol_requires_csv') as string) + } else { + const dataFilesForProtocolMap = runTimeParametersOverrides.reduce< + Record + >((acc, parameter) => { + // create {variableName: FileData} map for sending to /dataFiles endpoint + if ( + parameter.type === 'csv_file' && + parameter.file?.id == null && + parameter.file?.file != null + ) { + return { [parameter.variableName]: parameter.file.file } + } else if ( + parameter.type === 'csv_file' && + parameter.file?.id == null && + parameter.file?.filePath != null + ) { + return { [parameter.variableName]: parameter.file.filePath } + } + return acc + }, {}) + void Promise.all( + Object.entries(dataFilesForProtocolMap).map(([key, fileData]) => { + const fileResponse = uploadCsvFile(fileData) + const varName = Promise.resolve(key) + return Promise.all([fileResponse, varName]) + }) + ).then(responseTuples => { + const mappedResolvedCsvVariableToFileId = responseTuples.reduce< + Record + >((acc, [uploadedFileResponse, variableName]) => { + return { ...acc, [variableName]: uploadedFileResponse.data.id } + }, {}) + const runTimeParameterValues = getRunTimeParameterValuesForRun( + runTimeParametersOverrides + ) + const runTimeParameterFiles = getRunTimeParameterFilesForRun( + runTimeParametersOverrides, + mappedResolvedCsvVariableToFileId + ) + createProtocolAnalysis({ + protocolKey: protocolId, + runTimeParameterValues, + runTimeParameterFiles, + }) + createRun({ + protocolId, + labwareOffsets, + runTimeParameterValues, + runTimeParameterFiles, + }) + }) + } } else { setStartSetup(true) createProtocolAnalysis({ @@ -192,18 +263,12 @@ export function ProtocolSetupParameters({ { - history.goBack() + navigate(-1) }} onClickButton={handleConfirmValues} buttonText={t('confirm_values')} - ariaDisabled={ - enableCsvFile && - mostRecentAnalysis?.result === 'parameter-value-required' - } - buttonIsDisabled={ - enableCsvFile && - mostRecentAnalysis?.result === 'parameter-value-required' - } + ariaDisabled={enableCsvFile && hasMissingFileParam} + buttonIsDisabled={enableCsvFile && hasMissingFileParam} iconName={isLoading || startSetup ? 'ot-spinner' : undefined} iconPlacement="startIcon" secondaryButtonProps={{ @@ -225,26 +290,19 @@ export function ProtocolSetupParameters({ > {sortRuntimeParameters(runTimeParametersOverrides).map( (parameter, index) => { - const detailLabelForCsv = - mostRecentAnalysis?.result === 'parameter-value-required' - ? t('required') - : parameter.displayName - - let setupStatus: 'ready' | 'not ready' | 'general' | 'inform' = - 'inform' - if ( - enableCsvFile && - parameter.type === 'csv_file' && - mostRecentAnalysis?.result === 'parameter-value-required' - ) { - setupStatus = 'not ready' - } - if ( - enableCsvFile && - parameter.type === 'csv_file' && - mostRecentAnalysis?.result === 'ok' - ) { - setupStatus = 'ready' + let detail: string = '' + let setupStatus: ProtocolSetupStepStatus + if (enableCsvFile && parameter.type === 'csv_file') { + if (parameter.file?.fileName == null) { + detail = t('required') + setupStatus = 'not ready' + } else { + detail = parameter.file.fileName + setupStatus = 'ready' + } + } else { + detail = formatRunTimeParameterValue(parameter, t) + setupStatus = 'inform' } return ( @@ -260,11 +318,7 @@ export function ProtocolSetupParameters({ onClickSetupStep={() => { handleSetParameter(parameter) }} - detail={ - enableCsvFile && parameter.type === 'csv_file' - ? detailLabelForCsv - : formatRunTimeParameterValue(parameter, t) - } + detail={detail} description={ parameter.type === 'csv_file' ? null : parameter.description } @@ -289,8 +343,6 @@ export function ProtocolSetupParameters({ }} parameter={chooseCsvFileScreen} setParameter={updateParameters} - csvFileInfo={csvFileInfo} - setCsvFileInfo={setCSVFileInfo} /> ) } diff --git a/app/src/organisms/ProtocolUpload/hooks/__tests__/useCloneRun.test.tsx b/app/src/organisms/ProtocolUpload/hooks/__tests__/useCloneRun.test.tsx index 6feee78ec40..3056c99211e 100644 --- a/app/src/organisms/ProtocolUpload/hooks/__tests__/useCloneRun.test.tsx +++ b/app/src/organisms/ProtocolUpload/hooks/__tests__/useCloneRun.test.tsx @@ -113,6 +113,7 @@ describe('useCloneRun hook', () => { protocolId: 'protocolId', labwareOffsets: 'someOffset', runTimeParameterValues: {}, + runTimeParameterFiles: {}, }) }) it('should return a function that when called, calls createRun run with runTimeParameterValues overrides', async () => { @@ -129,7 +130,9 @@ describe('useCloneRun hook', () => { runTimeParameterValues: { number_param: 2, boolean_param: false, - file_param: { id: 'fileId_123' }, + }, + runTimeParameterFiles: { + file_param: 'fileId_123', }, }) }) diff --git a/app/src/organisms/ProtocolUpload/hooks/useCloneRun.ts b/app/src/organisms/ProtocolUpload/hooks/useCloneRun.ts index 08a4891aa1e..dc3b2e7902b 100644 --- a/app/src/organisms/ProtocolUpload/hooks/useCloneRun.ts +++ b/app/src/organisms/ProtocolUpload/hooks/useCloneRun.ts @@ -6,7 +6,10 @@ import { useCreateProtocolAnalysisMutation, } from '@opentrons/react-api-client' import { useNotifyRunQuery } from '../../../resources/runs' -import { getRunTimeParameterValuesForRun } from '../../Devices/utils' +import { + getRunTimeParameterValuesForRun, + getRunTimeParameterFilesForRun, +} from '../../Devices/utils' import type { Run } from '@opentrons/api-client' @@ -49,10 +52,22 @@ export function useCloneRun( const runTimeParameterValues = getRunTimeParameterValuesForRun( runTimeParameters ) + const runTimeParameterFiles = getRunTimeParameterFilesForRun( + runTimeParameters + ) if (triggerAnalysis && protocolKey != null) { - createProtocolAnalysis({ protocolKey, runTimeParameterValues }) + createProtocolAnalysis({ + protocolKey, + runTimeParameterValues, + runTimeParameterFiles, + }) } - createRun({ protocolId, labwareOffsets, runTimeParameterValues }) + createRun({ + protocolId, + labwareOffsets, + runTimeParameterValues, + runTimeParameterFiles, + }) } else { console.info('failed to clone run record, source run record not found') } diff --git a/app/src/organisms/ProtocolsLanding/ProtocolCard.tsx b/app/src/organisms/ProtocolsLanding/ProtocolCard.tsx index 874826e6abf..53411434f29 100644 --- a/app/src/organisms/ProtocolsLanding/ProtocolCard.tsx +++ b/app/src/organisms/ProtocolsLanding/ProtocolCard.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { format } from 'date-fns' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { ErrorBoundary } from 'react-error-boundary' import { @@ -60,7 +60,7 @@ interface ProtocolCardProps { storedProtocolData: StoredProtocolData } export function ProtocolCard(props: ProtocolCardProps): JSX.Element | null { - const history = useHistory() + const navigate = useNavigate() const { handleRunProtocol, handleSendProtocolToFlex, @@ -101,7 +101,7 @@ export function ProtocolCard(props: ProtocolCardProps): JSX.Element | null { padding={SPACING.spacing16} position="relative" onClick={() => { - history.push(`/protocols/${protocolKey}`) + navigate(`/protocols/${protocolKey}`) }} > diff --git a/app/src/organisms/ProtocolsLanding/ProtocolOverflowMenu.tsx b/app/src/organisms/ProtocolsLanding/ProtocolOverflowMenu.tsx index d64dd0f46dd..9412939d23f 100644 --- a/app/src/organisms/ProtocolsLanding/ProtocolOverflowMenu.tsx +++ b/app/src/organisms/ProtocolsLanding/ProtocolOverflowMenu.tsx @@ -3,7 +3,7 @@ import { css } from 'styled-components' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' import { useDispatch } from 'react-redux' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { Flex, @@ -62,7 +62,7 @@ export function ProtocolOverflowMenu( setShowOverflowMenu, } = useMenuHandleClickOutside() const dispatch = useDispatch() - const history = useHistory() + const navigate = useNavigate() const trackEvent = useTrackEvent() const { confirm: confirmDeleteProtocol, @@ -114,7 +114,7 @@ export function ProtocolOverflowMenu( } const handleClickTimeline: React.MouseEventHandler = e => { e.preventDefault() - history.push(`/protocols/${protocolKey}/timeline`) + navigate(`/protocols/${protocolKey}/timeline`) setShowOverflowMenu(prevShowOverflowMenu => !prevShowOverflowMenu) } return ( diff --git a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/AirGap.tsx b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/AirGap.tsx new file mode 100644 index 00000000000..1101cf3b9e3 --- /dev/null +++ b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/AirGap.tsx @@ -0,0 +1,213 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { createPortal } from 'react-dom' +import { + Flex, + SPACING, + DIRECTION_COLUMN, + POSITION_FIXED, + COLORS, + ALIGN_CENTER, +} from '@opentrons/components' +import { getTopPortalEl } from '../../../App/portal' +import { LargeButton } from '../../../atoms/buttons' +import { ChildNavigation } from '../../ChildNavigation' +import { InputField } from '../../../atoms/InputField' +import { ACTIONS } from '../constants' + +import type { + QuickTransferSummaryState, + QuickTransferSummaryAction, + FlowRateKind, +} from '../types' +import { i18n } from '../../../i18n' +import { NumericalKeyboard } from '../../../atoms/SoftwareKeyboard' + +interface AirGapProps { + onBack: () => void + state: QuickTransferSummaryState + dispatch: React.Dispatch + kind: FlowRateKind +} + +export function AirGap(props: AirGapProps): JSX.Element { + const { kind, onBack, state, dispatch } = props + const { t } = useTranslation('quick_transfer') + const keyboardRef = React.useRef(null) + + const [airGapEnabled, setAirGapEnabled] = React.useState( + kind === 'aspirate' + ? state.airGapAspirate != null + : state.airGapDispense != null + ) + const [currentStep, setCurrentStep] = React.useState(1) + const [volume, setVolume] = React.useState( + kind === 'aspirate' + ? state.airGapAspirate ?? null + : state.airGapDispense ?? null + ) + + const action = + kind === 'aspirate' + ? ACTIONS.SET_AIR_GAP_ASPIRATE + : ACTIONS.SET_AIR_GAP_DISPENSE + + const enableAirGapDisplayItems = [ + { + option: true, + description: t('option_enabled'), + onClick: () => { + setAirGapEnabled(true) + }, + }, + { + option: false, + description: t('option_disabled'), + onClick: () => { + setAirGapEnabled(false) + }, + }, + ] + + const handleClickBackOrExit = (): void => { + currentStep > 1 ? setCurrentStep(currentStep - 1) : onBack() + } + + const handleClickSaveOrContinue = (): void => { + if (currentStep === 1) { + if (airGapEnabled) { + setCurrentStep(currentStep + 1) + } else { + dispatch({ type: action, volume: undefined }) + onBack() + } + } else if (currentStep === 2) { + dispatch({ type: action, volume: volume ?? undefined }) + onBack() + } + } + + const setSaveOrContinueButtonText = + airGapEnabled && currentStep < 2 ? t('shared:continue') : t('shared:save') + + const maxPipetteVolume = Object.values(state.pipette.liquids)[0].maxVolume + const tipVolume = Object.values(state.tipRack.wells)[0].totalLiquidVolume + + // dispense air gap is performed whenever a tip is on its way to the trash, so + // we can have the max be at the max tip capacity + let maxAvailableCapacity = Math.min(maxPipetteVolume, tipVolume) + + // for aspirate, air gap behaves differently depending on the path + if (kind === 'aspirate') { + if (state.path === 'single') { + // for a single path, air gap capacity is just the difference between the + // pipette/tip capacity and the volume per well + maxAvailableCapacity = + Math.min(maxPipetteVolume, tipVolume) - state.volume + } else if (state.path === 'multiAspirate') { + // an aspirate air gap for multi aspirate will aspirate an air gap + // after each aspirate action, so we need to halve the available capacity for single path + // to get the amount available, assuming a min of 2 aspirates per dispense + maxAvailableCapacity = + (Math.min(maxPipetteVolume, tipVolume) - state.volume) / 2 + } else { + // aspirate air gap for multi dispense occurs once per asprirate and + // available volume is max capacity - volume*3 assuming a min of 2 dispenses + // per aspirate plus 1x the volume for disposal + maxAvailableCapacity = + Math.min(maxPipetteVolume, tipVolume) - state.volume / 3 + } + } + + const volumeRange = { min: 1, max: Math.floor(maxAvailableCapacity) } + const volumeError = + volume !== null && (volume < volumeRange.min || volume > volumeRange.max) + ? t(`value_out_of_range`, { + min: volumeRange.min, + max: volumeRange.max, + }) + : null + + let buttonIsDisabled = false + if (currentStep === 2) { + buttonIsDisabled = volume == null || volumeError != null + } + + return createPortal( + + + {currentStep === 1 ? ( + + {enableAirGapDisplayItems.map(displayItem => ( + + ))} + + ) : null} + {currentStep === 2 ? ( + + + + + + { + setVolume(Number(e)) + }} + /> + + + ) : null} + , + getTopPortalEl() + ) +} diff --git a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/BlowOut.tsx b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/BlowOut.tsx new file mode 100644 index 00000000000..a2d536459c0 --- /dev/null +++ b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/BlowOut.tsx @@ -0,0 +1,220 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { createPortal } from 'react-dom' +import { + Flex, + SPACING, + DIRECTION_COLUMN, + POSITION_FIXED, + COLORS, +} from '@opentrons/components' +import { + WASTE_CHUTE_FIXTURES, + FLEX_SINGLE_SLOT_BY_CUTOUT_ID, + TRASH_BIN_ADAPTER_FIXTURE, +} from '@opentrons/shared-data' +import { getTopPortalEl } from '../../../App/portal' +import { LargeButton } from '../../../atoms/buttons' +import { useNotifyDeckConfigurationQuery } from '../../../resources/deck_configuration' +import { ChildNavigation } from '../../ChildNavigation' +import { ACTIONS } from '../constants' + +import type { DeckConfiguration } from '@opentrons/shared-data' +import type { + QuickTransferSummaryState, + QuickTransferSummaryAction, + FlowRateKind, + BlowOutLocation, + TransferType, +} from '../types' +import { i18n } from '../../../i18n' + +interface BlowOutProps { + onBack: () => void + state: QuickTransferSummaryState + dispatch: React.Dispatch + kind: FlowRateKind +} + +export const useBlowOutLocationOptions = ( + deckConfig: DeckConfiguration, + transferType: TransferType +): Array<{ location: BlowOutLocation; description: string }> => { + const { t } = useTranslation('quick_transfer') + + const trashLocations = deckConfig.filter( + cutoutConfig => + WASTE_CHUTE_FIXTURES.includes(cutoutConfig.cutoutFixtureId) || + TRASH_BIN_ADAPTER_FIXTURE === cutoutConfig.cutoutFixtureId + ) + + // add trash bin in A3 if no trash or waste chute configured + if (trashLocations.length === 0) { + trashLocations.push({ + cutoutId: 'cutoutA3', + cutoutFixtureId: TRASH_BIN_ADAPTER_FIXTURE, + }) + } + const blowOutLocationItems: Array<{ + location: BlowOutLocation + description: string + }> = [] + if (transferType !== 'distribute') { + blowOutLocationItems.push({ + location: 'source_well', + description: t('blow_out_source_well'), + }) + } + if (transferType !== 'consolidate') { + blowOutLocationItems.push({ + location: 'dest_well', + description: t('blow_out_destination_well'), + }) + } + trashLocations.forEach(location => { + blowOutLocationItems.push({ + location, + description: + location.cutoutFixtureId === TRASH_BIN_ADAPTER_FIXTURE + ? t('trashBin_location', { + slotName: FLEX_SINGLE_SLOT_BY_CUTOUT_ID[location.cutoutId], + }) + : t('wasteChute_location', { + slotName: FLEX_SINGLE_SLOT_BY_CUTOUT_ID[location.cutoutId], + }), + }) + }) + return blowOutLocationItems +} + +export function BlowOut(props: BlowOutProps): JSX.Element { + const { onBack, state, dispatch } = props + const { t } = useTranslation('quick_transfer') + const deckConfig = useNotifyDeckConfigurationQuery().data ?? [] + + const [isBlowOutEnabled, setisBlowOutEnabled] = React.useState( + state.blowOut != null + ) + const [currentStep, setCurrentStep] = React.useState(1) + const [blowOutLocation, setBlowOutLocation] = React.useState< + BlowOutLocation | undefined + >(state.blowOut) + + const enableBlowOutDisplayItems = [ + { + value: true, + description: t('option_enabled'), + onClick: () => { + setisBlowOutEnabled(true) + }, + }, + { + value: false, + description: t('option_disabled'), + onClick: () => { + setisBlowOutEnabled(false) + }, + }, + ] + + const blowOutLocationItems = useBlowOutLocationOptions( + deckConfig, + state.transferType + ) + + const handleClickBackOrExit = (): void => { + currentStep > 1 ? setCurrentStep(currentStep - 1) : onBack() + } + + const handleClickSaveOrContinue = (): void => { + if (currentStep === 1) { + if (!isBlowOutEnabled) { + dispatch({ + type: ACTIONS.SET_BLOW_OUT, + location: undefined, + }) + onBack() + } else { + setCurrentStep(currentStep + 1) + } + } else { + dispatch({ + type: ACTIONS.SET_BLOW_OUT, + location: blowOutLocation, + }) + onBack() + } + } + + const saveOrContinueButtonText = + isBlowOutEnabled && currentStep < 2 + ? t('shared:continue') + : t('shared:save') + + let buttonIsDisabled = false + if (currentStep === 2) { + buttonIsDisabled = blowOutLocation == null + } + + return createPortal( + + + {currentStep === 1 ? ( + + {enableBlowOutDisplayItems.map(displayItem => ( + { + setisBlowOutEnabled(displayItem.value) + }} + buttonText={displayItem.description} + /> + ))} + + ) : null} + {currentStep === 2 ? ( + + {blowOutLocationItems.map(blowOutLocationItem => ( + { + setBlowOutLocation( + blowOutLocationItem.location as BlowOutLocation + ) + }} + buttonText={blowOutLocationItem.description} + /> + ))} + + ) : null} + , + getTopPortalEl() + ) +} diff --git a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/Delay.tsx b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/Delay.tsx new file mode 100644 index 00000000000..4b8414addbb --- /dev/null +++ b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/Delay.tsx @@ -0,0 +1,264 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { createPortal } from 'react-dom' +import { + Flex, + SPACING, + DIRECTION_COLUMN, + POSITION_FIXED, + COLORS, + ALIGN_CENTER, +} from '@opentrons/components' +import { getTopPortalEl } from '../../../App/portal' +import { LargeButton } from '../../../atoms/buttons' +import { ChildNavigation } from '../../ChildNavigation' +import { InputField } from '../../../atoms/InputField' +import { ACTIONS } from '../constants' + +import type { + QuickTransferSummaryState, + QuickTransferSummaryAction, + FlowRateKind, +} from '../types' +import { i18n } from '../../../i18n' +import { NumericalKeyboard } from '../../../atoms/SoftwareKeyboard' + +interface DelayProps { + onBack: () => void + state: QuickTransferSummaryState + dispatch: React.Dispatch + kind: FlowRateKind +} + +export function Delay(props: DelayProps): JSX.Element { + const { kind, onBack, state, dispatch } = props + const { t } = useTranslation('quick_transfer') + const keyboardRef = React.useRef(null) + + const [currentStep, setCurrentStep] = React.useState(1) + const [delayIsEnabled, setDelayIsEnabled] = React.useState( + kind === 'aspirate' + ? state.delayAspirate != null + : state.delayDispense != null + ) + const [delayDuration, setDelayDuration] = React.useState( + kind === 'aspirate' + ? state.delayAspirate?.delayDuration ?? null + : state.delayDispense?.delayDuration ?? null + ) + const [position, setPosition] = React.useState( + kind === 'aspirate' + ? state.delayAspirate?.positionFromBottom ?? null + : state.delayDispense?.positionFromBottom ?? null + ) + + const action = + kind === 'aspirate' + ? ACTIONS.SET_DELAY_ASPIRATE + : ACTIONS.SET_DELAY_DISPENSE + + const delayEnabledDisplayItems = [ + { + option: true, + description: t('option_enabled'), + onClick: () => { + setDelayIsEnabled(true) + }, + }, + { + option: false, + description: t('option_disabled'), + onClick: () => { + setDelayIsEnabled(false) + }, + }, + ] + + const handleClickBackOrExit = (): void => { + currentStep > 1 ? setCurrentStep(currentStep - 1) : onBack() + } + + const handleClickSaveOrContinue = (): void => { + if (currentStep === 1) { + if (!delayIsEnabled) { + dispatch({ + type: action, + delaySettings: undefined, + }) + onBack() + } else { + setCurrentStep(2) + } + } else if (currentStep === 2) { + setCurrentStep(3) + } else { + if (delayDuration != null && position != null) { + dispatch({ + type: action, + delaySettings: { + delayDuration, + positionFromBottom: position, + }, + }) + } + onBack() + } + } + + const setSaveOrContinueButtonText = + delayIsEnabled && currentStep < 3 ? t('shared:continue') : t('shared:save') + + let wellHeight = 1 + if (kind === 'aspirate') { + wellHeight = Math.max( + ...state.sourceWells.map(well => + state.source != null ? state.source.wells[well].depth : 0 + ) + ) + } else if (kind === 'dispense') { + const destLabwareDefinition = + state.destination === 'source' ? state.source : state.destination + wellHeight = Math.max( + ...state.destinationWells.map(well => + destLabwareDefinition != null + ? destLabwareDefinition.wells[well].depth + : 0 + ) + ) + } + + // the maxiumum allowed position for delay is 2x the height of the well + const positionRange = { min: 1, max: Math.floor(wellHeight * 2) } + const positionError = + position != null && + (position < positionRange.min || position > positionRange.max) + ? t(`value_out_of_range`, { + min: positionRange.min, + max: positionRange.max, + }) + : null + + let buttonIsDisabled = false + if (currentStep === 2) { + buttonIsDisabled = delayDuration == null + } else if (currentStep === 3) { + buttonIsDisabled = positionError != null || position == null + } + + return createPortal( + + + {currentStep === 1 ? ( + + {delayEnabledDisplayItems.map(displayItem => ( + + ))} + + ) : null} + {currentStep === 2 ? ( + + + + + + { + setDelayDuration(Number(e)) + }} + /> + + + ) : null} + {currentStep === 3 ? ( + + + + + + { + setPosition(Number(e)) + }} + /> + + + ) : null} + , + getTopPortalEl() + ) +} diff --git a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/FlowRate.tsx b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/FlowRate.tsx index d649dd38eb9..14d84dcd292 100644 --- a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/FlowRate.tsx +++ b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/FlowRate.tsx @@ -39,12 +39,8 @@ export function FlowRateEntry(props: FlowRateEntryProps): JSX.Element { const { i18n, t } = useTranslation(['quick_transfer', 'shared']) const keyboardRef = React.useRef(null) - const [flowRate, setFlowRate] = React.useState( - kind === 'aspirate' - ? state.aspirateFlowRate - : kind === 'dispense' - ? state.dispenseFlowRate - : null + const [flowRate, setFlowRate] = React.useState( + kind === 'aspirate' ? state.aspirateFlowRate : state.dispenseFlowRate ) // TODO (ba, 2024-07-02): use the pipette name once we add it to the v2 spec @@ -66,28 +62,27 @@ export function FlowRateEntry(props: FlowRateEntryProps): JSX.Element { LOW_VOLUME_PIPETTES.includes(pipetteName) ? liquidSpecs.lowVolumeDefault.supportedTips[tipType] : liquidSpecs.default.supportedTips[tipType] - const minFlowRate = 0.1 - const maxFlowRate = flowRatesForSupportedTip?.uiMaxFlowRate ?? 0 + const minFlowRate = 1 + const maxFlowRate = Math.floor(flowRatesForSupportedTip?.uiMaxFlowRate ?? 0) + + const flowRateAction = + kind === 'aspirate' + ? ACTIONS.SET_ASPIRATE_FLOW_RATE + : ACTIONS.SET_DISPENSE_FLOW_RATE let headerCopy: string = '' let textEntryCopy: string = '' - let flowRateAction: - | typeof ACTIONS.SET_ASPIRATE_FLOW_RATE - | typeof ACTIONS.SET_DISPENSE_FLOW_RATE - | null = null if (kind === 'aspirate') { headerCopy = t('aspirate_flow_rate') textEntryCopy = t('aspirate_flow_rate_µL') - flowRateAction = ACTIONS.SET_ASPIRATE_FLOW_RATE } else if (kind === 'dispense') { headerCopy = t('dispense_flow_rate') textEntryCopy = t('dispense_flow_rate_µL') - flowRateAction = ACTIONS.SET_DISPENSE_FLOW_RATE } const handleClickSave = (): void => { // the button will be disabled if this values is null - if (flowRate != null && flowRateAction != null) { + if (flowRate != null) { dispatch({ type: flowRateAction, rate: flowRate, @@ -97,7 +92,7 @@ export function FlowRateEntry(props: FlowRateEntryProps): JSX.Element { } const error = - flowRate !== null && (flowRate < minFlowRate || flowRate > maxFlowRate) + flowRate != null && (flowRate < minFlowRate || flowRate > maxFlowRate) ? t(`value_out_of_range`, { min: minFlowRate, max: maxFlowRate, @@ -112,7 +107,7 @@ export function FlowRateEntry(props: FlowRateEntryProps): JSX.Element { onClickBack={onBack} onClickButton={handleClickSave} top={SPACING.spacing8} - buttonIsDisabled={error != null || flowRate === null} + buttonIsDisabled={error != null || flowRate == null} /> void + state: QuickTransferSummaryState + dispatch: React.Dispatch + kind: FlowRateKind +} + +export function Mix(props: MixProps): JSX.Element { + const { kind, onBack, state, dispatch } = props + const { t } = useTranslation('quick_transfer') + const keyboardRef = React.useRef(null) + + const [mixIsEnabled, setMixIsEnabled] = React.useState( + kind === 'aspirate' + ? state.mixOnAspirate != null + : state.mixOnDispense != null + ) + const [currentStep, setCurrentStep] = React.useState(1) + const [mixVolume, setMixVolume] = React.useState( + kind === 'aspirate' + ? state.mixOnAspirate?.mixVolume ?? null + : state.mixOnDispense?.mixVolume ?? null + ) + const [mixReps, setMixReps] = React.useState( + kind === 'aspirate' + ? state.mixOnAspirate?.repititions ?? null + : state.mixOnDispense?.repititions ?? null + ) + + const mixAction = + kind === 'aspirate' + ? ACTIONS.SET_MIX_ON_ASPIRATE + : ACTIONS.SET_MIX_ON_DISPENSE + + const enableMixDisplayItems = [ + { + option: true, + description: t('option_enabled'), + onClick: () => { + setMixIsEnabled(true) + }, + }, + { + option: false, + description: t('option_disabled'), + onClick: () => { + setMixIsEnabled(false) + }, + }, + ] + + const handleClickBackOrExit = (): void => { + currentStep > 1 ? setCurrentStep(currentStep - 1) : onBack() + } + + const handleClickSaveOrContinue = (): void => { + if (currentStep === 1) { + if (!mixIsEnabled) { + dispatch({ + type: mixAction, + mixSettings: undefined, + }) + } else { + setCurrentStep(2) + } + } else if (currentStep === 2) { + setCurrentStep(3) + } else if (currentStep === 3) { + if (mixVolume != null && mixReps != null) { + dispatch({ + type: mixAction, + mixSettings: { mixVolume, repititions: mixReps }, + }) + } + onBack() + } + } + + const setSaveOrContinueButtonText = + mixIsEnabled && currentStep < 3 ? t('shared:continue') : t('shared:save') + + const maxPipetteVolume = Object.values(state.pipette.liquids)[0].maxVolume + const tipVolume = Object.values(state.tipRack.wells)[0].totalLiquidVolume + + const volumeRange = { min: 1, max: Math.min(maxPipetteVolume, tipVolume) } + const volumeError = + mixVolume != null && + (mixVolume < volumeRange.min || mixVolume > volumeRange.max) + ? t(`value_out_of_range`, { + min: volumeRange.min, + max: volumeRange.max, + }) + : null + + let buttonIsDisabled = false + if (currentStep === 2) { + buttonIsDisabled = mixVolume == null || volumeError != null + } else if (currentStep === 3) { + buttonIsDisabled = mixReps == null + } + + return createPortal( + + + {currentStep === 1 ? ( + + {enableMixDisplayItems.map(displayItem => ( + + ))} + + ) : null} + {currentStep === 2 ? ( + + + + + + { + setMixVolume(Number(e)) + }} + /> + + + ) : null} + {currentStep === 3 ? ( + + + + + + { + setMixReps(Number(e)) + }} + /> + + + ) : null} + , + getTopPortalEl() + ) +} diff --git a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/PipettePath.tsx b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/PipettePath.tsx index dd0964ca181..b93cee1a9d7 100644 --- a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/PipettePath.tsx +++ b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/PipettePath.tsx @@ -7,16 +7,25 @@ import { DIRECTION_COLUMN, POSITION_FIXED, COLORS, + ALIGN_CENTER, } from '@opentrons/components' +import { useNotifyDeckConfigurationQuery } from '../../../resources/deck_configuration' import { getTopPortalEl } from '../../../App/portal' import { LargeButton } from '../../../atoms/buttons' import { ChildNavigation } from '../../ChildNavigation' +import { useBlowOutLocationOptions } from './BlowOut' +import { getVolumeRange } from '../utils' import type { PathOption, QuickTransferSummaryState, QuickTransferSummaryAction, + BlowOutLocation, } from '../types' +import { ACTIONS } from '../constants' +import { i18n } from '../../../i18n' +import { InputField } from '../../../atoms/InputField' +import { NumericalKeyboard } from '../../../atoms/SoftwareKeyboard' interface PipettePathProps { onBack: () => void @@ -27,72 +36,195 @@ interface PipettePathProps { export function PipettePath(props: PipettePathProps): JSX.Element { const { onBack, state, dispatch } = props const { t } = useTranslation('quick_transfer') + const keyboardRef = React.useRef(null) + const deckConfig = useNotifyDeckConfigurationQuery().data ?? [] - const allowedPipettePathOptions: PathOption[] = ['single'] - if (state.sourceWells.length === 1 && state.destinationWells.length > 1) { - allowedPipettePathOptions.push('multiDispense') + const [selectedPath, setSelectedPath] = React.useState(state.path) + const [currentStep, setCurrentStep] = React.useState(1) + const [blowOutLocation, setBlowOutLocation] = React.useState< + BlowOutLocation | undefined + >(state.blowOut) + + const [disposalVolume, setDisposalVolume] = React.useState( + state.volume + ) + const volumeLimits = getVolumeRange(state) + + const allowedPipettePathOptions: Array<{ + pathOption: PathOption + description: string + }> = [{ pathOption: 'single', description: t('pipette_path_single') }] + if ( + state.transferType === 'distribute' && + volumeLimits.max >= state.volume * 3 + ) { + // we have the capacity for a multi dispense if we can fit at least 2x the volume per well + // for aspiration plus 1x the volume per well for disposal volume + allowedPipettePathOptions.push({ + pathOption: 'multiDispense', + description: t('pipette_path_multi_dispense'), + }) + // for multi aspirate we only need at least 2x the volume per well } else if ( - state.sourceWells.length > 1 && - state.destinationWells.length === 1 + state.transferType === 'consolidate' && + volumeLimits.max >= state.volume * 2 ) { - allowedPipettePathOptions.push('multiAspirate') + allowedPipettePathOptions.push({ + pathOption: 'multiAspirate', + description: t('pipette_path_multi_aspirate'), + }) } - const [ - selectedPipettePathOption, - setSelectedPipettePathOption, - ] = React.useState(state.path) - function getOptionCopy(option: PathOption): string { - switch (option) { - case 'single': - return t('pipette_path_single') - case 'multiAspirate': - return t('pipette_path_multi_aspirate') - case 'multiDispense': - return t('pipette_path_multi_dispense') - default: - return '' - } + const blowOutLocationItems = useBlowOutLocationOptions( + deckConfig, + state.transferType + ) + + const handleClickBackOrExit = (): void => { + currentStep > 1 ? setCurrentStep(currentStep - 1) : onBack() } - const handleClickSave = (): void => { - if (selectedPipettePathOption !== state.path) { + const handleClickSaveOrContinue = (): void => { + if (currentStep === 1) { + if (selectedPath !== 'multiDispense') { + dispatch({ + type: ACTIONS.SET_PIPETTE_PATH, + path: selectedPath, + }) + onBack() + } else { + setCurrentStep(2) + } + } else if (currentStep === 2) { + setCurrentStep(3) + } else { dispatch({ - type: 'SET_PIPETTE_PATH', - path: selectedPipettePathOption, + type: ACTIONS.SET_PIPETTE_PATH, + path: selectedPath as PathOption, + disposalVolume, + blowOutLocation, }) + onBack() } - onBack() } + + const saveOrContinueButtonText = + selectedPath === 'multiDispense' && currentStep < 3 + ? t('shared:continue') + : t('shared:save') + + const maxVolumeCapacity = volumeLimits.max - state.volume * 2 + const volumeRange = { min: 1, max: maxVolumeCapacity } + + const volumeError = + disposalVolume !== null && + (disposalVolume < volumeRange.min || disposalVolume > volumeRange.max) + ? t(`value_out_of_range`, { + min: volumeRange.min, + max: volumeRange.max, + }) + : null + + let buttonIsDisabled = false + if (currentStep === 2) { + buttonIsDisabled = disposalVolume == null || volumeError != null + } else if (currentStep === 3) { + buttonIsDisabled = blowOutLocation == null + } + return createPortal( - - {allowedPipettePathOptions.map(option => ( - { - setSelectedPipettePathOption(option) - }} - buttonText={getOptionCopy(option)} - /> - ))} - + {currentStep === 1 ? ( + + {allowedPipettePathOptions.map(option => ( + { + setSelectedPath(option.pathOption) + }} + buttonText={option.description} + /> + ))} + + ) : null} + {currentStep === 2 ? ( + + + + + + { + setDisposalVolume(Number(e)) + }} + /> + + + ) : null} + {currentStep === 3 ? ( + + {blowOutLocationItems.map(option => ( + { + setBlowOutLocation(option.location) + }} + buttonText={option.description} + /> + ))} + + ) : null} , getTopPortalEl() ) diff --git a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/TipPosition.tsx b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/TipPosition.tsx new file mode 100644 index 00000000000..0c365a6c336 --- /dev/null +++ b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/TipPosition.tsx @@ -0,0 +1,144 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { + Flex, + SPACING, + DIRECTION_COLUMN, + ALIGN_CENTER, + POSITION_FIXED, + COLORS, +} from '@opentrons/components' +import { getTopPortalEl } from '../../../App/portal' +import { ChildNavigation } from '../../ChildNavigation' +import { InputField } from '../../../atoms/InputField' +import { NumericalKeyboard } from '../../../atoms/SoftwareKeyboard' + +import type { + QuickTransferSummaryState, + QuickTransferSummaryAction, + FlowRateKind, +} from '../types' + +import { ACTIONS } from '../constants' +import { createPortal } from 'react-dom' + +interface TipPositionEntryProps { + onBack: () => void + state: QuickTransferSummaryState + dispatch: React.Dispatch + kind: FlowRateKind // TODO: rename flowRateKind to be generic +} + +export function TipPositionEntry(props: TipPositionEntryProps): JSX.Element { + const { onBack, state, dispatch, kind } = props + const { i18n, t } = useTranslation(['quick_transfer', 'shared']) + const keyboardRef = React.useRef(null) + + const [tipPosition, setTipPosition] = React.useState( + kind === 'aspirate' ? state.tipPositionAspirate : state.tipPositionDispense + ) + + let wellHeight = 1 + if (kind === 'aspirate') { + wellHeight = Math.max( + ...state.sourceWells.map(well => + state.source != null ? state.source.wells[well].depth : 0 + ) + ) + } else if (kind === 'dispense') { + const destLabwareDefinition = + state.destination === 'source' ? state.source : state.destination + wellHeight = Math.max( + ...state.destinationWells.map(well => + destLabwareDefinition != null + ? destLabwareDefinition.wells[well].depth + : 0 + ) + ) + } + + // the maxiumum allowed position is 2x the height of the well + const tipPositionRange = { min: 1, max: Math.floor(wellHeight * 2) } // TODO: set this based on range + + const textEntryCopy: string = t('distance_bottom_of_well_mm') + const tipPositionAction = + kind === 'aspirate' + ? ACTIONS.SET_ASPIRATE_TIP_POSITION + : ACTIONS.SET_DISPENSE_TIP_POSITION + + const handleClickSave = (): void => { + // the button will be disabled if this values is null + if (tipPosition != null) { + dispatch({ + type: tipPositionAction, + position: tipPosition, + }) + } + onBack() + } + + const error = + tipPosition != null && + (tipPosition < tipPositionRange.min || tipPosition > tipPositionRange.max) + ? t(`value_out_of_range`, { + min: Math.floor(tipPositionRange.min), + max: Math.floor(tipPositionRange.max), + }) + : null + + return createPortal( + + + + + + + + { + setTipPosition(Number(e)) + }} + /> + + + , + getTopPortalEl() + ) +} diff --git a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/TouchTip.tsx b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/TouchTip.tsx new file mode 100644 index 00000000000..5791ac2813c --- /dev/null +++ b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/TouchTip.tsx @@ -0,0 +1,209 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { createPortal } from 'react-dom' +import { + Flex, + SPACING, + DIRECTION_COLUMN, + POSITION_FIXED, + COLORS, + ALIGN_CENTER, +} from '@opentrons/components' +import { getTopPortalEl } from '../../../App/portal' +import { LargeButton } from '../../../atoms/buttons' +import { ChildNavigation } from '../../ChildNavigation' +import { InputField } from '../../../atoms/InputField' +import { ACTIONS } from '../constants' + +import type { + QuickTransferSummaryState, + QuickTransferSummaryAction, + FlowRateKind, +} from '../types' +import { i18n } from '../../../i18n' +import { NumericalKeyboard } from '../../../atoms/SoftwareKeyboard' + +interface TouchTipProps { + onBack: () => void + state: QuickTransferSummaryState + dispatch: React.Dispatch + kind: FlowRateKind +} + +export function TouchTip(props: TouchTipProps): JSX.Element { + const { kind, onBack, state, dispatch } = props + const { t } = useTranslation('quick_transfer') + const keyboardRef = React.useRef(null) + + const [touchTipIsEnabled, setTouchTipIsEnabled] = React.useState( + kind === 'aspirate' + ? state.touchTipAspirate != null + : state.touchTipDispense != null + ) + const [currentStep, setCurrentStep] = React.useState(1) + const [position, setPosition] = React.useState( + kind === 'aspirate' + ? state.touchTipAspirate ?? null + : state.touchTipDispense ?? null + ) + + const touchTipAction = + kind === 'aspirate' + ? ACTIONS.SET_TOUCH_TIP_ASPIRATE + : ACTIONS.SET_TOUCH_TIP_DISPENSE + + const enableTouchTipDisplayItems = [ + { + option: true, + description: t('option_enabled'), + onClick: () => { + setTouchTipIsEnabled(true) + }, + }, + { + option: false, + description: t('option_disabled'), + onClick: () => { + setTouchTipIsEnabled(false) + }, + }, + ] + + const handleClickBackOrExit = (): void => { + currentStep > 1 ? setCurrentStep(currentStep - 1) : onBack() + } + + const handleClickSaveOrContinue = (): void => { + if (currentStep === 1) { + if (!touchTipIsEnabled) { + dispatch({ type: touchTipAction, position: undefined }) + onBack() + } else { + setCurrentStep(2) + } + } else if (currentStep === 2) { + dispatch({ type: touchTipAction, position: position ?? undefined }) + onBack() + } + } + + const setSaveOrContinueButtonText = + touchTipIsEnabled && currentStep < 2 + ? t('shared:continue') + : t('shared:save') + + let wellHeight = 1 + if (kind === 'aspirate') { + wellHeight = Math.max( + ...state.sourceWells.map(well => + state.source !== null ? state.source.wells[well].depth : 0 + ) + ) + } else if (kind === 'dispense') { + const destLabwareDefinition = + state.destination === 'source' ? state.source : state.destination + wellHeight = Math.max( + ...state.destinationWells.map(well => + destLabwareDefinition !== null + ? destLabwareDefinition.wells[well].depth + : 0 + ) + ) + } + + // the allowed range for touch tip is half the height of the well to 1x the height + const positionRange = { min: Math.round(wellHeight / 2), max: wellHeight } + const positionError = + position !== null && + (position < positionRange.min || position > positionRange.max) + ? t(`value_out_of_range`, { + min: positionRange.min, + max: Math.floor(positionRange.max), + }) + : null + + let buttonIsDisabled = false + if (currentStep === 2) { + buttonIsDisabled = position == null || positionError != null + } + + return createPortal( + + + {currentStep === 1 ? ( + + {enableTouchTipDisplayItems.map(displayItem => ( + + ))} + + ) : null} + {currentStep === 2 ? ( + + + + + + { + setPosition(Number(e)) + }} + /> + + + ) : null} + , + getTopPortalEl() + ) +} diff --git a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/index.tsx b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/index.tsx index 609ac889b4c..5d79f2a85e5 100644 --- a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/index.tsx +++ b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/index.tsx @@ -1,11 +1,39 @@ import * as React from 'react' -import { Flex, SPACING, DIRECTION_COLUMN } from '@opentrons/components' -import { BaseSettings } from './BaseSettings' +import { useTranslation } from 'react-i18next' +import { + Flex, + SPACING, + DIRECTION_COLUMN, + JUSTIFY_SPACE_BETWEEN, + TYPOGRAPHY, + ALIGN_CENTER, + COLORS, + TEXT_ALIGN_RIGHT, + StyledText, + Icon, + SIZE_2, + TEXT_ALIGN_LEFT, +} from '@opentrons/components' import type { QuickTransferSummaryAction, QuickTransferSummaryState, } from '../types' +import { ACTIONS } from '../constants' +import { useToaster } from '../../../organisms/ToasterOven' +import { ListItem } from '../../../atoms/ListItem' +import { FlowRateEntry } from './FlowRate' +import { PipettePath } from './PipettePath' +import { TipPositionEntry } from './TipPosition' +import { Mix } from './Mix' +import { Delay } from './Delay' +import { TouchTip } from './TouchTip' +import { AirGap } from './AirGap' +import { BlowOut } from './BlowOut' +import { + TRASH_BIN_ADAPTER_FIXTURE, + WASTE_CHUTE_FIXTURES, +} from '@opentrons/shared-data' interface QuickTransferAdvancedSettingsProps { state: QuickTransferSummaryState @@ -16,14 +44,559 @@ export function QuickTransferAdvancedSettings( props: QuickTransferAdvancedSettingsProps ): JSX.Element | null { const { state, dispatch } = props + const { t, i18n } = useTranslation(['quick_transfer', 'shared']) + const [selectedSetting, setSelectedSetting] = React.useState( + null + ) + const { makeSnackbar } = useToaster() + + function getBlowoutValueCopy(): string | undefined { + if (state.blowOut === 'dest_well') { + return t('blow_out_into_destination_well') + } else if (state.blowOut === 'source_well') { + return t('blow_out_into_source_well') + } else if ( + state.blowOut != null && + state.blowOut.cutoutFixtureId === TRASH_BIN_ADAPTER_FIXTURE + ) { + return t('blow_out_into_trash_bin') + } else if ( + state.blowOut != null && + WASTE_CHUTE_FIXTURES.includes(state.blowOut.cutoutFixtureId) + ) { + return t('blow_out_into_waste_chute') + } + } + + let pipettePathValue: string = '' + if (state.path === 'single') { + pipettePathValue = t('pipette_path_single') + } else if (state.path === 'multiAspirate') { + pipettePathValue = t('pipette_path_multi_aspirate') + } else if (state.path === 'multiDispense') { + pipettePathValue = t('pipette_path_multi_dispense', { + volume: state.disposalVolume, + blowOutLocation: getBlowoutValueCopy(), + }) + } + + const destinationLabwareDef = + state.destination === 'source' ? state.source : state.destination + const sourceIsReservoir = + state.source.metadata.displayCategory === 'reservoir' + const destIsReservoir = + destinationLabwareDef.metadata.displayCategory === 'reservoir' + + const baseSettingsItems = [ + { + option: 'aspirate_flow_rate', + copy: t('aspirate_flow_rate'), + value: t('flow_rate_value', { flow_rate: state.aspirateFlowRate }), + enabled: true, + onClick: () => { + setSelectedSetting('aspirate_flow_rate') + }, + }, + { + option: 'dispense_flow_rate', + copy: t('dispense_flow_rate'), + value: t('flow_rate_value', { flow_rate: state.dispenseFlowRate }), + enabled: true, + onClick: () => { + setSelectedSetting('dispense_flow_rate') + }, + }, + { + option: 'pipette_path', + copy: t('pipette_path'), + value: pipettePathValue, + enabled: state.transferType !== 'transfer', + onClick: () => { + if (state.transferType !== 'transfer') { + setSelectedSetting('pipette_path') + } else { + makeSnackbar(t('advanced_setting_disabled') as string) + } + }, + }, + ] + + const aspirateSettingsItems = [ + { + option: 'tip_position', + copy: t('tip_position'), + value: + state.tipPositionAspirate !== null + ? t('tip_position_value', { position: state.tipPositionAspirate }) + : '', + enabled: true, + onClick: () => { + setSelectedSetting('aspirate_tip_position') + }, + }, + { + option: 'pre_wet_tip', + copy: t('pre_wet_tip'), + value: state.preWetTip ? t('option_enabled') : '', + enabled: true, + onClick: () => { + dispatch({ + type: ACTIONS.SET_PRE_WET_TIP, + preWetTip: !state.preWetTip, + }) + }, + }, + { + option: 'aspirate_mix', + copy: t('mix'), + value: + state.mixOnAspirate !== undefined + ? t('mix_value', { + volume: state.mixOnAspirate?.mixVolume, + reps: state.mixOnAspirate?.repititions, + }) + : '', + enabled: state.transferType === 'transfer', + onClick: () => { + if (state.transferType === 'transfer') { + setSelectedSetting('aspirate_mix') + } else { + makeSnackbar(t('advanced_setting_disabled') as string) + } + }, + }, + { + option: 'aspirate_delay', + copy: t('delay'), + value: + state.delayAspirate !== undefined + ? t('delay_value', { + delay: state.delayAspirate.delayDuration, + position: state.delayAspirate.positionFromBottom, + }) + : '', + enabled: true, + onClick: () => { + setSelectedSetting('aspirate_delay') + }, + }, + { + option: 'aspirate_touch_tip', + copy: t('touch_tip'), + value: + state.touchTipAspirate !== undefined + ? t('touch_tip_value', { position: state.touchTipAspirate }) + : '', + enabled: !sourceIsReservoir, + onClick: () => { + // disable for reservoir + if (!sourceIsReservoir) { + setSelectedSetting('aspirate_touch_tip') + } else { + makeSnackbar(t('advanced_setting_disabled') as string) + } + }, + }, + { + option: 'aspirate_air_gap', + copy: t('air_gap'), + value: + state.airGapAspirate !== undefined + ? t('air_gap_value', { volume: state.airGapAspirate }) + : '', + enabled: true, + onClick: () => { + setSelectedSetting('aspirate_air_gap') + }, + }, + ] + + const dispenseSettingsItems = [ + { + option: 'dispense_tip_position', + copy: t('tip_position'), + value: + state.tipPositionDispense !== undefined + ? t('tip_position_value', { position: state.tipPositionDispense }) + : '', + enabled: true, + onClick: () => { + setSelectedSetting('dispense_tip_position') + }, + }, + { + option: 'dispense_mix', + copy: t('mix'), + value: + state.mixOnDispense !== undefined + ? t('mix_value', { + volume: state.mixOnDispense?.mixVolume, + reps: state.mixOnDispense?.repititions, + }) + : '', + enabled: state.transferType === 'transfer', + onClick: () => { + if (state.transferType === 'transfer') { + setSelectedSetting('dispense_mix') + } else { + makeSnackbar(t('advanced_setting_disabled') as string) + } + }, + }, + { + option: 'dispense_delay', + copy: t('delay'), + value: + state.delayDispense !== undefined + ? t('delay_value', { + delay: state.delayDispense.delayDuration, + position: state.delayDispense.positionFromBottom, + }) + : '', + enabled: true, + onClick: () => { + setSelectedSetting('dispense_delay') + }, + }, + { + option: 'dispense_touch_tip', + copy: t('touch_tip'), + value: + state.touchTipDispense !== undefined + ? t('touch_tip_value', { position: state.touchTipDispense }) + : '', + enabled: !destIsReservoir, + onClick: () => { + if (!destIsReservoir) { + setSelectedSetting('dispense_touch_tip') + } else { + makeSnackbar(t('advanced_setting_disabled') as string) + } + }, + }, + { + option: 'dispense_air_gap', + copy: t('air_gap'), + value: + state.airGapDispense !== undefined + ? t('air_gap_value', { volume: state.airGapDispense }) + : '', + enabled: true, + onClick: () => { + setSelectedSetting('dispense_air_gap') + }, + }, + { + option: 'dispense_blow_out', + copy: t('blow_out'), + value: i18n.format(getBlowoutValueCopy(), 'capitalize'), + enabled: state.transferType !== 'distribute', + onClick: () => { + if (state.transferType === 'distribute') { + makeSnackbar(t('advanced_setting_disabled') as string) + } else { + setSelectedSetting('dispense_blow_out') + } + }, + }, + ] return ( - + {/* Base Settings */} + + {selectedSetting == null + ? baseSettingsItems.map(displayItem => ( + + + + {displayItem.copy} + + + + {displayItem.value} + + {displayItem.enabled ? ( + + ) : null} + + + + )) + : null} + {selectedSetting === 'aspirate_flow_rate' ? ( + { + setSelectedSetting(null) + }} + /> + ) : null} + {selectedSetting === 'dispense_flow_rate' ? ( + { + setSelectedSetting(null) + }} + /> + ) : null} + {selectedSetting === 'pipette_path' ? ( + { + setSelectedSetting(null) + }} + /> + ) : null} + + + {/* Aspirate Settings */} + + {selectedSetting === null ? ( + + {t('aspirate_settings')} + + ) : null} + + {selectedSetting === null + ? aspirateSettingsItems.map(displayItem => ( + + + + {displayItem.copy} + + + + {displayItem.value !== '' + ? displayItem.value + : t('option_disabled')} + + + {displayItem.option !== 'pre_wet_tip' ? ( + + ) : null} + + + + )) + : null} + {selectedSetting === 'aspirate_tip_position' ? ( + { + setSelectedSetting(null) + }} + /> + ) : null} + {selectedSetting === 'aspirate_mix' ? ( + { + setSelectedSetting(null) + }} + /> + ) : null} + {selectedSetting === 'aspirate_delay' ? ( + { + setSelectedSetting(null) + }} + /> + ) : null} + {selectedSetting === 'aspirate_touch_tip' ? ( + { + setSelectedSetting(null) + }} + /> + ) : null} + {selectedSetting === 'aspirate_air_gap' ? ( + { + setSelectedSetting(null) + }} + /> + ) : null} + + + + {/* Dispense Settings */} + + {selectedSetting === null ? ( + + {t('dispense_settings')} + + ) : null} + + {selectedSetting === null + ? dispenseSettingsItems.map(displayItem => ( + + + + {displayItem.copy} + + + + {displayItem.value !== '' + ? displayItem.value + : t('option_disabled')} + + + + + + )) + : null} + {selectedSetting === 'dispense_tip_position' ? ( + { + setSelectedSetting(null) + }} + /> + ) : null} + {selectedSetting === 'dispense_mix' ? ( + { + setSelectedSetting(null) + }} + /> + ) : null} + {selectedSetting === 'dispense_delay' ? ( + { + setSelectedSetting(null) + }} + /> + ) : null} + {selectedSetting === 'dispense_touch_tip' ? ( + { + setSelectedSetting(null) + }} + /> + ) : null} + {selectedSetting === 'dispense_air_gap' ? ( + { + setSelectedSetting(null) + }} + /> + ) : null} + {selectedSetting === 'dispense_blow_out' ? ( + { + setSelectedSetting(null) + }} + /> + ) : null} + + ) } diff --git a/app/src/organisms/QuickTransferFlow/SelectPipette.tsx b/app/src/organisms/QuickTransferFlow/SelectPipette.tsx index bafce8ced26..f0dbb87f35b 100644 --- a/app/src/organisms/QuickTransferFlow/SelectPipette.tsx +++ b/app/src/organisms/QuickTransferFlow/SelectPipette.tsx @@ -8,8 +8,9 @@ import { DIRECTION_COLUMN, } from '@opentrons/components' import { useInstrumentsQuery } from '@opentrons/react-api-client' -import { getPipetteSpecsV2, RIGHT, LEFT } from '@opentrons/shared-data' +import { RIGHT, LEFT } from '@opentrons/shared-data' import { RadioButton } from '../../atoms/buttons' +import { usePipetteSpecsV2 } from '../../resources/instruments/hooks' import { ChildNavigation } from '../ChildNavigation' import type { PipetteData, Mount } from '@opentrons/api-client' @@ -35,16 +36,12 @@ export function SelectPipette(props: SelectPipetteProps): JSX.Element { const leftPipette = attachedInstruments?.data.find( (i): i is PipetteData => i.ok && i.mount === LEFT ) - const leftPipetteSpecs = - leftPipette != null ? getPipetteSpecsV2(leftPipette.instrumentModel) : null + const leftPipetteSpecs = usePipetteSpecsV2(leftPipette?.instrumentModel) const rightPipette = attachedInstruments?.data.find( (i): i is PipetteData => i.ok && i.mount === RIGHT ) - const rightPipetteSpecs = - rightPipette != null - ? getPipetteSpecsV2(rightPipette.instrumentModel) - : null + const rightPipetteSpecs = usePipetteSpecsV2(rightPipette?.instrumentModel) // automatically select 96 channel if it is attached const [selectedPipette, setSelectedPipette] = React.useState< diff --git a/app/src/organisms/QuickTransferFlow/SummaryAndSettings.tsx b/app/src/organisms/QuickTransferFlow/SummaryAndSettings.tsx index 676e00eb4f3..44cfd13f5eb 100644 --- a/app/src/organisms/QuickTransferFlow/SummaryAndSettings.tsx +++ b/app/src/organisms/QuickTransferFlow/SummaryAndSettings.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { useQueryClient } from 'react-query' import { Flex, @@ -39,7 +39,7 @@ export function SummaryAndSettings( props: SummaryAndSettingsProps ): JSX.Element | null { const { exitButtonProps, state: wizardFlowState } = props - const history = useHistory() + const navigate = useNavigate() const queryClient = useQueryClient() const host = useHost() const { t } = useTranslation(['quick_transfer', 'shared']) @@ -76,7 +76,7 @@ export function SummaryAndSettings( queryClient.invalidateQueries([host, 'runs']).catch((e: Error) => { console.error(`error invalidating runs query: ${e.message}`) }) - history.push(`/runs/${data.data.id}/setup`) + navigate(`/runs/${data.data.id}/setup`) }, }, host @@ -92,7 +92,7 @@ export function SummaryAndSettings( files: [protocolFile], protocolKind: 'quick-transfer', }).then(() => { - history.push('/quick-transfer') + navigate('/quick-transfer') }) } diff --git a/app/src/organisms/QuickTransferFlow/__tests__/SelectPipette.test.tsx b/app/src/organisms/QuickTransferFlow/__tests__/SelectPipette.test.tsx index 2d6faa6ffa7..0f40c6f29b0 100644 --- a/app/src/organisms/QuickTransferFlow/__tests__/SelectPipette.test.tsx +++ b/app/src/organisms/QuickTransferFlow/__tests__/SelectPipette.test.tsx @@ -5,9 +5,12 @@ import { useInstrumentsQuery } from '@opentrons/react-api-client' import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' +import { useIsOEMMode } from '../../../resources/robot-settings/hooks' import { SelectPipette } from '../SelectPipette' vi.mock('@opentrons/react-api-client') +vi.mock('../../../resources/robot-settings/hooks') + const render = (props: React.ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, @@ -53,6 +56,7 @@ describe('SelectPipette', () => { ], }, } as any) + vi.mocked(useIsOEMMode).mockReturnValue(false) }) afterEach(() => { vi.resetAllMocks() diff --git a/app/src/organisms/QuickTransferFlow/__tests__/SummaryAndSettings.test.tsx b/app/src/organisms/QuickTransferFlow/__tests__/SummaryAndSettings.test.tsx index 3240f3af7f9..a6cfc429cb1 100644 --- a/app/src/organisms/QuickTransferFlow/__tests__/SummaryAndSettings.test.tsx +++ b/app/src/organisms/QuickTransferFlow/__tests__/SummaryAndSettings.test.tsx @@ -12,15 +12,15 @@ import { i18n } from '../../../i18n' import { SummaryAndSettings } from '../SummaryAndSettings' import { NameQuickTransfer } from '../NameQuickTransfer' import { Overview } from '../Overview' -import type * as ReactRouterDom from 'react-router-dom' +import type { NavigateFunction } from 'react-router-dom' -const mockPush = vi.fn() +const mockNavigate = vi.fn() vi.mock('react-router-dom', async importOriginal => { - const reactRouterDom = await importOriginal() + const reactRouterDom = await importOriginal() return { ...reactRouterDom, - useHistory: () => ({ push: mockPush } as any), + useNavigate: () => mockNavigate, } }) vi.mock('../Overview') @@ -76,7 +76,7 @@ describe('SummaryAndSettings', () => { mutateAsync: createProtocol, } as any) vi.mocked(useCreateRunMutation).mockReturnValue({ - createRun: createRun, + createRun, } as any) vi.mocked(createQuickTransferFile).mockReturnValue('' as any) createProtocol.mockResolvedValue({ diff --git a/app/src/organisms/QuickTransferFlow/__tests__/utils/getInitialSummaryState.test.ts b/app/src/organisms/QuickTransferFlow/__tests__/utils/getInitialSummaryState.test.ts index f5eac572400..4937af941d5 100644 --- a/app/src/organisms/QuickTransferFlow/__tests__/utils/getInitialSummaryState.test.ts +++ b/app/src/organisms/QuickTransferFlow/__tests__/utils/getInitialSummaryState.test.ts @@ -83,7 +83,7 @@ describe('getInitialSummaryState', () => { transferType: 'consolidate', aspirateFlowRate: 50, dispenseFlowRate: 75, - path: 'multiDispense', + path: 'multiAspirate', tipPositionAspirate: 1, preWetTip: false, tipPositionDispense: 1, @@ -133,7 +133,7 @@ describe('getInitialSummaryState', () => { transferType: 'distribute', aspirateFlowRate: 50, dispenseFlowRate: 75, - path: 'multiAspirate', + path: 'multiDispense', tipPositionAspirate: 1, preWetTip: false, tipPositionDispense: 1, @@ -142,6 +142,8 @@ describe('getInitialSummaryState', () => { cutoutId: 'cutoutA3', cutoutFixtureId: 'trashBinAdapter', }, + disposalVolume: props.state.volume, + blowOut: { cutoutId: 'cutoutA3', cutoutFixtureId: 'trashBinAdapter' }, }) }) it('generates the summary state with correct default value for 1 to n transfer with too high of volume for multiDispense', () => { diff --git a/app/src/organisms/QuickTransferFlow/index.tsx b/app/src/organisms/QuickTransferFlow/index.tsx index 9749d2cecb1..6b589ca0b45 100644 --- a/app/src/organisms/QuickTransferFlow/index.tsx +++ b/app/src/organisms/QuickTransferFlow/index.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { useConditionalConfirm, @@ -25,7 +25,7 @@ const QUICK_TRANSFER_WIZARD_STEPS = 8 const initialQuickTransferState: QuickTransferWizardState = {} export const QuickTransferFlow = (): JSX.Element => { - const history = useHistory() + const navigate = useNavigate() const { i18n, t } = useTranslation(['quick_transfer', 'shared']) const [state, dispatch] = React.useReducer( quickTransferWizardReducer, @@ -38,7 +38,7 @@ export const QuickTransferFlow = (): JSX.Element => { showConfirmation: showConfirmExit, cancel: cancelExit, } = useConditionalConfirm(() => { - history.push('/quick-transfer') + navigate('/quick-transfer') }, true) const exitButtonProps: React.ComponentProps = { diff --git a/app/src/organisms/QuickTransferFlow/reducers.ts b/app/src/organisms/QuickTransferFlow/reducers.ts index 368081f4362..81e2f56ee9b 100644 --- a/app/src/organisms/QuickTransferFlow/reducers.ts +++ b/app/src/organisms/QuickTransferFlow/reducers.ts @@ -58,12 +58,12 @@ export function quickTransferWizardReducer( state.sourceWells != null && state.sourceWells.length > action.wells.length ) { - transferType = DISTRIBUTE + transferType = CONSOLIDATE } else if ( state.sourceWells != null && state.sourceWells.length < action.wells.length ) { - transferType = CONSOLIDATE + transferType = DISTRIBUTE } return { pipette: state.pipette, @@ -73,7 +73,7 @@ export function quickTransferWizardReducer( sourceWells: state.sourceWells, destination: state.destination, destinationWells: action.wells, - transferType: transferType, + transferType, } } case 'SET_VOLUME': { @@ -110,9 +110,19 @@ export function quickTransferSummaryReducer( } } case 'SET_PIPETTE_PATH': { - return { - ...state, - path: action.path, + if (action.path === 'multiDispense') { + return { + ...state, + path: action.path, + disposalVolume: action.disposalVolume, + blowOut: action.blowOutLocation, + } + } else { + return { + ...state, + path: action.path, + disposalVolume: undefined, + } } } case 'SET_ASPIRATE_TIP_POSITION': { diff --git a/app/src/organisms/QuickTransferFlow/types.ts b/app/src/organisms/QuickTransferFlow/types.ts index f859d699dac..d4e83558a21 100644 --- a/app/src/organisms/QuickTransferFlow/types.ts +++ b/app/src/organisms/QuickTransferFlow/types.ts @@ -25,6 +25,7 @@ export type ChangeTipOptions = | 'perDest' | 'perSource' export type FlowRateKind = 'aspirate' | 'dispense' | 'blowout' +export type BlowOutLocation = 'source_well' | 'dest_well' | CutoutConfig export interface QuickTransferSummaryState { pipette: PipetteV2Specs @@ -61,7 +62,8 @@ export interface QuickTransferSummaryState { positionFromBottom: number } touchTipDispense?: number - blowOut?: string // trashBin or wasteChute or 'SOURCE_WELL' or 'DEST_WELL' + disposalVolume?: number + blowOut?: BlowOutLocation airGapDispense?: number changeTip: ChangeTipOptions dropTipLocation: CutoutConfig @@ -111,6 +113,8 @@ interface SetDispenseFlowRateAction { interface SetPipettePath { type: typeof ACTIONS.SET_PIPETTE_PATH path: PathOption + disposalVolume?: number + blowOutLocation?: BlowOutLocation } interface SetAspirateTipPosition { type: typeof ACTIONS.SET_ASPIRATE_TIP_POSITION @@ -160,7 +164,7 @@ interface SetTouchTipDispense { } interface SetBlowOut { type: typeof ACTIONS.SET_BLOW_OUT - location?: 'source_well' | 'dest_well' | 'trashBin' | 'wasteChute' + location?: BlowOutLocation } interface SetAirGapDispense { type: typeof ACTIONS.SET_AIR_GAP_DISPENSE diff --git a/app/src/organisms/QuickTransferFlow/utils/createQuickTransferFile.ts b/app/src/organisms/QuickTransferFlow/utils/createQuickTransferFile.ts index 5bed7582291..a4114c0a4d4 100644 --- a/app/src/organisms/QuickTransferFlow/utils/createQuickTransferFile.ts +++ b/app/src/organisms/QuickTransferFlow/utils/createQuickTransferFile.ts @@ -218,7 +218,7 @@ export function createQuickTransferFile( const labwareDefinitions = Object.values( invariantContext.labwareEntities - ).reduce<{ [x: string]: LabwareDefinition2 }>((acc, entity) => { + ).reduce>((acc, entity) => { return { ...acc, [entity.labwareDefURI]: entity.def } }, {}) diff --git a/app/src/organisms/QuickTransferFlow/utils/generateQuickTransferArgs.ts b/app/src/organisms/QuickTransferFlow/utils/generateQuickTransferArgs.ts index af8807261e3..4f571665b10 100644 --- a/app/src/organisms/QuickTransferFlow/utils/generateQuickTransferArgs.ts +++ b/app/src/organisms/QuickTransferFlow/utils/generateQuickTransferArgs.ts @@ -14,13 +14,14 @@ import { DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP, DEFAULT_MM_TOUCH_TIP_OFFSET_FROM_TOP, } from '../constants' -import type { QuickTransferSummaryState } from '../types' import type { + CutoutConfig, LabwareDefinition2, DeckConfiguration, PipetteName, NozzleConfigurationStyle, } from '@opentrons/shared-data' +import type { QuickTransferSummaryState } from '../types' import type { ConsolidateArgs, DistributeArgs, @@ -46,8 +47,7 @@ function getOrderedWells( } function getInvariantContextAndRobotState( - quickTransferState: QuickTransferSummaryState, - deckConfig: DeckConfiguration + quickTransferState: QuickTransferSummaryState ): { invariantContext: InvariantContext; robotState: RobotState } { const tipRackDefURI = getLabwareDefURI(quickTransferState.tipRack) let pipetteName = quickTransferState.pipette.model @@ -114,7 +114,7 @@ function getInvariantContextAndRobotState( labwareLocations = { ...labwareLocations, [tipRackId]: { - slot: adapterId != null ? adapterId : 'B2', + slot: adapterId ?? 'B2', }, [sourceLabwareId]: { slot: 'C2', @@ -140,7 +140,7 @@ function getInvariantContextAndRobotState( } } let additionalEquipmentEntities: AdditionalEquipmentEntities = {} - // TODO add check for blowout location here + if ( quickTransferState.dropTipLocation.cutoutFixtureId === TRASH_BIN_ADAPTER_FIXTURE @@ -155,7 +155,29 @@ function getInvariantContextAndRobotState( }, } } - // TODO add check for blowout location here + if ( + quickTransferState.blowOut != null && + quickTransferState.blowOut !== 'source_well' && + quickTransferState.blowOut !== 'dest_well' && + quickTransferState.blowOut?.cutoutFixtureId === TRASH_BIN_ADAPTER_FIXTURE + ) { + const trashLocation = quickTransferState.blowOut.cutoutId + const isSameTrash = Object.values(additionalEquipmentEntities).some( + entity => entity.location === trashLocation + ) + if (!isSameTrash) { + const trashId = `${uuid()}_trashBin` + additionalEquipmentEntities = { + ...additionalEquipmentEntities, + [trashId]: { + name: 'trashBin', + id: trashId, + location: trashLocation, + }, + } + } + } + if ( WASTE_CHUTE_FIXTURES.includes( quickTransferState.dropTipLocation.cutoutFixtureId @@ -172,12 +194,33 @@ function getInvariantContextAndRobotState( }, } } - + if ( + quickTransferState.blowOut != null && + quickTransferState.blowOut !== 'source_well' && + quickTransferState.blowOut !== 'dest_well' && + WASTE_CHUTE_FIXTURES.includes(quickTransferState.blowOut.cutoutFixtureId) + ) { + const wasteChuteLocation = quickTransferState.dropTipLocation.cutoutId + const isSameChute = Object.values(additionalEquipmentEntities).some( + entity => entity.location === wasteChuteLocation + ) + if (!isSameChute) { + const wasteChuteId = `${uuid()}_wasteChute` + additionalEquipmentEntities = { + ...additionalEquipmentEntities, + [wasteChuteId]: { + name: 'wasteChute', + id: wasteChuteId, + location: wasteChuteLocation, + }, + } + } + } const invariantContext = { labwareEntities, moduleEntities: {}, pipetteEntities, - additionalEquipmentEntities: additionalEquipmentEntities, + additionalEquipmentEntities, config: { OT_PD_DISABLE_MODULE_RESTRICTIONS: false }, } const moduleLocations = {} @@ -224,22 +267,25 @@ export function generateQuickTransferArgs( } } const { invariantContext, robotState } = getInvariantContextAndRobotState( - quickTransferState, - deckConfig + quickTransferState ) - // this cannot be 'dest_well' for multiDispense - let blowoutLocation = quickTransferState.blowOut - if (quickTransferState.blowOut === 'trashBin') { - const trashBinEntity = Object.values( - invariantContext.additionalEquipmentEntities - ).find(entity => entity.name === 'trashBin') - blowoutLocation = trashBinEntity?.id - } else if (quickTransferState.blowOut === 'wasteChute') { - const wasteChuteEntity = Object.values( + let blowoutLocation: string | undefined + if ( + quickTransferState?.blowOut != null && + quickTransferState.blowOut !== 'source_well' && + quickTransferState.blowOut !== 'dest_well' && + 'cutoutId' in quickTransferState.blowOut + ) { + const entity = Object.values( invariantContext.additionalEquipmentEntities - ).find(entity => entity.name === 'wasteChute') - blowoutLocation = wasteChuteEntity?.id + ).find(entity => { + const blowoutObject = quickTransferState.blowOut as CutoutConfig + return entity.location === blowoutObject.cutoutId + }) + blowoutLocation = entity?.id + } else { + blowoutLocation = quickTransferState.blowOut } const dropTipLocationEntity = Object.values( @@ -305,20 +351,18 @@ export function generateQuickTransferArgs( dispenseAirGapVolume: quickTransferState.airGapDispense ?? null, touchTipAfterAspirate: quickTransferState.touchTipAspirate != null, touchTipAfterAspirateOffsetMmFromBottom: - quickTransferState.touchTipAspirate != null - ? quickTransferState.touchTipAspirate - : getWellsDepth(quickTransferState.source, sourceWells) + - DEFAULT_MM_TOUCH_TIP_OFFSET_FROM_TOP, + quickTransferState.touchTipAspirate ?? + getWellsDepth(quickTransferState.source, sourceWells) + + DEFAULT_MM_TOUCH_TIP_OFFSET_FROM_TOP, touchTipAfterDispense: quickTransferState.touchTipDispense != null, touchTipAfterDispenseOffsetMmFromBottom: - quickTransferState.touchTipDispense != null - ? quickTransferState.touchTipDispense - : getWellsDepth( - quickTransferState.destination === 'source' - ? quickTransferState.source - : quickTransferState.destination, - destWells - ) + DEFAULT_MM_TOUCH_TIP_OFFSET_FROM_TOP, + quickTransferState.touchTipDispense ?? + getWellsDepth( + quickTransferState.destination === 'source' + ? quickTransferState.source + : quickTransferState.destination, + destWells + ) + DEFAULT_MM_TOUCH_TIP_OFFSET_FROM_TOP, dropTipLocation, aspirateXOffset: 0, aspirateYOffset: 0, @@ -390,7 +434,7 @@ export function generateQuickTransferArgs( const distributeStepArguments: DistributeArgs = { ...commonFields, commandCreatorFnName: 'distribute', - disposalVolume: quickTransferState.volume, + disposalVolume: quickTransferState.disposalVolume, mixBeforeAspirate: quickTransferState.mixOnAspirate != null ? { @@ -399,7 +443,7 @@ export function generateQuickTransferArgs( } : null, sourceWell: sourceWells[0], - destWells: destWells, + destWells, } return { stepArgs: distributeStepArguments, diff --git a/app/src/organisms/QuickTransferFlow/utils/getInitialSummaryState.ts b/app/src/organisms/QuickTransferFlow/utils/getInitialSummaryState.ts index ff582c53e01..d073dd13894 100644 --- a/app/src/organisms/QuickTransferFlow/utils/getInitialSummaryState.ts +++ b/app/src/organisms/QuickTransferFlow/utils/getInitialSummaryState.ts @@ -49,13 +49,13 @@ export function getInitialSummaryState( // for multiDispense the volume capacity must be at least 3x the volume per well // to account for the 1x volume per well disposal volume default if ( - state.transferType === 'consolidate' && + state.transferType === 'distribute' && volumeLimits.max >= state.volume * 3 ) { path = 'multiDispense' // for multiAspirate the volume capacity must be at least 2x the volume per well } else if ( - state.transferType === 'distribute' && + state.transferType === 'consolidate' && volumeLimits.max >= state.volume * 2 ) { path = 'multiAspirate' @@ -89,6 +89,8 @@ export function getInitialSummaryState( aspirateFlowRate: flowRatesForSupportedTip.defaultAspirateFlowRate.default, dispenseFlowRate: flowRatesForSupportedTip.defaultDispenseFlowRate.default, path, + disposalVolume: path === 'multiDispense' ? state.volume : undefined, + blowOut: path === 'multiDispense' ? trashConfigCutout : undefined, tipPositionAspirate: 1, preWetTip: false, tipPositionDispense: 1, diff --git a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/__tests__/WifiConnectionDetails.test.tsx b/app/src/organisms/RobotSettingsDashboard/NetworkSettings/__tests__/WifiConnectionDetails.test.tsx index a8ec836d044..02da87250d3 100644 --- a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/__tests__/WifiConnectionDetails.test.tsx +++ b/app/src/organisms/RobotSettingsDashboard/NetworkSettings/__tests__/WifiConnectionDetails.test.tsx @@ -17,12 +17,12 @@ vi.mock('../../../../redux/discovery') vi.mock('../../../../redux/networking') vi.mock('../NetworkDetailsModal') -const mockPush = vi.fn() +const mockNavigate = vi.fn() vi.mock('react-router-dom', async importOriginal => { const reactRouterDom = await importOriginal() return { ...reactRouterDom, - useHistory: () => ({ push: mockPush } as any), + useNavigate: () => mockNavigate, } }) diff --git a/app/src/organisms/RobotSettingsDashboard/RobotSystemVersionModal.tsx b/app/src/organisms/RobotSettingsDashboard/RobotSystemVersionModal.tsx index 703f252c82e..9b6467af878 100644 --- a/app/src/organisms/RobotSettingsDashboard/RobotSystemVersionModal.tsx +++ b/app/src/organisms/RobotSettingsDashboard/RobotSystemVersionModal.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { DIRECTION_COLUMN, @@ -29,7 +29,7 @@ export function RobotSystemVersionModal({ setShowModal, }: RobotSystemVersionModalProps): JSX.Element { const { t } = useTranslation(['device_settings', 'shared']) - const history = useHistory() + const navigate = useNavigate() const modalHeader: ModalHeaderBaseProps = { title: t('robot_system_version_available', { @@ -68,7 +68,7 @@ export function RobotSystemVersionModal({ { - history.push('/robot-settings/update-robot') + navigate('/robot-settings/update-robot') }} buttonText={t('shared:update')} /> diff --git a/app/src/organisms/RobotSettingsDashboard/__tests__/RobotSystemVersionModal.test.tsx b/app/src/organisms/RobotSettingsDashboard/__tests__/RobotSystemVersionModal.test.tsx index 6e6f4780e0d..887b36c332b 100644 --- a/app/src/organisms/RobotSettingsDashboard/__tests__/RobotSystemVersionModal.test.tsx +++ b/app/src/organisms/RobotSettingsDashboard/__tests__/RobotSystemVersionModal.test.tsx @@ -9,13 +9,13 @@ import { RobotSystemVersionModal } from '../RobotSystemVersionModal' import type * as Dom from 'react-router-dom' const mockFn = vi.fn() -const mockPush = vi.fn() +const mockNavigate = vi.fn() vi.mock('react-router-dom', async importOriginal => { const reactRouterDom = await importOriginal() return { ...reactRouterDom, - useHistory: () => ({ push: mockPush } as any), + useNavigate: () => mockNavigate, } }) @@ -52,7 +52,7 @@ describe('RobotSystemVersionModal', () => { it('should close the modal when tapping remind me later', () => { render(props) fireEvent.click(screen.getByText('Update')) - expect(mockPush).toHaveBeenCalledWith('/robot-settings/update-robot') + expect(mockNavigate).toHaveBeenCalledWith('/robot-settings/update-robot') }) it('should call the mock function when tapping update', () => { diff --git a/app/src/organisms/SendProtocolToFlexSlideout/__tests__/SendProtocolToFlexSlideout.test.tsx b/app/src/organisms/SendProtocolToFlexSlideout/__tests__/SendProtocolToFlexSlideout.test.tsx index 9f5279aa18f..cd3ab48d411 100644 --- a/app/src/organisms/SendProtocolToFlexSlideout/__tests__/SendProtocolToFlexSlideout.test.tsx +++ b/app/src/organisms/SendProtocolToFlexSlideout/__tests__/SendProtocolToFlexSlideout.test.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { fireEvent, screen } from '@testing-library/react' import { describe, it, expect, vi, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' -import { StaticRouter } from 'react-router-dom' +import { MemoryRouter } from 'react-router-dom' import { mockOT3HealthResponse, @@ -57,9 +57,9 @@ const render = ( props: React.ComponentProps ) => { return renderWithProviders( - + - , + , { i18nInstance: i18n, } diff --git a/app/src/organisms/UpdateAppModal/__tests__/UpdateAppModal.test.tsx b/app/src/organisms/UpdateAppModal/__tests__/UpdateAppModal.test.tsx index 95681e4a661..761cb07f0f8 100644 --- a/app/src/organisms/UpdateAppModal/__tests__/UpdateAppModal.test.tsx +++ b/app/src/organisms/UpdateAppModal/__tests__/UpdateAppModal.test.tsx @@ -27,9 +27,7 @@ vi.mock('react-router-dom', async importOriginal => { const actual = await importOriginal() return { ...actual, - useHistory: () => ({ - push: vi.fn(), - }), + useNavigate: () => vi.fn(), } }) diff --git a/app/src/organisms/UpdateAppModal/index.tsx b/app/src/organisms/UpdateAppModal/index.tsx index be41d6ed597..431bc144a31 100644 --- a/app/src/organisms/UpdateAppModal/index.tsx +++ b/app/src/organisms/UpdateAppModal/index.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { useSelector, useDispatch } from 'react-redux' import styled, { css } from 'styled-components' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { @@ -96,7 +96,7 @@ export function UpdateAppModal(props: UpdateAppModalProps): JSX.Element { } = updateState const releaseNotes = updateInfo?.releaseNotes const { t } = useTranslation(['app_settings', 'branded']) - const history = useHistory() + const navigate = useNavigate() const { removeActiveAppUpdateToast } = useRemoveActiveAppUpdateToast() const availableAppUpdateVersion = useSelector(getAvailableShellUpdate) ?? '' @@ -104,7 +104,7 @@ export function UpdateAppModal(props: UpdateAppModalProps): JSX.Element { setTimeout(() => dispatch(applyShellUpdate()), RESTART_APP_AFTER_TIME) const handleRemindMeLaterClick = (): void => { - history.push('/app-settings/general') + navigate('/app-settings/general') closeModal(true) } diff --git a/app/src/organisms/WellSelection/utils.ts b/app/src/organisms/WellSelection/utils.ts index 1fb5e66f678..bd6bc660a1a 100644 --- a/app/src/organisms/WellSelection/utils.ts +++ b/app/src/organisms/WellSelection/utils.ts @@ -26,7 +26,19 @@ export function clientRectToBoundingRect(rect: ClientRect): BoundingRect { } } +// TODO(jh, 07-17-24): Consider checking specific well labels instead of elementAtPoint as a more robust alternative. export const getCollidingWells = (rectPositions: GenericRect): WellGroup => { + const isElementVisible = (element: HTMLElement): boolean => { + const rect = element.getBoundingClientRect() + // If multiple well elements occupy the same x,y coordinate space, document.elementFromPoint() selects + // ONLY the "topmost" well element, accounting for z-index, stacking order, visibility, and opacity. + const elementAtPoint = document.elementFromPoint( + rect.left + rect.width / 2, + rect.top + rect.height / 2 + ) + return element.contains(elementAtPoint) + } + // Returns set of selected wells under a collision rect const { x0, y0, x1, y1 } = rectPositions const selectionBoundingRect = { @@ -41,12 +53,26 @@ export const getCollidingWells = (rectPositions: GenericRect): WellGroup => { `[${INTERACTIVE_WELL_DATA_ATTRIBUTE}]` ), ] - const collidedElems = selectableElems.filter((selectableElem, i) => - rectCollision( - selectionBoundingRect, - clientRectToBoundingRect(selectableElem.getBoundingClientRect()) + + const collidedElems = selectableElems.filter(selectableElem => { + const rect = selectableElem.getBoundingClientRect() + + // Check if the element is in the viewport and not obscured. + const isInViewport = + rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= + (window.innerHeight || document.documentElement.clientHeight) && + rect.right <= (window.innerWidth || document.documentElement.clientWidth) + + const isVisible = isInViewport && isElementVisible(selectableElem) + + return ( + isVisible && + rectCollision(selectionBoundingRect, clientRectToBoundingRect(rect)) ) - ) + }) + const collidedWellData = collidedElems.reduce( (acc: WellGroup, elem): WellGroup => { if ( diff --git a/app/src/pages/AppSettings/__test__/AppSettings.test.tsx b/app/src/pages/AppSettings/__test__/AppSettings.test.tsx index bc8942c85f9..fecd160e0e2 100644 --- a/app/src/pages/AppSettings/__test__/AppSettings.test.tsx +++ b/app/src/pages/AppSettings/__test__/AppSettings.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { vi, describe, beforeEach, it, expect, afterEach } from 'vitest' import { Route } from 'react-router' -import { MemoryRouter } from 'react-router-dom' +import { MemoryRouter, Routes } from 'react-router-dom' import { renderWithProviders } from '../../../__testing-utils__' @@ -22,9 +22,9 @@ vi.mock('../../../organisms/AppSettings/FeatureFlags') const render = (path = '/'): ReturnType => { return renderWithProviders( - - - + + } /> + , { i18nInstance: i18n, diff --git a/app/src/pages/AppSettings/index.tsx b/app/src/pages/AppSettings/index.tsx index 0cfe5872885..61b5f619c96 100644 --- a/app/src/pages/AppSettings/index.tsx +++ b/app/src/pages/AppSettings/index.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' -import { Redirect, useParams } from 'react-router-dom' +import { Navigate, useParams } from 'react-router-dom' import { ALIGN_START, @@ -28,7 +28,9 @@ import type { DesktopRouteParams, AppSettingsTab } from '../../App/types' export function AppSettings(): JSX.Element { const { t } = useTranslation('app_settings') const devToolsOn = useSelector(Config.getDevtoolsEnabled) - const { appSettingsTab } = useParams() + const { appSettingsTab } = useParams< + keyof DesktopRouteParams + >() as DesktopRouteParams const appSettingsContentByTab: { [K in AppSettingsTab]: JSX.Element @@ -41,7 +43,7 @@ export function AppSettings(): JSX.Element { const appSettingsContent = appSettingsContentByTab[appSettingsTab] ?? ( // default to the general tab if no tab or nonexistent tab is passed as a param - + ) return ( diff --git a/app/src/pages/ConnectViaEthernet/DisplayConnectionStatus.tsx b/app/src/pages/ConnectViaEthernet/DisplayConnectionStatus.tsx index 6e2ef24d5b3..177599adb5b 100644 --- a/app/src/pages/ConnectViaEthernet/DisplayConnectionStatus.tsx +++ b/app/src/pages/ConnectViaEthernet/DisplayConnectionStatus.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { ALIGN_CENTER, @@ -28,7 +28,7 @@ export function DisplayConnectionStatus({ setShowNetworkDetailsModal, }: DisplayConnectionStatusProps): JSX.Element { const { i18n, t } = useTranslation(['device_settings', 'shared']) - const history = useHistory() + const navigate = useNavigate() return ( @@ -94,7 +94,7 @@ export function DisplayConnectionStatus({ flex="1" buttonText={i18n.format(t('shared:continue'), 'capitalize')} onClick={() => { - history.push('/robot-settings/update-robot-during-onboarding') + navigate('/robot-settings/update-robot-during-onboarding') }} /> ) : null} diff --git a/app/src/pages/ConnectViaEthernet/TitleHeader.tsx b/app/src/pages/ConnectViaEthernet/TitleHeader.tsx index 40c3bf27fe8..c837499c22b 100644 --- a/app/src/pages/ConnectViaEthernet/TitleHeader.tsx +++ b/app/src/pages/ConnectViaEthernet/TitleHeader.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { ALIGN_CENTER, @@ -21,7 +21,7 @@ interface TitleHeaderProps { // Note (kj:05/12/2023) This might be a component later export function TitleHeader({ title }: TitleHeaderProps): JSX.Element { - const history = useHistory() + const navigate = useNavigate() return ( { - history.push('/network-setup') + navigate('/network-setup') }} data-testid={`${title}_header_back_button`} > diff --git a/app/src/pages/ConnectViaEthernet/__tests__/DisplayConnectionStatus.test.tsx b/app/src/pages/ConnectViaEthernet/__tests__/DisplayConnectionStatus.test.tsx index 76c195f3ff6..92864b2a1d0 100644 --- a/app/src/pages/ConnectViaEthernet/__tests__/DisplayConnectionStatus.test.tsx +++ b/app/src/pages/ConnectViaEthernet/__tests__/DisplayConnectionStatus.test.tsx @@ -6,15 +6,15 @@ import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' import { DisplayConnectionStatus } from '../../../pages/ConnectViaEthernet/DisplayConnectionStatus' -import type * as ReactRouterDom from 'react-router-dom' +import type { NavigateFunction } from 'react-router-dom' const mockFunc = vi.fn() -const mockPush = vi.fn() +const mockNavigate = vi.fn() vi.mock('react-router-dom', async importOriginal => { - const actual = await importOriginal() + const actual = await importOriginal() return { ...actual, - useHistory: () => ({ push: mockPush } as any), + useNavigate: () => mockNavigate, } }) @@ -70,7 +70,7 @@ describe('DisplayConnectionStatus', () => { it('should call a mock push when tapping continue button', () => { render(props) fireEvent.click(screen.getByText('Continue')) - expect(mockPush).toHaveBeenCalledWith( + expect(mockNavigate).toHaveBeenCalledWith( '/robot-settings/update-robot-during-onboarding' ) }) diff --git a/app/src/pages/ConnectViaEthernet/__tests__/TitleHeader.test.tsx b/app/src/pages/ConnectViaEthernet/__tests__/TitleHeader.test.tsx index 67921d8203c..14575779736 100644 --- a/app/src/pages/ConnectViaEthernet/__tests__/TitleHeader.test.tsx +++ b/app/src/pages/ConnectViaEthernet/__tests__/TitleHeader.test.tsx @@ -5,14 +5,14 @@ import { fireEvent, screen } from '@testing-library/react' import { renderWithProviders } from '../../../__testing-utils__' import { TitleHeader } from '../../../pages/ConnectViaEthernet/TitleHeader' -import type * as ReactRouterDom from 'react-router-dom' +import type { NavigateFunction } from 'react-router-dom' -const mockPush = vi.fn() +const mockNavigate = vi.fn() vi.mock('react-router-dom', async importOriginal => { - const actual = await importOriginal() + const actual = await importOriginal() return { ...actual, - useHistory: () => ({ push: mockPush } as any), + useNavigate: () => mockNavigate, } }) @@ -38,6 +38,6 @@ describe('TitleHeader', () => { it('should call a mock function when tapping back button', () => { render(props) fireEvent.click(screen.getByTestId('Ethernet_header_back_button')) - expect(mockPush).toHaveBeenCalledWith('/network-setup') + expect(mockNavigate).toHaveBeenCalledWith('/network-setup') }) }) diff --git a/app/src/pages/ConnectViaUSB/_tests__/ConnectedViaUSB.test.tsx b/app/src/pages/ConnectViaUSB/_tests__/ConnectedViaUSB.test.tsx index 16bc9bb98bf..79647ac1fb8 100644 --- a/app/src/pages/ConnectViaUSB/_tests__/ConnectedViaUSB.test.tsx +++ b/app/src/pages/ConnectViaUSB/_tests__/ConnectedViaUSB.test.tsx @@ -11,15 +11,15 @@ import { ConnectViaUSB } from '../../../pages/ConnectViaUSB' import type { UseQueryResult } from 'react-query' import type { ActiveConnections } from '@opentrons/api-client' -import type * as ReactRouterDom from 'react-router-dom' +import type { NavigateFunction } from 'react-router-dom' -const mockPush = vi.fn() +const mockNavigate = vi.fn() vi.mock('react-router-dom', async importOriginal => { - const actual = await importOriginal() + const actual = await importOriginal() return { ...actual, - useHistory: () => ({ push: mockPush } as any), + useNavigate: () => mockNavigate, } }) vi.mock('@opentrons/react-api-client') @@ -58,7 +58,7 @@ describe('ConnectViaUSB', () => { it('should call a mock function when tapping back button', () => { const [{ getByRole }] = render() fireEvent.click(getByRole('button')) - expect(mockPush).toHaveBeenCalledWith('/network-setup') + expect(mockNavigate).toHaveBeenCalledWith('/network-setup') }) it('should render successful connection text and button', () => { @@ -81,6 +81,6 @@ describe('ConnectViaUSB', () => { const [{ getByText }] = render() const button = getByText('Continue') fireEvent.click(button) - expect(mockPush).toHaveBeenCalledWith('/emergency-stop') + expect(mockNavigate).toHaveBeenCalledWith('/emergency-stop') }) }) diff --git a/app/src/pages/ConnectViaUSB/index.tsx b/app/src/pages/ConnectViaUSB/index.tsx index a28b97af85c..a802d36d891 100644 --- a/app/src/pages/ConnectViaUSB/index.tsx +++ b/app/src/pages/ConnectViaUSB/index.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { ALIGN_CENTER, BORDERS, @@ -23,7 +23,7 @@ import { MediumButton } from '../../atoms/buttons' export function ConnectViaUSB(): JSX.Element { const { i18n, t } = useTranslation(['device_settings', 'shared', 'branded']) - const history = useHistory() + const navigate = useNavigate() // TODO(bh, 2023-5-31): active connections from /system/connected isn't exactly the right way to monitor for a usb connection - // the system-server tracks active connections by authorization token, which is valid for 2 hours // another option is to report an active usb connection by monitoring usb port traffic (discovery-client polls health from the desktop app) @@ -50,7 +50,7 @@ export function ConnectViaUSB(): JSX.Element { { - history.push('/network-setup') + navigate('/network-setup') }} position={POSITION_ABSOLUTE} > @@ -104,7 +104,7 @@ export function ConnectViaUSB(): JSX.Element { { - history.push('/emergency-stop') + navigate('/emergency-stop') }} /> diff --git a/app/src/pages/DeckConfiguration/__tests__/DeckConfiguration.test.tsx b/app/src/pages/DeckConfiguration/__tests__/DeckConfiguration.test.tsx index d0fab1277b0..1e34e917eca 100644 --- a/app/src/pages/DeckConfiguration/__tests__/DeckConfiguration.test.tsx +++ b/app/src/pages/DeckConfiguration/__tests__/DeckConfiguration.test.tsx @@ -20,15 +20,15 @@ import { import type { UseQueryResult } from 'react-query' import type { DeckConfiguration } from '@opentrons/shared-data' import type * as Components from '@opentrons/components' -import type * as ReactRouterDom from 'react-router-dom' +import type { NavigateFunction } from 'react-router-dom' const mockUpdateDeckConfiguration = vi.fn() const mockGoBack = vi.fn() vi.mock('react-router-dom', async importOriginal => { - const actual = await importOriginal() + const actual = await importOriginal() return { ...actual, - useHistory: () => ({ goBack: mockGoBack } as any), + useNavigate: () => mockGoBack, } }) vi.mock('@opentrons/components', async importOriginal => { diff --git a/app/src/pages/DeckConfiguration/index.tsx b/app/src/pages/DeckConfiguration/index.tsx index f520b825177..ef27e81bc0a 100644 --- a/app/src/pages/DeckConfiguration/index.tsx +++ b/app/src/pages/DeckConfiguration/index.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { DeckConfigurator, @@ -28,7 +28,7 @@ export function DeckConfigurationEditor(): JSX.Element { 'devices_landing', 'shared', ]) - const history = useHistory() + const navigate = useNavigate() const [ showSetupInstructionsModal, setShowSetupInstructionsModal, @@ -49,7 +49,7 @@ export function DeckConfigurationEditor(): JSX.Element { const deckConfig = useNotifyDeckConfigurationQuery().data ?? [] const handleClickConfirm = (): void => { - history.goBack() + navigate(-1) } const secondaryButtonProps: React.ComponentProps = { diff --git a/app/src/pages/Devices/CalibrationDashboard/__tests__/CalibrationDashboard.test.tsx b/app/src/pages/Devices/CalibrationDashboard/__tests__/CalibrationDashboard.test.tsx index 1c46e097c41..7039e9836b5 100644 --- a/app/src/pages/Devices/CalibrationDashboard/__tests__/CalibrationDashboard.test.tsx +++ b/app/src/pages/Devices/CalibrationDashboard/__tests__/CalibrationDashboard.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { vi, describe, it, beforeEach } from 'vitest' import { screen } from '@testing-library/react' -import { MemoryRouter, Route } from 'react-router-dom' +import { MemoryRouter, Route, Routes } from 'react-router-dom' import { renderWithProviders } from '../../../../__testing-utils__' import { i18n } from '../../../../i18n' @@ -27,9 +27,12 @@ vi.mock('../../../../resources/runs') const render = (path = '/') => { return renderWithProviders( - - - + + } + /> + , { i18nInstance: i18n, diff --git a/app/src/pages/Devices/CalibrationDashboard/index.tsx b/app/src/pages/Devices/CalibrationDashboard/index.tsx index bcdd71cc31c..b875918c25e 100644 --- a/app/src/pages/Devices/CalibrationDashboard/index.tsx +++ b/app/src/pages/Devices/CalibrationDashboard/index.tsx @@ -12,7 +12,9 @@ import { useRobot } from '../../../organisms/Devices/hooks' import type { DesktopRouteParams } from '../../../App/types' export function CalibrationDashboard(): JSX.Element { - const { robotName } = useParams() + const { robotName } = useParams< + keyof DesktopRouteParams + >() as DesktopRouteParams const robot = useRobot(robotName) const [ dashboardOffsetCalLauncher, diff --git a/app/src/pages/Devices/DeviceDetails/__tests__/DeviceDetails.test.tsx b/app/src/pages/Devices/DeviceDetails/__tests__/DeviceDetails.test.tsx index 6c20bf55094..7c45cf32baa 100644 --- a/app/src/pages/Devices/DeviceDetails/__tests__/DeviceDetails.test.tsx +++ b/app/src/pages/Devices/DeviceDetails/__tests__/DeviceDetails.test.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { vi, it, describe, expect, beforeEach } from 'vitest' import { when } from 'vitest-when' import { screen } from '@testing-library/react' -import { MemoryRouter, Route } from 'react-router-dom' +import { MemoryRouter, Route, Routes } from 'react-router-dom' import { renderWithProviders } from '../../../../__testing-utils__' @@ -29,10 +29,10 @@ vi.mock('../../../../redux/discovery') const render = (path = '/') => { return renderWithProviders( - - - - devices page + + } /> + devices page} /> + , { i18nInstance: i18n, diff --git a/app/src/pages/Devices/DeviceDetails/index.tsx b/app/src/pages/Devices/DeviceDetails/index.tsx index 048a9921220..962dd229d87 100644 --- a/app/src/pages/Devices/DeviceDetails/index.tsx +++ b/app/src/pages/Devices/DeviceDetails/index.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useSelector } from 'react-redux' -import { Redirect, useParams } from 'react-router-dom' +import { Navigate, useParams } from 'react-router-dom' import { ApiHostProvider } from '@opentrons/react-api-client' @@ -12,7 +12,9 @@ import { DeviceDetailsComponent } from './DeviceDetailsComponent' import type { DesktopRouteParams } from '../../../App/types' export function DeviceDetails(): JSX.Element | null { - const { robotName } = useParams() + const { robotName } = useParams< + keyof DesktopRouteParams + >() as DesktopRouteParams const robot = useRobot(robotName) const isScanning = useSelector(getScanning) @@ -30,6 +32,6 @@ export function DeviceDetails(): JSX.Element | null { ) : ( - + ) } diff --git a/app/src/pages/Devices/DevicesLanding/NewRobotSetupHelp.tsx b/app/src/pages/Devices/DevicesLanding/NewRobotSetupHelp.tsx index dd02af14a64..a453f07935c 100644 --- a/app/src/pages/Devices/DevicesLanding/NewRobotSetupHelp.tsx +++ b/app/src/pages/Devices/DevicesLanding/NewRobotSetupHelp.tsx @@ -16,8 +16,10 @@ import { getTopPortalEl } from '../../../App/portal' import { LegacyModal } from '../../../molecules/LegacyModal' import { ExternalLink } from '../../../atoms/Link/ExternalLink' -const NEW_ROBOT_SETUP_SUPPORT_ARTICLE_HREF = - 'https://support.opentrons.com/s/ot2-get-started' +const NEW_FLEX_SETUP_SUPPORT_ARTICLE_HREF = + 'https://insights.opentrons.com/hubfs/Products/Flex/Opentrons%20Flex%20Quickstart%20Guide.pdf' +const NEW_OT2_SETUP_SUPPORT_ARTICLE_HREF = + 'https://insights.opentrons.com/hubfs/Products/OT-2/OT-2%20Quick%20Start%20Guide.pdf' export function NewRobotSetupHelp(): JSX.Element { const { t } = useTranslation(['devices_landing', 'shared']) @@ -46,10 +48,13 @@ export function NewRobotSetupHelp(): JSX.Element { > - {t('use_usb_cable_for_new_robot')} + {t('new_robot_instructions')} - - {t('learn_more_about_new_robot_setup')} + + {t('opentrons_flex_quickstart_guide')} + + + {t('ot2_quickstart_guide')} { diff --git a/app/src/pages/Devices/DevicesLanding/__tests__/NewRobotSetupHelp.test.tsx b/app/src/pages/Devices/DevicesLanding/__tests__/NewRobotSetupHelp.test.tsx index 51e4e994380..fb4cbadfd42 100644 --- a/app/src/pages/Devices/DevicesLanding/__tests__/NewRobotSetupHelp.test.tsx +++ b/app/src/pages/Devices/DevicesLanding/__tests__/NewRobotSetupHelp.test.tsx @@ -24,7 +24,7 @@ describe('NewRobotSetupHelp', () => { const link = screen.getByText('See how to set up a new robot') fireEvent.click(link) - screen.getByText('How to setup a new robot') + screen.getByText('How to set up a new robot') const closeButton = screen.getByRole('button', { name: 'close' }) fireEvent.click(closeButton) @@ -36,11 +36,30 @@ describe('NewRobotSetupHelp', () => { const link = screen.getByText('See how to set up a new robot') fireEvent.click(link) - expect(screen.getByText('How to setup a new robot')).toBeInTheDocument() + expect(screen.getByText('How to set up a new robot')).toBeInTheDocument() const xButton = screen.getByRole('button', { name: '' }) fireEvent.click(xButton) - expect(screen.queryByText('How to setup a new robot')).toBeFalsy() + expect(screen.queryByText('How to set up a new robot')).toBeFalsy() + }) + + it('renders the link and it has the correct href attribute', () => { + render() + const link = screen.getByText('See how to set up a new robot') + fireEvent.click(link) + const targetLinkUrlFlex = + 'https://insights.opentrons.com/hubfs/Products/Flex/Opentrons%20Flex%20Quickstart%20Guide.pdf' + const supportLinkFlex = screen.getByRole('link', { + name: 'Opentrons Flex Quickstart Guide', + }) + expect(supportLinkFlex).toHaveAttribute('href', targetLinkUrlFlex) + + const targetLinkUrlOt2 = + 'https://insights.opentrons.com/hubfs/Products/OT-2/OT-2%20Quick%20Start%20Guide.pdf' + const supportLinkOt2 = screen.getByRole('link', { + name: 'OT-2 Quickstart Guide', + }) + expect(supportLinkOt2).toHaveAttribute('href', targetLinkUrlOt2) }) }) diff --git a/app/src/pages/Devices/ProtocolRunDetails/__tests__/ProtocolRunDetails.test.tsx b/app/src/pages/Devices/ProtocolRunDetails/__tests__/ProtocolRunDetails.test.tsx index 23d9e3494c1..a5a1df6d218 100644 --- a/app/src/pages/Devices/ProtocolRunDetails/__tests__/ProtocolRunDetails.test.tsx +++ b/app/src/pages/Devices/ProtocolRunDetails/__tests__/ProtocolRunDetails.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { vi, it, describe, expect, beforeEach, afterEach } from 'vitest' -import { Route, MemoryRouter } from 'react-router-dom' +import { Route, MemoryRouter, Routes } from 'react-router-dom' import { fireEvent, screen } from '@testing-library/react' import { renderWithProviders } from '../../../../__testing-utils__' @@ -61,9 +61,12 @@ const mockMagneticModule = { const render = (path = '/') => { return renderWithProviders( - - - + + } + /> + , { i18nInstance: i18n, diff --git a/app/src/pages/Devices/ProtocolRunDetails/index.tsx b/app/src/pages/Devices/ProtocolRunDetails/index.tsx index 8fdec56aeaf..e77841ec980 100644 --- a/app/src/pages/Devices/ProtocolRunDetails/index.tsx +++ b/app/src/pages/Devices/ProtocolRunDetails/index.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import isEmpty from 'lodash/isEmpty' import { useTranslation } from 'react-i18next' import { useDispatch } from 'react-redux' -import { NavLink, Redirect, useParams } from 'react-router-dom' +import { NavLink, Navigate, useParams } from 'react-router-dom' import styled, { css } from 'styled-components' import { @@ -125,11 +125,9 @@ function RoundTab({ } export function ProtocolRunDetails(): JSX.Element | null { - const { - robotName, - runId, - protocolRunDetailsTab, - } = useParams() + const { robotName, runId, protocolRunDetailsTab } = useParams< + keyof DesktopRouteParams + >() as DesktopRouteParams const dispatch = useDispatch() const robot = useRobot(robotName) @@ -224,7 +222,8 @@ function PageContents(props: PageContentsProps): JSX.Element { protocolRunDetailsTab ] ?? ( // default to the setup tab if no tab or nonexistent tab is passed as a param - + + ) return ( @@ -277,7 +276,7 @@ const SetupTab = (props: SetupTabProps): JSX.Element | null => { /> {currentRunId !== runId ? ( // redirect to run preview if not current run - ) : null} @@ -305,7 +304,7 @@ const ParametersTab = (props: ParametersTabProps): JSX.Element | null => { tabName={t('parameters')} /> {disabled ? ( - ) : null} @@ -346,7 +345,8 @@ const ModuleControlsTab = ( /> {disabled ? ( // redirect to run preview if not current run - ) : null} diff --git a/app/src/pages/Devices/RobotSettings/__tests__/RobotSettings.test.tsx b/app/src/pages/Devices/RobotSettings/__tests__/RobotSettings.test.tsx index cb3aa3541e5..6961f74ad22 100644 --- a/app/src/pages/Devices/RobotSettings/__tests__/RobotSettings.test.tsx +++ b/app/src/pages/Devices/RobotSettings/__tests__/RobotSettings.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { vi, it, describe, expect, beforeEach, afterEach } from 'vitest' import { fireEvent, screen } from '@testing-library/react' -import { Route, MemoryRouter } from 'react-router-dom' +import { Route, MemoryRouter, Routes } from 'react-router-dom' import { renderWithProviders } from '../../../../__testing-utils__' import { i18n } from '../../../../i18n' @@ -28,12 +28,16 @@ vi.mock('../../../../redux/robot-update') const render = (path = '/') => { return renderWithProviders( - - - - -
    mock device details
    -
    + + } + /> + mock device details} + /> +
    , { i18nInstance: i18n, diff --git a/app/src/pages/Devices/RobotSettings/index.tsx b/app/src/pages/Devices/RobotSettings/index.tsx index c3d89245d0a..fd2e089a7d4 100644 --- a/app/src/pages/Devices/RobotSettings/index.tsx +++ b/app/src/pages/Devices/RobotSettings/index.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' -import { Redirect, useParams } from 'react-router-dom' +import { useParams, Navigate } from 'react-router-dom' import { BORDERS, @@ -39,7 +39,9 @@ import type { DesktopRouteParams, RobotSettingsTab } from '../../../App/types' export function RobotSettings(): JSX.Element | null { const { t } = useTranslation('device_settings') - const { robotName, robotSettingsTab } = useParams() + const { robotName, robotSettingsTab } = useParams< + keyof DesktopRouteParams + >() as DesktopRouteParams const robot = useRobot(robotName) const isCalibrationDisabled = robot?.status !== CONNECTABLE const isNetworkingDisabled = robot?.status === UNREACHABLE @@ -84,19 +86,19 @@ export function RobotSettings(): JSX.Element | null { (robot?.status === REACHABLE && robot?.serverHealthStatus !== 'ok')) && robotUpdateSession == null ) { - return + return } const cannotViewCalibration = robotSettingsTab === 'calibration' && isCalibrationDisabled const cannotViewFeatureFlags = robotSettingsTab === 'feature-flags' && !devToolsOn if (cannotViewCalibration || cannotViewFeatureFlags) { - return + return } const robotSettingsContent = robotSettingsContentByTab[robotSettingsTab] ?? ( // default to the calibration tab if no tab or nonexistent tab is passed as a param - + ) return ( diff --git a/app/src/pages/EmergencyStop/__tests__/EmergencyStop.test.tsx b/app/src/pages/EmergencyStop/__tests__/EmergencyStop.test.tsx index bbfee8eb376..6e34f86c218 100644 --- a/app/src/pages/EmergencyStop/__tests__/EmergencyStop.test.tsx +++ b/app/src/pages/EmergencyStop/__tests__/EmergencyStop.test.tsx @@ -7,7 +7,7 @@ import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' import { EmergencyStop } from '..' -import type * as ReactRouterDom from 'react-router-dom' +import type { NavigateFunction } from 'react-router-dom' vi.mock('@opentrons/react-api-client') @@ -19,12 +19,12 @@ const mockDisconnectedEstop = { rightEstopPhysicalStatus: 'notPresent', }, } as any -const mockPush = vi.fn() +const mockNavigate = vi.fn() vi.mock('react-router-dom', async importOriginal => { - const actual = await importOriginal() + const actual = await importOriginal() return { ...actual, - useHistory: () => ({ push: mockPush } as any), + useNavigate: () => mockNavigate, } }) @@ -85,6 +85,6 @@ describe('EmergencyStop', () => { } as any) render() fireEvent.click(screen.getByRole('button')) - expect(mockPush).toHaveBeenCalledWith('/robot-settings/rename-robot') + expect(mockNavigate).toHaveBeenCalledWith('/robot-settings/rename-robot') }) }) diff --git a/app/src/pages/EmergencyStop/index.tsx b/app/src/pages/EmergencyStop/index.tsx index c0d430ce12a..b5419ec5790 100644 --- a/app/src/pages/EmergencyStop/index.tsx +++ b/app/src/pages/EmergencyStop/index.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { ALIGN_CENTER, @@ -25,7 +25,7 @@ const ESTOP_STATUS_REFETCH_INTERVAL_MS = 10000 export function EmergencyStop(): JSX.Element { const { i18n, t } = useTranslation(['device_settings', 'shared']) - const history = useHistory() + const navigate = useNavigate() // Note here the touchscreen app is using status since status is linked to EstopPhysicalStatuses // left notPresent + right disengaged => disengaged @@ -102,7 +102,7 @@ export function EmergencyStop(): JSX.Element { buttonText={i18n.format(t('shared:continue'), 'capitalize')} disabled={!isEstopConnected} onClick={() => { - history.push('/robot-settings/rename-robot') + navigate('/robot-settings/rename-robot') }} />
    diff --git a/app/src/pages/InstrumentDetail/__tests__/InstrumentDetail.test.tsx b/app/src/pages/InstrumentDetail/__tests__/InstrumentDetail.test.tsx index af9489f2c6c..6b92a5ab9be 100644 --- a/app/src/pages/InstrumentDetail/__tests__/InstrumentDetail.test.tsx +++ b/app/src/pages/InstrumentDetail/__tests__/InstrumentDetail.test.tsx @@ -19,7 +19,7 @@ import type { Instruments } from '@opentrons/api-client' vi.mock('@opentrons/react-api-client') vi.mock('react-router-dom', () => ({ useParams: vi.fn(), - useHistory: vi.fn(), + useNavigate: vi.fn(), })) vi.mock('../../../resources/instruments/hooks') vi.mock('../../../resources/robot-settings/hooks') diff --git a/app/src/pages/InstrumentsDashboard/__tests__/InstrumentsDashboard.test.tsx b/app/src/pages/InstrumentsDashboard/__tests__/InstrumentsDashboard.test.tsx index 06e040bbe39..18b6d2cfb9c 100644 --- a/app/src/pages/InstrumentsDashboard/__tests__/InstrumentsDashboard.test.tsx +++ b/app/src/pages/InstrumentsDashboard/__tests__/InstrumentsDashboard.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Route, MemoryRouter } from 'react-router-dom' +import { Route, MemoryRouter, Routes } from 'react-router-dom' import { fireEvent, screen } from '@testing-library/react' import { renderWithProviders } from '../../../__testing-utils__' import { vi, describe, it, afterEach, beforeEach, expect } from 'vitest' @@ -93,18 +93,16 @@ vi.mock('../../../organisms/PipetteWizardFlows') vi.mock('../../../organisms/PipetteWizardFlows/ChoosePipette') vi.mock('../../../organisms/Navigation') -const render = () => { +const render = (path = '/') => { return renderWithProviders( - - - - - - - + + + } /> + } /> + , { i18nInstance: i18n } - ) + )[0] } describe('InstrumentsDashboard', () => { @@ -119,7 +117,7 @@ describe('InstrumentsDashboard', () => { vi.resetAllMocks() }) it('should render mount info for all attached mounts', () => { - render() + render('/instruments') screen.getByText('left Mount') screen.getByText('Flex 1-Channel 1000 μL') screen.getByText('right Mount') @@ -128,7 +126,7 @@ describe('InstrumentsDashboard', () => { screen.getByText('Flex Gripper') }) it('should route to left mount detail when instrument attached and clicked', () => { - render() + render('/instruments') fireEvent.click(screen.getByText('left Mount')) screen.getByText('serial number') screen.getByText(mockLeftPipetteData.serialNumber) @@ -139,7 +137,7 @@ describe('InstrumentsDashboard', () => { ) }) it('should route to right mount detail when instrument attached and clicked', () => { - render() + render('/instruments') fireEvent.click(screen.getByText('right Mount')) screen.getByText('serial number') screen.getByText(mockRightPipetteData.serialNumber) @@ -150,7 +148,7 @@ describe('InstrumentsDashboard', () => { ) }) it('should route to extension mount detail when instrument attached and clicked', () => { - render() + render('/instruments') fireEvent.click(screen.getByText('extension Mount')) screen.getByText('serial number') screen.getByText(mockGripperData.serialNumber) @@ -159,7 +157,7 @@ describe('InstrumentsDashboard', () => { vi.mocked(useInstrumentsQuery).mockReturnValue({ data: { data: [] }, } as any) - render() + render('/instruments') fireEvent.click(screen.getByText('left Mount')) expect(vi.mocked(ChoosePipette)).toHaveBeenCalled() }) @@ -167,7 +165,7 @@ describe('InstrumentsDashboard', () => { vi.mocked(useInstrumentsQuery).mockReturnValue({ data: { data: [] }, } as any) - render() + render('/instruments') fireEvent.click(screen.getByText('right Mount')) expect(vi.mocked(ChoosePipette)).toHaveBeenCalled() }) @@ -175,7 +173,7 @@ describe('InstrumentsDashboard', () => { vi.mocked(useInstrumentsQuery).mockReturnValue({ data: { data: [] }, } as any) - render() + render('/instruments') fireEvent.click(screen.getByText('extension Mount')) expect(vi.mocked(GripperWizardFlows)).toHaveBeenCalled() }) @@ -185,7 +183,7 @@ describe('InstrumentsDashboard', () => { data: [mock96ChannelData, mockGripperData], }, } as any) - render() + render('/instruments') screen.getByText('Left+Right Mounts') screen.getByText('extension Mount') }) diff --git a/app/src/pages/NameRobot/__tests__/NameRobot.test.tsx b/app/src/pages/NameRobot/__tests__/NameRobot.test.tsx index c7b9b546645..a91c108a527 100644 --- a/app/src/pages/NameRobot/__tests__/NameRobot.test.tsx +++ b/app/src/pages/NameRobot/__tests__/NameRobot.test.tsx @@ -19,20 +19,20 @@ import { } from '../../../redux/discovery/__fixtures__' import { NameRobot } from '..' -import type * as ReactRouterDom from 'react-router-dom' +import type { NavigateFunction } from 'react-router-dom' vi.mock('../../../redux/discovery/selectors') vi.mock('../../../redux/config') vi.mock('../../../redux/analytics') vi.mock('../../../organisms/RobotSettingsDashboard/NetworkSettings/hooks') -const mockPush = vi.fn() +const mockNavigate = vi.fn() vi.mock('react-router-dom', async importOriginal => { - const actual = await importOriginal() + const actual = await importOriginal() return { ...actual, - useHistory: () => ({ push: mockPush } as any), + useNavigate: () => mockNavigate, } }) @@ -142,6 +142,6 @@ describe('NameRobot', () => { vi.mocked(useIsUnboxingFlowOngoing).mockReturnValue(false) render() fireEvent.click(screen.getByTestId('name_back_button')) - expect(mockPush).toHaveBeenCalledWith('/robot-settings') + expect(mockNavigate).toHaveBeenCalledWith('/robot-settings') }) }) diff --git a/app/src/pages/NameRobot/index.tsx b/app/src/pages/NameRobot/index.tsx index eeed5b819e5..bd6f2180853 100644 --- a/app/src/pages/NameRobot/index.tsx +++ b/app/src/pages/NameRobot/index.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { Controller, useForm } from 'react-hook-form' import { useTranslation } from 'react-i18next' import { useSelector, useDispatch } from 'react-redux' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { ALIGN_CENTER, @@ -47,7 +47,7 @@ interface FormValues { export function NameRobot(): JSX.Element { const { t } = useTranslation(['device_settings', 'shared']) - const history = useHistory() + const navigate = useNavigate() const trackEvent = useTrackEvent() const localRobot = useSelector(getLocalRobot) const ipAddress = localRobot?.ip @@ -143,7 +143,7 @@ export function NameRobot(): JSX.Element { if (data.name != null) { setNewName(data.name) if (!isUnboxingFlowOngoing) { - history.push('/robot-settings') + navigate('/robot-settings') } else { setIsShowConfirmRobotName(true) } @@ -198,9 +198,9 @@ export function NameRobot(): JSX.Element { data-testid="name_back_button" onClick={() => { if (isUnboxingFlowOngoing) { - history.push('/emergency-stop') + navigate('/emergency-stop') } else { - history.push('/robot-settings') + navigate('/robot-settings') } }} > diff --git a/app/src/pages/NetworkSetupMenu/__tests__/NetworkSetupMenu.test.tsx b/app/src/pages/NetworkSetupMenu/__tests__/NetworkSetupMenu.test.tsx index f2febd1a32d..73f9312cc3c 100644 --- a/app/src/pages/NetworkSetupMenu/__tests__/NetworkSetupMenu.test.tsx +++ b/app/src/pages/NetworkSetupMenu/__tests__/NetworkSetupMenu.test.tsx @@ -6,15 +6,15 @@ import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' import { NetworkSetupMenu } from '..' -import type * as ReactRouterDom from 'react-router-dom' +import type { NavigateFunction } from 'react-router-dom' -const mockPush = vi.fn() +const mockNavigate = vi.fn() vi.mock('react-router-dom', async importOriginal => { - const actual = await importOriginal() + const actual = await importOriginal() return { ...actual, - useHistory: () => ({ push: mockPush } as any), + useNavigate: () => mockNavigate, } }) @@ -53,10 +53,10 @@ describe('NetworkSetupMenu', () => { const ethernetButton = screen.getByText('Ethernet') const usbButton = screen.getByText('USB') fireEvent.click(wifiButton) - expect(mockPush).toHaveBeenCalledWith('/network-setup/wifi') + expect(mockNavigate).toHaveBeenCalledWith('/network-setup/wifi') fireEvent.click(ethernetButton) - expect(mockPush).toHaveBeenCalledWith('/network-setup/ethernet') + expect(mockNavigate).toHaveBeenCalledWith('/network-setup/ethernet') fireEvent.click(usbButton) - expect(mockPush).toHaveBeenCalledWith('/network-setup/usb') + expect(mockNavigate).toHaveBeenCalledWith('/network-setup/usb') }) }) diff --git a/app/src/pages/ProtocolDashboard/LongPressModal.tsx b/app/src/pages/ProtocolDashboard/LongPressModal.tsx index a3287360b42..535b754259e 100644 --- a/app/src/pages/ProtocolDashboard/LongPressModal.tsx +++ b/app/src/pages/ProtocolDashboard/LongPressModal.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useDispatch, useSelector } from 'react-redux' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { Flex, Icon, SPACING, LegacyStyledText } from '@opentrons/components' import { useCreateRunMutation } from '@opentrons/react-api-client' @@ -28,7 +28,7 @@ export function LongPressModal({ setShowDeleteConfirmationModal, setTargetProtocolId, }: LongPressModalProps): JSX.Element { - const history = useHistory() + const navigate = useNavigate() let pinnedProtocolIds = useSelector(getPinnedProtocolIds) ?? [] const { i18n, t } = useTranslation(['protocol_info', 'shared']) const dispatch = useDispatch() @@ -49,7 +49,7 @@ export function LongPressModal({ const createRunUse = useCreateRunMutation({ onSuccess: data => { const runId: string = data.data.id - history.push(`/runs/${runId}/setup`) + navigate(`/runs/${runId}/setup`) }, }) const createRun = diff --git a/app/src/pages/ProtocolDashboard/PinnedProtocol.tsx b/app/src/pages/ProtocolDashboard/PinnedProtocol.tsx index d5e4a8c7347..6824a67d232 100644 --- a/app/src/pages/ProtocolDashboard/PinnedProtocol.tsx +++ b/app/src/pages/ProtocolDashboard/PinnedProtocol.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { formatDistance } from 'date-fns' import styled, { css } from 'styled-components' @@ -83,7 +83,7 @@ export function PinnedProtocol(props: PinnedProtocolProps): JSX.Element { isRequiredCSV = false, } = props const cardSize = size ?? 'full' - const history = useHistory() + const navigate = useNavigate() const longpress = useLongPress() const protocolName = protocol.metadata.protocolName ?? protocol.files[0].name const { t } = useTranslation('protocol_info') @@ -96,7 +96,7 @@ export function PinnedProtocol(props: PinnedProtocolProps): JSX.Element { protocolId: string ): void => { if (!longpress.isLongPressed) { - history.push(`/protocols/${protocolId}`) + navigate(`/protocols/${protocolId}`) } } React.useEffect(() => { diff --git a/app/src/pages/ProtocolDashboard/ProtocolCard.tsx b/app/src/pages/ProtocolDashboard/ProtocolCard.tsx index 22156779c42..f6acd4ed098 100644 --- a/app/src/pages/ProtocolDashboard/ProtocolCard.tsx +++ b/app/src/pages/ProtocolDashboard/ProtocolCard.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { Trans, useTranslation } from 'react-i18next' import { useQueryClient } from 'react-query' import { formatDistance } from 'date-fns' @@ -10,15 +10,15 @@ import { ALIGN_CENTER, ALIGN_END, BORDERS, + Chip, COLORS, DIRECTION_COLUMN, - DIRECTION_ROW, Flex, Icon, + LegacyStyledText, OVERFLOW_WRAP_ANYWHERE, OVERFLOW_WRAP_BREAK_WORD, SPACING, - LegacyStyledText, TYPOGRAPHY, useLongPress, } from '@opentrons/components' @@ -59,7 +59,7 @@ export function ProtocolCard(props: ProtocolCardProps): JSX.Element { setTargetProtocolId, setIsRequiredCSV, } = props - const history = useHistory() + const navigate = useNavigate() const [showIcon, setShowIcon] = React.useState(false) const [ showFailedAnalysisModal, @@ -120,7 +120,7 @@ export function ProtocolCard(props: ProtocolCardProps): JSX.Element { if (isFailedAnalysis) { setShowFailedAnalysisModal(true) } else if (!longpress.isLongPressed) { - history.push(`/protocols/${protocolId}`) + navigate(`/protocols/${protocolId}`) } } @@ -230,36 +230,14 @@ export function ProtocolCard(props: ProtocolCardProps): JSX.Element { gridGap={SPACING.spacing8} > {isFailedAnalysis ? ( - - - - {i18n.format(t('failed_analysis'), 'capitalize')} - - + ) : null} {isRequiredCSV ? ( - - - - {t('requires_csv')} - - + ) : null} { - const actual = await importOriginal() + const actual = await importOriginal() return { ...actual, - useHistory: () => ({ push: mockPush } as any), + useNavigate: () => mockNavigate, } }) vi.mock('@opentrons/components', async importOriginal => { @@ -119,7 +119,7 @@ describe('Pinned Protocol', () => { render(props) const name = screen.getByText('yay mock protocol') fireEvent.click(name) - expect(mockPush).toHaveBeenCalledWith('/protocols/mockProtocol1') + expect(mockNavigate).toHaveBeenCalledWith('/protocols/mockProtocol1') }) it('should display modal after long click', async () => { diff --git a/app/src/pages/ProtocolDashboard/__tests__/ProtocolCard.test.tsx b/app/src/pages/ProtocolDashboard/__tests__/ProtocolCard.test.tsx index 304087cf5bb..0e792cb8d70 100644 --- a/app/src/pages/ProtocolDashboard/__tests__/ProtocolCard.test.tsx +++ b/app/src/pages/ProtocolDashboard/__tests__/ProtocolCard.test.tsx @@ -14,24 +14,32 @@ import { i18n } from '../../../i18n' import { useFeatureFlag } from '../../../redux/config' import { ProtocolCard } from '../ProtocolCard' -import type * as ReactRouterDom from 'react-router-dom' +import type { NavigateFunction } from 'react-router-dom' import type { UseQueryResult } from 'react-query' import type { CompletedProtocolAnalysis, ProtocolResource, } from '@opentrons/shared-data' +import type { Chip } from '@opentrons/components' -const mockPush = vi.fn() +const mockNavigate = vi.fn() vi.mock('react-router-dom', async importOriginal => { - const actual = await importOriginal() + const actual = await importOriginal() return { ...actual, - useHistory: () => ({ push: mockPush } as any), + useNavigate: () => mockNavigate, } }) vi.mock('@opentrons/react-api-client') vi.mock('../../../redux/config') +vi.mock('@opentrons/components', async importOriginal => { + const actual = await importOriginal() + return { + ...actual, + Chip: vi.fn(() =>
    mock Chip
    ), + } +}) const mockProtocol: ProtocolResource = { id: 'mockProtocol1', @@ -107,7 +115,7 @@ describe('ProtocolCard', () => { const card = screen.getByTestId('protocol_card') expect(card).toHaveStyle(`background-color: ${COLORS.grey35}`) fireEvent.click(name) - expect(mockPush).toHaveBeenCalledWith('/protocols/mockProtocol1') + expect(mockNavigate).toHaveBeenCalledWith('/protocols/mockProtocol1') }) it('should display the analysis failed error modal when clicking on the protocol', () => { @@ -119,10 +127,8 @@ describe('ProtocolCard', () => { } as UseQueryResult) render(props) - screen.getByLabelText('failedAnalysis_icon') - screen.getByText('Failed analysis') fireEvent.click(screen.getByText('yay mock protocol')) - screen.getByText('Protocol analysis failed') + screen.getByText('mock Chip') screen.getByText( 'Delete the protocol, make changes to address the error, and resend the protocol to this robot from the Opentrons App.' ) @@ -164,8 +170,7 @@ describe('ProtocolCard', () => { vi.advanceTimersByTime(1005) }) expect(props.longPress).toHaveBeenCalled() - screen.getByLabelText('failedAnalysis_icon') - screen.getByText('Failed analysis') + screen.getByText('mock Chip') const card = screen.getByTestId('protocol_card') expect(card).toHaveStyle(`background-color: ${COLORS.red35}`) fireEvent.click(screen.getByText('yay mock protocol')) @@ -204,8 +209,7 @@ describe('ProtocolCard', () => { data: { result: 'parameter-value-required' } as any, } as UseQueryResult) render({ ...props, protocol: mockProtocolWithCSV }) - screen.getByLabelText('requiresCsv_file_icon') - screen.getByText('Requires CSV') + screen.getByText('mock Chip') const card = screen.getByTestId('protocol_card') expect(card).toHaveStyle(`background-color: ${COLORS.yellow35}`) }) diff --git a/app/src/pages/ProtocolDashboard/index.tsx b/app/src/pages/ProtocolDashboard/index.tsx index 455980f1eeb..79118a42860 100644 --- a/app/src/pages/ProtocolDashboard/index.tsx +++ b/app/src/pages/ProtocolDashboard/index.tsx @@ -9,10 +9,10 @@ import { DIRECTION_COLUMN, DIRECTION_ROW, Flex, + LegacyStyledText, POSITION_STATIC, POSITION_STICKY, SPACING, - LegacyStyledText, } from '@opentrons/components' import { useAllProtocolsQuery } from '@opentrons/react-api-client' diff --git a/app/src/pages/ProtocolDetails/__tests__/ProtocolDetails.test.tsx b/app/src/pages/ProtocolDetails/__tests__/ProtocolDetails.test.tsx index d4166d6a004..443677f7ae6 100644 --- a/app/src/pages/ProtocolDetails/__tests__/ProtocolDetails.test.tsx +++ b/app/src/pages/ProtocolDetails/__tests__/ProtocolDetails.test.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { vi, it, describe, expect, beforeEach, afterEach } from 'vitest' import { fireEvent, screen, waitFor } from '@testing-library/react' import { when } from 'vitest-when' -import { Route, MemoryRouter } from 'react-router-dom' +import { Route, MemoryRouter, Routes } from 'react-router-dom' import '@testing-library/jest-dom/vitest' import { renderWithProviders } from '../../../__testing-utils__' import { deleteProtocol, deleteRun, getProtocol } from '@opentrons/api-client' @@ -81,9 +81,9 @@ const MOCK_DATA = { const render = (path = '/protocols/fakeProtocolId') => { return renderWithProviders( - - - + + } /> + , { i18nInstance: i18n, diff --git a/app/src/pages/ProtocolDetails/index.tsx b/app/src/pages/ProtocolDetails/index.tsx index 6567f15e91b..cb7d979ea6f 100644 --- a/app/src/pages/ProtocolDetails/index.tsx +++ b/app/src/pages/ProtocolDetails/index.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next' import { useQueryClient } from 'react-query' import { deleteProtocol, deleteRun, getProtocol } from '@opentrons/api-client' import { useDispatch, useSelector } from 'react-redux' -import { useHistory, useParams } from 'react-router-dom' +import { useNavigate, useParams } from 'react-router-dom' import { ALIGN_CENTER, BORDERS, @@ -80,7 +80,7 @@ const ProtocolHeader = ({ isScrolled, isProtocolFetching, }: ProtocolHeaderProps): JSX.Element => { - const history = useHistory() + const navigate = useNavigate() const { t } = useTranslation(['protocol_info, protocol_details', 'shared']) const [truncate, setTruncate] = React.useState(true) const [startSetup, setStartSetup] = React.useState(false) @@ -115,7 +115,7 @@ const ProtocolHeader = ({ paddingLeft="0rem" paddingRight={SPACING.spacing24} onClick={() => { - history.push('/protocols') + navigate('/protocols') }} width="3rem" > @@ -309,7 +309,9 @@ export function ProtocolDetails(): JSX.Element | null { 'shared', ]) const enableCsvFile = useFeatureFlag('enableCsvFile') - const { protocolId } = useParams() + const { protocolId } = useParams< + keyof OnDeviceRouteParams + >() as OnDeviceRouteParams const { missingProtocolHardware, conflictedSlots, @@ -318,7 +320,7 @@ export function ProtocolDetails(): JSX.Element | null { const runTimeParameters = useRunTimeParameters(protocolId) const dispatch = useDispatch() - const history = useHistory() + const navigate = useNavigate() const host = useHost() const { makeSnackbar } = useToaster() const [showParameters, setShowParameters] = React.useState(false) @@ -426,11 +428,11 @@ export function ProtocolDetails(): JSX.Element | null { ) .then(() => deleteProtocol(host, protocolId)) .then(() => { - history.push('/protocols') + navigate('/protocols') }) .catch((e: Error) => { console.error(`error deleting resources: ${e.message}`) - history.push('/protocols') + navigate('/protocols') }) } else { console.error( diff --git a/app/src/pages/ProtocolSetup/__tests__/ProtocolSetup.test.tsx b/app/src/pages/ProtocolSetup/__tests__/ProtocolSetup.test.tsx index 30cfe51c947..1be58ae82f8 100644 --- a/app/src/pages/ProtocolSetup/__tests__/ProtocolSetup.test.tsx +++ b/app/src/pages/ProtocolSetup/__tests__/ProtocolSetup.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Route, MemoryRouter } from 'react-router-dom' +import { Route, MemoryRouter, Routes } from 'react-router-dom' import { fireEvent, screen } from '@testing-library/react' import { when } from 'vitest-when' import { vi, it, describe, expect, beforeEach, afterEach } from 'vitest' @@ -60,7 +60,7 @@ import { useNotifyDeckConfigurationQuery } from '../../../resources/deck_configu import type { UseQueryResult } from 'react-query' import type * as SharedData from '@opentrons/shared-data' -import type * as ReactRouterDom from 'react-router-dom' +import type { NavigateFunction } from 'react-router-dom' // Mock IntersectionObserver class IntersectionObserver { observe = vi.fn() @@ -74,7 +74,7 @@ Object.defineProperty(window, 'IntersectionObserver', { value: IntersectionObserver, }) -let mockHistoryPush = vi.fn() +let mockNavigate = vi.fn() vi.mock('@opentrons/shared-data', async importOriginal => { const sharedData = await importOriginal() @@ -85,12 +85,10 @@ vi.mock('@opentrons/shared-data', async importOriginal => { }) vi.mock('react-router-dom', async importOriginal => { - const reactRouterDom = await importOriginal() + const reactRouterDom = await importOriginal() return { ...reactRouterDom, - useHistory: () => ({ - push: mockHistoryPush, - }), + useNavigate: () => mockNavigate, } }) @@ -118,9 +116,9 @@ vi.mock('../../../resources/deck_configuration') const render = (path = '/') => { return renderWithProviders( - - - + + } /> + , { i18nInstance: i18n, @@ -193,7 +191,7 @@ describe('ProtocolSetup', () => { let mockLaunchLPC = vi.fn() beforeEach(() => { mockLaunchLPC = vi.fn() - mockHistoryPush = vi.fn() + mockNavigate = vi.fn() vi.mocked(useLPCDisabledReason).mockReturnValue(null) vi.mocked(useAttachedModules).mockReturnValue([]) vi.mocked(useModuleCalibrationStatus).mockReturnValue({ complete: true }) @@ -430,6 +428,6 @@ describe('ProtocolSetup', () => { it('should redirect to the protocols page when a run is stopped', () => { vi.mocked(useRunStatus).mockReturnValue(RUN_STATUS_STOPPED) render(`/runs/${RUN_ID}/setup/`) - expect(mockHistoryPush).toHaveBeenCalledWith('/protocols') + expect(mockNavigate).toHaveBeenCalledWith('/protocols') }) }) diff --git a/app/src/pages/ProtocolSetup/index.tsx b/app/src/pages/ProtocolSetup/index.tsx index 0956d9e9d5c..8954e7d0b01 100644 --- a/app/src/pages/ProtocolSetup/index.tsx +++ b/app/src/pages/ProtocolSetup/index.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import last from 'lodash/last' import { useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' -import { useHistory, useParams } from 'react-router-dom' +import { useNavigate, useParams } from 'react-router-dom' import first from 'lodash/first' import { css } from 'styled-components' @@ -102,9 +102,15 @@ import type { import type { ProtocolModuleInfo } from '../../organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo' const FETCH_DURATION_MS = 5000 + +export type ProtocolSetupStepStatus = + | 'ready' + | 'not ready' + | 'general' + | 'inform' interface ProtocolSetupStepProps { onClickSetupStep: () => void - status: 'ready' | 'not ready' | 'general' | 'inform' + status: ProtocolSetupStepStatus title: string // first line of detail text detail?: string | null @@ -276,7 +282,7 @@ function PrepareToRun({ runRecord, }: PrepareToRunProps): JSX.Element { const { t, i18n } = useTranslation(['protocol_setup', 'shared']) - const history = useHistory() + const navigate = useNavigate() const { makeSnackbar } = useToaster() const scrollRef = React.useRef(null) const [isScrolled, setIsScrolled] = React.useState(false) @@ -317,7 +323,7 @@ function PrepareToRun({ const runStatus = useRunStatus(runId) if (runStatus === RUN_STATUS_STOPPED) { - history.push('/protocols') + navigate('/protocols') } React.useEffect(() => { @@ -333,7 +339,7 @@ function PrepareToRun({ const onConfirmCancelClose = (): void => { setShowConfirmCancelModal(false) - history.goBack() + navigate(-1) } const protocolHasModules = @@ -826,7 +832,9 @@ export type SetupScreens = | 'view only parameters' export function ProtocolSetup(): JSX.Element { - const { runId } = useParams() + const { runId } = useParams< + keyof OnDeviceRouteParams + >() as OnDeviceRouteParams const { data: runRecord } = useNotifyRunQuery(runId, { staleTime: Infinity }) const { analysisErrors } = useProtocolAnalysisErrors(runId) const localRobot = useSelector(getLocalRobot) diff --git a/app/src/pages/Protocols/ProtocolDetails/ProtocolTimeline.tsx b/app/src/pages/Protocols/ProtocolDetails/ProtocolTimeline.tsx index e537b8b4b76..b8511ad4f87 100644 --- a/app/src/pages/Protocols/ProtocolDetails/ProtocolTimeline.tsx +++ b/app/src/pages/Protocols/ProtocolDetails/ProtocolTimeline.tsx @@ -12,7 +12,9 @@ import type { Dispatch, State } from '../../../redux/types' import type { DesktopRouteParams } from '../../../App/types' export function ProtocolTimeline(): JSX.Element { - const { protocolKey } = useParams() + const { protocolKey } = useParams< + keyof DesktopRouteParams + >() as DesktopRouteParams const dispatch = useDispatch() const storedProtocol = useSelector((state: State) => getStoredProtocol(state, protocolKey) diff --git a/app/src/pages/Protocols/ProtocolDetails/__tests__/ProtocolDetails.test.tsx b/app/src/pages/Protocols/ProtocolDetails/__tests__/ProtocolDetails.test.tsx index bb0a4d8f5e4..27e728dd844 100644 --- a/app/src/pages/Protocols/ProtocolDetails/__tests__/ProtocolDetails.test.tsx +++ b/app/src/pages/Protocols/ProtocolDetails/__tests__/ProtocolDetails.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { vi, it, describe, expect, beforeEach, afterEach } from 'vitest' import { screen } from '@testing-library/react' -import { Route, MemoryRouter } from 'react-router-dom' +import { Route, MemoryRouter, Routes } from 'react-router-dom' import { when } from 'vitest-when' import { renderWithProviders } from '../../../../__testing-utils__' @@ -34,12 +34,10 @@ const MOCK_STATE: State = { const render = (path = '/') => { return renderWithProviders( - - - - -
    protocols
    -
    + + } /> + protocols} /> +
    , { i18nInstance: i18n, diff --git a/app/src/pages/Protocols/ProtocolDetails/index.tsx b/app/src/pages/Protocols/ProtocolDetails/index.tsx index dc834092c5f..a75e3457540 100644 --- a/app/src/pages/Protocols/ProtocolDetails/index.tsx +++ b/app/src/pages/Protocols/ProtocolDetails/index.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { useParams, Redirect } from 'react-router-dom' +import { useParams, Navigate } from 'react-router-dom' import { useDispatch, useSelector } from 'react-redux' import { @@ -12,7 +12,9 @@ import type { Dispatch, State } from '../../../redux/types' import type { DesktopRouteParams } from '../../../App/types' export function ProtocolDetails(): JSX.Element { - const { protocolKey } = useParams() + const { protocolKey } = useParams< + keyof DesktopRouteParams + >() as DesktopRouteParams const dispatch = useDispatch() const storedProtocol = useSelector((state: State) => @@ -26,6 +28,6 @@ export function ProtocolDetails(): JSX.Element { return storedProtocol != null ? ( ) : ( - + ) } diff --git a/app/src/pages/QuickTransferDashboard/DeleteTransferConfirmationModal.tsx b/app/src/pages/QuickTransferDashboard/DeleteTransferConfirmationModal.tsx index 9916a576c1f..3bf006598b1 100644 --- a/app/src/pages/QuickTransferDashboard/DeleteTransferConfirmationModal.tsx +++ b/app/src/pages/QuickTransferDashboard/DeleteTransferConfirmationModal.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { useQueryClient } from 'react-query' import { useTranslation } from 'react-i18next' import styled from 'styled-components' @@ -35,7 +35,7 @@ export function DeleteTransferConfirmationModal({ setShowDeleteConfirmationModal, }: DeleteTransferConfirmationModalProps): JSX.Element { const { i18n, t } = useTranslation(['quick_transfer', 'shared']) - const history = useHistory() + const navigate = useNavigate() const { makeSnackbar } = useToaster() const [showIcon, setShowIcon] = React.useState(false) const modalHeader: ModalHeaderBaseProps = { @@ -77,11 +77,11 @@ export function DeleteTransferConfirmationModal({ .then(() => { setShowIcon(false) setShowDeleteConfirmationModal(false) - history.push('/quick-transfer') + navigate('/quick-transfer') makeSnackbar(t('deleted_transfer') as string) }) .catch((e: Error) => { - history.push('/quick-transfer') + navigate('/quick-transfer') console.error(`error deleting resources: ${e.message}`) }) } else { diff --git a/app/src/pages/QuickTransferDashboard/IntroductoryModal.tsx b/app/src/pages/QuickTransferDashboard/IntroductoryModal.tsx new file mode 100644 index 00000000000..560ceac280a --- /dev/null +++ b/app/src/pages/QuickTransferDashboard/IntroductoryModal.tsx @@ -0,0 +1,56 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { + SPACING, + StyledText, + Flex, + DIRECTION_COLUMN, + ALIGN_CENTER, + TEXT_ALIGN_CENTER, +} from '@opentrons/components' +import { Modal } from '../../molecules/Modal' +import { SmallButton } from '../../atoms/buttons' + +import imgSrc from '../../assets/images/on-device-display/odd-abstract-6.png' + +interface IntroductoryModalProps { + onClose: () => void +} + +export const IntroductoryModal = ( + props: IntroductoryModalProps +): JSX.Element => { + const { t } = useTranslation(['quick_transfer', 'shared']) + + return ( + + + {t('welcome_to_quick_transfer')} + + {t('welcome_to_quick_transfer')} + + + {t('a_way_to_move_liquid')} + + + + + + + ) +} diff --git a/app/src/pages/QuickTransferDashboard/LongPressModal.tsx b/app/src/pages/QuickTransferDashboard/LongPressModal.tsx index 0cdc777287d..b856d1296cf 100644 --- a/app/src/pages/QuickTransferDashboard/LongPressModal.tsx +++ b/app/src/pages/QuickTransferDashboard/LongPressModal.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useDispatch, useSelector } from 'react-redux' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { Flex, Icon, SPACING, LegacyStyledText } from '@opentrons/components' import { useCreateRunMutation } from '@opentrons/react-api-client' @@ -31,7 +31,7 @@ export function LongPressModal({ setShowDeleteConfirmationModal, setTargetTransferId, }: LongPressModalProps): JSX.Element { - const history = useHistory() + const navigate = useNavigate() let pinnedQuickTransferIds = useSelector(getPinnedQuickTransferIds) ?? [] const { i18n, t } = useTranslation(['quick_transfer', 'shared']) const dispatch = useDispatch() @@ -44,7 +44,7 @@ export function LongPressModal({ const { createRun } = useCreateRunMutation({ onSuccess: data => { const runId: string = data.data.id - history.push(`/runs/${runId}/setup`) + navigate(`/runs/${runId}/setup`) }, }) diff --git a/app/src/pages/QuickTransferDashboard/PinnedTransfer.tsx b/app/src/pages/QuickTransferDashboard/PinnedTransfer.tsx index 2ac60006813..cafcaa299a5 100644 --- a/app/src/pages/QuickTransferDashboard/PinnedTransfer.tsx +++ b/app/src/pages/QuickTransferDashboard/PinnedTransfer.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import styled, { css } from 'styled-components' import { @@ -71,7 +71,7 @@ export function PinnedTransfer(props: { setTargetTransferId, } = props const cardSize = props.cardSize ?? 'full' - const history = useHistory() + const navigate = useNavigate() const longpress = useLongPress() const transferName = transfer.metadata.protocolName ?? transfer.files[0].name @@ -80,7 +80,7 @@ export function PinnedTransfer(props: { transferId: string ): void => { if (!longpress.isLongPressed) { - history.push(`/quick-transfer/${transferId}`) + navigate(`/quick-transfer/${transferId}`) } } React.useEffect(() => { diff --git a/app/src/pages/QuickTransferDashboard/PipetteNotAttachedErrorModal.tsx b/app/src/pages/QuickTransferDashboard/PipetteNotAttachedErrorModal.tsx new file mode 100644 index 00000000000..a738a728139 --- /dev/null +++ b/app/src/pages/QuickTransferDashboard/PipetteNotAttachedErrorModal.tsx @@ -0,0 +1,56 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { + SPACING, + COLORS, + LegacyStyledText, + Flex, + DIRECTION_COLUMN, + TYPOGRAPHY, +} from '@opentrons/components' +import { Modal } from '../../molecules/Modal' +import { SmallButton } from '../../atoms/buttons' + +interface PipetteNotAttachedErrorModalProps { + onExit: () => void + onAttach: () => void +} + +export const PipetteNotAttachedErrorModal = ( + props: PipetteNotAttachedErrorModalProps +): JSX.Element => { + const { i18n, t } = useTranslation(['quick_transfer', 'shared', 'branded']) + + return ( + + + + {t('branded:attach_a_pipette_for_quick_transfer')} + + + + + + + + ) +} diff --git a/app/src/pages/QuickTransferDashboard/QuickTransferCard.tsx b/app/src/pages/QuickTransferDashboard/QuickTransferCard.tsx index 25b6983f9a2..7f273f30658 100644 --- a/app/src/pages/QuickTransferDashboard/QuickTransferCard.tsx +++ b/app/src/pages/QuickTransferDashboard/QuickTransferCard.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { Trans, useTranslation } from 'react-i18next' import { useQueryClient } from 'react-query' import last from 'lodash/last' @@ -53,7 +53,7 @@ export function QuickTransferCard(props: { setShowDeleteConfirmationModal, setTargetTransferId, } = props - const history = useHistory() + const navigate = useNavigate() const [showIcon, setShowIcon] = React.useState(false) const [ showFailedAnalysisModal, @@ -108,7 +108,7 @@ export function QuickTransferCard(props: { if (isFailedAnalysis) { setShowFailedAnalysisModal(true) } else if (!longpress.isLongPressed) { - history.push(`/quick-transfer/${transferId}`) + navigate(`/quick-transfer/${transferId}`) } } diff --git a/app/src/pages/QuickTransferDashboard/StorageLimitReachedErrorModal.tsx b/app/src/pages/QuickTransferDashboard/StorageLimitReachedErrorModal.tsx new file mode 100644 index 00000000000..d4c8a562306 --- /dev/null +++ b/app/src/pages/QuickTransferDashboard/StorageLimitReachedErrorModal.tsx @@ -0,0 +1,49 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { + SPACING, + COLORS, + LegacyStyledText, + Flex, + DIRECTION_COLUMN, + TYPOGRAPHY, +} from '@opentrons/components' +import { Modal } from '../../molecules/Modal' +import { SmallButton } from '../../atoms/buttons' + +interface StorageLimitReachedErrorModalProps { + onExit: () => void +} + +export const StorageLimitReachedErrorModal = ( + props: StorageLimitReachedErrorModalProps +): JSX.Element => { + const { i18n, t } = useTranslation(['quick_transfer', 'shared', 'branded']) + + return ( + + + + {t('branded:storage_limit_reached_description')} + + + + + + + ) +} diff --git a/app/src/pages/QuickTransferDashboard/__tests__/DeleteTransferConfirmationModal.test.tsx b/app/src/pages/QuickTransferDashboard/__tests__/DeleteTransferConfirmationModal.test.tsx index 7715f6efff8..177eb93a691 100644 --- a/app/src/pages/QuickTransferDashboard/__tests__/DeleteTransferConfirmationModal.test.tsx +++ b/app/src/pages/QuickTransferDashboard/__tests__/DeleteTransferConfirmationModal.test.tsx @@ -11,19 +11,19 @@ import { i18n } from '../../../i18n' import { useToaster } from '../../../organisms/ToasterOven' import { DeleteTransferConfirmationModal } from '../DeleteTransferConfirmationModal' -import type * as ReactRouterDom from 'react-router-dom' +import type { NavigateFunction } from 'react-router-dom' import type { HostConfig } from '@opentrons/api-client' -const mockPush = vi.fn() +const mockNavigate = vi.fn() vi.mock('@opentrons/api-client') vi.mock('@opentrons/react-api-client') vi.mock('../../../organisms/ToasterOven') vi.mock('react-router-dom', async importOriginal => { - const reactRouterDom = await importOriginal() + const reactRouterDom = await importOriginal() return { ...reactRouterDom, - useHistory: () => ({ push: mockPush } as any), + useNavigate: () => mockNavigate, } }) diff --git a/app/src/pages/QuickTransferDashboard/__tests__/PinnedTransfer.test.tsx b/app/src/pages/QuickTransferDashboard/__tests__/PinnedTransfer.test.tsx index 5ae26b3f1da..28588dbccb1 100644 --- a/app/src/pages/QuickTransferDashboard/__tests__/PinnedTransfer.test.tsx +++ b/app/src/pages/QuickTransferDashboard/__tests__/PinnedTransfer.test.tsx @@ -8,15 +8,15 @@ import { i18n } from '../../../i18n' import { PinnedTransfer } from '../PinnedTransfer' import type { ProtocolResource } from '@opentrons/shared-data' -import type * as ReactRouterDom from 'react-router-dom' +import type { NavigateFunction } from 'react-router-dom' -const mockPush = vi.fn() +const mockNavigate = vi.fn() vi.mock('react-router-dom', async importOriginal => { - const actual = await importOriginal() + const actual = await importOriginal() return { ...actual, - useHistory: () => ({ push: mockPush } as any), + useNavigate: () => mockNavigate, } }) @@ -63,7 +63,7 @@ describe('Pinned Transfer', () => { render() const name = screen.getByText('yay mock transfer') fireEvent.click(name) - expect(mockPush).toHaveBeenCalledWith('/quick-transfer/mockTransfer1') + expect(mockNavigate).toHaveBeenCalledWith('/quick-transfer/mockTransfer1') }) it('should display modal after long click', async () => { diff --git a/app/src/pages/QuickTransferDashboard/__tests__/QuickTransferCard.test.tsx b/app/src/pages/QuickTransferDashboard/__tests__/QuickTransferCard.test.tsx index de3a23700d9..6853233b08f 100644 --- a/app/src/pages/QuickTransferDashboard/__tests__/QuickTransferCard.test.tsx +++ b/app/src/pages/QuickTransferDashboard/__tests__/QuickTransferCard.test.tsx @@ -11,20 +11,20 @@ import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' import { QuickTransferCard } from '../QuickTransferCard' import { LongPressModal } from '../LongPressModal' -import type * as ReactRouterDom from 'react-router-dom' +import type { NavigateFunction } from 'react-router-dom' import type { UseQueryResult } from 'react-query' import type { CompletedProtocolAnalysis, ProtocolResource, } from '@opentrons/shared-data' -const mockPush = vi.fn() +const mockNavigate = vi.fn() vi.mock('react-router-dom', async importOriginal => { - const actual = await importOriginal() + const actual = await importOriginal() return { ...actual, - useHistory: () => ({ push: mockPush } as any), + useNavigate: () => mockNavigate, } }) vi.mock('@opentrons/react-api-client') @@ -81,7 +81,7 @@ describe('QuickTransferCard', () => { render() const name = screen.getByText('yay mock transfer') fireEvent.click(name) - expect(mockPush).toHaveBeenCalledWith('/quick-transfer/mockTransfer1') + expect(mockNavigate).toHaveBeenCalledWith('/quick-transfer/mockTransfer1') }) it('should display the analysis failed error modal when clicking on the transfer', () => { diff --git a/app/src/pages/QuickTransferDashboard/index.tsx b/app/src/pages/QuickTransferDashboard/index.tsx index 7d75986e757..b47d14f5ed8 100644 --- a/app/src/pages/QuickTransferDashboard/index.tsx +++ b/app/src/pages/QuickTransferDashboard/index.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { useDispatch, useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' @@ -16,28 +16,37 @@ import { SPACING, LegacyStyledText, } from '@opentrons/components' -import { useAllProtocolsQuery } from '@opentrons/react-api-client' +import { + useAllProtocolsQuery, + useInstrumentsQuery, +} from '@opentrons/react-api-client' import { SmallButton, FloatingActionButton } from '../../atoms/buttons' import { Navigation } from '../../organisms/Navigation' import { getPinnedQuickTransferIds, getQuickTransfersOnDeviceSortKey, + getHasDismissedQuickTransferIntro, updateConfigValue, } from '../../redux/config' import { PinnedTransferCarousel } from './PinnedTransferCarousel' import { sortQuickTransfers } from './utils' import { QuickTransferCard } from './QuickTransferCard' import { NoQuickTransfers } from './NoQuickTransfers' +import { PipetteNotAttachedErrorModal } from './PipetteNotAttachedErrorModal' +import { StorageLimitReachedErrorModal } from './StorageLimitReachedErrorModal' +import { IntroductoryModal } from './IntroductoryModal' import { DeleteTransferConfirmationModal } from './DeleteTransferConfirmationModal' import type { ProtocolResource } from '@opentrons/shared-data' +import type { PipetteData } from '@opentrons/api-client' import type { Dispatch } from '../../redux/types' import type { QuickTransfersOnDeviceSortKey } from '../../redux/config/types' export function QuickTransferDashboard(): JSX.Element { const protocols = useAllProtocolsQuery() - const history = useHistory() + const { data: attachedInstruments } = useInstrumentsQuery() + const navigate = useNavigate() const { t } = useTranslation(['quick_transfer', 'protocol_info']) const dispatch = useDispatch() const [navMenuIsOpened, setNavMenuIsOpened] = React.useState(false) @@ -49,8 +58,21 @@ export function QuickTransferDashboard(): JSX.Element { showDeleteConfirmationModal, setShowDeleteConfirmationModal, ] = React.useState(false) + const [ + showPipetteNotAttachedModal, + setShowPipetteNotAttaachedModal, + ] = React.useState(false) + const [ + showStorageLimitReachedModal, + setShowStorageLimitReachedModal, + ] = React.useState(false) const [targetTransferId, setTargetTransferId] = React.useState('') const sortBy = useSelector(getQuickTransfersOnDeviceSortKey) ?? 'alphabetical' + const hasDismissedIntro = useSelector(getHasDismissedQuickTransferIntro) + + const pipetteIsAttached = attachedInstruments?.data.some( + (i): i is PipetteData => i.ok && i.instrumentType === 'pipette' + ) const quickTransfersData = protocols.data?.data.filter(protocol => { return protocol.protocolKind === 'quick-transfer' @@ -123,14 +145,53 @@ export function QuickTransferDashboard(): JSX.Element { } } + const handleCreateNewQuickTransfer = (): void => { + if (!pipetteIsAttached) { + setShowPipetteNotAttaachedModal(true) + } else if (quickTransfersData.length >= 20) { + setShowStorageLimitReachedModal(true) + } else { + navigate('/quick-transfer/new') + } + } + return ( <> + {!hasDismissedIntro ? ( + + dispatch( + updateConfigValue( + 'protocols.hasDismissedQuickTransferIntro', + true + ) + ) + } + /> + ) : null} {showDeleteConfirmationModal ? ( ) : null} + {showPipetteNotAttachedModal ? ( + { + setShowPipetteNotAttaachedModal(false) + }} + onAttach={() => { + navigate('/instruments') + }} + /> + ) : null} + {showStorageLimitReachedModal ? ( + { + setShowStorageLimitReachedModal(false) + }} + /> + ) : null} { - history.push('/quick-transfer/new') - }} + onClick={handleCreateNewQuickTransfer} /> ) diff --git a/app/src/pages/QuickTransferDetails/__tests__/QuickTransferDetails.test.tsx b/app/src/pages/QuickTransferDetails/__tests__/QuickTransferDetails.test.tsx index f9a2c84c40c..929f5d46f82 100644 --- a/app/src/pages/QuickTransferDetails/__tests__/QuickTransferDetails.test.tsx +++ b/app/src/pages/QuickTransferDetails/__tests__/QuickTransferDetails.test.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { vi, it, describe, expect, beforeEach, afterEach } from 'vitest' import { fireEvent, screen } from '@testing-library/react' import { when } from 'vitest-when' -import { Route, MemoryRouter } from 'react-router-dom' +import { Route, MemoryRouter, Routes } from 'react-router-dom' import '@testing-library/jest-dom/vitest' import { renderWithProviders } from '../../../__testing-utils__' import { @@ -74,9 +74,12 @@ const MOCK_DATA = { const render = (path = '/quick-transfer/fakeTransferId') => { return renderWithProviders( - - - + + } + /> + , { i18nInstance: i18n, diff --git a/app/src/pages/QuickTransferDetails/index.tsx b/app/src/pages/QuickTransferDetails/index.tsx index 4bf429803b1..6742277aa95 100644 --- a/app/src/pages/QuickTransferDetails/index.tsx +++ b/app/src/pages/QuickTransferDetails/index.tsx @@ -3,7 +3,7 @@ import last from 'lodash/last' import { useTranslation } from 'react-i18next' import { useQueryClient } from 'react-query' import { useDispatch, useSelector } from 'react-redux' -import { useHistory, useParams } from 'react-router-dom' +import { useNavigate, useParams } from 'react-router-dom' import { ALIGN_CENTER, BORDERS, @@ -72,7 +72,7 @@ const QuickTransferHeader = ({ isScrolled, isTransferFetching, }: QuickTransferHeaderProps): JSX.Element => { - const history = useHistory() + const navigate = useNavigate() const { t } = useTranslation('protocol_details') const [truncate, setTruncate] = React.useState(true) const [startSetup, setStartSetup] = React.useState(false) @@ -107,7 +107,7 @@ const QuickTransferHeader = ({ paddingLeft="0rem" paddingRight={SPACING.spacing24} onClick={() => { - history.push('/quick-transfer') + navigate('/quick-transfer') }} width="3rem" > @@ -271,7 +271,9 @@ const TransferSectionContent = ({ export function QuickTransferDetails(): JSX.Element | null { const { t, i18n } = useTranslation(['quick_transfer', 'shared']) - const { quickTransferId: transferId } = useParams() + const { quickTransferId: transferId } = useParams< + keyof OnDeviceRouteParams + >() as OnDeviceRouteParams const { missingProtocolHardware, conflictedSlots, diff --git a/app/src/pages/RobotDashboard/__tests__/RobotDashboard.test.tsx b/app/src/pages/RobotDashboard/__tests__/RobotDashboard.test.tsx index f930ba4c6b7..df66762fdc9 100644 --- a/app/src/pages/RobotDashboard/__tests__/RobotDashboard.test.tsx +++ b/app/src/pages/RobotDashboard/__tests__/RobotDashboard.test.tsx @@ -19,15 +19,15 @@ import { RobotDashboard } from '..' import { useNotifyAllRunsQuery } from '../../../resources/runs' import type { ProtocolResource } from '@opentrons/shared-data' -import type * as ReactRouterDom from 'react-router-dom' +import type { NavigateFunction } from 'react-router-dom' -const mockPush = vi.fn() +const mockNavigate = vi.fn() vi.mock('react-router-dom', async importOriginal => { - const actual = await importOriginal() + const actual = await importOriginal() return { ...actual, - useHistory: () => ({ push: mockPush } as any), + useNavigate: () => mockNavigate, } }) vi.mock('@opentrons/react-api-client') diff --git a/app/src/pages/RunSummary/index.tsx b/app/src/pages/RunSummary/index.tsx index 7a57150ccab..348d0c6031f 100644 --- a/app/src/pages/RunSummary/index.tsx +++ b/app/src/pages/RunSummary/index.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useSelector } from 'react-redux' -import { useParams, useHistory } from 'react-router-dom' +import { useParams, useNavigate } from 'react-router-dom' import { useTranslation } from 'react-i18next' import styled, { css } from 'styled-components' @@ -68,9 +68,11 @@ import type { OnDeviceRouteParams } from '../../App/types' import type { PipetteWithTip } from '../../organisms/DropTipWizardFlows' export function RunSummary(): JSX.Element { - const { runId } = useParams() + const { runId } = useParams< + keyof OnDeviceRouteParams + >() as OnDeviceRouteParams const { t } = useTranslation('run_details') - const history = useHistory() + const navigate = useNavigate() const host = useHost() const { data: runRecord } = useNotifyRunQuery(runId, { staleTime: Infinity }) const isRunCurrent = Boolean(runRecord?.data?.current) @@ -144,7 +146,7 @@ export function RunSummary(): JSX.Element { const returnToDash = (): void => { closeCurrentRun() - history.push('/') + navigate('/') } // TODO(jh, 05-30-24): EXEC-487. Refactor reset() so we can redirect to the setup page, showing the shimmer skeleton instead. diff --git a/app/src/pages/RunningProtocol/__tests__/RunningProtocol.test.tsx b/app/src/pages/RunningProtocol/__tests__/RunningProtocol.test.tsx index d4d881a9e1e..1114f4964eb 100644 --- a/app/src/pages/RunningProtocol/__tests__/RunningProtocol.test.tsx +++ b/app/src/pages/RunningProtocol/__tests__/RunningProtocol.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Route, MemoryRouter } from 'react-router-dom' +import { Route, MemoryRouter, Routes } from 'react-router-dom' import { vi, it, describe, expect, beforeEach, afterEach } from 'vitest' import { when } from 'vitest-when' import { screen } from '@testing-library/react' @@ -9,6 +9,7 @@ import { RUN_STATUS_IDLE, RUN_STATUS_STOP_REQUESTED, RUN_STATUS_AWAITING_RECOVERY, + RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR, } from '@opentrons/api-client' import { useProtocolAnalysesQuery, @@ -81,9 +82,9 @@ const mockResumeRunFromRecovery = vi.fn() const render = (path = '/') => { return renderWithProviders( - - - + + } /> + ) } @@ -191,6 +192,14 @@ describe('RunningProtocol', () => { expect(vi.mocked(OpenDoorAlertModal)).toHaveBeenCalled() }) + it(`should render not open door alert modal, when run status is ${RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR}`, () => { + when(vi.mocked(useRunStatus)) + .calledWith(RUN_ID, { refetchInterval: 5000 }) + .thenReturn(RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR) + render(`/runs/${RUN_ID}/run`) + expect(vi.mocked(OpenDoorAlertModal)).not.toHaveBeenCalled() + }) + it(`should display a Run Paused splash screen if the run status is "${RUN_STATUS_AWAITING_RECOVERY}"`, () => { when(vi.mocked(useRunStatus)) .calledWith(RUN_ID, { refetchInterval: 5000 }) diff --git a/app/src/pages/RunningProtocol/index.tsx b/app/src/pages/RunningProtocol/index.tsx index 5c2377f473e..5240196e9e1 100644 --- a/app/src/pages/RunningProtocol/index.tsx +++ b/app/src/pages/RunningProtocol/index.tsx @@ -25,7 +25,6 @@ import { RUN_STATUS_STOP_REQUESTED, RUN_STATUS_BLOCKED_BY_OPEN_DOOR, RUN_STATUS_FINISHING, - RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR, } from '@opentrons/api-client' import { StepMeter } from '../../atoms/StepMeter' @@ -80,7 +79,9 @@ export type ScreenOption = | 'RunningProtocolCommandList' export function RunningProtocol(): JSX.Element { - const { runId } = useParams() + const { runId } = useParams< + keyof OnDeviceRouteParams + >() as OnDeviceRouteParams const [currentOption, setCurrentOption] = React.useState( 'CurrentRunningProtocolCommand' ) @@ -93,7 +94,7 @@ export function RunningProtocol(): JSX.Element { setInterventionModalCommandKey, ] = React.useState(null) const lastAnimatedCommand = React.useRef(null) - const swipe = useSwipe() + const { ref, style, swipeType, setSwipeType } = useSwipe() const robotSideAnalysis = useMostRecentCompletedAnalysis(runId) const lastRunCommand = useLastRunCommand(runId, { refetchInterval: LIVE_RUN_COMMANDS_POLL_MS, @@ -126,20 +127,20 @@ export function RunningProtocol(): JSX.Element { React.useEffect(() => { if ( currentOption === 'CurrentRunningProtocolCommand' && - swipe.swipeType === 'swipe-left' + swipeType === 'swipe-left' ) { setCurrentOption('RunningProtocolCommandList') - swipe.setSwipeType('') + setSwipeType('') } if ( currentOption === 'RunningProtocolCommandList' && - swipe.swipeType === 'swipe-right' + swipeType === 'swipe-right' ) { setCurrentOption('CurrentRunningProtocolCommand') - swipe.setSwipeType('') + setSwipeType('') } - }, [currentOption, swipe, swipe.setSwipeType]) + }, [currentOption, swipeType, setSwipeType]) React.useEffect(() => { if ( @@ -162,21 +163,26 @@ export function RunningProtocol(): JSX.Element { <> {isERActive ? ( ) : null} - {runStatus === RUN_STATUS_BLOCKED_BY_OPEN_DOOR || - runStatus === RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR ? ( + {runStatus === RUN_STATUS_BLOCKED_BY_OPEN_DOOR ? ( ) : null} {runStatus === RUN_STATUS_STOP_REQUESTED ? : null} + {/* note: this zindex is here to establish a zindex context for the bullets + so they're relatively-above this flex but not anything else like error + recovery + */} {robotSideAnalysis != null ? ( ) : null} diff --git a/app/src/pages/UpdateRobot/UpdateRobot.tsx b/app/src/pages/UpdateRobot/UpdateRobot.tsx index 810ff22bd40..413665365a0 100644 --- a/app/src/pages/UpdateRobot/UpdateRobot.tsx +++ b/app/src/pages/UpdateRobot/UpdateRobot.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useSelector, useDispatch } from 'react-redux' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { Flex, SPACING, DIRECTION_ROW } from '@opentrons/components' @@ -23,7 +23,7 @@ import { import type { State, Dispatch } from '../../redux/types' export function UpdateRobot(): JSX.Element { - const history = useHistory() + const navigate = useNavigate() const { i18n, t } = useTranslation(['device_settings', 'shared']) const localRobot = useSelector(getLocalRobot) const robotUpdateType = useSelector((state: State) => { @@ -48,7 +48,7 @@ export function UpdateRobot(): JSX.Element { buttonText={t('cancel_software_update')} onClick={() => { dispatch(clearRobotUpdateSession()) - history.goBack() + navigate(-1) }} /> + { + navigate(-1) + }} + /> ) : ( (true) - const history = useHistory() + const navigate = useNavigate() const { i18n, t } = useTranslation(['device_settings', 'shared']) const dispatchStartRobotUpdate = useDispatchStartRobotUpdate() const dispatch = useDispatch() @@ -86,7 +86,7 @@ export function UpdateRobotDuringOnboarding(): JSX.Element { buttonText={t('proceed_without_updating')} onClick={() => { dispatch(clearRobotUpdateSession()) - history.push('/emergency-stop') + navigate('/emergency-stop') }} /> { - history.push('/emergency-stop') + navigate('/emergency-stop') }} /> ) : ( diff --git a/app/src/pages/Welcome/__tests__/Welcome.test.tsx b/app/src/pages/Welcome/__tests__/Welcome.test.tsx index 4842206d807..756b7bcb4b5 100644 --- a/app/src/pages/Welcome/__tests__/Welcome.test.tsx +++ b/app/src/pages/Welcome/__tests__/Welcome.test.tsx @@ -8,16 +8,16 @@ import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' import { Welcome } from '..' -import type * as ReactRouterDom from 'react-router-dom' +import type { NavigateFunction } from 'react-router-dom' const PNG_FILE_NAME = 'welcome_background.png' -const mockPush = vi.fn() +const mockNavigate = vi.fn() vi.mock('react-router-dom', async importOriginal => { - const actual = await importOriginal() + const actual = await importOriginal() return { ...actual, - useHistory: () => ({ push: mockPush } as any), + useNavigate: () => mockNavigate, } }) @@ -44,9 +44,9 @@ describe('Welcome', () => { expect(image.getAttribute('src')).toContain(PNG_FILE_NAME) }) - it('should call mockPush when tapping Get started', () => { + it('should call mockNavigate when tapping Get started', () => { render() fireEvent.click(screen.getByRole('button', { name: 'Get started' })) - expect(mockPush).toHaveBeenCalledWith('/network-setup') + expect(mockNavigate).toHaveBeenCalledWith('/network-setup') }) }) diff --git a/app/src/pages/Welcome/index.tsx b/app/src/pages/Welcome/index.tsx index 867af7c3fd1..d43c9e9e054 100644 --- a/app/src/pages/Welcome/index.tsx +++ b/app/src/pages/Welcome/index.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { COLORS, DIRECTION_COLUMN, @@ -18,7 +18,7 @@ const IMAGE_ALT = 'Welcome screen background image' export function Welcome(): JSX.Element { const { t } = useTranslation(['device_settings', 'shared', 'branded']) - const history = useHistory() + const navigate = useNavigate() return ( { - history.push('/network-setup') + navigate('/network-setup') }} /> diff --git a/app/src/redux/reducer.ts b/app/src/redux/reducer.ts index 5bf33bb3b38..44831b0d70e 100644 --- a/app/src/redux/reducer.ts +++ b/app/src/redux/reducer.ts @@ -1,7 +1,4 @@ -import createHistory from 'history/createHashHistory' import { combineReducers } from 'redux' -import { connectRouter } from 'connected-react-router' -import type { RouterState } from 'connected-react-router' // api state import { robotApiReducer } from './robot-api/reducer' @@ -53,14 +50,8 @@ import { protocolStorageReducer } from './protocol-storage/reducer' import type { Reducer } from 'redux' import type { State, Action } from './types' -import type { History } from 'history' -export const history = createHistory() - -export const rootReducer: Reducer = combineReducers< - State, - Action ->({ +export const rootReducer: Reducer = combineReducers({ robotApi: robotApiReducer, robotAdmin: robotAdminReducer, robotControls: robotControlsReducer, @@ -77,7 +68,4 @@ export const rootReducer: Reducer = combineReducers< sessions: sessionReducer, calibration: calibrationReducer, protocolStorage: protocolStorageReducer, - router: connectRouter( - history as History> - ) as Reducer, }) diff --git a/app/src/redux/store.ts b/app/src/redux/store.ts index 0567386b313..0572d875655 100644 --- a/app/src/redux/store.ts +++ b/app/src/redux/store.ts @@ -1,22 +1,17 @@ import { createStore, applyMiddleware, compose } from 'redux' import thunk from 'redux-thunk' -import { routerMiddleware } from 'connected-react-router' import { createEpicMiddleware } from 'redux-observable' -import { rootReducer, history } from './reducer' +import { rootReducer } from './reducer' import { rootEpic } from './epic' import type { StoreEnhancer } from 'redux' -import type { Action, Middleware, State } from './types' +import type { Action, State } from './types' const epicMiddleware = createEpicMiddleware() -const middleware = applyMiddleware( - thunk, - epicMiddleware, - routerMiddleware(history) as Middleware -) +const middleware = applyMiddleware(thunk, epicMiddleware) const composeEnhancers = (window as any)?.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__?.({ maxAge: 200 }) ?? diff --git a/app/src/redux/types.ts b/app/src/redux/types.ts index a46c7f2dd96..9ed69c3e71f 100644 --- a/app/src/redux/types.ts +++ b/app/src/redux/types.ts @@ -1,7 +1,7 @@ /* eslint-disable no-use-before-define */ // application types import type { Store as ReduxStore, Dispatch as ReduxDispatch } from 'redux' -import type { RouterState, RouterAction } from 'connected-react-router' +import type { RouterAction } from 'connected-react-router' import type { Observable } from 'rxjs' import type { RobotApiState, RobotApiAction } from './robot-api/types' @@ -54,7 +54,6 @@ export interface State { readonly sessions: SessionState readonly calibration: CalibrationState readonly protocolStorage: ProtocolStorageState - readonly router: RouterState } export type Action = diff --git a/app/src/resources/instruments/hooks.ts b/app/src/resources/instruments/hooks.ts index 31a40f5fdd0..32135d716a4 100644 --- a/app/src/resources/instruments/hooks.ts +++ b/app/src/resources/instruments/hooks.ts @@ -2,6 +2,7 @@ import { getGripperDisplayName, getPipetteModelSpecs, getPipetteNameSpecs, + getPipetteSpecsV2, GRIPPER_MODELS, } from '@opentrons/shared-data' import { useIsOEMMode } from '../robot-settings/hooks' @@ -12,6 +13,7 @@ import type { PipetteModelSpecs, PipetteName, PipetteNameSpecs, + PipetteV2Specs, } from '@opentrons/shared-data' export function usePipetteNameSpecs( @@ -46,6 +48,22 @@ export function usePipetteModelSpecs( return { ...modelSpecificFields, displayName: pipetteNameSpecs.displayName } } +export function usePipetteSpecsV2( + name?: PipetteName | PipetteModel +): PipetteV2Specs | null { + const isOEMMode = useIsOEMMode() + const pipetteSpecs = getPipetteSpecsV2(name) + + if (pipetteSpecs == null) return null + + const brandedDisplayName = pipetteSpecs.displayName + const anonymizedDisplayName = pipetteSpecs.displayName.replace('Flex ', '') + + const displayName = isOEMMode ? anonymizedDisplayName : brandedDisplayName + + return { ...pipetteSpecs, displayName } +} + export function useGripperDisplayName(gripperModel: GripperModel): string { const isOEMMode = useIsOEMMode() diff --git a/components/package.json b/components/package.json index c0090f1503d..15306b92671 100644 --- a/components/package.json +++ b/components/package.json @@ -19,9 +19,7 @@ "homepage": "https://github.com/Opentrons/opentrons#readme", "peerDependencies": { "react": "18.2.0", - "react-dom": "18.2.0", - "react-router-dom": "5.3.4", - "@types/react-router-dom": "5.3.3" + "react-dom": "18.2.0" }, "dependencies": { "@opentrons/shared-data": "link:../shared-data", @@ -33,12 +31,11 @@ "@types/styled-components": "^5.1.26", "@types/webpack-env": "^1.16.0", "classnames": "2.2.5", - "interactjs": "^1.10.17", + "interactjs": "^1.10.27", "lodash": "4.17.21", "react-i18next": "13.5.0", "react-popper": "1.0.0", "react-remove-scroll": "2.4.3", - "react-router-dom": "5.3.4", "react-select": "5.4.0", "redux": "4.0.5", "styled-components": "5.3.6" diff --git a/components/src/atoms/StyledText/StyledText.tsx b/components/src/atoms/StyledText/StyledText.tsx index c4796c1f74d..3bb124a3def 100644 --- a/components/src/atoms/StyledText/StyledText.tsx +++ b/components/src/atoms/StyledText/StyledText.tsx @@ -31,6 +31,14 @@ const helixProductStyleMap = { } `, }, + headingMediumBold: { + as: 'h3', + style: css` + @media not (${RESPONSIVENESS.touchscreenMediaQuerySpecs}) { + font: ${HELIX_TYPOGRAPHY.fontStyleHeadingMediumBold}; + } + `, + }, headingMediumSemiBold: { as: 'h3', style: css` @@ -55,6 +63,14 @@ const helixProductStyleMap = { } `, }, + headingSmallSemiBold: { + as: 'h4', + style: css` + @media not (${RESPONSIVENESS.touchscreenMediaQuerySpecs}) { + font: ${HELIX_TYPOGRAPHY.fontStyleHeadingSmallBold}; + } + `, + }, bodyLargeSemiBold: { as: 'p', style: css` @@ -111,6 +127,14 @@ const helixProductStyleMap = { } `, }, + hidden: { + as: 'none', + style: css` + @media not (${RESPONSIVENESS.touchscreenMediaQuerySpecs}) { + display: none; + } + `, + }, } as const const ODDStyleMap = { @@ -163,7 +187,6 @@ const ODDStyleMap = { } `, }, - level3HeaderBold: { as: 'h3', style: css` @@ -249,6 +272,14 @@ const ODDStyleMap = { } `, }, + hidden: { + as: 'none', + style: css` + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + display: none; + } + `, + }, } as const export interface Props extends React.ComponentProps { diff --git a/components/src/atoms/StyledText/__tests__/StyledText.test.tsx b/components/src/atoms/StyledText/__tests__/StyledText.test.tsx deleted file mode 100644 index b6e0a3909f2..00000000000 --- a/components/src/atoms/StyledText/__tests__/StyledText.test.tsx +++ /dev/null @@ -1,290 +0,0 @@ -import * as React from 'react' -import { describe, it, expect } from 'vitest' -import '@testing-library/jest-dom/vitest' -import { screen } from '@testing-library/react' -import { TYPOGRAPHY } from '../../../ui-style-constants' -import { renderWithProviders } from '../../../testing/utils' -import { LegacyStyledText } from '..' - -const render = (props: React.ComponentProps) => { - return renderWithProviders()[0] -} - -describe('StyledText', () => { - let props: React.ComponentProps - // testing styles (font size, font weight, and line height) - it('should render h1 default style', () => { - props = { - as: 'h1', - children: 'h1Default', - } - render(props) - expect(screen.getByText('h1Default')).toHaveStyle( - `fontSize: ${TYPOGRAPHY.fontSizeH1}` - ) - expect(screen.getByText('h1Default')).toHaveStyle( - `fontWeight: ${TYPOGRAPHY.fontWeightSemiBold}` - ) - expect(screen.getByText('h1Default')).toHaveStyle( - `lineHeight: ${TYPOGRAPHY.lineHeight24}` - ) - }) - - it('should render h2 regular style', () => { - props = { - as: 'h2', - children: 'h2Regular', - } - render(props) - expect(screen.getByText('h2Regular')).toHaveStyle( - `fontSize: ${TYPOGRAPHY.fontSizeH2}` - ) - expect(screen.getByText('h2Regular')).toHaveStyle( - `fontWeight: ${TYPOGRAPHY.fontWeightRegular}` - ) - expect(screen.getByText('h2Regular')).toHaveStyle( - `lineHeight: ${TYPOGRAPHY.lineHeight20}` - ) - }) - - it('should render h3 regular style', () => { - props = { - as: 'h3', - children: 'h3Regular', - } - render(props) - expect(screen.getByText('h3Regular')).toHaveStyle( - `fontSize: ${TYPOGRAPHY.fontSizeH3}` - ) - expect(screen.getByText('h3Regular')).toHaveStyle( - `fontWeight: ${TYPOGRAPHY.fontWeightRegular}` - ) - expect(screen.getByText('h3Regular')).toHaveStyle( - `lineHeight: ${TYPOGRAPHY.lineHeight20}` - ) - }) - - it('should render h6 default style', () => { - props = { - as: 'h6', - children: 'h6Default', - } - render(props) - expect(screen.getByText('h6Default')).toHaveStyle( - `fontSize: ${TYPOGRAPHY.fontSizeH6}` - ) - expect(screen.getByText('h6Default')).toHaveStyle( - `fontWeight: ${TYPOGRAPHY.fontWeightRegular}` - ) - expect(screen.getByText('h6Default')).toHaveStyle( - `lineHeight: ${TYPOGRAPHY.lineHeight12}` - ) - expect(screen.getByText('h6Default')).toHaveStyle( - `textTransform: ${TYPOGRAPHY.textTransformUppercase}` - ) - }) - - it('should render p regular style', () => { - props = { - as: 'p', - children: 'pRegular', - } - render(props) - expect(screen.getByText('pRegular')).toHaveStyle( - `fontSize: ${TYPOGRAPHY.fontSizeP}` - ) - expect(screen.getByText('pRegular')).toHaveStyle( - `fontWeight: ${TYPOGRAPHY.fontWeightRegular}` - ) - expect(screen.getByText('pRegular')).toHaveStyle( - `lineHeight: ${TYPOGRAPHY.lineHeight20}` - ) - }) - - it('should render label regular style', () => { - props = { - as: 'label', - children: 'labelRegular', - } - render(props) - expect(screen.getByText('labelRegular')).toHaveStyle( - `fontSize: ${TYPOGRAPHY.fontSizeLabel}` - ) - expect(screen.getByText('labelRegular')).toHaveStyle( - `fontWeight: ${TYPOGRAPHY.fontWeightRegular}` - ) - expect(screen.getByText('labelRegular')).toHaveStyle( - `lineHeight: ${TYPOGRAPHY.lineHeight12}` - ) - }) - - it('should render h2 semibold style', () => { - props = { - as: 'h2SemiBold', - children: 'h2SemiBold', - } - render(props) - expect(screen.getByText('h2SemiBold')).toHaveStyle( - `fontSize: ${TYPOGRAPHY.fontSizeH2}` - ) - expect(screen.getByText('h2SemiBold')).toHaveStyle( - `fontWeight: ${TYPOGRAPHY.fontWeightSemiBold}` - ) - expect(screen.getByText('h2SemiBold')).toHaveStyle( - `lineHeight: ${TYPOGRAPHY.lineHeight20}` - ) - }) - - it('should render h3 semibold style', () => { - props = { - as: 'h3SemiBold', - children: 'h3SemiBold', - } - render(props) - expect(screen.getByText('h3SemiBold')).toHaveStyle( - `fontSize: ${TYPOGRAPHY.fontSizeH3}` - ) - expect(screen.getByText('h3SemiBold')).toHaveStyle( - `fontWeight: ${TYPOGRAPHY.fontWeightSemiBold}` - ) - expect(screen.getByText('h3SemiBold')).toHaveStyle( - `lineHeight: ${TYPOGRAPHY.lineHeight20}` - ) - }) - - it('should render h6 semibold style', () => { - props = { - as: 'h6SemiBold', - children: 'h6SemiBold', - } - render(props) - expect(screen.getByText('h6SemiBold')).toHaveStyle( - `fontSize: ${TYPOGRAPHY.fontSizeH6}` - ) - expect(screen.getByText('h6SemiBold')).toHaveStyle( - `fontWeight: ${TYPOGRAPHY.fontWeightSemiBold}` - ) - expect(screen.getByText('h6SemiBold')).toHaveStyle( - `lineHeight: ${TYPOGRAPHY.lineHeight12}` - ) - }) - - it('should render p semibold style', () => { - props = { - as: 'pSemiBold', - children: 'pSemiBold', - } - render(props) - expect(screen.getByText('pSemiBold')).toHaveStyle( - `fontSize: ${TYPOGRAPHY.fontSizeP}` - ) - expect(screen.getByText('pSemiBold')).toHaveStyle( - `fontWeight: ${TYPOGRAPHY.fontWeightSemiBold}` - ) - expect(screen.getByText('pSemiBold')).toHaveStyle( - `lineHeight: ${TYPOGRAPHY.lineHeight20}` - ) - }) - - it('should render label semibold style', () => { - props = { - as: 'labelSemiBold', - children: 'labelSemiBold', - } - render(props) - expect(screen.getByText('labelSemiBold')).toHaveStyle( - `fontSize: ${TYPOGRAPHY.fontSizeLabel}` - ) - expect(screen.getByText('labelSemiBold')).toHaveStyle( - `fontWeight: ${TYPOGRAPHY.fontWeightSemiBold}` - ) - expect(screen.getByText('labelSemiBold')).toHaveStyle( - `lineHeight: ${TYPOGRAPHY.lineHeight12}` - ) - }) - - it('should render header level 2 bold style', () => { - props = { - as: 'h2Bold', - children: 'h2Bold', - } - render(props) - expect(screen.getByText('h2Bold')).toHaveStyle( - `fontSize: ${TYPOGRAPHY.fontSize38}` - ) - expect(screen.getByText('h2Bold')).toHaveStyle( - `fontWeight: ${TYPOGRAPHY.fontWeightBold}` - ) - expect(screen.getByText('h2Bold')).toHaveStyle( - `lineHeight: ${TYPOGRAPHY.lineHeight48}` - ) - }) - - it('should render header level 3 bold style', () => { - props = { - as: 'h3Bold', - children: 'h3Bold', - } - render(props) - expect(screen.getByText('h3Bold')).toHaveStyle( - `fontSize: ${TYPOGRAPHY.fontSize32}` - ) - expect(screen.getByText('h3Bold')).toHaveStyle( - `fontWeight: ${TYPOGRAPHY.fontWeightBold}` - ) - expect(screen.getByText('h3Bold')).toHaveStyle( - `lineHeight: ${TYPOGRAPHY.lineHeight42}` - ) - }) - - it('should render header level 4 bold style', () => { - props = { - as: 'h4Bold', - children: 'h4Bold', - } - render(props) - expect(screen.getByText('h4Bold')).toHaveStyle( - `fontSize: ${TYPOGRAPHY.fontSize28}` - ) - expect(screen.getByText('h4Bold')).toHaveStyle( - `fontWeight: ${TYPOGRAPHY.fontWeightBold}` - ) - expect(screen.getByText('h4Bold')).toHaveStyle( - `lineHeight: ${TYPOGRAPHY.lineHeight36}` - ) - }) - - it('should render p bold style - bodyText bold', () => { - props = { - as: 'pBold', - children: 'pBold', - } - render(props) - expect(screen.getByText('pBold')).toHaveStyle( - `fontSize: ${TYPOGRAPHY.fontSize22}` - ) - expect(screen.getByText('pBold')).toHaveStyle( - `fontWeight: ${TYPOGRAPHY.fontWeightBold}` - ) - expect(screen.getByText('pBold')).toHaveStyle( - `lineHeight: ${TYPOGRAPHY.lineHeight28}` - ) - }) - - it('should render label bold style - smallBodyText bold', () => { - props = { - as: 'labelBold', - children: 'labelBold', - } - render(props) - expect(screen.getByText('labelBold')).toHaveStyle( - `fontSize: ${TYPOGRAPHY.fontSize20}` - ) - expect(screen.getByText('labelBold')).toHaveStyle( - `fontWeight: ${TYPOGRAPHY.fontWeightBold}` - ) - expect(screen.getByText('labelBold')).toHaveStyle( - `lineHeight: ${TYPOGRAPHY.lineHeight24}` - ) - }) -}) diff --git a/components/src/forms/RadioGroup.tsx b/components/src/forms/RadioGroup.tsx index d934616a227..5d409540032 100644 --- a/components/src/forms/RadioGroup.tsx +++ b/components/src/forms/RadioGroup.tsx @@ -50,7 +50,7 @@ export function RadioGroup(props: RadioGroupProps): JSX.Element { const useStyleUpdates = props.useBlueChecked && radio.value === props.value return ( -
  • ` with an optional icon, and an optional url for a React Router `NavLink` - * - */ -export const ListItem = React.forwardRef( - (props: ListItemProps, ref: React.ForwardedRef) => { - const { url, isDisabled, iconName, activeClassName, exact } = props - const onClick = props.onClick && !isDisabled ? props.onClick : undefined - // @ts-expect-error(sa, 2021-6-23): cast value to boolean - const className = classnames(props.className, styles.list_item, { - [styles.disabled]: isDisabled, - [styles.clickable]: onClick, - }) - - const itemIcon = iconName && ( - - ) - - if (url != null) { - return ( -
  • - - {itemIcon} - {props.children} - -
  • - ) - } - - return ( -
  • - {itemIcon} - {props.children} -
  • - ) - } -) diff --git a/components/src/lists/index.ts b/components/src/lists/index.ts index cd2586912f3..c11be61e41a 100644 --- a/components/src/lists/index.ts +++ b/components/src/lists/index.ts @@ -1,4 +1,3 @@ // list and list item components export * from './SidePanelGroup' export * from './TitledList' -export * from './ListItem' diff --git a/components/src/molecules/LocationIcon/__tests__/LocationIcon.test.tsx b/components/src/molecules/LocationIcon/__tests__/LocationIcon.test.tsx index 120a05096dd..1750d594d1d 100644 --- a/components/src/molecules/LocationIcon/__tests__/LocationIcon.test.tsx +++ b/components/src/molecules/LocationIcon/__tests__/LocationIcon.test.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { describe, it, beforeEach, expect } from 'vitest' import { renderWithProviders } from '../../../testing/utils' import { screen } from '@testing-library/react' -import { SPACING } from '../../../ui-style-constants' +import { SPACING, TYPOGRAPHY } from '../../../ui-style-constants' import { BORDERS, COLORS } from '../../../helix-design-system' import { LocationIcon } from '..' @@ -36,7 +36,10 @@ describe('LocationIcon', () => { it('should render slot name', () => { render(props) - screen.getByText('A1') + const text = screen.getByText('A1') + expect(text).toHaveStyle(`font-size: ${TYPOGRAPHY.fontSizeCaption}`) + expect(text).toHaveStyle('line-height: normal') + expect(text).toHaveStyle(` font-weight: ${TYPOGRAPHY.fontWeightBold}`) }) it('should render an icon', () => { diff --git a/components/src/molecules/LocationIcon/index.tsx b/components/src/molecules/LocationIcon/index.tsx index 773efbdbbef..6a922f155c0 100644 --- a/components/src/molecules/LocationIcon/index.tsx +++ b/components/src/molecules/LocationIcon/index.tsx @@ -37,7 +37,7 @@ const LOCATION_ICON_STYLE = css<{ padding: ${SPACING.spacing2} ${SPACING.spacing4}; border-radius: ${BORDERS.borderRadius4}; justify-content: ${JUSTIFY_CENTER}; - height: max-content; + height: max-content; // prevents the icon from being squished @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { border: 2px solid ${props => props.color ?? COLORS.black90}; @@ -49,7 +49,13 @@ const LOCATION_ICON_STYLE = css<{ ` const SLOT_NAME_TEXT_STYLE = css` - ${TYPOGRAPHY.smallBodyTextBold} + font-size: ${TYPOGRAPHY.fontSizeCaption}; + line-height: normal; + font-weight: ${TYPOGRAPHY.fontWeightBold}; + + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + ${TYPOGRAPHY.smallBodyTextBold} + } ` export function LocationIcon({ @@ -71,7 +77,7 @@ export function LocationIcon({ {iconName != null ? ( diff --git a/components/src/structure/PageTabs.tsx b/components/src/structure/PageTabs.tsx deleted file mode 100644 index 3475326e423..00000000000 --- a/components/src/structure/PageTabs.tsx +++ /dev/null @@ -1,46 +0,0 @@ -// page tabs bar - -import * as React from 'react' -import classnames from 'classnames' -import { Link } from 'react-router-dom' - -import styles from './structure.module.css' - -// TODO(bc, 2021-03-29): this component is only used in RA -// reconsider whether it belongs in components library -interface TabProps { - title: string - href: string - isActive: boolean - isDisabled: boolean -} - -export interface PageTabProps { - pages: TabProps[] -} - -export function PageTabs(props: PageTabProps): JSX.Element { - return ( - - ) -} - -function Tab(props: TabProps): JSX.Element { - const { isDisabled } = props - const tabLinkClass = classnames(styles.tab_link, { - [styles.active_tab_link]: props.isActive, - }) - - // TODO(mc, 2017-12-14): make a component for proper disabling of links - const MaybeLink: any = !isDisabled ? Link : 'span' - - return ( - -

    {props.title}

    -
    - ) -} diff --git a/components/src/structure/index.ts b/components/src/structure/index.ts index 90dbf88700e..371cfc731d6 100644 --- a/components/src/structure/index.ts +++ b/components/src/structure/index.ts @@ -1,6 +1,5 @@ // structure components -export * from './PageTabs' export * from './TitleBar' export * from './Card' export * from './Splash' diff --git a/components/src/tabbedNav/index.ts b/components/src/tabbedNav/index.ts deleted file mode 100644 index 0b670948e40..00000000000 --- a/components/src/tabbedNav/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -// navigational components - -// TODO(bc, 2021-03-29): these components are only used in one place -// reconsider whether they belong in components library -export * from './TabbedNavBar' -export * from './NavTab' -export * from './OutsideLinkTab' diff --git a/hardware-testing/Pipfile b/hardware-testing/Pipfile index 4f9d82e5964..1cbf12ae8b6 100644 --- a/hardware-testing/Pipfile +++ b/hardware-testing/Pipfile @@ -10,6 +10,7 @@ opentrons-hardware = {editable = true, path = "./../hardware", extras=['FLEX']} hardware-testing = { editable = true, path = "." } abr-testing = { editable = true, path = "./../abr-testing" } pyserial = "==3.5" +types-pytz = "*" [dev-packages] atomicwrites = "==1.4.1" diff --git a/hardware-testing/Pipfile.lock b/hardware-testing/Pipfile.lock index 7b9381ef39e..026dae40f4b 100644 --- a/hardware-testing/Pipfile.lock +++ b/hardware-testing/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "1e609e94df92fa225c1352401ecd3c21e2c7ec319754ae3f209155553712704f" + "sha256": "aa401ceace7aebaa4a5a7727066f35d36bb28179ae1850fbdffda93edf48e7b0" }, "pipfile-spec": 6, "requires": { @@ -25,6 +25,7 @@ "sha256:25816a9eef030c774beaee22189a24e29bc43f81cebe574ef723851eaf89ddee", "sha256:9651e1373873c75786101330e302e114f85b6e8b5ad70b491497c8b3609a8449" ], + "markers": "python_version >= '3.8'", "version": "==0.3.1" }, "anyio": { @@ -51,13 +52,21 @@ "markers": "python_version >= '3.7'", "version": "==8.1.7" }, + "colorama": { + "hashes": [ + "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", + "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" + ], + "markers": "platform_system == 'Windows'", + "version": "==0.4.6" + }, "exceptiongroup": { "hashes": [ - "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad", - "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16" + "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", + "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc" ], "markers": "python_version < '3.11'", - "version": "==1.2.1" + "version": "==1.2.2" }, "hardware-testing": { "editable": true, @@ -79,68 +88,6 @@ "markers": "python_version >= '3.7'", "version": "==4.17.3" }, - "msgpack": { - "hashes": [ - "sha256:00e073efcba9ea99db5acef3959efa45b52bc67b61b00823d2a1a6944bf45982", - "sha256:0726c282d188e204281ebd8de31724b7d749adebc086873a59efb8cf7ae27df3", - "sha256:0ceea77719d45c839fd73abcb190b8390412a890df2f83fb8cf49b2a4b5c2f40", - "sha256:114be227f5213ef8b215c22dde19532f5da9652e56e8ce969bf0a26d7c419fee", - "sha256:13577ec9e247f8741c84d06b9ece5f654920d8365a4b636ce0e44f15e07ec693", - "sha256:1876b0b653a808fcd50123b953af170c535027bf1d053b59790eebb0aeb38950", - "sha256:1ab0bbcd4d1f7b6991ee7c753655b481c50084294218de69365f8f1970d4c151", - "sha256:1cce488457370ffd1f953846f82323cb6b2ad2190987cd4d70b2713e17268d24", - "sha256:26ee97a8261e6e35885c2ecd2fd4a6d38252246f94a2aec23665a4e66d066305", - "sha256:3528807cbbb7f315bb81959d5961855e7ba52aa60a3097151cb21956fbc7502b", - "sha256:374a8e88ddab84b9ada695d255679fb99c53513c0a51778796fcf0944d6c789c", - "sha256:376081f471a2ef24828b83a641a02c575d6103a3ad7fd7dade5486cad10ea659", - "sha256:3923a1778f7e5ef31865893fdca12a8d7dc03a44b33e2a5f3295416314c09f5d", - "sha256:4916727e31c28be8beaf11cf117d6f6f188dcc36daae4e851fee88646f5b6b18", - "sha256:493c5c5e44b06d6c9268ce21b302c9ca055c1fd3484c25ba41d34476c76ee746", - "sha256:505fe3d03856ac7d215dbe005414bc28505d26f0c128906037e66d98c4e95868", - "sha256:5845fdf5e5d5b78a49b826fcdc0eb2e2aa7191980e3d2cfd2a30303a74f212e2", - "sha256:5c330eace3dd100bdb54b5653b966de7f51c26ec4a7d4e87132d9b4f738220ba", - "sha256:5dbf059fb4b7c240c873c1245ee112505be27497e90f7c6591261c7d3c3a8228", - "sha256:5e390971d082dba073c05dbd56322427d3280b7cc8b53484c9377adfbae67dc2", - "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273", - "sha256:64d0fcd436c5683fdd7c907eeae5e2cbb5eb872fafbc03a43609d7941840995c", - "sha256:69284049d07fce531c17404fcba2bb1df472bc2dcdac642ae71a2d079d950653", - "sha256:6a0e76621f6e1f908ae52860bdcb58e1ca85231a9b0545e64509c931dd34275a", - "sha256:73ee792784d48aa338bba28063e19a27e8d989344f34aad14ea6e1b9bd83f596", - "sha256:74398a4cf19de42e1498368c36eed45d9528f5fd0155241e82c4082b7e16cffd", - "sha256:7938111ed1358f536daf311be244f34df7bf3cdedb3ed883787aca97778b28d8", - "sha256:82d92c773fbc6942a7a8b520d22c11cfc8fd83bba86116bfcf962c2f5c2ecdaa", - "sha256:83b5c044f3eff2a6534768ccfd50425939e7a8b5cf9a7261c385de1e20dcfc85", - "sha256:8db8e423192303ed77cff4dce3a4b88dbfaf43979d280181558af5e2c3c71afc", - "sha256:9517004e21664f2b5a5fd6333b0731b9cf0817403a941b393d89a2f1dc2bd836", - "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3", - "sha256:99881222f4a8c2f641f25703963a5cefb076adffd959e0558dc9f803a52d6a58", - "sha256:9ee32dcb8e531adae1f1ca568822e9b3a738369b3b686d1477cbc643c4a9c128", - "sha256:a22e47578b30a3e199ab067a4d43d790249b3c0587d9a771921f86250c8435db", - "sha256:b5505774ea2a73a86ea176e8a9a4a7c8bf5d521050f0f6f8426afe798689243f", - "sha256:bd739c9251d01e0279ce729e37b39d49a08c0420d3fee7f2a4968c0576678f77", - "sha256:d16a786905034e7e34098634b184a7d81f91d4c3d246edc6bd7aefb2fd8ea6ad", - "sha256:d3420522057ebab1728b21ad473aa950026d07cb09da41103f8e597dfbfaeb13", - "sha256:d56fd9f1f1cdc8227d7b7918f55091349741904d9520c65f0139a9755952c9e8", - "sha256:d661dc4785affa9d0edfdd1e59ec056a58b3dbb9f196fa43587f3ddac654ac7b", - "sha256:dfe1f0f0ed5785c187144c46a292b8c34c1295c01da12e10ccddfc16def4448a", - "sha256:e1dd7839443592d00e96db831eddb4111a2a81a46b028f0facd60a09ebbdd543", - "sha256:e2872993e209f7ed04d963e4b4fbae72d034844ec66bc4ca403329db2074377b", - "sha256:e2f879ab92ce502a1e65fce390eab619774dda6a6ff719718069ac94084098ce", - "sha256:e3aa7e51d738e0ec0afbed661261513b38b3014754c9459508399baf14ae0c9d", - "sha256:e532dbd6ddfe13946de050d7474e3f5fb6ec774fbb1a188aaf469b08cf04189a", - "sha256:e6b7842518a63a9f17107eb176320960ec095a8ee3b4420b5f688e24bf50c53c", - "sha256:e75753aeda0ddc4c28dce4c32ba2f6ec30b1b02f6c0b14e547841ba5b24f753f", - "sha256:eadb9f826c138e6cf3c49d6f8de88225a3c0ab181a9b4ba792e006e5292d150e", - "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011", - "sha256:ef254a06bcea461e65ff0373d8a0dd1ed3aa004af48839f002a0c994a6f72d04", - "sha256:f3709997b228685fe53e8c433e2df9f0cdb5f4542bd5114ed17ac3c0129b0480", - "sha256:f51bab98d52739c50c56658cc303f190785f9a2cd97b823357e7aeae54c8f68a", - "sha256:f9904e24646570539a8950400602d66d2b2c492b9010ea7e965025cb71d0c86d", - "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d" - ], - "markers": "platform_system != 'Windows'", - "version": "==1.0.8" - }, "numpy": { "hashes": [ "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", @@ -205,53 +152,60 @@ }, "packaging": { "hashes": [ - "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", - "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" + "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", + "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" ], - "markers": "python_version >= '3.7'", - "version": "==24.0" + "markers": "python_version >= '3.8'", + "version": "==24.1" }, "pydantic": { "hashes": [ - "sha256:005655cabc29081de8243126e036f2065bd7ea5b9dff95fde6d2c642d39755de", - "sha256:0d142fa1b8f2f0ae11ddd5e3e317dcac060b951d605fda26ca9b234b92214986", - "sha256:22ed12ee588b1df028a2aa5d66f07bf8f8b4c8579c2e96d5a9c1f96b77f3bb55", - "sha256:2746189100c646682eff0bce95efa7d2e203420d8e1c613dc0c6b4c1d9c1fde4", - "sha256:28e552a060ba2740d0d2aabe35162652c1459a0b9069fe0db7f4ee0e18e74d58", - "sha256:3287e1614393119c67bd4404f46e33ae3be3ed4cd10360b48d0a4459f420c6a3", - "sha256:3350f527bb04138f8aff932dc828f154847fbdc7a1a44c240fbfff1b57f49a12", - "sha256:3453685ccd7140715e05f2193d64030101eaad26076fad4e246c1cc97e1bb30d", - "sha256:394f08750bd8eaad714718812e7fab615f873b3cdd0b9d84e76e51ef3b50b6b7", - "sha256:4e316e54b5775d1eb59187f9290aeb38acf620e10f7fd2f776d97bb788199e53", - "sha256:50f1666a9940d3d68683c9d96e39640f709d7a72ff8702987dab1761036206bb", - "sha256:51d405b42f1b86703555797270e4970a9f9bd7953f3990142e69d1037f9d9e51", - "sha256:584f2d4c98ffec420e02305cf675857bae03c9d617fcfdc34946b1160213a948", - "sha256:5e09c19df304b8123938dc3c53d3d3be6ec74b9d7d0d80f4f4b5432ae16c2022", - "sha256:676ed48f2c5bbad835f1a8ed8a6d44c1cd5a21121116d2ac40bd1cd3619746ed", - "sha256:67f1a1fb467d3f49e1708a3f632b11c69fccb4e748a325d5a491ddc7b5d22383", - "sha256:6a51a1dd4aa7b3f1317f65493a182d3cff708385327c1c82c81e4a9d6d65b2e4", - "sha256:6bd7030c9abc80134087d8b6e7aa957e43d35714daa116aced57269a445b8f7b", - "sha256:75279d3cac98186b6ebc2597b06bcbc7244744f6b0b44a23e4ef01e5683cc0d2", - "sha256:7ac9237cd62947db00a0d16acf2f3e00d1ae9d3bd602b9c415f93e7a9fc10528", - "sha256:7ea210336b891f5ea334f8fc9f8f862b87acd5d4a0cbc9e3e208e7aa1775dabf", - "sha256:82790d4753ee5d00739d6cb5cf56bceb186d9d6ce134aca3ba7befb1eedbc2c8", - "sha256:92229f73400b80c13afcd050687f4d7e88de9234d74b27e6728aa689abcf58cc", - "sha256:9bea1f03b8d4e8e86702c918ccfd5d947ac268f0f0cc6ed71782e4b09353b26f", - "sha256:a980a77c52723b0dc56640ced396b73a024d4b74f02bcb2d21dbbac1debbe9d0", - "sha256:af9850d98fc21e5bc24ea9e35dd80a29faf6462c608728a110c0a30b595e58b7", - "sha256:bbc6989fad0c030bd70a0b6f626f98a862224bc2b1e36bfc531ea2facc0a340c", - "sha256:be51dd2c8596b25fe43c0a4a59c2bee4f18d88efb8031188f9e7ddc6b469cf44", - "sha256:c365ad9c394f9eeffcb30a82f4246c0006417f03a7c0f8315d6211f25f7cb654", - "sha256:c3d5731a120752248844676bf92f25a12f6e45425e63ce22e0849297a093b5b0", - "sha256:ca832e124eda231a60a041da4f013e3ff24949d94a01154b137fc2f2a43c3ffb", - "sha256:d207d5b87f6cbefbdb1198154292faee8017d7495a54ae58db06762004500d00", - "sha256:d31ee5b14a82c9afe2bd26aaa405293d4237d0591527d9129ce36e58f19f95c1", - "sha256:d3b5c4cbd0c9cb61bbbb19ce335e1f8ab87a811f6d589ed52b0254cf585d709c", - "sha256:d573082c6ef99336f2cb5b667b781d2f776d4af311574fb53d908517ba523c22", - "sha256:e49db944fad339b2ccb80128ffd3f8af076f9f287197a480bf1e4ca053a866f0" + "sha256:098ad8de840c92ea586bf8efd9e2e90c6339d33ab5c1cfbb85be66e4ecf8213f", + "sha256:0e2495309b1266e81d259a570dd199916ff34f7f51f1b549a0d37a6d9b17b4dc", + "sha256:0fa51175313cc30097660b10eec8ca55ed08bfa07acbfe02f7a42f6c242e9a4b", + "sha256:11289fa895bcbc8f18704efa1d8020bb9a86314da435348f59745473eb042e6b", + "sha256:2a72d2a5ff86a3075ed81ca031eac86923d44bc5d42e719d585a8eb547bf0c9b", + "sha256:371dcf1831f87c9e217e2b6a0c66842879a14873114ebb9d0861ab22e3b5bb1e", + "sha256:409b2b36d7d7d19cd8310b97a4ce6b1755ef8bd45b9a2ec5ec2b124db0a0d8f3", + "sha256:4866a1579c0c3ca2c40575398a24d805d4db6cb353ee74df75ddeee3c657f9a7", + "sha256:48db882e48575ce4b39659558b2f9f37c25b8d348e37a2b4e32971dd5a7d6227", + "sha256:525bbef620dac93c430d5d6bdbc91bdb5521698d434adf4434a7ef6ffd5c4b7f", + "sha256:543da3c6914795b37785703ffc74ba4d660418620cc273490d42c53949eeeca6", + "sha256:62d96b8799ae3d782df7ec9615cb59fc32c32e1ed6afa1b231b0595f6516e8ab", + "sha256:6654028d1144df451e1da69a670083c27117d493f16cf83da81e1e50edce72ad", + "sha256:7017971ffa7fd7808146880aa41b266e06c1e6e12261768a28b8b41ba55c8076", + "sha256:7623b59876f49e61c2e283551cc3647616d2fbdc0b4d36d3d638aae8547ea681", + "sha256:7e17c0ee7192e54a10943f245dc79e36d9fe282418ea05b886e1c666063a7b54", + "sha256:820ae12a390c9cbb26bb44913c87fa2ff431a029a785642c1ff11fed0a095fcb", + "sha256:94833612d6fd18b57c359a127cbfd932d9150c1b72fea7c86ab58c2a77edd7c7", + "sha256:95ef534e3c22e5abbdbdd6f66b6ea9dac3ca3e34c5c632894f8625d13d084cbe", + "sha256:9c803a5113cfab7bbb912f75faa4fc1e4acff43e452c82560349fff64f852e1b", + "sha256:9e53fb834aae96e7b0dadd6e92c66e7dd9cdf08965340ed04c16813102a47fab", + "sha256:ab2f976336808fd5d539fdc26eb51f9aafc1f4b638e212ef6b6f05e753c8011d", + "sha256:ad1e33dc6b9787a6f0f3fd132859aa75626528b49cc1f9e429cdacb2608ad5f0", + "sha256:ae5184e99a060a5c80010a2d53c99aee76a3b0ad683d493e5f0620b5d86eeb75", + "sha256:aeb4e741782e236ee7dc1fb11ad94dc56aabaf02d21df0e79e0c21fe07c95741", + "sha256:b4ad32aed3bf5eea5ca5decc3d1bbc3d0ec5d4fbcd72a03cdad849458decbc63", + "sha256:b8ad363330557beac73159acfbeed220d5f1bfcd6b930302a987a375e02f74fd", + "sha256:bfbb18b616abc4df70591b8c1ff1b3eabd234ddcddb86b7cac82657ab9017e33", + "sha256:c1e51d1af306641b7d1574d6d3307eaa10a4991542ca324f0feb134fee259815", + "sha256:c31d281c7485223caf6474fc2b7cf21456289dbaa31401844069b77160cab9c7", + "sha256:c7e8988bb16988890c985bd2093df9dd731bfb9d5e0860db054c23034fab8f7a", + "sha256:c87cedb4680d1614f1d59d13fea353faf3afd41ba5c906a266f3f2e8c245d655", + "sha256:cafb9c938f61d1b182dfc7d44a7021326547b7b9cf695db5b68ec7b590214773", + "sha256:d2f89a719411cb234105735a520b7c077158a81e0fe1cb05a79c01fc5eb59d3c", + "sha256:d4b40c9e13a0b61583e5599e7950490c700297b4a375b55b2b592774332798b7", + "sha256:d4ecb515fa7cb0e46e163ecd9d52f9147ba57bc3633dca0e586cdb7a232db9e3", + "sha256:d8c209af63ccd7b22fba94b9024e8b7fd07feffee0001efae50dd99316b27768", + "sha256:db3b48d9283d80a314f7a682f7acae8422386de659fffaba454b77a083c3937d", + "sha256:e41b5b973e5c64f674b3b4720286ded184dcc26a691dd55f34391c62c6934688", + "sha256:e840e6b2026920fc3f250ea8ebfdedf6ea7a25b77bf04c6576178e681942ae0f", + "sha256:ebb249096d873593e014535ab07145498957091aa6ae92759a32d40cb9998e2e", + "sha256:f434160fb14b353caf634149baaf847206406471ba70e64657c1e8330277a991", + "sha256:fa43f362b46741df8f201bf3e7dff3569fa92069bcc7b4a740dea3602e27ab7a" ], "markers": "python_version >= '3.7'", - "version": "==1.10.15" + "version": "==1.10.17" }, "pyrsistent": { "hashes": [ @@ -304,15 +258,36 @@ "sha256:6ad50f4613289f3c4d276b6d2ac8901d776dcb929994cce93f55a69e858c595f", "sha256:7eea9b81b0ff908000a825db024313f622895bd578e8a17433e0474cd7d2da83" ], + "markers": "python_version >= '3.7'", "version": "==4.2.2" }, + "pywin32": { + "hashes": [ + "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d", + "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65", + "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e", + "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b", + "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4", + "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040", + "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a", + "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36", + "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8", + "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e", + "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802", + "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a", + "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407", + "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0" + ], + "markers": "platform_system == 'Windows' and platform_python_implementation == 'CPython'", + "version": "==306" + }, "setuptools": { "hashes": [ - "sha256:54faa7f2e8d2d11bcd2c07bed282eef1046b5c080d1c32add737d7b5817b1ad4", - "sha256:f211a66637b8fa059bb28183da127d4e86396c991a942b028c6650d4319c3fd0" + "sha256:f171bab1dfbc86b132997f26a119f6056a57950d058587841a0082e8830f9dc5", + "sha256:fe384da74336c398e0d956d1cae0669bc02eed936cdb1d49b57de1990dc11ffc" ], "markers": "python_version >= '3.8'", - "version": "==70.0.0" + "version": "==70.3.0" }, "sniffio": { "hashes": [ @@ -322,13 +297,22 @@ "markers": "python_version >= '3.7'", "version": "==1.3.1" }, + "types-pytz": { + "hashes": [ + "sha256:6810c8a1f68f21fdf0f4f374a432487c77645a0ac0b31de4bf4690cf21ad3981", + "sha256:8335d443310e2db7b74e007414e74c4f53b67452c0cb0d228ca359ccfba59659" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==2024.1.0.20240417" + }, "typing-extensions": { "hashes": [ - "sha256:8cbcdc8606ebcb0d95453ad7dc5065e6237b6aa230a31e81d0f440c30fed5fd8", - "sha256:b349c66bea9016ac22978d800cfff206d5f9816951f12a7d0ec5578b0a819594" + "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", + "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" ], "markers": "python_version >= '3.8'", - "version": "==4.12.0" + "version": "==4.12.2" }, "wrapt": { "hashes": [ @@ -455,11 +439,11 @@ }, "certifi": { "hashes": [ - "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f", - "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1" + "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b", + "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90" ], "markers": "python_version >= '3.6'", - "version": "==2024.2.2" + "version": "==2024.7.4" }, "charset-normalizer": { "hashes": [ @@ -479,70 +463,69 @@ }, "colorama": { "hashes": [ - "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", - "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" + "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", + "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" ], - "index": "pypi", - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==0.4.4" + "markers": "platform_system == 'Windows'", + "version": "==0.4.6" }, "coverage": { "hashes": [ - "sha256:015eddc5ccd5364dcb902eaecf9515636806fa1e0d5bef5769d06d0f31b54523", - "sha256:04aefca5190d1dc7a53a4c1a5a7f8568811306d7a8ee231c42fb69215571944f", - "sha256:05ac5f60faa0c704c0f7e6a5cbfd6f02101ed05e0aee4d2822637a9e672c998d", - "sha256:0bbddc54bbacfc09b3edaec644d4ac90c08ee8ed4844b0f86227dcda2d428fcb", - "sha256:1d2a830ade66d3563bb61d1e3c77c8def97b30ed91e166c67d0632c018f380f0", - "sha256:239a4e75e09c2b12ea478d28815acf83334d32e722e7433471fbf641c606344c", - "sha256:244f509f126dc71369393ce5fea17c0592c40ee44e607b6d855e9c4ac57aac98", - "sha256:25a5caf742c6195e08002d3b6c2dd6947e50efc5fc2c2205f61ecb47592d2d83", - "sha256:296a7d9bbc598e8744c00f7a6cecf1da9b30ae9ad51c566291ff1314e6cbbed8", - "sha256:2e079c9ec772fedbade9d7ebc36202a1d9ef7291bc9b3a024ca395c4d52853d7", - "sha256:33ca90a0eb29225f195e30684ba4a6db05dbef03c2ccd50b9077714c48153cac", - "sha256:33fc65740267222fc02975c061eb7167185fef4cc8f2770267ee8bf7d6a42f84", - "sha256:341dd8f61c26337c37988345ca5c8ccabeff33093a26953a1ac72e7d0103c4fb", - "sha256:34d6d21d8795a97b14d503dcaf74226ae51eb1f2bd41015d3ef332a24d0a17b3", - "sha256:3538d8fb1ee9bdd2e2692b3b18c22bb1c19ffbefd06880f5ac496e42d7bb3884", - "sha256:38a3b98dae8a7c9057bd91fbf3415c05e700a5114c5f1b5b0ea5f8f429ba6614", - "sha256:3d5a67f0da401e105753d474369ab034c7bae51a4c31c77d94030d59e41df5bd", - "sha256:50084d3516aa263791198913a17354bd1dc627d3c1639209640b9cac3fef5807", - "sha256:55f689f846661e3f26efa535071775d0483388a1ccfab899df72924805e9e7cd", - "sha256:5bc5a8c87714b0c67cfeb4c7caa82b2d71e8864d1a46aa990b5588fa953673b8", - "sha256:62bda40da1e68898186f274f832ef3e759ce929da9a9fd9fcf265956de269dbc", - "sha256:705f3d7c2b098c40f5b81790a5fedb274113373d4d1a69e65f8b68b0cc26f6db", - "sha256:75e3f4e86804023e991096b29e147e635f5e2568f77883a1e6eed74512659ab0", - "sha256:7b2a19e13dfb5c8e145c7a6ea959485ee8e2204699903c88c7d25283584bfc08", - "sha256:7cec2af81f9e7569280822be68bd57e51b86d42e59ea30d10ebdbb22d2cb7232", - "sha256:8383a6c8cefba1b7cecc0149415046b6fc38836295bc4c84e820872eb5478b3d", - "sha256:8c836309931839cca658a78a888dab9676b5c988d0dd34ca247f5f3e679f4e7a", - "sha256:8e317953bb4c074c06c798a11dbdd2cf9979dbcaa8ccc0fa4701d80042d4ebf1", - "sha256:923b7b1c717bd0f0f92d862d1ff51d9b2b55dbbd133e05680204465f454bb286", - "sha256:990fb20b32990b2ce2c5f974c3e738c9358b2735bc05075d50a6f36721b8f303", - "sha256:9aad68c3f2566dfae84bf46295a79e79d904e1c21ccfc66de88cd446f8686341", - "sha256:a5812840d1d00eafae6585aba38021f90a705a25b8216ec7f66aebe5b619fb84", - "sha256:a6519d917abb15e12380406d721e37613e2a67d166f9fb7e5a8ce0375744cd45", - "sha256:ab0b028165eea880af12f66086694768f2c3139b2c31ad5e032c8edbafca6ffc", - "sha256:aea7da970f1feccf48be7335f8b2ca64baf9b589d79e05b9397a06696ce1a1ec", - "sha256:b1196e13c45e327d6cd0b6e471530a1882f1017eb83c6229fc613cd1a11b53cd", - "sha256:b368e1aee1b9b75757942d44d7598dcd22a9dbb126affcbba82d15917f0cc155", - "sha256:bde997cac85fcac227b27d4fb2c7608a2c5f6558469b0eb704c5726ae49e1c52", - "sha256:c4c2872b3c91f9baa836147ca33650dc5c172e9273c808c3c3199c75490e709d", - "sha256:c59d2ad092dc0551d9f79d9d44d005c945ba95832a6798f98f9216ede3d5f485", - "sha256:d1da0a2e3b37b745a2b2a678a4c796462cf753aebf94edcc87dcc6b8641eae31", - "sha256:d8b7339180d00de83e930358223c617cc343dd08e1aa5ec7b06c3a121aec4e1d", - "sha256:dd4b3355b01273a56b20c219e74e7549e14370b31a4ffe42706a8cda91f19f6d", - "sha256:e08c470c2eb01977d221fd87495b44867a56d4d594f43739a8028f8646a51e0d", - "sha256:f5102a92855d518b0996eb197772f5ac2a527c0ec617124ad5242a3af5e25f85", - "sha256:f542287b1489c7a860d43a7d8883e27ca62ab84ca53c965d11dac1d3a1fab7ce", - "sha256:f78300789a708ac1f17e134593f577407d52d0417305435b134805c4fb135adb", - "sha256:f81bc26d609bf0fbc622c7122ba6307993c83c795d2d6f6f6fd8c000a770d974", - "sha256:f836c174c3a7f639bded48ec913f348c4761cbf49de4a20a956d3431a7c9cb24", - "sha256:fa21a04112c59ad54f69d80e376f7f9d0f5f9123ab87ecd18fbb9ec3a2beed56", - "sha256:fcf7d1d6f5da887ca04302db8e0e0cf56ce9a5e05f202720e49b3e8157ddb9a9", - "sha256:fd27d8b49e574e50caa65196d908f80e4dff64d7e592d0c59788b45aad7e8b35" + "sha256:0086cd4fc71b7d485ac93ca4239c8f75732c2ae3ba83f6be1c9be59d9e2c6382", + "sha256:01c322ef2bbe15057bc4bf132b525b7e3f7206f071799eb8aa6ad1940bcf5fb1", + "sha256:03cafe82c1b32b770a29fd6de923625ccac3185a54a5e66606da26d105f37dac", + "sha256:044a0985a4f25b335882b0966625270a8d9db3d3409ddc49a4eb00b0ef5e8cee", + "sha256:07ed352205574aad067482e53dd606926afebcb5590653121063fbf4e2175166", + "sha256:0d1b923fc4a40c5832be4f35a5dab0e5ff89cddf83bb4174499e02ea089daf57", + "sha256:0e7b27d04131c46e6894f23a4ae186a6a2207209a05df5b6ad4caee6d54a222c", + "sha256:1fad32ee9b27350687035cb5fdf9145bc9cf0a094a9577d43e909948ebcfa27b", + "sha256:289cc803fa1dc901f84701ac10c9ee873619320f2f9aff38794db4a4a0268d51", + "sha256:3c59105f8d58ce500f348c5b56163a4113a440dad6daa2294b5052a10db866da", + "sha256:46c3d091059ad0b9c59d1034de74a7f36dcfa7f6d3bde782c49deb42438f2450", + "sha256:482855914928c8175735a2a59c8dc5806cf7d8f032e4820d52e845d1f731dca2", + "sha256:49c76cdfa13015c4560702574bad67f0e15ca5a2872c6a125f6327ead2b731dd", + "sha256:4b03741e70fb811d1a9a1d75355cf391f274ed85847f4b78e35459899f57af4d", + "sha256:4bea27c4269234e06f621f3fac3925f56ff34bc14521484b8f66a580aacc2e7d", + "sha256:4d5fae0a22dc86259dee66f2cc6c1d3e490c4a1214d7daa2a93d07491c5c04b6", + "sha256:543ef9179bc55edfd895154a51792b01c017c87af0ebaae092720152e19e42ca", + "sha256:54dece71673b3187c86226c3ca793c5f891f9fc3d8aa183f2e3653da18566169", + "sha256:6379688fb4cfa921ae349c76eb1a9ab26b65f32b03d46bb0eed841fd4cb6afb1", + "sha256:65fa405b837060db569a61ec368b74688f429b32fa47a8929a7a2f9b47183713", + "sha256:6616d1c9bf1e3faea78711ee42a8b972367d82ceae233ec0ac61cc7fec09fa6b", + "sha256:6fe885135c8a479d3e37a7aae61cbd3a0fb2deccb4dda3c25f92a49189f766d6", + "sha256:7221f9ac9dad9492cecab6f676b3eaf9185141539d5c9689d13fd6b0d7de840c", + "sha256:76d5f82213aa78098b9b964ea89de4617e70e0d43e97900c2778a50856dac605", + "sha256:7792f0ab20df8071d669d929c75c97fecfa6bcab82c10ee4adb91c7a54055463", + "sha256:831b476d79408ab6ccfadaaf199906c833f02fdb32c9ab907b1d4aa0713cfa3b", + "sha256:9146579352d7b5f6412735d0f203bbd8d00113a680b66565e205bc605ef81bc6", + "sha256:9cc44bf0315268e253bf563f3560e6c004efe38f76db03a1558274a6e04bf5d5", + "sha256:a73d18625f6a8a1cbb11eadc1d03929f9510f4131879288e3f7922097a429f63", + "sha256:a8659fd33ee9e6ca03950cfdcdf271d645cf681609153f218826dd9805ab585c", + "sha256:a94925102c89247530ae1dab7dc02c690942566f22e189cbd53579b0693c0783", + "sha256:ad4567d6c334c46046d1c4c20024de2a1c3abc626817ae21ae3da600f5779b44", + "sha256:b2e16f4cd2bc4d88ba30ca2d3bbf2f21f00f382cf4e1ce3b1ddc96c634bc48ca", + "sha256:bbdf9a72403110a3bdae77948b8011f644571311c2fb35ee15f0f10a8fc082e8", + "sha256:beb08e8508e53a568811016e59f3234d29c2583f6b6e28572f0954a6b4f7e03d", + "sha256:c4cbe651f3904e28f3a55d6f371203049034b4ddbce65a54527a3f189ca3b390", + "sha256:c7b525ab52ce18c57ae232ba6f7010297a87ced82a2383b1afd238849c1ff933", + "sha256:ca5d79cfdae420a1d52bf177de4bc2289c321d6c961ae321503b2ca59c17ae67", + "sha256:cdab02a0a941af190df8782aafc591ef3ad08824f97850b015c8c6a8b3877b0b", + "sha256:d17c6a415d68cfe1091d3296ba5749d3d8696e42c37fca5d4860c5bf7b729f03", + "sha256:d39bd10f0ae453554798b125d2f39884290c480f56e8a02ba7a6ed552005243b", + "sha256:d4b3cd1ca7cd73d229487fa5caca9e4bc1f0bca96526b922d61053ea751fe791", + "sha256:d50a252b23b9b4dfeefc1f663c568a221092cbaded20a05a11665d0dbec9b8fb", + "sha256:da8549d17489cd52f85a9829d0e1d91059359b3c54a26f28bec2c5d369524807", + "sha256:dcd070b5b585b50e6617e8972f3fbbee786afca71b1936ac06257f7e178f00f6", + "sha256:ddaaa91bfc4477d2871442bbf30a125e8fe6b05da8a0015507bfbf4718228ab2", + "sha256:df423f351b162a702c053d5dddc0fc0ef9a9e27ea3f449781ace5f906b664428", + "sha256:dff044f661f59dace805eedb4a7404c573b6ff0cdba4a524141bc63d7be5c7fd", + "sha256:e7e128f85c0b419907d1f38e616c4f1e9f1d1b37a7949f44df9a73d5da5cd53c", + "sha256:ed8d1d1821ba5fc88d4a4f45387b65de52382fa3ef1f0115a4f7a20cdfab0e94", + "sha256:f2501d60d7497fd55e391f423f965bbe9e650e9ffc3c627d5f0ac516026000b8", + "sha256:f7db0b6ae1f96ae41afe626095149ecd1b212b424626175a6633c2999eaad45b" ], "markers": "python_version >= '3.8'", - "version": "==7.5.3" + "version": "==7.6.0" }, "flake8": { "hashes": [ @@ -643,11 +626,11 @@ }, "packaging": { "hashes": [ - "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", - "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" + "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", + "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" ], - "markers": "python_version >= '3.7'", - "version": "==24.0" + "markers": "python_version >= '3.8'", + "version": "==24.1" }, "pathspec": { "hashes": [ @@ -766,19 +749,19 @@ }, "typing-extensions": { "hashes": [ - "sha256:8cbcdc8606ebcb0d95453ad7dc5065e6237b6aa230a31e81d0f440c30fed5fd8", - "sha256:b349c66bea9016ac22978d800cfff206d5f9816951f12a7d0ec5578b0a819594" + "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", + "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" ], "markers": "python_version >= '3.8'", - "version": "==4.12.0" + "version": "==4.12.2" }, "urllib3": { "hashes": [ - "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07", - "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0" + "sha256:37a0344459b199fce0e80b0d3569837ec6b6937435c5244e7fd73fa6006830f3", + "sha256:3e3d753a8618b86d7de333b4223005f68720bcd6a7d2bcb9fbd2229ec7c1e429" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==1.26.18" + "version": "==1.26.19" } } } diff --git a/hardware-testing/hardware_testing/gravimetric/config.py b/hardware-testing/hardware_testing/gravimetric/config.py index 67a24d08171..53663fdd614 100644 --- a/hardware-testing/hardware_testing/gravimetric/config.py +++ b/hardware-testing/hardware_testing/gravimetric/config.py @@ -90,17 +90,15 @@ class PhotometricConfig(VolumetricConfig): 50: { 1: { 50: { - "max_z_distance": 20, - "mount_speed": 11, - "plunger_speed": 21, + "mount_speed": 5, + "plunger_speed": 20, "sensor_threshold_pascals": 15, }, }, 8: { 50: { - "max_z_distance": 20, - "mount_speed": 11, - "plunger_speed": 21, + "mount_speed": 5, + "plunger_speed": 20, "sensor_threshold_pascals": 15, }, }, @@ -108,61 +106,52 @@ class PhotometricConfig(VolumetricConfig): 1000: { 1: { 50: { - "max_z_distance": 20, "mount_speed": 5, - "plunger_speed": 10, + "plunger_speed": 20, "sensor_threshold_pascals": 15, }, 200: { - "max_z_distance": 20, "mount_speed": 5, - "plunger_speed": 10, + "plunger_speed": 20, "sensor_threshold_pascals": 15, }, 1000: { - "max_z_distance": 20, "mount_speed": 5, - "plunger_speed": 11, + "plunger_speed": 20, "sensor_threshold_pascals": 15, }, }, 8: { 50: { - "max_z_distance": 20, "mount_speed": 5, - "plunger_speed": 10, + "plunger_speed": 20, "sensor_threshold_pascals": 15, }, 200: { - "max_z_distance": 20, "mount_speed": 5, - "plunger_speed": 10, + "plunger_speed": 20, "sensor_threshold_pascals": 15, }, 1000: { - "max_z_distance": 20, "mount_speed": 5, - "plunger_speed": 11, + "plunger_speed": 20, "sensor_threshold_pascals": 15, }, }, 96: { 50: { - "max_z_distance": 20, "mount_speed": 5, - "plunger_speed": 10, + "plunger_speed": 20, "sensor_threshold_pascals": 15, }, 200: { - "max_z_distance": 20, "mount_speed": 5, - "plunger_speed": 10, + "plunger_speed": 20, "sensor_threshold_pascals": 15, }, 1000: { - "max_z_distance": 20, "mount_speed": 5, - "plunger_speed": 11, + "plunger_speed": 20, "sensor_threshold_pascals": 15, }, }, diff --git a/hardware-testing/hardware_testing/gravimetric/helpers.py b/hardware-testing/hardware_testing/gravimetric/helpers.py index 02510c99f24..31541d59f5a 100644 --- a/hardware-testing/hardware_testing/gravimetric/helpers.py +++ b/hardware-testing/hardware_testing/gravimetric/helpers.py @@ -168,7 +168,7 @@ def _jog_to_find_liquid_height( ctx: ProtocolContext, pipette: InstrumentContext, well: Well ) -> float: _well_depth = well.depth - _liquid_height = _well_depth + _liquid_height = _well_depth + 2 _jog_size = -1.0 if ctx.is_simulating(): return _liquid_height - 1 @@ -445,7 +445,10 @@ def _load_pipette( # so we need to decrease the pick-up current to work with 1 tip. if pipette.channels == 8 and not increment and not photometric: pipette._core.configure_nozzle_layout( - style=NozzleLayout.SINGLE, primary_nozzle="A1", front_right_nozzle="A1" + style=NozzleLayout.SINGLE, + primary_nozzle="A1", + front_right_nozzle="A1", + back_left_nozzle="A1", ) # override deck conflict checking cause we specially lay out our tipracks DeckConflit.check_safe_for_pipette_movement = ( diff --git a/hardware-testing/hardware_testing/liquid_sense/__main__.py b/hardware-testing/hardware_testing/liquid_sense/__main__.py index b35da3a76ba..f17c08677fd 100644 --- a/hardware-testing/hardware_testing/liquid_sense/__main__.py +++ b/hardware-testing/hardware_testing/liquid_sense/__main__.py @@ -6,7 +6,7 @@ import subprocess from time import sleep import os -from typing import List, Any, Optional +from typing import List, Any, Optional, Dict import traceback import sys @@ -27,6 +27,7 @@ get_testing_data_directory, ) from opentrons_hardware.hardware_control.motion_planning import move_utils +from opentrons_hardware.hardware_control import tool_sensors from opentrons.protocol_api import InstrumentContext, ProtocolContext from opentrons.protocol_engine.types import LabwareOffset @@ -36,8 +37,11 @@ from .post_process import process_csv_directory, process_google_sheet from hardware_testing.protocols.liquid_sense_lpc import ( - liquid_sense_ot3_p50_single_vial, - liquid_sense_ot3_p1000_single_vial, + liquid_sense_ot3_p50_single_96well, + liquid_sense_ot3_p1000_96_1well, + liquid_sense_ot3_p1000_single_96well, + liquid_sense_ot3_p50_multi_12well, + liquid_sense_ot3_p1000_multi_12well, ) try: @@ -67,15 +71,15 @@ MAX_PROBE_SECONDS = 3.5 -LIQUID_SENSE_CFG = { +LIQUID_SENSE_CFG: Dict[int, Dict[int, Any]] = { 50: { - 1: liquid_sense_ot3_p50_single_vial, - 8: None, + 1: liquid_sense_ot3_p50_single_96well, + 8: liquid_sense_ot3_p50_multi_12well, }, 1000: { - 1: liquid_sense_ot3_p1000_single_vial, - 8: None, - 96: None, + 1: liquid_sense_ot3_p1000_single_96well, + 8: liquid_sense_ot3_p1000_multi_12well, + 96: liquid_sense_ot3_p1000_96_1well, }, } @@ -118,6 +122,7 @@ class RunArgs: trials_before_jog: int no_multi_pass: int test_well: str + wet: bool @classmethod def _get_protocol_context(cls, args: argparse.Namespace) -> ProtocolContext: @@ -227,6 +232,7 @@ def build_run_args(cls, args: argparse.Namespace) -> "RunArgs": protocol_cfg.LABWARE_ON_SCALE, # type: ignore[union-attr] args.z_speed, ) + tool_sensors.PLUNGER_SOLO_MOVE_TIME = args.p_solo_time return RunArgs( tip_volumes=tip_volumes, run_id=run_id, @@ -250,6 +256,7 @@ def build_run_args(cls, args: argparse.Namespace) -> "RunArgs": trials_before_jog=args.trials_before_jog, no_multi_pass=args.no_multi_pass, test_well=args.test_well, + wet=args.wet, ) @@ -265,12 +272,16 @@ def build_run_args(cls, args: argparse.Namespace) -> "RunArgs": parser.add_argument("--return-tip", action="store_true") parser.add_argument("--trials", type=int, default=7) parser.add_argument("--trials-before-jog", type=int, default=7) - parser.add_argument("--z-speed", type=float, default=1) + parser.add_argument("--z-speed", type=float, default=5) parser.add_argument("--aspirate", action="store_true") - parser.add_argument("--plunger-speed", type=float, default=-1.0) + parser.add_argument("--plunger-speed", type=float, default=20) parser.add_argument("--no-multi-pass", action="store_true") + parser.add_argument("--wet", action="store_true") parser.add_argument("--starting-tip", type=str, default="A1") parser.add_argument("--test-well", type=str, default="A1") + parser.add_argument( + "--p-solo-time", type=float, default=tool_sensors.PLUNGER_SOLO_MOVE_TIME + ) parser.add_argument("--google-sheet-name", type=str, default="LLD-Shared-Data") parser.add_argument( "--gd-parent-folder", type=str, default="1b2V85fDPA0tNqjEhyHOGCWRZYgn8KsGf" diff --git a/hardware-testing/hardware_testing/liquid_sense/execute.py b/hardware-testing/hardware_testing/liquid_sense/execute.py index 679a7306967..05f5165b020 100644 --- a/hardware-testing/hardware_testing/liquid_sense/execute.py +++ b/hardware-testing/hardware_testing/liquid_sense/execute.py @@ -1,4 +1,6 @@ """Logic for running a single liquid probe test.""" +import csv +from enum import Enum from typing import Dict, Any, List, Tuple, Optional from .report import store_tip_results, store_trial, store_baseline_trial from opentrons.config.types import LiquidProbeSettings, OutputOptions @@ -16,6 +18,7 @@ Axis, top_types, ) +from opentrons.hardware_control.dev_types import PipetteDict from hardware_testing.gravimetric.measurement.scale import Scale from hardware_testing.gravimetric.measurement.record import ( @@ -39,6 +42,14 @@ pass +class LLDResult(Enum): + """Result Strings.""" + + success = "success" + not_found = "not found" + blockage = "blockage" + + def _load_tipracks( ctx: ProtocolContext, pipette_channels: int, protocol_cfg: Any, tip: int ) -> List[Labware]: @@ -257,11 +268,13 @@ def _get_target_height() -> None: run_args.pipette.pick_up_tip(tips[0]) del tips[: run_args.pipette_channels] - run_args.pipette.move_to(test_well.top()) + run_args.pipette.move_to(test_well.top(z=2)) + if run_args.wet: + run_args.pipette.move_to(test_well.bottom(1)) + run_args.pipette.move_to(test_well.top(z=2)) start_pos = hw_api.current_position_ot3(OT3Mount.LEFT) - height = _run_trial(run_args, tip, test_well, trial, start_pos) + height, result = _run_trial(run_args, tip, test_well, trial, start_pos) end_pos = hw_api.current_position_ot3(OT3Mount.LEFT) - run_args.pipette.blow_out() tip_length_offset = 0.0 if run_args.dial_indicator is not None: run_args.pipette._retract() @@ -297,6 +310,7 @@ def _get_target_height() -> None: plunger_start - end_pos[Axis.P_L], tip_length_offset, liquid_height_from_deck, + result.value, google_sheet, run_args.run_id, sheet_id, @@ -335,14 +349,17 @@ def find_max_z_distances( """ hw_mount = OT3Mount.LEFT if run_args.pipette.mount == "left" else OT3Mount.RIGHT hw_api = get_sync_hw_api(run_args.ctx) - lld_settings = hw_api._pipette_handler.get_pipette(hw_mount).lld_settings - - z_speed = run_args.z_speed - max_z_distance = ( - well.top().point.z - - well.bottom().point.z - - lld_settings[f"t{int(tip)}"]["minHeight"] + attached_instrument: PipetteDict = hw_api._pipette_handler.get_attached_instrument( + hw_mount ) + lld_settings = attached_instrument["lld_settings"] + z_speed = run_args.z_speed + if lld_settings is not None: + min_height = lld_settings[f"t{int(tip)}"]["minHeight"] + else: + ui.print_warning("No minimum height for pipette") + min_height = 0.5 + max_z_distance = well.top().point.z - well.bottom().point.z - min_height + 2 plunger_travel = get_plunger_travel(run_args) if p_speed == 0: p_travel_time = 10.0 @@ -357,13 +374,23 @@ def find_max_z_distances( return z_travels +def _test_for_blockage(datafile: str, threshold: float) -> bool: + with open(datafile, "r") as file: + reader = csv.reader(file) + reader_list = list(reader) + for i in range(1, len(reader_list)): + if i > 1 and abs(float(reader_list[i][1])) > threshold: + return abs(float(reader_list[i][1]) - float(reader_list[i - 1][1])) > 40 + return False + + def _run_trial( run_args: RunArgs, tip: int, well: Well, trial: int, start_pos: Dict[Axis, float], -) -> float: +) -> Tuple[float, LLDResult]: hw_api = get_sync_hw_api(run_args.ctx) lqid_cfg: Dict[str, int] = LIQUID_PROBE_SETTINGS[run_args.pipette_volume][ run_args.pipette_channels @@ -410,9 +437,17 @@ def _run_trial( # TODO add in stuff for secondary probe try: height = hw_api.liquid_probe(hw_mount, z_distance, lps, probe_target) + result: LLDResult = LLDResult.success + if not run_args.ctx.is_simulating(): + for probe in data_files: + if _test_for_blockage(data_files[probe], lps.sensor_threshold_pascals): + result = LLDResult.blockage + break except PipetteLiquidNotFoundError as lnf: ui.print_info(f"Liquid not found current position {lnf.detail}") + result = LLDResult.not_found + run_args.recorder.clear_sample_tag() ui.print_info(f"Trial {trial} complete") - return height + return height, result diff --git a/hardware-testing/hardware_testing/liquid_sense/post_process.py b/hardware-testing/hardware_testing/liquid_sense/post_process.py index 0c4e80df713..cebfc014f17 100644 --- a/hardware-testing/hardware_testing/liquid_sense/post_process.py +++ b/hardware-testing/hardware_testing/liquid_sense/post_process.py @@ -34,6 +34,8 @@ 13: "AO", } +BASELINE_TRIAL_LINE_NUMBER = 43 + def _get_pressure_results(result_file: str) -> Tuple[float, float, float, List[float]]: z_velocity: float = 0.0 @@ -137,11 +139,13 @@ def process_csv_directory( # noqa: C901 for row in summary_reader: final_report_writer.writerow(row) s += 1 - if s == 44: + if s == BASELINE_TRIAL_LINE_NUMBER: meniscus_travel = float(row[6]) - if s >= 45 and s < 45 + (trials * len(tips)): + if s >= (BASELINE_TRIAL_LINE_NUMBER + 1) and s < ( + BASELINE_TRIAL_LINE_NUMBER + 1 + (trials * len(tips)) + ): # while processing this grab the tip offsets from the summary - tip_offsets[tips[int((s - 45) / trials)]].append(float(row[8])) + tip_offsets[tips[int((s - 44) / trials)]].append(float(row[8])) # summary_reader.line_num is the last line in the summary that has text pressures_start_line = summary_reader.line_num + 3 # calculate where the start and end of each block of data we want to graph diff --git a/hardware-testing/hardware_testing/liquid_sense/report.py b/hardware-testing/hardware_testing/liquid_sense/report.py index f7b0b5b7b16..c68f679cf00 100644 --- a/hardware-testing/hardware_testing/liquid_sense/report.py +++ b/hardware-testing/hardware_testing/liquid_sense/report.py @@ -83,13 +83,13 @@ def build_config_section() -> CSVSection: def build_trials_section(trials: int, tips: List[int]) -> CSVSection: """Build section.""" lines: List[Union[CSVLine, CSVLineRepeating]] = [ - CSVLine("trial_number", [str, str, str, str, str, str, str, str, str]) + CSVLine("trial_number", [str, str, str, str, str, str, str, str, str, str]) ] lines.extend( [ CSVLine( f"trial-baseline-{tip}ul", - [float, float, float, float, float, float, float, float], + [float, float, float, float, float, float, float, float, str], ) for tip in tips ] @@ -98,7 +98,7 @@ def build_trials_section(trials: int, tips: List[int]) -> CSVSection: [ CSVLine( f"trial-{t + 1}-{tip}ul", - [float, float, float, float, float, float, float, float, float], + [float, float, float, float, float, float, float, float, float, str], ) for tip in tips for t in range(trials) @@ -195,6 +195,7 @@ def store_baseline_trial( 0, 0, measured_error, + "Baseline", ], ) @@ -211,6 +212,7 @@ def store_trial( plunger_travel: float, tip_length_offset: float, target_height: float, + result: str, google_sheet: Optional[google_sheets_tool.google_sheet], sheet_name: str, sheet_id: Optional[str], @@ -229,6 +231,7 @@ def store_trial( tip_length_offset, height + tip_length_offset, target_height, + result, ], ) if google_sheet is not None and sheet_id is not None: @@ -305,6 +308,7 @@ def build_ls_report( "tip_length_offset", "adjusted_height", "target_height", + "result", ], ) return report diff --git a/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_capacitance.py b/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_capacitance.py index 3a3b7cc128b..f3146d54f74 100644 --- a/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_capacitance.py +++ b/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_capacitance.py @@ -173,7 +173,6 @@ async def _probe(distance: float, speed: float) -> float: NodeId.pipette_left, NodeId.head_l, distance=distance, - plunger_speed=speed, mount_speed=speed, sensor_id=sensor_id, relative_threshold_pf=default_probe_cfg.sensor_threshold_pf, diff --git a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_96_1well.py b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_96_1well.py new file mode 100644 index 00000000000..ae89b4550a7 --- /dev/null +++ b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_96_1well.py @@ -0,0 +1,41 @@ +"""lld OT3 P1000.""" +from opentrons.protocol_api import ProtocolContext, OFF_DECK + +metadata = {"protocolName": "liquid-sense-ot3-p1000-96"} +requirements = {"robotType": "Flex", "apiLevel": "2.17"} + +SLOT_SCALE = 1 +SLOT_DIAL = 9 + +SLOTS_TIPRACK = { + 50: [2, 3, 4, 5, 6], + 200: [2, 3, 4, 5, 6], + 1000: [2, 3, 4, 5, 6], +} + +LABWARE_ON_SCALE = "nest_1_reservoir_195ml" + + +def run(ctx: ProtocolContext) -> None: + """Run.""" + trash = ctx.load_trash_bin("A3") + vial = ctx.load_labware(LABWARE_ON_SCALE, SLOT_SCALE) + dial = ctx.load_labware("dial_indicator", SLOT_DIAL) + pipette = ctx.load_instrument("flex_96channel_1000", "left") + adapters = [ + ctx.load_adapter("opentrons_flex_96_tiprack_adapter", slot) + for slot in SLOTS_TIPRACK[50] + ] + for size, slots in SLOTS_TIPRACK.items(): + tipracks = [ + adapter.load_labware(f"opentrons_flex_96_tiprack_{size}uL") + for adapter in adapters + ] + for rack in tipracks: + pipette.pick_up_tip(rack["A1"]) + pipette.aspirate(10, vial["A1"].top()) + pipette.dispense(10, vial["A1"].top()) + pipette.aspirate(10, dial["A1"].top()) + pipette.dispense(10, dial["A1"].top()) + pipette.drop_tip(trash) + ctx.move_labware(rack, OFF_DECK) diff --git a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_multi_12well.py b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_multi_12well.py new file mode 100644 index 00000000000..b77461598c8 --- /dev/null +++ b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_multi_12well.py @@ -0,0 +1,32 @@ +"""Liquid Sense OT3 P1000.""" +from opentrons.protocol_api import ProtocolContext, OFF_DECK + +metadata = {"protocolName": "liquid-sense-ot3-p50-multi"} +requirements = {"robotType": "Flex", "apiLevel": "2.15"} + +SLOT_SCALE = 1 +SLOT_DIAL = 9 +SLOTS_TIPRACK = { + 50: [3], + 200: [3], + 1000: [3], +} +LABWARE_ON_SCALE = "nest_12_reservoir_15ml" + + +def run(ctx: ProtocolContext) -> None: + """Run.""" + trash = ctx.load_trash_bin("A3") + vial = ctx.load_labware(LABWARE_ON_SCALE, SLOT_SCALE) + dial = ctx.load_labware("dial_indicator", SLOT_DIAL) + pipette = ctx.load_instrument("flex_8channel_1000", "left") + for size, slots in SLOTS_TIPRACK.items(): + for slot in slots: + rack = ctx.load_labware(f"opentrons_flex_96_tiprack_{size}uL", slot) + pipette.pick_up_tip(rack["A1"]) + pipette.aspirate(10, vial["A1"].top()) + pipette.dispense(10, vial["A1"].top()) + pipette.aspirate(10, dial["A1"].top()) + pipette.dispense(10, dial["A1"].top()) + pipette.drop_tip(trash) + ctx.move_labware(rack, OFF_DECK) diff --git a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_single_vial.py b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_single_96well.py similarity index 89% rename from hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_single_vial.py rename to hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_single_96well.py index d760f8da0ed..af96858af57 100644 --- a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_single_vial.py +++ b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_single_96well.py @@ -1,7 +1,7 @@ """Liquid Sense OT3.""" from opentrons.protocol_api import ProtocolContext, OFF_DECK -metadata = {"protocolName": "liquid-sense-ot3-p1000-single-vial"} +metadata = {"protocolName": "liquid-sense-ot3-p1000-single-96well"} requirements = {"robotType": "Flex", "apiLevel": "2.17"} SLOT_SCALE = 1 @@ -11,7 +11,7 @@ 200: [3], 1000: [3], } -LABWARE_ON_SCALE = "radwag_pipette_calibration_vial" +LABWARE_ON_SCALE = "corning_96_wellplate_360ul_flat" def run(ctx: ProtocolContext) -> None: diff --git a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_multi_12well.py b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_multi_12well.py new file mode 100644 index 00000000000..455565001cf --- /dev/null +++ b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_multi_12well.py @@ -0,0 +1,30 @@ +"""Liquid Sense OT3 P1000.""" +from opentrons.protocol_api import ProtocolContext + +metadata = {"protocolName": "liquid-sense-ot3-p50-multi"} +requirements = {"robotType": "Flex", "apiLevel": "2.15"} + +SLOT_SCALE = 1 +SLOT_DIAL = 9 +SLOTS_TIPRACK = {50: [3]} +LABWARE_ON_SCALE = "nest_12_reservoir_15ml" + + +def run(ctx: ProtocolContext) -> None: + """Run.""" + trash = ctx.load_trash_bin("A3") + tipracks = [ + ctx.load_labware(f"opentrons_flex_96_tiprack_{size}uL", slot) + for size, slots in SLOTS_TIPRACK.items() + for slot in slots + ] + vial = ctx.load_labware(LABWARE_ON_SCALE, SLOT_SCALE) + dial = ctx.load_labware("dial_indicator", SLOT_DIAL) + pipette = ctx.load_instrument("flex_8channel_50", "left") + for rack in tipracks: + pipette.pick_up_tip(rack["A1"]) + pipette.aspirate(10, vial["A1"].top()) + pipette.dispense(10, vial["A1"].top()) + pipette.aspirate(1, dial["A1"].top()) + pipette.dispense(1, dial["A1"].top()) + pipette.drop_tip(trash) diff --git a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_single_vial.py b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_single_96well.py similarity index 88% rename from hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_single_vial.py rename to hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_single_96well.py index 7a7e607d08e..9b597074a34 100644 --- a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_single_vial.py +++ b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_single_96well.py @@ -1,7 +1,7 @@ """Liquid Sense OT3.""" from opentrons.protocol_api import ProtocolContext, OFF_DECK -metadata = {"protocolName": "liquid-sense-ot3-p50-single-vial"} +metadata = {"protocolName": "liquid-sense-ot3-p50-single-96well"} requirements = {"robotType": "Flex", "apiLevel": "2.17"} SLOT_SCALE = 1 @@ -9,7 +9,7 @@ SLOTS_TIPRACK = { 50: [3], } -LABWARE_ON_SCALE = "radwag_pipette_calibration_vial" +LABWARE_ON_SCALE = "corning_96_wellplate_360ul_flat" def run(ctx: ProtocolContext) -> None: diff --git a/hardware-testing/hardware_testing/scripts/abr_asair_sensor.py b/hardware-testing/hardware_testing/scripts/abr_asair_sensor.py index 33195dacd5a..fc46fcd8df0 100644 --- a/hardware-testing/hardware_testing/scripts/abr_asair_sensor.py +++ b/hardware-testing/hardware_testing/scripts/abr_asair_sensor.py @@ -8,6 +8,7 @@ from typing import List import os import argparse +import pytz class _ABRAsairSensor: @@ -49,12 +50,14 @@ def __init__(self, robot: str, duration: int, frequency: int) -> None: "There are no google sheets credentials. Make sure credentials in jupyter notebook." ) results_list = [] # type: List - start_time = datetime.datetime.now() + timezone = pytz.timezone("America/New_York") + start_time = datetime.datetime.now(timezone) + # start_time = datetime.datetime.now(tz=tzinfo.utcoffset(timezone)) while True: env_data = sensor.get_reading() - timestamp = datetime.datetime.now() + timestamp = datetime.datetime.now(timezone) # Time adjustment for ABR robot timezone - new_timestamp = timestamp - datetime.timedelta(hours=5) + new_timestamp = timestamp date = new_timestamp.date() time = new_timestamp.time() temp = env_data.temperature @@ -72,7 +75,7 @@ def __init__(self, robot: str, duration: int, frequency: int) -> None: results_list.append(row) # Check if duration elapsed - elapsed_time = datetime.datetime.now() - start_time + elapsed_time = datetime.datetime.now(timezone) - start_time if elapsed_time.total_seconds() >= duration * 60: break # write to google sheet diff --git a/hardware/Pipfile b/hardware/Pipfile index b02e50c7c51..eed4c866740 100644 --- a/hardware/Pipfile +++ b/hardware/Pipfile @@ -8,7 +8,7 @@ python-can = "==4.2.2" pyserial = "==3.5" typing-extensions = ">=4.0.0,<5" numpy = "==1.22.3" -pydantic = "==1.9.2" +pydantic = "==1.10.12" [dev-packages] pytest = "==7.4.4" diff --git a/hardware/Pipfile.lock b/hardware/Pipfile.lock index ccab8884999..faae814c9ef 100644 --- a/hardware/Pipfile.lock +++ b/hardware/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "04ae6d52e739cf67e21d13822316f7dc2030d272976c0a9cfd0f7d35db743301" + "sha256": "de5368468f6a396a6a3bd854b5e2035ce9c68ef958832198866c883b2b17c2e2" }, "pipfile-spec": 6, "requires": { @@ -18,65 +18,65 @@ "default": { "msgpack": { "hashes": [ - "sha256:04ad6069c86e531682f9e1e71b71c1c3937d6014a7c3e9edd2aa81ad58842862", - "sha256:0bfdd914e55e0d2c9e1526de210f6fe8ffe9705f2b1dfcc4aecc92a4cb4b533d", - "sha256:1dc93e8e4653bdb5910aed79f11e165c85732067614f180f70534f056da97db3", - "sha256:1e2d69948e4132813b8d1131f29f9101bc2c915f26089a6d632001a5c1349672", - "sha256:235a31ec7db685f5c82233bddf9858748b89b8119bf4538d514536c485c15fe0", - "sha256:27dcd6f46a21c18fa5e5deed92a43d4554e3df8d8ca5a47bf0615d6a5f39dbc9", - "sha256:28efb066cde83c479dfe5a48141a53bc7e5f13f785b92ddde336c716663039ee", - "sha256:3476fae43db72bd11f29a5147ae2f3cb22e2f1a91d575ef130d2bf49afd21c46", - "sha256:36e17c4592231a7dbd2ed09027823ab295d2791b3b1efb2aee874b10548b7524", - "sha256:384d779f0d6f1b110eae74cb0659d9aa6ff35aaf547b3955abf2ab4c901c4819", - "sha256:38949d30b11ae5f95c3c91917ee7a6b239f5ec276f271f28638dec9156f82cfc", - "sha256:3967e4ad1aa9da62fd53e346ed17d7b2e922cba5ab93bdd46febcac39be636fc", - "sha256:3e7bf4442b310ff154b7bb9d81eb2c016b7d597e364f97d72b1acc3817a0fdc1", - "sha256:3f0c8c6dfa6605ab8ff0611995ee30d4f9fcff89966cf562733b4008a3d60d82", - "sha256:484ae3240666ad34cfa31eea7b8c6cd2f1fdaae21d73ce2974211df099a95d81", - "sha256:4a7b4f35de6a304b5533c238bee86b670b75b03d31b7797929caa7a624b5dda6", - "sha256:4cb14ce54d9b857be9591ac364cb08dc2d6a5c4318c1182cb1d02274029d590d", - "sha256:4e71bc4416de195d6e9b4ee93ad3f2f6b2ce11d042b4d7a7ee00bbe0358bd0c2", - "sha256:52700dc63a4676669b341ba33520f4d6e43d3ca58d422e22ba66d1736b0a6e4c", - "sha256:572efc93db7a4d27e404501975ca6d2d9775705c2d922390d878fcf768d92c87", - "sha256:576eb384292b139821c41995523654ad82d1916da6a60cff129c715a6223ea84", - "sha256:5b0bf0effb196ed76b7ad883848143427a73c355ae8e569fa538365064188b8e", - "sha256:5b6ccc0c85916998d788b295765ea0e9cb9aac7e4a8ed71d12e7d8ac31c23c95", - "sha256:5ed82f5a7af3697b1c4786053736f24a0efd0a1b8a130d4c7bfee4b9ded0f08f", - "sha256:6d4c80667de2e36970ebf74f42d1088cc9ee7ef5f4e8c35eee1b40eafd33ca5b", - "sha256:730076207cb816138cf1af7f7237b208340a2c5e749707457d70705715c93b93", - "sha256:7687e22a31e976a0e7fc99c2f4d11ca45eff652a81eb8c8085e9609298916dcf", - "sha256:822ea70dc4018c7e6223f13affd1c5c30c0f5c12ac1f96cd8e9949acddb48a61", - "sha256:84b0daf226913133f899ea9b30618722d45feffa67e4fe867b0b5ae83a34060c", - "sha256:85765fdf4b27eb5086f05ac0491090fc76f4f2b28e09d9350c31aac25a5aaff8", - "sha256:8dd178c4c80706546702c59529ffc005681bd6dc2ea234c450661b205445a34d", - "sha256:8f5b234f567cf76ee489502ceb7165c2a5cecec081db2b37e35332b537f8157c", - "sha256:98bbd754a422a0b123c66a4c341de0474cad4a5c10c164ceed6ea090f3563db4", - "sha256:993584fc821c58d5993521bfdcd31a4adf025c7d745bbd4d12ccfecf695af5ba", - "sha256:a40821a89dc373d6427e2b44b572efc36a2778d3f543299e2f24eb1a5de65415", - "sha256:b291f0ee7961a597cbbcc77709374087fa2a9afe7bdb6a40dbbd9b127e79afee", - "sha256:b573a43ef7c368ba4ea06050a957c2a7550f729c31f11dd616d2ac4aba99888d", - "sha256:b610ff0f24e9f11c9ae653c67ff8cc03c075131401b3e5ef4b82570d1728f8a9", - "sha256:bdf38ba2d393c7911ae989c3bbba510ebbcdf4ecbdbfec36272abe350c454075", - "sha256:bfef2bb6ef068827bbd021017a107194956918ab43ce4d6dc945ffa13efbc25f", - "sha256:cab3db8bab4b7e635c1c97270d7a4b2a90c070b33cbc00c99ef3f9be03d3e1f7", - "sha256:cb70766519500281815dfd7a87d3a178acf7ce95390544b8c90587d76b227681", - "sha256:cca1b62fe70d761a282496b96a5e51c44c213e410a964bdffe0928e611368329", - "sha256:ccf9a39706b604d884d2cb1e27fe973bc55f2890c52f38df742bc1d79ab9f5e1", - "sha256:dc43f1ec66eb8440567186ae2f8c447d91e0372d793dfe8c222aec857b81a8cf", - "sha256:dd632777ff3beaaf629f1ab4396caf7ba0bdd075d948a69460d13d44357aca4c", - "sha256:e45ae4927759289c30ccba8d9fdce62bb414977ba158286b5ddaf8df2cddb5c5", - "sha256:e50ebce52f41370707f1e21a59514e3375e3edd6e1832f5e5235237db933c98b", - "sha256:ebbbba226f0a108a7366bf4b59bf0f30a12fd5e75100c630267d94d7f0ad20e5", - "sha256:ec79ff6159dffcc30853b2ad612ed572af86c92b5168aa3fc01a67b0fa40665e", - "sha256:f0936e08e0003f66bfd97e74ee530427707297b0d0361247e9b4f59ab78ddc8b", - "sha256:f26a07a6e877c76a88e3cecac8531908d980d3d5067ff69213653649ec0f60ad", - "sha256:f64e376cd20d3f030190e8c32e1c64582eba56ac6dc7d5b0b49a9d44021b52fd", - "sha256:f6ffbc252eb0d229aeb2f9ad051200668fc3a9aaa8994e49f0cb2ffe2b7867e7", - "sha256:f9a7c509542db4eceed3dcf21ee5267ab565a83555c9b88a8109dcecc4709002", - "sha256:ff1d0899f104f3921d94579a5638847f783c9b04f2d5f229392ca77fba5b82fc" + "sha256:00e073efcba9ea99db5acef3959efa45b52bc67b61b00823d2a1a6944bf45982", + "sha256:0726c282d188e204281ebd8de31724b7d749adebc086873a59efb8cf7ae27df3", + "sha256:0ceea77719d45c839fd73abcb190b8390412a890df2f83fb8cf49b2a4b5c2f40", + "sha256:114be227f5213ef8b215c22dde19532f5da9652e56e8ce969bf0a26d7c419fee", + "sha256:13577ec9e247f8741c84d06b9ece5f654920d8365a4b636ce0e44f15e07ec693", + "sha256:1876b0b653a808fcd50123b953af170c535027bf1d053b59790eebb0aeb38950", + "sha256:1ab0bbcd4d1f7b6991ee7c753655b481c50084294218de69365f8f1970d4c151", + "sha256:1cce488457370ffd1f953846f82323cb6b2ad2190987cd4d70b2713e17268d24", + "sha256:26ee97a8261e6e35885c2ecd2fd4a6d38252246f94a2aec23665a4e66d066305", + "sha256:3528807cbbb7f315bb81959d5961855e7ba52aa60a3097151cb21956fbc7502b", + "sha256:374a8e88ddab84b9ada695d255679fb99c53513c0a51778796fcf0944d6c789c", + "sha256:376081f471a2ef24828b83a641a02c575d6103a3ad7fd7dade5486cad10ea659", + "sha256:3923a1778f7e5ef31865893fdca12a8d7dc03a44b33e2a5f3295416314c09f5d", + "sha256:4916727e31c28be8beaf11cf117d6f6f188dcc36daae4e851fee88646f5b6b18", + "sha256:493c5c5e44b06d6c9268ce21b302c9ca055c1fd3484c25ba41d34476c76ee746", + "sha256:505fe3d03856ac7d215dbe005414bc28505d26f0c128906037e66d98c4e95868", + "sha256:5845fdf5e5d5b78a49b826fcdc0eb2e2aa7191980e3d2cfd2a30303a74f212e2", + "sha256:5c330eace3dd100bdb54b5653b966de7f51c26ec4a7d4e87132d9b4f738220ba", + "sha256:5dbf059fb4b7c240c873c1245ee112505be27497e90f7c6591261c7d3c3a8228", + "sha256:5e390971d082dba073c05dbd56322427d3280b7cc8b53484c9377adfbae67dc2", + "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273", + "sha256:64d0fcd436c5683fdd7c907eeae5e2cbb5eb872fafbc03a43609d7941840995c", + "sha256:69284049d07fce531c17404fcba2bb1df472bc2dcdac642ae71a2d079d950653", + "sha256:6a0e76621f6e1f908ae52860bdcb58e1ca85231a9b0545e64509c931dd34275a", + "sha256:73ee792784d48aa338bba28063e19a27e8d989344f34aad14ea6e1b9bd83f596", + "sha256:74398a4cf19de42e1498368c36eed45d9528f5fd0155241e82c4082b7e16cffd", + "sha256:7938111ed1358f536daf311be244f34df7bf3cdedb3ed883787aca97778b28d8", + "sha256:82d92c773fbc6942a7a8b520d22c11cfc8fd83bba86116bfcf962c2f5c2ecdaa", + "sha256:83b5c044f3eff2a6534768ccfd50425939e7a8b5cf9a7261c385de1e20dcfc85", + "sha256:8db8e423192303ed77cff4dce3a4b88dbfaf43979d280181558af5e2c3c71afc", + "sha256:9517004e21664f2b5a5fd6333b0731b9cf0817403a941b393d89a2f1dc2bd836", + "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3", + "sha256:99881222f4a8c2f641f25703963a5cefb076adffd959e0558dc9f803a52d6a58", + "sha256:9ee32dcb8e531adae1f1ca568822e9b3a738369b3b686d1477cbc643c4a9c128", + "sha256:a22e47578b30a3e199ab067a4d43d790249b3c0587d9a771921f86250c8435db", + "sha256:b5505774ea2a73a86ea176e8a9a4a7c8bf5d521050f0f6f8426afe798689243f", + "sha256:bd739c9251d01e0279ce729e37b39d49a08c0420d3fee7f2a4968c0576678f77", + "sha256:d16a786905034e7e34098634b184a7d81f91d4c3d246edc6bd7aefb2fd8ea6ad", + "sha256:d3420522057ebab1728b21ad473aa950026d07cb09da41103f8e597dfbfaeb13", + "sha256:d56fd9f1f1cdc8227d7b7918f55091349741904d9520c65f0139a9755952c9e8", + "sha256:d661dc4785affa9d0edfdd1e59ec056a58b3dbb9f196fa43587f3ddac654ac7b", + "sha256:dfe1f0f0ed5785c187144c46a292b8c34c1295c01da12e10ccddfc16def4448a", + "sha256:e1dd7839443592d00e96db831eddb4111a2a81a46b028f0facd60a09ebbdd543", + "sha256:e2872993e209f7ed04d963e4b4fbae72d034844ec66bc4ca403329db2074377b", + "sha256:e2f879ab92ce502a1e65fce390eab619774dda6a6ff719718069ac94084098ce", + "sha256:e3aa7e51d738e0ec0afbed661261513b38b3014754c9459508399baf14ae0c9d", + "sha256:e532dbd6ddfe13946de050d7474e3f5fb6ec774fbb1a188aaf469b08cf04189a", + "sha256:e6b7842518a63a9f17107eb176320960ec095a8ee3b4420b5f688e24bf50c53c", + "sha256:e75753aeda0ddc4c28dce4c32ba2f6ec30b1b02f6c0b14e547841ba5b24f753f", + "sha256:eadb9f826c138e6cf3c49d6f8de88225a3c0ab181a9b4ba792e006e5292d150e", + "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011", + "sha256:ef254a06bcea461e65ff0373d8a0dd1ed3aa004af48839f002a0c994a6f72d04", + "sha256:f3709997b228685fe53e8c433e2df9f0cdb5f4542bd5114ed17ac3c0129b0480", + "sha256:f51bab98d52739c50c56658cc303f190785f9a2cd97b823357e7aeae54c8f68a", + "sha256:f9904e24646570539a8950400602d66d2b2c492b9010ea7e965025cb71d0c86d", + "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d" ], "markers": "platform_system != 'Windows'", - "version": "==1.0.7" + "version": "==1.0.8" }, "numpy": { "hashes": [ @@ -107,53 +107,54 @@ }, "packaging": { "hashes": [ - "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", - "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", + "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" ], - "markers": "python_version >= '3.7'", - "version": "==23.2" + "markers": "python_version >= '3.8'", + "version": "==24.1" }, "pydantic": { "hashes": [ - "sha256:1061c6ee6204f4f5a27133126854948e3b3d51fcc16ead2e5d04378c199b2f44", - "sha256:19b5686387ea0d1ea52ecc4cffb71abb21702c5e5b2ac626fd4dbaa0834aa49d", - "sha256:2bd446bdb7755c3a94e56d7bdfd3ee92396070efa8ef3a34fab9579fe6aa1d84", - "sha256:328558c9f2eed77bd8fffad3cef39dbbe3edc7044517f4625a769d45d4cf7555", - "sha256:32e0b4fb13ad4db4058a7c3c80e2569adbd810c25e6ca3bbd8b2a9cc2cc871d7", - "sha256:3ee0d69b2a5b341fc7927e92cae7ddcfd95e624dfc4870b32a85568bd65e6131", - "sha256:4aafd4e55e8ad5bd1b19572ea2df546ccace7945853832bb99422a79c70ce9b8", - "sha256:4b3946f87e5cef3ba2e7bd3a4eb5a20385fe36521d6cc1ebf3c08a6697c6cfb3", - "sha256:4de71c718c9756d679420c69f216776c2e977459f77e8f679a4a961dc7304a56", - "sha256:5565a49effe38d51882cb7bac18bda013cdb34d80ac336428e8908f0b72499b0", - "sha256:5803ad846cdd1ed0d97eb00292b870c29c1f03732a010e66908ff48a762f20e4", - "sha256:5da164119602212a3fe7e3bc08911a89db4710ae51444b4224c2382fd09ad453", - "sha256:615661bfc37e82ac677543704437ff737418e4ea04bef9cf11c6d27346606044", - "sha256:78a4d6bdfd116a559aeec9a4cfe77dda62acc6233f8b56a716edad2651023e5e", - "sha256:7d0f183b305629765910eaad707800d2f47c6ac5bcfb8c6397abdc30b69eeb15", - "sha256:7ead3cd020d526f75b4188e0a8d71c0dbbe1b4b6b5dc0ea775a93aca16256aeb", - "sha256:84d76ecc908d917f4684b354a39fd885d69dd0491be175f3465fe4b59811c001", - "sha256:8cb0bc509bfb71305d7a59d00163d5f9fc4530f0881ea32c74ff4f74c85f3d3d", - "sha256:91089b2e281713f3893cd01d8e576771cd5bfdfbff5d0ed95969f47ef6d676c3", - "sha256:9c9e04a6cdb7a363d7cb3ccf0efea51e0abb48e180c0d31dca8d247967d85c6e", - "sha256:a8c5360a0297a713b4123608a7909e6869e1b56d0e96eb0d792c27585d40757f", - "sha256:afacf6d2a41ed91fc631bade88b1d319c51ab5418870802cedb590b709c5ae3c", - "sha256:b34ba24f3e2d0b39b43f0ca62008f7ba962cff51efa56e64ee25c4af6eed987b", - "sha256:bd67cb2c2d9602ad159389c29e4ca964b86fa2f35c2faef54c3eb28b4efd36c8", - "sha256:c0f5e142ef8217019e3eef6ae1b6b55f09a7a15972958d44fbd228214cede567", - "sha256:cdb4272678db803ddf94caa4f94f8672e9a46bae4a44f167095e4d06fec12979", - "sha256:d70916235d478404a3fa8c997b003b5f33aeac4686ac1baa767234a0f8ac2326", - "sha256:d8ce3fb0841763a89322ea0432f1f59a2d3feae07a63ea2c958b2315e1ae8adb", - "sha256:e0b214e57623a535936005797567231a12d0da0c29711eb3514bc2b3cd008d0f", - "sha256:e631c70c9280e3129f071635b81207cad85e6c08e253539467e4ead0e5b219aa", - "sha256:e78578f0c7481c850d1c969aca9a65405887003484d24f6110458fb02cca7747", - "sha256:f0ca86b525264daa5f6b192f216a0d1e860b7383e3da1c65a1908f9c02f42801", - "sha256:f1a68f4f65a9ee64b6ccccb5bf7e17db07caebd2730109cb8a95863cfa9c4e55", - "sha256:fafe841be1103f340a24977f61dee76172e4ae5f647ab9e7fd1e1fca51524f08", - "sha256:ff68fc85355532ea77559ede81f35fff79a6a5543477e168ab3a381887caea76" + "sha256:0fe8a415cea8f340e7a9af9c54fc71a649b43e8ca3cc732986116b3cb135d303", + "sha256:1289c180abd4bd4555bb927c42ee42abc3aee02b0fb2d1223fb7c6e5bef87dbe", + "sha256:1eb2085c13bce1612da8537b2d90f549c8cbb05c67e8f22854e201bde5d98a47", + "sha256:2031de0967c279df0d8a1c72b4ffc411ecd06bac607a212892757db7462fc494", + "sha256:2a7bac939fa326db1ab741c9d7f44c565a1d1e80908b3797f7f81a4f86bc8d33", + "sha256:2d5a58feb9a39f481eda4d5ca220aa8b9d4f21a41274760b9bc66bfd72595b86", + "sha256:2f9a6fab5f82ada41d56b0602606a5506aab165ca54e52bc4545028382ef1c5d", + "sha256:2fcfb5296d7877af406ba1547dfde9943b1256d8928732267e2653c26938cd9c", + "sha256:549a8e3d81df0a85226963611950b12d2d334f214436a19537b2efed61b7639a", + "sha256:598da88dfa127b666852bef6d0d796573a8cf5009ffd62104094a4fe39599565", + "sha256:5d1197e462e0364906cbc19681605cb7c036f2475c899b6f296104ad42b9f5fb", + "sha256:69328e15cfda2c392da4e713443c7dbffa1505bc9d566e71e55abe14c97ddc62", + "sha256:6a9dfa722316f4acf4460afdf5d41d5246a80e249c7ff475c43a3a1e9d75cf62", + "sha256:6b30bcb8cbfccfcf02acb8f1a261143fab622831d9c0989707e0e659f77a18e0", + "sha256:6c076be61cd0177a8433c0adcb03475baf4ee91edf5a4e550161ad57fc90f523", + "sha256:771735dc43cf8383959dc9b90aa281f0b6092321ca98677c5fb6125a6f56d58d", + "sha256:795e34e6cc065f8f498c89b894a3c6da294a936ee71e644e4bd44de048af1405", + "sha256:87afda5539d5140cb8ba9e8b8c8865cb5b1463924d38490d73d3ccfd80896b3f", + "sha256:8fb2aa3ab3728d950bcc885a2e9eff6c8fc40bc0b7bb434e555c215491bcf48b", + "sha256:a1fcb59f2f355ec350073af41d927bf83a63b50e640f4dbaa01053a28b7a7718", + "sha256:a5e7add47a5b5a40c49b3036d464e3c7802f8ae0d1e66035ea16aa5b7a3923ed", + "sha256:a73f489aebd0c2121ed974054cb2759af8a9f747de120acd2c3394cf84176ccb", + "sha256:ab26038b8375581dc832a63c948f261ae0aa21f1d34c1293469f135fa92972a5", + "sha256:b0d191db0f92dfcb1dec210ca244fdae5cbe918c6050b342d619c09d31eea0cc", + "sha256:b749a43aa51e32839c9d71dc67eb1e4221bb04af1033a32e3923d46f9effa942", + "sha256:b7ccf02d7eb340b216ec33e53a3a629856afe1c6e0ef91d84a4e6f2fb2ca70fe", + "sha256:ba5b2e6fe6ca2b7e013398bc7d7b170e21cce322d266ffcd57cca313e54fb246", + "sha256:ba5c4a8552bff16c61882db58544116d021d0b31ee7c66958d14cf386a5b5350", + "sha256:c79e6a11a07da7374f46970410b41d5e266f7f38f6a17a9c4823db80dadf4303", + "sha256:ca48477862372ac3770969b9d75f1bf66131d386dba79506c46d75e6b48c1e09", + "sha256:dea7adcc33d5d105896401a1f37d56b47d443a2b2605ff8a969a0ed5543f7e33", + "sha256:e0a16d274b588767602b7646fa05af2782576a6cf1022f4ba74cbb4db66f6ca8", + "sha256:e4129b528c6baa99a429f97ce733fff478ec955513630e61b49804b6cf9b224a", + "sha256:e5f805d2d5d0a41633651a73fa4ecdd0b3d7a49de4ec3fadf062fe16501ddbf1", + "sha256:ef6c96b2baa2100ec91a4b428f80d8f28a3c9e53568219b6c298c1125572ebc6", + "sha256:fdbdd1d630195689f325c9ef1a12900524dceb503b00a987663ff4f58669b93d" ], "index": "pypi", - "markers": "python_full_version >= '3.6.1'", - "version": "==1.9.2" + "markers": "python_version >= '3.7'", + "version": "==1.10.12" }, "pyserial": { "hashes": [ @@ -174,20 +175,20 @@ }, "setuptools": { "hashes": [ - "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05", - "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78" + "sha256:3d8531791a27056f4a38cd3e54084d8b1c4228ff9cf3f2d7dd075ec99f9fd70d", + "sha256:f501b6e6db709818dc76882582d9c516bf3b67b948864c5fa1d1624c09a49207" ], "markers": "python_version >= '3.8'", - "version": "==69.0.3" + "version": "==71.0.3" }, "typing-extensions": { "hashes": [ - "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", - "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" + "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", + "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==4.9.0" + "version": "==4.12.2" }, "wrapt": { "hashes": [ @@ -315,114 +316,114 @@ }, "contourpy": { "hashes": [ - "sha256:0274c1cb63625972c0c007ab14dd9ba9e199c36ae1a231ce45d725cbcbfd10a8", - "sha256:0d7e03c0f9a4f90dc18d4e77e9ef4ec7b7bbb437f7f675be8e530d65ae6ef956", - "sha256:11f8d2554e52f459918f7b8e6aa20ec2a3bce35ce95c1f0ef4ba36fbda306df5", - "sha256:139d8d2e1c1dd52d78682f505e980f592ba53c9f73bd6be102233e358b401063", - "sha256:16a7380e943a6d52472096cb7ad5264ecee36ed60888e2a3d3814991a0107286", - "sha256:171f311cb758de7da13fc53af221ae47a5877be5a0843a9fe150818c51ed276a", - "sha256:18fc2b4ed8e4a8fe849d18dce4bd3c7ea637758c6343a1f2bae1e9bd4c9f4686", - "sha256:1c203f617abc0dde5792beb586f827021069fb6d403d7f4d5c2b543d87edceb9", - "sha256:1c2559d6cffc94890b0529ea7eeecc20d6fadc1539273aa27faf503eb4656d8f", - "sha256:1c88dfb9e0c77612febebb6ac69d44a8d81e3dc60f993215425b62c1161353f4", - "sha256:1e9dc350fb4c58adc64df3e0703ab076f60aac06e67d48b3848c23647ae4310e", - "sha256:247b9d16535acaa766d03037d8e8fb20866d054d3c7fbf6fd1f993f11fc60ca0", - "sha256:266270c6f6608340f6c9836a0fb9b367be61dde0c9a9a18d5ece97774105ff3e", - "sha256:34b9071c040d6fe45d9826cbbe3727d20d83f1b6110d219b83eb0e2a01d79488", - "sha256:3d7d1f8871998cdff5d2ff6a087e5e1780139abe2838e85b0b46b7ae6cc25399", - "sha256:461e3ae84cd90b30f8d533f07d87c00379644205b1d33a5ea03381edc4b69431", - "sha256:464b423bc2a009088f19bdf1f232299e8b6917963e2b7e1d277da5041f33a779", - "sha256:491b1917afdd8638a05b611a56d46587d5a632cabead889a5440f7c638bc6ed9", - "sha256:4a1b1208102be6e851f20066bf0e7a96b7d48a07c9b0cfe6d0d4545c2f6cadab", - "sha256:575bcaf957a25d1194903a10bc9f316c136c19f24e0985a2b9b5608bdf5dbfe0", - "sha256:5c6b28956b7b232ae801406e529ad7b350d3f09a4fde958dfdf3c0520cdde0dd", - "sha256:5d16edfc3fc09968e09ddffada434b3bf989bf4911535e04eada58469873e28e", - "sha256:5fd1810973a375ca0e097dee059c407913ba35723b111df75671a1976efa04bc", - "sha256:67b7f17679fa62ec82b7e3e611c43a016b887bd64fb933b3ae8638583006c6d6", - "sha256:68ce4788b7d93e47f84edd3f1f95acdcd142ae60bc0e5493bfd120683d2d4316", - "sha256:6d3364b999c62f539cd403f8123ae426da946e142312a514162adb2addd8d808", - "sha256:6e739530c662a8d6d42c37c2ed52a6f0932c2d4a3e8c1f90692ad0ce1274abe0", - "sha256:6fdd887f17c2f4572ce548461e4f96396681212d858cae7bd52ba3310bc6f00f", - "sha256:78e6ad33cf2e2e80c5dfaaa0beec3d61face0fb650557100ee36db808bfa6843", - "sha256:884c3f9d42d7218304bc74a8a7693d172685c84bd7ab2bab1ee567b769696df9", - "sha256:8d8faf05be5ec8e02a4d86f616fc2a0322ff4a4ce26c0f09d9f7fb5330a35c95", - "sha256:999c71939aad2780f003979b25ac5b8f2df651dac7b38fb8ce6c46ba5abe6ae9", - "sha256:99ad97258985328b4f207a5e777c1b44a83bfe7cf1f87b99f9c11d4ee477c4de", - "sha256:9e6c93b5b2dbcedad20a2f18ec22cae47da0d705d454308063421a3b290d9ea4", - "sha256:ab459a1cbbf18e8698399c595a01f6dcc5c138220ca3ea9e7e6126232d102bb4", - "sha256:b69303ceb2e4d4f146bf82fda78891ef7bcd80c41bf16bfca3d0d7eb545448aa", - "sha256:b7caf9b241464c404613512d5594a6e2ff0cc9cb5615c9475cc1d9b514218ae8", - "sha256:b95a225d4948b26a28c08307a60ac00fb8671b14f2047fc5476613252a129776", - "sha256:bd2f1ae63998da104f16a8b788f685e55d65760cd1929518fd94cd682bf03e41", - "sha256:be16975d94c320432657ad2402f6760990cb640c161ae6da1363051805fa8108", - "sha256:ce96dd400486e80ac7d195b2d800b03e3e6a787e2a522bfb83755938465a819e", - "sha256:dbd50d0a0539ae2e96e537553aff6d02c10ed165ef40c65b0e27e744a0f10af8", - "sha256:dd10c26b4eadae44783c45ad6655220426f971c61d9b239e6f7b16d5cdaaa727", - "sha256:ebeac59e9e1eb4b84940d076d9f9a6cec0064e241818bcb6e32124cc5c3e377a" + "sha256:00e5388f71c1a0610e6fe56b5c44ab7ba14165cdd6d695429c5cd94021e390b2", + "sha256:10a37ae557aabf2509c79715cd20b62e4c7c28b8cd62dd7d99e5ed3ce28c3fd9", + "sha256:11959f0ce4a6f7b76ec578576a0b61a28bdc0696194b6347ba3f1c53827178b9", + "sha256:187fa1d4c6acc06adb0fae5544c59898ad781409e61a926ac7e84b8f276dcef4", + "sha256:1a07fc092a4088ee952ddae19a2b2a85757b923217b7eed584fdf25f53a6e7ce", + "sha256:1cac0a8f71a041aa587410424ad46dfa6a11f6149ceb219ce7dd48f6b02b87a7", + "sha256:1d59e739ab0e3520e62a26c60707cc3ab0365d2f8fecea74bfe4de72dc56388f", + "sha256:2855c8b0b55958265e8b5888d6a615ba02883b225f2227461aa9127c578a4922", + "sha256:2e785e0f2ef0d567099b9ff92cbfb958d71c2d5b9259981cd9bee81bd194c9a4", + "sha256:309be79c0a354afff9ff7da4aaed7c3257e77edf6c1b448a779329431ee79d7e", + "sha256:39f3ecaf76cd98e802f094e0d4fbc6dc9c45a8d0c4d185f0f6c2234e14e5f75b", + "sha256:457499c79fa84593f22454bbd27670227874cd2ff5d6c84e60575c8b50a69619", + "sha256:49e70d111fee47284d9dd867c9bb9a7058a3c617274900780c43e38d90fe1205", + "sha256:4c75507d0a55378240f781599c30e7776674dbaf883a46d1c90f37e563453480", + "sha256:4c863140fafc615c14a4bf4efd0f4425c02230eb8ef02784c9a156461e62c965", + "sha256:4d8908b3bee1c889e547867ca4cdc54e5ab6be6d3e078556814a22457f49423c", + "sha256:5b9eb0ca724a241683c9685a484da9d35c872fd42756574a7cfbf58af26677fd", + "sha256:6022cecf8f44e36af10bd9118ca71f371078b4c168b6e0fab43d4a889985dbb5", + "sha256:6150ffa5c767bc6332df27157d95442c379b7dce3a38dff89c0f39b63275696f", + "sha256:62828cada4a2b850dbef89c81f5a33741898b305db244904de418cc957ff05dc", + "sha256:7b4182299f251060996af5249c286bae9361fa8c6a9cda5efc29fe8bfd6062ec", + "sha256:94b34f32646ca0414237168d68a9157cb3889f06b096612afdd296003fdd32fd", + "sha256:9ce6889abac9a42afd07a562c2d6d4b2b7134f83f18571d859b25624a331c90b", + "sha256:9cffe0f850e89d7c0012a1fb8730f75edd4320a0a731ed0c183904fe6ecfc3a9", + "sha256:a12a813949e5066148712a0626895c26b2578874e4cc63160bb007e6df3436fe", + "sha256:a1eea9aecf761c661d096d39ed9026574de8adb2ae1c5bd7b33558af884fb2ce", + "sha256:a31f94983fecbac95e58388210427d68cd30fe8a36927980fab9c20062645609", + "sha256:ac58bdee53cbeba2ecad824fa8159493f0bf3b8ea4e93feb06c9a465d6c87da8", + "sha256:af3f4485884750dddd9c25cb7e3915d83c2db92488b38ccb77dd594eac84c4a0", + "sha256:b33d2bc4f69caedcd0a275329eb2198f560b325605810895627be5d4b876bf7f", + "sha256:b59c0ffceff8d4d3996a45f2bb6f4c207f94684a96bf3d9728dbb77428dd8cb8", + "sha256:bb6834cbd983b19f06908b45bfc2dad6ac9479ae04abe923a275b5f48f1a186b", + "sha256:bd3db01f59fdcbce5b22afad19e390260d6d0222f35a1023d9adc5690a889364", + "sha256:bd7c23df857d488f418439686d3b10ae2fbf9bc256cd045b37a8c16575ea1040", + "sha256:c2528d60e398c7c4c799d56f907664673a807635b857df18f7ae64d3e6ce2d9f", + "sha256:d31a63bc6e6d87f77d71e1abbd7387ab817a66733734883d1fc0021ed9bfa083", + "sha256:d4492d82b3bc7fbb7e3610747b159869468079fe149ec5c4d771fa1f614a14df", + "sha256:ddcb8581510311e13421b1f544403c16e901c4e8f09083c881fab2be80ee31ba", + "sha256:e1d59258c3c67c865435d8fbeb35f8c59b8bef3d6f46c1f29f6123556af28445", + "sha256:eb3315a8a236ee19b6df481fc5f997436e8ade24a9f03dfdc6bd490fea20c6da", + "sha256:ef2b055471c0eb466033760a521efb9d8a32b99ab907fc8358481a1dd29e3bd3", + "sha256:ef5adb9a3b1d0c645ff694f9bca7702ec2c70f4d734f9922ea34de02294fdf72", + "sha256:f32c38afb74bd98ce26de7cc74a67b40afb7b05aae7b42924ea990d51e4dac02", + "sha256:fe0ccca550bb8e5abc22f530ec0466136379c01321fd94f30a22231e8a48d985" ], "markers": "python_version >= '3.9'", - "version": "==1.2.0" + "version": "==1.2.1" }, "coverage": { "extras": [ "toml" ], "hashes": [ - "sha256:04387a4a6ecb330c1878907ce0dc04078ea72a869263e53c72a1ba5bbdf380ca", - "sha256:0676cd0ba581e514b7f726495ea75aba3eb20899d824636c6f59b0ed2f88c471", - "sha256:0e8d06778e8fbffccfe96331a3946237f87b1e1d359d7fbe8b06b96c95a5407a", - "sha256:0eb3c2f32dabe3a4aaf6441dde94f35687224dfd7eb2a7f47f3fd9428e421058", - "sha256:109f5985182b6b81fe33323ab4707011875198c41964f014579cf82cebf2bb85", - "sha256:13eaf476ec3e883fe3e5fe3707caeb88268a06284484a3daf8250259ef1ba143", - "sha256:164fdcc3246c69a6526a59b744b62e303039a81e42cfbbdc171c91a8cc2f9446", - "sha256:26776ff6c711d9d835557ee453082025d871e30b3fd6c27fcef14733f67f0590", - "sha256:26f66da8695719ccf90e794ed567a1549bb2644a706b41e9f6eae6816b398c4a", - "sha256:29f3abe810930311c0b5d1a7140f6395369c3db1be68345638c33eec07535105", - "sha256:316543f71025a6565677d84bc4df2114e9b6a615aa39fb165d697dba06a54af9", - "sha256:36b0ea8ab20d6a7564e89cb6135920bc9188fb5f1f7152e94e8300b7b189441a", - "sha256:3cc9d4bc55de8003663ec94c2f215d12d42ceea128da8f0f4036235a119c88ac", - "sha256:485e9f897cf4856a65a57c7f6ea3dc0d4e6c076c87311d4bc003f82cfe199d25", - "sha256:5040148f4ec43644702e7b16ca864c5314ccb8ee0751ef617d49aa0e2d6bf4f2", - "sha256:51456e6fa099a8d9d91497202d9563a320513fcf59f33991b0661a4a6f2ad450", - "sha256:53d7d9158ee03956e0eadac38dfa1ec8068431ef8058fe6447043db1fb40d932", - "sha256:5a10a4920def78bbfff4eff8a05c51be03e42f1c3735be42d851f199144897ba", - "sha256:5b14b4f8760006bfdb6e08667af7bc2d8d9bfdb648351915315ea17645347137", - "sha256:5b2ccb7548a0b65974860a78c9ffe1173cfb5877460e5a229238d985565574ae", - "sha256:697d1317e5290a313ef0d369650cfee1a114abb6021fa239ca12b4849ebbd614", - "sha256:6ae8c9d301207e6856865867d762a4b6fd379c714fcc0607a84b92ee63feff70", - "sha256:707c0f58cb1712b8809ece32b68996ee1e609f71bd14615bd8f87a1293cb610e", - "sha256:74775198b702868ec2d058cb92720a3c5a9177296f75bd97317c787daf711505", - "sha256:756ded44f47f330666843b5781be126ab57bb57c22adbb07d83f6b519783b870", - "sha256:76f03940f9973bfaee8cfba70ac991825611b9aac047e5c80d499a44079ec0bc", - "sha256:79287fd95585ed36e83182794a57a46aeae0b64ca53929d1176db56aacc83451", - "sha256:799c8f873794a08cdf216aa5d0531c6a3747793b70c53f70e98259720a6fe2d7", - "sha256:7d360587e64d006402b7116623cebf9d48893329ef035278969fa3bbf75b697e", - "sha256:80b5ee39b7f0131ebec7968baa9b2309eddb35b8403d1869e08f024efd883566", - "sha256:815ac2d0f3398a14286dc2cea223a6f338109f9ecf39a71160cd1628786bc6f5", - "sha256:83c2dda2666fe32332f8e87481eed056c8b4d163fe18ecc690b02802d36a4d26", - "sha256:846f52f46e212affb5bcf131c952fb4075b55aae6b61adc9856222df89cbe3e2", - "sha256:936d38794044b26c99d3dd004d8af0035ac535b92090f7f2bb5aa9c8e2f5cd42", - "sha256:9864463c1c2f9cb3b5db2cf1ff475eed2f0b4285c2aaf4d357b69959941aa555", - "sha256:995ea5c48c4ebfd898eacb098164b3cc826ba273b3049e4a889658548e321b43", - "sha256:a1526d265743fb49363974b7aa8d5899ff64ee07df47dd8d3e37dcc0818f09ed", - "sha256:a56de34db7b7ff77056a37aedded01b2b98b508227d2d0979d373a9b5d353daa", - "sha256:a7c97726520f784239f6c62506bc70e48d01ae71e9da128259d61ca5e9788516", - "sha256:b8e99f06160602bc64da35158bb76c73522a4010f0649be44a4e167ff8555952", - "sha256:bb1de682da0b824411e00a0d4da5a784ec6496b6850fdf8c865c1d68c0e318dd", - "sha256:bf477c355274a72435ceb140dc42de0dc1e1e0bf6e97195be30487d8eaaf1a09", - "sha256:bf635a52fc1ea401baf88843ae8708591aa4adff875e5c23220de43b1ccf575c", - "sha256:bfd5db349d15c08311702611f3dccbef4b4e2ec148fcc636cf8739519b4a5c0f", - "sha256:c530833afc4707fe48524a44844493f36d8727f04dcce91fb978c414a8556cc6", - "sha256:cc6d65b21c219ec2072c1293c505cf36e4e913a3f936d80028993dd73c7906b1", - "sha256:cd3c1e4cb2ff0083758f09be0f77402e1bdf704adb7f89108007300a6da587d0", - "sha256:cfd2a8b6b0d8e66e944d47cdec2f47c48fef2ba2f2dff5a9a75757f64172857e", - "sha256:d0ca5c71a5a1765a0f8f88022c52b6b8be740e512980362f7fdbb03725a0d6b9", - "sha256:e7defbb9737274023e2d7af02cac77043c86ce88a907c58f42b580a97d5bcca9", - "sha256:e9d1bf53c4c8de58d22e0e956a79a5b37f754ed1ffdbf1a260d9dcfa2d8a325e", - "sha256:ea81d8f9691bb53f4fb4db603203029643caffc82bf998ab5b59ca05560f4c06" + "sha256:0086cd4fc71b7d485ac93ca4239c8f75732c2ae3ba83f6be1c9be59d9e2c6382", + "sha256:01c322ef2bbe15057bc4bf132b525b7e3f7206f071799eb8aa6ad1940bcf5fb1", + "sha256:03cafe82c1b32b770a29fd6de923625ccac3185a54a5e66606da26d105f37dac", + "sha256:044a0985a4f25b335882b0966625270a8d9db3d3409ddc49a4eb00b0ef5e8cee", + "sha256:07ed352205574aad067482e53dd606926afebcb5590653121063fbf4e2175166", + "sha256:0d1b923fc4a40c5832be4f35a5dab0e5ff89cddf83bb4174499e02ea089daf57", + "sha256:0e7b27d04131c46e6894f23a4ae186a6a2207209a05df5b6ad4caee6d54a222c", + "sha256:1fad32ee9b27350687035cb5fdf9145bc9cf0a094a9577d43e909948ebcfa27b", + "sha256:289cc803fa1dc901f84701ac10c9ee873619320f2f9aff38794db4a4a0268d51", + "sha256:3c59105f8d58ce500f348c5b56163a4113a440dad6daa2294b5052a10db866da", + "sha256:46c3d091059ad0b9c59d1034de74a7f36dcfa7f6d3bde782c49deb42438f2450", + "sha256:482855914928c8175735a2a59c8dc5806cf7d8f032e4820d52e845d1f731dca2", + "sha256:49c76cdfa13015c4560702574bad67f0e15ca5a2872c6a125f6327ead2b731dd", + "sha256:4b03741e70fb811d1a9a1d75355cf391f274ed85847f4b78e35459899f57af4d", + "sha256:4bea27c4269234e06f621f3fac3925f56ff34bc14521484b8f66a580aacc2e7d", + "sha256:4d5fae0a22dc86259dee66f2cc6c1d3e490c4a1214d7daa2a93d07491c5c04b6", + "sha256:543ef9179bc55edfd895154a51792b01c017c87af0ebaae092720152e19e42ca", + "sha256:54dece71673b3187c86226c3ca793c5f891f9fc3d8aa183f2e3653da18566169", + "sha256:6379688fb4cfa921ae349c76eb1a9ab26b65f32b03d46bb0eed841fd4cb6afb1", + "sha256:65fa405b837060db569a61ec368b74688f429b32fa47a8929a7a2f9b47183713", + "sha256:6616d1c9bf1e3faea78711ee42a8b972367d82ceae233ec0ac61cc7fec09fa6b", + "sha256:6fe885135c8a479d3e37a7aae61cbd3a0fb2deccb4dda3c25f92a49189f766d6", + "sha256:7221f9ac9dad9492cecab6f676b3eaf9185141539d5c9689d13fd6b0d7de840c", + "sha256:76d5f82213aa78098b9b964ea89de4617e70e0d43e97900c2778a50856dac605", + "sha256:7792f0ab20df8071d669d929c75c97fecfa6bcab82c10ee4adb91c7a54055463", + "sha256:831b476d79408ab6ccfadaaf199906c833f02fdb32c9ab907b1d4aa0713cfa3b", + "sha256:9146579352d7b5f6412735d0f203bbd8d00113a680b66565e205bc605ef81bc6", + "sha256:9cc44bf0315268e253bf563f3560e6c004efe38f76db03a1558274a6e04bf5d5", + "sha256:a73d18625f6a8a1cbb11eadc1d03929f9510f4131879288e3f7922097a429f63", + "sha256:a8659fd33ee9e6ca03950cfdcdf271d645cf681609153f218826dd9805ab585c", + "sha256:a94925102c89247530ae1dab7dc02c690942566f22e189cbd53579b0693c0783", + "sha256:ad4567d6c334c46046d1c4c20024de2a1c3abc626817ae21ae3da600f5779b44", + "sha256:b2e16f4cd2bc4d88ba30ca2d3bbf2f21f00f382cf4e1ce3b1ddc96c634bc48ca", + "sha256:bbdf9a72403110a3bdae77948b8011f644571311c2fb35ee15f0f10a8fc082e8", + "sha256:beb08e8508e53a568811016e59f3234d29c2583f6b6e28572f0954a6b4f7e03d", + "sha256:c4cbe651f3904e28f3a55d6f371203049034b4ddbce65a54527a3f189ca3b390", + "sha256:c7b525ab52ce18c57ae232ba6f7010297a87ced82a2383b1afd238849c1ff933", + "sha256:ca5d79cfdae420a1d52bf177de4bc2289c321d6c961ae321503b2ca59c17ae67", + "sha256:cdab02a0a941af190df8782aafc591ef3ad08824f97850b015c8c6a8b3877b0b", + "sha256:d17c6a415d68cfe1091d3296ba5749d3d8696e42c37fca5d4860c5bf7b729f03", + "sha256:d39bd10f0ae453554798b125d2f39884290c480f56e8a02ba7a6ed552005243b", + "sha256:d4b3cd1ca7cd73d229487fa5caca9e4bc1f0bca96526b922d61053ea751fe791", + "sha256:d50a252b23b9b4dfeefc1f663c568a221092cbaded20a05a11665d0dbec9b8fb", + "sha256:da8549d17489cd52f85a9829d0e1d91059359b3c54a26f28bec2c5d369524807", + "sha256:dcd070b5b585b50e6617e8972f3fbbee786afca71b1936ac06257f7e178f00f6", + "sha256:ddaaa91bfc4477d2871442bbf30a125e8fe6b05da8a0015507bfbf4718228ab2", + "sha256:df423f351b162a702c053d5dddc0fc0ef9a9e27ea3f449781ace5f906b664428", + "sha256:dff044f661f59dace805eedb4a7404c573b6ff0cdba4a524141bc63d7be5c7fd", + "sha256:e7e128f85c0b419907d1f38e616c4f1e9f1d1b37a7949f44df9a73d5da5cd53c", + "sha256:ed8d1d1821ba5fc88d4a4f45387b65de52382fa3ef1f0115a4f7a20cdfab0e94", + "sha256:f2501d60d7497fd55e391f423f965bbe9e650e9ffc3c627d5f0ac516026000b8", + "sha256:f7db0b6ae1f96ae41afe626095149ecd1b212b424626175a6633c2999eaad45b" ], "markers": "python_version >= '3.8'", - "version": "==7.4.0" + "version": "==7.6.0" }, "cycler": { "hashes": [ @@ -434,11 +435,11 @@ }, "exceptiongroup": { "hashes": [ - "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14", - "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68" + "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", + "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc" ], "markers": "python_version < '3.11'", - "version": "==1.2.0" + "version": "==1.2.2" }, "flake8": { "hashes": [ @@ -478,60 +479,60 @@ }, "fonttools": { "hashes": [ - "sha256:0255dbc128fee75fb9be364806b940ed450dd6838672a150d501ee86523ac61e", - "sha256:0a00bd0e68e88987dcc047ea31c26d40a3c61185153b03457956a87e39d43c37", - "sha256:0a1d313a415eaaba2b35d6cd33536560deeebd2ed758b9bfb89ab5d97dc5deac", - "sha256:0f750037e02beb8b3569fbff701a572e62a685d2a0e840d75816592280e5feae", - "sha256:13819db8445a0cec8c3ff5f243af6418ab19175072a9a92f6cc8ca7d1452754b", - "sha256:254d9a6f7be00212bf0c3159e0a420eb19c63793b2c05e049eb337f3023c5ecc", - "sha256:29495d6d109cdbabe73cfb6f419ce67080c3ef9ea1e08d5750240fd4b0c4763b", - "sha256:32ab2e9702dff0dd4510c7bb958f265a8d3dd5c0e2547e7b5f7a3df4979abb07", - "sha256:3480eeb52770ff75140fe7d9a2ec33fb67b07efea0ab5129c7e0c6a639c40c70", - "sha256:3a808f3c1d1df1f5bf39be869b6e0c263570cdafb5bdb2df66087733f566ea71", - "sha256:3b629108351d25512d4ea1a8393a2dba325b7b7d7308116b605ea3f8e1be88df", - "sha256:3d71606c9321f6701642bd4746f99b6089e53d7e9817fc6b964e90d9c5f0ecc6", - "sha256:3e2b95dce2ead58fb12524d0ca7d63a63459dd489e7e5838c3cd53557f8933e1", - "sha256:4a5a5318ba5365d992666ac4fe35365f93004109d18858a3e18ae46f67907670", - "sha256:4c811d3c73b6abac275babb8aa439206288f56fdb2c6f8835e3d7b70de8937a7", - "sha256:4e743935139aa485fe3253fc33fe467eab6ea42583fa681223ea3f1a93dd01e6", - "sha256:4ec558c543609e71b2275c4894e93493f65d2f41c15fe1d089080c1d0bb4d635", - "sha256:5465df494f20a7d01712b072ae3ee9ad2887004701b95cb2cc6dcb9c2c97a899", - "sha256:5b60e3afa9635e3dfd3ace2757039593e3bd3cf128be0ddb7a1ff4ac45fa5a50", - "sha256:63fbed184979f09a65aa9c88b395ca539c94287ba3a364517698462e13e457c9", - "sha256:69731e8bea0578b3c28fdb43dbf95b9386e2d49a399e9a4ad736b8e479b08085", - "sha256:6dd58cc03016b281bd2c74c84cdaa6bd3ce54c5a7f47478b7657b930ac3ed8eb", - "sha256:740947906590a878a4bde7dd748e85fefa4d470a268b964748403b3ab2aeed6c", - "sha256:7df26dd3650e98ca45f1e29883c96a0b9f5bb6af8d632a6a108bc744fa0bd9b3", - "sha256:7eb7ad665258fba68fd22228a09f347469d95a97fb88198e133595947a20a184", - "sha256:7ee48bd9d6b7e8f66866c9090807e3a4a56cf43ffad48962725a190e0dd774c8", - "sha256:86e0427864c6c91cf77f16d1fb9bf1bbf7453e824589e8fb8461b6ee1144f506", - "sha256:8f57ecd742545362a0f7186774b2d1c53423ed9ece67689c93a1055b236f638c", - "sha256:90f898cdd67f52f18049250a6474185ef6544c91f27a7bee70d87d77a8daf89c", - "sha256:94208ea750e3f96e267f394d5588579bb64cc628e321dbb1d4243ffbc291b18b", - "sha256:a1c154bb85dc9a4cf145250c88d112d88eb414bad81d4cb524d06258dea1bdc0", - "sha256:a5d77479fb885ef38a16a253a2f4096bc3d14e63a56d6246bfdb56365a12b20c", - "sha256:a86a5ab2873ed2575d0fcdf1828143cfc6b977ac448e3dc616bb1e3d20efbafa", - "sha256:ac71e2e201df041a2891067dc36256755b1229ae167edbdc419b16da78732c2f", - "sha256:b3e1304e5f19ca861d86a72218ecce68f391646d85c851742d265787f55457a4", - "sha256:b8be28c036b9f186e8c7eaf8a11b42373e7e4949f9e9f370202b9da4c4c3f56c", - "sha256:c19044256c44fe299d9a73456aabee4b4d06c6b930287be93b533b4737d70aa1", - "sha256:d49ce3ea7b7173faebc5664872243b40cf88814ca3eb135c4a3cdff66af71946", - "sha256:e040f905d542362e07e72e03612a6270c33d38281fd573160e1003e43718d68d", - "sha256:eabae77a07c41ae0b35184894202305c3ad211a93b2eb53837c2a1143c8bc952", - "sha256:f791446ff297fd5f1e2247c188de53c1bfb9dd7f0549eba55b73a3c2087a2703", - "sha256:f83a4daef6d2a202acb9bf572958f91cfde5b10c8ee7fb1d09a4c81e5d851fd8" + "sha256:02569e9a810f9d11f4ae82c391ebc6fb5730d95a0657d24d754ed7763fb2d122", + "sha256:0679a30b59d74b6242909945429dbddb08496935b82f91ea9bf6ad240ec23397", + "sha256:10f5e6c3510b79ea27bb1ebfcc67048cde9ec67afa87c7dd7efa5c700491ac7f", + "sha256:2af40ae9cdcb204fc1d8f26b190aa16534fcd4f0df756268df674a270eab575d", + "sha256:32f029c095ad66c425b0ee85553d0dc326d45d7059dbc227330fc29b43e8ba60", + "sha256:35250099b0cfb32d799fb5d6c651220a642fe2e3c7d2560490e6f1d3f9ae9169", + "sha256:3b3c8ebafbee8d9002bd8f1195d09ed2bd9ff134ddec37ee8f6a6375e6a4f0e8", + "sha256:4824c198f714ab5559c5be10fd1adf876712aa7989882a4ec887bf1ef3e00e31", + "sha256:5ff7e5e9bad94e3a70c5cd2fa27f20b9bb9385e10cddab567b85ce5d306ea923", + "sha256:651390c3b26b0c7d1f4407cad281ee7a5a85a31a110cbac5269de72a51551ba2", + "sha256:6e08f572625a1ee682115223eabebc4c6a2035a6917eac6f60350aba297ccadb", + "sha256:6ed170b5e17da0264b9f6fae86073be3db15fa1bd74061c8331022bca6d09bab", + "sha256:73379d3ffdeecb376640cd8ed03e9d2d0e568c9d1a4e9b16504a834ebadc2dfb", + "sha256:75a157d8d26c06e64ace9df037ee93a4938a4606a38cb7ffaf6635e60e253b7a", + "sha256:791b31ebbc05197d7aa096bbc7bd76d591f05905d2fd908bf103af4488e60670", + "sha256:7b6b35e52ddc8fb0db562133894e6ef5b4e54e1283dff606fda3eed938c36fc8", + "sha256:84ec3fb43befb54be490147b4a922b5314e16372a643004f182babee9f9c3407", + "sha256:8959a59de5af6d2bec27489e98ef25a397cfa1774b375d5787509c06659b3671", + "sha256:9dfdae43b7996af46ff9da520998a32b105c7f098aeea06b2226b30e74fbba88", + "sha256:9e6ceba2a01b448e36754983d376064730690401da1dd104ddb543519470a15f", + "sha256:9efd176f874cb6402e607e4cc9b4a9cd584d82fc34a4b0c811970b32ba62501f", + "sha256:a1c7c5aa18dd3b17995898b4a9b5929d69ef6ae2af5b96d585ff4005033d82f0", + "sha256:aae7bd54187e8bf7fd69f8ab87b2885253d3575163ad4d669a262fe97f0136cb", + "sha256:b21952c092ffd827504de7e66b62aba26fdb5f9d1e435c52477e6486e9d128b2", + "sha256:b96cd370a61f4d083c9c0053bf634279b094308d52fdc2dd9a22d8372fdd590d", + "sha256:becc5d7cb89c7b7afa8321b6bb3dbee0eec2b57855c90b3e9bf5fb816671fa7c", + "sha256:bee32ea8765e859670c4447b0817514ca79054463b6b79784b08a8df3a4d78e3", + "sha256:c6e7170d675d12eac12ad1a981d90f118c06cf680b42a2d74c6c931e54b50719", + "sha256:c818c058404eb2bba05e728d38049438afd649e3c409796723dfc17cd3f08749", + "sha256:c8696544c964500aa9439efb6761947393b70b17ef4e82d73277413f291260a4", + "sha256:c9cd19cf4fe0595ebdd1d4915882b9440c3a6d30b008f3cc7587c1da7b95be5f", + "sha256:d4d0096cb1ac7a77b3b41cd78c9b6bc4a400550e21dc7a92f2b5ab53ed74eb02", + "sha256:d92d3c2a1b39631a6131c2fa25b5406855f97969b068e7e08413325bc0afba58", + "sha256:da33440b1413bad53a8674393c5d29ce64d8c1a15ef8a77c642ffd900d07bfe1", + "sha256:e013aae589c1c12505da64a7d8d023e584987e51e62006e1bb30d72f26522c41", + "sha256:e128778a8e9bc11159ce5447f76766cefbd876f44bd79aff030287254e4752c4", + "sha256:e54f1bba2f655924c1138bbc7fa91abd61f45c68bd65ab5ed985942712864bbb", + "sha256:e5b708073ea3d684235648786f5f6153a48dc8762cdfe5563c57e80787c29fbb", + "sha256:e8bf06b94694251861ba7fdeea15c8ec0967f84c3d4143ae9daf42bbc7717fe3", + "sha256:f08df60fbd8d289152079a65da4e66a447efc1d5d5a4d3f299cdd39e3b2e4a7d", + "sha256:f1f8758a2ad110bd6432203a344269f445a2907dc24ef6bccfd0ac4e14e0d71d", + "sha256:f677ce218976496a587ab17140da141557beb91d2a5c1a14212c994093f2eae2" ], "markers": "python_version >= '3.8'", - "version": "==4.47.2" + "version": "==4.53.1" }, "hypothesis": { "hashes": [ - "sha256:848ea0952f0bdfd02eac59e41b03f1cbba8fa2cffeffa8db328bbd6cfe159974", - "sha256:955a57e56be4607c81c17ca53e594af54aadeed91e07b88bb7f84e8208ea7739" + "sha256:2beb7a148e95a2067563bcca017d71cc286805c792e43ec5cb155ed6d0a1990d", + "sha256:3b0d080bfd3b303e91388507ac7edebd7039ffcc96ac2cfcdc3c45806352c09f" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==6.96.1" + "version": "==6.96.4" }, "iniconfig": { "hashes": [ @@ -543,10 +544,19 @@ }, "jsonschema": { "hashes": [ - "sha256:5f9c0a719ca2ce14c5de2fd350a64fd2d13e8539db29836a86adc990bb1a068f", - "sha256:8d4a2b7b6c2237e0199c8ea1a6d3e05bf118e289ae2b9d7ba444182a2959560d" + "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", + "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566" + ], + "markers": "python_version >= '3.8'", + "version": "==4.23.0" + }, + "jsonschema-specifications": { + "hashes": [ + "sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc", + "sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c" ], - "version": "==3.0.2" + "markers": "python_version >= '3.8'", + "version": "==2023.12.1" }, "kiwisolver": { "hashes": [ @@ -660,38 +670,38 @@ }, "matplotlib": { "hashes": [ - "sha256:01a978b871b881ee76017152f1f1a0cbf6bd5f7b8ff8c96df0df1bd57d8755a1", - "sha256:03f9d160a29e0b65c0790bb07f4f45d6a181b1ac33eb1bb0dd225986450148f0", - "sha256:091275d18d942cf1ee9609c830a1bc36610607d8223b1b981c37d5c9fc3e46a4", - "sha256:09796f89fb71a0c0e1e2f4bdaf63fb2cefc84446bb963ecdeb40dfee7dfa98c7", - "sha256:0f4fc5d72b75e2c18e55eb32292659cf731d9d5b312a6eb036506304f4675630", - "sha256:172f4d0fbac3383d39164c6caafd3255ce6fa58f08fc392513a0b1d3b89c4f89", - "sha256:1b0f3b8ea0e99e233a4bcc44590f01604840d833c280ebb8fe5554fd3e6cfe8d", - "sha256:3773002da767f0a9323ba1a9b9b5d00d6257dbd2a93107233167cfb581f64717", - "sha256:46a569130ff53798ea5f50afce7406e91fdc471ca1e0e26ba976a8c734c9427a", - "sha256:4c318c1e95e2f5926fba326f68177dee364aa791d6df022ceb91b8221bd0a627", - "sha256:4e208f46cf6576a7624195aa047cb344a7f802e113bb1a06cfd4bee431de5e31", - "sha256:533b0e3b0c6768eef8cbe4b583731ce25a91ab54a22f830db2b031e83cca9213", - "sha256:5864bdd7da445e4e5e011b199bb67168cdad10b501750367c496420f2ad00843", - "sha256:5ba9cbd8ac6cf422f3102622b20f8552d601bf8837e49a3afed188d560152788", - "sha256:6f9c6976748a25e8b9be51ea028df49b8e561eed7809146da7a47dbecebab367", - "sha256:7c48d9e221b637c017232e3760ed30b4e8d5dfd081daf327e829bf2a72c731b4", - "sha256:830f00640c965c5b7f6bc32f0d4ce0c36dfe0379f7dd65b07a00c801713ec40a", - "sha256:9a5430836811b7652991939012f43d2808a2db9b64ee240387e8c43e2e5578c8", - "sha256:aa11b3c6928a1e496c1a79917d51d4cd5d04f8a2e75f21df4949eeefdf697f4b", - "sha256:b78e4f2cedf303869b782071b55fdde5987fda3038e9d09e58c91cc261b5ad18", - "sha256:b9576723858a78751d5aacd2497b8aef29ffea6d1c95981505877f7ac28215c6", - "sha256:bddfb1db89bfaa855912261c805bd0e10218923cc262b9159a49c29a7a1c1afa", - "sha256:c7d36c2209d9136cd8e02fab1c0ddc185ce79bc914c45054a9f514e44c787917", - "sha256:d1095fecf99eeb7384dabad4bf44b965f929a5f6079654b681193edf7169ec20", - "sha256:d7b1704a530395aaf73912be741c04d181f82ca78084fbd80bc737be04848331", - "sha256:d86593ccf546223eb75a39b44c32788e6f6440d13cfc4750c1c15d0fcb850b63", - "sha256:deaed9ad4da0b1aea77fe0aa0cebb9ef611c70b3177be936a95e5d01fa05094f", - "sha256:ef8345b48e95cee45ff25192ed1f4857273117917a4dcd48e3905619bcd9c9b8" + "sha256:1c13f041a7178f9780fb61cc3a2b10423d5e125480e4be51beaf62b172413b67", + "sha256:232ce322bfd020a434caaffbd9a95333f7c2491e59cfc014041d95e38ab90d1c", + "sha256:493e9f6aa5819156b58fce42b296ea31969f2aab71c5b680b4ea7a3cb5c07d94", + "sha256:50bac6e4d77e4262c4340d7a985c30912054745ec99756ce213bfbc3cb3808eb", + "sha256:606e3b90897554c989b1e38a258c626d46c873523de432b1462f295db13de6f9", + "sha256:6209e5c9aaccc056e63b547a8152661324404dd92340a6e479b3a7f24b42a5d0", + "sha256:6485ac1f2e84676cff22e693eaa4fbed50ef5dc37173ce1f023daef4687df616", + "sha256:6addbd5b488aedb7f9bc19f91cd87ea476206f45d7116fcfe3d31416702a82fa", + "sha256:72f9322712e4562e792b2961971891b9fbbb0e525011e09ea0d1f416c4645661", + "sha256:7a6769f58ce51791b4cb8b4d7642489df347697cd3e23d88266aaaee93b41d9a", + "sha256:8080d5081a86e690d7688ffa542532e87f224c38a6ed71f8fbed34dd1d9fedae", + "sha256:843cbde2f0946dadd8c5c11c6d91847abd18ec76859dc319362a0964493f0ba6", + "sha256:8aac397d5e9ec158960e31c381c5ffc52ddd52bd9a47717e2a694038167dffea", + "sha256:8f65c9f002d281a6e904976007b2d46a1ee2bcea3a68a8c12dda24709ddc9106", + "sha256:90df07db7b599fe7035d2f74ab7e438b656528c68ba6bb59b7dc46af39ee48ef", + "sha256:9bb0189011785ea794ee827b68777db3ca3f93f3e339ea4d920315a0e5a78d54", + "sha256:a0e47eda4eb2614300fc7bb4657fced3e83d6334d03da2173b09e447418d499f", + "sha256:abc9d838f93583650c35eca41cfcec65b2e7cb50fd486da6f0c49b5e1ed23014", + "sha256:ac24233e8f2939ac4fd2919eed1e9c0871eac8057666070e94cbf0b33dd9c338", + "sha256:b12ba985837e4899b762b81f5b2845bd1a28f4fdd1a126d9ace64e9c4eb2fb25", + "sha256:b7a2a253d3b36d90c8993b4620183b55665a429da8357a4f621e78cd48b2b30b", + "sha256:c7064120a59ce6f64103c9cefba8ffe6fba87f2c61d67c401186423c9a20fd35", + "sha256:c89ee9314ef48c72fe92ce55c4e95f2f39d70208f9f1d9db4e64079420d8d732", + "sha256:cc4ccdc64e3039fc303defd119658148f2349239871db72cd74e2eeaa9b80b71", + "sha256:ce1edd9f5383b504dbc26eeea404ed0a00656c526638129028b758fd43fc5f10", + "sha256:ecd79298550cba13a43c340581a3ec9c707bd895a6a061a78fa2524660482fc0", + "sha256:f51c4c869d4b60d769f7b4406eec39596648d9d70246428745a681c327a8ad30", + "sha256:fb44f53af0a62dc80bba4443d9b27f2fde6acfdac281d95bc872dc148a6509cc" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==3.8.2" + "version": "==3.8.4" }, "mccabe": { "hashes": [ @@ -781,16 +791,16 @@ }, "opentrons-shared-data": { "editable": true, - "markers": "python_version >= '3.7'", + "markers": "python_version >= '3.10'", "path": "../shared-data/python" }, "packaging": { "hashes": [ - "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", - "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", + "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" ], - "markers": "python_version >= '3.7'", - "version": "==23.2" + "markers": "python_version >= '3.8'", + "version": "==24.1" }, "pathspec": { "hashes": [ @@ -802,93 +812,105 @@ }, "pillow": { "hashes": [ - "sha256:0304004f8067386b477d20a518b50f3fa658a28d44e4116970abfcd94fac34a8", - "sha256:0689b5a8c5288bc0504d9fcee48f61a6a586b9b98514d7d29b840143d6734f39", - "sha256:0eae2073305f451d8ecacb5474997c08569fb4eb4ac231ffa4ad7d342fdc25ac", - "sha256:0fb3e7fc88a14eacd303e90481ad983fd5b69c761e9e6ef94c983f91025da869", - "sha256:11fa2e5984b949b0dd6d7a94d967743d87c577ff0b83392f17cb3990d0d2fd6e", - "sha256:127cee571038f252a552760076407f9cff79761c3d436a12af6000cd182a9d04", - "sha256:154e939c5f0053a383de4fd3d3da48d9427a7e985f58af8e94d0b3c9fcfcf4f9", - "sha256:15587643b9e5eb26c48e49a7b33659790d28f190fc514a322d55da2fb5c2950e", - "sha256:170aeb00224ab3dc54230c797f8404507240dd868cf52066f66a41b33169bdbe", - "sha256:1b5e1b74d1bd1b78bc3477528919414874748dd363e6272efd5abf7654e68bef", - "sha256:1da3b2703afd040cf65ec97efea81cfba59cdbed9c11d8efc5ab09df9509fc56", - "sha256:1e23412b5c41e58cec602f1135c57dfcf15482013ce6e5f093a86db69646a5aa", - "sha256:2247178effb34a77c11c0e8ac355c7a741ceca0a732b27bf11e747bbc950722f", - "sha256:257d8788df5ca62c980314053197f4d46eefedf4e6175bc9412f14412ec4ea2f", - "sha256:3031709084b6e7852d00479fd1d310b07d0ba82765f973b543c8af5061cf990e", - "sha256:322209c642aabdd6207517e9739c704dc9f9db943015535783239022002f054a", - "sha256:322bdf3c9b556e9ffb18f93462e5f749d3444ce081290352c6070d014c93feb2", - "sha256:33870dc4653c5017bf4c8873e5488d8f8d5f8935e2f1fb9a2208c47cdd66efd2", - "sha256:35bb52c37f256f662abdfa49d2dfa6ce5d93281d323a9af377a120e89a9eafb5", - "sha256:3c31822339516fb3c82d03f30e22b1d038da87ef27b6a78c9549888f8ceda39a", - "sha256:3eedd52442c0a5ff4f887fab0c1c0bb164d8635b32c894bc1faf4c618dd89df2", - "sha256:3ff074fc97dd4e80543a3e91f69d58889baf2002b6be64347ea8cf5533188213", - "sha256:47c0995fc4e7f79b5cfcab1fc437ff2890b770440f7696a3ba065ee0fd496563", - "sha256:49d9ba1ed0ef3e061088cd1e7538a0759aab559e2e0a80a36f9fd9d8c0c21591", - "sha256:51f1a1bffc50e2e9492e87d8e09a17c5eea8409cda8d3f277eb6edc82813c17c", - "sha256:52a50aa3fb3acb9cf7213573ef55d31d6eca37f5709c69e6858fe3bc04a5c2a2", - "sha256:54f1852cd531aa981bc0965b7d609f5f6cc8ce8c41b1139f6ed6b3c54ab82bfb", - "sha256:609448742444d9290fd687940ac0b57fb35e6fd92bdb65386e08e99af60bf757", - "sha256:69ffdd6120a4737710a9eee73e1d2e37db89b620f702754b8f6e62594471dee0", - "sha256:6fad5ff2f13d69b7e74ce5b4ecd12cc0ec530fcee76356cac6742785ff71c452", - "sha256:7049e301399273a0136ff39b84c3678e314f2158f50f517bc50285fb5ec847ad", - "sha256:70c61d4c475835a19b3a5aa42492409878bbca7438554a1f89d20d58a7c75c01", - "sha256:716d30ed977be8b37d3ef185fecb9e5a1d62d110dfbdcd1e2a122ab46fddb03f", - "sha256:753cd8f2086b2b80180d9b3010dd4ed147efc167c90d3bf593fe2af21265e5a5", - "sha256:773efe0603db30c281521a7c0214cad7836c03b8ccff897beae9b47c0b657d61", - "sha256:7823bdd049099efa16e4246bdf15e5a13dbb18a51b68fa06d6c1d4d8b99a796e", - "sha256:7c8f97e8e7a9009bcacbe3766a36175056c12f9a44e6e6f2d5caad06dcfbf03b", - "sha256:823ef7a27cf86df6597fa0671066c1b596f69eba53efa3d1e1cb8b30f3533068", - "sha256:8373c6c251f7ef8bda6675dd6d2b3a0fcc31edf1201266b5cf608b62a37407f9", - "sha256:83b2021f2ade7d1ed556bc50a399127d7fb245e725aa0113ebd05cfe88aaf588", - "sha256:870ea1ada0899fd0b79643990809323b389d4d1d46c192f97342eeb6ee0b8483", - "sha256:8d12251f02d69d8310b046e82572ed486685c38f02176bd08baf216746eb947f", - "sha256:9c23f307202661071d94b5e384e1e1dc7dfb972a28a2310e4ee16103e66ddb67", - "sha256:9d189550615b4948f45252d7f005e53c2040cea1af5b60d6f79491a6e147eef7", - "sha256:a086c2af425c5f62a65e12fbf385f7c9fcb8f107d0849dba5839461a129cf311", - "sha256:a2b56ba36e05f973d450582fb015594aaa78834fefe8dfb8fcd79b93e64ba4c6", - "sha256:aebb6044806f2e16ecc07b2a2637ee1ef67a11840a66752751714a0d924adf72", - "sha256:b1b3020d90c2d8e1dae29cf3ce54f8094f7938460fb5ce8bc5c01450b01fbaf6", - "sha256:b4b6b1e20608493548b1f32bce8cca185bf0480983890403d3b8753e44077129", - "sha256:b6f491cdf80ae540738859d9766783e3b3c8e5bd37f5dfa0b76abdecc5081f13", - "sha256:b792a349405fbc0163190fde0dc7b3fef3c9268292586cf5645598b48e63dc67", - "sha256:b7c2286c23cd350b80d2fc9d424fc797575fb16f854b831d16fd47ceec078f2c", - "sha256:babf5acfede515f176833ed6028754cbcd0d206f7f614ea3447d67c33be12516", - "sha256:c365fd1703040de1ec284b176d6af5abe21b427cb3a5ff68e0759e1e313a5e7e", - "sha256:c4225f5220f46b2fde568c74fca27ae9771536c2e29d7c04f4fb62c83275ac4e", - "sha256:c570f24be1e468e3f0ce7ef56a89a60f0e05b30a3669a459e419c6eac2c35364", - "sha256:c6dafac9e0f2b3c78df97e79af707cdc5ef8e88208d686a4847bab8266870023", - "sha256:c8de2789052ed501dd829e9cae8d3dcce7acb4777ea4a479c14521c942d395b1", - "sha256:cb28c753fd5eb3dd859b4ee95de66cc62af91bcff5db5f2571d32a520baf1f04", - "sha256:cb4c38abeef13c61d6916f264d4845fab99d7b711be96c326b84df9e3e0ff62d", - "sha256:d1b35bcd6c5543b9cb547dee3150c93008f8dd0f1fef78fc0cd2b141c5baf58a", - "sha256:d8e6aeb9201e655354b3ad049cb77d19813ad4ece0df1249d3c793de3774f8c7", - "sha256:d8ecd059fdaf60c1963c58ceb8997b32e9dc1b911f5da5307aab614f1ce5c2fb", - "sha256:da2b52b37dad6d9ec64e653637a096905b258d2fc2b984c41ae7d08b938a67e4", - "sha256:e87f0b2c78157e12d7686b27d63c070fd65d994e8ddae6f328e0dcf4a0cd007e", - "sha256:edca80cbfb2b68d7b56930b84a0e45ae1694aeba0541f798e908a49d66b837f1", - "sha256:f379abd2f1e3dddb2b61bc67977a6b5a0a3f7485538bcc6f39ec76163891ee48", - "sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868" + "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885", + "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea", + "sha256:06b2f7898047ae93fad74467ec3d28fe84f7831370e3c258afa533f81ef7f3df", + "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5", + "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c", + "sha256:0ae24a547e8b711ccaaf99c9ae3cd975470e1a30caa80a6aaee9a2f19c05701d", + "sha256:134ace6dc392116566980ee7436477d844520a26a4b1bd4053f6f47d096997fd", + "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06", + "sha256:1b5dea9831a90e9d0721ec417a80d4cbd7022093ac38a568db2dd78363b00908", + "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a", + "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be", + "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0", + "sha256:298478fe4f77a4408895605f3482b6cc6222c018b2ce565c2b6b9c354ac3229b", + "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80", + "sha256:2db98790afc70118bd0255c2eeb465e9767ecf1f3c25f9a1abb8ffc8cfd1fe0a", + "sha256:32cda9e3d601a52baccb2856b8ea1fc213c90b340c542dcef77140dfa3278a9e", + "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9", + "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696", + "sha256:43efea75eb06b95d1631cb784aa40156177bf9dd5b4b03ff38979e048258bc6b", + "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309", + "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e", + "sha256:5161eef006d335e46895297f642341111945e2c1c899eb406882a6c61a4357ab", + "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d", + "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060", + "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d", + "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d", + "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4", + "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3", + "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6", + "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb", + "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94", + "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b", + "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496", + "sha256:73664fe514b34c8f02452ffb73b7a92c6774e39a647087f83d67f010eb9a0cf0", + "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319", + "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b", + "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856", + "sha256:7970285ab628a3779aecc35823296a7869f889b8329c16ad5a71e4901a3dc4ef", + "sha256:7a8d4bade9952ea9a77d0c3e49cbd8b2890a399422258a77f357b9cc9be8d680", + "sha256:7c1ee6f42250df403c5f103cbd2768a28fe1a0ea1f0f03fe151c8741e1469c8b", + "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42", + "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e", + "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597", + "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a", + "sha256:87dd88ded2e6d74d31e1e0a99a726a6765cda32d00ba72dc37f0651f306daaa8", + "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3", + "sha256:8d4d5063501b6dd4024b8ac2f04962d661222d120381272deea52e3fc52d3736", + "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da", + "sha256:930044bb7679ab003b14023138b50181899da3f25de50e9dbee23b61b4de2126", + "sha256:950be4d8ba92aca4b2bb0741285a46bfae3ca699ef913ec8416c1b78eadd64cd", + "sha256:961a7293b2457b405967af9c77dcaa43cc1a8cd50d23c532e62d48ab6cdd56f5", + "sha256:9b885f89040bb8c4a1573566bbb2f44f5c505ef6e74cec7ab9068c900047f04b", + "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026", + "sha256:a02364621fe369e06200d4a16558e056fe2805d3468350df3aef21e00d26214b", + "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc", + "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46", + "sha256:b15e02e9bb4c21e39876698abf233c8c579127986f8207200bc8a8f6bb27acf2", + "sha256:b2724fdb354a868ddf9a880cb84d102da914e99119211ef7ecbdc613b8c96b3c", + "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe", + "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984", + "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a", + "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70", + "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca", + "sha256:c76e5786951e72ed3686e122d14c5d7012f16c8303a674d18cdcd6d89557fc5b", + "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91", + "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3", + "sha256:d7480af14364494365e89d6fddc510a13e5a2c3584cb19ef65415ca57252fb84", + "sha256:dbc6ae66518ab3c5847659e9988c3b60dc94ffb48ef9168656e0019a93dbf8a1", + "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5", + "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be", + "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f", + "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc", + "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9", + "sha256:e88d5e6ad0d026fba7bdab8c3f225a69f063f116462c49892b0149e21b6c0a0e", + "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141", + "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef", + "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22", + "sha256:f7baece4ce06bade126fb84b8af1c33439a76d8a6fd818970215e0560ca28c27", + "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e", + "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1" ], "markers": "python_version >= '3.8'", - "version": "==10.2.0" + "version": "==10.4.0" }, "platformdirs": { "hashes": [ - "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380", - "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420" + "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee", + "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3" ], "markers": "python_version >= '3.8'", - "version": "==4.1.0" + "version": "==4.2.2" }, "pluggy": { "hashes": [ - "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12", - "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7" + "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", + "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669" ], "markers": "python_version >= '3.8'", - "version": "==1.3.0" + "version": "==1.5.0" }, "pycodestyle": { "hashes": [ @@ -900,45 +922,46 @@ }, "pydantic": { "hashes": [ - "sha256:1061c6ee6204f4f5a27133126854948e3b3d51fcc16ead2e5d04378c199b2f44", - "sha256:19b5686387ea0d1ea52ecc4cffb71abb21702c5e5b2ac626fd4dbaa0834aa49d", - "sha256:2bd446bdb7755c3a94e56d7bdfd3ee92396070efa8ef3a34fab9579fe6aa1d84", - "sha256:328558c9f2eed77bd8fffad3cef39dbbe3edc7044517f4625a769d45d4cf7555", - "sha256:32e0b4fb13ad4db4058a7c3c80e2569adbd810c25e6ca3bbd8b2a9cc2cc871d7", - "sha256:3ee0d69b2a5b341fc7927e92cae7ddcfd95e624dfc4870b32a85568bd65e6131", - "sha256:4aafd4e55e8ad5bd1b19572ea2df546ccace7945853832bb99422a79c70ce9b8", - "sha256:4b3946f87e5cef3ba2e7bd3a4eb5a20385fe36521d6cc1ebf3c08a6697c6cfb3", - "sha256:4de71c718c9756d679420c69f216776c2e977459f77e8f679a4a961dc7304a56", - "sha256:5565a49effe38d51882cb7bac18bda013cdb34d80ac336428e8908f0b72499b0", - "sha256:5803ad846cdd1ed0d97eb00292b870c29c1f03732a010e66908ff48a762f20e4", - "sha256:5da164119602212a3fe7e3bc08911a89db4710ae51444b4224c2382fd09ad453", - "sha256:615661bfc37e82ac677543704437ff737418e4ea04bef9cf11c6d27346606044", - "sha256:78a4d6bdfd116a559aeec9a4cfe77dda62acc6233f8b56a716edad2651023e5e", - "sha256:7d0f183b305629765910eaad707800d2f47c6ac5bcfb8c6397abdc30b69eeb15", - "sha256:7ead3cd020d526f75b4188e0a8d71c0dbbe1b4b6b5dc0ea775a93aca16256aeb", - "sha256:84d76ecc908d917f4684b354a39fd885d69dd0491be175f3465fe4b59811c001", - "sha256:8cb0bc509bfb71305d7a59d00163d5f9fc4530f0881ea32c74ff4f74c85f3d3d", - "sha256:91089b2e281713f3893cd01d8e576771cd5bfdfbff5d0ed95969f47ef6d676c3", - "sha256:9c9e04a6cdb7a363d7cb3ccf0efea51e0abb48e180c0d31dca8d247967d85c6e", - "sha256:a8c5360a0297a713b4123608a7909e6869e1b56d0e96eb0d792c27585d40757f", - "sha256:afacf6d2a41ed91fc631bade88b1d319c51ab5418870802cedb590b709c5ae3c", - "sha256:b34ba24f3e2d0b39b43f0ca62008f7ba962cff51efa56e64ee25c4af6eed987b", - "sha256:bd67cb2c2d9602ad159389c29e4ca964b86fa2f35c2faef54c3eb28b4efd36c8", - "sha256:c0f5e142ef8217019e3eef6ae1b6b55f09a7a15972958d44fbd228214cede567", - "sha256:cdb4272678db803ddf94caa4f94f8672e9a46bae4a44f167095e4d06fec12979", - "sha256:d70916235d478404a3fa8c997b003b5f33aeac4686ac1baa767234a0f8ac2326", - "sha256:d8ce3fb0841763a89322ea0432f1f59a2d3feae07a63ea2c958b2315e1ae8adb", - "sha256:e0b214e57623a535936005797567231a12d0da0c29711eb3514bc2b3cd008d0f", - "sha256:e631c70c9280e3129f071635b81207cad85e6c08e253539467e4ead0e5b219aa", - "sha256:e78578f0c7481c850d1c969aca9a65405887003484d24f6110458fb02cca7747", - "sha256:f0ca86b525264daa5f6b192f216a0d1e860b7383e3da1c65a1908f9c02f42801", - "sha256:f1a68f4f65a9ee64b6ccccb5bf7e17db07caebd2730109cb8a95863cfa9c4e55", - "sha256:fafe841be1103f340a24977f61dee76172e4ae5f647ab9e7fd1e1fca51524f08", - "sha256:ff68fc85355532ea77559ede81f35fff79a6a5543477e168ab3a381887caea76" + "sha256:0fe8a415cea8f340e7a9af9c54fc71a649b43e8ca3cc732986116b3cb135d303", + "sha256:1289c180abd4bd4555bb927c42ee42abc3aee02b0fb2d1223fb7c6e5bef87dbe", + "sha256:1eb2085c13bce1612da8537b2d90f549c8cbb05c67e8f22854e201bde5d98a47", + "sha256:2031de0967c279df0d8a1c72b4ffc411ecd06bac607a212892757db7462fc494", + "sha256:2a7bac939fa326db1ab741c9d7f44c565a1d1e80908b3797f7f81a4f86bc8d33", + "sha256:2d5a58feb9a39f481eda4d5ca220aa8b9d4f21a41274760b9bc66bfd72595b86", + "sha256:2f9a6fab5f82ada41d56b0602606a5506aab165ca54e52bc4545028382ef1c5d", + "sha256:2fcfb5296d7877af406ba1547dfde9943b1256d8928732267e2653c26938cd9c", + "sha256:549a8e3d81df0a85226963611950b12d2d334f214436a19537b2efed61b7639a", + "sha256:598da88dfa127b666852bef6d0d796573a8cf5009ffd62104094a4fe39599565", + "sha256:5d1197e462e0364906cbc19681605cb7c036f2475c899b6f296104ad42b9f5fb", + "sha256:69328e15cfda2c392da4e713443c7dbffa1505bc9d566e71e55abe14c97ddc62", + "sha256:6a9dfa722316f4acf4460afdf5d41d5246a80e249c7ff475c43a3a1e9d75cf62", + "sha256:6b30bcb8cbfccfcf02acb8f1a261143fab622831d9c0989707e0e659f77a18e0", + "sha256:6c076be61cd0177a8433c0adcb03475baf4ee91edf5a4e550161ad57fc90f523", + "sha256:771735dc43cf8383959dc9b90aa281f0b6092321ca98677c5fb6125a6f56d58d", + "sha256:795e34e6cc065f8f498c89b894a3c6da294a936ee71e644e4bd44de048af1405", + "sha256:87afda5539d5140cb8ba9e8b8c8865cb5b1463924d38490d73d3ccfd80896b3f", + "sha256:8fb2aa3ab3728d950bcc885a2e9eff6c8fc40bc0b7bb434e555c215491bcf48b", + "sha256:a1fcb59f2f355ec350073af41d927bf83a63b50e640f4dbaa01053a28b7a7718", + "sha256:a5e7add47a5b5a40c49b3036d464e3c7802f8ae0d1e66035ea16aa5b7a3923ed", + "sha256:a73f489aebd0c2121ed974054cb2759af8a9f747de120acd2c3394cf84176ccb", + "sha256:ab26038b8375581dc832a63c948f261ae0aa21f1d34c1293469f135fa92972a5", + "sha256:b0d191db0f92dfcb1dec210ca244fdae5cbe918c6050b342d619c09d31eea0cc", + "sha256:b749a43aa51e32839c9d71dc67eb1e4221bb04af1033a32e3923d46f9effa942", + "sha256:b7ccf02d7eb340b216ec33e53a3a629856afe1c6e0ef91d84a4e6f2fb2ca70fe", + "sha256:ba5b2e6fe6ca2b7e013398bc7d7b170e21cce322d266ffcd57cca313e54fb246", + "sha256:ba5c4a8552bff16c61882db58544116d021d0b31ee7c66958d14cf386a5b5350", + "sha256:c79e6a11a07da7374f46970410b41d5e266f7f38f6a17a9c4823db80dadf4303", + "sha256:ca48477862372ac3770969b9d75f1bf66131d386dba79506c46d75e6b48c1e09", + "sha256:dea7adcc33d5d105896401a1f37d56b47d443a2b2605ff8a969a0ed5543f7e33", + "sha256:e0a16d274b588767602b7646fa05af2782576a6cf1022f4ba74cbb4db66f6ca8", + "sha256:e4129b528c6baa99a429f97ce733fff478ec955513630e61b49804b6cf9b224a", + "sha256:e5f805d2d5d0a41633651a73fa4ecdd0b3d7a49de4ec3fadf062fe16501ddbf1", + "sha256:ef6c96b2baa2100ec91a4b428f80d8f28a3c9e53568219b6c298c1125572ebc6", + "sha256:fdbdd1d630195689f325c9ef1a12900524dceb503b00a987663ff4f58669b93d" ], "index": "pypi", - "markers": "python_full_version >= '3.6.1'", - "version": "==1.9.2" + "markers": "python_version >= '3.7'", + "version": "==1.10.12" }, "pydocstyle": { "hashes": [ @@ -958,49 +981,11 @@ }, "pyparsing": { "hashes": [ - "sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb", - "sha256:ede28a1a32462f5a9705e07aea48001a08f7cf81a021585011deba701581a0db" + "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad", + "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742" ], "markers": "python_full_version >= '3.6.8'", - "version": "==3.1.1" - }, - "pyrsistent": { - "hashes": [ - "sha256:0724c506cd8b63c69c7f883cc233aac948c1ea946ea95996ad8b1380c25e1d3f", - "sha256:09848306523a3aba463c4b49493a760e7a6ca52e4826aa100ee99d8d39b7ad1e", - "sha256:0f3b1bcaa1f0629c978b355a7c37acd58907390149b7311b5db1b37648eb6958", - "sha256:21cc459636983764e692b9eba7144cdd54fdec23ccdb1e8ba392a63666c60c34", - "sha256:2e14c95c16211d166f59c6611533d0dacce2e25de0f76e4c140fde250997b3ca", - "sha256:2e2c116cc804d9b09ce9814d17df5edf1df0c624aba3b43bc1ad90411487036d", - "sha256:4021a7f963d88ccd15b523787d18ed5e5269ce57aa4037146a2377ff607ae87d", - "sha256:4c48f78f62ab596c679086084d0dd13254ae4f3d6c72a83ffdf5ebdef8f265a4", - "sha256:4f5c2d012671b7391803263419e31b5c7c21e7c95c8760d7fc35602353dee714", - "sha256:58b8f6366e152092194ae68fefe18b9f0b4f89227dfd86a07770c3d86097aebf", - "sha256:59a89bccd615551391f3237e00006a26bcf98a4d18623a19909a2c48b8e986ee", - "sha256:5cdd7ef1ea7a491ae70d826b6cc64868de09a1d5ff9ef8d574250d0940e275b8", - "sha256:6288b3fa6622ad8a91e6eb759cfc48ff3089e7c17fb1d4c59a919769314af224", - "sha256:6d270ec9dd33cdb13f4d62c95c1a5a50e6b7cdd86302b494217137f760495b9d", - "sha256:79ed12ba79935adaac1664fd7e0e585a22caa539dfc9b7c7c6d5ebf91fb89054", - "sha256:7d29c23bdf6e5438c755b941cef867ec2a4a172ceb9f50553b6ed70d50dfd656", - "sha256:8441cf9616d642c475684d6cf2520dd24812e996ba9af15e606df5f6fd9d04a7", - "sha256:881bbea27bbd32d37eb24dd320a5e745a2a5b092a17f6debc1349252fac85423", - "sha256:8c3aba3e01235221e5b229a6c05f585f344734bd1ad42a8ac51493d74722bbce", - "sha256:a14798c3005ec892bbada26485c2eea3b54109cb2533713e355c806891f63c5e", - "sha256:b14decb628fac50db5e02ee5a35a9c0772d20277824cfe845c8a8b717c15daa3", - "sha256:b318ca24db0f0518630e8b6f3831e9cba78f099ed5c1d65ffe3e023003043ba0", - "sha256:c1beb78af5423b879edaf23c5591ff292cf7c33979734c99aa66d5914ead880f", - "sha256:c55acc4733aad6560a7f5f818466631f07efc001fd023f34a6c203f8b6df0f0b", - "sha256:ca52d1ceae015859d16aded12584c59eb3825f7b50c6cfd621d4231a6cc624ce", - "sha256:cae40a9e3ce178415040a0383f00e8d68b569e97f31928a3a8ad37e3fde6df6a", - "sha256:e78d0c7c1e99a4a45c99143900ea0546025e41bb59ebc10182e947cf1ece9174", - "sha256:ef3992833fbd686ee783590639f4b8343a57f1f75de8633749d984dc0eb16c86", - "sha256:f058a615031eea4ef94ead6456f5ec2026c19fb5bd6bfe86e9665c4158cf802f", - "sha256:f5ac696f02b3fc01a710427585c855f65cd9c640e14f52abe52020722bb4906b", - "sha256:f920385a11207dc372a028b3f1e1038bb244b3ec38d448e6d8e43c6b3ba20e98", - "sha256:fed2c3216a605dc9a6ea50c7e84c82906e3684c4e80d2908208f662a6cbf9022" - ], - "markers": "python_version >= '3.8'", - "version": "==0.20.0" + "version": "==3.1.2" }, "pytest": { "hashes": [ @@ -1013,12 +998,12 @@ }, "pytest-asyncio": { "hashes": [ - "sha256:37a9d912e8338ee7b4a3e917381d1c95bfc8682048cb0fbc35baba316ec1faba", - "sha256:af313ce900a62fbe2b1aed18e37ad757f1ef9940c6b6a88e2954de38d6b1fb9f" + "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2", + "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==0.23.3" + "version": "==0.23.8" }, "pytest-cov": { "hashes": [ @@ -1039,19 +1024,124 @@ }, "python-dateutil": { "hashes": [ - "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", - "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" + "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.8.2" + "version": "==2.9.0.post0" }, - "setuptools": { + "referencing": { + "hashes": [ + "sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c", + "sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de" + ], + "markers": "python_version >= '3.8'", + "version": "==0.35.1" + }, + "rpds-py": { "hashes": [ - "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05", - "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78" + "sha256:0121803b0f424ee2109d6e1f27db45b166ebaa4b32ff47d6aa225642636cd834", + "sha256:06925c50f86da0596b9c3c64c3837b2481337b83ef3519e5db2701df695453a4", + "sha256:071d4adc734de562bd11d43bd134330fb6249769b2f66b9310dab7460f4bf714", + "sha256:1540d807364c84516417115c38f0119dfec5ea5c0dd9a25332dea60b1d26fc4d", + "sha256:15e65395a59d2e0e96caf8ee5389ffb4604e980479c32742936ddd7ade914b22", + "sha256:19d02c45f2507b489fd4df7b827940f1420480b3e2e471e952af4d44a1ea8e34", + "sha256:1c26da90b8d06227d7769f34915913911222d24ce08c0ab2d60b354e2d9c7aff", + "sha256:1d16089dfa58719c98a1c06f2daceba6d8e3fb9b5d7931af4a990a3c486241cb", + "sha256:1dd46f309e953927dd018567d6a9e2fb84783963650171f6c5fe7e5c41fd5666", + "sha256:2575efaa5d949c9f4e2cdbe7d805d02122c16065bfb8d95c129372d65a291a0b", + "sha256:3208f9aea18991ac7f2b39721e947bbd752a1abbe79ad90d9b6a84a74d44409b", + "sha256:329c719d31362355a96b435f4653e3b4b061fcc9eba9f91dd40804ca637d914e", + "sha256:3384d278df99ec2c6acf701d067147320b864ef6727405d6470838476e44d9e8", + "sha256:34a01a4490e170376cd79258b7f755fa13b1a6c3667e872c8e35051ae857a92b", + "sha256:354f3a91718489912f2e0fc331c24eaaf6a4565c080e00fbedb6015857c00582", + "sha256:37f46bb11858717e0efa7893c0f7055c43b44c103e40e69442db5061cb26ed34", + "sha256:3b4cf5a9497874822341c2ebe0d5850fed392034caadc0bad134ab6822c0925b", + "sha256:3f148c3f47f7f29a79c38cc5d020edcb5ca780020fab94dbc21f9af95c463581", + "sha256:443cec402ddd650bb2b885113e1dcedb22b1175c6be223b14246a714b61cd521", + "sha256:462b0c18fbb48fdbf980914a02ee38c423a25fcc4cf40f66bacc95a2d2d73bc8", + "sha256:474bc83233abdcf2124ed3f66230a1c8435896046caa4b0b5ab6013c640803cc", + "sha256:4d438e4c020d8c39961deaf58f6913b1bf8832d9b6f62ec35bd93e97807e9cbc", + "sha256:4fdc9afadbeb393b4bbbad75481e0ea78e4469f2e1d713a90811700830b553a9", + "sha256:5039e3cef7b3e7a060de468a4a60a60a1f31786da94c6cb054e7a3c75906111c", + "sha256:5095a7c838a8647c32aa37c3a460d2c48debff7fc26e1136aee60100a8cd8f68", + "sha256:52e466bea6f8f3a44b1234570244b1cff45150f59a4acae3fcc5fd700c2993ca", + "sha256:535d4b52524a961d220875688159277f0e9eeeda0ac45e766092bfb54437543f", + "sha256:57dbc9167d48e355e2569346b5aa4077f29bf86389c924df25c0a8b9124461fb", + "sha256:5a4b07cdf3f84310c08c1de2c12ddadbb7a77568bcb16e95489f9c81074322ed", + "sha256:5c872814b77a4e84afa293a1bee08c14daed1068b2bb1cc312edbf020bbbca2b", + "sha256:5f83689a38e76969327e9b682be5521d87a0c9e5a2e187d2bc6be4765f0d4600", + "sha256:688aa6b8aa724db1596514751ffb767766e02e5c4a87486ab36b8e1ebc1aedac", + "sha256:6b130bd4163c93798a6b9bb96be64a7c43e1cec81126ffa7ffaa106e1fc5cef5", + "sha256:6b31f059878eb1f5da8b2fd82480cc18bed8dcd7fb8fe68370e2e6285fa86da6", + "sha256:6d45080095e585f8c5097897313def60caa2046da202cdb17a01f147fb263b81", + "sha256:6f2f78ef14077e08856e788fa482107aa602636c16c25bdf59c22ea525a785e9", + "sha256:6fe87efd7f47266dfc42fe76dae89060038f1d9cb911f89ae7e5084148d1cc08", + "sha256:75969cf900d7be665ccb1622a9aba225cf386bbc9c3bcfeeab9f62b5048f4a07", + "sha256:75a6076289b2df6c8ecb9d13ff79ae0cad1d5fb40af377a5021016d58cd691ec", + "sha256:78d57546bad81e0da13263e4c9ce30e96dcbe720dbff5ada08d2600a3502e526", + "sha256:79e205c70afddd41f6ee79a8656aec738492a550247a7af697d5bd1aee14f766", + "sha256:7c98298a15d6b90c8f6e3caa6457f4f022423caa5fa1a1ca7a5e9e512bdb77a4", + "sha256:7ec72df7354e6b7f6eb2a17fa6901350018c3a9ad78e48d7b2b54d0412539a67", + "sha256:81ea573aa46d3b6b3d890cd3c0ad82105985e6058a4baed03cf92518081eec8c", + "sha256:8344127403dea42f5970adccf6c5957a71a47f522171fafaf4c6ddb41b61703a", + "sha256:8445f23f13339da640d1be8e44e5baf4af97e396882ebbf1692aecd67f67c479", + "sha256:850720e1b383df199b8433a20e02b25b72f0fded28bc03c5bd79e2ce7ef050be", + "sha256:88cb4bac7185a9f0168d38c01d7a00addece9822a52870eee26b8d5b61409213", + "sha256:8a790d235b9d39c70a466200d506bb33a98e2ee374a9b4eec7a8ac64c2c261fa", + "sha256:8b1a94b8afc154fbe36978a511a1f155f9bd97664e4f1f7a374d72e180ceb0ae", + "sha256:8d6ad132b1bc13d05ffe5b85e7a01a3998bf3a6302ba594b28d61b8c2cf13aaf", + "sha256:8eb488ef928cdbc05a27245e52de73c0d7c72a34240ef4d9893fdf65a8c1a955", + "sha256:90bf55d9d139e5d127193170f38c584ed3c79e16638890d2e36f23aa1630b952", + "sha256:9133d75dc119a61d1a0ded38fb9ba40a00ef41697cc07adb6ae098c875195a3f", + "sha256:93a91c2640645303e874eada51f4f33351b84b351a689d470f8108d0e0694210", + "sha256:959179efb3e4a27610e8d54d667c02a9feaa86bbabaf63efa7faa4dfa780d4f1", + "sha256:9625367c8955e4319049113ea4f8fee0c6c1145192d57946c6ffcd8fe8bf48dd", + "sha256:9da6f400eeb8c36f72ef6646ea530d6d175a4f77ff2ed8dfd6352842274c1d8b", + "sha256:9e65489222b410f79711dc3d2d5003d2757e30874096b2008d50329ea4d0f88c", + "sha256:a3e2fd14c5d49ee1da322672375963f19f32b3d5953f0615b175ff7b9d38daed", + "sha256:a5a7c1062ef8aea3eda149f08120f10795835fc1c8bc6ad948fb9652a113ca55", + "sha256:a5da93debdfe27b2bfc69eefb592e1831d957b9535e0943a0ee8b97996de21b5", + "sha256:a6e605bb9edcf010f54f8b6a590dd23a4b40a8cb141255eec2a03db249bc915b", + "sha256:a707b158b4410aefb6b054715545bbb21aaa5d5d0080217290131c49c2124a6e", + "sha256:a8b6683a37338818646af718c9ca2a07f89787551057fae57c4ec0446dc6224b", + "sha256:aa5476c3e3a402c37779e95f7b4048db2cb5b0ed0b9d006983965e93f40fe05a", + "sha256:ab1932ca6cb8c7499a4d87cb21ccc0d3326f172cfb6a64021a889b591bb3045c", + "sha256:ae8b6068ee374fdfab63689be0963333aa83b0815ead5d8648389a8ded593378", + "sha256:b0906357f90784a66e89ae3eadc2654f36c580a7d65cf63e6a616e4aec3a81be", + "sha256:b0da31853ab6e58a11db3205729133ce0df26e6804e93079dee095be3d681dc1", + "sha256:b1c30841f5040de47a0046c243fc1b44ddc87d1b12435a43b8edff7e7cb1e0d0", + "sha256:b228e693a2559888790936e20f5f88b6e9f8162c681830eda303bad7517b4d5a", + "sha256:b7cc6cb44f8636fbf4a934ca72f3e786ba3c9f9ba4f4d74611e7da80684e48d2", + "sha256:ba0ed0dc6763d8bd6e5de5cf0d746d28e706a10b615ea382ac0ab17bb7388633", + "sha256:bc9128e74fe94650367fe23f37074f121b9f796cabbd2f928f13e9661837296d", + "sha256:bcf426a8c38eb57f7bf28932e68425ba86def6e756a5b8cb4731d8e62e4e0223", + "sha256:bec35eb20792ea64c3c57891bc3ca0bedb2884fbac2c8249d9b731447ecde4fa", + "sha256:c3444fe52b82f122d8a99bf66777aed6b858d392b12f4c317da19f8234db4533", + "sha256:c5c9581019c96f865483d031691a5ff1cc455feb4d84fc6920a5ffc48a794d8a", + "sha256:c6feacd1d178c30e5bc37184526e56740342fd2aa6371a28367bad7908d454fc", + "sha256:c8f77e661ffd96ff104bebf7d0f3255b02aa5d5b28326f5408d6284c4a8b3248", + "sha256:cb0f6eb3a320f24b94d177e62f4074ff438f2ad9d27e75a46221904ef21a7b05", + "sha256:ce84a7efa5af9f54c0aa7692c45861c1667080814286cacb9958c07fc50294fb", + "sha256:cf902878b4af334a09de7a45badbff0389e7cf8dc2e4dcf5f07125d0b7c2656d", + "sha256:dab8d921b55a28287733263c0e4c7db11b3ee22aee158a4de09f13c93283c62d", + "sha256:dc9ac4659456bde7c567107556ab065801622396b435a3ff213daef27b495388", + "sha256:dd36b712d35e757e28bf2f40a71e8f8a2d43c8b026d881aa0c617b450d6865c9", + "sha256:e19509145275d46bc4d1e16af0b57a12d227c8253655a46bbd5ec317e941279d", + "sha256:e21cc693045fda7f745c790cb687958161ce172ffe3c5719ca1764e752237d16", + "sha256:e54548e0be3ac117595408fd4ca0ac9278fde89829b0b518be92863b17ff67a2", + "sha256:e5b9fc03bf76a94065299d4a2ecd8dfbae4ae8e2e8098bbfa6ab6413ca267709", + "sha256:e8481b946792415adc07410420d6fc65a352b45d347b78fec45d8f8f0d7496f0", + "sha256:ebcbf356bf5c51afc3290e491d3722b26aaf5b6af3c1c7f6a1b757828a46e336", + "sha256:ef9101f3f7b59043a34f1dccbb385ca760467590951952d6701df0da9893ca0c", + "sha256:f2afd2164a1e85226fcb6a1da77a5c8896c18bfe08e82e8ceced5181c42d2179", + "sha256:f629ecc2db6a4736b5ba95a8347b0089240d69ad14ac364f557d52ad68cf94b0", + "sha256:f68eea5df6347d3f1378ce992d86b2af16ad7ff4dcb4a19ccdc23dea901b87fb", + "sha256:f757f359f30ec7dcebca662a6bd46d1098f8b9fb1fcd661a9e13f2e8ce343ba1", + "sha256:fb37bd599f031f1a6fb9e58ec62864ccf3ad549cf14bac527dbfa97123edcca4" ], "markers": "python_version >= '3.8'", - "version": "==69.0.3" + "version": "==0.19.0" }, "six": { "hashes": [ @@ -1085,21 +1175,21 @@ }, "types-mock": { "hashes": [ - "sha256:13ca379d5710ccb3f18f69ade5b08881874cb83383d8fb49b1d4dac9d5c5d090", - "sha256:3d116955495935b0bcba14954b38d97e507cd43eca3e3700fc1b8e4f5c6bf2c7" + "sha256:5281a645d72e827d70043e3cc144fe33b1c003db084f789dc203aa90e812a5a4", + "sha256:d586a01d39ad919d3ddcd73de6cde73ca7f3c69707219f722d1b8d7733641ad7" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==5.1.0.20240106" + "version": "==5.1.0.20240425" }, "typing-extensions": { "hashes": [ - "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", - "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" + "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", + "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==4.9.0" + "version": "==4.12.2" } } } diff --git a/hardware/opentrons_hardware/drivers/can_bus/settings.py b/hardware/opentrons_hardware/drivers/can_bus/settings.py index c36bd791fb9..9ebb990d9c9 100644 --- a/hardware/opentrons_hardware/drivers/can_bus/settings.py +++ b/hardware/opentrons_hardware/drivers/can_bus/settings.py @@ -50,21 +50,21 @@ class DriverSettings(BaseSettings): """Settings for driver building.""" interface: str = Field( - DEFAULT_INTERFACE, + default=DEFAULT_INTERFACE, description=f"Can either be {OPENTRONS_INTERFACE} for simple socket " f"or a python can interface.", ) bit_rate: int = Field( - DEFAULT_BITRATE, + default=DEFAULT_BITRATE, description=f"Bit rate. Not applicable to {OPENTRONS_INTERFACE} interface.", ) - channel: str = Field(DEFAULT_CHANNEL, description="The SocketCan channel.") + channel: str = Field(default=DEFAULT_CHANNEL, description="The SocketCan channel.") - host: str = Field(DEFAULT_HOST, description=f"{OPENTRONS_INTERFACE} only.") - port: int = Field(DEFAULT_PORT, description=f"{OPENTRONS_INTERFACE} only.") - fcan_clock: int = Field(DEFAULT_FDCAN_CLK, description="pcan only.") - sample_rate: float = Field(DEFAULT_SAMPLE_RATE, description="pcan only.") - jump_width: int = Field(DEFAULT_JUMP_WIDTH_SEG, descript="pcan only.") + host: str = Field(default=DEFAULT_HOST, description=f"{OPENTRONS_INTERFACE} only.") + port: int = Field(default=DEFAULT_PORT, description=f"{OPENTRONS_INTERFACE} only.") + fcan_clock: int = Field(default=DEFAULT_FDCAN_CLK, description="pcan only.") + sample_rate: float = Field(default=DEFAULT_SAMPLE_RATE, description="pcan only.") + jump_width: int = Field(default=DEFAULT_JUMP_WIDTH_SEG, description="pcan only.") class Config: # noqa: D106 env_prefix = "OT3_CAN_DRIVER_" diff --git a/hardware/opentrons_hardware/hardware_control/tool_sensors.py b/hardware/opentrons_hardware/hardware_control/tool_sensors.py index c4ea96fccff..eeb4736a6d9 100644 --- a/hardware/opentrons_hardware/hardware_control/tool_sensors.py +++ b/hardware/opentrons_hardware/hardware_control/tool_sensors.py @@ -82,8 +82,6 @@ # FIXME we should organize all of these functions to use the sensor drivers. # FIXME we should restrict some of these functions by instrument type. -PLUNGER_SOLO_MOVE_TIME = 0.2 - def _fix_pass_step_for_buffer( move_group: MoveGroupStep, @@ -393,6 +391,7 @@ async def liquid_probe( plunger_speed: float, mount_speed: float, threshold_pascals: float, + plunger_impulse_time: float, csv_output: bool = False, sync_buffer_output: bool = False, can_bus_only_output: bool = False, @@ -425,7 +424,7 @@ async def liquid_probe( sensor_driver, True, ) - p_prep_distance = float(PLUNGER_SOLO_MOVE_TIME * plunger_speed) + p_prep_distance = float(plunger_impulse_time * plunger_speed) p_pass_distance = float(max_p_distance - p_prep_distance) max_z_distance = (p_pass_distance / plunger_speed) * mount_speed @@ -433,7 +432,7 @@ async def liquid_probe( distance={tool: float64(p_prep_distance)}, velocity={tool: float64(plunger_speed)}, acceleration={}, - duration=float64(PLUNGER_SOLO_MOVE_TIME), + duration=float64(plunger_impulse_time), present_nodes=[tool], ) sensor_group = _build_pass_step( @@ -519,7 +518,6 @@ async def capacitive_probe( tool: InstrumentProbeTarget, mover: NodeId, distance: float, - plunger_speed: float, mount_speed: float, sensor_id: SensorId = SensorId.S0, relative_threshold_pf: float = 1.0, @@ -538,6 +536,8 @@ async def capacitive_probe( """ log_files: Dict[SensorId, str] = {} if not data_files else data_files sensor_driver = SensorDriver() + pipette_present = tool in [NodeId.pipette_left, NodeId.pipette_right] + capacitive_sensors = await _setup_capacitive_sensors( messenger, sensor_id, @@ -546,10 +546,18 @@ async def capacitive_probe( sensor_driver, ) + probe_distance = {mover: distance} + probe_speed = {mover: mount_speed} + movers = [mover] + if pipette_present: + probe_distance[tool] = 0.0 + probe_speed[tool] = 0.0 + movers.append(tool) + sensor_group = _build_pass_step( - movers=[mover, tool], - distance={mover: distance, tool: 0.0}, - speed={mover: mount_speed, tool: 0.0}, + movers=movers, + distance=probe_distance, + speed=probe_speed, sensor_type=SensorType.capacitive, sensor_id=sensor_id, stop_condition=MoveStopCondition.sync_line, @@ -557,9 +565,9 @@ async def capacitive_probe( if sync_buffer_output: sensor_group = _fix_pass_step_for_buffer( sensor_group, - movers=[mover, tool], - distance={mover: distance, tool: distance}, - speed={mover: mount_speed, tool: plunger_speed}, + movers=movers, + distance=probe_distance, + speed=probe_speed, sensor_type=SensorType.capacitive, sensor_id=sensor_id, stop_condition=MoveStopCondition.sync_line, diff --git a/hardware/tests/opentrons_hardware/hardware_control/test_tool_sensors.py b/hardware/tests/opentrons_hardware/hardware_control/test_tool_sensors.py index 4065874739b..f4dddc8ca37 100644 --- a/hardware/tests/opentrons_hardware/hardware_control/test_tool_sensors.py +++ b/hardware/tests/opentrons_hardware/hardware_control/test_tool_sensors.py @@ -217,6 +217,7 @@ def move_responder( mount_speed=10, plunger_speed=8, threshold_pascals=threshold_pascals, + plunger_impulse_time=0.2, csv_output=False, sync_buffer_output=False, can_bus_only_output=False, @@ -348,6 +349,7 @@ def move_responder( mount_speed=10, plunger_speed=8, threshold_pascals=14, + plunger_impulse_time=0.2, csv_output=csv_output, sync_buffer_output=sync_buffer_output, can_bus_only_output=can_bus_only_output, @@ -445,7 +447,7 @@ def move_responder( message_send_loopback.add_responder(move_responder) status = await capacitive_probe( - mock_messenger, target_node, motor_node, distance, speed, speed + mock_messenger, target_node, motor_node, distance, speed ) assert status.motor_position == 10 # this comes from the current_position_um above assert status.encoder_position == 10 diff --git a/labware-library/package.json b/labware-library/package.json index 9c0010a60c2..7c8e0cfa8e3 100644 --- a/labware-library/package.json +++ b/labware-library/package.json @@ -23,7 +23,6 @@ "@types/jszip": "3.1.7", "@types/mixpanel-browser": "^2.35.6", "@types/query-string": "6.2.0", - "@types/react-router-dom": "5.3.3", "@types/webpack-env": "^1.16.0", "@types/yup": "0.29.11" }, @@ -41,7 +40,7 @@ "query-string": "6.2.0", "react": "18.2.0", "react-dom": "18.2.0", - "react-router-dom": "5.3.4", + "react-router-dom": "6.24.1", "yup": "0.32.9" } } diff --git a/labware-library/src/components/App/index.tsx b/labware-library/src/components/App/index.tsx index 8b18a6d431d..bb9b907da77 100644 --- a/labware-library/src/components/App/index.tsx +++ b/labware-library/src/components/App/index.tsx @@ -1,7 +1,7 @@ // main application wrapper component import * as React from 'react' import cx from 'classnames' - +import { useLocation } from 'react-router-dom' import { DefinitionRoute } from '../../definitions' import { useFilters } from '../../filters' import { Nav, Breadcrumbs } from '../Nav' @@ -14,7 +14,8 @@ import styles from './styles.module.css' import type { DefinitionRouteRenderProps } from '../../definitions' export function AppComponent(props: DefinitionRouteRenderProps): JSX.Element { - const { definition, location } = props + const { definition } = props + const location = useLocation() const scrollRef = React.useRef(null) const filters = useFilters(location) const isDetailPage = Boolean(definition) diff --git a/labware-library/src/components/Sidebar/FilterManufacturer.tsx b/labware-library/src/components/Sidebar/FilterManufacturer.tsx index 315cc9c071e..6aed332f847 100644 --- a/labware-library/src/components/Sidebar/FilterManufacturer.tsx +++ b/labware-library/src/components/Sidebar/FilterManufacturer.tsx @@ -1,6 +1,6 @@ // filter labware by manufacturer import * as React from 'react' -import { withRouter } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { SelectField } from '@opentrons/components' import { getAllManufacturers, buildFiltersUrl } from '../../filters' import styles from './styles.module.css' @@ -8,17 +8,17 @@ import styles from './styles.module.css' import { MANUFACTURER, MANUFACTURER_VALUES } from '../../localization' import type { SelectOptionOrGroup } from '@opentrons/components' -import type { RouteComponentProps } from 'react-router-dom' import type { FilterParams } from '../../types' -export interface FilterManufacturerProps extends RouteComponentProps { +export interface FilterManufacturerProps { filters: FilterParams } -export function FilterManufacturerComponent( +export function FilterManufacturer( props: FilterManufacturerProps ): JSX.Element { - const { history, filters } = props + const { filters } = props + const navigate = useNavigate() const manufacturers = getAllManufacturers() const options: SelectOptionOrGroup[] = manufacturers.map(value => ({ value, @@ -37,14 +37,10 @@ export function FilterManufacturerComponent( options={options} onValueChange={(_, value) => { if (value) { - history.push(buildFiltersUrl({ ...filters, manufacturer: value })) + navigate(buildFiltersUrl({ ...filters, manufacturer: value })) } }} /> ) } -// @ts-expect-error react router type not portable -export const FilterManufacturer: (props: { - filters: FilterParams -}) => JSX.Element = withRouter(FilterManufacturerComponent) diff --git a/labware-library/src/components/ui/Link.tsx b/labware-library/src/components/ui/Link.tsx index 4672b01a3dd..f51572e6695 100644 --- a/labware-library/src/components/ui/Link.tsx +++ b/labware-library/src/components/ui/Link.tsx @@ -1,16 +1,15 @@ // internal link that preserves query parameters import * as React from 'react' -import { withRouter, Link as BaseLink } from 'react-router-dom' -import type { RouteComponentProps } from 'react-router-dom' +import { Link as BaseLink, useLocation } from 'react-router-dom' -export interface LinkProps extends RouteComponentProps { +export interface LinkProps { to: string children?: React.ReactNode className?: string } -export function WrappedLink(props: LinkProps): JSX.Element { - const { to, children, className, location } = props +export function Link({ to, children, className }: LinkProps): JSX.Element { + const location = useLocation() return ( ) } - -// @ts-expect-error react router type not portable -export const Link: (props: { - to: string - children?: React.ReactNode - className?: string -}) => JSX.Element = withRouter(WrappedLink) diff --git a/labware-library/src/definitions.tsx b/labware-library/src/definitions.tsx index b1f76208177..7a3c8bd0e8e 100644 --- a/labware-library/src/definitions.tsx +++ b/labware-library/src/definitions.tsx @@ -1,16 +1,14 @@ // labware definition helpers // TODO(mc, 2019-03-18): move to shared-data? import * as React from 'react' -import { Route } from 'react-router-dom' +import { useParams } from 'react-router-dom' import groupBy from 'lodash/groupBy' import uniq from 'lodash/uniq' import { LABWAREV2_DO_NOT_LIST, getAllDefinitions as _getAllDefinitions, } from '@opentrons/shared-data' -import { getPublicPath } from './public-path' -import type { RouteComponentProps } from 'react-router-dom' import type { LabwareDefinition2 } from '@opentrons/shared-data' import type { LabwareList, LabwareDefinition } from './types' @@ -76,7 +74,7 @@ export function getDefinition( return def || null } -export interface DefinitionRouteRenderProps extends RouteComponentProps { +export interface DefinitionRouteRenderProps { definition: LabwareDefinition | null } @@ -84,21 +82,13 @@ export interface DefinitionRouteProps { render: (props: DefinitionRouteRenderProps) => React.ReactNode } -export function DefinitionRoute(props: DefinitionRouteProps): JSX.Element { - return ( - { - const { loadName } = routeProps.match.params - const definition = getDefinition(loadName) +export const DefinitionRoute: React.FC = ({ render }) => { + const { loadName } = useParams<{ loadName: string }>() + const definition = getDefinition(loadName) - // TODO(mc, 2019-04-10): handle 404 if loadName exists but definition - // isn't found + // TODO: handle 404 if loadName exists but definition isn't found - return props.render({ ...routeProps, definition }) - }} - /> - ) + return <>{render({ definition })} } export const NEW_LABWARE_DEFS = [ diff --git a/labware-library/src/filters.tsx b/labware-library/src/filters.tsx index 0ce9323879f..65e77f538b9 100644 --- a/labware-library/src/filters.tsx +++ b/labware-library/src/filters.tsx @@ -8,8 +8,8 @@ import uniq from 'lodash/uniq' import { getAllDefinitions } from './definitions' import { getPublicPath } from './public-path' -import type { Location } from 'history' import type { FilterParams, LabwareDefinition, LabwareList } from './types' +import type { Location } from 'react-router-dom' export const FILTER_OFF = 'all' @@ -30,7 +30,7 @@ export function getAllManufacturers(): string[] { return uniq([FILTER_OFF, ...brands, ...wellGroupBrands]) } -export function useFilters(location: Location): FilterParams { +export function useFilters(location: Location): FilterParams { const [params, setParams] = useState({ category: FILTER_OFF, manufacturer: FILTER_OFF, diff --git a/labware-library/src/index.tsx b/labware-library/src/index.tsx index 9bac13fe66c..7f244f10e75 100644 --- a/labware-library/src/index.tsx +++ b/labware-library/src/index.tsx @@ -1,7 +1,7 @@ // labware library entry import * as React from 'react' import { hydrate, render } from 'react-dom' -import { BrowserRouter, Route, Switch } from 'react-router-dom' +import { BrowserRouter, Route, Routes } from 'react-router-dom' import { App } from './components/App' import { LabwareCreator } from './labware-creator' @@ -19,10 +19,10 @@ if (!$root) { const Root = (): JSX.Element => ( - - - - + + } /> + } /> + ) diff --git a/package.json b/package.json index 0fe8099d97e..0727c37a148 100755 --- a/package.json +++ b/package.json @@ -63,7 +63,6 @@ "@types/react-color": "^3.0.6", "@types/react-dom": "18.2.0", "@types/react-redux": "7.1.32", - "@types/react-router-dom": "5.3.3", "@types/redux-mock-store": "^1.0.2", "@types/semver": "^7.3.6", "@typescript-eslint/eslint-plugin": "^6.20.0", diff --git a/performance-metrics/Pipfile b/performance-metrics/Pipfile index b57cd339814..d3104f8c27c 100644 --- a/performance-metrics/Pipfile +++ b/performance-metrics/Pipfile @@ -6,7 +6,9 @@ name = "pypi" [packages] performance-metrics = {file = ".", editable = true} psutil = "==6.0.0" -systemd-python = "234" +# systemd-python errors upon installation if the host machine does not have systemd. +# Mark it as Linux-only so we can have dev environments on such machines. +systemd-python = { version = "==234", markers="sys_platform=='linux'" } [dev-packages] pytest = "==7.4.4" diff --git a/performance-metrics/Pipfile.lock b/performance-metrics/Pipfile.lock index 9bb77d23330..3680792e6bf 100644 --- a/performance-metrics/Pipfile.lock +++ b/performance-metrics/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "a123325c3bebd1451774ebfadc532a4ca78a92897b80cf1d20ded030e145bac2" + "sha256": "259baa6d557d8069c945cca566e64d0135576284244f14461c67e044366535b5" }, "pipfile-spec": 6, "requires": { @@ -49,6 +49,7 @@ "sha256:fd0e44bf70eadae45aadc292cb0a7eb5b0b6372cd1b391228047d33895db83e7" ], "index": "pypi", + "markers": "sys_platform == 'linux'", "version": "==234" } }, @@ -101,11 +102,11 @@ }, "exceptiongroup": { "hashes": [ - "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad", - "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16" + "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", + "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc" ], "markers": "python_version < '3.11'", - "version": "==1.2.1" + "version": "==1.2.2" }, "flake8": { "hashes": [ @@ -268,12 +269,12 @@ }, "pytest-asyncio": { "hashes": [ - "sha256:009b48127fbe44518a547bddd25611551b0e43ccdbf1e67d12479f569832c20b", - "sha256:5f5c72948f4c49e7db4f29f2521d4031f1c27f86e57b046126654083d4770268" + "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2", + "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==0.23.7" + "version": "==0.23.8" }, "snowballstemmer": { "hashes": [ diff --git a/protocol-designer/package.json b/protocol-designer/package.json index 564ebdb2fe1..21e7b98e18f 100755 --- a/protocol-designer/package.json +++ b/protocol-designer/package.json @@ -54,6 +54,7 @@ "redux": "4.0.5", "redux-actions": "2.2.1", "react-popper": "1.0.0", + "react-router-dom": "6.24.1", "redux-thunk": "2.3.0", "reselect": "4.0.0", "styled-components": "5.3.6", diff --git a/protocol-designer/src/components/App.tsx b/protocol-designer/src/App.tsx similarity index 50% rename from protocol-designer/src/components/App.tsx rename to protocol-designer/src/App.tsx index a64dbf917ac..7699de60ce2 100644 --- a/protocol-designer/src/components/App.tsx +++ b/protocol-designer/src/App.tsx @@ -1,12 +1,6 @@ import * as React from 'react' import { ProtocolEditor } from './ProtocolEditor' -import '../css/reset.module.css' - export function App(): JSX.Element { - return ( -
    - -
    - ) + return } diff --git a/protocol-designer/src/Navbar.tsx b/protocol-designer/src/Navbar.tsx new file mode 100644 index 00000000000..1c19f7f1605 --- /dev/null +++ b/protocol-designer/src/Navbar.tsx @@ -0,0 +1,62 @@ +import * as React from 'react' +import { NavLink } from 'react-router-dom' +import styled from 'styled-components' + +import { + ALIGN_CENTER, + ALIGN_FLEX_START, + ALIGN_STRETCH, + COLORS, + DIRECTION_COLUMN, + FLEX_NONE, + Flex, + JUSTIFY_SPACE_BETWEEN, + SPACING, + LegacyStyledText, + TYPOGRAPHY, + DIRECTION_ROW, +} from '@opentrons/components' + +import type { RouteProps } from './types' + +export function Navbar({ routes }: { routes: RouteProps[] }): JSX.Element { + const navRoutes = routes.filter( + ({ navLinkTo }: RouteProps) => navLinkTo != null + ) + return ( + + + {navRoutes.map(({ name, navLinkTo }: RouteProps) => ( + + + {name} + + + ))} + + + ) +} + +const NavbarLink = styled(NavLink)` + color: ${COLORS.black90}; + text-decoration: none; + align-self: ${ALIGN_STRETCH}; + &:hover { + color: ${COLORS.black70}; + } +` diff --git a/protocol-designer/src/ProtocolEditor.tsx b/protocol-designer/src/ProtocolEditor.tsx new file mode 100644 index 00000000000..e66bb285e12 --- /dev/null +++ b/protocol-designer/src/ProtocolEditor.tsx @@ -0,0 +1,99 @@ +import * as React from 'react' +import cx from 'classnames' +import { DndProvider } from 'react-dnd' +import { BrowserRouter } from 'react-router-dom' +import { useDispatch, useSelector } from 'react-redux' +import { HTML5Backend } from 'react-dnd-html5-backend' +import { + DIRECTION_COLUMN, + DIRECTION_ROW, + Flex, + PrimaryButton, + SPACING, +} from '@opentrons/components' +import { getEnableRedesign } from './feature-flags/selectors' +import { setFeatureFlags } from './feature-flags/actions' +import { ComputingSpinner } from './components/ComputingSpinner' +import { ConnectedNav } from './containers/ConnectedNav' +import { Sidebar } from './containers/ConnectedSidebar' +import { ConnectedTitleBar } from './containers/ConnectedTitleBar' +import { MainPanel } from './containers/ConnectedMainPanel' +import { PortalRoot as MainPageModalPortalRoot } from './components/portals/MainPageModalPortal' +import { MAIN_CONTENT_FORCED_SCROLL_CLASSNAME } from './ui/steps/utils' +import { PrereleaseModeIndicator } from './components/PrereleaseModeIndicator' +import { PortalRoot as TopPortalRoot } from './components/portals/TopPortal' +import { FileUploadMessageModal } from './components/modals/FileUploadMessageModal/FileUploadMessageModal' +import { LabwareUploadMessageModal } from './components/modals/LabwareUploadMessageModal/LabwareUploadMessageModal' +import { GateModal } from './components/modals/GateModal' +import { CreateFileWizard } from './components/modals/CreateFileWizard' +import { AnnouncementModal } from './components/modals/AnnouncementModal' +import { ProtocolRoutes } from './ProtocolRoutes' + +import styles from './components/ProtocolEditor.module.css' +import './css/reset.module.css' + +const showGateModal = + process.env.NODE_ENV === 'production' || process.env.OT_PD_SHOW_GATE + +function ProtocolEditorComponent(): JSX.Element { + const enableRedesign = useSelector(getEnableRedesign) + const dispatch = useDispatch() + + return ( +
    + + {enableRedesign ? ( + + + { + dispatch(setFeatureFlags({ OT_PD_ENABLE_REDESIGN: false })) + }} + > + turn off redesign + + + + + + + ) : ( +
    + + + {showGateModal ? : null} + +
    + + +
    + + +
    + + + + + + + +
    +
    +
    +
    + )} +
    + ) +} + +export const ProtocolEditor = (): JSX.Element => ( + + + +) diff --git a/protocol-designer/src/ProtocolRoutes.tsx b/protocol-designer/src/ProtocolRoutes.tsx new file mode 100644 index 00000000000..b9d167acd95 --- /dev/null +++ b/protocol-designer/src/ProtocolRoutes.tsx @@ -0,0 +1,72 @@ +import * as React from 'react' +import { Route, Navigate, Routes, useLocation } from 'react-router-dom' +import { Box } from '@opentrons/components' +import { Landing } from './pages/Landing' +import { ProtocolOverview } from './pages/ProtocolOverview' +import { Liquids } from './pages/Liquids' +import { StartingDeckState } from './pages/StartingDeckState' +import { ProtocolSteps } from './pages/ProtocolSteps' +import { CreateNewProtocol } from './pages/CreateNewProtocol' +import { Navbar } from './Navbar' + +import type { RouteProps } from './types' + +const LANDING_ROUTE = '/' +const pdRoutes: RouteProps[] = [ + { + Component: ProtocolOverview, + name: 'Protocol overview', + navLinkTo: '/overview', + path: '/overview', + }, + { + Component: Liquids, + name: 'Liquids', + navLinkTo: '/liquids', + path: '/liquids', + }, + { + Component: StartingDeckState, + name: 'Starting deck state', + navLinkTo: '/startingDeckState', + path: '/startingDeckState', + }, + { + Component: ProtocolSteps, + name: 'Protocol steps', + navLinkTo: '/steps', + path: '/steps', + }, + { + Component: CreateNewProtocol, + name: 'Create new protocol', + navLinkTo: '/createNew', + path: '/createNew', + }, +] + +export function ProtocolRoutes(): JSX.Element { + const location = useLocation() + const currentPath = location.pathname + const landingPage: RouteProps = { + Component: Landing, + name: 'Landing', + navLinkTo: '/', + path: '/', + } + const allRoutes: RouteProps[] = [...pdRoutes, landingPage] + + return ( + <> + {currentPath === LANDING_ROUTE ? null : } + + + {allRoutes.map(({ Component, path }: RouteProps) => { + return } /> + })} + } /> + + + + ) +} diff --git a/protocol-designer/src/components/EditModules.tsx b/protocol-designer/src/components/EditModules.tsx index d40b685fed1..a62c1d97ca6 100644 --- a/protocol-designer/src/components/EditModules.tsx +++ b/protocol-designer/src/components/EditModules.tsx @@ -12,6 +12,7 @@ import { } from '../step-forms' import { moveDeckItem } from '../labware-ingred/actions/actions' import { getRobotType } from '../file-data/selectors' +import { getEnableMoam } from '../feature-flags/selectors' import { EditMultipleModulesModal } from './modals/EditModulesModal/EditMultipleModulesModal' import { useBlockingHint } from './Hints/useBlockingHint' import { MagneticModuleWarningModalContent } from './modals/EditModulesModal/MagneticModuleWarningModalContent' @@ -31,17 +32,17 @@ export interface ModelModuleInfo { slot: string } -const MOAM_MODULE_TYPES: ModuleType[] = [ - TEMPERATURE_MODULE_TYPE, - HEATERSHAKER_MODULE_TYPE, - MAGNETIC_BLOCK_TYPE, -] - export const EditModules = (props: EditModulesProps): JSX.Element => { const { onCloseClick, moduleToEdit } = props + const enableMoam = useSelector(getEnableMoam) const { moduleId, moduleType } = moduleToEdit const _initialDeckSetup = useSelector(stepFormSelectors.getInitialDeckSetup) const robotType = useSelector(getRobotType) + + const MOAM_MODULE_TYPES: ModuleType[] = enableMoam + ? [TEMPERATURE_MODULE_TYPE, HEATERSHAKER_MODULE_TYPE, MAGNETIC_BLOCK_TYPE] + : [TEMPERATURE_MODULE_TYPE] + const showMultipleModuleModal = robotType === FLEX_ROBOT_TYPE && MOAM_MODULE_TYPES.includes(moduleType) diff --git a/protocol-designer/src/components/ProtocolEditor.tsx b/protocol-designer/src/components/ProtocolEditor.tsx deleted file mode 100644 index 140b0ae08ee..00000000000 --- a/protocol-designer/src/components/ProtocolEditor.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import * as React from 'react' -import cx from 'classnames' -import { DndProvider } from 'react-dnd' -import { HTML5Backend } from 'react-dnd-html5-backend' -import { ComputingSpinner } from '../components/ComputingSpinner' -import { ConnectedNav } from '../containers/ConnectedNav' -import { Sidebar } from '../containers/ConnectedSidebar' -import { ConnectedTitleBar } from '../containers/ConnectedTitleBar' -import { MainPanel } from '../containers/ConnectedMainPanel' -import { PortalRoot as MainPageModalPortalRoot } from '../components/portals/MainPageModalPortal' -import { MAIN_CONTENT_FORCED_SCROLL_CLASSNAME } from '../ui/steps/utils' -import { PrereleaseModeIndicator } from './PrereleaseModeIndicator' -import { PortalRoot as TopPortalRoot } from './portals/TopPortal' -import { FileUploadMessageModal } from './modals/FileUploadMessageModal/FileUploadMessageModal' -import { LabwareUploadMessageModal } from './modals/LabwareUploadMessageModal/LabwareUploadMessageModal' -import { GateModal } from './modals/GateModal' -import { AnnouncementModal } from './modals/AnnouncementModal' -import styles from './ProtocolEditor.module.css' -import { CreateFileWizard } from './modals/CreateFileWizard' - -const showGateModal = - process.env.NODE_ENV === 'production' || process.env.OT_PD_SHOW_GATE - -function ProtocolEditorComponent(): JSX.Element { - return ( -
    - - - {showGateModal ? : null} - -
    - - -
    - - -
    - - - - - - - -
    -
    -
    -
    - ) -} - -export const ProtocolEditor = (): JSX.Element => ( - - - -) diff --git a/protocol-designer/src/components/StepCreationButton.tsx b/protocol-designer/src/components/StepCreationButton.tsx index a0aac17ceff..802a14862e7 100644 --- a/protocol-designer/src/components/StepCreationButton.tsx +++ b/protocol-designer/src/components/StepCreationButton.tsx @@ -21,6 +21,7 @@ import { selectors as stepFormSelectors, getIsModuleOnDeck, } from '../step-forms' +import { getEnableComment } from '../feature-flags/selectors' import { ConfirmDeleteModal, CLOSE_UNSAVED_STEP_FORM, @@ -105,19 +106,33 @@ export function StepButtonItem(props: StepButtonItemProps): JSX.Element { } export const StepCreationButton = (): JSX.Element => { + const enableComment = useSelector(getEnableComment) + const getSupportedSteps = (): Array< Exclude - > => [ - 'comment', - 'moveLabware', - 'moveLiquid', - 'mix', - 'pause', - 'heaterShaker', - 'magnet', - 'temperature', - 'thermocycler', - ] + > => + enableComment + ? [ + 'comment', + 'moveLabware', + 'moveLiquid', + 'mix', + 'pause', + 'heaterShaker', + 'magnet', + 'temperature', + 'thermocycler', + ] + : [ + 'moveLabware', + 'moveLiquid', + 'mix', + 'pause', + 'heaterShaker', + 'magnet', + 'temperature', + 'thermocycler', + ] const currentFormIsPresaved = useSelector( stepFormSelectors.getCurrentFormIsPresaved @@ -131,7 +146,7 @@ export const StepCreationButton = (): JSX.Element => { Exclude, boolean > = { - comment: true, + comment: enableComment, moveLabware: true, moveLiquid: true, mix: true, diff --git a/protocol-designer/src/components/__tests__/EditModules.test.tsx b/protocol-designer/src/components/__tests__/EditModules.test.tsx index 7f50b877744..3474c46c3d5 100644 --- a/protocol-designer/src/components/__tests__/EditModules.test.tsx +++ b/protocol-designer/src/components/__tests__/EditModules.test.tsx @@ -9,14 +9,16 @@ import { import { i18n } from '../../localization' import { getInitialDeckSetup } from '../../step-forms/selectors' import { getDismissedHints } from '../../tutorial/selectors' -import { EditModules } from '../EditModules' -import { EditModulesModal } from '../modals/EditModulesModal' +import { getEnableMoam } from '../../feature-flags/selectors' import { renderWithProviders } from '../../__testing-utils__' import { getRobotType } from '../../file-data/selectors' import { EditMultipleModulesModal } from '../modals/EditModulesModal/EditMultipleModulesModal' +import { EditModules } from '../EditModules' +import { EditModulesModal } from '../modals/EditModulesModal' import type { HintKey } from '../../tutorial' +vi.mock('../../feature-flags/selectors') vi.mock('../../step-forms/selectors') vi.mock('../modals/EditModulesModal/EditMultipleModulesModal') vi.mock('../modals/EditModulesModal') @@ -56,6 +58,7 @@ describe('EditModules', () => { labware: {}, additionalEquipmentOnDeck: {}, }) + vi.mocked(getEnableMoam).mockReturnValue(true) vi.mocked(EditModulesModal).mockReturnValue(
    mock EditModulesModal
    ) diff --git a/protocol-designer/src/components/__tests__/StepCreationButton.test.tsx b/protocol-designer/src/components/__tests__/StepCreationButton.test.tsx index 167dd538fdd..2086c396857 100644 --- a/protocol-designer/src/components/__tests__/StepCreationButton.test.tsx +++ b/protocol-designer/src/components/__tests__/StepCreationButton.test.tsx @@ -9,10 +9,12 @@ import { } from '../../step-forms/selectors' import { getIsMultiSelectMode } from '../../ui/steps' import { i18n } from '../../localization' +import { getEnableComment } from '../../feature-flags/selectors' import { StepCreationButton } from '../StepCreationButton' vi.mock('../../step-forms/selectors') vi.mock('../../ui/steps') +vi.mock('../../feature-flags/selectors') const render = () => { return renderWithProviders(, { i18nInstance: i18n })[0] @@ -20,6 +22,7 @@ const render = () => { describe('StepCreationButton', () => { beforeEach(() => { + vi.mocked(getEnableComment).mockReturnValue(true) vi.mocked(getCurrentFormIsPresaved).mockReturnValue(false) vi.mocked(getCurrentFormHasUnsavedChanges).mockReturnValue(false) vi.mocked(getIsMultiSelectMode).mockReturnValue(false) @@ -34,6 +37,7 @@ describe('StepCreationButton', () => { render() const addStep = screen.getByRole('button', { name: '+ Add Step' }) fireEvent.click(addStep) + screen.getByText('comment') screen.getByText('move labware') screen.getByText('transfer') screen.getByText('mix') diff --git a/protocol-designer/src/components/modals/CreateFileWizard/ModulesAndOtherTile.tsx b/protocol-designer/src/components/modals/CreateFileWizard/ModulesAndOtherTile.tsx index b253f772656..ef4c96977ac 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/ModulesAndOtherTile.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/ModulesAndOtherTile.tsx @@ -37,6 +37,7 @@ import gripperImage from '../../../images/flex_gripper.png' import wasteChuteImage from '../../../images/waste_chute.png' import trashBinImage from '../../../images/flex_trash_bin.png' import { uuid } from '../../../utils' +import { getEnableMoam } from '../../../feature-flags/selectors' import { selectors as featureFlagSelectors } from '../../../feature-flags' import { CrashInfoBox, ModuleDiagram } from '../../modules' import { ModuleFields } from '../FilePipettesModal/ModuleFields' @@ -201,16 +202,15 @@ export function ModulesAndOtherTile(props: WizardTileProps): JSX.Element { function FlexModuleFields(props: WizardTileProps): JSX.Element { const { watch, setValue } = props + const enableMoam = useSelector(getEnableMoam) const modules = watch('modules') const additionalEquipment = watch('additionalEquipment') const enableAbsorbanceReader = useSelector( featureFlagSelectors.getEnableAbsorbanceReader ) - const MOAM_MODULE_TYPES: ModuleType[] = [ - TEMPERATURE_MODULE_TYPE, - HEATERSHAKER_MODULE_TYPE, - MAGNETIC_BLOCK_TYPE, - ] + const MOAM_MODULE_TYPES: ModuleType[] = enableMoam + ? [TEMPERATURE_MODULE_TYPE, HEATERSHAKER_MODULE_TYPE, MAGNETIC_BLOCK_TYPE] + : [TEMPERATURE_MODULE_TYPE] const moduleTypesOnDeck = modules != null ? Object.values(modules).map(module => module.type) : [] diff --git a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/ModulesAndOtherTile.test.tsx b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/ModulesAndOtherTile.test.tsx index ba9924ee13e..63da7f3ed30 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/ModulesAndOtherTile.test.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/ModulesAndOtherTile.test.tsx @@ -5,7 +5,10 @@ import { fireEvent, screen, cleanup } from '@testing-library/react' import { FLEX_ROBOT_TYPE, OT2_ROBOT_TYPE } from '@opentrons/shared-data' import { renderWithProviders } from '../../../../__testing-utils__' import { i18n } from '../../../../localization' -import { getDisableModuleRestrictions } from '../../../../feature-flags/selectors' +import { + getDisableModuleRestrictions, + getEnableMoam, +} from '../../../../feature-flags/selectors' import { CrashInfoBox } from '../../../modules' import { ModuleFields } from '../../FilePipettesModal/ModuleFields' import { ModulesAndOtherTile } from '../ModulesAndOtherTile' @@ -58,6 +61,7 @@ describe('ModulesAndOtherTile', () => { ...props, ...mockWizardTileProps, } as WizardTileProps + vi.mocked(getEnableMoam).mockReturnValue(true) vi.mocked(CrashInfoBox).mockReturnValue(
    mock CrashInfoBox
    ) vi.mocked(EquipmentOption).mockReturnValue(
    mock EquipmentOption
    ) vi.mocked(getDisableModuleRestrictions).mockReturnValue(false) diff --git a/protocol-designer/src/containers/ConnectedNav.tsx b/protocol-designer/src/containers/ConnectedNav.tsx index fcc7d2c3311..d22fd9420c7 100644 --- a/protocol-designer/src/containers/ConnectedNav.tsx +++ b/protocol-designer/src/containers/ConnectedNav.tsx @@ -2,9 +2,11 @@ import * as React from 'react' import { useDispatch, useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' import { KNOWLEDGEBASE_ROOT_URL } from '../components/KnowledgeBaseLink' -import { NavTab, TabbedNavBar, OutsideLinkTab } from '@opentrons/components' import { selectors as fileSelectors } from '../file-data' import { actions, selectors } from '../navigation' +import { TabbedNavBar } from './TabbedNavBar' +import { NavTab } from './NavTab' +import { OutsideLinkTab } from './OutsideLinkTab' import type { Page } from '../navigation' export function ConnectedNav(): JSX.Element { diff --git a/components/src/tabbedNav/NavTab.tsx b/protocol-designer/src/containers/NavTab.tsx similarity index 90% rename from components/src/tabbedNav/NavTab.tsx rename to protocol-designer/src/containers/NavTab.tsx index a94f7c6e876..a409789b029 100644 --- a/components/src/tabbedNav/NavTab.tsx +++ b/protocol-designer/src/containers/NavTab.tsx @@ -2,12 +2,10 @@ import * as React from 'react' import { NavLink } from 'react-router-dom' import classnames from 'classnames' -import styles from './navbar.module.css' -import { Button } from '../buttons' -import { NotificationIcon } from '../icons' +import { Button, NotificationIcon } from '@opentrons/components' +import type { IconName, ButtonProps } from '@opentrons/components' -import type { IconName } from '../icons' -import type { ButtonProps } from '../buttons' +import styles from './navbar.module.css' export interface NavTabProps { /** optional click event for nav button */ diff --git a/components/src/tabbedNav/OutsideLinkTab.tsx b/protocol-designer/src/containers/OutsideLinkTab.tsx similarity index 92% rename from components/src/tabbedNav/OutsideLinkTab.tsx rename to protocol-designer/src/containers/OutsideLinkTab.tsx index e935df8ffd1..6ebbd5aca35 100644 --- a/components/src/tabbedNav/OutsideLinkTab.tsx +++ b/protocol-designer/src/containers/OutsideLinkTab.tsx @@ -1,11 +1,9 @@ import * as React from 'react' import cx from 'classnames' +import { Button, NotificationIcon } from '@opentrons/components' +import type { IconName } from '@opentrons/components' import styles from './navbar.module.css' -import { Button } from '../buttons' -import { NotificationIcon } from '../icons' - -import type { IconName } from '../icons' export interface OutsideLinkTabProps { /** optional click event for nav button */ diff --git a/components/src/tabbedNav/TabbedNavBar.tsx b/protocol-designer/src/containers/TabbedNavBar.tsx similarity index 100% rename from components/src/tabbedNav/TabbedNavBar.tsx rename to protocol-designer/src/containers/TabbedNavBar.tsx diff --git a/components/src/tabbedNav/navbar.module.css b/protocol-designer/src/containers/navbar.module.css similarity index 96% rename from components/src/tabbedNav/navbar.module.css rename to protocol-designer/src/containers/navbar.module.css index c1119a6a10c..078a300a395 100644 --- a/components/src/tabbedNav/navbar.module.css +++ b/protocol-designer/src/containers/navbar.module.css @@ -1,4 +1,4 @@ -@import '../index.module.css'; +@import '@opentrons/components/styles'; .navbar { flex: none; diff --git a/protocol-designer/src/feature-flags/reducers.ts b/protocol-designer/src/feature-flags/reducers.ts index 3e60f923a08..f08b2fa1081 100644 --- a/protocol-designer/src/feature-flags/reducers.ts +++ b/protocol-designer/src/feature-flags/reducers.ts @@ -25,6 +25,9 @@ const initialFlags: Flags = { process.env.OT_PD_ALLOW_ALL_TIPRACKS === '1' || false, OT_PD_ENABLE_ABSORBANCE_READER: process.env.OT_PD_ENABLE_ABSORBANCE_READER === '1' || false, + OT_PD_ENABLE_REDESIGN: process.env.OT_PD_ENABLE_REDESIGN === '1' || false, + OT_PD_ENABLE_MOAM: process.env.OT_PD_ENABLE_MOAM === '1' || false, + OT_PD_ENABLE_COMMENT: process.env.OT_PD_ENABLE_COMMENT === '1' || false, } // @ts-expect-error(sa, 2021-6-10): cannot use string literals as action type // TODO IMMEDIATELY: refactor this to the old fashioned way if we cannot have type safety: https://github.com/redux-utilities/redux-actions/issues/282#issuecomment-595163081 diff --git a/protocol-designer/src/feature-flags/selectors.ts b/protocol-designer/src/feature-flags/selectors.ts index fe18b33fb49..c70fcff00ef 100644 --- a/protocol-designer/src/feature-flags/selectors.ts +++ b/protocol-designer/src/feature-flags/selectors.ts @@ -2,10 +2,16 @@ import { createSelector } from 'reselect' import { getFlagsFromQueryParams } from './utils' import type { BaseState, Selector } from '../types' import type { Flags } from './types' -export const getFeatureFlagData = (state: BaseState): Flags => ({ - ...state.featureFlags.flags, - ...getFlagsFromQueryParams(), -}) + +const getFeatureFlags = (state: BaseState): Flags => state.featureFlags.flags + +export const getFeatureFlagData: Selector = createSelector( + [getFeatureFlags, getFlagsFromQueryParams], + (flags, queryParamsFlags) => ({ + ...flags, + ...queryParamsFlags, + }) +) export const getEnabledPrereleaseMode: Selector< boolean | null | undefined > = createSelector(getFeatureFlagData, flags => flags.PRERELEASE_MODE) @@ -23,3 +29,15 @@ export const getEnableAbsorbanceReader: Selector = createSelector( getFeatureFlagData, flags => flags.OT_PD_ENABLE_ABSORBANCE_READER ?? false ) +export const getEnableRedesign: Selector = createSelector( + getFeatureFlagData, + flags => flags.OT_PD_ENABLE_REDESIGN ?? false +) +export const getEnableMoam: Selector = createSelector( + getFeatureFlagData, + flags => flags.OT_PD_ENABLE_MOAM ?? false +) +export const getEnableComment: Selector = createSelector( + getFeatureFlagData, + flags => flags.OT_PD_ENABLE_COMMENT ?? false +) diff --git a/protocol-designer/src/feature-flags/types.ts b/protocol-designer/src/feature-flags/types.ts index 5f2ade969d7..5a1dfb810e3 100644 --- a/protocol-designer/src/feature-flags/types.ts +++ b/protocol-designer/src/feature-flags/types.ts @@ -23,7 +23,6 @@ export const DEPRECATED_FLAGS = [ 'OT_PD_ALLOW_96_CHANNEL', 'OT_PD_ENABLE_FLEX_DECK_MODIFICATION', 'OT_PD_ENABLE_MULTI_TIP', - 'OT_PD_ENABLE_MOAM', ] // union of feature flag string constant IDs export type FlagTypes = @@ -31,6 +30,9 @@ export type FlagTypes = | 'OT_PD_DISABLE_MODULE_RESTRICTIONS' | 'OT_PD_ALLOW_ALL_TIPRACKS' | 'OT_PD_ENABLE_ABSORBANCE_READER' + | 'OT_PD_ENABLE_REDESIGN' + | 'OT_PD_ENABLE_MOAM' + | 'OT_PD_ENABLE_COMMENT' // flags that are not in this list only show in prerelease mode export const userFacingFlags: FlagTypes[] = [ 'OT_PD_DISABLE_MODULE_RESTRICTIONS', @@ -40,5 +42,8 @@ export const allFlags: FlagTypes[] = [ ...userFacingFlags, 'PRERELEASE_MODE', 'OT_PD_ENABLE_ABSORBANCE_READER', + 'OT_PD_ENABLE_REDESIGN', + 'OT_PD_ENABLE_MOAM', + 'OT_PD_ENABLE_COMMENT', ] export type Flags = Partial> diff --git a/protocol-designer/src/index.tsx b/protocol-designer/src/index.tsx index 6f59322b947..aa88505e153 100644 --- a/protocol-designer/src/index.tsx +++ b/protocol-designer/src/index.tsx @@ -4,10 +4,10 @@ import { Provider } from 'react-redux' import { I18nextProvider } from 'react-i18next' import { configureStore } from './configureStore' -import { App } from './components/App' import { initialize } from './initialize' import { initializeMixpanel } from './analytics/mixpanel' import { i18n } from './localization' +import { App } from './App' // initialize Redux const store = configureStore() diff --git a/protocol-designer/src/localization/en/feature_flags.json b/protocol-designer/src/localization/en/feature_flags.json index 4902fa784d7..233a6632c69 100644 --- a/protocol-designer/src/localization/en/feature_flags.json +++ b/protocol-designer/src/localization/en/feature_flags.json @@ -15,5 +15,17 @@ "OT_PD_ENABLE_ABSORBANCE_READER": { "title": "Enable absorbance plate reader", "description": "Enable absorbance plate reader support." + }, + "OT_PD_ENABLE_REDESIGN": { + "title": "Enable redesign", + "description": "A whole new world." + }, + "OT_PD_ENABLE_MOAM": { + "title": "Enable multiple modules", + "description": "Enable multiple heater-shakers and magnetic blocks for Flex only." + }, + "OT_PD_ENABLE_COMMENT": { + "title": "Enable comment step", + "description": "You can add comments anywhere between timeline steps." } } diff --git a/protocol-designer/src/localization/en/index.ts b/protocol-designer/src/localization/en/index.ts index 36f6464d56e..8d1c9ca4b79 100644 --- a/protocol-designer/src/localization/en/index.ts +++ b/protocol-designer/src/localization/en/index.ts @@ -12,6 +12,10 @@ import nav from './nav.json' import shared from './shared.json' import tooltip from './tooltip.json' import well_selection from './well_selection.json' +import liquids from './liquids.json' +import protocol_overview from './protocol_overview.json' +import protocol_steps from './protocol_steps.json' +import starting_deck_state from './starting_deck_state.json' export const en = { alert, @@ -28,4 +32,8 @@ export const en = { shared, tooltip, well_selection, + liquids, + protocol_overview, + protocol_steps, + starting_deck_state, } diff --git a/protocol-designer/src/localization/en/liquids.json b/protocol-designer/src/localization/en/liquids.json new file mode 100644 index 00000000000..761f8b0fb2a --- /dev/null +++ b/protocol-designer/src/localization/en/liquids.json @@ -0,0 +1,3 @@ +{ + "liquids": "Liquids" +} diff --git a/protocol-designer/src/localization/en/protocol_overview.json b/protocol-designer/src/localization/en/protocol_overview.json new file mode 100644 index 00000000000..475116fc440 --- /dev/null +++ b/protocol-designer/src/localization/en/protocol_overview.json @@ -0,0 +1,3 @@ +{ + "protocol_overview": "Protocol overview" +} diff --git a/protocol-designer/src/localization/en/protocol_steps.json b/protocol-designer/src/localization/en/protocol_steps.json new file mode 100644 index 00000000000..e3b17ae7dfa --- /dev/null +++ b/protocol-designer/src/localization/en/protocol_steps.json @@ -0,0 +1,3 @@ +{ + "protocol_steps": "Protocol steps" +} diff --git a/protocol-designer/src/localization/en/shared.json b/protocol-designer/src/localization/en/shared.json index 41798ebbc15..6e2f2386cdc 100644 --- a/protocol-designer/src/localization/en/shared.json +++ b/protocol-designer/src/localization/en/shared.json @@ -3,10 +3,13 @@ "amount": "Amount:", "cancel": "Cancel", "confirm_reorder": "Are you sure you want to reorder these steps, it may cause errors?", + "create_new": "Create new", + "create_opentrons_protocol": "Create Opentrons protocol", "done": "Done", "edit": "edit", "exit": "exit", "go_back": "go back", + "import": "Import", "next": "next", "remove": "remove", "step": "Step {{current}} / {{max}}" diff --git a/protocol-designer/src/localization/en/starting_deck_state.json b/protocol-designer/src/localization/en/starting_deck_state.json new file mode 100644 index 00000000000..453c7406bb4 --- /dev/null +++ b/protocol-designer/src/localization/en/starting_deck_state.json @@ -0,0 +1,3 @@ +{ + "starting_deck_state": "Starting deck state" +} diff --git a/protocol-designer/src/localization/en/tooltip.json b/protocol-designer/src/localization/en/tooltip.json index a3175765b40..f8af2ec8725 100644 --- a/protocol-designer/src/localization/en/tooltip.json +++ b/protocol-designer/src/localization/en/tooltip.json @@ -15,6 +15,7 @@ "y_position_value": "A negative value moves towards the front", "step_description": { + "comment": "Add a comment anywhere between steps in your protocol", "heaterShaker": "Set heat, shake, or labware latch commands for the Heater-Shaker module", "magnet": "Engage or disengage the Magnetic module.", "mix": "Mix contents of wells/tubes.", diff --git a/protocol-designer/src/localization/index.ts b/protocol-designer/src/localization/index.ts index 744080537c8..7b6188486f5 100644 --- a/protocol-designer/src/localization/index.ts +++ b/protocol-designer/src/localization/index.ts @@ -24,6 +24,10 @@ i18n.use(initReactI18next).init( 'nav', 'tooltip', 'well_selection', + 'liquids', + 'protocol_overview', + 'protocol_steps', + 'starting_deck_state', ], defaultNS: 'shared', interpolation: { diff --git a/protocol-designer/src/pages/CreateNewProtocol/__tests__/CreateNewProtocol.test.tsx b/protocol-designer/src/pages/CreateNewProtocol/__tests__/CreateNewProtocol.test.tsx new file mode 100644 index 00000000000..8ca87d9d825 --- /dev/null +++ b/protocol-designer/src/pages/CreateNewProtocol/__tests__/CreateNewProtocol.test.tsx @@ -0,0 +1,3 @@ +import { it } from 'vitest' + +it.todo('write test for CreateNewProtocol') diff --git a/protocol-designer/src/pages/CreateNewProtocol/index.tsx b/protocol-designer/src/pages/CreateNewProtocol/index.tsx new file mode 100644 index 00000000000..9a1cb694ff3 --- /dev/null +++ b/protocol-designer/src/pages/CreateNewProtocol/index.tsx @@ -0,0 +1,5 @@ +import * as React from 'react' + +export function CreateNewProtocol(): JSX.Element { + return
    Create new protocol
    +} diff --git a/protocol-designer/src/pages/Landing/__tests__/Landing.test.tsx b/protocol-designer/src/pages/Landing/__tests__/Landing.test.tsx new file mode 100644 index 00000000000..b8cc9d03fa2 --- /dev/null +++ b/protocol-designer/src/pages/Landing/__tests__/Landing.test.tsx @@ -0,0 +1,3 @@ +import { it } from 'vitest' + +it.todo('write test for Landing page') diff --git a/protocol-designer/src/pages/Landing/index.tsx b/protocol-designer/src/pages/Landing/index.tsx new file mode 100644 index 00000000000..5b02885c46f --- /dev/null +++ b/protocol-designer/src/pages/Landing/index.tsx @@ -0,0 +1,27 @@ +import * as React from 'react' +import { NavLink } from 'react-router-dom' +import { + Flex, + SPACING, + DIRECTION_COLUMN, + ALIGN_CENTER, +} from '@opentrons/components' +import { useTranslation } from 'react-i18next' + +export function Landing(): JSX.Element { + const { t } = useTranslation('shared') + + return ( + + {t('create_opentrons_protocol')} + + {t('create_new')} + {t('import')} + + + ) +} diff --git a/protocol-designer/src/pages/Liquids/__tests__/Liquids.test.tsx b/protocol-designer/src/pages/Liquids/__tests__/Liquids.test.tsx new file mode 100644 index 00000000000..687f5b4c417 --- /dev/null +++ b/protocol-designer/src/pages/Liquids/__tests__/Liquids.test.tsx @@ -0,0 +1,3 @@ +import { it } from 'vitest' + +it.todo('write test for Liquids') diff --git a/protocol-designer/src/pages/Liquids/index.tsx b/protocol-designer/src/pages/Liquids/index.tsx new file mode 100644 index 00000000000..dc97bb06111 --- /dev/null +++ b/protocol-designer/src/pages/Liquids/index.tsx @@ -0,0 +1,7 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' + +export function Liquids(): JSX.Element { + const { t } = useTranslation('liquids') + return
    {t('liquids')}
    +} diff --git a/protocol-designer/src/pages/ProtocolOverview/__tests__/ProtocolOverview.test.tsx b/protocol-designer/src/pages/ProtocolOverview/__tests__/ProtocolOverview.test.tsx new file mode 100644 index 00000000000..d2b34d8ff5e --- /dev/null +++ b/protocol-designer/src/pages/ProtocolOverview/__tests__/ProtocolOverview.test.tsx @@ -0,0 +1,3 @@ +import { it } from 'vitest' + +it.todo('write test for ProtocolOverview') diff --git a/protocol-designer/src/pages/ProtocolOverview/index.tsx b/protocol-designer/src/pages/ProtocolOverview/index.tsx new file mode 100644 index 00000000000..a6be423cc74 --- /dev/null +++ b/protocol-designer/src/pages/ProtocolOverview/index.tsx @@ -0,0 +1,8 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' + +export function ProtocolOverview(): JSX.Element { + const { t } = useTranslation('protocol_overview') + + return
    {t('protocol_overview')}
    +} diff --git a/protocol-designer/src/pages/ProtocolSteps/__tests__/ProtocolSteps.test.tsx b/protocol-designer/src/pages/ProtocolSteps/__tests__/ProtocolSteps.test.tsx new file mode 100644 index 00000000000..a8970f1d9d2 --- /dev/null +++ b/protocol-designer/src/pages/ProtocolSteps/__tests__/ProtocolSteps.test.tsx @@ -0,0 +1,3 @@ +import { it } from 'vitest' + +it.todo('write test for ProtocolSteps') diff --git a/protocol-designer/src/pages/ProtocolSteps/index.tsx b/protocol-designer/src/pages/ProtocolSteps/index.tsx new file mode 100644 index 00000000000..706c9402531 --- /dev/null +++ b/protocol-designer/src/pages/ProtocolSteps/index.tsx @@ -0,0 +1,8 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' + +export function ProtocolSteps(): JSX.Element { + const { t } = useTranslation('protocol_steps') + + return
    {t('protocol_steps')}
    +} diff --git a/protocol-designer/src/pages/StartingDeckState/__tests__/StartingDeckState.test.tsx b/protocol-designer/src/pages/StartingDeckState/__tests__/StartingDeckState.test.tsx new file mode 100644 index 00000000000..8ab312f8435 --- /dev/null +++ b/protocol-designer/src/pages/StartingDeckState/__tests__/StartingDeckState.test.tsx @@ -0,0 +1,3 @@ +import { it } from 'vitest' + +it.todo('write test for StartingDeckState') diff --git a/protocol-designer/src/pages/StartingDeckState/index.tsx b/protocol-designer/src/pages/StartingDeckState/index.tsx new file mode 100644 index 00000000000..3027818c73b --- /dev/null +++ b/protocol-designer/src/pages/StartingDeckState/index.tsx @@ -0,0 +1,8 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' + +export function StartingDeckState(): JSX.Element { + const { t } = useTranslation('starting_deck_state') + + return
    {t('starting_deck_state')}
    +} diff --git a/protocol-designer/src/step-forms/selectors/index.ts b/protocol-designer/src/step-forms/selectors/index.ts index 70b4ff44ce2..e67eabdcdb5 100644 --- a/protocol-designer/src/step-forms/selectors/index.ts +++ b/protocol-designer/src/step-forms/selectors/index.ts @@ -613,6 +613,7 @@ export const getInvariantContext: Selector< featureFlagSelectors.getDisableModuleRestrictions, featureFlagSelectors.getAllowAllTipracks, featureFlagSelectors.getEnableAbsorbanceReader, + featureFlagSelectors.getEnableRedesign, ( labwareEntities, moduleEntities, @@ -620,7 +621,8 @@ export const getInvariantContext: Selector< additionalEquipmentEntities, disableModuleRestrictions, allowAllTipracks, - enableAbsorbanceReader + enableAbsorbanceReader, + enableEnableRedesign ) => ({ labwareEntities, moduleEntities, @@ -630,6 +632,7 @@ export const getInvariantContext: Selector< OT_PD_ALLOW_ALL_TIPRACKS: Boolean(allowAllTipracks), OT_PD_DISABLE_MODULE_RESTRICTIONS: Boolean(disableModuleRestrictions), OT_PD_ENABLE_ABSORBANCE_READER: Boolean(enableAbsorbanceReader), + OT_PD_ENABLE_REDESIGN: Boolean(enableEnableRedesign), }, }) ) diff --git a/protocol-designer/src/types.ts b/protocol-designer/src/types.ts index 0feb5692270..274f38948ac 100644 --- a/protocol-designer/src/types.ts +++ b/protocol-designer/src/types.ts @@ -46,3 +46,17 @@ export type WellVolumes = Record export type DeckSlot = string export type NozzleType = NozzleConfigurationStyle | '8-channel' + +export interface RouteProps { + /** the component rendered by a route match + * drop developed components into slots held by placeholder div components + * */ + Component: React.FC + /** a route/page name to render in the nav bar + */ + name: string + /** the path for navigation linking, for example to push to a default tab + */ + path: string + navLinkTo: string +} diff --git a/react-api-client/src/protocols/useCreateProtocolAnalysisMutation.ts b/react-api-client/src/protocols/useCreateProtocolAnalysisMutation.ts index f8ba6e10586..f2acace20ce 100644 --- a/react-api-client/src/protocols/useCreateProtocolAnalysisMutation.ts +++ b/react-api-client/src/protocols/useCreateProtocolAnalysisMutation.ts @@ -4,7 +4,8 @@ import { useHost } from '../api' import type { ErrorResponse, HostConfig, - RunTimeParameterCreateData, + RunTimeParameterFilesCreateData, + RunTimeParameterValuesCreateData, } from '@opentrons/api-client' import type { ProtocolAnalysisSummary } from '@opentrons/shared-data' import type { AxiosError } from 'axios' @@ -16,7 +17,8 @@ import type { export interface CreateProtocolAnalysisVariables { protocolKey: string - runTimeParameterValues?: RunTimeParameterCreateData + runTimeParameterValues?: RunTimeParameterValuesCreateData + runTimeParameterFiles?: RunTimeParameterFilesCreateData forceReAnalyze?: boolean } export type UseCreateProtocolMutationResult = UseMutationResult< @@ -53,11 +55,17 @@ export function useCreateProtocolAnalysisMutation( CreateProtocolAnalysisVariables >( [host, 'protocols', protocolId, 'analyses'], - ({ protocolKey, runTimeParameterValues, forceReAnalyze }) => + ({ + protocolKey, + runTimeParameterValues, + runTimeParameterFiles, + forceReAnalyze, + }) => createProtocolAnalysis( host as HostConfig, protocolKey, runTimeParameterValues, + runTimeParameterFiles, forceReAnalyze ) .then(response => { diff --git a/react-api-client/src/protocols/useCreateProtocolMutation.ts b/react-api-client/src/protocols/useCreateProtocolMutation.ts index 49c034e071e..5bc4e974b63 100644 --- a/react-api-client/src/protocols/useCreateProtocolMutation.ts +++ b/react-api-client/src/protocols/useCreateProtocolMutation.ts @@ -11,14 +11,16 @@ import type { ErrorResponse, HostConfig, Protocol, - RunTimeParameterCreateData, + RunTimeParameterFilesCreateData, + RunTimeParameterValuesCreateData, } from '@opentrons/api-client' export interface CreateProtocolVariables { files: File[] protocolKey?: string protocolKind?: string - runTimeParameterValues?: RunTimeParameterCreateData + runTimeParameterValues?: RunTimeParameterValuesCreateData + runTimeParameterFiles?: RunTimeParameterFilesCreateData } export type UseCreateProtocolMutationResult = UseMutationResult< Protocol, @@ -58,13 +60,15 @@ export function useCreateProtocolMutation( protocolKey, protocolKind = 'standard', runTimeParameterValues, + runTimeParameterFiles, }) => createProtocol( host as HostConfig, protocolFiles, protocolKey, protocolKind, - runTimeParameterValues + runTimeParameterValues, + runTimeParameterFiles ) .then(response => { const protocolId = response.data.data.id diff --git a/robot-server/robot_server/data_files/data_files_store.py b/robot-server/robot_server/data_files/data_files_store.py index 4d247e78ddf..fb84d15634e 100644 --- a/robot-server/robot_server/data_files/data_files_store.py +++ b/robot-server/robot_server/data_files/data_files_store.py @@ -10,6 +10,8 @@ from robot_server.persistence.database import sqlite_rowid from robot_server.persistence.tables import data_files_table +from .models import FileIdNotFoundError + @dataclass(frozen=True) class DataFileInfo: @@ -50,16 +52,30 @@ async def insert(self, file_info: DataFileInfo) -> None: with self._sql_engine.begin() as transaction: transaction.execute(statement) + def get(self, data_file_id: str) -> DataFileInfo: + """Get data file info from the database.""" + statement = sqlalchemy.select(data_files_table).where( + data_files_table.c.id == data_file_id + ) + with self._sql_engine.begin() as transaction: + try: + data_file_row = transaction.execute(statement).one() + except sqlalchemy.exc.NoResultFound as e: + raise FileIdNotFoundError(data_file_id) from e + + return _convert_row_data_file_info(data_file_row) + def _sql_get_all_from_engine(self) -> List[DataFileInfo]: statement = sqlalchemy.select(data_files_table).order_by(sqlite_rowid) with self._sql_engine.begin() as transaction: all_rows = transaction.execute(statement).all() - return [ - DataFileInfo( - id=sql_row.id, - name=sql_row.name, - created_at=sql_row.created_at, - file_hash=sql_row.file_hash, - ) - for sql_row in all_rows - ] + return [_convert_row_data_file_info(sql_row) for sql_row in all_rows] + + +def _convert_row_data_file_info(row: sqlalchemy.engine.Row) -> DataFileInfo: + return DataFileInfo( + id=row.id, + name=row.name, + created_at=row.created_at, + file_hash=row.file_hash, + ) diff --git a/robot-server/robot_server/data_files/models.py b/robot-server/robot_server/data_files/models.py index f5a83d42acf..7396743076a 100644 --- a/robot-server/robot_server/data_files/models.py +++ b/robot-server/robot_server/data_files/models.py @@ -3,6 +3,8 @@ from pydantic import Field +from opentrons_shared_data.errors import GeneralError + from robot_server.service.json_api import ResourceModel @@ -12,3 +14,14 @@ class DataFile(ResourceModel): id: str = Field(..., description="A unique identifier for this file.") name: str = Field(..., description="Name of the uploaded file.") createdAt: datetime = Field(..., description="When this data file was *uploaded*.") + + +class FileIdNotFoundError(GeneralError): + """Error raised when a given file ID is not found in the store.""" + + def __init__(self, data_file_id: str) -> None: + """Initialize the error message from the missing ID.""" + super().__init__( + message=f"Data file {data_file_id} was not found.", + detail={"dataFileId": data_file_id}, + ) diff --git a/robot-server/robot_server/data_files/router.py b/robot-server/robot_server/data_files/router.py index 56ef5a80ba1..e997059230f 100644 --- a/robot-server/robot_server/data_files/router.py +++ b/robot-server/robot_server/data_files/router.py @@ -4,7 +4,7 @@ from textwrap import dedent from typing import Optional, Literal, Union -from fastapi import APIRouter, UploadFile, File, Form, Depends, status +from fastapi import APIRouter, UploadFile, File, Form, Depends, Response, status from opentrons.protocol_reader import FileHasher, FileReaderWriter from robot_server.service.json_api import ( @@ -14,7 +14,7 @@ from robot_server.errors.error_responses import ErrorDetails, ErrorBody from .dependencies import get_data_files_directory, get_data_files_store from .data_files_store import DataFilesStore, DataFileInfo -from .models import DataFile +from .models import DataFile, FileIdNotFoundError from ..protocols.dependencies import get_file_hasher, get_file_reader_writer from ..service.dependencies import get_current_time, get_unique_id @@ -42,6 +42,13 @@ class FileNotFound(ErrorDetails): title: str = "Specified file path not found on the robot" +class FileIdNotFound(ErrorDetails): + """An error returned when specified file id was not found on the robot.""" + + id: Literal["FileIdNotFound"] = "FileIdNotFound" + title: str = "Specified file id not found on the robot" + + class UnexpectedFileFormat(ErrorDetails): """An error returned when specified file is not in expected format.""" @@ -141,3 +148,73 @@ async def upload_data_file( ), status_code=status.HTTP_201_CREATED, ) + + +@PydanticResponse.wrap_route( + datafiles_router.get, + path="/dataFiles/{dataFileId}", + summary="Get information about an uploaded data file", + responses={ + status.HTTP_200_OK: {"model": SimpleBody[DataFile]}, + status.HTTP_404_NOT_FOUND: {"model": ErrorBody[FileIdNotFound]}, + }, +) +async def get_data_file_info_by_id( + dataFileId: str, + data_files_store: DataFilesStore = Depends(get_data_files_store), +) -> PydanticResponse[SimpleBody[DataFile]]: + """Get data file info by ID. + + Args: + dataFileId: Data file identifier to fetch. + data_files_store: In-memory database of data file resources. + """ + try: + resource = data_files_store.get(dataFileId) + except FileIdNotFoundError as e: + raise FileIdNotFound(detail=str(e)).as_error(status.HTTP_404_NOT_FOUND) + + return await PydanticResponse.create( + content=SimpleBody.construct( + data=DataFile.construct( + id=resource.id, + name=resource.name, + createdAt=resource.created_at, + ) + ), + status_code=status.HTTP_200_OK, + ) + + +@datafiles_router.get( + path="/dataFiles/{dataFileId}/download", + summary="Get an uploaded data file", + responses={ + status.HTTP_404_NOT_FOUND: { + "model": ErrorBody[Union[FileIdNotFound, FileNotFound]] + }, + }, +) +async def get_data_file( + dataFileId: str, + data_files_directory: Path = Depends(get_data_files_directory), + data_files_store: DataFilesStore = Depends(get_data_files_store), + file_reader_writer: FileReaderWriter = Depends(get_file_reader_writer), +) -> Response: + """Get the requested data file by id.""" + try: + data_file_info = data_files_store.get(dataFileId) + except FileIdNotFoundError as e: + raise FileIdNotFound(detail=str(e)).as_error(status.HTTP_404_NOT_FOUND) + + try: + [buffered_file] = await file_reader_writer.read( + files=[data_files_directory / dataFileId / data_file_info.name] + ) + except FileNotFoundError as e: + raise FileNotFound(detail=str(e)).as_error(status.HTTP_404_NOT_FOUND) from e + + return Response( + content=buffered_file.contents.decode("utf-8"), + media_type="text/plain", + ) diff --git a/robot-server/robot_server/health/models.py b/robot-server/robot_server/health/models.py index 16090ade4d5..9f886ca8f2f 100644 --- a/robot-server/robot_server/health/models.py +++ b/robot-server/robot_server/health/models.py @@ -10,26 +10,44 @@ class HealthLinks(BaseModel): apiLog: str = Field( ..., - description="The path to the API logs endpoint", + description=( + "The path to the API logs endpoint." + " Deprecated: Use the `logs` field of the `GET /health` response" + " or refer to the OpenAPI specification of the `/logs` endpoint, instead." + ), examples=["/logs/api.log"], + deprecated=True, ) serialLog: str = Field( ..., - description="The path to the motor control serial communication logs endpoint", + description=( + "The path to the motor control serial communication logs endpoint." + " Deprecated: Use the `logs` field of the `GET /health` response" + " or refer to the OpenAPI specification of the `/logs` endpoint, instead." + ), examples=["/logs/serial.log"], + deprecated=True, ) serverLog: str = Field( ..., - description="The path to the HTTP server logs endpoint", + description=( + "The path to the HTTP server logs endpoint." + " Deprecated: Use the `logs` field of the `GET /health` response" + " or refer to the OpenAPI specification of the `/logs` endpoint, instead." + ), examples=["/logs/server.log"], + deprecated=True, ) oddLog: typing.Optional[str] = Field( None, description=( "The path to the on-device display app logs endpoint" - " (only present on the Opentrons Flex)" + " (only present on the Opentrons Flex)." + " Deprecated: Use the `logs` field of the `GET /health` response" + " or refer to the OpenAPI specification of the `/logs` endpoint, instead." ), examples=["/logs/touchscreen.log"], + deprecated=True, ) apiSpec: str = Field( ..., diff --git a/robot-server/robot_server/health/router.py b/robot-server/robot_server/health/router.py index 610979f0b3a..9d9572bfc9b 100644 --- a/robot-server/robot_server/health/router.py +++ b/robot-server/robot_server/health/router.py @@ -22,11 +22,17 @@ _log = logging.getLogger(__name__) -OT2_LOG_PATHS = ["/logs/serial.log", "/logs/api.log", "/logs/server.log"] +OT2_LOG_PATHS = [ + "/logs/serial.log", + "/logs/api.log", + "/logs/server.log", + "/logs/update_server.log", +] FLEX_LOG_PATHS = [ "/logs/serial.log", "/logs/api.log", "/logs/server.log", + "/logs/update_server.log", "/logs/touchscreen.log", ] VERSION_PATH = "/etc/VERSION.json" diff --git a/robot-server/robot_server/service/legacy/models/logs.py b/robot-server/robot_server/service/legacy/models/logs.py index b4dd56edbe2..5a501fdd2ff 100644 --- a/robot-server/robot_server/service/legacy/models/logs.py +++ b/robot-server/robot_server/service/legacy/models/logs.py @@ -8,6 +8,7 @@ class LogIdentifier(str, Enum): serial = "serial.log" server = "server.log" api_server = "combined_api_server.log" + update_server = "update_server.log" touchscreen = "touchscreen.log" diff --git a/robot-server/robot_server/service/legacy/routers/logs.py b/robot-server/robot_server/service/legacy/routers/logs.py index fe270611eb7..589413181fb 100644 --- a/robot-server/robot_server/service/legacy/routers/logs.py +++ b/robot-server/robot_server/service/legacy/routers/logs.py @@ -12,6 +12,7 @@ LogIdentifier.serial: log_control.SERIAL_SPECIAL, LogIdentifier.server: "uvicorn", LogIdentifier.api_server: "opentrons-robot-server", + LogIdentifier.update_server: "opentrons-update-server", LogIdentifier.touchscreen: "opentrons-robot-app", } diff --git a/robot-server/tests/data_files/test_data_files_store.py b/robot-server/tests/data_files/test_data_files_store.py index 8533f5997ff..18910c1de83 100644 --- a/robot-server/tests/data_files/test_data_files_store.py +++ b/robot-server/tests/data_files/test_data_files_store.py @@ -4,6 +4,7 @@ from sqlalchemy.engine import Engine as SQLEngine from robot_server.data_files.data_files_store import DataFilesStore, DataFileInfo +from robot_server.data_files.models import FileIdNotFoundError @pytest.fixture @@ -46,3 +47,25 @@ async def test_insert_file_info_with_existing_id( await subject.insert(data_file_info1) with pytest.raises(Exception): await subject.insert(data_file_info2) + + +async def test_insert_data_file_info_and_get_by_id( + subject: DataFilesStore, +) -> None: + """It should get the inserted data file info from the database.""" + data_file_info = DataFileInfo( + id="file-id", + name="file-name", + file_hash="abc", + created_at=datetime(year=2024, month=7, day=15, tzinfo=timezone.utc), + ) + await subject.insert(data_file_info) + assert subject.get("file-id") == data_file_info + + +def test_get_by_id_raises( + subject: DataFilesStore, +) -> None: + """It should raise if the requested data file id does not exist.""" + with pytest.raises(FileIdNotFoundError): + assert subject.get("file-id") diff --git a/robot-server/tests/data_files/test_router.py b/robot-server/tests/data_files/test_router.py index dd0f7b3112e..2594ec40329 100644 --- a/robot-server/tests/data_files/test_router.py +++ b/robot-server/tests/data_files/test_router.py @@ -9,8 +9,12 @@ from opentrons.protocol_reader import FileHasher, FileReaderWriter, BufferedFile from robot_server.data_files.data_files_store import DataFilesStore, DataFileInfo -from robot_server.data_files.models import DataFile -from robot_server.data_files.router import upload_data_file +from robot_server.data_files.models import DataFile, FileIdNotFoundError +from robot_server.data_files.router import ( + upload_data_file, + get_data_file_info_by_id, + get_data_file, +) from robot_server.errors.error_responses import ApiError @@ -231,3 +235,90 @@ async def test_upload_non_csv_file( ) assert exc_info.value.status_code == 422 assert exc_info.value.content["errors"][0]["id"] == "UnexpectedFileFormat" + + +async def test_get_data_file_info( + decoy: Decoy, + data_files_store: DataFilesStore, +) -> None: + """It should get the data file info from the provided data file id.""" + decoy.when(data_files_store.get("data-file-id")).then_return( + DataFileInfo( + id="qwerty", + name="abc.xyz", + file_hash="123", + created_at=datetime(year=2024, month=7, day=15), + ) + ) + + result = await get_data_file_info_by_id( + "data-file-id", + data_files_store=data_files_store, + ) + assert result.status_code == 200 + assert result.content.data == DataFile( + id="qwerty", + name="abc.xyz", + createdAt=datetime(year=2024, month=7, day=15), + ) + + +async def test_get_data_file_info_nonexistant( + decoy: Decoy, + data_files_store: DataFilesStore, +) -> None: + """It should return a 404 with a FileIdNotFound error.""" + decoy.when(data_files_store.get("data-file-id")).then_raise( + FileIdNotFoundError("oops") + ) + + with pytest.raises(ApiError) as exc_info: + await get_data_file_info_by_id( + "data-file-id", + data_files_store=data_files_store, + ) + assert exc_info.value.status_code == 404 + assert exc_info.value.content["errors"][0]["id"] == "FileIdNotFound" + + +async def test_get_data_file( + decoy: Decoy, + data_files_store: DataFilesStore, + file_reader_writer: FileReaderWriter, +) -> None: + """It should return the existing file.""" + data_files_directory = Path("/dev/null") + + decoy.when(data_files_store.get("data-file-id")).then_return( + DataFileInfo( + id="qwerty", + name="abc.xyz", + file_hash="123", + created_at=datetime(year=2024, month=7, day=15), + ) + ) + + decoy.when( + await file_reader_writer.read( + files=[data_files_directory / "data-file-id" / "abc.xyz"] + ) + ).then_return( + [ + BufferedFile( + name="123.456", + contents=bytes("some_content", encoding="utf-8"), + path=None, + ) + ] + ) + + result = await get_data_file( + "data-file-id", + data_files_directory=data_files_directory, + data_files_store=data_files_store, + file_reader_writer=file_reader_writer, + ) + + assert result.status_code == 200 + assert result.body == b"some_content" + assert result.media_type == "text/plain" diff --git a/robot-server/tests/health/test_health_router.py b/robot-server/tests/health/test_health_router.py index 0fa37526a3e..554ac2b7528 100644 --- a/robot-server/tests/health/test_health_router.py +++ b/robot-server/tests/health/test_health_router.py @@ -25,7 +25,12 @@ def test_get_health( "api_version": "mytestapiversion", "fw_version": "FW111", "board_revision": "BR2.1", - "logs": ["/logs/serial.log", "/logs/api.log", "/logs/server.log"], + "logs": [ + "/logs/serial.log", + "/logs/api.log", + "/logs/server.log", + "/logs/update_server.log", + ], "system_version": "mytestsystemversion", "minimum_protocol_api_version": list(MIN_SUPPORTED_VERSION), "maximum_protocol_api_version": list(MAX_SUPPORTED_VERSION), @@ -63,7 +68,12 @@ def test_get_health_with_none_version( "api_version": "mytestapiversion", "fw_version": "FW111", "board_revision": "BR2.1", - "logs": ["/logs/serial.log", "/logs/api.log", "/logs/server.log"], + "logs": [ + "/logs/serial.log", + "/logs/api.log", + "/logs/server.log", + "/logs/update_server.log", + ], "system_version": "mytestsystemversion", "minimum_protocol_api_version": list(MIN_SUPPORTED_VERSION), "maximum_protocol_api_version": list(MAX_SUPPORTED_VERSION), diff --git a/robot-server/tests/integration/data_files/test.csv b/robot-server/tests/integration/data_files/test.csv new file mode 100644 index 00000000000..c407ab6b4e2 --- /dev/null +++ b/robot-server/tests/integration/data_files/test.csv @@ -0,0 +1,3 @@ +a,b,c +1,2,3 +x,y,z \ No newline at end of file diff --git a/robot-server/tests/integration/fixtures.py b/robot-server/tests/integration/fixtures.py index 69d9ccdb00a..08175cc6b99 100644 --- a/robot-server/tests/integration/fixtures.py +++ b/robot-server/tests/integration/fixtures.py @@ -18,7 +18,12 @@ def check_health_response(response: Response) -> None: "api_version": __version__, "fw_version": "Virtual Smoothie", "board_revision": "2.1", - "logs": ["/logs/serial.log", "/logs/api.log", "/logs/server.log"], + "logs": [ + "/logs/serial.log", + "/logs/api.log", + "/logs/server.log", + "/logs/update_server.log", + ], "system_version": config.OT_SYSTEM_VERSION, "robot_model": "OT-2 Standard", "minimum_protocol_api_version": list(MIN_SUPPORTED_VERSION), @@ -47,6 +52,7 @@ def check_ot3_health_response(response: Response) -> None: "/logs/serial.log", "/logs/api.log", "/logs/server.log", + "/logs/update_server.log", "/logs/touchscreen.log", ], "system_version": config.OT_SYSTEM_VERSION, diff --git a/robot-server/tests/integration/http_api/data_files/test_upload_and_download_data_file.py b/robot-server/tests/integration/http_api/data_files/test_upload_and_download_data_file.py new file mode 100644 index 00000000000..9b61af10484 --- /dev/null +++ b/robot-server/tests/integration/http_api/data_files/test_upload_and_download_data_file.py @@ -0,0 +1,22 @@ +from tests.integration.robot_client import RobotClient + + +async def test_upload_and_download_data_file( + ot2_server_base_url: str, +) -> None: + """Test uploading data files and downloading the contents.""" + async with RobotClient.make( + base_url=ot2_server_base_url, version="*" + ) as robot_client: + response = await robot_client.post_data_files( + req_body={"filePath": "./tests/integration/data_files/test.csv"} + ) + assert response.status_code == 201 + response_data = response.json()["data"] + assert response_data["name"] == "test.csv" + + data_file_id = response_data["id"] + + response = await robot_client.get_data_files_download(data_file_id=data_file_id) + assert response.status_code == 200 + assert response.read().decode("utf-8") == "a,b,c\n1,2,3\nx,y,z" diff --git a/robot-server/tests/integration/http_api/data_files/test_upload_data_file.tavern.yaml b/robot-server/tests/integration/http_api/data_files/test_upload_data_file.tavern.yaml index 9d13807624b..14540f3c502 100644 --- a/robot-server/tests/integration/http_api/data_files/test_upload_data_file.tavern.yaml +++ b/robot-server/tests/integration/http_api/data_files/test_upload_data_file.tavern.yaml @@ -14,6 +14,7 @@ stages: save: json: file_info: data + data_file_id: data.id status_code: 201 json: data: @@ -32,7 +33,7 @@ stages: json: data: !force_original_structure '{file_info}' - - name: Upload color_codes_2.csv file using file path + - name: Upload sample_records.csv file using file path request: url: '{ot2_server_base_url}/dataFiles' method: POST @@ -48,3 +49,14 @@ stages: id: !anystr name: "sample_record.csv" createdAt: !re_fullmatch "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d+(Z|([+-]\\d{2}:\\d{2}))" + - name: Get color_codes.csv file info + request: + url: '{ot2_server_base_url}/dataFiles/{data_file_id}' + method: GET + response: + status_code: 200 + json: + data: + id: '{data_file_id}' + name: "color_codes.csv" + createdAt: !re_fullmatch "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d+(Z|([+-]\\d{2}:\\d{2}))" diff --git a/robot-server/tests/integration/robot_client.py b/robot-server/tests/integration/robot_client.py index 9af11d50cdb..b6d9614d7b2 100644 --- a/robot-server/tests/integration/robot_client.py +++ b/robot-server/tests/integration/robot_client.py @@ -332,6 +332,23 @@ async def put_deck_configuration( response.raise_for_status() return response + async def post_data_files(self, req_body: Dict[str, object]) -> Response: + """POST /dataFiles""" + response = await self.httpx_client.post( + url=f"{self.base_url}/dataFiles", + data=req_body, + ) + response.raise_for_status() + return response + + async def get_data_files_download(self, data_file_id: str) -> Response: + """GET /dataFiles/{data_file_id}/download""" + response = await self.httpx_client.get( + url=f"{self.base_url}/dataFiles/{data_file_id}/download", + ) + response.raise_for_status() + return response + async def poll_until_run_completes( robot_client: RobotClient, run_id: str, poll_interval: float = _RUN_POLL_INTERVAL diff --git a/scripts/python_build_utils.py b/scripts/python_build_utils.py index 317775cd2f2..7124e695cd2 100644 --- a/scripts/python_build_utils.py +++ b/scripts/python_build_utils.py @@ -56,9 +56,18 @@ def normalize_version(package, project, extra_tag='', git_dir=None): # the way they vendor dependencies, like the packaging module that # provides the way to normalize version numbers for wheel file names. So # we try all the possible ways to find it. + # Since 71.0.0 they have removed the need for extern + # So depending on the version of 3.10 you're building on you may or may not + # need to use the extern or import it directly try: - # new way - from setuptools.extern import packaging + import setuptools + major, minor, patch = [int(x, 10) for x in setuptools.__version__.split('.')] + if major < 71: + # new way + from setuptools.extern import packaging + else: + # new new way + import packaging except ImportError: # old way from pkg_resources.extern import packaging diff --git a/shared-data/command/schemas/8.json b/shared-data/command/schemas/8.json index d6fb78e12c3..d985fc7b902 100644 --- a/shared-data/command/schemas/8.json +++ b/shared-data/command/schemas/8.json @@ -41,6 +41,7 @@ "verifyTipPresence": "#/definitions/VerifyTipPresenceCreate", "getTipPresence": "#/definitions/GetTipPresenceCreate", "liquidProbe": "#/definitions/LiquidProbeCreate", + "tryLiquidProbe": "#/definitions/TryLiquidProbeCreate", "heaterShaker/waitForTemperature": "#/definitions/opentrons__protocol_engine__commands__heater_shaker__wait_for_temperature__WaitForTemperatureCreate", "heaterShaker/setTargetTemperature": "#/definitions/opentrons__protocol_engine__commands__heater_shaker__set_target_temperature__SetTargetTemperatureCreate", "heaterShaker/deactivateHeater": "#/definitions/DeactivateHeaterCreate", @@ -179,6 +180,9 @@ { "$ref": "#/definitions/LiquidProbeCreate" }, + { + "$ref": "#/definitions/TryLiquidProbeCreate" + }, { "$ref": "#/definitions/opentrons__protocol_engine__commands__heater_shaker__wait_for_temperature__WaitForTemperatureCreate" }, @@ -635,9 +639,15 @@ "description": "The front right nozzle in your configuration.", "pattern": "[A-Z]\\d{1,2}", "type": "string" + }, + "backLeftNozzle": { + "title": "Backleftnozzle", + "description": "The back left nozzle in your configuration.", + "pattern": "[A-Z]\\d{1,2}", + "type": "string" } }, - "required": ["primaryNozzle", "frontRightNozzle"] + "required": ["primaryNozzle", "frontRightNozzle", "backLeftNozzle"] }, "ConfigureNozzleLayoutParams": { "title": "ConfigureNozzleLayoutParams", @@ -2767,7 +2777,7 @@ }, "LiquidProbeParams": { "title": "LiquidProbeParams", - "description": "Parameters required to liquid probe a specific well.", + "description": "Parameters required for a `liquidProbe` command.", "type": "object", "properties": { "labwareId": { @@ -2799,7 +2809,7 @@ }, "LiquidProbeCreate": { "title": "LiquidProbeCreate", - "description": "Create LiquidProbe command request model.", + "description": "The request model for a `liquidProbe` command.", "type": "object", "properties": { "commandType": { @@ -2827,6 +2837,68 @@ }, "required": ["params"] }, + "TryLiquidProbeParams": { + "title": "TryLiquidProbeParams", + "description": "Parameters required for a `tryLiquidProbe` command.", + "type": "object", + "properties": { + "labwareId": { + "title": "Labwareid", + "description": "Identifier of labware to use.", + "type": "string" + }, + "wellName": { + "title": "Wellname", + "description": "Name of well to use in labware.", + "type": "string" + }, + "wellLocation": { + "title": "Welllocation", + "description": "Relative well location at which to perform the operation", + "allOf": [ + { + "$ref": "#/definitions/WellLocation" + } + ] + }, + "pipetteId": { + "title": "Pipetteid", + "description": "Identifier of pipette to use for liquid handling.", + "type": "string" + } + }, + "required": ["labwareId", "wellName", "pipetteId"] + }, + "TryLiquidProbeCreate": { + "title": "TryLiquidProbeCreate", + "description": "The request model for a `tryLiquidProbe` command.", + "type": "object", + "properties": { + "commandType": { + "title": "Commandtype", + "default": "tryLiquidProbe", + "enum": ["tryLiquidProbe"], + "type": "string" + }, + "params": { + "$ref": "#/definitions/TryLiquidProbeParams" + }, + "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"] + }, "opentrons__protocol_engine__commands__heater_shaker__wait_for_temperature__WaitForTemperatureParams": { "title": "WaitForTemperatureParams", "description": "Input parameters to wait for a Heater-Shaker's target temperature.", diff --git a/shared-data/command/types/pipetting.ts b/shared-data/command/types/pipetting.ts index 73daf7df1f0..7c4c46ef487 100644 --- a/shared-data/command/types/pipetting.ts +++ b/shared-data/command/types/pipetting.ts @@ -18,6 +18,7 @@ export type PipettingRunTimeCommand = | TouchTipRunTimeCommand | VerifyTipPresenceRunTimeCommand | LiquidProbeRunTimeCommand + | TryLiquidProbeRunTimeCommand export type PipettingCreateCommand = | AspirateCreateCommand @@ -36,6 +37,7 @@ export type PipettingCreateCommand = | TouchTipCreateCommand | VerifyTipPresenceCreateCommand | LiquidProbeCreateCommand + | TryLiquidProbeCreateCommand export interface ConfigureForVolumeCreateCommand extends CommonCommandCreateInfo { @@ -206,6 +208,16 @@ export interface LiquidProbeRunTimeCommand result?: Record } +export interface TryLiquidProbeCreateCommand extends CommonCommandCreateInfo { + commandType: 'tryLiquidProbe' + params: WellLocationParam & PipetteAccessParams +} +export interface TryLiquidProbeRunTimeCommand + extends CommonCommandRunTimeInfo, + TryLiquidProbeCreateCommand { + result?: Record +} + export type AspDispAirgapParams = FlowRateParams & PipetteAccessParams & VolumeParams & diff --git a/shared-data/deck/definitions/4/ot2_standard.json b/shared-data/deck/definitions/4/ot2_standard.json index 344469c65d3..e576a393e2c 100644 --- a/shared-data/deck/definitions/4/ot2_standard.json +++ b/shared-data/deck/definitions/4/ot2_standard.json @@ -8,7 +8,12 @@ "tags": ["ot2", "12 slots", "standard"] }, "robot": { - "model": "OT-2 Standard" + "model": "OT-2 Standard", + "extents": [446.75, 347.5, 0.0], + "mountOffsets": { + "left": [-34, 0.0, 0.0], + "right": [0.0, 0.0, 0.0] + } }, "locations": { "addressableAreas": [ diff --git a/shared-data/gripper/definitions/1/gripperV1.3.json b/shared-data/gripper/definitions/1/gripperV1.3.json new file mode 100644 index 00000000000..ef26cc062ba --- /dev/null +++ b/shared-data/gripper/definitions/1/gripperV1.3.json @@ -0,0 +1,29 @@ +{ + "$otSharedSchema": "gripper/schemas/1", + "model": "gripperV1.3", + "schemaVersion": 1, + "displayName": "Flex Gripper", + "gripForceProfile": { + "polynomial": [ + [0, 3.759869], + [1, 0.81005], + [2, 0.04597701] + ], + "defaultGripForce": 15.0, + "defaultIdleForce": 10.0, + "defaultHomeForce": 12.0, + "min": 2.0, + "max": 30.0 + }, + "geometry": { + "baseOffsetFromMount": [19.5, -74.325, -94.825], + "jawCenterOffsetFromBase": [0.0, 0.0, -86.475], + "pinOneOffsetFromBase": [6.0, -54.0, -98.475], + "pinTwoOffsetFromBase": [6.0, 54.0, -98.475], + "jawWidth": { + "min": 60.0, + "max": 92.0 + }, + "maxAllowedGripError": 3.0 + } +} diff --git a/shared-data/js/constants.ts b/shared-data/js/constants.ts index d1ce19a8a56..79241e1f508 100644 --- a/shared-data/js/constants.ts +++ b/shared-data/js/constants.ts @@ -48,7 +48,13 @@ export const MAGNETIC_BLOCK_V1: 'magneticBlockV1' = 'magneticBlockV1' export const GRIPPER_V1: 'gripperV1' = 'gripperV1' export const GRIPPER_V1_1: 'gripperV1.1' = 'gripperV1.1' export const GRIPPER_V1_2: 'gripperV1.2' = 'gripperV1.2' -export const GRIPPER_MODELS = [GRIPPER_V1, GRIPPER_V1_1, GRIPPER_V1_2] +export const GRIPPER_V1_3: 'gripperV1.3' = 'gripperV1.3' +export const GRIPPER_MODELS = [ + GRIPPER_V1, + GRIPPER_V1_1, + GRIPPER_V1_2, + GRIPPER_V1_3, +] // robot display name export const OT2_DISPLAY_NAME: 'Opentrons OT-2' = 'Opentrons OT-2' diff --git a/shared-data/js/gripper.ts b/shared-data/js/gripper.ts index 15c1d3f7f7b..9bc8282421e 100644 --- a/shared-data/js/gripper.ts +++ b/shared-data/js/gripper.ts @@ -1,8 +1,14 @@ import gripperV1 from '../gripper/definitions/1/gripperV1.json' import gripperV1_1 from '../gripper/definitions/1/gripperV1.1.json' import gripperV1_2 from '../gripper/definitions/1/gripperV1.2.json' +import gripperV1_3 from '../gripper/definitions/1/gripperV1.3.json' -import { GRIPPER_V1, GRIPPER_V1_1, GRIPPER_V1_2 } from './constants' +import { + GRIPPER_V1, + GRIPPER_V1_1, + GRIPPER_V1_2, + GRIPPER_V1_3, +} from './constants' import type { GripperModel, GripperDefinition } from './types' @@ -16,6 +22,8 @@ export const getGripperDef = ( return gripperV1_1 as GripperDefinition case GRIPPER_V1_2: return gripperV1_2 as GripperDefinition + case GRIPPER_V1_3: + return gripperV1_3 as GripperDefinition default: console.warn( `Could not find a gripper with model ${gripperModel}, falling back to most recent definition: ${GRIPPER_V1_2}` diff --git a/shared-data/js/pipettes.ts b/shared-data/js/pipettes.ts index 0901d10ae42..2921696b820 100644 --- a/shared-data/js/pipettes.ts +++ b/shared-data/js/pipettes.ts @@ -191,7 +191,7 @@ or PipetteModel such as 'p300_single_v1.3' and converts it to channels, model, and version in order to return the correct pipette schema v2 json files. **/ export const getPipetteSpecsV2 = ( - name: PipetteName | PipetteModel + name?: PipetteName | PipetteModel ): PipetteV2Specs | null => { if (name == null) { return null diff --git a/shared-data/js/types.ts b/shared-data/js/types.ts index 6fed46b5b74..c347042f4e4 100644 --- a/shared-data/js/types.ts +++ b/shared-data/js/types.ts @@ -24,6 +24,7 @@ import type { GRIPPER_V1, GRIPPER_V1_1, GRIPPER_V1_2, + GRIPPER_V1_3, EXTENSION, MAGNETIC_BLOCK_V1, } from './constants' @@ -238,6 +239,7 @@ export type GripperModel = | typeof GRIPPER_V1 | typeof GRIPPER_V1_1 | typeof GRIPPER_V1_2 + | typeof GRIPPER_V1_3 export type ModuleModelWithLegacy = | ModuleModel @@ -623,27 +625,65 @@ export interface NumberParameter extends BaseRunTimeParameter { value: number } -export interface Choice { +export interface NumberChoice { displayName: string - value: number | boolean | string + value: number +} + +export interface BooleanChoice { + displayName: string + value: boolean +} + +export interface StringChoice { + displayName: string + value: string +} + +export type Choice = NumberChoice | BooleanChoice | StringChoice + +interface NumberChoiceParameter extends BaseRunTimeParameter { + type: NumberParameterType + choices: NumberChoice[] + default: number + value: number } -export interface ChoiceParameter extends BaseRunTimeParameter { - type: NumberParameterType | BooleanParameterType | StringParameterType - choices: Choice[] - default: number | boolean | string - value: number | boolean | string +interface BooleanChoiceParameter extends BaseRunTimeParameter { + type: BooleanParameterType + choices: BooleanChoice[] + default: boolean + value: boolean } +interface StringChoiceParameter extends BaseRunTimeParameter { + type: StringParameterType + choices: StringChoice[] + default: string + value: string +} + +export type ChoiceParameter = + | NumberChoiceParameter + | BooleanChoiceParameter + | StringChoiceParameter + interface BooleanParameter extends BaseRunTimeParameter { type: BooleanParameterType default: boolean value: boolean } +export interface CsvFileParameterFileData { + id?: string + file?: File | null + filePath?: string + fileName?: string +} + export interface CsvFileParameter extends BaseRunTimeParameter { type: CsvFileParameterType - file?: { id?: string; file?: File | null } | null + file?: CsvFileParameterFileData | null } type NumberParameterType = 'int' | 'float' diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_3.json b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_3.json index 62033d0444c..f7a517120de 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_3.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_3.json @@ -61,7 +61,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.13, + "current": 0.2, "tipOverlaps": { "v0": { "default": 10.5, @@ -79,7 +79,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.19, + "current": 0.2, "tipOverlaps": { "v0": { "default": 10.5, diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_4.json b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_4.json index efc70c53611..33eca65b2fc 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_4.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_4.json @@ -24,8 +24,8 @@ "SingleA1": { "default": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 11.0, + "current": 0.15, "tipOverlaps": { "v0": { "default": 10.5, @@ -37,13 +37,13 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.67, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.67 + "default": 10.08, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.08, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.26, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.08, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.26 } } } @@ -51,8 +51,8 @@ "SingleH1": { "default": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 11.0, + "current": 0.15, "tipOverlaps": { "v0": { "default": 10.5, @@ -64,13 +64,13 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.67, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.67 + "default": 9.52, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.71, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.52, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.2, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.71, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.52, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.2 } } } @@ -79,7 +79,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.14, + "current": 0.2, "tipOverlaps": { "v0": { "default": 10.5, @@ -91,13 +91,13 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.67, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.67 + "default": 9.24, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.52, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.24, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.73, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.52, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.24, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.73 } } } @@ -106,7 +106,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.21, + "current": 0.2, "tipOverlaps": { "v0": { "default": 10.5, @@ -118,13 +118,13 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.67, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.67 + "default": 9.2, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.8, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.8 } } } @@ -133,7 +133,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.28, + "current": 0.35, "tipOverlaps": { "v0": { "default": 10.5, @@ -145,13 +145,13 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.67, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.67 + "default": 9.2, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.8, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.8 } } } @@ -160,7 +160,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.34, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -172,13 +172,13 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.67, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.67 + "default": 9.2, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.8, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.8 } } } @@ -187,7 +187,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.41, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -199,13 +199,13 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.67, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.67 + "default": 9.2, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.8, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.8 } } } @@ -214,7 +214,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.48, + "current": 0.5, "tipOverlaps": { "v0": { "default": 10.5, @@ -226,13 +226,13 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.67, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.67 + "default": 9.13, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.23, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.13, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.9, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.23, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.13, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.9 } } } @@ -260,6 +260,15 @@ "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.42, "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27, "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.67 + }, + "v3": { + "default": 9.28, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.37, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.28, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.67, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.37, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.28, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.67 } } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_5.json b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_5.json index efc70c53611..33eca65b2fc 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_5.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_5.json @@ -24,8 +24,8 @@ "SingleA1": { "default": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 11.0, + "current": 0.15, "tipOverlaps": { "v0": { "default": 10.5, @@ -37,13 +37,13 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.67, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.67 + "default": 10.08, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.08, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.26, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.08, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.26 } } } @@ -51,8 +51,8 @@ "SingleH1": { "default": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 11.0, + "current": 0.15, "tipOverlaps": { "v0": { "default": 10.5, @@ -64,13 +64,13 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.67, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.67 + "default": 9.52, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.71, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.52, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.2, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.71, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.52, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.2 } } } @@ -79,7 +79,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.14, + "current": 0.2, "tipOverlaps": { "v0": { "default": 10.5, @@ -91,13 +91,13 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.67, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.67 + "default": 9.24, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.52, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.24, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.73, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.52, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.24, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.73 } } } @@ -106,7 +106,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.21, + "current": 0.2, "tipOverlaps": { "v0": { "default": 10.5, @@ -118,13 +118,13 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.67, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.67 + "default": 9.2, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.8, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.8 } } } @@ -133,7 +133,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.28, + "current": 0.35, "tipOverlaps": { "v0": { "default": 10.5, @@ -145,13 +145,13 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.67, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.67 + "default": 9.2, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.8, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.8 } } } @@ -160,7 +160,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.34, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -172,13 +172,13 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.67, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.67 + "default": 9.2, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.8, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.8 } } } @@ -187,7 +187,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.41, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -199,13 +199,13 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.67, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.67 + "default": 9.2, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.8, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.8 } } } @@ -214,7 +214,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.48, + "current": 0.5, "tipOverlaps": { "v0": { "default": 10.5, @@ -226,13 +226,13 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.67, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.67 + "default": 9.13, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.23, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.13, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.9, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.23, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.13, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.9 } } } @@ -260,6 +260,15 @@ "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.42, "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27, "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.67 + }, + "v3": { + "default": 9.28, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.37, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.28, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.67, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.37, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.28, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.67 } } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_3.json b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_3.json index 514677f27f1..d5ef4b281ce 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_3.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_3.json @@ -53,7 +53,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.13, + "current": 0.2, "tipOverlaps": { "v0": { "default": 10.5, @@ -67,7 +67,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.19, + "current": 0.2, "tipOverlaps": { "v0": { "default": 10.5, diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_4.json b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_4.json index 9ca1bfa8926..d62d02e1b06 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_4.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_4.json @@ -24,8 +24,8 @@ "SingleA1": { "default": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 11.0, + "current": 0.15, "tipOverlaps": { "v0": { "default": 10.5, @@ -33,9 +33,9 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27 + "default": 10.08, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.08, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.08 } } } @@ -43,8 +43,8 @@ "SingleH1": { "default": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 11.0, + "current": 0.15, "tipOverlaps": { "v0": { "default": 10.5, @@ -52,9 +52,9 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27 + "default": 9.52, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.52, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.52 } } } @@ -63,7 +63,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.14, + "current": 0.2, "tipOverlaps": { "v0": { "default": 10.5, @@ -71,9 +71,9 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27 + "default": 9.24, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.24, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.24 } } } @@ -90,9 +90,9 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27 + "default": 9.2, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.2 } } } @@ -101,7 +101,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.28, + "current": 0.35, "tipOverlaps": { "v0": { "default": 10.5, @@ -109,9 +109,9 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27 + "default": 9.2, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.2 } } } @@ -120,7 +120,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.34, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -128,9 +128,9 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27 + "default": 9.2, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.2 } } } @@ -139,7 +139,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.41, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -147,9 +147,9 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27 + "default": 9.2, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.2 } } } @@ -158,7 +158,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.48, + "current": 0.5, "tipOverlaps": { "v0": { "default": 10.5, @@ -166,9 +166,9 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27 + "default": 9.13, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.13, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.13 } } } @@ -188,6 +188,11 @@ "default": 10.5, "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27 + }, + "v3": { + "default": 9.28, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.28, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.28 } } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_5.json b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_5.json index 9ca1bfa8926..d62d02e1b06 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_5.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_5.json @@ -24,8 +24,8 @@ "SingleA1": { "default": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 11.0, + "current": 0.15, "tipOverlaps": { "v0": { "default": 10.5, @@ -33,9 +33,9 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27 + "default": 10.08, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.08, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.08 } } } @@ -43,8 +43,8 @@ "SingleH1": { "default": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 11.0, + "current": 0.15, "tipOverlaps": { "v0": { "default": 10.5, @@ -52,9 +52,9 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27 + "default": 9.52, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.52, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.52 } } } @@ -63,7 +63,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.14, + "current": 0.2, "tipOverlaps": { "v0": { "default": 10.5, @@ -71,9 +71,9 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27 + "default": 9.24, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.24, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.24 } } } @@ -90,9 +90,9 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27 + "default": 9.2, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.2 } } } @@ -101,7 +101,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.28, + "current": 0.35, "tipOverlaps": { "v0": { "default": 10.5, @@ -109,9 +109,9 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27 + "default": 9.2, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.2 } } } @@ -120,7 +120,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.34, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -128,9 +128,9 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27 + "default": 9.2, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.2 } } } @@ -139,7 +139,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.41, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -147,9 +147,9 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27 + "default": 9.2, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.2 } } } @@ -158,7 +158,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.48, + "current": 0.5, "tipOverlaps": { "v0": { "default": 10.5, @@ -166,9 +166,9 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27 + "default": 9.13, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.13, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.13 } } } @@ -188,6 +188,11 @@ "default": 10.5, "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27 + }, + "v3": { + "default": 9.28, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.28, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.28 } } } diff --git a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_5.json b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_5.json index 8a45197f246..9c6df88b575 100644 --- a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_5.json +++ b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_5.json @@ -11,6 +11,34 @@ "SingleH12": ["H12"], "Column1": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"], "Column12": ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"], + "RowA": [ + "A1", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "A10", + "A11", + "A12" + ], + "RowH": [ + "H1", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9", + "H10", + "H11", + "H12" + ], "Full": [ "A1", "A2", @@ -119,8 +147,8 @@ "SingleA1": { "default": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -132,20 +160,20 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.21, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.74, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.16 + "default": 9.884, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.884, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.884 } } }, "t1000": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -153,16 +181,16 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.21 + "default": 10.335, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.335 } } }, "t200": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -170,16 +198,16 @@ "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.74 + "default": 9.981, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.981 } } }, "t50": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -187,9 +215,9 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.16 + "default": 9.884, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.884, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.884 } } } @@ -197,8 +225,8 @@ "SingleH1": { "default": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -210,20 +238,20 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.21, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.74, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.16 + "default": 9.884, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.884, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.884 } } }, "t1000": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -231,16 +259,16 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.21 + "default": 10.335, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.335 } } }, "t200": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -248,16 +276,16 @@ "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.74 + "default": 9.981, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.981 } } }, "t50": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -265,9 +293,9 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.16 + "default": 9.884, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.884, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.884 } } } @@ -275,8 +303,8 @@ "SingleA12": { "default": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -288,20 +316,20 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.21, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.74, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.16 + "default": 9.884, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.884, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.884 } } }, "t1000": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -309,16 +337,16 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.21 + "default": 10.335, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.335 } } }, "t200": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -326,16 +354,16 @@ "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.74 + "default": 9.981, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.981 } } }, "t50": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -343,9 +371,9 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.16 + "default": 9.884, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.884, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.884 } } } @@ -353,8 +381,8 @@ "SingleH12": { "default": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -366,20 +394,20 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.21, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.74, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.16 + "default": 9.884, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.884, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.884 } } }, "t1000": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -387,16 +415,16 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.21 + "default": 10.335, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.335 } } }, "t200": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -404,16 +432,16 @@ "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.74 + "default": 9.981, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.981 } } }, "t50": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -421,9 +449,9 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.16 + "default": 9.884, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.884, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.884 } } } @@ -451,6 +479,15 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.21, "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.74, "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.16 + }, + "v3": { + "default": 9.49, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.52, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.29, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.49, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.29, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.49, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.52 } } }, @@ -472,6 +509,11 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.21, "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.74, "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.16 + }, + "v3": { + "default": 10.29, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.29, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.29 } } }, @@ -493,6 +535,11 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.21, "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.74, "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.16 + }, + "v3": { + "default": 9.49, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.49, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.49 } } }, @@ -510,6 +557,11 @@ "default": 10.5, "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16, "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.16 + }, + "v3": { + "default": 9.52, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.52, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.52 } } } @@ -537,6 +589,15 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.21, "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.74, "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.16 + }, + "v3": { + "default": 9.49, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.52, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.29, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.49, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.29, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.49, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.52 } } }, @@ -552,8 +613,17 @@ }, "v1": { "default": 10.5, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16, "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.21 + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.21, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.74, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.16 + }, + "v3": { + "default": 10.29, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.29, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.29 } } }, @@ -569,8 +639,17 @@ }, "v1": { "default": 10.5, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21, "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.74 + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.21, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.74, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.16 + }, + "v3": { + "default": 9.49, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.49, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.49 } } }, @@ -588,6 +667,53 @@ "default": 10.5, "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16, "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.16 + }, + "v3": { + "default": 9.52, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.52, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.52 + } + } + } + }, + "RowA": { + "default": { + "speed": 10.0, + "distance": 13.0, + "current": 0.55, + "tipOverlaps": { + "v0": { + "default": 10.5, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.5, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5 + }, + "v1": { + "default": 9.379, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.379, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.87, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.6 + } + } + } + }, + "RowH": { + "default": { + "speed": 10.0, + "distance": 13.0, + "current": 0.55, + "tipOverlaps": { + "v0": { + "default": 10.5, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.5, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5 + }, + "v1": { + "default": 9.401, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.401, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.876, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.415 } } } diff --git a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_6.json b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_6.json index 949593f74c6..7bcfb04e4f0 100644 --- a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_6.json +++ b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_6.json @@ -147,65 +147,77 @@ "SingleA1": { "default": { "speed": 10.0, - "distance": 13.0, - "current": 0.15, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5, "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74 + "default": 9.884, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.884, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.884 } } }, "t1000": { "speed": 10.0, - "distance": 13.0, - "current": 0.15, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21 + "default": 10.335, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.335 } } }, "t200": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74 + "default": 9.981, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.981 } } }, "t50": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16 + "default": 9.884, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.884, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.884 } } } @@ -213,65 +225,77 @@ "SingleH1": { "default": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5, "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74 + "default": 9.884, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.884, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.884 } } }, - "1000": { + "t1000": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21 + "default": 10.335, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.335 } } }, "t200": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74 + "default": 9.981, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.981 } } }, "t50": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16 + "default": 9.884, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.884, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.884 } } } @@ -279,65 +303,77 @@ "SingleA12": { "default": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5, "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74 + "default": 9.884, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.884, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.884 } } }, "t1000": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21 + "default": 10.335, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.335 } } }, "t200": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74 + "default": 9.981, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.981 } } }, "t50": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16 + "default": 9.884, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.884, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.884 } } } @@ -345,65 +381,77 @@ "SingleH12": { "default": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5, "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74 + "default": 9.884, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.884, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.884 } } }, "t1000": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21 + "default": 10.335, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.335 } } }, "t200": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74 + "default": 9.981, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.981 } } }, "t50": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16 + "default": 9.884, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.884, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.884 } } } @@ -418,13 +466,25 @@ "default": 10.5, "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5, "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.5 }, "v1": { "default": 10.5, "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16, "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21, "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74 + }, + "v3": { + "default": 9.49, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.52, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.29, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.49, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.29, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.49, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.52 } } }, @@ -435,11 +495,17 @@ "tipOverlaps": { "v0": { "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.5 }, "v1": { "default": 10.5, "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21 + }, + "v3": { + "default": 10.29, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.29, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.29 } } }, @@ -450,11 +516,17 @@ "tipOverlaps": { "v0": { "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.5 }, "v1": { "default": 10.5, "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74 + }, + "v3": { + "default": 9.49, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.49, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.49 } } }, @@ -465,11 +537,17 @@ "tipOverlaps": { "v0": { "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.5 }, "v1": { "default": 10.5, "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16 + }, + "v3": { + "default": 9.52, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.52, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.52 } } } @@ -484,13 +562,25 @@ "default": 10.5, "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5, "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.5 }, "v1": { "default": 10.5, "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16, "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21, "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74 + }, + "v3": { + "default": 9.49, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.52, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.29, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.49, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.29, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.49, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.52 } } }, @@ -501,11 +591,17 @@ "tipOverlaps": { "v0": { "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.5 }, "v1": { "default": 10.5, "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21 + }, + "v3": { + "default": 10.29, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.29, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.29 } } }, @@ -516,11 +612,17 @@ "tipOverlaps": { "v0": { "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.5 }, "v1": { "default": 10.5, "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74 + }, + "v3": { + "default": 9.49, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.49, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.49 } } }, @@ -531,11 +633,17 @@ "tipOverlaps": { "v0": { "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.5 }, "v1": { "default": 10.5, "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16 + }, + "v3": { + "default": 9.52, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.52, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.52 } } } @@ -553,10 +661,10 @@ "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74 + "default": 9.379, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.379, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.87, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.6 } } } @@ -574,10 +682,10 @@ "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74 + "default": 9.401, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.401, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.876, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.415 } } } diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_6.json b/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_6.json index 7fe702bc97d..474bfca8df5 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_6.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_6.json @@ -21,50 +21,50 @@ "aspirate": { "default": { "1": [ - [0.462, 0.5646, 0.0415], - [0.648, 0.3716, 0.1307], - [1.032, 0.2742, 0.1938], - [1.37, 0.1499, 0.3221], - [2.014, 0.1044, 0.3845], - [2.772, 0.0432, 0.5076], - [3.05, -0.0809, 0.8517], - [3.4, 0.0256, 0.5268], - [3.962, 0.0612, 0.4057], - [4.438, 0.0572, 0.4217], - [5.164, 0.018, 0.5955], - [5.966, 0.0095, 0.6393], - [7.38, 0.0075, 0.6514], - [9.128, 0.0049, 0.6705], - [10.16, 0.0033, 0.6854], - [13.812, 0.0024, 0.6948], - [27.204, 0.0008, 0.7165], - [50.614, 0.0002, 0.7328], - [53.046, -0.0005, 0.7676] + [0.5175, 0.200812, 0.234783], + [0.675, 0.306434, 0.180124], + [0.965, 0.202975, 0.249958], + [1.3175, 0.174205, 0.277721], + [1.9175, 0.098216, 0.377837], + [2.71, 0.059576, 0.451928], + [3.0675, -0.013969, 0.651237], + [3.4125, 0.02245, 0.539521], + [3.8375, 0.027713, 0.52156], + [4.39, 0.072919, 0.348082], + [5.14, 0.022924, 0.567563], + [5.97, 0.013633, 0.615316], + [7.415, 0.009346, 0.640908], + [9.1925, 0.005809, 0.667137], + [10.235, 0.003508, 0.688285], + [13.885, 0.001976, 0.703972], + [27.41, 0.000922, 0.718603], + [51.005, 0.000207, 0.738189], + [53.4675, -0.000417, 0.770047] ] } }, "dispense": { "default": { "1": [ - [0.462, 0.5646, 0.0415], - [0.648, 0.3716, 0.1307], - [1.032, 0.2742, 0.1938], - [1.37, 0.1499, 0.3221], - [2.014, 0.1044, 0.3845], - [2.772, 0.0432, 0.5076], - [3.05, -0.0809, 0.8517], - [3.4, 0.0256, 0.5268], - [3.962, 0.0612, 0.4057], - [4.438, 0.0572, 0.4217], - [5.164, 0.018, 0.5955], - [5.966, 0.0095, 0.6393], - [7.38, 0.0075, 0.6514], - [9.128, 0.0049, 0.6705], - [10.16, 0.0033, 0.6854], - [13.812, 0.0024, 0.6948], - [27.204, 0.0008, 0.7165], - [50.614, 0.0002, 0.7328], - [53.046, -0.0005, 0.7676] + [0.5175, 0.200812, 0.234783], + [0.675, 0.306434, 0.180124], + [0.965, 0.202975, 0.249958], + [1.3175, 0.174205, 0.277721], + [1.9175, 0.098216, 0.377837], + [2.71, 0.059576, 0.451928], + [3.0675, -0.013969, 0.651237], + [3.4125, 0.02245, 0.539521], + [3.8375, 0.027713, 0.52156], + [4.39, 0.072919, 0.348082], + [5.14, 0.022924, 0.567563], + [5.97, 0.013633, 0.615316], + [7.415, 0.009346, 0.640908], + [9.1925, 0.005809, 0.667137], + [10.235, 0.003508, 0.688285], + [13.885, 0.001976, 0.703972], + [27.41, 0.000922, 0.718603], + [51.005, 0.000207, 0.738189], + [53.4675, -0.000417, 0.770047] ] } }, diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_6.json b/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_6.json index 33e1410ce99..e27cb962b70 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_6.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_6.json @@ -21,46 +21,48 @@ "aspirate": { "default": { "1": [ - [0.11, 0.207815, 0.040201], - [0.65, 0.43933, 0.014735], - [1.04, 0.256666, 0.133466], - [1.67, 0.147126, 0.247388], - [2.45, 0.078774, 0.361536], - [2.89, 0.042387, 0.450684], - [3.2, 0.014781, 0.530464], - [3.79, 0.071819, 0.347944], - [4.22, 0.051592, 0.424605], - [4.93, 0.021219, 0.552775], - [5.81, 0.023461, 0.541725], - [7.21, 0.008959, 0.625982], - [8.93, 0.005456, 0.651235], - [10.0, 0.007108, 0.636489], - [13.61, 0.002591, 0.681656], - [26.99, 0.001163, 0.701094], - [45.25, 0.000207, 0.726887] + [0.541667, 0.302563, 0.190632], + [0.668333, 0.225982, 0.232113], + [1.028333, 0.255401, 0.212451], + [1.365, 0.149807, 0.321038], + [1.965, 0.091112, 0.401156], + [2.78, 0.060163, 0.46197], + [3.136667, -0.019962, 0.684718], + [3.471667, 0.014059, 0.578005], + [3.916667, 0.031571, 0.51721], + [4.458333, 0.069665, 0.368009], + [5.173333, 0.015715, 0.608534], + [5.99, 0.011271, 0.631526], + [7.39, 0.006269, 0.661487], + [9.18, 0.006559, 0.659347], + [10.193333, 0.001667, 0.704254], + [13.87, 0.002548, 0.695269], + [27.368333, 0.000899, 0.718151], + [45.855, 0.000182, 0.737772] ] } }, "dispense": { "default": { "1": [ - [0.11, 0.207815, 0.040201], - [0.65, 0.43933, 0.014735], - [1.04, 0.256666, 0.133466], - [1.67, 0.147126, 0.247388], - [2.45, 0.078774, 0.361536], - [2.89, 0.042387, 0.450684], - [3.2, 0.014781, 0.530464], - [3.79, 0.071819, 0.347944], - [4.22, 0.051592, 0.424605], - [4.93, 0.021219, 0.552775], - [5.81, 0.023461, 0.541725], - [7.21, 0.008959, 0.625982], - [8.93, 0.005456, 0.651235], - [10.0, 0.007108, 0.636489], - [13.61, 0.002591, 0.681656], - [26.99, 0.001163, 0.701094], - [45.25, 0.000207, 0.726887] + [0.541667, 0.302563, 0.190632], + [0.668333, 0.225982, 0.232113], + [1.028333, 0.255401, 0.212451], + [1.365, 0.149807, 0.321038], + [1.965, 0.091112, 0.401156], + [2.78, 0.060163, 0.46197], + [3.136667, -0.019962, 0.684718], + [3.471667, 0.014059, 0.578005], + [3.916667, 0.031571, 0.51721], + [4.458333, 0.069665, 0.368009], + [5.173333, 0.015715, 0.608534], + [5.99, 0.011271, 0.631526], + [7.39, 0.006269, 0.661487], + [9.18, 0.006559, 0.659347], + [10.193333, 0.001667, 0.704254], + [13.87, 0.002548, 0.695269], + [27.368333, 0.000899, 0.718151], + [45.855, 0.000182, 0.737772] ] } }, diff --git a/shared-data/protocol/fixtures/6/heaterShakerCommands.json b/shared-data/protocol/fixtures/6/heaterShakerCommands.json index 8307cf8aedd..3c4733c0268 100644 --- a/shared-data/protocol/fixtures/6/heaterShakerCommands.json +++ b/shared-data/protocol/fixtures/6/heaterShakerCommands.json @@ -1241,7 +1241,7 @@ "commands": [ { "commandType": "loadPipette", - "id": "0abc123", + "key": "0abc123", "params": { "pipetteId": "pipetteId", "mount": "left" @@ -1249,7 +1249,7 @@ }, { "commandType": "loadModule", - "id": "1abc123", + "key": "1abc123", "params": { "moduleId": "magneticModuleId", "location": { "slotName": "3" } @@ -1257,7 +1257,7 @@ }, { "commandType": "loadModule", - "id": "2abc123", + "key": "2abc123", "params": { "moduleId": "heaterShakerId", "location": { "slotName": "1" } @@ -1265,7 +1265,7 @@ }, { "commandType": "loadLabware", - "id": "3abc123", + "key": "3abc123", "params": { "labwareId": "sourcePlateId", "location": { @@ -1275,7 +1275,7 @@ }, { "commandType": "loadLabware", - "id": "4abc123", + "key": "4abc123", "params": { "labwareId": "destPlateId", "location": { @@ -1285,7 +1285,7 @@ }, { "commandType": "loadLabware", - "id": "5abc123", + "key": "5abc123", "params": { "labwareId": "tipRackId", "location": { "slotName": "8" } @@ -1293,7 +1293,7 @@ }, { "commandType": "loadLabware", - "id": "6abc123", + "key": "6abc123", "params": { "labwareId": "fixedTrash", "location": { @@ -1303,7 +1303,7 @@ }, { "commandType": "loadLiquid", - "id": "7abc123", + "key": "7abc123", "params": { "liquidId": "waterId", "labwareId": "sourcePlateId", @@ -1316,7 +1316,7 @@ { "commandType": "pickUpTip", - "id": "8abc123", + "key": "8abc123", "params": { "pipetteId": "pipetteId", "labwareId": "tipRackId", @@ -1325,7 +1325,7 @@ }, { "commandType": "aspirate", - "id": "9abc123", + "key": "9abc123", "params": { "pipetteId": "pipetteId", "labwareId": "sourcePlateId", @@ -1340,14 +1340,14 @@ }, { "commandType": "delay", - "id": "10abc123", + "key": "10abc123", "params": { "seconds": 42 } }, { "commandType": "dispense", - "id": "11abc123", + "key": "11abc123", "params": { "pipetteId": "pipetteId", "labwareId": "destPlateId", @@ -1362,7 +1362,7 @@ }, { "commandType": "heaterShaker/setAndWaitForShakeSpeed", - "id": "13abc123", + "key": "13abc123", "params": { "moduleId": "heaterShakerId", "rpm": 2000 @@ -1370,7 +1370,7 @@ }, { "commandType": "heaterShaker/setTargetTemperature", - "id": "14abc123", + "key": "14abc123", "params": { "moduleId": "heaterShakerId", "celsius": 42 @@ -1378,28 +1378,28 @@ }, { "commandType": "heaterShaker/waitForTemperature", - "id": "15abc123", + "key": "15abc123", "params": { "moduleId": "heaterShakerId" } }, { "commandType": "heaterShaker/openLabwareLatch", - "id": "16abc123", + "key": "16abc123", "params": { "moduleId": "heaterShakerId" } }, { "commandType": "heaterShaker/closeLabwareLatch", - "id": "17abc123", + "key": "17abc123", "params": { "moduleId": "heaterShakerId" } }, { "commandType": "heaterShaker/deactivateHeater", - "id": "16abc123", + "key": "16abc123", "params": { "moduleId": "heaterShakerId" } diff --git a/shared-data/protocol/fixtures/6/heaterShakerCommandsWithResultsKey.json b/shared-data/protocol/fixtures/6/heaterShakerCommandsWithResultsKey.json index ef83c29f0a6..6a028cf04e9 100644 --- a/shared-data/protocol/fixtures/6/heaterShakerCommandsWithResultsKey.json +++ b/shared-data/protocol/fixtures/6/heaterShakerCommandsWithResultsKey.json @@ -2494,7 +2494,7 @@ }, "commands": [ { - "id": "0", + "key": "0", "commandType": "loadPipette", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -2502,7 +2502,7 @@ } }, { - "id": "1", + "key": "1", "commandType": "loadPipette", "params": { "pipetteId": "4da579b0-a9bf-11eb-bce6-9f1d5b9c1a1b", @@ -2510,7 +2510,7 @@ } }, { - "id": "2", + "key": "2", "commandType": "loadModule", "params": { "moduleId": "3e012450-3412-11eb-ad93-ed232a2337cf:heaterShakerModuleType", @@ -2523,7 +2523,7 @@ } }, { - "id": "5", + "key": "5", "commandType": "loadLabware", "params": { "labwareId": "fixedTrash", @@ -2590,7 +2590,7 @@ } }, { - "id": "6", + "key": "6", "commandType": "loadLabware", "params": { "labwareId": "3e047fb0-3412-11eb-ad93-ed232a2337cf:opentrons/opentrons_96_tiprack_1000ul/1", @@ -3622,7 +3622,7 @@ } }, { - "id": "7", + "key": "7", "commandType": "loadLabware", "params": { "labwareId": "5ae317e0-3412-11eb-ad93-ed232a2337cf:opentrons/nest_1_reservoir_195ml/1", @@ -3691,7 +3691,7 @@ } }, { - "id": "8", + "key": "8", "commandType": "loadLabware", "params": { "labwareId": "aac5d680-3412-11eb-ad93-ed232a2337cf:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", diff --git a/shared-data/protocol/fixtures/6/multipleTempModules.json b/shared-data/protocol/fixtures/6/multipleTempModules.json index ecca66a0271..bba498a875d 100644 --- a/shared-data/protocol/fixtures/6/multipleTempModules.json +++ b/shared-data/protocol/fixtures/6/multipleTempModules.json @@ -4561,7 +4561,7 @@ }, "commands": [ { - "id": "0", + "key": "0", "commandType": "loadPipette", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -4569,7 +4569,7 @@ } }, { - "id": "1", + "key": "1", "commandType": "loadPipette", "params": { "pipetteId": "4da579b0-a9bf-11eb-bce6-9f1d5b9c1a1b", @@ -4577,7 +4577,7 @@ } }, { - "id": "2", + "key": "2", "commandType": "loadModule", "params": { "moduleId": "3e012450-3412-11eb-ad93-ed232a2337cf:magneticModuleType", @@ -4590,7 +4590,7 @@ } }, { - "id": "3", + "key": "3", "commandType": "loadModule", "params": { "moduleId": "3e0283e0-3412-11eb-ad93-ed232a2337cf:temperatureModuleType1", @@ -4603,7 +4603,7 @@ } }, { - "id": "4", + "key": "4", "commandType": "loadModule", "params": { "moduleId": "3e039550-3412-11eb-ad93-ed232a2337cf:temperatureModuleType2", @@ -4616,7 +4616,7 @@ } }, { - "id": "5", + "key": "5", "commandType": "loadLabware", "params": { "labwareId": "fixedTrash", @@ -4683,7 +4683,7 @@ } }, { - "id": "6", + "key": "6", "commandType": "loadLabware", "params": { "labwareId": "3e047fb0-3412-11eb-ad93-ed232a2337cf:opentrons/opentrons_96_tiprack_1000ul/1", @@ -5715,7 +5715,7 @@ } }, { - "id": "7", + "key": "7", "commandType": "loadLabware", "params": { "labwareId": "5ae317e0-3412-11eb-ad93-ed232a2337cf:opentrons/nest_1_reservoir_195ml/1", @@ -5784,7 +5784,7 @@ } }, { - "id": "8", + "key": "8", "commandType": "loadLabware", "params": { "labwareId": "aac5d680-3412-11eb-ad93-ed232a2337cf:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", @@ -6815,7 +6815,7 @@ } }, { - "id": "9", + "key": "9", "commandType": "loadLabware", "params": { "labwareId": "60e8b050-3412-11eb-ad93-ed232a2337cf:opentrons/corning_24_wellplate_3.4ml_flat/1", @@ -7121,7 +7121,7 @@ } }, { - "id": "10", + "key": "10", "commandType": "loadLabware", "params": { "labwareId": "ada13110-3412-11eb-ad93-ed232a2337cf:opentrons/opentrons_96_aluminumblock_generic_pcr_strip_200ul/1", @@ -8160,7 +8160,7 @@ } }, { - "id": "11", + "key": "11", "commandType": "loadLabware", "params": { "labwareId": "b0103540-3412-11eb-ad93-ed232a2337cf:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", @@ -9191,7 +9191,7 @@ } }, { - "id": "12", + "key": "12", "commandType": "loadLabware", "params": { "labwareId": "faa13a50-a9bf-11eb-bce6-9f1d5b9c1a1b:opentrons/opentrons_96_tiprack_20ul/1", @@ -10223,7 +10223,7 @@ } }, { - "id": "13", + "key": "13", "commandType": "loadLabware", "params": { "labwareId": "53d3b350-a9c0-11eb-bce6-9f1d5b9c1a1b", @@ -10529,7 +10529,7 @@ } }, { - "id": "14", + "key": "14", "commandType": "pickUpTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10538,7 +10538,7 @@ } }, { - "id": "15", + "key": "15", "commandType": "aspirate", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10553,7 +10553,7 @@ } }, { - "id": "16", + "key": "16", "commandType": "dispense", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10568,7 +10568,7 @@ } }, { - "id": "17", + "key": "17", "commandType": "dropTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10577,7 +10577,7 @@ } }, { - "id": "18", + "key": "18", "commandType": "pickUpTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10586,7 +10586,7 @@ } }, { - "id": "19", + "key": "19", "commandType": "aspirate", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10601,7 +10601,7 @@ } }, { - "id": "20", + "key": "20", "commandType": "dispense", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10616,7 +10616,7 @@ } }, { - "id": "21", + "key": "21", "commandType": "dropTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10625,7 +10625,7 @@ } }, { - "id": "22", + "key": "22", "commandType": "pickUpTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10634,7 +10634,7 @@ } }, { - "id": "23", + "key": "23", "commandType": "aspirate", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10649,7 +10649,7 @@ } }, { - "id": "24", + "key": "24", "commandType": "dispense", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10664,7 +10664,7 @@ } }, { - "id": "25", + "key": "25", "commandType": "dropTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10673,7 +10673,7 @@ } }, { - "id": "26", + "key": "26", "commandType": "pickUpTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10682,7 +10682,7 @@ } }, { - "id": "27", + "key": "27", "commandType": "aspirate", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10697,7 +10697,7 @@ } }, { - "id": "28", + "key": "28", "commandType": "dispense", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10712,7 +10712,7 @@ } }, { - "id": "29", + "key": "29", "commandType": "dropTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10721,7 +10721,7 @@ } }, { - "id": "30", + "key": "30", "commandType": "pickUpTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10730,7 +10730,7 @@ } }, { - "id": "31", + "key": "31", "commandType": "aspirate", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10745,7 +10745,7 @@ } }, { - "id": "32", + "key": "32", "commandType": "dispense", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10760,7 +10760,7 @@ } }, { - "id": "33", + "key": "33", "commandType": "dropTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10769,7 +10769,7 @@ } }, { - "id": "34", + "key": "34", "commandType": "pickUpTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10778,7 +10778,7 @@ } }, { - "id": "35", + "key": "35", "commandType": "aspirate", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10793,7 +10793,7 @@ } }, { - "id": "36", + "key": "36", "commandType": "dispense", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10808,7 +10808,7 @@ } }, { - "id": "37", + "key": "37", "commandType": "dropTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10817,7 +10817,7 @@ } }, { - "id": "38", + "key": "38", "commandType": "pickUpTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10826,7 +10826,7 @@ } }, { - "id": "39", + "key": "39", "commandType": "aspirate", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10841,7 +10841,7 @@ } }, { - "id": "40", + "key": "40", "commandType": "dispense", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10856,7 +10856,7 @@ } }, { - "id": "41", + "key": "41", "commandType": "dropTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10865,7 +10865,7 @@ } }, { - "id": "42", + "key": "42", "commandType": "pickUpTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10874,7 +10874,7 @@ } }, { - "id": "43", + "key": "43", "commandType": "aspirate", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10889,7 +10889,7 @@ } }, { - "id": "44", + "key": "44", "commandType": "dispense", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10904,7 +10904,7 @@ } }, { - "id": "45", + "key": "45", "commandType": "dropTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10913,7 +10913,7 @@ } }, { - "id": "46", + "key": "46", "commandType": "pickUpTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10922,7 +10922,7 @@ } }, { - "id": "47", + "key": "47", "commandType": "aspirate", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10937,7 +10937,7 @@ } }, { - "id": "48", + "key": "48", "commandType": "dispense", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10952,7 +10952,7 @@ } }, { - "id": "49", + "key": "49", "commandType": "dropTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10961,7 +10961,7 @@ } }, { - "id": "50", + "key": "50", "commandType": "pickUpTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10970,7 +10970,7 @@ } }, { - "id": "51", + "key": "51", "commandType": "aspirate", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10985,7 +10985,7 @@ } }, { - "id": "52", + "key": "52", "commandType": "dispense", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -11000,7 +11000,7 @@ } }, { - "id": "53", + "key": "53", "commandType": "dropTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -11009,7 +11009,7 @@ } }, { - "id": "54", + "key": "54", "commandType": "pickUpTip", "params": { "pipetteId": "4da579b0-a9bf-11eb-bce6-9f1d5b9c1a1b", @@ -11018,7 +11018,7 @@ } }, { - "id": "55", + "key": "55", "commandType": "aspirate", "params": { "pipetteId": "4da579b0-a9bf-11eb-bce6-9f1d5b9c1a1b", @@ -11033,7 +11033,7 @@ } }, { - "id": "56", + "key": "56", "commandType": "dispense", "params": { "pipetteId": "4da579b0-a9bf-11eb-bce6-9f1d5b9c1a1b", @@ -11048,7 +11048,7 @@ } }, { - "id": "57", + "key": "57", "commandType": "dropTip", "params": { "pipetteId": "4da579b0-a9bf-11eb-bce6-9f1d5b9c1a1b", diff --git a/shared-data/protocol/fixtures/6/multipleTipracks.json b/shared-data/protocol/fixtures/6/multipleTipracks.json index cc38a6c9f89..f63048ade3a 100644 --- a/shared-data/protocol/fixtures/6/multipleTipracks.json +++ b/shared-data/protocol/fixtures/6/multipleTipracks.json @@ -1967,7 +1967,7 @@ "commands": [ { "commandType": "loadPipette", - "id": "0abc1", + "key": "0abc1", "params": { "pipetteId": "50d23e00-0042-11ec-8258-f7ffdf5ad45a", "mount": "left" @@ -1978,7 +1978,7 @@ }, { "commandType": "loadPipette", - "id": "0abc2", + "key": "0abc2", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", "mount": "right" @@ -1989,7 +1989,7 @@ }, { "commandType": "loadLabware", - "id": "0abc3", + "key": "0abc3", "params": { "labwareId": "fixedTrash", "location": { "slotName": "12" } @@ -2043,7 +2043,7 @@ }, { "commandType": "loadLabware", - "id": "0abc4", + "key": "0abc4", "params": { "labwareId": "50d3ebb0-0042-11ec-8258-f7ffdf5ad45a:opentrons/opentrons_96_tiprack_300ul/1", "location": { "slotName": "1" } @@ -3069,7 +3069,7 @@ }, { "commandType": "loadLabware", - "id": "0abc5", + "key": "0abc5", "params": { "labwareId": "9fbc1db0-0042-11ec-8258-f7ffdf5ad45a:opentrons/nest_12_reservoir_15ml/1", "location": { "slotName": "10" } @@ -3266,7 +3266,7 @@ }, { "commandType": "loadLabware", - "id": "0abc6", + "key": "0abc6", "params": { "labwareId": "e24818a0-0042-11ec-8258-f7ffdf5ad45a", "location": { "slotName": "2" } @@ -4291,7 +4291,7 @@ } }, { - "id": "0", + "key": "0", "commandType": "pickUpTip", "params": { "pipetteId": "50d23e00-0042-11ec-8258-f7ffdf5ad45a", @@ -4300,7 +4300,7 @@ } }, { - "id": "1", + "key": "1", "commandType": "aspirate", "params": { "pipetteId": "50d23e00-0042-11ec-8258-f7ffdf5ad45a", @@ -4315,7 +4315,7 @@ } }, { - "id": "2", + "key": "2", "commandType": "dispense", "params": { "pipetteId": "50d23e00-0042-11ec-8258-f7ffdf5ad45a", @@ -4330,7 +4330,7 @@ } }, { - "id": "3", + "key": "3", "commandType": "dropTip", "params": { "pipetteId": "50d23e00-0042-11ec-8258-f7ffdf5ad45a", @@ -4339,7 +4339,7 @@ } }, { - "id": "4", + "key": "4", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4348,7 +4348,7 @@ } }, { - "id": "5", + "key": "5", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4363,7 +4363,7 @@ } }, { - "id": "6", + "key": "6", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4378,7 +4378,7 @@ } }, { - "id": "7", + "key": "7", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4387,7 +4387,7 @@ } }, { - "id": "8", + "key": "8", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4396,7 +4396,7 @@ } }, { - "id": "9", + "key": "9", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4411,7 +4411,7 @@ } }, { - "id": "10", + "key": "10", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4426,7 +4426,7 @@ } }, { - "id": "11", + "key": "11", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4435,7 +4435,7 @@ } }, { - "id": "12", + "key": "12", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4444,7 +4444,7 @@ } }, { - "id": "13", + "key": "13", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4459,7 +4459,7 @@ } }, { - "id": "14", + "key": "14", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4474,7 +4474,7 @@ } }, { - "id": "15", + "key": "15", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4483,7 +4483,7 @@ } }, { - "id": "16", + "key": "16", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4492,7 +4492,7 @@ } }, { - "id": "17", + "key": "17", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4507,7 +4507,7 @@ } }, { - "id": "18", + "key": "18", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4522,7 +4522,7 @@ } }, { - "id": "19", + "key": "19", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4531,7 +4531,7 @@ } }, { - "id": "20", + "key": "20", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4540,7 +4540,7 @@ } }, { - "id": "21", + "key": "21", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4555,7 +4555,7 @@ } }, { - "id": "22", + "key": "22", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4570,7 +4570,7 @@ } }, { - "id": "23", + "key": "23", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4579,7 +4579,7 @@ } }, { - "id": "24", + "key": "24", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4588,7 +4588,7 @@ } }, { - "id": "25", + "key": "25", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4603,7 +4603,7 @@ } }, { - "id": "26", + "key": "26", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4618,7 +4618,7 @@ } }, { - "id": "27", + "key": "27", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4627,7 +4627,7 @@ } }, { - "id": "28", + "key": "28", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4636,7 +4636,7 @@ } }, { - "id": "29", + "key": "29", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4651,7 +4651,7 @@ } }, { - "id": "30", + "key": "30", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4666,7 +4666,7 @@ } }, { - "id": "31", + "key": "31", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4675,7 +4675,7 @@ } }, { - "id": "32", + "key": "32", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4684,7 +4684,7 @@ } }, { - "id": "33", + "key": "33", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4699,7 +4699,7 @@ } }, { - "id": "34", + "key": "34", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4714,7 +4714,7 @@ } }, { - "id": "35", + "key": "35", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4723,7 +4723,7 @@ } }, { - "id": "36", + "key": "36", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4732,7 +4732,7 @@ } }, { - "id": "37", + "key": "37", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4747,7 +4747,7 @@ } }, { - "id": "38", + "key": "38", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4762,7 +4762,7 @@ } }, { - "id": "39", + "key": "39", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4771,7 +4771,7 @@ } }, { - "id": "40", + "key": "40", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4780,7 +4780,7 @@ } }, { - "id": "41", + "key": "41", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4795,7 +4795,7 @@ } }, { - "id": "42", + "key": "42", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4810,7 +4810,7 @@ } }, { - "id": "43", + "key": "43", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4819,7 +4819,7 @@ } }, { - "id": "44", + "key": "44", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4828,7 +4828,7 @@ } }, { - "id": "45", + "key": "45", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4843,7 +4843,7 @@ } }, { - "id": "46", + "key": "46", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4858,7 +4858,7 @@ } }, { - "id": "47", + "key": "47", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4867,7 +4867,7 @@ } }, { - "id": "48", + "key": "48", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4876,7 +4876,7 @@ } }, { - "id": "49", + "key": "49", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4891,7 +4891,7 @@ } }, { - "id": "50", + "key": "50", "commandType": "moveToWell", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4908,14 +4908,14 @@ } }, { - "id": "51", + "key": "51", "commandType": "delay", "params": { "seconds": 1 } }, { - "id": "52", + "key": "52", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4930,7 +4930,7 @@ } }, { - "id": "53", + "key": "53", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", diff --git a/shared-data/protocol/fixtures/6/multipleTipracksWithTC.json b/shared-data/protocol/fixtures/6/multipleTipracksWithTC.json index 702610d744b..7c54b41b358 100644 --- a/shared-data/protocol/fixtures/6/multipleTipracksWithTC.json +++ b/shared-data/protocol/fixtures/6/multipleTipracksWithTC.json @@ -3012,7 +3012,7 @@ }, "commands": [ { - "id": "0abc1", + "key": "0abc1", "commandType": "loadPipette", "params": { "pipetteId": "50d23e00-0042-11ec-8258-f7ffdf5ad45a", @@ -3023,7 +3023,7 @@ } }, { - "id": "0abc2", + "key": "0abc2", "commandType": "loadPipette", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -3035,7 +3035,7 @@ }, { "commandType": "loadModule", - "id": "00abc2", + "key": "00abc2", "params": { "moduleId": "18f0c1b0-0122-11ec-88a3-f1745cf9b36c:thermocyclerModuleType", "location": { @@ -3047,7 +3047,7 @@ } }, { - "id": "0abc3", + "key": "0abc3", "commandType": "loadLabware", "params": { "labwareId": "fixedTrash", @@ -3103,7 +3103,7 @@ } }, { - "id": "0abc4", + "key": "0abc4", "commandType": "loadLabware", "params": { "labwareId": "50d3ebb0-0042-11ec-8258-f7ffdf5ad45a:opentrons/opentrons_96_tiprack_300ul/1", @@ -4131,7 +4131,7 @@ } }, { - "id": "0abc5", + "key": "0abc5", "commandType": "loadLabware", "params": { "labwareId": "9fbc1db0-0042-11ec-8258-f7ffdf5ad45a:opentrons/nest_12_reservoir_15ml/1", @@ -4330,7 +4330,7 @@ } }, { - "id": "0abc6", + "key": "0abc6", "commandType": "loadLabware", "params": { "labwareId": "e24818a0-0042-11ec-8258-f7ffdf5ad45a", @@ -5358,7 +5358,7 @@ } }, { - "id": "0abc7", + "key": "0abc7", "commandType": "loadLabware", "params": { "labwareId": "1dc0c050-0122-11ec-88a3-f1745cf9b36c:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", @@ -6383,7 +6383,7 @@ } }, { - "id": "7", + "key": "7", "commandType": "pickUpTip", "params": { "pipetteId": "50d23e00-0042-11ec-8258-f7ffdf5ad45a", @@ -6392,7 +6392,7 @@ } }, { - "id": "8", + "key": "8", "commandType": "aspirate", "params": { "pipetteId": "50d23e00-0042-11ec-8258-f7ffdf5ad45a", @@ -6407,7 +6407,7 @@ } }, { - "id": "9", + "key": "9", "commandType": "dispense", "params": { "pipetteId": "50d23e00-0042-11ec-8258-f7ffdf5ad45a", @@ -6422,7 +6422,7 @@ } }, { - "id": "10", + "key": "10", "commandType": "dropTip", "params": { "pipetteId": "50d23e00-0042-11ec-8258-f7ffdf5ad45a", @@ -6431,7 +6431,7 @@ } }, { - "id": "11", + "key": "11", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6440,7 +6440,7 @@ } }, { - "id": "12", + "key": "12", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6455,7 +6455,7 @@ } }, { - "id": "13", + "key": "13", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6470,7 +6470,7 @@ } }, { - "id": "14", + "key": "14", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6479,7 +6479,7 @@ } }, { - "id": "15", + "key": "15", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6488,7 +6488,7 @@ } }, { - "id": "16", + "key": "16", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6503,7 +6503,7 @@ } }, { - "id": "17", + "key": "17", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6518,7 +6518,7 @@ } }, { - "id": "18", + "key": "18", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6527,7 +6527,7 @@ } }, { - "id": "19", + "key": "19", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6536,7 +6536,7 @@ } }, { - "id": "20", + "key": "20", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6551,7 +6551,7 @@ } }, { - "id": "21", + "key": "21", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6566,7 +6566,7 @@ } }, { - "id": "22", + "key": "22", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6575,7 +6575,7 @@ } }, { - "id": "23", + "key": "23", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6584,7 +6584,7 @@ } }, { - "id": "24", + "key": "24", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6599,7 +6599,7 @@ } }, { - "id": "25", + "key": "25", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6614,7 +6614,7 @@ } }, { - "id": "26", + "key": "26", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6623,7 +6623,7 @@ } }, { - "id": "27", + "key": "27", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6632,7 +6632,7 @@ } }, { - "id": "28", + "key": "28", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6647,7 +6647,7 @@ } }, { - "id": "29", + "key": "29", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6662,7 +6662,7 @@ } }, { - "id": "30", + "key": "30", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6671,7 +6671,7 @@ } }, { - "id": "31", + "key": "31", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6680,7 +6680,7 @@ } }, { - "id": "32", + "key": "32", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6695,7 +6695,7 @@ } }, { - "id": "33", + "key": "33", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6710,7 +6710,7 @@ } }, { - "id": "34", + "key": "34", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6719,7 +6719,7 @@ } }, { - "id": "35", + "key": "35", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6728,7 +6728,7 @@ } }, { - "id": "36", + "key": "36", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6743,7 +6743,7 @@ } }, { - "id": "37", + "key": "37", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6758,7 +6758,7 @@ } }, { - "id": "38", + "key": "38", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6767,7 +6767,7 @@ } }, { - "id": "39", + "key": "39", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6776,7 +6776,7 @@ } }, { - "id": "40", + "key": "40", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6791,7 +6791,7 @@ } }, { - "id": "41", + "key": "41", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6806,7 +6806,7 @@ } }, { - "id": "42", + "key": "42", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6815,7 +6815,7 @@ } }, { - "id": "43", + "key": "43", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6824,7 +6824,7 @@ } }, { - "id": "44", + "key": "44", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6839,7 +6839,7 @@ } }, { - "id": "45", + "key": "45", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6854,7 +6854,7 @@ } }, { - "id": "46", + "key": "46", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6863,7 +6863,7 @@ } }, { - "id": "47", + "key": "47", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6872,7 +6872,7 @@ } }, { - "id": "48", + "key": "48", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6887,7 +6887,7 @@ } }, { - "id": "49", + "key": "49", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6902,7 +6902,7 @@ } }, { - "id": "50", + "key": "50", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6911,7 +6911,7 @@ } }, { - "id": "51", + "key": "51", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6920,7 +6920,7 @@ } }, { - "id": "52", + "key": "52", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6935,7 +6935,7 @@ } }, { - "id": "53", + "key": "53", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6950,7 +6950,7 @@ } }, { - "id": "54", + "key": "54", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6959,7 +6959,7 @@ } }, { - "id": "55", + "key": "55", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6968,7 +6968,7 @@ } }, { - "id": "56", + "key": "56", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6983,7 +6983,7 @@ } }, { - "id": "57", + "key": "57", "commandType": "moveToWell", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -7000,14 +7000,14 @@ } }, { - "id": "58", + "key": "58", "commandType": "delay", "params": { "seconds": 1 } }, { - "id": "59", + "key": "59", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -7022,7 +7022,7 @@ } }, { - "id": "60", + "key": "60", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -7031,14 +7031,14 @@ } }, { - "id": "61", + "key": "61", "commandType": "thermocycler/closeLid", "params": { "moduleId": "18f0c1b0-0122-11ec-88a3-f1745cf9b36c:thermocyclerModuleType" } }, { - "id": "62", + "key": "62", "commandType": "thermocycler/setTargetLidTemperature", "params": { "moduleId": "18f0c1b0-0122-11ec-88a3-f1745cf9b36c:thermocyclerModuleType", @@ -7046,7 +7046,7 @@ } }, { - "id": "63", + "key": "63", "commandType": "thermocycler/waitForLidTemperature", "params": { "moduleId": "18f0c1b0-0122-11ec-88a3-f1745cf9b36c:thermocyclerModuleType", diff --git a/shared-data/protocol/fixtures/6/oneTiprack.json b/shared-data/protocol/fixtures/6/oneTiprack.json index d61e5c9e108..7dd58e7a064 100644 --- a/shared-data/protocol/fixtures/6/oneTiprack.json +++ b/shared-data/protocol/fixtures/6/oneTiprack.json @@ -1224,7 +1224,7 @@ }, "commands": [ { - "id": "0abc1", + "key": "0abc1", "commandType": "loadPipette", "params": { "pipetteId": "pipetteId", @@ -1232,7 +1232,7 @@ } }, { - "id": "0abc2", + "key": "0abc2", "commandType": "loadLabware", "params": { "labwareId": "fixedTrash", @@ -1242,7 +1242,7 @@ } }, { - "id": "0abc3", + "key": "0abc3", "commandType": "loadLabware", "params": { "labwareId": "tiprackId", @@ -1252,7 +1252,7 @@ } }, { - "id": "0abc4", + "key": "0abc4", "commandType": "loadLabware", "params": { "labwareId": "sourcePlateId", @@ -1262,7 +1262,7 @@ } }, { - "id": "0abc4", + "key": "0abc4", "commandType": "loadLabware", "params": { "labwareId": "destPlateId", @@ -1272,7 +1272,7 @@ } }, { - "id": "0", + "key": "0", "commandType": "pickUpTip", "params": { "pipetteId": "pipetteId", @@ -1281,7 +1281,7 @@ } }, { - "id": "1", + "key": "1", "commandType": "aspirate", "params": { "pipetteId": "pipetteId", @@ -1296,14 +1296,14 @@ } }, { - "id": "2", + "key": "2", "commandType": "delay", "params": { "seconds": 42 } }, { - "id": "3", + "key": "3", "commandType": "dispense", "params": { "pipetteId": "pipetteId", @@ -1318,7 +1318,7 @@ } }, { - "id": "4", + "key": "4", "commandType": "touchTip", "params": { "pipetteId": "pipetteId", @@ -1331,7 +1331,7 @@ } }, { - "id": "5", + "key": "5", "commandType": "blowout", "params": { "pipetteId": "pipetteId", @@ -1345,7 +1345,7 @@ } }, { - "id": "7", + "key": "7", "commandType": "moveToWell", "params": { "pipetteId": "pipetteId", @@ -1357,7 +1357,7 @@ } }, { - "id": "8", + "key": "8", "commandType": "moveToWell", "params": { "pipetteId": "pipetteId", @@ -1376,7 +1376,7 @@ } }, { - "id": "9", + "key": "9", "commandType": "dropTip", "params": { "pipetteId": "pipetteId", diff --git a/shared-data/protocol/fixtures/6/simpleV6.json b/shared-data/protocol/fixtures/6/simpleV6.json index 6349396dcf6..65745a1decb 100644 --- a/shared-data/protocol/fixtures/6/simpleV6.json +++ b/shared-data/protocol/fixtures/6/simpleV6.json @@ -1239,7 +1239,7 @@ "commands": [ { "commandType": "loadPipette", - "id": "0abc123", + "key": "0abc123", "params": { "pipetteId": "pipetteId", "mount": "left" @@ -1247,7 +1247,7 @@ }, { "commandType": "loadModule", - "id": "1abc123", + "key": "1abc123", "params": { "moduleId": "magneticModuleId", "location": { "slotName": "3" } @@ -1255,7 +1255,7 @@ }, { "commandType": "loadModule", - "id": "2abc123", + "key": "2abc123", "params": { "moduleId": "temperatureModuleId", "location": { "slotName": "1" } @@ -1263,7 +1263,7 @@ }, { "commandType": "loadLabware", - "id": "3abc123", + "key": "3abc123", "params": { "labwareId": "sourcePlateId", "location": { @@ -1273,7 +1273,7 @@ }, { "commandType": "loadLabware", - "id": "4abc123", + "key": "4abc123", "params": { "labwareId": "destPlateId", "location": { @@ -1283,7 +1283,7 @@ }, { "commandType": "loadLabware", - "id": "5abc123", + "key": "5abc123", "params": { "labwareId": "tipRackId", "location": { "slotName": "8" } @@ -1291,7 +1291,7 @@ }, { "commandType": "loadLiquid", - "id": "7abc123", + "key": "7abc123", "params": { "liquidId": "waterId", "labwareId": "sourcePlateId", @@ -1303,12 +1303,12 @@ }, { "commandType": "home", - "id": "00abc123", + "key": "00abc123", "params": {} }, { "commandType": "pickUpTip", - "id": "8abc123", + "key": "8abc123", "params": { "pipetteId": "pipetteId", "labwareId": "tipRackId", @@ -1317,7 +1317,7 @@ }, { "commandType": "aspirate", - "id": "9abc123", + "key": "9abc123", "params": { "pipetteId": "pipetteId", "labwareId": "sourcePlateId", @@ -1332,14 +1332,14 @@ }, { "commandType": "delay", - "id": "10abc123", + "key": "10abc123", "params": { "seconds": 42 } }, { "commandType": "dispense", - "id": "11abc123", + "key": "11abc123", "params": { "pipetteId": "pipetteId", "labwareId": "destPlateId", @@ -1354,7 +1354,7 @@ }, { "commandType": "touchTip", - "id": "12abc123", + "key": "12abc123", "params": { "pipetteId": "pipetteId", "labwareId": "destPlateId", @@ -1369,7 +1369,7 @@ }, { "commandType": "blowout", - "id": "13abc123", + "key": "13abc123", "params": { "pipetteId": "pipetteId", "labwareId": "destPlateId", @@ -1383,7 +1383,7 @@ }, { "commandType": "moveToWell", - "id": "15abc123", + "key": "15abc123", "params": { "pipetteId": "pipetteId", "labwareId": "destPlateId", @@ -1392,7 +1392,7 @@ }, { "commandType": "moveToWell", - "id": "16abc123", + "key": "16abc123", "params": { "pipetteId": "pipetteId", "labwareId": "destPlateId", @@ -1408,7 +1408,7 @@ }, { "commandType": "dropTip", - "id": "17abc123", + "key": "17abc123", "params": { "pipetteId": "pipetteId", "labwareId": "fixedTrash", @@ -1417,7 +1417,7 @@ }, { "commandType": "waitForResume", - "id": "18abc123", + "key": "18abc123", "params": { "message": "pause command" } @@ -1425,7 +1425,7 @@ { "commandType": "moveToCoordinates", - "id": "16abc123", + "key": "16abc123", "params": { "pipetteId": "pipetteId", "coordinates": { "x": 0, "y": 0, "z": 0 }, @@ -1436,7 +1436,7 @@ }, { "commandType": "moveRelative", - "id": "18abc123", + "key": "18abc123", "params": { "pipetteId": "pipetteId", "axis": "x", @@ -1445,7 +1445,7 @@ }, { "commandType": "moveRelative", - "id": "19abc123", + "key": "19abc123", "params": { "pipetteId": "pipetteId", "axis": "y", @@ -1454,14 +1454,14 @@ }, { "commandType": "savePosition", - "id": "21abc123", + "key": "21abc123", "params": { "pipetteId": "pipetteId" } }, { "commandType": "moveRelative", - "id": "20abc123", + "key": "20abc123", "params": { "pipetteId": "pipetteId", "axis": "z", @@ -1470,7 +1470,7 @@ }, { "commandType": "savePosition", - "id": "21abc123", + "key": "21abc123", "params": { "pipetteId": "pipetteId", "positionId": "positionId" diff --git a/shared-data/protocol/fixtures/6/tempAndMagModuleCommands.json b/shared-data/protocol/fixtures/6/tempAndMagModuleCommands.json index ad3031beaa4..167d9aa08da 100644 --- a/shared-data/protocol/fixtures/6/tempAndMagModuleCommands.json +++ b/shared-data/protocol/fixtures/6/tempAndMagModuleCommands.json @@ -1238,7 +1238,7 @@ "commands": [ { "commandType": "loadPipette", - "id": "0abc123", + "key": "0abc123", "params": { "pipetteId": "pipetteId", "mount": "left" @@ -1246,7 +1246,7 @@ }, { "commandType": "loadModule", - "id": "1abc123", + "key": "1abc123", "params": { "moduleId": "magneticModuleId", "location": { "slotName": "3" } @@ -1254,7 +1254,7 @@ }, { "commandType": "loadModule", - "id": "2abc123", + "key": "2abc123", "params": { "moduleId": "temperatureModuleId", "location": { "slotName": "1" } @@ -1262,7 +1262,7 @@ }, { "commandType": "loadLabware", - "id": "3abc123", + "key": "3abc123", "params": { "labwareId": "sourcePlateId", "location": { @@ -1272,7 +1272,7 @@ }, { "commandType": "loadLabware", - "id": "4abc123", + "key": "4abc123", "params": { "labwareId": "destPlateId", "location": { @@ -1282,7 +1282,7 @@ }, { "commandType": "loadLabware", - "id": "5abc123", + "key": "5abc123", "params": { "labwareId": "tipRackId", "location": { "slotName": "8" } @@ -1290,7 +1290,7 @@ }, { "commandType": "loadLabware", - "id": "6abc123", + "key": "6abc123", "params": { "labwareId": "fixedTrash", "location": { @@ -1300,7 +1300,7 @@ }, { "commandType": "loadLiquid", - "id": "7abc123", + "key": "7abc123", "params": { "liquidId": "waterId", "labwareId": "sourcePlateId", @@ -1313,7 +1313,7 @@ { "commandType": "pickUpTip", - "id": "8abc123", + "key": "8abc123", "params": { "pipetteId": "pipetteId", "labwareId": "tipRackId", @@ -1322,7 +1322,7 @@ }, { "commandType": "aspirate", - "id": "9abc123", + "key": "9abc123", "params": { "pipetteId": "pipetteId", "labwareId": "sourcePlateId", @@ -1337,14 +1337,14 @@ }, { "commandType": "delay", - "id": "10abc123", + "key": "10abc123", "params": { "seconds": 42 } }, { "commandType": "dispense", - "id": "11abc123", + "key": "11abc123", "params": { "pipetteId": "pipetteId", "labwareId": "destPlateId", @@ -1359,7 +1359,7 @@ }, { "commandType": "temperatureModule/setTargetTemperature", - "id": "12abc123", + "key": "12abc123", "params": { "moduleId": "temperatureModuleId", "celsius": 80 @@ -1367,7 +1367,7 @@ }, { "commandType": "temperatureModule/waitForTemperature", - "id": "13abc123", + "key": "13abc123", "params": { "moduleId": "temperatureModuleId", "celsius": 80 @@ -1375,14 +1375,14 @@ }, { "commandType": "temperatureModule/deactivate", - "id": "14abc123", + "key": "14abc123", "params": { "moduleId": "temperatureModuleId" } }, { "commandType": "magneticModule/engage", - "id": "15abc123", + "key": "15abc123", "params": { "moduleId": "magneticModuleId", "height": 10 @@ -1390,7 +1390,7 @@ }, { "commandType": "magneticModule/disengage", - "id": "16abc123", + "key": "16abc123", "params": { "moduleId": "magneticModuleId" } diff --git a/shared-data/protocol/fixtures/6/transferSettings.json b/shared-data/protocol/fixtures/6/transferSettings.json index 2f316d324d8..28c12fa9700 100644 --- a/shared-data/protocol/fixtures/6/transferSettings.json +++ b/shared-data/protocol/fixtures/6/transferSettings.json @@ -4561,7 +4561,7 @@ }, "commands": [ { - "id": "0", + "key": "0", "commandType": "loadPipette", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -4569,7 +4569,7 @@ } }, { - "id": "1", + "key": "1", "commandType": "loadPipette", "params": { "pipetteId": "4da579b0-a9bf-11eb-bce6-9f1d5b9c1a1b", @@ -4577,7 +4577,7 @@ } }, { - "id": "2", + "key": "2", "commandType": "loadModule", "params": { "moduleId": "3e012450-3412-11eb-ad93-ed232a2337cf:magneticModuleType", @@ -4590,7 +4590,7 @@ } }, { - "id": "3", + "key": "3", "commandType": "loadModule", "params": { "moduleId": "3e0283e0-3412-11eb-ad93-ed232a2337cf:temperatureModuleType", @@ -4603,7 +4603,7 @@ } }, { - "id": "4", + "key": "4", "commandType": "loadModule", "params": { "moduleId": "3e039550-3412-11eb-ad93-ed232a2337cf:thermocyclerModuleType", @@ -4616,7 +4616,7 @@ } }, { - "id": "5", + "key": "5", "commandType": "loadLabware", "params": { "labwareId": "fixedTrash", @@ -4683,7 +4683,7 @@ } }, { - "id": "6", + "key": "6", "commandType": "loadLabware", "params": { "labwareId": "3e047fb0-3412-11eb-ad93-ed232a2337cf:opentrons/opentrons_96_tiprack_1000ul/1", @@ -5715,7 +5715,7 @@ } }, { - "id": "7", + "key": "7", "commandType": "loadLabware", "params": { "labwareId": "5ae317e0-3412-11eb-ad93-ed232a2337cf:opentrons/nest_1_reservoir_195ml/1", @@ -5784,7 +5784,7 @@ } }, { - "id": "8", + "key": "8", "commandType": "loadLabware", "params": { "labwareId": "aac5d680-3412-11eb-ad93-ed232a2337cf:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", @@ -6815,7 +6815,7 @@ } }, { - "id": "9", + "key": "9", "commandType": "loadLabware", "params": { "labwareId": "60e8b050-3412-11eb-ad93-ed232a2337cf:opentrons/corning_24_wellplate_3.4ml_flat/1", @@ -7121,7 +7121,7 @@ } }, { - "id": "10", + "key": "10", "commandType": "loadLabware", "params": { "labwareId": "ada13110-3412-11eb-ad93-ed232a2337cf:opentrons/opentrons_96_aluminumblock_generic_pcr_strip_200ul/1", @@ -8160,7 +8160,7 @@ } }, { - "id": "11", + "key": "11", "commandType": "loadLabware", "params": { "labwareId": "b0103540-3412-11eb-ad93-ed232a2337cf:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", @@ -9191,7 +9191,7 @@ } }, { - "id": "12", + "key": "12", "commandType": "loadLabware", "params": { "labwareId": "faa13a50-a9bf-11eb-bce6-9f1d5b9c1a1b:opentrons/opentrons_96_tiprack_20ul/1", @@ -10223,7 +10223,7 @@ } }, { - "id": "13", + "key": "13", "commandType": "loadLabware", "params": { "labwareId": "53d3b350-a9c0-11eb-bce6-9f1d5b9c1a1b", @@ -10529,7 +10529,7 @@ } }, { - "id": "14", + "key": "14", "commandType": "pickUpTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10538,7 +10538,7 @@ } }, { - "id": "15", + "key": "15", "commandType": "aspirate", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10553,7 +10553,7 @@ } }, { - "id": "16", + "key": "16", "commandType": "dispense", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10568,7 +10568,7 @@ } }, { - "id": "17", + "key": "17", "commandType": "dropTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10577,7 +10577,7 @@ } }, { - "id": "18", + "key": "18", "commandType": "pickUpTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10586,7 +10586,7 @@ } }, { - "id": "19", + "key": "19", "commandType": "aspirate", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10601,7 +10601,7 @@ } }, { - "id": "20", + "key": "20", "commandType": "dispense", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10616,7 +10616,7 @@ } }, { - "id": "21", + "key": "21", "commandType": "dropTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10625,7 +10625,7 @@ } }, { - "id": "22", + "key": "22", "commandType": "pickUpTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10634,7 +10634,7 @@ } }, { - "id": "23", + "key": "23", "commandType": "aspirate", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10649,7 +10649,7 @@ } }, { - "id": "24", + "key": "24", "commandType": "dispense", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10664,7 +10664,7 @@ } }, { - "id": "25", + "key": "25", "commandType": "dropTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10673,7 +10673,7 @@ } }, { - "id": "26", + "key": "26", "commandType": "pickUpTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10682,7 +10682,7 @@ } }, { - "id": "27", + "key": "27", "commandType": "aspirate", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10697,7 +10697,7 @@ } }, { - "id": "28", + "key": "28", "commandType": "dispense", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10712,7 +10712,7 @@ } }, { - "id": "29", + "key": "29", "commandType": "dropTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10721,7 +10721,7 @@ } }, { - "id": "30", + "key": "30", "commandType": "pickUpTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10730,7 +10730,7 @@ } }, { - "id": "31", + "key": "31", "commandType": "aspirate", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10745,7 +10745,7 @@ } }, { - "id": "32", + "key": "32", "commandType": "dispense", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10760,7 +10760,7 @@ } }, { - "id": "33", + "key": "33", "commandType": "dropTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10769,7 +10769,7 @@ } }, { - "id": "34", + "key": "34", "commandType": "pickUpTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10778,7 +10778,7 @@ } }, { - "id": "35", + "key": "35", "commandType": "aspirate", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10793,7 +10793,7 @@ } }, { - "id": "36", + "key": "36", "commandType": "dispense", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10808,7 +10808,7 @@ } }, { - "id": "37", + "key": "37", "commandType": "dropTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10817,7 +10817,7 @@ } }, { - "id": "38", + "key": "38", "commandType": "pickUpTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10826,7 +10826,7 @@ } }, { - "id": "39", + "key": "39", "commandType": "aspirate", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10841,7 +10841,7 @@ } }, { - "id": "40", + "key": "40", "commandType": "dispense", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10856,7 +10856,7 @@ } }, { - "id": "41", + "key": "41", "commandType": "dropTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10865,7 +10865,7 @@ } }, { - "id": "42", + "key": "42", "commandType": "pickUpTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10874,7 +10874,7 @@ } }, { - "id": "43", + "key": "43", "commandType": "aspirate", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10889,7 +10889,7 @@ } }, { - "id": "44", + "key": "44", "commandType": "dispense", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10904,7 +10904,7 @@ } }, { - "id": "45", + "key": "45", "commandType": "dropTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10913,7 +10913,7 @@ } }, { - "id": "46", + "key": "46", "commandType": "pickUpTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10922,7 +10922,7 @@ } }, { - "id": "47", + "key": "47", "commandType": "aspirate", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10937,7 +10937,7 @@ } }, { - "id": "48", + "key": "48", "commandType": "dispense", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10952,7 +10952,7 @@ } }, { - "id": "49", + "key": "49", "commandType": "dropTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10961,7 +10961,7 @@ } }, { - "id": "50", + "key": "50", "commandType": "pickUpTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10970,7 +10970,7 @@ } }, { - "id": "51", + "key": "51", "commandType": "aspirate", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10985,7 +10985,7 @@ } }, { - "id": "52", + "key": "52", "commandType": "dispense", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -11000,7 +11000,7 @@ } }, { - "id": "53", + "key": "53", "commandType": "dropTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -11009,7 +11009,7 @@ } }, { - "id": "54", + "key": "54", "commandType": "pickUpTip", "params": { "pipetteId": "4da579b0-a9bf-11eb-bce6-9f1d5b9c1a1b", @@ -11018,7 +11018,7 @@ } }, { - "id": "55", + "key": "55", "commandType": "aspirate", "params": { "pipetteId": "4da579b0-a9bf-11eb-bce6-9f1d5b9c1a1b", @@ -11033,7 +11033,7 @@ } }, { - "id": "56", + "key": "56", "commandType": "dispense", "params": { "pipetteId": "4da579b0-a9bf-11eb-bce6-9f1d5b9c1a1b", @@ -11048,7 +11048,7 @@ } }, { - "id": "57", + "key": "57", "commandType": "dropTip", "params": { "pipetteId": "4da579b0-a9bf-11eb-bce6-9f1d5b9c1a1b", diff --git a/shared-data/protocol/fixtures/7/simpleV7.json b/shared-data/protocol/fixtures/7/simpleV7.json index 5ba560f83ca..aa0b78f9915 100644 --- a/shared-data/protocol/fixtures/7/simpleV7.json +++ b/shared-data/protocol/fixtures/7/simpleV7.json @@ -1208,7 +1208,7 @@ "commands": [ { "commandType": "loadPipette", - "id": "0abc123", + "key": "0abc123", "params": { "pipetteId": "pipetteId", "pipetteName": "p1000_96", @@ -1217,7 +1217,7 @@ }, { "commandType": "loadModule", - "id": "1abc123", + "key": "1abc123", "params": { "moduleId": "magneticModuleId", "model": "magneticModuleV2", @@ -1226,7 +1226,7 @@ }, { "commandType": "loadModule", - "id": "2abc123", + "key": "2abc123", "params": { "moduleId": "temperatureModuleId", "model": "temperatureModuleV2", @@ -1235,7 +1235,7 @@ }, { "commandType": "loadLabware", - "id": "3abc123", + "key": "3abc123", "params": { "labwareId": "sourcePlateId", "loadName": "armadillo_96_wellplate_200ul_pcr_full_skirt", @@ -1249,7 +1249,7 @@ }, { "commandType": "loadLabware", - "id": "4abc123", + "key": "4abc123", "params": { "labwareId": "destPlateId", "loadName": "armadillo_96_wellplate_200ul_pcr_full_skirt", @@ -1263,7 +1263,7 @@ }, { "commandType": "loadLabware", - "id": "5abc123", + "key": "5abc123", "params": { "labwareId": "tipRackId", "location": { "slotName": "8" }, @@ -1275,7 +1275,7 @@ }, { "commandType": "loadLabware", - "id": "6abc123", + "key": "6abc123", "params": { "labwareId": "fixedTrash", "location": { @@ -1289,7 +1289,7 @@ }, { "commandType": "loadLiquid", - "id": "7abc123", + "key": "7abc123", "params": { "liquidId": "waterId", "labwareId": "sourcePlateId", @@ -1301,12 +1301,12 @@ }, { "commandType": "home", - "id": "00abc123", + "key": "00abc123", "params": {} }, { "commandType": "pickUpTip", - "id": "8abc123", + "key": "8abc123", "params": { "pipetteId": "pipetteId", "labwareId": "tipRackId", @@ -1315,7 +1315,7 @@ }, { "commandType": "aspirate", - "id": "9abc123", + "key": "9abc123", "params": { "pipetteId": "pipetteId", "labwareId": "sourcePlateId", @@ -1330,14 +1330,14 @@ }, { "commandType": "waitForDuration", - "id": "10abc123", + "key": "10abc123", "params": { "seconds": 42 } }, { "commandType": "dispense", - "id": "11abc123", + "key": "11abc123", "params": { "pipetteId": "pipetteId", "labwareId": "destPlateId", @@ -1352,7 +1352,7 @@ }, { "commandType": "touchTip", - "id": "12abc123", + "key": "12abc123", "params": { "pipetteId": "pipetteId", "labwareId": "destPlateId", @@ -1365,7 +1365,7 @@ }, { "commandType": "blowout", - "id": "13abc123", + "key": "13abc123", "params": { "pipetteId": "pipetteId", "labwareId": "destPlateId", @@ -1379,7 +1379,7 @@ }, { "commandType": "moveToCoordinates", - "id": "14abc123", + "key": "14abc123", "params": { "pipetteId": "pipetteId", "coordinates": { "x": 100, "y": 100, "z": 100 } @@ -1387,7 +1387,7 @@ }, { "commandType": "moveToWell", - "id": "15abc123", + "key": "15abc123", "params": { "pipetteId": "pipetteId", "labwareId": "destPlateId", @@ -1396,7 +1396,7 @@ }, { "commandType": "moveToWell", - "id": "16abc123", + "key": "16abc123", "params": { "pipetteId": "pipetteId", "labwareId": "destPlateId", @@ -1411,7 +1411,7 @@ }, { "commandType": "dropTip", - "id": "17abc123", + "key": "17abc123", "params": { "pipetteId": "pipetteId", "labwareId": "fixedTrash", @@ -1420,7 +1420,7 @@ }, { "commandType": "waitForResume", - "id": "18abc123", + "key": "18abc123", "params": { "message": "pause command" } @@ -1428,7 +1428,7 @@ { "commandType": "moveToCoordinates", - "id": "16abc123", + "key": "16abc123", "params": { "pipetteId": "pipetteId", "coordinates": { "x": 0, "y": 0, "z": 0 }, @@ -1438,7 +1438,7 @@ }, { "commandType": "moveRelative", - "id": "18abc123", + "key": "18abc123", "params": { "pipetteId": "pipetteId", "axis": "x", @@ -1447,7 +1447,7 @@ }, { "commandType": "moveRelative", - "id": "19abc123", + "key": "19abc123", "params": { "pipetteId": "pipetteId", "axis": "y", @@ -1456,14 +1456,14 @@ }, { "commandType": "savePosition", - "id": "21abc123", + "key": "21abc123", "params": { "pipetteId": "pipetteId" } }, { "commandType": "moveRelative", - "id": "20abc123", + "key": "20abc123", "params": { "pipetteId": "pipetteId", "axis": "z", @@ -1472,7 +1472,7 @@ }, { "commandType": "savePosition", - "id": "21abc123", + "key": "21abc123", "params": { "pipetteId": "pipetteId", "positionId": "positionId" diff --git a/shared-data/python/Pipfile b/shared-data/python/Pipfile index 0d11a1d68c9..70cc1cc48c7 100644 --- a/shared-data/python/Pipfile +++ b/shared-data/python/Pipfile @@ -26,5 +26,5 @@ pytest-clarity = "~=1.0.0" [packages] opentrons-shared-data = { editable = true, path = "." } -jsonschema = "==4.21.1" +jsonschema = "==4.17.3" pydantic = "==1.10.12" diff --git a/shared-data/python/Pipfile.lock b/shared-data/python/Pipfile.lock index a125943127f..957723991c1 100644 --- a/shared-data/python/Pipfile.lock +++ b/shared-data/python/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "9b5174a247c5fe717a5db26f523afd532a6b0fc27943d86f6588839785ef51f4" + "sha256": "861a8ab74e5057cb08962b9e5753828739acf1aaf11f9c883d9042281f201880" }, "pipfile-spec": 6, "requires": {}, @@ -24,24 +24,16 @@ }, "jsonschema": { "hashes": [ - "sha256:7996507afae316306f9e2290407761157c6f78002dcf7419acb99822143d1c6f", - "sha256:85727c00279f5fa6bedbe6238d2aa6403bedd8b4864ab11207d07df3cc1b2ee5" + "sha256:0f864437ab8b6076ba6707453ef8f98a6a0d512a80e93f8abdb676f737ecb60d", + "sha256:a870ad254da1a8ca84b6a2905cac29d265f805acc57af304784962a2aa6508f6" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==4.21.1" - }, - "jsonschema-specifications": { - "hashes": [ - "sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc", - "sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c" - ], - "markers": "python_version >= '3.8'", - "version": "==2023.12.1" + "markers": "python_version >= '3.7'", + "version": "==4.17.3" }, "opentrons-shared-data": { "editable": true, - "markers": "python_version >= '3.7'", + "markers": "python_version >= '3.10'", "path": "." }, "pydantic": { @@ -87,126 +79,51 @@ "markers": "python_version >= '3.7'", "version": "==1.10.12" }, - "referencing": { - "hashes": [ - "sha256:3c57da0513e9563eb7e203ebe9bb3a1b509b042016433bd1e45a2853466c3dd3", - "sha256:7e4dc12271d8e15612bfe35792f5ea1c40970dadf8624602e33db2758f7ee554" - ], - "markers": "python_version >= '3.8'", - "version": "==0.32.1" - }, - "rpds-py": { - "hashes": [ - "sha256:01f58a7306b64e0a4fe042047dd2b7d411ee82e54240284bab63e325762c1147", - "sha256:0210b2668f24c078307260bf88bdac9d6f1093635df5123789bfee4d8d7fc8e7", - "sha256:02866e060219514940342a1f84303a1ef7a1dad0ac311792fbbe19b521b489d2", - "sha256:0387ce69ba06e43df54e43968090f3626e231e4bc9150e4c3246947567695f68", - "sha256:060f412230d5f19fc8c8b75f315931b408d8ebf56aec33ef4168d1b9e54200b1", - "sha256:071bc28c589b86bc6351a339114fb7a029f5cddbaca34103aa573eba7b482382", - "sha256:0bfb09bf41fe7c51413f563373e5f537eaa653d7adc4830399d4e9bdc199959d", - "sha256:10162fe3f5f47c37ebf6d8ff5a2368508fe22007e3077bf25b9c7d803454d921", - "sha256:149c5cd24f729e3567b56e1795f74577aa3126c14c11e457bec1b1c90d212e38", - "sha256:1701fc54460ae2e5efc1dd6350eafd7a760f516df8dbe51d4a1c79d69472fbd4", - "sha256:1957a2ab607f9added64478a6982742eb29f109d89d065fa44e01691a20fc20a", - "sha256:1a746a6d49665058a5896000e8d9d2f1a6acba8a03b389c1e4c06e11e0b7f40d", - "sha256:1bfcad3109c1e5ba3cbe2f421614e70439f72897515a96c462ea657261b96518", - "sha256:1d36b2b59e8cc6e576f8f7b671e32f2ff43153f0ad6d0201250a7c07f25d570e", - "sha256:1db228102ab9d1ff4c64148c96320d0be7044fa28bd865a9ce628ce98da5973d", - "sha256:1dc29db3900cb1bb40353772417800f29c3d078dbc8024fd64655a04ee3c4bdf", - "sha256:1e626b365293a2142a62b9a614e1f8e331b28f3ca57b9f05ebbf4cf2a0f0bdc5", - "sha256:1f3c3461ebb4c4f1bbc70b15d20b565759f97a5aaf13af811fcefc892e9197ba", - "sha256:20de7b7179e2031a04042e85dc463a93a82bc177eeba5ddd13ff746325558aa6", - "sha256:24e4900a6643f87058a27320f81336d527ccfe503984528edde4bb660c8c8d59", - "sha256:2528ff96d09f12e638695f3a2e0c609c7b84c6df7c5ae9bfeb9252b6fa686253", - "sha256:25f071737dae674ca8937a73d0f43f5a52e92c2d178330b4c0bb6ab05586ffa6", - "sha256:270987bc22e7e5a962b1094953ae901395e8c1e1e83ad016c5cfcfff75a15a3f", - "sha256:292f7344a3301802e7c25c53792fae7d1593cb0e50964e7bcdcc5cf533d634e3", - "sha256:2953937f83820376b5979318840f3ee47477d94c17b940fe31d9458d79ae7eea", - "sha256:2a792b2e1d3038daa83fa474d559acfd6dc1e3650ee93b2662ddc17dbff20ad1", - "sha256:2a7b2f2f56a16a6d62e55354dd329d929560442bd92e87397b7a9586a32e3e76", - "sha256:2f4eb548daf4836e3b2c662033bfbfc551db58d30fd8fe660314f86bf8510b93", - "sha256:3664d126d3388a887db44c2e293f87d500c4184ec43d5d14d2d2babdb4c64cad", - "sha256:3677fcca7fb728c86a78660c7fb1b07b69b281964673f486ae72860e13f512ad", - "sha256:380e0df2e9d5d5d339803cfc6d183a5442ad7ab3c63c2a0982e8c824566c5ccc", - "sha256:3ac732390d529d8469b831949c78085b034bff67f584559340008d0f6041a049", - "sha256:4128980a14ed805e1b91a7ed551250282a8ddf8201a4e9f8f5b7e6225f54170d", - "sha256:4341bd7579611cf50e7b20bb8c2e23512a3dc79de987a1f411cb458ab670eb90", - "sha256:436474f17733c7dca0fbf096d36ae65277e8645039df12a0fa52445ca494729d", - "sha256:4dc889a9d8a34758d0fcc9ac86adb97bab3fb7f0c4d29794357eb147536483fd", - "sha256:4e21b76075c01d65d0f0f34302b5a7457d95721d5e0667aea65e5bb3ab415c25", - "sha256:516fb8c77805159e97a689e2f1c80655c7658f5af601c34ffdb916605598cda2", - "sha256:5576ee2f3a309d2bb403ec292d5958ce03953b0e57a11d224c1f134feaf8c40f", - "sha256:5a024fa96d541fd7edaa0e9d904601c6445e95a729a2900c5aec6555fe921ed6", - "sha256:5d0e8a6434a3fbf77d11448c9c25b2f25244226cfbec1a5159947cac5b8c5fa4", - "sha256:5e7d63ec01fe7c76c2dbb7e972fece45acbb8836e72682bde138e7e039906e2c", - "sha256:60e820ee1004327609b28db8307acc27f5f2e9a0b185b2064c5f23e815f248f8", - "sha256:637b802f3f069a64436d432117a7e58fab414b4e27a7e81049817ae94de45d8d", - "sha256:65dcf105c1943cba45d19207ef51b8bc46d232a381e94dd38719d52d3980015b", - "sha256:698ea95a60c8b16b58be9d854c9f993c639f5c214cf9ba782eca53a8789d6b19", - "sha256:70fcc6c2906cfa5c6a552ba7ae2ce64b6c32f437d8f3f8eea49925b278a61453", - "sha256:720215373a280f78a1814becb1312d4e4d1077b1202a56d2b0815e95ccb99ce9", - "sha256:7450dbd659fed6dd41d1a7d47ed767e893ba402af8ae664c157c255ec6067fde", - "sha256:7b7d9ca34542099b4e185b3c2a2b2eda2e318a7dbde0b0d83357a6d4421b5296", - "sha256:7fbd70cb8b54fe745301921b0816c08b6d917593429dfc437fd024b5ba713c58", - "sha256:81038ff87a4e04c22e1d81f947c6ac46f122e0c80460b9006e6517c4d842a6ec", - "sha256:810685321f4a304b2b55577c915bece4c4a06dfe38f6e62d9cc1d6ca8ee86b99", - "sha256:82ada4a8ed9e82e443fcef87e22a3eed3654dd3adf6e3b3a0deb70f03e86142a", - "sha256:841320e1841bb53fada91c9725e766bb25009cfd4144e92298db296fb6c894fb", - "sha256:8587fd64c2a91c33cdc39d0cebdaf30e79491cc029a37fcd458ba863f8815383", - "sha256:8ffe53e1d8ef2520ebcf0c9fec15bb721da59e8ef283b6ff3079613b1e30513d", - "sha256:9051e3d2af8f55b42061603e29e744724cb5f65b128a491446cc029b3e2ea896", - "sha256:91e5a8200e65aaac342a791272c564dffcf1281abd635d304d6c4e6b495f29dc", - "sha256:93432e747fb07fa567ad9cc7aaadd6e29710e515aabf939dfbed8046041346c6", - "sha256:938eab7323a736533f015e6069a7d53ef2dcc841e4e533b782c2bfb9fb12d84b", - "sha256:9584f8f52010295a4a417221861df9bea4c72d9632562b6e59b3c7b87a1522b7", - "sha256:9737bdaa0ad33d34c0efc718741abaafce62fadae72c8b251df9b0c823c63b22", - "sha256:99da0a4686ada4ed0f778120a0ea8d066de1a0a92ab0d13ae68492a437db78bf", - "sha256:99f567dae93e10be2daaa896e07513dd4bf9c2ecf0576e0533ac36ba3b1d5394", - "sha256:9bdf1303df671179eaf2cb41e8515a07fc78d9d00f111eadbe3e14262f59c3d0", - "sha256:9f0e4dc0f17dcea4ab9d13ac5c666b6b5337042b4d8f27e01b70fae41dd65c57", - "sha256:a000133a90eea274a6f28adc3084643263b1e7c1a5a66eb0a0a7a36aa757ed74", - "sha256:a3264e3e858de4fc601741498215835ff324ff2482fd4e4af61b46512dd7fc83", - "sha256:a71169d505af63bb4d20d23a8fbd4c6ce272e7bce6cc31f617152aa784436f29", - "sha256:a967dd6afda7715d911c25a6ba1517975acd8d1092b2f326718725461a3d33f9", - "sha256:aa5bfb13f1e89151ade0eb812f7b0d7a4d643406caaad65ce1cbabe0a66d695f", - "sha256:ae35e8e6801c5ab071b992cb2da958eee76340e6926ec693b5ff7d6381441745", - "sha256:b686f25377f9c006acbac63f61614416a6317133ab7fafe5de5f7dc8a06d42eb", - "sha256:b760a56e080a826c2e5af09002c1a037382ed21d03134eb6294812dda268c811", - "sha256:b86b21b348f7e5485fae740d845c65a880f5d1eda1e063bc59bef92d1f7d0c55", - "sha256:b9412abdf0ba70faa6e2ee6c0cc62a8defb772e78860cef419865917d86c7342", - "sha256:bd345a13ce06e94c753dab52f8e71e5252aec1e4f8022d24d56decd31e1b9b23", - "sha256:be22ae34d68544df293152b7e50895ba70d2a833ad9566932d750d3625918b82", - "sha256:bf046179d011e6114daf12a534d874958b039342b347348a78b7cdf0dd9d6041", - "sha256:c3d2010656999b63e628a3c694f23020322b4178c450dc478558a2b6ef3cb9bb", - "sha256:c64602e8be701c6cfe42064b71c84ce62ce66ddc6422c15463fd8127db3d8066", - "sha256:d65e6b4f1443048eb7e833c2accb4fa7ee67cc7d54f31b4f0555b474758bee55", - "sha256:d8bbd8e56f3ba25a7d0cf980fc42b34028848a53a0e36c9918550e0280b9d0b6", - "sha256:da1ead63368c04a9bded7904757dfcae01eba0e0f9bc41d3d7f57ebf1c04015a", - "sha256:dbbb95e6fc91ea3102505d111b327004d1c4ce98d56a4a02e82cd451f9f57140", - "sha256:dbc56680ecf585a384fbd93cd42bc82668b77cb525343170a2d86dafaed2a84b", - "sha256:df3b6f45ba4515632c5064e35ca7f31d51d13d1479673185ba8f9fefbbed58b9", - "sha256:dfe07308b311a8293a0d5ef4e61411c5c20f682db6b5e73de6c7c8824272c256", - "sha256:e796051f2070f47230c745d0a77a91088fbee2cc0502e9b796b9c6471983718c", - "sha256:efa767c220d94aa4ac3a6dd3aeb986e9f229eaf5bce92d8b1b3018d06bed3772", - "sha256:f0b8bf5b8db49d8fd40f54772a1dcf262e8be0ad2ab0206b5a2ec109c176c0a4", - "sha256:f175e95a197f6a4059b50757a3dca33b32b61691bdbd22c29e8a8d21d3914cae", - "sha256:f2f3b28b40fddcb6c1f1f6c88c6f3769cd933fa493ceb79da45968a21dccc920", - "sha256:f6c43b6f97209e370124baf2bf40bb1e8edc25311a158867eb1c3a5d449ebc7a", - "sha256:f7f4cb1f173385e8a39c29510dd11a78bf44e360fb75610594973f5ea141028b", - "sha256:fad059a4bd14c45776600d223ec194e77db6c20255578bb5bcdd7c18fd169361", - "sha256:ff1dcb8e8bc2261a088821b2595ef031c91d499a0c1b031c152d43fe0a6ecec8", - "sha256:ffee088ea9b593cc6160518ba9bd319b5475e5f3e578e4552d63818773c6f56a" + "pyrsistent": { + "hashes": [ + "sha256:0724c506cd8b63c69c7f883cc233aac948c1ea946ea95996ad8b1380c25e1d3f", + "sha256:09848306523a3aba463c4b49493a760e7a6ca52e4826aa100ee99d8d39b7ad1e", + "sha256:0f3b1bcaa1f0629c978b355a7c37acd58907390149b7311b5db1b37648eb6958", + "sha256:21cc459636983764e692b9eba7144cdd54fdec23ccdb1e8ba392a63666c60c34", + "sha256:2e14c95c16211d166f59c6611533d0dacce2e25de0f76e4c140fde250997b3ca", + "sha256:2e2c116cc804d9b09ce9814d17df5edf1df0c624aba3b43bc1ad90411487036d", + "sha256:4021a7f963d88ccd15b523787d18ed5e5269ce57aa4037146a2377ff607ae87d", + "sha256:4c48f78f62ab596c679086084d0dd13254ae4f3d6c72a83ffdf5ebdef8f265a4", + "sha256:4f5c2d012671b7391803263419e31b5c7c21e7c95c8760d7fc35602353dee714", + "sha256:58b8f6366e152092194ae68fefe18b9f0b4f89227dfd86a07770c3d86097aebf", + "sha256:59a89bccd615551391f3237e00006a26bcf98a4d18623a19909a2c48b8e986ee", + "sha256:5cdd7ef1ea7a491ae70d826b6cc64868de09a1d5ff9ef8d574250d0940e275b8", + "sha256:6288b3fa6622ad8a91e6eb759cfc48ff3089e7c17fb1d4c59a919769314af224", + "sha256:6d270ec9dd33cdb13f4d62c95c1a5a50e6b7cdd86302b494217137f760495b9d", + "sha256:79ed12ba79935adaac1664fd7e0e585a22caa539dfc9b7c7c6d5ebf91fb89054", + "sha256:7d29c23bdf6e5438c755b941cef867ec2a4a172ceb9f50553b6ed70d50dfd656", + "sha256:8441cf9616d642c475684d6cf2520dd24812e996ba9af15e606df5f6fd9d04a7", + "sha256:881bbea27bbd32d37eb24dd320a5e745a2a5b092a17f6debc1349252fac85423", + "sha256:8c3aba3e01235221e5b229a6c05f585f344734bd1ad42a8ac51493d74722bbce", + "sha256:a14798c3005ec892bbada26485c2eea3b54109cb2533713e355c806891f63c5e", + "sha256:b14decb628fac50db5e02ee5a35a9c0772d20277824cfe845c8a8b717c15daa3", + "sha256:b318ca24db0f0518630e8b6f3831e9cba78f099ed5c1d65ffe3e023003043ba0", + "sha256:c1beb78af5423b879edaf23c5591ff292cf7c33979734c99aa66d5914ead880f", + "sha256:c55acc4733aad6560a7f5f818466631f07efc001fd023f34a6c203f8b6df0f0b", + "sha256:ca52d1ceae015859d16aded12584c59eb3825f7b50c6cfd621d4231a6cc624ce", + "sha256:cae40a9e3ce178415040a0383f00e8d68b569e97f31928a3a8ad37e3fde6df6a", + "sha256:e78d0c7c1e99a4a45c99143900ea0546025e41bb59ebc10182e947cf1ece9174", + "sha256:ef3992833fbd686ee783590639f4b8343a57f1f75de8633749d984dc0eb16c86", + "sha256:f058a615031eea4ef94ead6456f5ec2026c19fb5bd6bfe86e9665c4158cf802f", + "sha256:f5ac696f02b3fc01a710427585c855f65cd9c640e14f52abe52020722bb4906b", + "sha256:f920385a11207dc372a028b3f1e1038bb244b3ec38d448e6d8e43c6b3ba20e98", + "sha256:fed2c3216a605dc9a6ea50c7e84c82906e3684c4e80d2908208f662a6cbf9022" ], "markers": "python_version >= '3.8'", - "version": "==0.17.1" + "version": "==0.20.0" }, "typing-extensions": { "hashes": [ - "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", - "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" + "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", + "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" ], "markers": "python_version >= '3.8'", - "version": "==4.9.0" + "version": "==4.12.2" } }, "develop": { @@ -226,6 +143,14 @@ "markers": "python_version >= '3.7'", "version": "==23.2.0" }, + "backports.tarfile": { + "hashes": [ + "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", + "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991" + ], + "markers": "python_version < '3.12'", + "version": "==1.2.0" + }, "black": { "hashes": [ "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b", @@ -258,11 +183,11 @@ }, "certifi": { "hashes": [ - "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1", - "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474" + "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b", + "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90" ], "markers": "python_version >= '3.6'", - "version": "==2023.11.17" + "version": "==2024.7.4" }, "charset-normalizer": { "hashes": [ @@ -381,85 +306,85 @@ "toml" ], "hashes": [ - "sha256:04387a4a6ecb330c1878907ce0dc04078ea72a869263e53c72a1ba5bbdf380ca", - "sha256:0676cd0ba581e514b7f726495ea75aba3eb20899d824636c6f59b0ed2f88c471", - "sha256:0e8d06778e8fbffccfe96331a3946237f87b1e1d359d7fbe8b06b96c95a5407a", - "sha256:0eb3c2f32dabe3a4aaf6441dde94f35687224dfd7eb2a7f47f3fd9428e421058", - "sha256:109f5985182b6b81fe33323ab4707011875198c41964f014579cf82cebf2bb85", - "sha256:13eaf476ec3e883fe3e5fe3707caeb88268a06284484a3daf8250259ef1ba143", - "sha256:164fdcc3246c69a6526a59b744b62e303039a81e42cfbbdc171c91a8cc2f9446", - "sha256:26776ff6c711d9d835557ee453082025d871e30b3fd6c27fcef14733f67f0590", - "sha256:26f66da8695719ccf90e794ed567a1549bb2644a706b41e9f6eae6816b398c4a", - "sha256:29f3abe810930311c0b5d1a7140f6395369c3db1be68345638c33eec07535105", - "sha256:316543f71025a6565677d84bc4df2114e9b6a615aa39fb165d697dba06a54af9", - "sha256:36b0ea8ab20d6a7564e89cb6135920bc9188fb5f1f7152e94e8300b7b189441a", - "sha256:3cc9d4bc55de8003663ec94c2f215d12d42ceea128da8f0f4036235a119c88ac", - "sha256:485e9f897cf4856a65a57c7f6ea3dc0d4e6c076c87311d4bc003f82cfe199d25", - "sha256:5040148f4ec43644702e7b16ca864c5314ccb8ee0751ef617d49aa0e2d6bf4f2", - "sha256:51456e6fa099a8d9d91497202d9563a320513fcf59f33991b0661a4a6f2ad450", - "sha256:53d7d9158ee03956e0eadac38dfa1ec8068431ef8058fe6447043db1fb40d932", - "sha256:5a10a4920def78bbfff4eff8a05c51be03e42f1c3735be42d851f199144897ba", - "sha256:5b14b4f8760006bfdb6e08667af7bc2d8d9bfdb648351915315ea17645347137", - "sha256:5b2ccb7548a0b65974860a78c9ffe1173cfb5877460e5a229238d985565574ae", - "sha256:697d1317e5290a313ef0d369650cfee1a114abb6021fa239ca12b4849ebbd614", - "sha256:6ae8c9d301207e6856865867d762a4b6fd379c714fcc0607a84b92ee63feff70", - "sha256:707c0f58cb1712b8809ece32b68996ee1e609f71bd14615bd8f87a1293cb610e", - "sha256:74775198b702868ec2d058cb92720a3c5a9177296f75bd97317c787daf711505", - "sha256:756ded44f47f330666843b5781be126ab57bb57c22adbb07d83f6b519783b870", - "sha256:76f03940f9973bfaee8cfba70ac991825611b9aac047e5c80d499a44079ec0bc", - "sha256:79287fd95585ed36e83182794a57a46aeae0b64ca53929d1176db56aacc83451", - "sha256:799c8f873794a08cdf216aa5d0531c6a3747793b70c53f70e98259720a6fe2d7", - "sha256:7d360587e64d006402b7116623cebf9d48893329ef035278969fa3bbf75b697e", - "sha256:80b5ee39b7f0131ebec7968baa9b2309eddb35b8403d1869e08f024efd883566", - "sha256:815ac2d0f3398a14286dc2cea223a6f338109f9ecf39a71160cd1628786bc6f5", - "sha256:83c2dda2666fe32332f8e87481eed056c8b4d163fe18ecc690b02802d36a4d26", - "sha256:846f52f46e212affb5bcf131c952fb4075b55aae6b61adc9856222df89cbe3e2", - "sha256:936d38794044b26c99d3dd004d8af0035ac535b92090f7f2bb5aa9c8e2f5cd42", - "sha256:9864463c1c2f9cb3b5db2cf1ff475eed2f0b4285c2aaf4d357b69959941aa555", - "sha256:995ea5c48c4ebfd898eacb098164b3cc826ba273b3049e4a889658548e321b43", - "sha256:a1526d265743fb49363974b7aa8d5899ff64ee07df47dd8d3e37dcc0818f09ed", - "sha256:a56de34db7b7ff77056a37aedded01b2b98b508227d2d0979d373a9b5d353daa", - "sha256:a7c97726520f784239f6c62506bc70e48d01ae71e9da128259d61ca5e9788516", - "sha256:b8e99f06160602bc64da35158bb76c73522a4010f0649be44a4e167ff8555952", - "sha256:bb1de682da0b824411e00a0d4da5a784ec6496b6850fdf8c865c1d68c0e318dd", - "sha256:bf477c355274a72435ceb140dc42de0dc1e1e0bf6e97195be30487d8eaaf1a09", - "sha256:bf635a52fc1ea401baf88843ae8708591aa4adff875e5c23220de43b1ccf575c", - "sha256:bfd5db349d15c08311702611f3dccbef4b4e2ec148fcc636cf8739519b4a5c0f", - "sha256:c530833afc4707fe48524a44844493f36d8727f04dcce91fb978c414a8556cc6", - "sha256:cc6d65b21c219ec2072c1293c505cf36e4e913a3f936d80028993dd73c7906b1", - "sha256:cd3c1e4cb2ff0083758f09be0f77402e1bdf704adb7f89108007300a6da587d0", - "sha256:cfd2a8b6b0d8e66e944d47cdec2f47c48fef2ba2f2dff5a9a75757f64172857e", - "sha256:d0ca5c71a5a1765a0f8f88022c52b6b8be740e512980362f7fdbb03725a0d6b9", - "sha256:e7defbb9737274023e2d7af02cac77043c86ce88a907c58f42b580a97d5bcca9", - "sha256:e9d1bf53c4c8de58d22e0e956a79a5b37f754ed1ffdbf1a260d9dcfa2d8a325e", - "sha256:ea81d8f9691bb53f4fb4db603203029643caffc82bf998ab5b59ca05560f4c06" + "sha256:0086cd4fc71b7d485ac93ca4239c8f75732c2ae3ba83f6be1c9be59d9e2c6382", + "sha256:01c322ef2bbe15057bc4bf132b525b7e3f7206f071799eb8aa6ad1940bcf5fb1", + "sha256:03cafe82c1b32b770a29fd6de923625ccac3185a54a5e66606da26d105f37dac", + "sha256:044a0985a4f25b335882b0966625270a8d9db3d3409ddc49a4eb00b0ef5e8cee", + "sha256:07ed352205574aad067482e53dd606926afebcb5590653121063fbf4e2175166", + "sha256:0d1b923fc4a40c5832be4f35a5dab0e5ff89cddf83bb4174499e02ea089daf57", + "sha256:0e7b27d04131c46e6894f23a4ae186a6a2207209a05df5b6ad4caee6d54a222c", + "sha256:1fad32ee9b27350687035cb5fdf9145bc9cf0a094a9577d43e909948ebcfa27b", + "sha256:289cc803fa1dc901f84701ac10c9ee873619320f2f9aff38794db4a4a0268d51", + "sha256:3c59105f8d58ce500f348c5b56163a4113a440dad6daa2294b5052a10db866da", + "sha256:46c3d091059ad0b9c59d1034de74a7f36dcfa7f6d3bde782c49deb42438f2450", + "sha256:482855914928c8175735a2a59c8dc5806cf7d8f032e4820d52e845d1f731dca2", + "sha256:49c76cdfa13015c4560702574bad67f0e15ca5a2872c6a125f6327ead2b731dd", + "sha256:4b03741e70fb811d1a9a1d75355cf391f274ed85847f4b78e35459899f57af4d", + "sha256:4bea27c4269234e06f621f3fac3925f56ff34bc14521484b8f66a580aacc2e7d", + "sha256:4d5fae0a22dc86259dee66f2cc6c1d3e490c4a1214d7daa2a93d07491c5c04b6", + "sha256:543ef9179bc55edfd895154a51792b01c017c87af0ebaae092720152e19e42ca", + "sha256:54dece71673b3187c86226c3ca793c5f891f9fc3d8aa183f2e3653da18566169", + "sha256:6379688fb4cfa921ae349c76eb1a9ab26b65f32b03d46bb0eed841fd4cb6afb1", + "sha256:65fa405b837060db569a61ec368b74688f429b32fa47a8929a7a2f9b47183713", + "sha256:6616d1c9bf1e3faea78711ee42a8b972367d82ceae233ec0ac61cc7fec09fa6b", + "sha256:6fe885135c8a479d3e37a7aae61cbd3a0fb2deccb4dda3c25f92a49189f766d6", + "sha256:7221f9ac9dad9492cecab6f676b3eaf9185141539d5c9689d13fd6b0d7de840c", + "sha256:76d5f82213aa78098b9b964ea89de4617e70e0d43e97900c2778a50856dac605", + "sha256:7792f0ab20df8071d669d929c75c97fecfa6bcab82c10ee4adb91c7a54055463", + "sha256:831b476d79408ab6ccfadaaf199906c833f02fdb32c9ab907b1d4aa0713cfa3b", + "sha256:9146579352d7b5f6412735d0f203bbd8d00113a680b66565e205bc605ef81bc6", + "sha256:9cc44bf0315268e253bf563f3560e6c004efe38f76db03a1558274a6e04bf5d5", + "sha256:a73d18625f6a8a1cbb11eadc1d03929f9510f4131879288e3f7922097a429f63", + "sha256:a8659fd33ee9e6ca03950cfdcdf271d645cf681609153f218826dd9805ab585c", + "sha256:a94925102c89247530ae1dab7dc02c690942566f22e189cbd53579b0693c0783", + "sha256:ad4567d6c334c46046d1c4c20024de2a1c3abc626817ae21ae3da600f5779b44", + "sha256:b2e16f4cd2bc4d88ba30ca2d3bbf2f21f00f382cf4e1ce3b1ddc96c634bc48ca", + "sha256:bbdf9a72403110a3bdae77948b8011f644571311c2fb35ee15f0f10a8fc082e8", + "sha256:beb08e8508e53a568811016e59f3234d29c2583f6b6e28572f0954a6b4f7e03d", + "sha256:c4cbe651f3904e28f3a55d6f371203049034b4ddbce65a54527a3f189ca3b390", + "sha256:c7b525ab52ce18c57ae232ba6f7010297a87ced82a2383b1afd238849c1ff933", + "sha256:ca5d79cfdae420a1d52bf177de4bc2289c321d6c961ae321503b2ca59c17ae67", + "sha256:cdab02a0a941af190df8782aafc591ef3ad08824f97850b015c8c6a8b3877b0b", + "sha256:d17c6a415d68cfe1091d3296ba5749d3d8696e42c37fca5d4860c5bf7b729f03", + "sha256:d39bd10f0ae453554798b125d2f39884290c480f56e8a02ba7a6ed552005243b", + "sha256:d4b3cd1ca7cd73d229487fa5caca9e4bc1f0bca96526b922d61053ea751fe791", + "sha256:d50a252b23b9b4dfeefc1f663c568a221092cbaded20a05a11665d0dbec9b8fb", + "sha256:da8549d17489cd52f85a9829d0e1d91059359b3c54a26f28bec2c5d369524807", + "sha256:dcd070b5b585b50e6617e8972f3fbbee786afca71b1936ac06257f7e178f00f6", + "sha256:ddaaa91bfc4477d2871442bbf30a125e8fe6b05da8a0015507bfbf4718228ab2", + "sha256:df423f351b162a702c053d5dddc0fc0ef9a9e27ea3f449781ace5f906b664428", + "sha256:dff044f661f59dace805eedb4a7404c573b6ff0cdba4a524141bc63d7be5c7fd", + "sha256:e7e128f85c0b419907d1f38e616c4f1e9f1d1b37a7949f44df9a73d5da5cd53c", + "sha256:ed8d1d1821ba5fc88d4a4f45387b65de52382fa3ef1f0115a4f7a20cdfab0e94", + "sha256:f2501d60d7497fd55e391f423f965bbe9e650e9ffc3c627d5f0ac516026000b8", + "sha256:f7db0b6ae1f96ae41afe626095149ecd1b212b424626175a6633c2999eaad45b" ], "markers": "python_version >= '3.8'", - "version": "==7.4.0" + "version": "==7.6.0" }, "docutils": { "hashes": [ - "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6", - "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b" + "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", + "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2" ], - "markers": "python_version >= '3.7'", - "version": "==0.20.1" + "markers": "python_version >= '3.9'", + "version": "==0.21.2" }, "exceptiongroup": { "hashes": [ - "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14", - "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68" + "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", + "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc" ], "markers": "python_version < '3.11'", - "version": "==1.2.0" + "version": "==1.2.2" }, "execnet": { "hashes": [ - "sha256:88256416ae766bc9e8895c76a87928c0012183da3cc4fc18016e6f050e025f41", - "sha256:cc59bc4423742fd71ad227122eb0dd44db51efb3dc4095b45ac9a08c770096af" + "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc", + "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3" ], - "markers": "python_version >= '3.7'", - "version": "==2.0.2" + "markers": "python_version >= '3.8'", + "version": "==2.1.1" }, "flake8": { "hashes": [ @@ -499,19 +424,19 @@ }, "idna": { "hashes": [ - "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", - "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" + "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", + "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0" ], "markers": "python_version >= '3.5'", - "version": "==3.6" + "version": "==3.7" }, "importlib-metadata": { "hashes": [ - "sha256:4805911c3a4ec7c3966410053e9ec6a1fecd629117df5adee56dfc9432a1081e", - "sha256:f238736bb06590ae52ac1fab06a3a9ef1d8dce2b7a35b5ab329371d6c8f5d2cc" + "sha256:15584cf2b1bf449d98ff8a6ff1abef57bf20f3ac6454f431736cd3e660921b2f", + "sha256:188bd24e4c346d3f0a933f275c2fec67050326a856b9a359881d7c2a697e8812" ], "markers": "python_version >= '3.8'", - "version": "==7.0.1" + "version": "==8.0.0" }, "iniconfig": { "hashes": [ @@ -523,19 +448,35 @@ }, "jaraco.classes": { "hashes": [ - "sha256:10afa92b6743f25c0cf5f37c6bb6e18e2c5bb84a16527ccfc0040ea377e7aaeb", - "sha256:c063dd08e89217cee02c8d5e5ec560f2c8ce6cdc2fcdc2e68f7b2e5547ed3621" + "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", + "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790" ], "markers": "python_version >= '3.8'", - "version": "==3.3.0" + "version": "==3.4.0" + }, + "jaraco.context": { + "hashes": [ + "sha256:3e16388f7da43d384a1a7cd3452e72e14732ac9fe459678773a3608a812bf266", + "sha256:c2f67165ce1f9be20f32f650f25d8edfc1646a8aeee48ae06fb35f90763576d2" + ], + "markers": "python_version >= '3.8'", + "version": "==5.3.0" + }, + "jaraco.functools": { + "hashes": [ + "sha256:3b24ccb921d6b593bdceb56ce14799204f473976e2a9d4b15b04d0f2c2326664", + "sha256:d33fa765374c0611b52f8b3a795f8900869aa88c84769d4d1746cd68fb28c3e8" + ], + "markers": "python_version >= '3.8'", + "version": "==4.0.1" }, "keyring": { "hashes": [ - "sha256:4446d35d636e6a10b8bce7caa66913dd9eca5fd222ca03a3d42c38608ac30836", - "sha256:e730ecffd309658a08ee82535a3b5ec4b4c8669a9be11efb66249d8e0aeb9a25" + "sha256:2458681cdefc0dbc0b7eb6cf75d0b98e59f9ad9b2d4edd319d18f68bdca95e50", + "sha256:daaffd42dbda25ddafb1ad5fec4024e5bbcfe424597ca1ca452b299861e49f1b" ], "markers": "python_version >= '3.8'", - "version": "==24.3.0" + "version": "==25.2.1" }, "markdown-it-py": { "hashes": [ @@ -563,11 +504,11 @@ }, "more-itertools": { "hashes": [ - "sha256:686b06abe565edfab151cb8fd385a05651e1fdf8f0a14191e4439283421f8684", - "sha256:8fccb480c43d3e99a00087634c06dd02b0d50fbf088b380de5a41a015ec239e1" + "sha256:e5d93ef411224fbcef366a6e8ddc4c5781bc6359d43412a65dd5964e46111463", + "sha256:ea6a02e24a9161e51faad17a8782b92a0df82c12c1c8886fec7f0c3fa1a1b320" ], "markers": "python_version >= '3.8'", - "version": "==10.2.0" + "version": "==10.3.0" }, "mypy": { "hashes": [ @@ -613,32 +554,32 @@ }, "nh3": { "hashes": [ - "sha256:0d02d0ff79dfd8208ed25a39c12cbda092388fff7f1662466e27d97ad011b770", - "sha256:3277481293b868b2715907310c7be0f1b9d10491d5adf9fce11756a97e97eddf", - "sha256:3b803a5875e7234907f7d64777dfde2b93db992376f3d6d7af7f3bc347deb305", - "sha256:427fecbb1031db085eaac9931362adf4a796428ef0163070c484b5a768e71601", - "sha256:5f0d77272ce6d34db6c87b4f894f037d55183d9518f948bba236fe81e2bb4e28", - "sha256:60684857cfa8fdbb74daa867e5cad3f0c9789415aba660614fe16cd66cbb9ec7", - "sha256:6f42f99f0cf6312e470b6c09e04da31f9abaadcd3eb591d7d1a88ea931dca7f3", - "sha256:86e447a63ca0b16318deb62498db4f76fc60699ce0a1231262880b38b6cff911", - "sha256:8d595df02413aa38586c24811237e95937ef18304e108b7e92c890a06793e3bf", - "sha256:9c0d415f6b7f2338f93035bba5c0d8c1b464e538bfbb1d598acd47d7969284f0", - "sha256:a5167a6403d19c515217b6bcaaa9be420974a6ac30e0da9e84d4fc67a5d474c5", - "sha256:ac19c0d68cd42ecd7ead91a3a032fdfff23d29302dbb1311e641a130dfefba97", - "sha256:b1e97221cedaf15a54f5243f2c5894bb12ca951ae4ddfd02a9d4ea9df9e1a29d", - "sha256:bc2d086fb540d0fa52ce35afaded4ea526b8fc4d3339f783db55c95de40ef02e", - "sha256:d1e30ff2d8d58fb2a14961f7aac1bbb1c51f9bdd7da727be35c63826060b0bf3", - "sha256:f3b53ba93bb7725acab1e030bc2ecd012a817040fd7851b332f86e2f9bb98dc6" - ], - "version": "==0.2.15" + "sha256:0411beb0589eacb6734f28d5497ca2ed379eafab8ad8c84b31bb5c34072b7164", + "sha256:14c5a72e9fe82aea5fe3072116ad4661af5cf8e8ff8fc5ad3450f123e4925e86", + "sha256:19aaba96e0f795bd0a6c56291495ff59364f4300d4a39b29a0abc9cb3774a84b", + "sha256:34c03fa78e328c691f982b7c03d4423bdfd7da69cd707fe572f544cf74ac23ad", + "sha256:36c95d4b70530b320b365659bb5034341316e6a9b30f0b25fa9c9eff4c27a204", + "sha256:3a157ab149e591bb638a55c8c6bcb8cdb559c8b12c13a8affaba6cedfe51713a", + "sha256:42c64511469005058cd17cc1537578eac40ae9f7200bedcfd1fc1a05f4f8c200", + "sha256:5f36b271dae35c465ef5e9090e1fdaba4a60a56f0bb0ba03e0932a66f28b9189", + "sha256:6955369e4d9f48f41e3f238a9e60f9410645db7e07435e62c6a9ea6135a4907f", + "sha256:7b7c2a3c9eb1a827d42539aa64091640bd275b81e097cd1d8d82ef91ffa2e811", + "sha256:8ce0f819d2f1933953fca255db2471ad58184a60508f03e6285e5114b6254844", + "sha256:94a166927e53972a9698af9542ace4e38b9de50c34352b962f4d9a7d4c927af4", + "sha256:a7f1b5b2c15866f2db413a3649a8fe4fd7b428ae58be2c0f6bca5eefd53ca2be", + "sha256:c8b3a1cebcba9b3669ed1a84cc65bf005728d2f0bc1ed2a6594a992e817f3a50", + "sha256:de3ceed6e661954871d6cd78b410213bdcb136f79aafe22aa7182e028b8c7307", + "sha256:f0eca9ca8628dbb4e916ae2491d72957fdd35f7a5d326b7032a345f111ac07fe" + ], + "version": "==0.2.18" }, "packaging": { "hashes": [ - "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", - "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", + "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" ], - "markers": "python_version >= '3.7'", - "version": "==23.2" + "markers": "python_version >= '3.8'", + "version": "==24.1" }, "pathspec": { "hashes": [ @@ -650,27 +591,27 @@ }, "pkginfo": { "hashes": [ - "sha256:4b7a555a6d5a22169fcc9cf7bfd78d296b0361adad412a346c1226849af5e546", - "sha256:8fd5896e8718a4372f0ea9cc9d96f6417c9b986e23a4d116dda26b62cc29d046" + "sha256:2e0dca1cf4c8e39644eed32408ea9966ee15e0d324c62ba899a393b3c6b467aa", + "sha256:bfa76a714fdfc18a045fcd684dbfc3816b603d9d075febef17cb6582bea29573" ], - "markers": "python_version >= '3.6'", - "version": "==1.9.6" + "markers": "python_version >= '3.8'", + "version": "==1.11.1" }, "platformdirs": { "hashes": [ - "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380", - "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420" + "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee", + "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3" ], "markers": "python_version >= '3.8'", - "version": "==4.1.0" + "version": "==4.2.2" }, "pluggy": { "hashes": [ - "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12", - "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7" + "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", + "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669" ], "markers": "python_version >= '3.8'", - "version": "==1.3.0" + "version": "==1.5.0" }, "pprintpp": { "hashes": [ @@ -705,11 +646,11 @@ }, "pygments": { "hashes": [ - "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c", - "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367" + "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", + "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a" ], - "markers": "python_version >= '3.7'", - "version": "==2.17.2" + "markers": "python_version >= '3.8'", + "version": "==2.18.0" }, "pytest": { "hashes": [ @@ -748,19 +689,19 @@ }, "readme-renderer": { "hashes": [ - "sha256:13d039515c1f24de668e2c93f2e877b9dbe6c6c32328b90a40a49d8b2b85f36d", - "sha256:2d55489f83be4992fe4454939d1a051c33edbab778e82761d060c9fc6b308cd1" + "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", + "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1" ], - "markers": "python_version >= '3.8'", - "version": "==42.0" + "markers": "python_version >= '3.9'", + "version": "==44.0" }, "requests": { "hashes": [ - "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", - "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" + "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", + "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" ], - "markers": "python_version >= '3.7'", - "version": "==2.31.0" + "markers": "python_version >= '3.8'", + "version": "==2.32.3" }, "requests-toolbelt": { "hashes": [ @@ -780,11 +721,11 @@ }, "rich": { "hashes": [ - "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa", - "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235" + "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222", + "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432" ], "markers": "python_full_version >= '3.7.0'", - "version": "==13.7.0" + "version": "==13.7.1" }, "snowballstemmer": { "hashes": [ @@ -821,19 +762,19 @@ }, "typing-extensions": { "hashes": [ - "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", - "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" + "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", + "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" ], "markers": "python_version >= '3.8'", - "version": "==4.9.0" + "version": "==4.12.2" }, "urllib3": { "hashes": [ - "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3", - "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54" + "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", + "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168" ], "markers": "python_version >= '3.8'", - "version": "==2.1.0" + "version": "==2.2.2" }, "wheel": { "hashes": [ @@ -846,11 +787,11 @@ }, "zipp": { "hashes": [ - "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31", - "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0" + "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19", + "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c" ], "markers": "python_version >= '3.8'", - "version": "==3.17.0" + "version": "==3.19.2" } } } diff --git a/shared-data/python/opentrons_shared_data/gripper/gripper_definition.py b/shared-data/python/opentrons_shared_data/gripper/gripper_definition.py index 4c4c30c623b..707d960a9ba 100644 --- a/shared-data/python/opentrons_shared_data/gripper/gripper_definition.py +++ b/shared-data/python/opentrons_shared_data/gripper/gripper_definition.py @@ -24,6 +24,7 @@ class GripperModel(str, Enum): v1 = "gripperV1" v1_1 = "gripperV1.1" v1_2 = "gripperV1.2" + v1_3 = "gripperV1.3" def __str__(self) -> str: """Model name.""" @@ -31,6 +32,7 @@ def __str__(self) -> str: self.__class__.v1: "gripperV1", self.__class__.v1_1: "gripperV1.1", self.__class__.v1_2: "gripperV1.2", + self.__class__.v1_3: "gripperV1.3", } return enum_to_str[self] diff --git a/shared-data/python/opentrons_shared_data/pipette/pipette_definition.py b/shared-data/python/opentrons_shared_data/pipette/pipette_definition.py index 61e8f94143e..f37051b69a2 100644 --- a/shared-data/python/opentrons_shared_data/pipette/pipette_definition.py +++ b/shared-data/python/opentrons_shared_data/pipette/pipette_definition.py @@ -8,7 +8,7 @@ # The highest and lowest existing overlap version values. TIP_OVERLAP_VERSION_MINIMUM = 0 -TIP_OVERLAP_VERSION_MAXIMUM = 1 +TIP_OVERLAP_VERSION_MAXIMUM = 3 PLUNGER_CURRENT_MINIMUM = 0.1 PLUNGER_CURRENT_MAXIMUM = 1.5 diff --git a/shared-data/python/opentrons_shared_data/robot/dev_types.py b/shared-data/python/opentrons_shared_data/robot/dev_types.py index 555ec6008ba..90f0f19c1c4 100644 --- a/shared-data/python/opentrons_shared_data/robot/dev_types.py +++ b/shared-data/python/opentrons_shared_data/robot/dev_types.py @@ -1,7 +1,7 @@ """opentrons_shared_data.robot.dev_types: types for robot def.""" import enum from typing import NewType, List, Dict, Any -from typing_extensions import Literal, TypedDict +from typing_extensions import Literal, TypedDict, NotRequired RobotSchemaVersion1 = Literal[1] @@ -29,9 +29,19 @@ def robot_literal_to_enum(cls, robot_type: RobotType) -> "RobotTypeEnum": # No final `else` statement, depend on mypy exhaustiveness checking +class mountOffset(TypedDict): + """The mount offsets for a given robot type based off the center of the carriage..""" + + left: List[float] + right: List[float] + gripper: NotRequired[List[float]] + + class RobotDefinition(TypedDict): """A python version of the robot definition type.""" displayName: str robotType: RobotType models: List[str] + extents: List[float] + mountOffsets: mountOffset diff --git a/shared-data/python/setup.py b/shared-data/python/setup.py index 4e1720cb610..c82e4de1777 100644 --- a/shared-data/python/setup.py +++ b/shared-data/python/setup.py @@ -141,7 +141,7 @@ def get_version(): ) PACKAGES = find_packages(where=".", exclude=["tests", "tests.*"]) INSTALL_REQUIRES = [ - "jsonschema>=3.0.1,<4.18.0", + "jsonschema>=3.0.1,<5", "typing-extensions>=4.0.0,<5", "pydantic>=1.10.9,<2.0.0", ] diff --git a/shared-data/robot/definitions/1/ot2.json b/shared-data/robot/definitions/1/ot2.json index 9f6a48be16c..50c6eb4256a 100644 --- a/shared-data/robot/definitions/1/ot2.json +++ b/shared-data/robot/definitions/1/ot2.json @@ -1,5 +1,10 @@ { "displayName": "OT-2", "robotType": "OT-2 Standard", - "models": ["OT-2 Standard", "OT-2 Refresh"] + "models": ["OT-2 Standard", "OT-2 Refresh"], + "extents": [446.75, 347.5, 0.0], + "mountOffsets": { + "left": [-34.0, 0.0, 0.0], + "right": [0.0, 0.0, 0.0] + } } diff --git a/shared-data/robot/definitions/1/ot3.json b/shared-data/robot/definitions/1/ot3.json index 3916570adee..eb3a943d886 100644 --- a/shared-data/robot/definitions/1/ot3.json +++ b/shared-data/robot/definitions/1/ot3.json @@ -1,5 +1,11 @@ { "displayName": "OT-3", "robotType": "OT-3 Standard", - "models": ["OT-3 Standard"] + "models": ["OT-3 Standard"], + "extents": [477.2, 493.8, 0.0], + "mountOffsets": { + "left": [-13.5, -60.5, 255.675], + "right": [40.5, -60.5, 255.675], + "gripper": [84.55, -12.75, 93.85] + } } diff --git a/shared-data/robot/schemas/1.json b/shared-data/robot/schemas/1.json index 5f409032451..44e25e6caf5 100644 --- a/shared-data/robot/schemas/1.json +++ b/shared-data/robot/schemas/1.json @@ -5,11 +5,18 @@ "robotType": { "type": "string", "enum": ["OT-2 Standard", "OT-3 Standard"] + }, + "xyzArray": { + "type": "array", + "description": "Array of 3 numbers, [x, y, z]", + "items": { "type": "number" }, + "minItems": 3, + "maxItems": 3 } }, "description": "Describes an Opentrons liquid handling robot.", "type": "object", - "required": ["displayName", "robotType", "models"], + "required": ["displayName", "robotType", "models", "extents", "mountOffsets"], "properties": { "displayName": { "description": "A user-facing friendly name for the machine.", @@ -25,6 +32,29 @@ "items": { "type": "string" } + }, + "extents": { + "description": "The maximum addressable coordinates of the deck without instruments.", + "$ref": "#/definitions/xyzArray" + }, + "mountOffsets": { + "description": "The physical mount offsets from the center of the instrument carriage.", + "type": "object", + "required": ["left", "right"], + "properties": { + "left": { + "description": "The left mount offset from the center of the carriage to the center of the left mount", + "$ref": "#/definitions/xyzArray" + }, + "right": { + "description": "The right mount offset from the center of the carriage to the center of the right mount", + "$ref": "#/definitions/xyzArray" + }, + "gripper": { + "description": "The gripper mount offset from the center of the carriage to the center of the gripper, only on OT-3 Standard definitions", + "$ref": "#/definitions/xyzArray" + } + } } } } diff --git a/yarn.lock b/yarn.lock index 7d15ddae134..05383e56198 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1769,7 +1769,7 @@ core-js-pure "^3.30.2" regenerator-runtime "^0.14.0" -"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.5", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.21.0", "@babel/runtime@^7.22.5", "@babel/runtime@^7.23.8", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.10.5", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.21.0", "@babel/runtime@^7.22.5", "@babel/runtime@^7.23.8", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.24.4" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.4.tgz#de795accd698007a66ba44add6cc86542aff1edd" integrity sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA== @@ -3269,7 +3269,7 @@ react-intersection-observer "^8.33.1" react-markdown "9.0.1" react-redux "8.1.2" - react-router-dom "5.3.4" + react-router-dom "6.24.1" react-select "5.4.0" react-simple-keyboard "^3.7.0" react-viewport-list "6.3.0" @@ -3295,12 +3295,11 @@ "@types/styled-components" "^5.1.26" "@types/webpack-env" "^1.16.0" classnames "2.2.5" - interactjs "^1.10.17" + interactjs "^1.10.27" lodash "4.17.21" react-i18next "13.5.0" react-popper "1.0.0" react-remove-scroll "2.4.3" - react-router-dom "5.3.4" react-select "5.4.0" redux "4.0.5" styled-components "5.3.6" @@ -3338,7 +3337,7 @@ query-string "6.2.0" react "18.2.0" react-dom "18.2.0" - react-router-dom "5.3.4" + react-router-dom "6.24.1" yup "0.32.9" "@opentrons/react-api-client@link:react-api-client": @@ -3729,6 +3728,11 @@ "@react-spring/shared" "~9.6.1" "@react-spring/types" "~9.6.1" +"@remix-run/router@1.17.1": + version "1.17.1" + resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.17.1.tgz#bf93997beb81863fde042ebd05013a2618471362" + integrity sha512-mCOMec4BKd6BRGBZeSnGiIgwsbLGp3yhVqAD8H+PxiRNEHgDpZb8J1TnrSDlg97t0ySKMQJTHCWBCmBpSmkF6Q== + "@rollup/plugin-alias@^3.1.2": version "3.1.9" resolved "https://registry.yarnpkg.com/@rollup/plugin-alias/-/plugin-alias-3.1.9.tgz#a5d267548fe48441f34be8323fb64d1d4a1b3fdf" @@ -5345,11 +5349,6 @@ dependencies: "@types/unist" "*" -"@types/history@^4.7.11": - version "4.7.11" - resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64" - integrity sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA== - "@types/hoist-non-react-statics@*", "@types/hoist-non-react-statics@^3.3.0", "@types/hoist-non-react-statics@^3.3.1": version "3.3.5" resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz#dab7867ef789d87e2b4b0003c9d65c49cc44a494" @@ -5579,23 +5578,6 @@ hoist-non-react-statics "^3.3.0" redux "^4.0.0" -"@types/react-router-dom@5.3.3": - version "5.3.3" - resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.3.tgz#e9d6b4a66fcdbd651a5f106c2656a30088cc1e83" - integrity sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw== - dependencies: - "@types/history" "^4.7.11" - "@types/react" "*" - "@types/react-router" "*" - -"@types/react-router@*": - version "5.1.20" - resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.20.tgz#88eccaa122a82405ef3efbcaaa5dcdd9f021387c" - integrity sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q== - dependencies: - "@types/history" "^4.7.11" - "@types/react" "*" - "@types/react-transition-group@^4.4.0": version "4.4.10" resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.10.tgz#6ee71127bdab1f18f11ad8fb3322c6da27c327ac" @@ -12822,18 +12804,6 @@ history@4.7.2: value-equal "^0.4.0" warning "^3.0.0" -history@^4.9.0: - version "4.10.1" - resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3" - integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew== - dependencies: - "@babel/runtime" "^7.1.2" - loose-envify "^1.2.0" - resolve-pathname "^3.0.0" - tiny-invariant "^1.0.2" - tiny-warning "^1.0.0" - value-equal "^1.0.1" - hmac-drbg@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -12843,7 +12813,7 @@ hmac-drbg@^1.0.1: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: +hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -13325,7 +13295,7 @@ inline-style-parser@0.2.3: resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.2.3.tgz#e35c5fb45f3a83ed7849fe487336eb7efa25971c" integrity sha512-qlD8YNDqyTKTyuITrDOffsl6Tdhv+UC4hcdAVuQsK4IMQ99nSgd1MIA/Q+jQYoh9r3hVUXhYh7urSRmXPkW04g== -interactjs@^1.10.17: +interactjs@^1.10.27: version "1.10.27" resolved "https://registry.yarnpkg.com/interactjs/-/interactjs-1.10.27.tgz#16499aba4987a5ccfdaddca7d1ba7bb1118e14d0" integrity sha512-y/8RcCftGAF24gSp76X2JS3XpHiUvDQyhF8i7ujemBz77hwiHDuJzftHx7thY8cxGogwGiPJ+o97kWB6eAXnsA== @@ -14832,7 +14802,7 @@ longest@^1.0.1: resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" integrity sha512-k+yt5n3l48JU4k8ftnKG6V7u32wyH2NfKzeMto9F/QRE0amxy/LayxwlvjjkZEIzqR+19IrtFO8p5kB9QaYUFg== -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -16989,13 +16959,6 @@ path-to-regexp@3.0.0: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.0.0.tgz#c981a218f3df543fa28696be2f88e0c58d2e012a" integrity sha512-ZOtfhPttCrqp2M1PBBH4X13XlvnfhIwD7yCLx+GoGoXRPQyxGOTdQMpIzPSPKXAJT/JQrdfFrgdJOyAzvgpQ9A== -path-to-regexp@^1.7.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" - integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== - dependencies: - isarray "0.0.1" - path-type@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" @@ -18475,7 +18438,7 @@ react-is@18.1.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.1.0.tgz#61aaed3096d30eacf2a2127118b5b41387d32a67" integrity sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg== -react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0: +react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -18574,33 +18537,20 @@ react-remove-scroll@2.5.5: use-callback-ref "^1.3.0" use-sidecar "^1.1.2" -react-router-dom@5.3.4: - version "5.3.4" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.3.4.tgz#2ed62ffd88cae6db134445f4a0c0ae8b91d2e5e6" - integrity sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ== +react-router-dom@6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.24.1.tgz#b1a22f7d6c5a1bfce30732bd370713f991ab4de4" + integrity sha512-U19KtXqooqw967Vw0Qcn5cOvrX5Ejo9ORmOtJMzYWtCT4/WOfFLIZGGsVLxcd9UkBO0mSTZtXqhZBsWlHr7+Sg== dependencies: - "@babel/runtime" "^7.12.13" - history "^4.9.0" - loose-envify "^1.3.1" - prop-types "^15.6.2" - react-router "5.3.4" - tiny-invariant "^1.0.2" - tiny-warning "^1.0.0" - -react-router@5.3.4: - version "5.3.4" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.3.4.tgz#8ca252d70fcc37841e31473c7a151cf777887bb5" - integrity sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA== - dependencies: - "@babel/runtime" "^7.12.13" - history "^4.9.0" - hoist-non-react-statics "^3.1.0" - loose-envify "^1.3.1" - path-to-regexp "^1.7.0" - prop-types "^15.6.2" - react-is "^16.6.0" - tiny-invariant "^1.0.2" - tiny-warning "^1.0.0" + "@remix-run/router" "1.17.1" + react-router "6.24.1" + +react-router@6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.24.1.tgz#5a3bbba0000afba68d42915456ca4c806f37a7de" + integrity sha512-PTXFXGK2pyXpHzVo3rR9H7ip4lSPZZc0bHG5CARmj65fTT6qG7sTngmb6lcYu1gf3y/8KxORoy9yn59pGpCnpg== + dependencies: + "@remix-run/router" "1.17.1" react-select@5.4.0: version "5.4.0" @@ -19281,11 +19231,6 @@ resolve-pathname@^2.2.0: resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-2.2.0.tgz#7e9ae21ed815fd63ab189adeee64dc831eefa879" integrity sha512-bAFz9ld18RzJfddgrO2e/0S2O81710++chRMUxHjXOYKF6jTAMrUNZrEZ1PvV0zlhfjidm08iRPdTLPno1FuRg== -resolve-pathname@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd" - integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng== - resolve-pkg-maps@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" @@ -20436,7 +20381,16 @@ strict-uri-encode@^2.0.0: resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" integrity sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ== -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -20566,7 +20520,7 @@ stringify-object@^3.2.1: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -20587,6 +20541,13 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -21142,12 +21103,12 @@ tiny-case@^1.0.3: resolved "https://registry.yarnpkg.com/tiny-case/-/tiny-case-1.0.3.tgz#d980d66bc72b5d5a9ca86fb7c9ffdb9c898ddd03" integrity sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q== -tiny-invariant@^1.0.2, tiny-invariant@^1.3.1, tiny-invariant@^1.3.3: +tiny-invariant@^1.3.1, tiny-invariant@^1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== -tiny-warning@^1.0.0, tiny-warning@^1.0.2: +tiny-warning@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== @@ -22157,11 +22118,6 @@ value-equal@^0.4.0: resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-0.4.0.tgz#c5bdd2f54ee093c04839d71ce2e4758a6890abc7" integrity sha512-x+cYdNnaA3CxvMaTX0INdTCN8m8aF2uY9BvEqmxuYp8bL09cs/kWVQPVGcA35fMktdOsP69IgU7wFj/61dJHEw== -value-equal@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c" - integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw== - vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" @@ -22807,7 +22763,7 @@ worker-plugin@^5.0.0: dependencies: loader-utils "^1.1.0" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -22834,6 +22790,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"