Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(app): add and use deck map component in interventionmodal #15570

Merged
merged 9 commits into from
Jul 5, 2024
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 => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice!

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
Loading