Skip to content

Commit

Permalink
refactor(app): add and use deck map component in interventionmodal (#…
Browse files Browse the repository at this point in the history
…15570)

Adds DeckMapContent to intervention modal. This is a small wrapper
around one of two different kinds of deck map:
- A labware-rendering map that is also capable of rendering highlights
around some labware, used in some selection screens in error recovery
- A deck config style block map that supports clicking on some of the
blocks to select them, used in some selection screens in the drop tip
wizard (and thus in error recovery)

Stories for the deckmap are [on
storybook](https://s3-us-west-2.amazonaws.com/opentrons-components/exec-501-deckmap-content/index.html?path=/docs/app-molecules-interventionmodal-deckmapcontent--docs),
where it's rendered side-by-side with the standin since it is only
destined to be used in a two-column layout.

Extra fun changes:
- Use the ODD text size args for _all_ slot labels in `BaseDeck`,
because otherwise they are completely unreadable once the deckmap gets
to small. I think this looks a lot better everywhere, and is actually
readable when the deckmap is small, so let's go with it; may want to
come back and make these something more specific on desktop
- Error recovery has a `RecoveryMap`, a large data structure that is
core to the wizard flow and defines how users move between steps and
screens. Error recovery also had a `RecoveryMap`, which was a component
for rendering a deckmap, and a `useRecoveryMapUtils`, a hook for getting
that component's arguments. Now it uses the `DeckMapContent` component
above directly, and `useDeckMapUtils`, a hook for getting that
component's arguments
- There's something about the deck config style map that isn't quite
right and I think that it's because of this:
https://opentrons.atlassian.net/browse/EXEC-513

Review requests:
- i'm pretty sure what I did to drop tip will work but it might be
reasonably called somewhat gross. this is all somewhat gross though, so
c'est la vie
- the deckmap slot labels do not render right in firefox at these sizes.
if you're looking at storybook, you're going to have to use chrome
- fix an issue that was preventing ER wizard from being displayed on ODD
Testing, todo:
- [ ] error recovery
- [ ] drop tip in error recovery
- [ ] drop tip outside of error recovery


Closes EXEC-501
  • Loading branch information
sfoster1 committed Jul 5, 2024
1 parent f756e90 commit 1578adf
Show file tree
Hide file tree
Showing 20 changed files with 684 additions and 334 deletions.
157 changes: 157 additions & 0 deletions app/src/molecules/InterventionModal/DeckMapContent.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import * as React from 'react'

import { css } from 'styled-components'
import { DeckMapContent } from '.'
import { Box, RESPONSIVENESS, BORDERS } from '@opentrons/components'
import type { Meta, StoryObj } from '@storybook/react'
import {
FLEX_ROBOT_TYPE,
OT2_ROBOT_TYPE,
fixture96Plate,
fixtureTiprack1000ul,
HEATERSHAKER_MODULE_V1,
MAGNETIC_BLOCK_V1,
TEMPERATURE_MODULE_V2,
THERMOCYCLER_MODULE_V2,
} from '@opentrons/shared-data'
import type { ModuleLocation, LabwareDefinition2 } from '@opentrons/shared-data'
import {
EXTENDED_DECK_CONFIG_FIXTURE,
STANDARD_SLOT_DECK_CONFIG_FIXTURE,
WASTE_CHUTE_DECK_CONFIG_FIXTURE,
} from './__fixtures__'
import { TwoColumn } from './TwoColumn'
import { StandInContent } from './story-utils/StandIn'

const DEFAULT_MODULES_ON_DECK = [
{
moduleLocation: { slotName: 'B1' },
moduleModel: THERMOCYCLER_MODULE_V2,
nestedLabwareDef: fixture96Plate as LabwareDefinition2,
innerProps: { lidMotorState: 'open' },
},
{
moduleLocation: { slotName: 'D1' },
moduleModel: TEMPERATURE_MODULE_V2,
nestedLabwareDef: fixture96Plate as LabwareDefinition2,
},
{
moduleLocation: { slotName: 'B3' },
moduleModel: HEATERSHAKER_MODULE_V1,
nestedLabwareDef: fixture96Plate as LabwareDefinition2,
},
{
moduleLocation: { slotName: 'D2' },
moduleModel: MAGNETIC_BLOCK_V1,
nestedLabwareDef: fixture96Plate as LabwareDefinition2,
},
]

const DEFAULT_LABWARE_ON_DECK = [
{
labwareLocation: { slotName: 'C2' },
definition: fixture96Plate as LabwareDefinition2,
},
{
labwareLocation: { slotName: 'C3' },
definition: fixtureTiprack1000ul as LabwareDefinition2,
},
]

const CONSOLE_LOG_ON_SELECT = (location: ModuleLocation): void => {
console.log(`selected location is ${location?.slotName}`)
}

const meta: Meta<React.ComponentProps<typeof DeckMapContent>> = {
title: 'App/Molecules/InterventionModal/DeckMapContent',
component: DeckMapContent,
argTypes: {
robotType: {
control: {
type: 'select',
},
options: [OT2_ROBOT_TYPE, FLEX_ROBOT_TYPE],
default: FLEX_ROBOT_TYPE,
},
kind: {
control: {
type: 'select',
},
options: ['intervention', 'deck-config'],
},
setSelectedLocation: {
control: {
type: 'select',
},
options: ['print-to-console'],
mapping: {
'print-to-console': CONSOLE_LOG_ON_SELECT,
},
if: { arg: 'kind', eq: 'deck-config' },
},
deckConfig: {
control: {
type: 'select',
},
options: ['staging-area', 'waste-chute', 'standard'],
mapping: {
'staging-area': EXTENDED_DECK_CONFIG_FIXTURE,
'waste-chute': WASTE_CHUTE_DECK_CONFIG_FIXTURE,
standard: STANDARD_SLOT_DECK_CONFIG_FIXTURE,
},
if: { arg: 'kind', eq: 'intervention' },
},
labwareOnDeck: {
if: { arg: 'kind', eq: 'intervention' },
},
modulesOnDeck: {
if: { arg: 'kind', eq: 'intervention' },
},
highlightLabwareEventuallyIn: {
if: { arg: 'kind', eq: 'intervention' },
},
},
decorators: [
Story => (
<Box
css={css`
border: 4px solid #000000;
border-radius: ${BORDERS.borderRadius8};
max-width: 47rem;
@media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} {
max-width: 62rem;
max-height: 33.5rem;
}
`}
>
<TwoColumn>
<StandInContent />
<Story />
</TwoColumn>
</Box>
),
],
}

export default meta

type Story = StoryObj<typeof DeckMapContent>

export const InterventionMap: Story = {
args: {
kind: 'intervention',
robotType: FLEX_ROBOT_TYPE,
deckConfig: EXTENDED_DECK_CONFIG_FIXTURE,
labwareOnDeck: DEFAULT_LABWARE_ON_DECK,
modulesOnDeck: DEFAULT_MODULES_ON_DECK,
highlightLabwareEventuallyIn: ['thermocyclerModuleV2', 'C3'],
},
}

export const DeckConfigMap: Story = {
args: {
kind: 'deck-config',
robotType: FLEX_ROBOT_TYPE,
setSelectedLocation: CONSOLE_LOG_ON_SELECT,
},
}
176 changes: 176 additions & 0 deletions app/src/molecules/InterventionModal/DeckMapContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import * as React from 'react'
import { css } from 'styled-components'
import {
Box,
BaseDeck,
RobotCoordsForeignDiv,
COLORS,
DIRECTION_COLUMN,
DISPLAY_FLEX,
JUSTIFY_FLEX_END,
useDeckLocationSelect,
} from '@opentrons/components'

import type {
LabwareDefinition2,
RobotType,
ModuleLocation,
LabwareLocation,
} from '@opentrons/shared-data'

export type MapKind = 'intervention' | 'deck-config'

export interface InterventionStyleDeckMapContentProps
extends Pick<
React.ComponentProps<typeof BaseDeck>,
'deckConfig' | 'robotType' | 'labwareOnDeck' | 'modulesOnDeck'
> {
kind: 'intervention'
highlightLabwareEventuallyIn: string[]
}

export interface DeckConfigStyleDeckMapContentProps {
kind: 'deck-config'
robotType: RobotType
setSelectedLocation: (location: ModuleLocation) => void
}

export type DeckMapContentProps =
| DeckConfigStyleDeckMapContentProps
| InterventionStyleDeckMapContentProps

export const DeckMapContent: (
props: DeckMapContentProps
) => JSX.Element = props =>
props.kind === 'intervention' ? (
<InterventionStyleDeckMapContent {...props} />
) : (
<DeckConfigStyleDeckMapContent {...props} />
)

function InterventionStyleDeckMapContent(
props: InterventionStyleDeckMapContentProps
): JSX.Element {
const labwareWithHighlights =
props.labwareOnDeck?.map(labwareOnDeck =>
props.highlightLabwareEventuallyIn.reduce(
(found, locationToMatch) =>
found ||
getIsLabwareMatch(labwareOnDeck.labwareLocation, locationToMatch),
false
)
? {
...labwareOnDeck,
labwareChildren: (
<LabwareHighlight
highlight={true}
definition={labwareOnDeck.definition}
/>
),
}
: labwareOnDeck
) ?? []
const modulesWithHighlights =
props.modulesOnDeck?.map(module =>
props.highlightLabwareEventuallyIn.reduce(
(found, locationToMatch) =>
found || getIsLabwareMatch(module.moduleLocation, locationToMatch),
false
)
? {
...module,
moduleChildren:
module?.nestedLabwareDef != null ? (
<LabwareHighlight
highlight={true}
definition={module.nestedLabwareDef}
/>
) : undefined,
}
: module
) ?? []
return (
<BaseDeck
deckConfig={props.deckConfig}
robotType={props.robotType}
labwareOnDeck={labwareWithHighlights}
modulesOnDeck={modulesWithHighlights}
/>
)
}

function DeckConfigStyleDeckMapContent({
robotType,
setSelectedLocation,
}: DeckConfigStyleDeckMapContentProps): JSX.Element {
const { DeckLocationSelect, selectedLocation } = useDeckLocationSelect(
robotType,
'default'
)
React.useEffect(() => {
setSelectedLocation != null && setSelectedLocation(selectedLocation)
}, [selectedLocation, setSelectedLocation])
return <>{DeckLocationSelect}</>
}

export function LabwareHighlight({
highlight,
definition,
}: {
highlight: boolean
definition: LabwareDefinition2
}): JSX.Element {
const width = definition.dimensions.xDimension
const height = definition.dimensions.yDimension

return (
<RobotCoordsForeignDiv
x={definition.cornerOffsetFromSlot.x}
y={definition.cornerOffsetFromSlot.y}
{...{ width, height }}
innerDivProps={{
display: DISPLAY_FLEX,
flexDirection: DIRECTION_COLUMN,
justifyContent: JUSTIFY_FLEX_END,
width: '100%',
height: '100%',
}}
>
<Box
width="100%"
height="100%"
css={highlight ? HIGHLIGHT_STYLE : undefined}
/>
</RobotCoordsForeignDiv>
)
}

const HIGHLIGHT_STYLE = css`
border-radius: 7.04px;
border: 3px solid ${COLORS.blue50};
box-shadow: 0 0 4px 3px #74b0ff;
`

export function getIsLabwareMatch(
locationToCheck: LabwareLocation | ModuleLocation,
deckRootLocation: string
): boolean {
if (typeof locationToCheck === 'string') {
// This is the "off deck" case, which we do not render (and therefore return false).
return false
} else if ('slotName' in locationToCheck) {
// This is if we're checking a module or a labware loaded on a slot
return locationToCheck.slotName === deckRootLocation
} else if ('addressableAreaName' in locationToCheck) {
// This is if we're loaded on an AA like a staging slot
return locationToCheck.addressableAreaName === deckRootLocation
} else {
// Defaulted cases:
// if ('moduleId' in locationToCheck), e.g. on a module:
// this should never happen because labware that is loaded on a module wouldn't be
// in onDeckLabware, and onDeckModules is for modules not labware.
// if ('labwareId' in locationToCheck), e.g. stacked labware:
// this should never happen because we don't really render it properly here
return false
}
}
14 changes: 1 addition & 13 deletions app/src/molecules/InterventionModal/TwoColumn.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import {
Flex,
DIRECTION_COLUMN,
Box,
BORDERS,
} from '@opentrons/components'
import { InlineNotification } from '../../atoms/InlineNotification'

import { TwoColumn as TwoColumnComponent } from './'
import { StandInContent } from './story-utils/StandIn'

import type { Meta, StoryObj } from '@storybook/react'

Expand All @@ -30,18 +30,6 @@ interface StorybookArgs {
rightText?: string
}

function StandInContent(): JSX.Element {
return (
<Box
border={'4px dashed #A864FFFF'}
borderRadius={BORDERS.borderRadius8}
margin={SPACING.spacing16}
height="104px"
backgroundColor="#A864FF19"
/>
)
}

interface NotificationProps {
heading?: string
message?: string
Expand Down
2 changes: 1 addition & 1 deletion app/src/molecules/InterventionModal/TwoColumn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function TwoColumn({
<Box flex="1" minWidth="17.1875rem">
{leftElement}
</Box>
<Box flex="1" minWidth="275px">
<Box flex="1" minWidth="17.1875rem">
{rightElement}
</Box>
</Flex>
Expand Down
Loading

0 comments on commit 1578adf

Please sign in to comment.