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
Prev Previous commit
Next Next commit
feat(app): add DeckMapContent
add deckmap fixtures and such

more deckmap changes
  • Loading branch information
sfoster1 committed Jul 5, 2024
commit ef2cde7edb19376d09704a35c06456c1d9624e37
152 changes: 152 additions & 0 deletions app/src/molecules/InterventionModal/DeckMapContent.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
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(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
}
}
Loading