diff --git a/app/src/assets/localization/en/module_wizard_flows.json b/app/src/assets/localization/en/module_wizard_flows.json index b347ced8f01..636bb368662 100644 --- a/app/src/assets/localization/en/module_wizard_flows.json +++ b/app/src/assets/localization/en/module_wizard_flows.json @@ -24,6 +24,7 @@ "get_started": "To get started, remove labware from the deck and clean up the working area to make the calibration easier. Also gather the needed equipment shown to the right.The calibration adapter came with your module. The pipette probe came with your Flex pipette.", "install_adapter": "Place calibration adapter in {{module}}", "install_calibration_adapter": "Install calibration adapter", + "location_occupied": "A {{fixture}} is currently specified here on the deck configuration", "module_calibrating": "Stand back, {{moduleName}} is calibrating", "module_calibration_failed": "Module calibration was unsuccessful. Make sure the calibration adapter is fully seated on the module and try again. If you still have trouble, contact Opentrons Support.{{error}}", "module_calibration": "Module calibration", @@ -39,6 +40,7 @@ "recalibrate": "Recalibrate", "select_location": "Select module location", "select_the_slot": "Select the slot where you installed the {{module}} on the deck map to the right. The location must be correct for successful calibration.", + "slot_unavailable": "Slot unavailable", "stand_back_exiting": "Stand back, robot is in motion", "stand_back": "Stand back, calibration in progress", "start_setup": "Start setup", diff --git a/app/src/molecules/GenericWizardTile/index.tsx b/app/src/molecules/GenericWizardTile/index.tsx index 930269bc129..de4d904e948 100644 --- a/app/src/molecules/GenericWizardTile/index.tsx +++ b/app/src/molecules/GenericWizardTile/index.tsx @@ -19,9 +19,11 @@ import { DISPLAY_INLINE_BLOCK, ALIGN_CENTER, ALIGN_FLEX_END, + useHoverTooltip, } from '@opentrons/components' import { getIsOnDevice } from '../../redux/config' import { StyledText } from '../../atoms/text' +import { Tooltip } from '../../atoms/Tooltip' import { NeedHelpLink } from '../../organisms/CalibrationPanels' import { SmallButton } from '../../atoms/buttons' @@ -90,6 +92,7 @@ export interface GenericWizardTileProps { proceedIsDisabled?: boolean proceedButton?: JSX.Element backIsDisabled?: boolean + disableProceedReason?: string } export function GenericWizardTile(props: GenericWizardTileProps): JSX.Element { @@ -104,9 +107,11 @@ export function GenericWizardTile(props: GenericWizardTileProps): JSX.Element { proceedIsDisabled, proceedButton, backIsDisabled, + disableProceedReason, } = props const { t } = useTranslation('shared') const isOnDevice = useSelector(getIsOnDevice) + const [targetProps, tooltipProps] = useHoverTooltip() let buttonPositioning: string = '' if ( @@ -158,19 +163,35 @@ export function GenericWizardTile(props: GenericWizardTileProps): JSX.Element { {getHelp != null ? : null} {proceed != null && proceedButton == null ? ( isOnDevice ? ( - + <> + + {disableProceedReason != null && ( + + {disableProceedReason} + + )} + ) : ( - - {proceedButtonText} - + <> + + {proceedButtonText} + + {disableProceedReason != null && ( + + {disableProceedReason} + + )} + ) ) : null} {proceed == null && proceedButton != null ? proceedButton : null} diff --git a/app/src/organisms/ModuleWizardFlows/SelectLocation.tsx b/app/src/organisms/ModuleWizardFlows/SelectLocation.tsx index 09087740eec..ea801c48f4c 100644 --- a/app/src/organisms/ModuleWizardFlows/SelectLocation.tsx +++ b/app/src/organisms/ModuleWizardFlows/SelectLocation.tsx @@ -3,10 +3,10 @@ import { useTranslation } from 'react-i18next' import { css } from 'styled-components' import { FLEX_ROBOT_TYPE, - ModuleLocation, getDeckDefFromRobotType, getModuleDisplayName, THERMOCYCLER_MODULE_TYPE, + CutoutConfig, } from '@opentrons/shared-data' import { RESPONSIVENESS, @@ -31,6 +31,7 @@ export const BODY_STYLE = css` interface SelectLocationProps extends ModuleCalibrationWizardStepProps { setSlotName: React.Dispatch> availableSlotNames: string[] + occupiedCutouts: CutoutConfig[] } export const SelectLocation = ( props: SelectLocationProps @@ -41,6 +42,7 @@ export const SelectLocation = ( slotName, setSlotName, availableSlotNames, + occupiedCutouts, } = props const { t } = useTranslation('module_wizard_flows') const moduleName = getModuleDisplayName(attachedModule.moduleModel) @@ -58,6 +60,7 @@ export const SelectLocation = ( ) + return ( setSlotName(loc.slotName)} - disabledLocations={deckDef.locations.addressableAreas.reduce< - ModuleLocation[] - >((acc, slot) => { - if (availableSlotNames.some(slotName => slotName === slot.id)) - return acc - return [...acc, { slotName: slot.id }] - }, [])} + availableSlotNames={availableSlotNames} + occupiedCutouts={occupiedCutouts} isThermocycler={ attachedModule.moduleType === THERMOCYCLER_MODULE_TYPE } + showTooltipOnDisabled={true} /> } bodyText={bodyText} proceedButtonText={t('confirm_location')} proceed={handleOnClick} + proceedIsDisabled={slotName == null} + disableProceedReason={ + slotName == null + ? 'Current deck configuration prevents module placement' + : undefined + } /> ) } diff --git a/app/src/organisms/ModuleWizardFlows/index.tsx b/app/src/organisms/ModuleWizardFlows/index.tsx index 9d2e90f7bf9..61d3bdd5bec 100644 --- a/app/src/organisms/ModuleWizardFlows/index.tsx +++ b/app/src/organisms/ModuleWizardFlows/index.tsx @@ -1,14 +1,18 @@ import * as React from 'react' import { useSelector } from 'react-redux' import { Trans, useTranslation } from 'react-i18next' -import { useDeleteMaintenanceRunMutation } from '@opentrons/react-api-client' +import { + useDeleteMaintenanceRunMutation, + useCurrentMaintenanceRun, + useDeckConfigurationQuery, +} from '@opentrons/react-api-client' import { COLORS } from '@opentrons/components' import { - CreateCommand, getModuleType, getModuleDisplayName, + FLEX_CUTOUT_BY_SLOT_ID, + SINGLE_SLOT_FIXTURES, } from '@opentrons/shared-data' - import { LegacyModalShell } from '../../molecules/LegacyModal' import { Portal } from '../../App/portal' import { StyledText } from '../../atoms/text' @@ -29,9 +33,13 @@ import { PlaceAdapter } from './PlaceAdapter' import { SelectLocation } from './SelectLocation' import { Success } from './Success' import { DetachProbe } from './DetachProbe' -import { useNotifyCurrentMaintenanceRun } from '../../resources/maintenance_runs/useNotifyCurrentMaintenanceRun' import type { AttachedModule, CommandData } from '@opentrons/api-client' +import type { + CreateCommand, + CutoutConfig, + SingleSlotCutoutFixtureId, +} from '@opentrons/shared-data' interface ModuleWizardFlowsProps { attachedModule: AttachedModule @@ -64,10 +72,26 @@ export const ModuleWizardFlows = ( : attachedPipettes.right const moduleCalibrationSteps = getModuleCalibrationSteps() + const deckConfig = useDeckConfigurationQuery().data ?? [] + const occupiedCutouts = deckConfig.filter( + (fixture: CutoutConfig) => + !SINGLE_SLOT_FIXTURES.includes( + fixture.cutoutFixtureId as SingleSlotCutoutFixtureId + ) + ) const availableSlotNames = - FLEX_SLOT_NAMES_BY_MOD_TYPE[getModuleType(attachedModule.moduleModel)] ?? [] + FLEX_SLOT_NAMES_BY_MOD_TYPE[ + getModuleType(attachedModule.moduleModel) + ]?.filter( + slot => + !occupiedCutouts.some( + (occCutout: CutoutConfig) => + occCutout.cutoutId === FLEX_CUTOUT_BY_SLOT_ID[slot] + ) + ) ?? [] + const [slotName, setSlotName] = React.useState( - initialSlotName != null ? initialSlotName : availableSlotNames?.[0] ?? 'D1' + initialSlotName != null ? initialSlotName : availableSlotNames?.[0] ?? null ) const [currentStepIndex, setCurrentStepIndex] = React.useState(0) const totalStepCount = moduleCalibrationSteps.length - 1 @@ -91,7 +115,7 @@ export const ModuleWizardFlows = ( setMonitorMaintenanceRunForDeletion, ] = React.useState(false) - const { data: maintenanceRunData } = useNotifyCurrentMaintenanceRun({ + const { data: maintenanceRunData } = useCurrentMaintenanceRun({ refetchInterval: RUN_REFETCH_INTERVAL, enabled: createdMaintenanceRunId != null, }) @@ -104,7 +128,9 @@ export const ModuleWizardFlows = ( createTargetedMaintenanceRun, isLoading: isCreateLoading, } = useCreateTargetedMaintenanceRunMutation({ - onSuccess: response => { + onSuccess: (response: { + data: { id: React.SetStateAction } + }) => { setCreatedMaintenanceRunId(response.data.id) }, }) @@ -149,8 +175,12 @@ export const ModuleWizardFlows = ( } const { deleteMaintenanceRun } = useDeleteMaintenanceRunMutation({ - onSuccess: () => handleClose(), - onError: () => handleClose(), + onSuccess: () => { + handleClose() + }, + onError: () => { + handleClose() + }, }) const handleCleanUpAndClose = (): void => { @@ -158,7 +188,7 @@ export const ModuleWizardFlows = ( if (maintenanceRunData?.data.id == null) handleClose() else { chainRunCommands( - maintenanceRunData?.data.id, + maintenanceRunData?.data.id as string, [{ commandType: 'home' as const, params: {} }], false ) @@ -190,7 +220,7 @@ export const ModuleWizardFlows = ( continuePastCommandFailure: boolean ): Promise => chainRunCommands( - maintenanceRunData?.data.id, + maintenanceRunData?.data.id as string, commands, continuePastCommandFailure ) @@ -275,6 +305,7 @@ export const ModuleWizardFlows = ( {...calibrateBaseProps} availableSlotNames={availableSlotNames} setSlotName={setSlotName} + occupiedCutouts={occupiedCutouts} /> ) } else if (currentStep.section === SECTIONS.PLACE_ADAPTER) { diff --git a/components/src/hooks/useSelectDeckLocation/index.tsx b/components/src/hooks/useSelectDeckLocation/index.tsx index 7bd9f9d974f..c7ccf53e6bf 100644 --- a/components/src/hooks/useSelectDeckLocation/index.tsx +++ b/components/src/hooks/useSelectDeckLocation/index.tsx @@ -1,15 +1,20 @@ import * as React from 'react' import isEqual from 'lodash/isEqual' - +import { useTranslation } from 'react-i18next' import { + CutoutConfig, FLEX_CUTOUT_BY_SLOT_ID, + FLEX_SINGLE_SLOT_BY_CUTOUT_ID, FLEX_ROBOT_TYPE, getDeckDefFromRobotType, getPositionFromSlotId, + getFixtureDisplayName, isAddressableAreaStandardSlot, OT2_ROBOT_TYPE, + AddressableArea, + CoordinateTuple, + CutoutFixtureId, } from '@opentrons/shared-data' - import { DeckFromLayers, LegacyDeckSlotLocation, @@ -75,19 +80,55 @@ interface DeckLocationSelectProps { selectedLocation: ModuleLocation theme?: DeckLocationSelectThemes setSelectedLocation?: (loc: ModuleLocation) => void - disabledLocations?: ModuleLocation[] + availableSlotNames?: string[] + occupiedCutouts?: CutoutConfig[] isThermocycler?: boolean + showTooltipOnDisabled?: boolean } + export function DeckLocationSelect({ deckDef, selectedLocation, setSelectedLocation, - disabledLocations = [], + availableSlotNames, + occupiedCutouts = [], theme = 'default', isThermocycler = false, + showTooltipOnDisabled = false, }: DeckLocationSelectProps): JSX.Element { const robotType = deckDef.robot.model + const { t } = useTranslation('module_wizard_flows') + + const [hoveredData, setHoveredData] = React.useState<{ + slot: AddressableArea + slotPosition: CoordinateTuple | null + isDisabled: boolean + disabledReason?: CutoutFixtureId | null + } | null>(null) + + const handleMouseEnter = ( + slot: AddressableArea, + slotPosition: CoordinateTuple | null, + isDisabled: boolean, + disabledReason?: CutoutFixtureId | null + ): void => { + if (isDisabled) { + setHoveredData({ + slot: slot, + slotPosition: slotPosition, + isDisabled: isDisabled, + disabledReason: disabledReason, + }) + } else { + setHoveredData(null) + } + } + + const handleMouseLeave = (): void => { + setHoveredData(null) + } + return ( { const slotLocation = { slotName: slot.id } - const isDisabled = disabledLocations.some( - l => - typeof l === 'object' && 'slotName' in l && l.slotName === slot.id - ) + const isDisabled = + availableSlotNames !== undefined + ? !availableSlotNames.some(slotName => slotName === slot.id) + : false + + const disabledReason = + occupiedCutouts.find( + cutout => + FLEX_SINGLE_SLOT_BY_CUTOUT_ID[cutout.cutoutId] === slot.id + )?.cutoutFixtureId ?? null const isSelected = isEqual(selectedLocation, slotLocation) let fill = theme === 'default' ? COLORS.purple35 : COLORS.grey35 if (isSelected) @@ -148,22 +195,33 @@ export function DeckLocationSelect({ return ( {robotType === FLEX_ROBOT_TYPE ? ( - - !isDisabled && - setSelectedLocation != null && - setSelectedLocation(slotLocation) - } - cursor={ - setSelectedLocation == null || isDisabled || isSelected - ? 'default' - : 'pointer' - } - deckDefinition={deckDef} - /> + <> + + !isDisabled && + setSelectedLocation != null && + setSelectedLocation(slotLocation) + } + cursor={ + setSelectedLocation == null || isDisabled || isSelected + ? 'default' + : 'pointer' + } + deckDefinition={deckDef} + onMouseEnter={() => + handleMouseEnter( + slot, + slotPosition, + isDisabled, + disabledReason + ) + } + onMouseLeave={handleMouseLeave} + /> + ) : ( ) : null} + {hoveredData != null && + hoveredData.isDisabled && + hoveredData.slotPosition != null && + showTooltipOnDisabled && ( + + + {hoveredData.disabledReason != null + ? t('location_occupied', { + fixture: getFixtureDisplayName( + hoveredData.disabledReason + ).toLowerCase(), + }) + : 'Slot unavailable'} + + + )} ) } diff --git a/shared-data/js/fixtures.ts b/shared-data/js/fixtures.ts index f0b906575d8..7e2f117bca8 100644 --- a/shared-data/js/fixtures.ts +++ b/shared-data/js/fixtures.ts @@ -50,6 +50,22 @@ export const FLEX_CUTOUT_BY_SLOT_ID: { [slotId: string]: CutoutId } = { D4: 'cutoutD3', } +// mapping of Flex single slot cutouts to deck slots +export const FLEX_SINGLE_SLOT_BY_CUTOUT_ID: { [CutoutId: string]: string } = { + cutoutA1: 'A1', + cutoutA2: 'A2', + cutoutA3: 'A3', + cutoutB1: 'B1', + cutoutB2: 'B2', + cutoutB3: 'B3', + cutoutC1: 'C1', + cutoutC2: 'C2', + cutoutC3: 'C3', + cutoutD1: 'D1', + cutoutD2: 'D2', + cutoutD3: 'D3', +} + // returns the position associated with a slot id export function getPositionFromSlotId( slotId: string,