diff --git a/app/src/molecules/InterventionModal/DeckMapContent.stories.tsx b/app/src/molecules/InterventionModal/DeckMapContent.stories.tsx new file mode 100644 index 00000000000..56c896655e0 --- /dev/null +++ b/app/src/molecules/InterventionModal/DeckMapContent.stories.tsx @@ -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> = { + 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 => ( + + + + + + + ), + ], +} + +export default meta + +type Story = StoryObj + +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, + }, +} diff --git a/app/src/molecules/InterventionModal/DeckMapContent.tsx b/app/src/molecules/InterventionModal/DeckMapContent.tsx new file mode 100644 index 00000000000..a45bc920e0a --- /dev/null +++ b/app/src/molecules/InterventionModal/DeckMapContent.tsx @@ -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, + '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' ? ( + + ) : ( + + ) + +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: ( + + ), + } + : labwareOnDeck + ) ?? [] + const modulesWithHighlights = + props.modulesOnDeck?.map(module => + props.highlightLabwareEventuallyIn.reduce( + (found, locationToMatch) => + found || getIsLabwareMatch(module.moduleLocation, locationToMatch), + false + ) + ? { + ...module, + moduleChildren: + module?.nestedLabwareDef != null ? ( + + ) : undefined, + } + : module + ) ?? [] + return ( + + ) +} + +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 ( + + + + ) +} + +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 + } +} diff --git a/app/src/molecules/InterventionModal/TwoColumn.stories.tsx b/app/src/molecules/InterventionModal/TwoColumn.stories.tsx index 075be6acf6e..84722fbf00b 100644 --- a/app/src/molecules/InterventionModal/TwoColumn.stories.tsx +++ b/app/src/molecules/InterventionModal/TwoColumn.stories.tsx @@ -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' @@ -30,18 +30,6 @@ interface StorybookArgs { rightText?: string } -function StandInContent(): JSX.Element { - return ( - - ) -} - interface NotificationProps { heading?: string message?: string diff --git a/app/src/molecules/InterventionModal/TwoColumn.tsx b/app/src/molecules/InterventionModal/TwoColumn.tsx index 634a77adb4b..8e87a2d62b5 100644 --- a/app/src/molecules/InterventionModal/TwoColumn.tsx +++ b/app/src/molecules/InterventionModal/TwoColumn.tsx @@ -14,7 +14,7 @@ export function TwoColumn({ {leftElement} - + {rightElement} diff --git a/app/src/molecules/InterventionModal/__fixtures__/deckSetup.ts b/app/src/molecules/InterventionModal/__fixtures__/deckSetup.ts new file mode 100644 index 00000000000..08c91cf53f9 --- /dev/null +++ b/app/src/molecules/InterventionModal/__fixtures__/deckSetup.ts @@ -0,0 +1,165 @@ +import { + SINGLE_LEFT_SLOT_FIXTURE, + SINGLE_CENTER_SLOT_FIXTURE, + TRASH_BIN_ADAPTER_FIXTURE, + SINGLE_RIGHT_SLOT_FIXTURE, + STAGING_AREA_RIGHT_SLOT_FIXTURE, + WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE, +} from '@opentrons/shared-data' + +import type { DeckConfiguration } from '@opentrons/shared-data' + +export const STANDARD_SLOT_DECK_CONFIG_FIXTURE: DeckConfiguration = [ + { + cutoutId: 'cutoutA1', + cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE, + }, + { + cutoutId: 'cutoutB1', + cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE, + }, + { + cutoutId: 'cutoutC1', + cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE, + }, + { + cutoutId: 'cutoutD1', + cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE, + }, + { + cutoutId: 'cutoutA2', + cutoutFixtureId: SINGLE_CENTER_SLOT_FIXTURE, + }, + { + cutoutId: 'cutoutB2', + cutoutFixtureId: SINGLE_CENTER_SLOT_FIXTURE, + }, + { + cutoutId: 'cutoutC2', + cutoutFixtureId: SINGLE_CENTER_SLOT_FIXTURE, + }, + { + cutoutId: 'cutoutD2', + cutoutFixtureId: SINGLE_CENTER_SLOT_FIXTURE, + }, + { + cutoutId: 'cutoutA3', + cutoutFixtureId: TRASH_BIN_ADAPTER_FIXTURE, + }, + { + cutoutId: 'cutoutB3', + cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE, + }, + { + cutoutId: 'cutoutC3', + cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE, + }, + { + cutoutId: 'cutoutD3', + cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE, + }, +] + +// contains staging area fixtures +export const EXTENDED_DECK_CONFIG_FIXTURE: DeckConfiguration = [ + { + cutoutId: 'cutoutA1', + cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE, + }, + { + cutoutId: 'cutoutB1', + cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE, + }, + { + cutoutId: 'cutoutC1', + cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE, + }, + { + cutoutId: 'cutoutD1', + cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE, + }, + { + cutoutId: 'cutoutA2', + cutoutFixtureId: SINGLE_CENTER_SLOT_FIXTURE, + }, + { + cutoutId: 'cutoutB2', + cutoutFixtureId: SINGLE_CENTER_SLOT_FIXTURE, + }, + { + cutoutId: 'cutoutC2', + cutoutFixtureId: SINGLE_CENTER_SLOT_FIXTURE, + }, + { + cutoutId: 'cutoutD2', + cutoutFixtureId: SINGLE_CENTER_SLOT_FIXTURE, + }, + { + cutoutId: 'cutoutA3', + cutoutFixtureId: TRASH_BIN_ADAPTER_FIXTURE, + }, + { + cutoutId: 'cutoutB3', + cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, + }, + { + cutoutId: 'cutoutC3', + cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, + }, + { + cutoutId: 'cutoutD3', + cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, + }, +] + +// contains waste chute fixture +export const WASTE_CHUTE_DECK_CONFIG_FIXTURE: DeckConfiguration = [ + { + cutoutId: 'cutoutA1', + cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE, + }, + { + cutoutId: 'cutoutB1', + cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE, + }, + { + cutoutId: 'cutoutC1', + cutoutFixtureId: TRASH_BIN_ADAPTER_FIXTURE, + }, + { + cutoutId: 'cutoutD1', + cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE, + }, + { + cutoutId: 'cutoutA2', + cutoutFixtureId: SINGLE_CENTER_SLOT_FIXTURE, + }, + { + cutoutId: 'cutoutB2', + cutoutFixtureId: SINGLE_CENTER_SLOT_FIXTURE, + }, + { + cutoutId: 'cutoutC2', + cutoutFixtureId: SINGLE_CENTER_SLOT_FIXTURE, + }, + { + cutoutId: 'cutoutD2', + cutoutFixtureId: SINGLE_CENTER_SLOT_FIXTURE, + }, + { + cutoutId: 'cutoutA3', + cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE, + }, + { + cutoutId: 'cutoutB3', + cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, + }, + { + cutoutId: 'cutoutC3', + cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, + }, + { + cutoutId: 'cutoutD3', + cutoutFixtureId: WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE, + }, +] diff --git a/app/src/molecules/InterventionModal/__fixtures__/index.ts b/app/src/molecules/InterventionModal/__fixtures__/index.ts new file mode 100644 index 00000000000..2397001b976 --- /dev/null +++ b/app/src/molecules/InterventionModal/__fixtures__/index.ts @@ -0,0 +1 @@ +export * from './deckSetup' diff --git a/app/src/molecules/InterventionModal/index.tsx b/app/src/molecules/InterventionModal/index.tsx index 83183de6319..53e81b213a0 100644 --- a/app/src/molecules/InterventionModal/index.tsx +++ b/app/src/molecules/InterventionModal/index.tsx @@ -25,12 +25,14 @@ import { TwoColumn } from './TwoColumn' import { OneColumn } from './OneColumn' import { ModalContentMixed } from './ModalContentMixed' import { DescriptionContent } from './DescriptionContent' +import { DeckMapContent } from './DeckMapContent' export { ModalContentOneColSimpleButtons, TwoColumn, OneColumn, ModalContentMixed, DescriptionContent, + DeckMapContent, } export type ModalType = 'intervention-required' | 'error' diff --git a/app/src/molecules/InterventionModal/story-utils/StandIn.tsx b/app/src/molecules/InterventionModal/story-utils/StandIn.tsx new file mode 100644 index 00000000000..f6ac9e7dd78 --- /dev/null +++ b/app/src/molecules/InterventionModal/story-utils/StandIn.tsx @@ -0,0 +1,14 @@ +import * as React from 'react' +import { Box, BORDERS, SPACING } from '@opentrons/components' + +export function StandInContent(): JSX.Element { + return ( + + ) +} diff --git a/app/src/organisms/DropTipWizardFlows/ChooseLocation.tsx b/app/src/organisms/DropTipWizardFlows/ChooseLocation.tsx index 7eb619dc619..e1c284efc4c 100644 --- a/app/src/organisms/DropTipWizardFlows/ChooseLocation.tsx +++ b/app/src/organisms/DropTipWizardFlows/ChooseLocation.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { css } from 'styled-components' +import styled, { css } from 'styled-components' import { useTranslation } from 'react-i18next' import { @@ -8,23 +8,25 @@ import { Btn, COLORS, DIRECTION_COLUMN, - DIRECTION_ROW, Flex, - JUSTIFY_CENTER, JUSTIFY_SPACE_BETWEEN, + JUSTIFY_FLEX_START, PrimaryButton, RESPONSIVENESS, SPACING, LegacyStyledText, TYPOGRAPHY, - useDeckLocationSelect, + DISPLAY_INLINE_BLOCK, } from '@opentrons/components' import { getDeckDefFromRobotType } from '@opentrons/shared-data' import { SmallButton } from '../../atoms/buttons' -import { TwoUpTileLayout } from '../LabwarePositionCheck/TwoUpTileLayout' +import { TwoColumn, DeckMapContent } from '../../molecules/InterventionModal' -import type { AddressableAreaName } from '@opentrons/shared-data' +import type { + AddressableAreaName, + ModuleLocation, +} from '@opentrons/shared-data' import type { DropTipWizardContainerProps } from './types' // TODO: get help link article URL @@ -36,6 +38,16 @@ type ChooseLocationProps = DropTipWizardContainerProps & { body: string | JSX.Element moveToAddressableArea: (addressableArea: AddressableAreaName) => Promise } +const Title = styled.h1` + ${TYPOGRAPHY.h1Default}; + margin-bottom: ${SPACING.spacing8}; + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + ${TYPOGRAPHY.level4HeaderSemiBold}; + margin-bottom: 0; + height: ${SPACING.spacing40}; + display: ${DISPLAY_INLINE_BLOCK}; + } +` export const ChooseLocation = ( props: ChooseLocationProps @@ -47,17 +59,17 @@ export const ChooseLocation = ( body, robotType, moveToAddressableArea, - isOnDevice, } = props const { i18n, t } = useTranslation(['drop_tip_wizard', 'shared']) + const [ + selectedLocation, + setSelectedLocation, + ] = React.useState() const deckDef = getDeckDefFromRobotType(robotType) - const { DeckLocationSelect, selectedLocation } = useDeckLocationSelect( - robotType - ) const handleConfirmPosition = (): void => { const deckSlot = deckDef.locations.addressableAreas.find( - l => l.id === selectedLocation.slotName + l => l.id === selectedLocation?.slotName )?.id if (deckSlot != null) { @@ -66,97 +78,58 @@ export const ChooseLocation = ( }) } } - - if (isOnDevice) { - return ( - - - - - {title} - - {body} - - - {DeckLocationSelect} - + return ( + + + + {title} + {body} - + + + { + handleGoBack() + }} > - { - handleGoBack() - }} - > - - {t('shared:go_back')} - - - - - - ) - } else { - return ( - - - { - handleGoBack() - }} - > - - {t('shared:go_back')} - - - - {i18n.format(t('move_to_slot'), 'capitalize')} - - - } + + {t('shared:go_back')} + + + + {i18n.format(t('move_to_slot'), 'capitalize')} + + - ) - } + + ) } -const TILE_CONTAINER_STYLE = css` - flex-direction: ${DIRECTION_COLUMN}; - justify-content: ${JUSTIFY_SPACE_BETWEEN}; - height: 24.625rem; - @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { - height: 29.5rem; - } -` const GO_BACK_BUTTON_STYLE = css` ${TYPOGRAPHY.pSemiBold}; color: ${COLORS.grey50}; @@ -182,3 +155,16 @@ const ALIGN_BUTTONS = css` align-items: ${ALIGN_CENTER}; } ` + +const CONTAINER_STYLE = css` + flex-direction: ${DIRECTION_COLUMN}; + justify-content: ${JUSTIFY_SPACE_BETWEEN}; + padding: ${SPACING.spacing32}; + height: 24.625rem; + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + justify-content: ${JUSTIFY_FLEX_START}; + gap: ${SPACING.spacing32}; + padding: none; + height: 29.5rem; + } +` diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/FillWellAndSkip.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/FillWellAndSkip.tsx index 7b7aaf7d68e..fb21adc202a 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/FillWellAndSkip.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/FillWellAndSkip.tsx @@ -14,10 +14,9 @@ import { RecoveryFooterButtons, RecoverySingleColumnContent, LeftColumnLabwareInfo, - RecoveryMap, TwoColTextAndFailedStepNextStep, } from '../shared' -import { TwoColumn } from '../../../molecules/InterventionModal' +import { TwoColumn, DeckMapContent } from '../../../molecules/InterventionModal' import { SelectRecoveryOption } from './SelectRecoveryOption' import type { RecoveryContentProps } from '../types' @@ -45,7 +44,12 @@ export function FillWellAndSkip(props: RecoveryContentProps): JSX.Element { } export function FillWell(props: RecoveryContentProps): JSX.Element | null { - const { isOnDevice, routeUpdateActions, failedLabwareUtils } = props + const { + isOnDevice, + routeUpdateActions, + failedLabwareUtils, + deckMapUtils, + } = props const { t } = useTranslation('error_recovery') const { goBackPrevStep, proceedNextStep } = routeUpdateActions @@ -63,7 +67,7 @@ export function FillWell(props: RecoveryContentProps): JSX.Element | null { /> - + { {props.title} )), - RecoveryMap: vi.fn(() =>
MOCK_RECOVERY_MAP
), } }) +vi.mock('../../../../molecules/InterventionModal/DeckMapContent', () => ({ + DeckMapContent: vi.fn(() =>
MOCK_RECOVERY_MAP
), +})) vi.mock('../CancelRun') vi.mock('../SelectRecoveryOption') vi.mock('../../../../molecules/Command') diff --git a/app/src/organisms/ErrorRecoveryFlows/__fixtures__/index.ts b/app/src/organisms/ErrorRecoveryFlows/__fixtures__/index.ts index d171ca2f91a..5af0d0d3887 100644 --- a/app/src/organisms/ErrorRecoveryFlows/__fixtures__/index.ts +++ b/app/src/organisms/ErrorRecoveryFlows/__fixtures__/index.ts @@ -68,7 +68,7 @@ export const mockRecoveryContentProps: RecoveryContentProps = { currentRecoveryOptionUtils: {} as any, failedLabwareUtils: { pickUpTipLabware: mockPickUpTipLabware } as any, failedPipetteInfo: {} as any, - recoveryMapUtils: {} as any, + deckMapUtils: { setSelectedLocation: () => {} } as any, stepCounts: {} as any, protocolAnalysis: { commands: [mockFailedCommand] } as any, trackExternalMap: () => null, diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryMapUtils.test.tsx b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useDeckMapUtils.test.ts similarity index 94% rename from app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryMapUtils.test.tsx rename to app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useDeckMapUtils.test.ts index 1a521443d78..4e341acda99 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryMapUtils.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useDeckMapUtils.test.ts @@ -1,4 +1,3 @@ -import * as React from 'react' import { describe, it, expect, beforeEach, vi } from 'vitest' import { @@ -17,8 +16,7 @@ import { getRunCurrentModulesInfo, getRunCurrentLabwareOnDeck, getRunCurrentModulesOnDeck, -} from '../useRecoveryMapUtils' -import { LabwareHighlight } from '../../shared' +} from '../useDeckMapUtils' import type { LabwareDefinition2 } from '@opentrons/shared-data' @@ -78,13 +76,11 @@ describe('getRunCurrentModulesOnDeck', () => { moduleLocation: { slotName: 'A1' }, innerProps: {}, nestedLabwareDef: mockLabwareDef, - moduleChildren: ( - - ), + highlight: 'MOCK_MODULE_ID', }, ]) }) - it('should set moduleChildren to null if getIsLabwareMatch returns false', () => { + it('should set highlight to null if getIsLabwareMatch returns false', () => { const result = getRunCurrentModulesOnDeck({ failedLabwareUtils: mockFailedLabwareUtils, currentModulesInfo: [ @@ -95,10 +91,10 @@ describe('getRunCurrentModulesOnDeck', () => { ], }) - expect(result[0].moduleChildren).toBeNull() + expect(result[0].highlight).toBeNull() }) - it('should set moduleChildren to null if nestedLabwareDef is null', () => { + it('should set highlight to null if nestedLabwareDef is null', () => { const result = getRunCurrentModulesOnDeck({ failedLabwareUtils: mockFailedLabwareUtils, currentModulesInfo: [ @@ -106,7 +102,7 @@ describe('getRunCurrentModulesOnDeck', () => { ], }) - expect(result[0].moduleChildren).toBeNull() + expect(result[0].highlight).toBeNull() }) }) @@ -139,14 +135,12 @@ describe('getRunCurrentLabwareOnDeck', () => { { labwareLocation: { slotName: 'A1' }, definition: mockLabwareDef, - labwareChildren: ( - - ), + highlight: 'A1', }, ]) }) - it('should set labwareChildren to null if getIsLabwareMatch returns false', () => { + it('should set highlight to null if getIsLabwareMatch returns false', () => { const result = getRunCurrentLabwareOnDeck({ failedLabwareUtils: { ...mockFailedLabwareUtils, @@ -158,7 +152,7 @@ describe('getRunCurrentLabwareOnDeck', () => { currentLabwareInfo: [mockCurrentLabwareInfo], }) - expect(result[0].labwareChildren).toBeNull() + expect(result[0].highlight).toBeNull() }) }) diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryMapUtils.tsx b/app/src/organisms/ErrorRecoveryFlows/hooks/useDeckMapUtils.ts similarity index 80% rename from app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryMapUtils.tsx rename to app/src/organisms/ErrorRecoveryFlows/hooks/useDeckMapUtils.ts index e0e749be0b9..3c8a45faabb 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryMapUtils.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useDeckMapUtils.ts @@ -10,8 +10,6 @@ import { THERMOCYCLER_MODULE_V1, } from '@opentrons/shared-data' -import { LabwareHighlight } from '../shared' - import type { Run } from '@opentrons/api-client' import type { DeckDefinition, @@ -21,29 +19,33 @@ import type { LabwareLocation, CutoutConfigProtocolSpec, LoadedLabware, + RobotType, } from '@opentrons/shared-data' import type { ErrorRecoveryFlowsProps } from '..' import type { UseFailedLabwareUtilsResult } from './useFailedLabwareUtils' -interface UseRecoveryMapUtilsProps { +interface UseDeckMapUtilsProps { runId: ErrorRecoveryFlowsProps['runId'] protocolAnalysis: ErrorRecoveryFlowsProps['protocolAnalysis'] failedLabwareUtils: UseFailedLabwareUtilsResult runRecord?: Run } -export interface UseRecoveryMapUtilsResult { +export interface UseDeckMapUtilsResult { deckConfig: CutoutConfigProtocolSpec[] - runCurrentModules: RunCurrentModulesOnDeck[] - runCurrentLabware: RunCurrentLabwareOnDeck[] + modulesOnDeck: RunCurrentModulesOnDeck[] + labwareOnDeck: RunCurrentLabwareOnDeck[] + highlightLabwareEventuallyIn: string[] + kind: 'intervention' + robotType: RobotType } // Returns the utilities needed by the Recovery Deck Map. -export function useRecoveryMapUtils({ +export function useDeckMapUtils({ protocolAnalysis, runRecord, runId, failedLabwareUtils, -}: UseRecoveryMapUtilsProps): UseRecoveryMapUtilsResult { +}: UseDeckMapUtilsProps): UseDeckMapUtilsResult { const robotType = protocolAnalysis?.robotType ?? OT2_ROBOT_TYPE const deckConfig = getSimplestDeckConfigForProtocol(protocolAnalysis) const deckDef = getDeckDefFromRobotType(robotType) @@ -83,8 +85,23 @@ export function useRecoveryMapUtils({ return { deckConfig, - runCurrentModules, - runCurrentLabware, + modulesOnDeck: runCurrentModules.map( + ({ moduleModel, moduleLocation, innerProps, nestedLabwareDef }) => ({ + moduleModel, + moduleLocation, + innerProps, + nestedLabwareDef, + }) + ), + labwareOnDeck: runCurrentLabware.map(({ labwareLocation, definition }) => ({ + labwareLocation, + definition, + })), + highlightLabwareEventuallyIn: [...runCurrentModules, ...runCurrentLabware] + .map(el => el.highlight) + .filter(maybeSlot => maybeSlot != null) as string[], + kind: 'intervention', + robotType, } } @@ -101,7 +118,6 @@ interface RunCurrentModulesOnDeck { lidMotorState?: undefined } nestedLabwareDef: LabwareDefinition2 | null - moduleChildren: JSX.Element | null } // Builds the necessary module object expected by BaseDeck. @@ -109,62 +125,49 @@ export function getRunCurrentModulesOnDeck({ failedLabwareUtils, currentModulesInfo, }: { - failedLabwareUtils: UseRecoveryMapUtilsProps['failedLabwareUtils'] + failedLabwareUtils: UseDeckMapUtilsProps['failedLabwareUtils'] currentModulesInfo: RunCurrentModuleInfo[] -}): RunCurrentModulesOnDeck[] { +}): Array { const { failedLabware } = failedLabwareUtils return currentModulesInfo.map( - ({ moduleDef, slotName, nestedLabwareDef, nestedLabwareSlotName }) => { - const isLabwareMatch = getIsLabwareMatch( - nestedLabwareSlotName, - failedLabware - ) - - return { - moduleModel: moduleDef.model, - moduleLocation: { slotName }, - innerProps: - moduleDef.model === THERMOCYCLER_MODULE_V1 - ? { lidMotorState: 'open' } - : {}, - - nestedLabwareDef, - moduleChildren: - isLabwareMatch && nestedLabwareDef != null ? ( - - ) : null, - } - } + ({ moduleDef, slotName, nestedLabwareDef, nestedLabwareSlotName }) => ({ + moduleModel: moduleDef.model, + moduleLocation: { slotName }, + innerProps: + moduleDef.model === THERMOCYCLER_MODULE_V1 + ? { lidMotorState: 'open' } + : {}, + + nestedLabwareDef, + highlight: getIsLabwareMatch(nestedLabwareSlotName, failedLabware) + ? nestedLabwareSlotName + : null, + }) ) } interface RunCurrentLabwareOnDeck { labwareLocation: LabwareLocation definition: LabwareDefinition2 - labwareChildren: JSX.Element | null } // Builds the necessary labware object expected by BaseDeck. export function getRunCurrentLabwareOnDeck({ currentLabwareInfo, failedLabwareUtils, }: { - failedLabwareUtils: UseRecoveryMapUtilsProps['failedLabwareUtils'] + failedLabwareUtils: UseDeckMapUtilsProps['failedLabwareUtils'] currentLabwareInfo: RunCurrentLabwareInfo[] -}): RunCurrentLabwareOnDeck[] { +}): Array { const { failedLabware } = failedLabwareUtils - return currentLabwareInfo.map(({ slotName, labwareDef, labwareLocation }) => { - const isLabwareMatch = getIsLabwareMatch(slotName, failedLabware) - - return { + return currentLabwareInfo.map( + ({ slotName, labwareDef, labwareLocation }) => ({ labwareLocation, definition: labwareDef, - labwareChildren: isLabwareMatch ? ( - - ) : null, - } - }) + highlight: getIsLabwareMatch(slotName, failedLabware) ? slotName : null, + }) + ) } interface RunCurrentModuleInfo { @@ -181,8 +184,8 @@ export const getRunCurrentModulesInfo = ({ deckDef, protocolAnalysis, }: { - protocolAnalysis: UseRecoveryMapUtilsProps['protocolAnalysis'] - runRecord: UseRecoveryMapUtilsProps['runRecord'] + protocolAnalysis: UseDeckMapUtilsProps['protocolAnalysis'] + runRecord: UseDeckMapUtilsProps['runRecord'] deckDef: DeckDefinition }): RunCurrentModuleInfo[] => { if (runRecord == null || protocolAnalysis == null) { @@ -248,8 +251,8 @@ export function getRunCurrentLabwareInfo({ runRecord, protocolAnalysis, }: { - runRecord: UseRecoveryMapUtilsProps['runRecord'] - protocolAnalysis: UseRecoveryMapUtilsProps['protocolAnalysis'] + runRecord: UseDeckMapUtilsProps['runRecord'] + protocolAnalysis: UseDeckMapUtilsProps['protocolAnalysis'] }): RunCurrentLabwareInfo[] { if (runRecord == null || protocolAnalysis == null) { return [] diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useERUtils.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useERUtils.ts index b5f414b0546..926d4ee8ff8 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useERUtils.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useERUtils.ts @@ -6,7 +6,7 @@ import { useRecoveryTipStatus } from './useRecoveryTipStatus' import { useRecoveryRouting } from './useRecoveryRouting' import { useFailedLabwareUtils } from './useFailedLabwareUtils' import { getFailedCommandPipetteInfo, getNextStep } from '../utils' -import { useRecoveryMapUtils } from './useRecoveryMapUtils' +import { useDeckMapUtils } from './useDeckMapUtils' import { useNotifyAllCommandsQuery, useNotifyRunQuery, @@ -21,7 +21,7 @@ import type { UseRouteUpdateActionsResult } from './useRouteUpdateActions' import type { UseRecoveryCommandsResult } from './useRecoveryCommands' import type { RecoveryTipStatusUtils } from './useRecoveryTipStatus' import type { UseFailedLabwareUtilsResult } from './useFailedLabwareUtils' -import type { UseRecoveryMapUtilsResult } from './useRecoveryMapUtils' +import type { UseDeckMapUtilsResult } from './useDeckMapUtils' import type { CurrentRecoveryOptionUtils } from './useRecoveryRouting' import type { StepCounts } from '../../../resources/protocols/hooks' @@ -37,7 +37,7 @@ export interface ERUtilsResults { recoveryCommands: UseRecoveryCommandsResult tipStatusUtils: RecoveryTipStatusUtils failedLabwareUtils: UseFailedLabwareUtilsResult - recoveryMapUtils: UseRecoveryMapUtilsResult + deckMapUtils: UseDeckMapUtilsResult getRecoveryOptionCopy: ReturnType failedPipetteInfo: PipetteData | null hasLaunchedRecovery: boolean @@ -109,7 +109,7 @@ export function useERUtils({ routeUpdateActions, }) - const recoveryMapUtils = useRecoveryMapUtils({ + const deckMapUtils = useDeckMapUtils({ runId, runRecord, protocolAnalysis, @@ -133,7 +133,7 @@ export function useERUtils({ tipStatusUtils, failedLabwareUtils, failedPipetteInfo, - recoveryMapUtils, + deckMapUtils, getRecoveryOptionCopy, stepCounts, commandAfterFailedCommand, diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryMap.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryMap.tsx deleted file mode 100644 index 9f6c1573bb5..00000000000 --- a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryMap.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import * as React from 'react' -import { css } from 'styled-components' - -import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' -import { - BaseDeck, - Box, - RobotCoordsForeignDiv, - COLORS, - DIRECTION_COLUMN, - DISPLAY_FLEX, - JUSTIFY_FLEX_END, -} from '@opentrons/components' - -import type { LabwareDefinition2 } from '@opentrons/shared-data' -import type { RecoveryContentProps } from '../types' - -export function RecoveryMap({ - isOnDevice, - recoveryMapUtils, -}: RecoveryContentProps): JSX.Element | null { - const { deckConfig, runCurrentModules, runCurrentLabware } = recoveryMapUtils - - if (isOnDevice) { - return ( - - ) - } else { - return null - } -} - -export function LabwareHighlight({ - highlight, - definition, -}: { - highlight: boolean - definition: LabwareDefinition2 -}): JSX.Element { - const width = definition.dimensions.xDimension - const height = definition.dimensions.yDimension - - return ( - - - - ) -} - -const HIGHLIGHT_STYLE = css` - border-radius: 7.04px; - border: 3px solid ${COLORS.blue50}; - box-shadow: 0 0 4px 3px #74b0ff; -` diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/ReplaceTips.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/ReplaceTips.tsx index 62815b403e8..cbf795778c8 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/ReplaceTips.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/ReplaceTips.tsx @@ -4,10 +4,9 @@ import { Flex } from '@opentrons/components' import { useTranslation } from 'react-i18next' import { RecoverySingleColumnContent } from './RecoverySingleColumnContent' -import { TwoColumn } from '../../../molecules/InterventionModal' +import { TwoColumn, DeckMapContent } from '../../../molecules/InterventionModal' import { RecoveryFooterButtons } from './RecoveryFooterButtons' import { LeftColumnLabwareInfo } from './LeftColumnLabwareInfo' -import { RecoveryMap } from './RecoveryMap' import type { RecoveryContentProps } from '../types' @@ -17,6 +16,7 @@ export function ReplaceTips(props: RecoveryContentProps): JSX.Element | null { routeUpdateActions, failedPipetteInfo, failedLabwareUtils, + deckMapUtils, } = props const { relevantWellName } = failedLabwareUtils const { proceedNextStep } = routeUpdateActions @@ -47,7 +47,7 @@ export function ReplaceTips(props: RecoveryContentProps): JSX.Element | null { bannerText={t('replace_tips_and_select_location')} /> - + { - const actual = await importOriginal() - return { - ...actual, - BaseDeck: vi - .fn() - .mockImplementation(props =>
MOCK_BASE_DECK
), - } -}) - -const render = (props: React.ComponentProps) => { - return renderWithProviders(, { - i18nInstance: i18n, - })[0] -} - -describe('RecoveryMap', () => { - let props: React.ComponentProps - const mockDeckConfig = 'MOCK_DECK_CONFIG' - const mockRunCurrentModules = 'MOCK_RUN_MODULES' - const mockRunCurrentLw = 'MOCK_RUN_LW' - - const mockRecoveryMapUtils = { - deckConfig: mockDeckConfig, - runCurrentModules: mockRunCurrentModules, - runCurrentLabware: mockRunCurrentLw, - } as any - - beforeEach(() => { - props = { - ...mockRecoveryContentProps, - recoveryMapUtils: mockRecoveryMapUtils, - } - }) - - it('renders the BaseDeck with appropriate props when on ODD', () => { - render(props) - - screen.getByText('MOCK_BASE_DECK') - expect(vi.mocked(BaseDeck)).toHaveBeenCalledWith( - { - deckConfig: mockDeckConfig, - robotType: FLEX_ROBOT_TYPE, - modulesOnDeck: mockRunCurrentModules, - labwareOnDeck: mockRunCurrentLw, - }, - {} - ) - }) -}) diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/index.ts b/app/src/organisms/ErrorRecoveryFlows/shared/index.ts index 72ca995e267..48c78d1ddd1 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/index.ts +++ b/app/src/organisms/ErrorRecoveryFlows/shared/index.ts @@ -6,7 +6,6 @@ export { export { ReplaceTips } from './ReplaceTips' export { SelectTips } from './SelectTips' export { TwoColTextAndFailedStepNextStep } from './TwoColTextAndFailedStepNextStep' -export { RecoveryMap, LabwareHighlight } from './RecoveryMap' export { LeftColumnLabwareInfo } from './LeftColumnLabwareInfo' export { TipSelection } from './TipSelection' export { TipSelectionModal } from './TipSelectionModal' diff --git a/components/src/molecules/LocationIcon/index.tsx b/components/src/molecules/LocationIcon/index.tsx index 082833c5cd1..773efbdbbef 100644 --- a/components/src/molecules/LocationIcon/index.tsx +++ b/components/src/molecules/LocationIcon/index.tsx @@ -3,7 +3,7 @@ import { css } from 'styled-components' import { Icon } from '../../icons' import { Flex, Text } from '../../primitives' -import { ALIGN_CENTER } from '../../styles' +import { ALIGN_CENTER, JUSTIFY_CENTER } from '../../styles' import { RESPONSIVENESS, SPACING, TYPOGRAPHY } from '../../ui-style-constants' import { BORDERS, COLORS } from '../../helix-design-system' @@ -36,6 +36,7 @@ const LOCATION_ICON_STYLE = css<{ width: ${props => props.width ?? 'max-content'}; padding: ${SPACING.spacing2} ${SPACING.spacing4}; border-radius: ${BORDERS.borderRadius4}; + justify-content: ${JUSTIFY_CENTER}; height: max-content; @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { @@ -48,13 +49,7 @@ const LOCATION_ICON_STYLE = css<{ ` const SLOT_NAME_TEXT_STYLE = css` - font-size: ${TYPOGRAPHY.fontSizeCaption}; - line-height: ${TYPOGRAPHY.lineHeightNormal}; - font-weight: ${TYPOGRAPHY.fontWeightBold}; - - @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { - ${TYPOGRAPHY.smallBodyTextBold} - } + ${TYPOGRAPHY.smallBodyTextBold} ` export function LocationIcon({