Skip to content

Commit

Permalink
fix(app): Fix nested labware UI in Error Recovery (#16634)
Browse files Browse the repository at this point in the history
Closes RQA-3440

This PR fixes copy and deck map issues when addressing labware nested in other labware.

5eab60c - Split the util that gets the labware location and renders appropriate copy into two separate utils.

c753bc5 - Fixes copy issues. We recently refactored slot location display utils, and I just forgot to use the correct, new prop here and update relevant i18n strings.

caf9340 - Fixes deck map issues. Essentially, if there are multiple labware in the slot with the failed labware, only render the topmost labware. This is consistent with deck map behavior in other places in the app. Yes, it's a pain and annoying to have to do this outside of the deck map, but after discussion with other FE devs, it sounds like the deck map redesign will fundamentally change the way we render all objects on the deck, so I don't think it's worth the effort to improve deck map internals currently.
  • Loading branch information
mjhuff authored Oct 30, 2024
1 parent 094be6e commit 9a5f116
Show file tree
Hide file tree
Showing 9 changed files with 419 additions and 226 deletions.
4 changes: 2 additions & 2 deletions app/src/assets/localization/en/error_recovery.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@
"remove_any_attached_tips": "Remove any attached tips",
"replace_tips_and_select_loc_partial_tip": "Replace tips and select the last location used for partial tip pickup.",
"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}} in Slot {{slot}}",
"replace_with_new_tip_rack": "Replace with new tip rack in Slot {{slot}}",
"replace_used_tips_in_rack_location": "Replace used tips in rack location {{location}} in {{slot}}",
"replace_with_new_tip_rack": "Replace with new tip rack in {{slot}}",
"resume": "Resume",
"retry_dropping_tip": "Retry dropping tip",
"retry_now": "Retry now",
Expand Down
202 changes: 55 additions & 147 deletions app/src/local-resources/labware/utils/getLabwareDisplayLocation.ts
Original file line number Diff line number Diff line change
@@ -1,180 +1,88 @@
import {
getLabwareDefURI,
getLabwareDisplayName,
getModuleDisplayName,
getModuleType,
getOccludedSlotCountForModule,
} from '@opentrons/shared-data'

import {
getModuleModel,
getModuleDisplayLocation,
} from '/app/local-resources/modules'
import { getLabwareLocation } from './getLabwareLocation'

import type { TFunction } from 'i18next'
import type {
LabwareDefinition2,
LabwareLocation,
RobotType,
} from '@opentrons/shared-data'
import type { LoadedLabwares } from '/app/local-resources/labware'
import type { LoadedModules } from '/app/local-resources/modules'
LocationSlotOnlyParams,
LocationFullParams,
} from './getLabwareLocation'

interface LabwareDisplayLocationBaseParams {
location: LabwareLocation | null
loadedModules: LoadedModules
loadedLabwares: LoadedLabwares
robotType: RobotType
export interface DisplayLocationSlotOnlyParams extends LocationSlotOnlyParams {
t: TFunction
isOnDevice?: boolean
}

export interface LabwareDisplayLocationSlotOnly
extends LabwareDisplayLocationBaseParams {
detailLevel: 'slot-only'
}

export interface LabwareDisplayLocationFull
extends LabwareDisplayLocationBaseParams {
detailLevel?: 'full'
allRunDefs: LabwareDefinition2[]
export interface DisplayLocationFullParams extends LocationFullParams {
t: TFunction
isOnDevice?: boolean
}

export type LabwareDisplayLocationParams =
| LabwareDisplayLocationSlotOnly
| LabwareDisplayLocationFull
export type DisplayLocationParams =
| DisplayLocationSlotOnlyParams
| DisplayLocationFullParams

// detailLevel applies to nested labware. If 'full', return copy that includes the actual peripheral that nests the
// labware, ex, "in module XYZ in slot C1".
// If 'slot-only', return only the slot name, ex "in slot C1".
export function getLabwareDisplayLocation(
params: LabwareDisplayLocationParams
params: DisplayLocationParams
): string {
const {
loadedLabwares,
loadedModules,
location,
robotType,
t,
isOnDevice = false,
detailLevel = 'full',
} = params
const { t, isOnDevice = false } = params
const locationResult = getLabwareLocation(params)

if (location == null) {
console.error('Cannot get labware display location. No location provided.')
if (locationResult == null) {
return ''
} else if (location === 'offDeck') {
return t('off_deck')
} else if ('slotName' in location) {
return isOnDevice
? location.slotName
: t('slot', { slot_name: location.slotName })
} else if ('addressableAreaName' in location) {
return isOnDevice
? location.addressableAreaName
: t('slot', { slot_name: location.addressableAreaName })
} else if ('moduleId' in location) {
const moduleModel = getModuleModel(loadedModules, location.moduleId)
if (moduleModel == null) {
console.error('labware is located on an unknown module model')
return ''
}
const slotName = getModuleDisplayLocation(loadedModules, location.moduleId)
}

if (detailLevel === 'slot-only') {
return t('slot', { slot_name: slotName })
}
const { slotName, moduleModel, adapterName } = locationResult

return isOnDevice
? `${getModuleDisplayName(moduleModel)}, ${slotName}`
: t('module_in_slot', {
count: getOccludedSlotCountForModule(
getModuleType(moduleModel),
robotType
),
module: getModuleDisplayName(moduleModel),
slot_name: slotName,
})
} else if ('labwareId' in location) {
if (!Array.isArray(loadedLabwares)) {
console.error('Cannot get display location from loaded labwares object')
return ''
if (slotName === 'offDeck') {
return t('off_deck')
}
// Simple slot location
else if (moduleModel == null && adapterName == null) {
return isOnDevice ? slotName : t('slot', { slot_name: slotName })
}
// Module location without adapter
else if (moduleModel != null && adapterName == null) {
if (params.detailLevel === 'slot-only') {
return t('slot', { slot_name: slotName })
} else {
return isOnDevice
? `${getModuleDisplayName(moduleModel)}, ${slotName}`
: t('module_in_slot', {
count: getOccludedSlotCountForModule(
getModuleType(moduleModel),
params.robotType
),
module: getModuleDisplayName(moduleModel),
slot_name: slotName,
})
}
const adapter = loadedLabwares.find(lw => lw.id === location.labwareId)

if (adapter == null) {
console.error('labware is located on an unknown adapter')
return ''
} else if (detailLevel === 'slot-only') {
return getLabwareDisplayLocation({
...params,
location: adapter.location,
}
// Adapter locations
else if (adapterName != null) {
if (moduleModel == null) {
return t('adapter_in_slot', {
adapter: adapterName,
slot: slotName,
})
} else if (detailLevel === 'full') {
const { allRunDefs } = params as LabwareDisplayLocationFull
const adapterDef = allRunDefs.find(
def => getLabwareDefURI(def) === adapter?.definitionUri
)
const adapterDisplayName =
adapterDef != null ? getLabwareDisplayName(adapterDef) : ''

if (adapter.location === 'offDeck') {
return t('off_deck')
} else if (
'slotName' in adapter.location ||
'addressableAreaName' in adapter.location
) {
const slotName =
'slotName' in adapter.location
? adapter.location.slotName
: adapter.location.addressableAreaName
return t('adapter_in_slot', {
adapter: adapterDisplayName,
slot: slotName,
})
} else if ('moduleId' in adapter.location) {
const moduleIdUnderAdapter = adapter.location.moduleId

if (!Array.isArray(loadedModules)) {
console.error(
'Cannot get display location from loaded modules object'
)
return ''
}

const moduleModel = loadedModules.find(
module => module.id === moduleIdUnderAdapter
)?.model
if (moduleModel == null) {
console.error('labware is located on an adapter on an unknown module')
return ''
}
const slotName = getModuleDisplayLocation(
loadedModules,
adapter.location.moduleId
)

return t('adapter_in_mod_in_slot', {
count: getOccludedSlotCountForModule(
getModuleType(moduleModel),
robotType
),
module: getModuleDisplayName(moduleModel),
adapter: adapterDisplayName,
slot: slotName,
})
} else {
console.error(
'Unhandled adapter location for determining display location.'
)
return ''
}
} else {
console.error('Unhandled detail level for determining display location.')
return ''
return t('adapter_in_mod_in_slot', {
count: getOccludedSlotCountForModule(
getModuleType(moduleModel),
params.robotType
),
module: getModuleDisplayName(moduleModel),
adapter: adapterName,
slot: slotName,
})
}
} else {
console.error('display location could not be established: ', location)
return ''
}
}
147 changes: 147 additions & 0 deletions app/src/local-resources/labware/utils/getLabwareLocation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { getLabwareDefURI, getLabwareDisplayName } from '@opentrons/shared-data'

import {
getModuleDisplayLocation,
getModuleModel,
} from '/app/local-resources/modules'

import type {
LabwareDefinition2,
LabwareLocation,
ModuleModel,
RobotType,
} from '@opentrons/shared-data'
import type { LoadedLabwares } from '/app/local-resources/labware'
import type { LoadedModules } from '/app/local-resources/modules'

export interface LocationResult {
slotName: string
moduleModel?: ModuleModel
adapterName?: string
}

interface BaseParams {
location: LabwareLocation | null
loadedModules: LoadedModules
loadedLabwares: LoadedLabwares
robotType: RobotType
}

export interface LocationSlotOnlyParams extends BaseParams {
detailLevel: 'slot-only'
}

export interface LocationFullParams extends BaseParams {
allRunDefs: LabwareDefinition2[]
detailLevel?: 'full'
}

export type GetLabwareLocationParams =
| LocationSlotOnlyParams
| LocationFullParams

// detailLevel returns additional information about the module and adapter in the same location, if applicable.
// if 'slot-only', returns the underlying slot location.
export function getLabwareLocation(
params: GetLabwareLocationParams
): LocationResult | null {
const {
loadedLabwares,
loadedModules,
location,
detailLevel = 'full',
} = params

if (location == null) {
return null
} else if (location === 'offDeck') {
return { slotName: 'offDeck' }
} else if ('slotName' in location) {
return { slotName: location.slotName }
} else if ('addressableAreaName' in location) {
return { slotName: location.addressableAreaName }
} else if ('moduleId' in location) {
const moduleModel = getModuleModel(loadedModules, location.moduleId)
if (moduleModel == null) {
console.error('labware is located on an unknown module model')
return null
}
const slotName = getModuleDisplayLocation(loadedModules, location.moduleId)

return {
slotName,
moduleModel,
}
} else if ('labwareId' in location) {
if (!Array.isArray(loadedLabwares)) {
console.error('Cannot get location from loaded labwares object')
return null
}

const adapter = loadedLabwares.find(lw => lw.id === location.labwareId)

if (adapter == null) {
console.error('labware is located on an unknown adapter')
return null
} else if (detailLevel === 'slot-only') {
return getLabwareLocation({
...params,
location: adapter.location,
})
} else if (detailLevel === 'full') {
const { allRunDefs } = params as LocationFullParams
const adapterDef = allRunDefs.find(
def => getLabwareDefURI(def) === adapter?.definitionUri
)
const adapterName =
adapterDef != null ? getLabwareDisplayName(adapterDef) : ''

if (adapter.location === 'offDeck') {
return { slotName: 'offDeck', adapterName }
} else if (
'slotName' in adapter.location ||
'addressableAreaName' in adapter.location
) {
const slotName =
'slotName' in adapter.location
? adapter.location.slotName
: adapter.location.addressableAreaName
return { slotName, adapterName }
} else if ('moduleId' in adapter.location) {
const moduleIdUnderAdapter = adapter.location.moduleId

if (!Array.isArray(loadedModules)) {
console.error('Cannot get location from loaded modules object')
return null
}

const moduleModel = loadedModules.find(
module => module.id === moduleIdUnderAdapter
)?.model

if (moduleModel == null) {
console.error('labware is located on an adapter on an unknown module')
return null
}

const slotName = getModuleDisplayLocation(
loadedModules,
adapter.location.moduleId
)

return {
slotName,
moduleModel,
adapterName,
}
} else {
return null
}
} else {
console.error('Unhandled detailLevel.')
return null
}
} else {
return null
}
}
1 change: 1 addition & 0 deletions app/src/local-resources/labware/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from './getLabwareDefinitionsFromCommands'
export * from './getLabwareName'
export * from './getLoadedLabware'
export * from './getLabwareDisplayLocation'
export * from './getLabwareLocation'
Loading

0 comments on commit 9a5f116

Please sign in to comment.