-
Notifications
You must be signed in to change notification settings - Fork 178
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor(app): add and use deck map component in interventionmodal (#…
…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
Showing
20 changed files
with
684 additions
and
334 deletions.
There are no files selected for viewing
157 changes: 157 additions & 0 deletions
157
app/src/molecules/InterventionModal/DeckMapContent.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.