Skip to content

Commit

Permalink
fix(app): mag block with staging area location conflict, handle no co…
Browse files Browse the repository at this point in the history
…nfigurable modules

Add special-case affordances for the magnetic block with staging area slot fixture in the protocol
setup for run experience on the ODD and desktop. Show different modals depending on the presence of
matching but configured modules in the ChooseModuleToConfigure conflict resolution experience on ODD
and desktop

Closes PLAT-298, Closes PLAT-293, Closes PLAT-294
  • Loading branch information
b-cooper committed Apr 29, 2024
1 parent 7159d37 commit 89a70b8
Show file tree
Hide file tree
Showing 16 changed files with 265 additions and 159 deletions.
2 changes: 2 additions & 0 deletions app/src/App/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ export function useCurrentRunRoute(): string | null {
enabled: currentRunId != null,
})

console.log('currentRunId')

const runStatus = runRecord?.data.status
const runActions = runRecord?.data.actions
if (runRecord == null || runStatus == null || runActions == null) return null
Expand Down
6 changes: 4 additions & 2 deletions app/src/assets/localization/en/protocol_setup.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"calibration_status": "calibration status",
"calibration": "Calibration",
"cancel_and_restart_to_edit": "Cancel the run and restart setup to edit",
"cancel_protocol_and_edit_deck_config": "Cancel protocol and edit deck configuration",
"exit_to_deck_configuration": "Exit to deck configuration",
"choose_enum": "Choose {{displayName}}",
"closing": "Closing...",
"complete_setup_before_proceeding": "complete setup before continuing run",
Expand Down Expand Up @@ -197,6 +197,7 @@
"pipette_offset_cal_description": "This measures a pipette’s X, Y and Z values in relation to the pipette mount and the deck. Pipette Offset Calibration relies on Deck Calibration and Tip Length Calibration. ",
"pipette_offset_cal": "Pipette Offset Calibration",
"placement": "Placement",
"plug_in_module_to_configure": "Plug in a {{module}} to add it to the slot",
"plug_in_required_module_plural": "Plug in and power up the required modules to continue",
"plug_in_required_module": "Plug in and power up the required module to continue",
"prepare_to_run": "Prepare to run",
Expand Down Expand Up @@ -250,7 +251,8 @@
"slot_number": "Slot Number",
"status": "Status",
"step": "STEP {{index}}",
"there_are_no_unconfigured_modules": "There are no un-configured {{module}} connected to the robot. Plug one in or remove an existing {{module}}, move it to the right place, and update the deck configuration.",
"there_are_no_unconfigured_modules": "No {{module}} is connected. Attach one and place it in {{slot}}",
"there_are_other_configured_modules": "A {{module}} is already configured in a different slot. Exit run setup and update your deck configuration to move to an already connected module. Or attach another {{module}} to continue setup.",
"tip_length_cal_description_bullet": "Perform Tip Length Calibration for each new tip type used on a pipette.",
"tip_length_cal_description": "This measures the Z distance between the bottom of the tip and the pipette’s nozzle. If you redo the tip length calibration for the tip you used to calibrate a pipette, you will also have to redo that Pipette Offset Calibration.",
"tip_length_cal_title": "Tip Length Calibration",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ import { useHistory } from 'react-router-dom'
import { useModulesQuery } from '@opentrons/react-api-client'
import {
ALIGN_CENTER,
BORDERS,
COLORS,
DIRECTION_COLUMN,
DIRECTION_ROW,
Flex,
PrimaryButton,
Icon,
SPACING,
SecondaryButton,
StyledText,
TEXT_ALIGN_CENTER,
TYPOGRAPHY,
} from '@opentrons/components'
import {
Expand All @@ -28,6 +32,7 @@ import { SmallButton } from '../../../../atoms/buttons'
import { useCloseCurrentRun } from '../../../ProtocolUpload/hooks'

import type { ModuleModel, DeckDefinition } from '@opentrons/shared-data'
import type { AttachedModule } from '@opentrons/api-client'

const EQUIPMENT_POLL_MS = 5000
interface ModuleFixtureOption {
Expand Down Expand Up @@ -58,19 +63,23 @@ export const ChooseModuleToConfigureModal = (
displaySlotName,
} = props
const { t } = useTranslation(['protocol_setup', 'shared'])
const history = useHistory()
const { closeCurrentRun } = useCloseCurrentRun()
const attachedModules =
useModulesQuery({ refetchInterval: EQUIPMENT_POLL_MS })?.data?.data ?? []
const deckConfig = useNotifyDeckConfigurationQuery()?.data ?? []
const unconfiguredModuleMatches =
attachedModules.filter(
attachedMod =>
attachedMod.moduleModel === requiredModuleModel &&
!deckConfig.some(
({ opentronsModuleSerialNumber }) =>
attachedMod.serialNumber === opentronsModuleSerialNumber
)
const [configuredModuleMatches, unconfiguredModuleMatches] =
attachedModules.reduce<[AttachedModule[], AttachedModule[]]>(
(acc, attachedMod) => {
if (attachedMod.moduleModel === requiredModuleModel) {
return deckConfig.some(
({ opentronsModuleSerialNumber }) =>
attachedMod.serialNumber === opentronsModuleSerialNumber
)
? [[...acc[0], attachedMod], acc[1]]
: [acc[0], [...acc[1], attachedMod]]
}
return acc
},
[[], []]
) ?? []

const connectedOptions: ModuleFixtureOption[] = unconfiguredModuleMatches.map(
Expand Down Expand Up @@ -103,31 +112,8 @@ export const ChooseModuleToConfigureModal = (
)
}
)
const handleCancelRun = (): void => {
closeCurrentRun()
}
const handleNavigateToDeviceDetails = (): void => {
history.push(`/devices/${robotName}`)
}
const emptyState = (
<Flex flexDirection={DIRECTION_COLUMN} gridGap={SPACING.spacing16}>
<StyledText as="p">
{t('there_are_no_unconfigured_modules', {
module: getModuleDisplayName(requiredModuleModel),
})}
</StyledText>
{isOnDevice ? (
<SmallButton
onClick={handleCancelRun}
buttonText={t('cancel_protocol_and_edit_deck_config')}
/>
) : (
<PrimaryButton onClick={handleNavigateToDeviceDetails}>
{t('update_deck_config')}
</PrimaryButton>
)}
</Flex>
)

const moduleDisplayName = getModuleDisplayName(requiredModuleModel)

const contents =
fixtureOptions.length > 0 ? (
Expand All @@ -138,7 +124,15 @@ export const ChooseModuleToConfigureModal = (
</Flex>
</Flex>
) : (
emptyState
<NoUnconfiguredModules
{...{
isOnDevice,
configuredModuleMatches,
moduleDisplayName,
displaySlotName,
robotName,
}}
/>
)

return createPortal(
Expand All @@ -153,11 +147,7 @@ export const ChooseModuleToConfigureModal = (
>
<Flex flexDirection={DIRECTION_COLUMN} gridGap={SPACING.spacing32}>
<Flex flexDirection={DIRECTION_COLUMN}>
<Flex
flexDirection={DIRECTION_COLUMN}
paddingTop={SPACING.spacing8}
gridGap={SPACING.spacing8}
>
<Flex flexDirection={DIRECTION_COLUMN} gridGap={SPACING.spacing8}>
{contents}
</Flex>
</Flex>
Expand Down Expand Up @@ -195,3 +185,86 @@ export const ChooseModuleToConfigureModal = (
getTopPortalEl()
)
}

interface NoUnconfiguredModulesProps {
moduleDisplayName: string
displaySlotName: string
configuredModuleMatches: AttachedModule[]
isOnDevice: boolean
robotName: string
}
function NoUnconfiguredModules(props: NoUnconfiguredModulesProps): JSX.Element {
const { moduleDisplayName, displaySlotName, isOnDevice, robotName } = props
const configuredModuleMatches = ['feer']
const { t } = useTranslation('protocol_setup')
const history = useHistory()
const { closeCurrentRun } = useCloseCurrentRun()
const handleCancelRun = (): void => {
closeCurrentRun()
}
const handleNavigateToDeviceDetails = (): void => {
history.push(`/devices/${robotName}`)
}
const exitButton = isOnDevice ? (
<SmallButton
onClick={handleCancelRun}
buttonType="secondary"
buttonText={t('exit_to_deck_configuration')}
/>
) : (
<SecondaryButton onClick={handleNavigateToDeviceDetails}>
{t('exit_to_deck_configuration')}
</SecondaryButton>
)

const loadingBlock = (
<Flex
paddingX={SPACING.spacing80}
paddingY={SPACING.spacing40}
gridGap={isOnDevice ? SPACING.spacing40 : SPACING.spacing10}
borderRadius={isOnDevice ? BORDERS.borderRadius12 : BORDERS.borderRadius8}
backgroundColor={COLORS.grey35}
flexDirection={DIRECTION_COLUMN}
alignItems={ALIGN_CENTER}
>
<Icon
size={isOnDevice ? '2rem' : '1.25rem'}
marginLeft={SPACING.spacing8}
name="ot-spinner"
spin
/>
<StyledText
as={isOnDevice ? 'h4' : 'p'}
color={COLORS.grey60}
textAlign={TEXT_ALIGN_CENTER}
>
{t('plug_in_module_to_configure', { module: moduleDisplayName })}
</StyledText>
</Flex>
)
return (
<Flex flexDirection={DIRECTION_COLUMN} gridGap={SPACING.spacing32}>
{configuredModuleMatches.length > 0 ? (
<>
<StyledText as="p">
{t('there_are_other_configured_modules', {
module: moduleDisplayName,
})}
</StyledText>
{loadingBlock}
{exitButton}
</>
) : (
<>
<StyledText as="p">
{t('there_are_no_unconfigured_modules', {
module: moduleDisplayName,
slot: displaySlotName,
})}
</StyledText>
{loadingBlock}
</>
)}
</Flex>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import {
TYPOGRAPHY,
} from '@opentrons/components'
import {
FLEX_MODULE_ADDRESSABLE_AREAS,
FLEX_ROBOT_TYPE,
FLEX_USB_MODULE_ADDRESSABLE_AREAS,
SINGLE_SLOT_FIXTURES,
Expand Down Expand Up @@ -51,11 +50,11 @@ export const SetupFixtureList = (props: SetupFixtureListProps): JSX.Element => {
return (
<>
{deckConfigCompatibility.map(cutoutConfigAndCompatibility => {
// filter out all fixtures that only provide usb module addressable areas
// filter out all fixtures that only provide usb module addressable areas
// (i.e. everything but MagBlockV1 and StagingAreaWithMagBlockV1)
// as they're handled in the Modules Table
return cutoutConfigAndCompatibility.requiredAddressableAreas.every(raa =>
FLEX_USB_MODULE_ADDRESSABLE_AREAS.includes(raa)
// as they're handled in the Modules Table
return cutoutConfigAndCompatibility.requiredAddressableAreas.every(
raa => FLEX_USB_MODULE_ADDRESSABLE_AREAS.includes(raa)
) ? null : (
<FixtureListItem
key={cutoutConfigAndCompatibility.cutoutId}
Expand Down Expand Up @@ -179,7 +178,10 @@ export function FixtureListItem({
}
/>
) : null}
<Flex flexDirection={DIRECTION_COLUMN} alignItems={ALIGN_FLEX_START}>
<Flex
flexDirection={DIRECTION_COLUMN}
alignItems={ALIGN_FLEX_START}
>
<StyledText
css={TYPOGRAPHY.pSemiBold}
marginLeft={SPACING.spacing20}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,10 @@ export function getFixtureImage(cutoutFixtureId: CutoutFixtureId): string {
return temperatureModule
} else if (cutoutFixtureId === MAGNETIC_BLOCK_V1_FIXTURE) {
return magneticBlockGen1
} else if (cutoutFixtureId === STAGING_AREA_SLOT_WITH_MAGNETIC_BLOCK_V1_FIXTURE) {
return stagingAreaMagneticBlockGen1
} else if (
cutoutFixtureId === STAGING_AREA_SLOT_WITH_MAGNETIC_BLOCK_V1_FIXTURE
) {
return stagingAreaMagneticBlockGen1
} else {
return 'Error: unknown fixture'
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,7 @@ export function getAddressableAreaFromConfig(
providedAddressableAreas.some(aa =>
WASTE_CHUTE_ADDRESSABLE_AREAS.includes(aa)
)
)
{
) {
// match number of channels to provided waste chute addressable area
if (
pipetteChannels === 1 &&
Expand Down
25 changes: 12 additions & 13 deletions app/src/organisms/ProtocolDetails/RobotConfigurationDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -179,19 +179,18 @@ export const RobotConfigurationDetails = (
label={getCutoutDisplayName(fixture.cutoutId)}
item={
<>
{MAGNETIC_BLOCK_FIXTURES.includes(fixture.cutoutFixtureId)
? (
<ModuleIcon
key={index}
moduleType={MAGNETIC_BLOCK_TYPE}
marginRight={SPACING.spacing4}
alignSelf={ALIGN_CENTER}
color={COLORS.grey50}
height={SIZE_1}
minWidth={SIZE_1}
minHeight={SIZE_1}
/>
) : null}
{MAGNETIC_BLOCK_FIXTURES.includes(fixture.cutoutFixtureId) ? (
<ModuleIcon
key={index}
moduleType={MAGNETIC_BLOCK_TYPE}
marginRight={SPACING.spacing4}
alignSelf={ALIGN_CENTER}
color={COLORS.grey50}
height={SIZE_1}
minWidth={SIZE_1}
minHeight={SIZE_1}
/>
) : null}
<StyledText as="p">
{getFixtureDisplayName(fixture.cutoutFixtureId)}
</StyledText>
Expand Down
Loading

0 comments on commit 89a70b8

Please sign in to comment.