diff --git a/app/src/assets/localization/en/device_settings.json b/app/src/assets/localization/en/device_settings.json index 8ecaf1a094f..9066277f49f 100644 --- a/app/src/assets/localization/en/device_settings.json +++ b/app/src/assets/localization/en/device_settings.json @@ -17,6 +17,7 @@ "calibrate_deck_description": "For pre-2019 robots that do not have crosses etched on the deck.", "calibrate_deck_to_dots": "Calibrate deck to dots", "calibrate_deck": "Calibrate deck", + "calibrate_gripper": "Calibrate gripper", "calibrate_now": "Calibrate now", "calibrate_pipette": "Calibrate Pipette Offset", "calibration_health_check_description": "Check the accuracy of key calibration points without recalibrating the robot.", @@ -121,8 +122,9 @@ "firmware_version": "Firmware Version", "fully_calibrate_before_checking_health": "Fully calibrate your robot before checking calibration health", "go_to_advanced_settings": "Go to Advanced App Settings", - "gripper_calibration_description": "Placeholder for gripper calibration section", + "gripper_calibration_description": "Gripper calibration uses a metal pin to determine the gripper's exact position relative to precision-cut squares on deck slots.", "gripper_calibration_title": "Gripper Calibration", + "gripper_serial": "Gripper Serial", "health_check": "Check health", "hide": "Hide", "historic_offsets_description": "Use stored data when setting up a protocol.", @@ -187,6 +189,7 @@ "proceed_without_updating": "Proceed without updating", "protocol_run_history": "Protocol run History", "recalibrate_deck": "Recalibrate deck", + "recalibrate_gripper": "Recalibrate gripper", "recalibrate_now": "Recalibrate now", "recalibrate_pipette": "Recalibrate Pipette Offset", "recalibrate_tip_and_pipette": "Recalibrate Tip Length and Pipette Offset", diff --git a/app/src/organisms/RobotSettingsCalibration/RobotSettingsGripperCalibration.tsx b/app/src/organisms/RobotSettingsCalibration/RobotSettingsGripperCalibration.tsx index 5f29a3d3508..7a24702fef9 100644 --- a/app/src/organisms/RobotSettingsCalibration/RobotSettingsGripperCalibration.tsx +++ b/app/src/organisms/RobotSettingsCalibration/RobotSettingsGripperCalibration.tsx @@ -1,20 +1,73 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' - +import styled, { css } from 'styled-components' import { Box, Flex, ALIGN_CENTER, + COLORS, + BORDERS, JUSTIFY_SPACE_BETWEEN, SPACING, TYPOGRAPHY, + DIRECTION_COLUMN, + POSITION_RELATIVE, + ALIGN_FLEX_END, + POSITION_ABSOLUTE, + useOnClickOutside, } from '@opentrons/components' - +import { useMenuHandleClickOutside } from '../../atoms/MenuList/hooks' +import { OverflowBtn } from '../../atoms/MenuList/OverflowBtn' +import { MenuItem } from '../../atoms/MenuList/MenuItem' import { StyledText } from '../../atoms/text' +import { GripperWizardFlows } from '../../organisms/GripperWizardFlows' +import { formatLastCalibrated } from './CalibrationDetails/utils' +import type { GripperData } from '@opentrons/api-client' -export function RobotSettingsGripperCalibration(): JSX.Element { - const { t } = useTranslation('device_settings') +const StyledTable = styled.table` + width: 100%; + border-collapse: collapse; + text-align: left; +` +const StyledTableHeader = styled.th` + ${TYPOGRAPHY.labelSemiBold} + padding: ${SPACING.spacing8}; +` +const StyledTableRow = styled.tr` + padding: ${SPACING.spacing8}; + border-bottom: ${BORDERS.lineBorder}; +` +const StyledTableCell = styled.td` + padding: ${SPACING.spacing8}; + text-overflow: wrap; +` + +const BODY_STYLE = css` + box-shadow: 0 0 0 1px ${COLORS.medGreyEnabled}; + border-radius: 3px; +` +export interface RobotSettingsGripperCalibrationProps { + gripper: GripperData +} + +export function RobotSettingsGripperCalibration( + props: RobotSettingsGripperCalibrationProps +): JSX.Element { + const { t } = useTranslation('device_settings') + const { gripper } = props + const { + menuOverlay, + handleOverflowClick, + showOverflowMenu, + setShowOverflowMenu, + } = useMenuHandleClickOutside() + const calsOverflowWrapperRef = useOnClickOutside({ + onClickOutside: () => setShowOverflowMenu(false), + }) + const [showWizardFlow, setShowWizardFlow] = React.useState(false) + const gripperCalibrationLastModified = + gripper.data.calibratedOffset?.last_modified return ( @@ -25,6 +78,78 @@ export function RobotSettingsGripperCalibration(): JSX.Element { {t('gripper_calibration_description')} + + + + {t('gripper_serial')} + + {t('last_calibrated_label')} + + + + + + + {gripper.serialNumber} + + + + {gripperCalibrationLastModified != null ? ( + + {formatLastCalibrated(gripperCalibrationLastModified)} + + ) : ( + + {t('not_calibrated_short')} + + )} + + + + + + {showWizardFlow ? ( + setShowWizardFlow(false)} + /> + ) : null} + {showOverflowMenu ? ( + + setShowWizardFlow(true)}> + {t( + gripperCalibrationLastModified == null + ? 'calibrate_gripper' + : 'recalibrate_gripper' + )} + + + ) : null} + {menuOverlay} + + + + + diff --git a/app/src/organisms/RobotSettingsCalibration/__tests__/RobotSettingsCalibration.test.tsx b/app/src/organisms/RobotSettingsCalibration/__tests__/RobotSettingsCalibration.test.tsx index 1923d2f44ff..09d296a5958 100644 --- a/app/src/organisms/RobotSettingsCalibration/__tests__/RobotSettingsCalibration.test.tsx +++ b/app/src/organisms/RobotSettingsCalibration/__tests__/RobotSettingsCalibration.test.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { when, resetAllWhenMocks } from 'jest-when' import { renderWithProviders } from '@opentrons/components' - +import { useInstrumentsQuery } from '@opentrons/react-api-client' import { i18n } from '../../../i18n' import { CalibrationStatusCard } from '../../../organisms/CalibrationStatusCard' import { useFeatureFlag } from '../../../redux/config' @@ -36,6 +36,7 @@ import { RobotSettingsCalibration } from '..' import type { AttachedPipettesByMount } from '../../../redux/pipettes/types' +jest.mock('@opentrons/react-api-client/src/instruments/useInstrumentsQuery') jest.mock('../../../organisms/CalibrationStatusCard') jest.mock('../../../redux/config') jest.mock('../../../redux/sessions/selectors') @@ -55,6 +56,9 @@ const mockAttachedPipettes: AttachedPipettesByMount = { const mockUsePipetteOffsetCalibrations = usePipetteOffsetCalibrations as jest.MockedFunction< typeof usePipetteOffsetCalibrations > +const mockUseInstrumentsQuery = useInstrumentsQuery as jest.MockedFunction< + typeof useInstrumentsQuery +> const mockUseRobot = useRobot as jest.MockedFunction const mockUseAttachedPipettes = useAttachedPipettes as jest.MockedFunction< typeof useAttachedPipettes @@ -121,6 +125,16 @@ describe('RobotSettingsCalibration', () => { left: null, right: null, }) + mockUseInstrumentsQuery.mockReturnValue({ + data: { + data: [ + { + ok: true, + instrumentType: 'gripper', + } as any, + ], + }, + } as any) mockUsePipetteOffsetCalibrations.mockReturnValue([ mockPipetteOffsetCalibration1, mockPipetteOffsetCalibration2, diff --git a/app/src/organisms/RobotSettingsCalibration/__tests__/RobotSettingsGripperCalibration.test.tsx b/app/src/organisms/RobotSettingsCalibration/__tests__/RobotSettingsGripperCalibration.test.tsx index f110b113383..2ff8e23f95c 100644 --- a/app/src/organisms/RobotSettingsCalibration/__tests__/RobotSettingsGripperCalibration.test.tsx +++ b/app/src/organisms/RobotSettingsCalibration/__tests__/RobotSettingsGripperCalibration.test.tsx @@ -3,17 +3,91 @@ import * as React from 'react' import { renderWithProviders } from '@opentrons/components' import { i18n } from '../../../i18n' +import { GripperWizardFlows } from '../../../organisms/GripperWizardFlows' +import { formatLastCalibrated } from '../CalibrationDetails/utils' import { RobotSettingsGripperCalibration } from '../RobotSettingsGripperCalibration' +import type { GripperData } from '@opentrons/api-client' +import type { RobotSettingsGripperCalibrationProps } from '../RobotSettingsGripperCalibration' +jest.mock('../../../organisms/GripperWizardFlows') +jest.mock('../CalibrationDetails/utils') -const render = () => { - return renderWithProviders(, { +const mockGripperWizardFlows = GripperWizardFlows as jest.MockedFunction< + typeof GripperWizardFlows +> +const mockFormatLastCalibrated = formatLastCalibrated as jest.MockedFunction< + typeof formatLastCalibrated +> + +let props = { + gripper: { + serialNumber: 'mockSerial123', + data: { + calibratedOffset: { + last_modified: '12345', + }, + }, + } as GripperData, +} + +const render = (props: RobotSettingsGripperCalibrationProps) => { + return renderWithProviders(, { i18nInstance: i18n, }) } describe('RobotSettingsGripperCalibration', () => { + beforeEach(() => { + mockFormatLastCalibrated.mockReturnValue('last calibrated 1/2/3') + mockGripperWizardFlows.mockReturnValue(<>Mock Wizard Flow) + }) it('renders a title and description - Gripper Calibration section', () => { - const [{ getByText }] = render() + const [{ getByText }] = render(props) getByText('Gripper Calibration') + getByText( + `Gripper calibration uses a metal pin to determine the gripper's exact position relative to precision-cut squares on deck slots.` + ) + getByText('Gripper Serial') + getByText('Last Calibrated') + }) + it('renders last calibrated date and recalibrate button if calibration data exists', () => { + const [{ getByText, getByRole }] = render(props) + getByText('mockSerial123') + getByText('last calibrated 1/2/3') + const overflowButton = getByRole('button', { + name: 'CalibrationOverflowMenu_button_gripperCalibration', + }) + overflowButton.click() + getByText('Recalibrate gripper') + }) + it('renders not calibrated and calibrate button if calibration data does not exist', () => { + props = { + gripper: { + serialNumber: 'mockSerial123', + data: { + calibratedOffset: { + last_modified: undefined, + }, + }, + } as GripperData, + } + + const [{ getByText, getByRole }] = render(props) + getByText('mockSerial123') + getByText('Not calibrated') + const overflowButton = getByRole('button', { + name: 'CalibrationOverflowMenu_button_gripperCalibration', + }) + overflowButton.click() + getByText('Calibrate gripper') + }) + it('renders gripper wizard flows when calibrate is pressed', () => { + const [{ getByText, getByRole }] = render(props) + const overflowButton = getByRole('button', { + name: 'CalibrationOverflowMenu_button_gripperCalibration', + }) + overflowButton.click() + const calibrateButton = getByText('Calibrate gripper') + calibrateButton.click() + getByText('Mock Wizard Flow') }) }) diff --git a/app/src/organisms/RobotSettingsCalibration/index.tsx b/app/src/organisms/RobotSettingsCalibration/index.tsx index 086ad913fdd..03883cc6511 100644 --- a/app/src/organisms/RobotSettingsCalibration/index.tsx +++ b/app/src/organisms/RobotSettingsCalibration/index.tsx @@ -6,6 +6,7 @@ import { useAllPipetteOffsetCalibrationsQuery, useAllTipLengthCalibrationsQuery, useCalibrationStatusQuery, + useInstrumentsQuery, } from '@opentrons/react-api-client' import { Portal } from '../../App/portal' @@ -40,6 +41,7 @@ import type { DeckCalibrationSession, } from '../../redux/sessions/types' import type { State, Dispatch } from '../../redux/types' +import { GripperData } from '@opentrons/api-client' const CALS_FETCH_MS = 5000 @@ -120,6 +122,11 @@ export function RobotSettingsCalibration({ const pipetteOffsetCalibrations = useAllPipetteOffsetCalibrationsQuery().data ?.data + const { data: attachedInstruments } = useInstrumentsQuery() + const attachedGripper = + (attachedInstruments?.data ?? []).find( + (i): i is GripperData => i.instrumentType === 'gripper' && i.ok + ) ?? null const attachedPipettesFromInstrumentQuery = useAttachedPipettesFromInstrumentsQuery() const attachedPipettes = useAttachedPipettes() const { isRunRunning: isRunning } = useRunStatuses() @@ -317,7 +324,9 @@ export function RobotSettingsCalibration({ updateRobotStatus={updateRobotStatus} /> - + {attachedGripper != null && ( + + )} ) : ( <>