diff --git a/app/src/App/hooks.ts b/app/src/App/hooks.ts index fa500fb19f2..48c471fa320 100644 --- a/app/src/App/hooks.ts +++ b/app/src/App/hooks.ts @@ -1,4 +1,4 @@ -import { useCallback, useContext, useEffect, useRef } from 'react' +import { useCallback, useContext, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useQueryClient } from 'react-query' import { useDispatch } from 'react-redux' @@ -15,6 +15,10 @@ import { useCreateLiveCommandMutation, useHost, } from '@opentrons/react-api-client' +import { + ABSORBANCE_READER_TYPE, + FLEX_STACKER_MODULE_TYPE, +} from '@opentrons/shared-data' import { useToaster } from '/app/organisms/ToasterOven' import { checkShellUpdate } from '/app/redux/shell' @@ -26,7 +30,10 @@ import { useCurrentRunId } from '../resources/runs' import { SharedScrollRefContext } from './ODDProviders/ScrollRefProvider' import type { AttachedModule } from '@opentrons/api-client' -import type { SetStatusBarCreateCommand } from '@opentrons/shared-data' +import type { + ModuleType, + SetStatusBarCreateCommand, +} from '@opentrons/shared-data' import type { Dispatch } from '/app/redux/types' const UPDATE_RECHECK_INTERVAL_MS = 60000 @@ -128,7 +135,14 @@ export function useProtocolReceiptToast(): void { }, [protocolIds]) } -export function useGetNewModules(): AttachedModule[] { +const MODULES_NOT_REQUIRING_PIPETTE_FOR_SETUP: ModuleType[] = [ + ABSORBANCE_READER_TYPE, + FLEX_STACKER_MODULE_TYPE, +] + +const MODULES_NOT_REQUIRING_CALIBRATION = MODULES_NOT_REQUIRING_PIPETTE_FOR_SETUP + +export function useGetModulesNeedingSetup(): AttachedModule[] { const attachedModules = useAttachedModules({ refetchInterval: ATTACHED_MODULE_POLL_MS, @@ -141,33 +155,44 @@ export function useGetNewModules(): AttachedModule[] { const modulesInDeckConfig = deckConfig ?.filter(c => c.opentronsModuleSerialNumber) .map(m => m.opentronsModuleSerialNumber) - const newModules = attachedModules.filter( + return attachedModules.filter( m => - m.moduleOffset === undefined && - !modulesInDeckConfig.includes(m.serialNumber) + !modulesInDeckConfig.includes(m.serialNumber) || + (!MODULES_NOT_REQUIRING_CALIBRATION.includes(m.moduleType) && + m.moduleOffset === undefined) ) - return newModules } return [] } +export function useGetModulesNeedingSetupThatCanCurrentlyBeSetUp(): AttachedModule[] { + const modulesRequiringSetup = useGetModulesNeedingSetup() + const attachedPipettes = useAttachedPipettes(modulesRequiringSetup.length > 0) + return modulesRequiringSetup.filter( + m => + MODULES_NOT_REQUIRING_PIPETTE_FOR_SETUP.includes(m.moduleType) || + attachedPipettes.left != null || + attachedPipettes.right != null + ) +} + export function useModuleAttachedToast( launchModuleSetupCallback: () => void ): void { - const newModules = useGetNewModules() + const currentlySetuppableModules = useGetModulesNeedingSetupThatCanCurrentlyBeSetUp() + const currentRunId = useCurrentRunId({ refetchInterval: CURRENT_RUN_POLL }) - const attachedPipettes = useAttachedPipettes(newModules.length > 0) const { t, i18n } = useTranslation(['module_wizard_flows', 'shared']) const { makeToast } = useToaster() - const moduleSerials = newModules.map(m => m.serialNumber) + const moduleSerials = currentlySetuppableModules.map(m => m.serialNumber) const moduleSerialsRef = useRef(moduleSerials) const runInProgress = currentRunId != null + const [firstRun, setFirstRun] = useState(true) + useEffect(() => { const newModuleSerials = difference(moduleSerials, moduleSerialsRef.current) - const hasPipette = - attachedPipettes.left != null || attachedPipettes.right != null - if (!runInProgress && hasPipette && newModuleSerials.length > 0) { + if (!runInProgress && newModuleSerials.length > 0) { makeToast(t('module_added') as string, 'info', { buttonText: i18n.format(t('shared:close'), 'capitalize'), linkText: t('module_added_link'), @@ -177,9 +202,10 @@ export function useModuleAttachedToast( }) } moduleSerialsRef.current = moduleSerials + setFirstRun(false) // dont want this hook to rerun when other deps change // eslint-disable-next-line react-hooks/exhaustive-deps - }, [moduleSerials, runInProgress]) + }, [moduleSerials, runInProgress, firstRun]) } export function useScrollRef(): { diff --git a/app/src/assets/localization/en/module_wizard_flows.json b/app/src/assets/localization/en/module_wizard_flows.json index a707f6328d5..3895babd4af 100644 --- a/app/src/assets/localization/en/module_wizard_flows.json +++ b/app/src/assets/localization/en/module_wizard_flows.json @@ -15,6 +15,7 @@ "close_doors_description": "The robot needs to safely move to its home location before you can set the labware shuttle onto the track.", "close_stacker_doors": "Close robot door and all stacker doors", "complete_calibration": "Complete calibration", + "connect_a_pipette_to_set_up_more_modules": "Connect a pipette to set up more modules.", "confirm_location": "Confirm location", "confirm_placement": "Confirm placement", "door_circuit_error": "Robot is open", diff --git a/app/src/molecules/InterventionModal/ModalContentOneColSimpleButtons.tsx b/app/src/molecules/InterventionModal/ModalContentOneColSimpleButtons.tsx index 2dcd6ca1521..edb60bf6f08 100644 --- a/app/src/molecules/InterventionModal/ModalContentOneColSimpleButtons.tsx +++ b/app/src/molecules/InterventionModal/ModalContentOneColSimpleButtons.tsx @@ -24,6 +24,8 @@ export interface ModalContentOneColSimpleButtonsProps { buttons: ButtonProps[] onSelect?: ChangeEventHandler initialSelected?: string + subText?: string + scroll?: boolean } export function ModalContentOneColSimpleButtons( @@ -33,16 +35,26 @@ export function ModalContentOneColSimpleButtons( props.initialSelected ?? null ) return ( - - - + + + + {props.headline} + + + - {props.headline} - - {props.buttons.map((buttonProps, idx) => ( ))} + {props.subText != null ? ( + + {props.subText} + + ) : null} diff --git a/app/src/molecules/UpdateBanner/__tests__/UpdateBanner.test.tsx b/app/src/molecules/UpdateBanner/__tests__/UpdateBanner.test.tsx index cf80182754b..4db745f0326 100644 --- a/app/src/molecules/UpdateBanner/__tests__/UpdateBanner.test.tsx +++ b/app/src/molecules/UpdateBanner/__tests__/UpdateBanner.test.tsx @@ -123,7 +123,7 @@ describe('Module Update Banner', () => { expect(screen.queryByText('Setup module')).not.toBeInTheDocument() }) - it('should not render module setup banner if pipette calibration is required', () => { + it('should render module setup banner even if pipette calibration is required', () => { props = { ...props, updateType: 'setup', @@ -135,7 +135,7 @@ describe('Module Update Banner', () => { 'ModuleCard_calibration_update_banner_test_number' ) ).not.toBeInTheDocument() - expect(screen.queryByText('Setup module')).not.toBeInTheDocument() + screen.getByText('Setup module') }) it('should not render a module setup link if pipette firmware update is required', () => { diff --git a/app/src/molecules/UpdateBanner/index.tsx b/app/src/molecules/UpdateBanner/index.tsx index 7d559b68afa..015b5d82ca7 100644 --- a/app/src/molecules/UpdateBanner/index.tsx +++ b/app/src/molecules/UpdateBanner/index.tsx @@ -51,14 +51,17 @@ export const UpdateBanner = ({ return t('module_calibration_required') } - const canProceed = - updateType === 'firmware' - ? true - : !isEstopNotDisengaged && - !isTooHot && - !attachPipetteRequired && - !calibratePipetteRequired && - !updatePipetteFWRequired + const proceedChecks = { + firmware: () => true, + calibration: () => + !isEstopNotDisengaged && + !isTooHot && + !attachPipetteRequired && + !calibratePipetteRequired && + !updatePipetteFWRequired, + setup: () => true, + } + const canProceed = proceedChecks[updateType]() const getMessage = (): string => { switch (updateType) { diff --git a/app/src/organisms/ModuleWizardFlows/AttachProbe.tsx b/app/src/organisms/ModuleWizardFlows/AttachProbe.tsx index be3718377bf..c26a2d8626e 100644 --- a/app/src/organisms/ModuleWizardFlows/AttachProbe.tsx +++ b/app/src/organisms/ModuleWizardFlows/AttachProbe.tsx @@ -21,9 +21,9 @@ import { SimpleWizardInProgressBody } from '/app/molecules/SimpleWizardBody' import { getFixtureIdByCutoutId } from './getFixtureIdByCutoutId' import type { CreateCommand, DeckConfiguration } from '@opentrons/shared-data' -import type { ModuleSetupWizardStepProps } from './types' +import type { ModuleSetupWizardRequiresPipetteStepProps } from './types' -interface AttachProbeProps extends ModuleSetupWizardStepProps { +interface AttachProbeProps extends ModuleSetupWizardRequiresPipetteStepProps { adapterId: string | null deckConfig: DeckConfiguration } diff --git a/app/src/organisms/ModuleWizardFlows/BeforeBeginning.tsx b/app/src/organisms/ModuleWizardFlows/BeforeBeginning.tsx index 2d297e9d05f..1a3fe3dd65e 100644 --- a/app/src/organisms/ModuleWizardFlows/BeforeBeginning.tsx +++ b/app/src/organisms/ModuleWizardFlows/BeforeBeginning.tsx @@ -12,7 +12,7 @@ import { GenericWizardTile } from '/app/molecules/GenericWizardTile' import { WizardRequiredEquipmentList } from '/app/molecules/WizardRequiredEquipmentList' import type { AttachedModule } from '@opentrons/api-client' -import type { ModuleSetupWizardStepProps } from './types' +import type { ModuleSetupWizardMaybePipetteStepProps } from './types' interface EqipmentItem { loadName: string @@ -20,7 +20,7 @@ interface EqipmentItem { subtitle?: string } -interface BeforeBeginningProps extends ModuleSetupWizardStepProps {} +interface BeforeBeginningProps extends ModuleSetupWizardMaybePipetteStepProps {} export function BeforeBeginning(props: BeforeBeginningProps): JSX.Element { const { proceed, attachedModule, setErrorMessage } = props diff --git a/app/src/organisms/ModuleWizardFlows/CheckStackerInstall.tsx b/app/src/organisms/ModuleWizardFlows/CheckStackerInstall.tsx index 4cc8a140605..4aa02a094e4 100644 --- a/app/src/organisms/ModuleWizardFlows/CheckStackerInstall.tsx +++ b/app/src/organisms/ModuleWizardFlows/CheckStackerInstall.tsx @@ -11,9 +11,10 @@ import { useSendIdentifyStacker } from './hooks' import type { AttachedModule } from '@opentrons/api-client' import type { DeckConfiguration } from '@opentrons/shared-data' -import type { ModuleSetupWizardStepProps } from './types' +import type { ModuleSetupWizardMaybePipetteStepProps } from './types' -interface CheckStackerInstallProps extends ModuleSetupWizardStepProps { +interface CheckStackerInstallProps + extends ModuleSetupWizardMaybePipetteStepProps { deckConfig: DeckConfiguration attachedModules: AttachedModule[] doorOpenStatus: boolean diff --git a/app/src/organisms/ModuleWizardFlows/CloseStackerDoor.tsx b/app/src/organisms/ModuleWizardFlows/CloseStackerDoor.tsx index 7036e57b045..5c2ea2db195 100644 --- a/app/src/organisms/ModuleWizardFlows/CloseStackerDoor.tsx +++ b/app/src/organisms/ModuleWizardFlows/CloseStackerDoor.tsx @@ -13,9 +13,9 @@ import { } from '/app/molecules/SimpleWizardBody' import type { CreateCommand, DeckConfiguration } from '@opentrons/shared-data' -import type { ModuleSetupWizardStepProps } from './types' +import type { ModuleSetupWizardMaybePipetteStepProps } from './types' -interface CloseDoorProps extends ModuleSetupWizardStepProps { +interface CloseDoorProps extends ModuleSetupWizardMaybePipetteStepProps { deckConfig: DeckConfiguration } diff --git a/app/src/organisms/ModuleWizardFlows/DetachProbe.tsx b/app/src/organisms/ModuleWizardFlows/DetachProbe.tsx index af4909e0df9..2b57a78afde 100644 --- a/app/src/organisms/ModuleWizardFlows/DetachProbe.tsx +++ b/app/src/organisms/ModuleWizardFlows/DetachProbe.tsx @@ -15,7 +15,7 @@ import detachProbe8 from '/app/assets/videos/pipette-wizard-flows/Pipette_Detach import detachProbe96 from '/app/assets/videos/pipette-wizard-flows/Pipette_Detach_Probe_96.webm' import { GenericWizardTile } from '/app/molecules/GenericWizardTile' -import type { ModuleSetupWizardStepProps } from './types' +import type { ModuleSetupWizardRequiresPipetteStepProps } from './types' const BODY_STYLE = css` ${TYPOGRAPHY.pRegular}; @@ -26,7 +26,9 @@ const BODY_STYLE = css` } ` -export function DetachProbe(props: ModuleSetupWizardStepProps): JSX.Element { +export function DetachProbe( + props: ModuleSetupWizardRequiresPipetteStepProps +): JSX.Element { const { attachedPipette, proceed, goBack } = props const { t, i18n } = useTranslation('module_wizard_flows') diff --git a/app/src/organisms/ModuleWizardFlows/InstallShuttle.tsx b/app/src/organisms/ModuleWizardFlows/InstallShuttle.tsx index cb41e681d16..4f2a81de527 100644 --- a/app/src/organisms/ModuleWizardFlows/InstallShuttle.tsx +++ b/app/src/organisms/ModuleWizardFlows/InstallShuttle.tsx @@ -22,9 +22,9 @@ import { SimpleWizardBody } from '/app/molecules/SimpleWizardBody' import type { AttachedModule } from '@opentrons/api-client' import type { DeckConfiguration } from '@opentrons/shared-data' -import type { ModuleSetupWizardStepProps } from './types' +import type { ModuleSetupWizardMaybePipetteStepProps } from './types' -interface InstallShuttleProps extends ModuleSetupWizardStepProps { +interface InstallShuttleProps extends ModuleSetupWizardMaybePipetteStepProps { deckConfig: DeckConfiguration attachedModules: AttachedModule[] } diff --git a/app/src/organisms/ModuleWizardFlows/PlaceAdapter.tsx b/app/src/organisms/ModuleWizardFlows/PlaceAdapter.tsx index b1766bdc748..71d0b9a4572 100644 --- a/app/src/organisms/ModuleWizardFlows/PlaceAdapter.tsx +++ b/app/src/organisms/ModuleWizardFlows/PlaceAdapter.tsx @@ -33,9 +33,9 @@ import { SimpleWizardInProgressBody } from '/app/molecules/SimpleWizardBody' import { LEFT_SLOTS } from './constants' import type { CreateCommand, DeckConfiguration } from '@opentrons/shared-data' -import type { ModuleSetupWizardStepProps } from './types' +import type { ModuleSetupWizardRequiresPipetteStepProps } from './types' -interface PlaceAdapterProps extends ModuleSetupWizardStepProps { +interface PlaceAdapterProps extends ModuleSetupWizardRequiresPipetteStepProps { deckConfig: DeckConfiguration setCreatedAdapterId: (adapterId: string) => void } diff --git a/app/src/organisms/ModuleWizardFlows/SelectLocation.tsx b/app/src/organisms/ModuleWizardFlows/SelectLocation.tsx index 7a9cce8ecb3..0df08f8af5e 100644 --- a/app/src/organisms/ModuleWizardFlows/SelectLocation.tsx +++ b/app/src/organisms/ModuleWizardFlows/SelectLocation.tsx @@ -42,7 +42,7 @@ import type { CutoutId, DeckConfiguration, } from '@opentrons/shared-data' -import type { ModuleSetupWizardStepProps } from './types' +import type { ModuleSetupWizardMaybePipetteStepProps } from './types' export const BODY_STYLE = css` ${TYPOGRAPHY.pRegular}; @@ -52,7 +52,8 @@ export const BODY_STYLE = css` line-height: 1.75rem; } ` -export interface SelectLocationProps extends ModuleSetupWizardStepProps { +export interface SelectLocationProps + extends ModuleSetupWizardMaybePipetteStepProps { deckConfig: DeckConfiguration createMaintenanceRun: CreateMaintenanceRunType isLoadedInRun: boolean diff --git a/app/src/organisms/ModuleWizardFlows/SelectModule.tsx b/app/src/organisms/ModuleWizardFlows/SelectModule.tsx index 038913e0350..5177dce7be0 100644 --- a/app/src/organisms/ModuleWizardFlows/SelectModule.tsx +++ b/app/src/organisms/ModuleWizardFlows/SelectModule.tsx @@ -8,13 +8,17 @@ import { Flex, JUSTIFY_FLEX_END, JUSTIFY_SPACE_BETWEEN, + OVERFLOW_AUTO, PrimaryButton, RESPONSIVENESS, SPACING, } from '@opentrons/components' import { getModuleDisplayName } from '@opentrons/shared-data' -import { useGetNewModules } from '/app/App/hooks' +import { + useGetModulesNeedingSetup, + useGetModulesNeedingSetupThatCanCurrentlyBeSetUp, +} from '/app/App/hooks' import { SmallButton } from '/app/atoms/buttons' import { i18n } from '/app/i18n' import { useModuleUSBPort } from '/app/local-resources/modules' @@ -42,7 +46,7 @@ interface ModuleNameAndPort { port: string } -export function SelectModule(props: SelectModuleProps): JSX.Element { +export function SelectModule(props: SelectModuleProps): JSX.Element | null { const { buildFlowForSelectedModule, isOnDevice, @@ -54,13 +58,23 @@ export function SelectModule(props: SelectModuleProps): JSX.Element { const { t } = useTranslation('module_wizard_flows') const { parseModuleUSBPort } = useModuleUSBPort() - const availableModules = useGetNewModules() + // Every module that needs setup (isn't calibrated, isn't in deck config) that also + // CAN be set up with the current robot configuration (pipettes or not pipettes) + const allSetupable = useGetModulesNeedingSetupThatCanCurrentlyBeSetUp() + // Every module that needs setup, but not all are guaranteed to be able to be set up + // right now (e.g. because they need calibration but we don't have a pipette) + const allNeedingSetup = useGetModulesNeedingSetup() const newModules = - attachedModuleOnLaunch !== null - ? [attachedModuleOnLaunch] - : availableModules - - const isSingleModule = newModules.length === 1 + attachedModuleOnLaunch == null ? allSetupable : [attachedModuleOnLaunch] + // if there are more modules that need setup than modules that can be set up, then + // it follows that some modules need setup but cannot be set up. in that case we want + // a warning + const hasUnsetupabbleModules = allNeedingSetup.length > allSetupable.length + // And our special short-circuit flows where we never show a menu if there's only one + // entry should be avoided if we have that warning + const isSingleModule = newModules.length === 1 && !hasUnsetupabbleModules + // Unless, of course, we're being invoked by a caller giving us a specific module + const shortCircuitFlow = attachedModuleOnLaunch != null || isSingleModule const sendIdentifyStacker = useSendIdentifyStacker() const getModuleNameAndPort = (module: AttachedModule): ModuleNameAndPort => { @@ -71,11 +85,11 @@ export function SelectModule(props: SelectModuleProps): JSX.Element { // Handler for when there is one module useEffect(() => { - if (isSingleModule) { + if (shortCircuitFlow) { setSelectedModule(newModules[0]) sendIdentifyStacker(newModules[0], true) } - }, [isSingleModule]) + }, [shortCircuitFlow]) // Handler for when there are multiple modules. const handleModuleSelected = (serialNumber: string): void => { @@ -114,8 +128,9 @@ export function SelectModule(props: SelectModuleProps): JSX.Element { padding-left: ${SPACING.spacing32}; } ` - - if (isSingleModule && selectedModule != null) { + if (newModules.length === 0) { + return null + } else if (shortCircuitFlow && selectedModule != null) { const m = getModuleNameAndPort(selectedModule) return ( { handleModuleSelected(event.target.value) }} + subText={ + hasUnsetupabbleModules + ? t('connect_a_pipette_to_set_up_more_modules') + : null + } + scroll={true} /> diff --git a/app/src/organisms/ModuleWizardFlows/Success.tsx b/app/src/organisms/ModuleWizardFlows/Success.tsx index db5c6edfedc..604e2aa265d 100644 --- a/app/src/organisms/ModuleWizardFlows/Success.tsx +++ b/app/src/organisms/ModuleWizardFlows/Success.tsx @@ -14,14 +14,14 @@ import { } from '@opentrons/components' import { getModuleDisplayName } from '@opentrons/shared-data' -import { useGetNewModules } from '/app/App/hooks' +import { useGetModulesNeedingSetupThatCanCurrentlyBeSetUp } from '/app/App/hooks' import { SmallButton } from '/app/atoms/buttons' import { SimpleWizardBody } from '/app/molecules/SimpleWizardBody' import { useSendIdentifyStacker } from './hooks' import type { AttachedModule } from '@opentrons/api-client' -import type { ModuleSetupWizardStepProps } from './types' +import type { ModuleSetupWizardMaybePipetteStepProps } from './types' export const BODY_STYLE = css` ${TYPOGRAPHY.pRegular}; @@ -32,7 +32,7 @@ export const BODY_STYLE = css` } ` -interface SuccessProps extends ModuleSetupWizardStepProps { +interface SuccessProps extends ModuleSetupWizardMaybePipetteStepProps { setSelectedModule: (module: AttachedModule | null) => void attachedModuleOnLaunch?: AttachedModule | null } @@ -50,7 +50,7 @@ export function Success(props: SuccessProps): JSX.Element { const { t } = useTranslation('module_wizard_flows') const sendIdentifyStacker = useSendIdentifyStacker() const moduleDisplayName = getModuleDisplayName(attachedModule.moduleModel) - const newModules = useGetNewModules() + const newModules = useGetModulesNeedingSetupThatCanCurrentlyBeSetUp() const handleOnClick = (restart: boolean): void => { if (restart) { diff --git a/app/src/organisms/ModuleWizardFlows/UpdateFirmware.tsx b/app/src/organisms/ModuleWizardFlows/UpdateFirmware.tsx index fdcf425362c..7d5cb8c3ef3 100644 --- a/app/src/organisms/ModuleWizardFlows/UpdateFirmware.tsx +++ b/app/src/organisms/ModuleWizardFlows/UpdateFirmware.tsx @@ -24,12 +24,12 @@ import { useSendIdentifyStacker } from './hooks' import type { AttachedModule } from '@opentrons/api-client' import type { Dispatch, State } from '/app/redux/types' -import type { ModuleSetupWizardStepProps } from './types' +import type { ModuleSetupWizardMaybePipetteStepProps } from './types' const EQUIPMENT_POLL_MS = 3000 const MODULE_TIMEOUT_MS = 60000 const NO_UPDATE_FOUND_TIMEOUT_MS = 2000 -interface UpdateFirmwareProps extends ModuleSetupWizardStepProps { +interface UpdateFirmwareProps extends ModuleSetupWizardMaybePipetteStepProps { robotName: string patchModuleAfterUpdate: (module: AttachedModule) => void } diff --git a/app/src/organisms/ModuleWizardFlows/index.tsx b/app/src/organisms/ModuleWizardFlows/index.tsx index 4e1e950e753..eff8177fbc3 100644 --- a/app/src/organisms/ModuleWizardFlows/index.tsx +++ b/app/src/organisms/ModuleWizardFlows/index.tsx @@ -89,7 +89,6 @@ export function ModuleWizardFlows( const doorStatus = useIsDoorOpen(robotName).isDoorOpen - if (wizardFlowBaseProps.attachedPipette == null) return null if (showLaunchSetup || wizardFlowBaseProps.attachedModule == null) { return ( ) case SECTIONS.PLACE_ADAPTER: - return ( + return wizardFlowBaseProps.attachedPipette == null ? null : ( ) case SECTIONS.ATTACH_PROBE: - return ( + return wizardFlowBaseProps.attachedPipette == null ? null : ( ) case SECTIONS.DETACH_PROBE: - return ( + return wizardFlowBaseProps.attachedPipette == null ? null : ( void goBack: () => void restartSetup: () => void @@ -60,12 +60,25 @@ export interface ModuleSetupWizardStepProps { setIsModuleUpdating: (updating: boolean) => void maintenanceRunId: string | null attachedModule: AttachedModule - attachedPipette: PipetteInformation errorMessage: string | null setErrorMessage: (message: string | null) => void isOnDevice: boolean } +export interface ModuleSetupWizardRequiresPipetteStepProps + extends ModuleSetupWizardBaseStepProps { + attachedPipette: PipetteInformation +} + +export interface ModuleSetupWizardMaybePipetteStepProps + extends ModuleSetupWizardBaseStepProps { + attachedPipette: PipetteInformation | null +} + +export type ModuleSetupWizardStepProps = + | ModuleSetupWizardMaybePipetteStepProps + | ModuleSetupWizardRequiresPipetteStepProps + export type ModuleWizardFlow = typeof FLOWS.SETUP export interface BeforeBeginningStep {