diff --git a/api-client/src/protocols/utils.ts b/api-client/src/protocols/utils.ts index dabc7f7a5c9..71a75a13fe1 100644 --- a/api-client/src/protocols/utils.ts +++ b/api-client/src/protocols/utils.ts @@ -227,7 +227,7 @@ export function parseInitialLoadedModulesBySlot( ) } -interface LoadedFixturesBySlot { +export interface LoadedFixturesBySlot { [slotName: string]: LoadFixtureRunTimeCommand } export function parseInitialLoadedFixturesByCutout( diff --git a/app/src/assets/images/staging_area_slot.png b/app/src/assets/images/staging_area_slot.png new file mode 100644 index 00000000000..61f394b56a4 Binary files /dev/null and b/app/src/assets/images/staging_area_slot.png differ diff --git a/app/src/assets/localization/en/protocol_setup.json b/app/src/assets/localization/en/protocol_setup.json index fc99548e57c..c305199f2db 100644 --- a/app/src/assets/localization/en/protocol_setup.json +++ b/app/src/assets/localization/en/protocol_setup.json @@ -52,6 +52,7 @@ "extra_attention_warning_title": "Secure labware and modules before proceeding to run", "extra_module_attached": "Extra module attached", "feedback_form_link": "Let us know!", + "fixture_name": "fixture", "get_labware_offset_data": "Get Labware Offset Data", "heater_shaker_extra_attention": "Use latch controls for easy placement of labware.", "heater_shaker_labware_list_view": "To add labware, use the toggle to control the latch", @@ -59,6 +60,7 @@ "initial_liquids_num_plural": "{{count}} initial liquids", "initial_liquids_num": "{{count}} initial liquid", "initial_location": "Initial Location", + "install_modules_and_fixtures": "Install the required modules and power them on. Install the required fixtures and review the deck configuration.", "instruments_connected_plural": "{{count}} instruments attached", "instruments_connected": "{{count}} instrument attached", "instruments": "Instruments", @@ -104,11 +106,12 @@ "missing": "Missing", "modal_instructions_title": "{{moduleName}} Setup Instructions", "modal_instructions": "For step-by-step instructions on setting up your module, consult the Quickstart Guide that came in its box. You can also click the link below or scan the QR code to visit the modules section of the Opentrons Help Center.", + "module_and_deck_setup": "Modules & deck", "module_connected": "Connected", "module_disconnected": "Disconnected", "module_instructions_link": "{{moduleName}} setup instructions", "module_mismatch_body": "Check that the modules connected to this robot are of the right type and generation", - "module_name": "Module Name", + "module_name": "Module", "module_not_connected": "Not connected", "module_setup_step_description_plural": "Install the required modules and power them on.", "module_setup_step_description": "Install the required modules and power them on.", @@ -132,6 +135,7 @@ "n_a": "N/A", "no_data": "no data", "no_labware_offset_data": "no labware offset data yet", + "no_modules_or_fixtures": "No modules or fixtures are specified for this protocol.", "no_modules_specified": "no modules are specified for this protocol.", "no_modules_used_in_this_protocol": "No modules used in this protocol", "no_tiprack_loaded": "Protocol must load a tip rack", @@ -214,8 +218,9 @@ "usb_port_connected": "USB Port {{port}}", "view_current_offsets": "View current offsets", "view_moam": "View setup instructions for placing modules of the same type to the robot.", - "view_module_setup_instructions": "View module setup instructions", + "view_setup_instructions": "View setup instructions", "volume": "Volume", + "update_deck": "Update deck", "what_labware_offset_is": "A Labware Offset is a type of positional adjustment that accounts for small, real-world variances in the overall position of the labware on a robot’s deck. Labware Offset data is unique to a specific combination of labware definition, deck slot, and robot.", "why_use_lpc": "Labware Position Check is intended to correct for minor variances. Opentrons does not recommend using Labware Position Check to compensate for large positional adjustments. Needing to set large labware offsets could indicate a problem with robot calibration." } diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunSetup.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunSetup.tsx index 286f0a827f2..5b7b06d3558 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunSetup.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunSetup.tsx @@ -1,7 +1,10 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' -import { parseAllRequiredModuleModels } from '@opentrons/api-client' +import { + LoadedFixturesBySlot, + parseAllRequiredModuleModels, +} from '@opentrons/api-client' import { Flex, ALIGN_CENTER, @@ -14,9 +17,14 @@ import { TYPOGRAPHY, Link, } from '@opentrons/components' +import { + STAGING_AREA_LOAD_NAME, + WASTE_CHUTE_LOAD_NAME, +} from '@opentrons/shared-data' import { Line } from '../../../atoms/structure' import { StyledText } from '../../../atoms/text' +import { useFeatureFlag } from '../../../redux/config' import { InfoMessage } from '../../../molecules/InfoMessage' import { useIsOT3, @@ -31,7 +39,7 @@ import { useMostRecentCompletedAnalysis } from '../../LabwarePositionCheck/useMo import { SetupLabware } from './SetupLabware' import { SetupLabwarePositionCheck } from './SetupLabwarePositionCheck' import { SetupRobotCalibration } from './SetupRobotCalibration' -import { SetupModules } from './SetupModules' +import { SetupModuleAndDeck } from './SetupModuleAndDeck' import { SetupStep } from './SetupStep' import { SetupLiquids } from './SetupLiquids' import { EmptySetupStep } from './EmptySetupStep' @@ -66,6 +74,40 @@ export function ProtocolRunSetup({ const storedProtocolAnalysis = useStoredProtocolAnalysis(runId) const protocolData = robotProtocolAnalysis ?? storedProtocolAnalysis const modules = parseAllRequiredModuleModels(protocolData?.commands ?? []) + const enableDeckConfig = useFeatureFlag('enableDeckConfiguration') + // TODO(Jr, 10/4/23): stubbed in the fixtures for now - delete IMMEDIATELY + // const loadedFixturesBySlot = parseInitialLoadedFixturesByCutout( + // protocolData?.commands ?? [] + // ) + + const STUBBED_LOAD_FIXTURE_BY_SLOT: LoadedFixturesBySlot = { + D3: { + id: 'stubbed_load_fixture', + commandType: 'loadFixture', + params: { + fixtureId: 'stubbedFixtureId', + loadName: WASTE_CHUTE_LOAD_NAME, + location: { cutout: 'D3' }, + }, + createdAt: 'fakeTimestamp', + startedAt: 'fakeTimestamp', + completedAt: 'fakeTimestamp', + status: 'succeeded', + }, + B3: { + id: 'stubbed_load_fixture_2', + commandType: 'loadFixture', + params: { + fixtureId: 'stubbedFixtureId_2', + loadName: STAGING_AREA_LOAD_NAME, + location: { cutout: 'B3' }, + }, + createdAt: 'fakeTimestamp', + startedAt: 'fakeTimestamp', + completedAt: 'fakeTimestamp', + status: 'succeeded', + }, + } const robot = useRobot(robotName) const calibrationStatus = useRunCalibrationStatus(robotName, runId) const isOT3 = useIsOT3(robotName) @@ -111,6 +153,19 @@ export function ProtocolRunSetup({ if (robot == null) return null const hasLiquids = protocolData != null && protocolData.liquids?.length > 0 const hasModules = protocolData != null && modules.length > 0 + const hasFixtures = + protocolData != null && Object.keys(STUBBED_LOAD_FIXTURE_BY_SLOT).length > 0 + + let moduleDescription: string = t(`${MODULE_SETUP_KEY}_description`, { + count: modules.length, + }) + if (!hasModules && !enableDeckConfig) { + moduleDescription = i18n.format(t('no_modules_specified'), 'capitalize') + } else if (isOT3 && enableDeckConfig && (hasModules || hasFixtures)) { + moduleDescription = t('install_modules_and_fixtures') + } else if (isOT3 && enableDeckConfig && !hasModules && !hasFixtures) { + moduleDescription = t('no_modules_or_fixtures') + } const StepDetailMap: Record< StepKey, @@ -139,17 +194,15 @@ export function ProtocolRunSetup({ }, [MODULE_SETUP_KEY]: { stepInternals: ( - setExpandedStepKey(LPC_KEY)} robotName={robotName} runId={runId} + loadedFixturesBySlot={STUBBED_LOAD_FIXTURE_BY_SLOT} + hasModules={hasModules} /> ), - description: !hasModules - ? i18n.format(t('no_modules_specified'), 'capitalize') - : t(`${MODULE_SETUP_KEY}_description`, { - count: modules.length, - }), + description: moduleDescription, }, [LPC_KEY]: { stepInternals: ( @@ -207,40 +260,51 @@ export function ProtocolRunSetup({ {t('protocol_analysis_failed')} ) : ( - stepsKeysInOrder.map((stepKey, index) => ( - - {(stepKey === 'liquid_setup_step' && !hasLiquids) || - (stepKey === 'module_setup_step' && !hasModules) ? ( - - ) : ( - - stepKey === expandedStepKey - ? setExpandedStepKey(null) - : setExpandedStepKey(stepKey) - } - rightElement={ - - } - > - {StepDetailMap[stepKey].stepInternals} - - )} - {index !== stepsKeysInOrder.length - 1 ? ( - - ) : null} - - )) + stepsKeysInOrder.map((stepKey, index) => { + const setupStepTitle = t( + isOT3 && stepKey === MODULE_SETUP_KEY && enableDeckConfig + ? `module_and_deck_setup` + : `${stepKey}_title` + ) + const showEmptySetupStep = + (stepKey === 'liquid_setup_step' && !hasLiquids) || + (stepKey === 'module_setup_step' && + ((!enableDeckConfig && !hasModules) || + (enableDeckConfig && !hasModules && !hasFixtures))) + return ( + + {showEmptySetupStep ? ( + + ) : ( + + stepKey === expandedStepKey + ? setExpandedStepKey(null) + : setExpandedStepKey(stepKey) + } + rightElement={ + + } + > + {StepDetailMap[stepKey].stepInternals} + + )} + {index !== stepsKeysInOrder.length - 1 ? ( + + ) : null} + + ) + }) )} ) : ( diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModules/MultipleModulesModal.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/MultipleModulesModal.tsx similarity index 100% rename from app/src/organisms/Devices/ProtocolRun/SetupModules/MultipleModulesModal.tsx rename to app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/MultipleModulesModal.tsx diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupFixtureList.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupFixtureList.tsx new file mode 100644 index 00000000000..69d4c156e9d --- /dev/null +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupFixtureList.tsx @@ -0,0 +1,201 @@ +import * as React from 'react' +import map from 'lodash/map' +import { css } from 'styled-components' +import { useTranslation } from 'react-i18next' +import { + BORDERS, + Box, + Btn, + COLORS, + DIRECTION_COLUMN, + DIRECTION_ROW, + Flex, + JUSTIFY_CENTER, + JUSTIFY_SPACE_BETWEEN, + SPACING, + TYPOGRAPHY, +} from '@opentrons/components' +import { + FixtureLoadName, + getFixtureDisplayName, + LoadFixtureRunTimeCommand, +} from '@opentrons/shared-data' +import { + useLoadedFixturesConfigStatus, + NOT_CONFIGURED, + CONFIGURED, +} from '../../../../resources/deck_configuration/hooks' +import { StyledText } from '../../../../atoms/text' +import { StatusLabel } from '../../../../atoms/StatusLabel' +import { TertiaryButton } from '../../../../atoms/buttons/TertiaryButton' +import { getFixtureImage } from './utils' + +import type { LoadedFixturesBySlot } from '@opentrons/api-client' + +interface SetupFixtureListProps { + loadedFixturesBySlot: LoadedFixturesBySlot +} + +export const SetupFixtureList = (props: SetupFixtureListProps): JSX.Element => { + const { loadedFixturesBySlot } = props + const { t, i18n } = useTranslation('protocol_setup') + return ( + <> + + + {i18n.format(t('fixture_name'), 'capitalize')} + + + {t('location')} + + + {t('status')} + + + + {map(loadedFixturesBySlot, ({ params, id }) => { + const { loadName, location } = params + console.log(id) + return ( + + ) + })} + + + ) +} + +interface FixtureListItemProps { + loadedFixtures: LoadFixtureRunTimeCommand[] + loadName: FixtureLoadName + cutout: string + commandId: string +} + +export function FixtureListItem({ + loadedFixtures, + loadName, + cutout, + commandId, +}: FixtureListItemProps): JSX.Element { + const { t } = useTranslation('protocol_setup') + const configuration = useLoadedFixturesConfigStatus(loadedFixtures) + const configurationStatus = configuration.find( + config => config.id === commandId + )?.configurationStatus + + let statusLabel + if (configurationStatus !== CONFIGURED) { + statusLabel = ( + + ) + } else { + statusLabel = ( + + ) + } + + return ( + <> + + + + + + + {getFixtureDisplayName(loadName)} + + console.log('wire this up')} + > + + {t('view_setup_instructions')} + + + + + + {cutout} + + + {statusLabel} + {configurationStatus === NOT_CONFIGURED ? ( + // TODO(jr, 10/4/23): wire up update deck cta + console.log('wire this up')}> + {t('update_deck')} + + ) : null} + + + + + ) +} diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModules/SetupModulesList.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx similarity index 95% rename from app/src/organisms/Devices/ProtocolRun/SetupModules/SetupModulesList.tsx rename to app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx index 8abb5e0c730..115df603d3c 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModules/SetupModulesList.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx @@ -244,10 +244,10 @@ export function ModulesListItem({ - - {t('view_module_setup_instructions')} + + {t('view_setup_instructions')} @@ -380,14 +376,11 @@ export function ModulesListItem({ - {t('slot_location', { - slotName: - getModuleType(moduleModel) === 'thermocyclerModuleType' - ? isOT3 - ? TC_MODULE_LOCATION_OT3 - : TC_MODULE_LOCATION_OT2 - : slotName, - })} + {getModuleType(moduleModel) === 'thermocyclerModuleType' + ? isOT3 + ? TC_MODULE_LOCATION_OT3 + : TC_MODULE_LOCATION_OT2 + : slotName} {moduleModel === MAGNETIC_BLOCK_V1 ? ( diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModules/SetupModulesMap.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesMap.tsx similarity index 100% rename from app/src/organisms/Devices/ProtocolRun/SetupModules/SetupModulesMap.tsx rename to app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesMap.tsx diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModules/UnMatchedModuleWarning.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/UnMatchedModuleWarning.tsx similarity index 100% rename from app/src/organisms/Devices/ProtocolRun/SetupModules/UnMatchedModuleWarning.tsx rename to app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/UnMatchedModuleWarning.tsx diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModules/__tests__/MultipleModuleModal.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/MultipleModuleModal.test.tsx similarity index 100% rename from app/src/organisms/Devices/ProtocolRun/SetupModules/__tests__/MultipleModuleModal.test.tsx rename to app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/MultipleModuleModal.test.tsx diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupFixtureList.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupFixtureList.test.tsx new file mode 100644 index 00000000000..77d6b7882b9 --- /dev/null +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupFixtureList.test.tsx @@ -0,0 +1,89 @@ +import * as React from 'react' +import { renderWithProviders } from '@opentrons/components' +import { + LoadFixtureRunTimeCommand, + WASTE_CHUTE_LOAD_NAME, + WASTE_CHUTE_SLOT, +} from '@opentrons/shared-data' +import { i18n } from '../../../../../i18n' +import { useLoadedFixturesConfigStatus } from '../../../../../resources/deck_configuration/hooks' +import { SetupFixtureList } from '../SetupFixtureList' +import type { LoadedFixturesBySlot } from '@opentrons/api-client' + +jest.mock('../../../../../resources/deck_configuration/hooks') + +const mockUseLoadedFixturesConfigStatus = useLoadedFixturesConfigStatus as jest.MockedFunction< + typeof useLoadedFixturesConfigStatus +> + +const mockLoadedFixture = { + id: 'stubbed_load_fixture', + commandType: 'loadFixture', + params: { + fixtureId: 'stubbedFixtureId', + loadName: WASTE_CHUTE_LOAD_NAME, + location: { cutout: 'D3' }, + }, + createdAt: 'fakeTimestamp', + startedAt: 'fakeTimestamp', + completedAt: 'fakeTimestamp', + status: 'succeeded', +} as LoadFixtureRunTimeCommand + +const mockLoadedFixturesBySlot: LoadedFixturesBySlot = { + D3: mockLoadedFixture, +} + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + }) +} + +describe('SetupFixtureList', () => { + let props: React.ComponentProps + beforeEach(() => { + props = { + loadedFixturesBySlot: mockLoadedFixturesBySlot, + } + mockUseLoadedFixturesConfigStatus.mockReturnValue([ + { + ...mockLoadedFixture, + configurationStatus: 'configured', + }, + ]) + }) + + it('should render the headers and a fixture with configured status', () => { + const { getByText, getByRole } = render(props)[0] + getByText('Fixture') + getByText('Location') + getByText('Status') + getByText('Waste Chute') + getByRole('button', { name: 'View setup instructions' }) + getByText(WASTE_CHUTE_SLOT) + getByText('Configured') + }) + it('should render the headers and a fixture with conflicted status', () => { + mockUseLoadedFixturesConfigStatus.mockReturnValue([ + { + ...mockLoadedFixture, + configurationStatus: 'conflicting', + }, + ]) + const { getByText } = render(props)[0] + getByText('Conflicting') + }) + it('should render the headers and a fixture with not configured status and button', () => { + mockUseLoadedFixturesConfigStatus.mockReturnValue([ + { + ...mockLoadedFixture, + configurationStatus: 'not configured', + }, + ]) + const { getByText, getByRole } = render(props)[0] + getByText('Not configured') + getByRole('button', { name: 'Update deck' }).click() + // TODO(Jr, 10/5/23): add test coverage for button + }) +}) diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModules/__tests__/SetupModules.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesAndDeck.test.tsx similarity index 59% rename from app/src/organisms/Devices/ProtocolRun/SetupModules/__tests__/SetupModules.test.tsx rename to app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesAndDeck.test.tsx index 60e17d5153d..d04d7dd4c9a 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModules/__tests__/SetupModules.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesAndDeck.test.tsx @@ -1,20 +1,42 @@ import * as React from 'react' import { fireEvent } from '@testing-library/react' import { when } from 'jest-when' -import { i18n } from '../../../../../i18n' import { renderWithProviders } from '@opentrons/components' -import { SetupModules } from '../index' -import { SetupModulesList } from '../SetupModulesList' -import { SetupModulesMap } from '../SetupModulesMap' +import { WASTE_CHUTE_LOAD_NAME } from '@opentrons/shared-data' +import { i18n } from '../../../../../i18n' +import { useFeatureFlag } from '../../../../../redux/config' import { useRunHasStarted, useUnmatchedModulesForProtocol, } from '../../../hooks' +import { SetupModuleAndDeck } from '../index' +import { SetupModulesList } from '../SetupModulesList' +import { SetupModulesMap } from '../SetupModulesMap' +import { SetupFixtureList } from '../SetupFixtureList' import { mockTemperatureModule } from '../../../../../redux/modules/__fixtures__' +import { LoadedFixturesBySlot } from '@opentrons/api-client' + +const mockLoadedFixturesBySlot: LoadedFixturesBySlot = { + D3: { + id: 'stubbed_load_fixture', + commandType: 'loadFixture', + params: { + fixtureId: 'stubbedFixtureId', + loadName: WASTE_CHUTE_LOAD_NAME, + location: { cutout: 'D3' }, + }, + createdAt: 'fakeTimestamp', + startedAt: 'fakeTimestamp', + completedAt: 'fakeTimestamp', + status: 'succeeded', + }, +} jest.mock('../../../hooks') jest.mock('../SetupModulesList') jest.mock('../SetupModulesMap') +jest.mock('../SetupFixtureList') +jest.mock('../../../../../redux/config') const mockUseRunHasStarted = useRunHasStarted as jest.MockedFunction< typeof useRunHasStarted @@ -25,27 +47,35 @@ const mockUseUnmatchedModulesForProtocol = useUnmatchedModulesForProtocol as jes const mockSetupModulesList = SetupModulesList as jest.MockedFunction< typeof SetupModulesList > +const mockSetupFixtureList = SetupFixtureList as jest.MockedFunction< + typeof SetupFixtureList +> const mockSetupModulesMap = SetupModulesMap as jest.MockedFunction< typeof SetupModulesMap > - +const mockUseFeatureFlag = useFeatureFlag as jest.MockedFunction< + typeof useFeatureFlag +> const MOCK_ROBOT_NAME = 'otie' const MOCK_RUN_ID = '1' -const render = (props: React.ComponentProps) => { - return renderWithProviders( - jest.fn()} - />, - { i18nInstance: i18n } - )[0] +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] } -describe('SetupModules', () => { - let props: React.ComponentProps +describe('SetupModuleAndDeck', () => { + let props: React.ComponentProps beforeEach(() => { + props = { + robotName: MOCK_ROBOT_NAME, + runId: MOCK_RUN_ID, + expandLabwarePositionCheckStep: () => jest.fn(), + hasModules: true, + loadedFixturesBySlot: {}, + } + mockSetupFixtureList.mockReturnValue(
Mock setup fixture list
) mockSetupModulesList.mockReturnValue(
Mock setup modules list
) mockSetupModulesMap.mockReturnValue(
Mock setup modules map
) when(mockUseRunHasStarted).calledWith(MOCK_RUN_ID).mockReturnValue(false) @@ -55,6 +85,9 @@ describe('SetupModules', () => { missingModuleIds: [], remainingAttachedModules: [], }) + when(mockUseFeatureFlag) + .calledWith('enableDeckConfiguration') + .mockReturnValue(false) }) it('renders the list and map view buttons', () => { @@ -92,6 +125,18 @@ describe('SetupModules', () => { getByText('Mock setup modules list') }) + it('should render the SetupModulesList and SetupFixtureList component when clicking List View and ff is on', () => { + when(mockUseFeatureFlag) + .calledWith('enableDeckConfiguration') + .mockReturnValue(true) + props.loadedFixturesBySlot = mockLoadedFixturesBySlot + const { getByRole, getByText } = render(props) + const button = getByRole('button', { name: 'List View' }) + fireEvent.click(button) + getByText('Mock setup modules list') + getByText('Mock setup fixture list') + }) + it('should render the SetupModulesMap component when clicking Map View', () => { const { getByRole, getByText } = render(props) const button = getByRole('button', { name: 'Map View' }) diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModules/__tests__/SetupModulesList.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx similarity index 98% rename from app/src/organisms/Devices/ProtocolRun/SetupModules/__tests__/SetupModulesList.test.tsx rename to app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx index de99d48b823..41d29f99c23 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModules/__tests__/SetupModulesList.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx @@ -144,7 +144,7 @@ describe('SetupModulesList', () => { .calledWith(ROBOT_NAME, RUN_ID) .mockReturnValue({}) const { getByText } = render(props) - getByText('Module Name') + getByText('Module') getByText('Location') getByText('Status') }) @@ -170,7 +170,7 @@ describe('SetupModulesList', () => { const { getByText } = render(props) getByText('Magnetic Module') - getByText('Slot 1') + getByText('1') getByText('Connected') }) @@ -192,7 +192,7 @@ describe('SetupModulesList', () => { const { getByText } = render(props) getByText('Magnetic Module') - getByText('Slot 1') + getByText('1') getByText('Not connected') }) @@ -224,7 +224,7 @@ describe('SetupModulesList', () => { const { getByText } = render(props) getByText('Thermocycler Module') - getByText('Slot 7,8,10,11') + getByText('7,8,10,11') getByText('Connected') }) @@ -253,7 +253,7 @@ describe('SetupModulesList', () => { const { getByText, getByRole } = render(props) getByText('Thermocycler Module') - getByText('Slot A1+B1') + getByText('A1+B1') getByRole('button', { name: 'Calibrate now' }).click() await waitFor(() => { getByText('mock ModuleWizardFlows') @@ -321,7 +321,7 @@ describe('SetupModulesList', () => { const { getByText } = render(props) getByText('Thermocycler Module') - getByText('Slot A1+B1') + getByText('A1+B1') getByText('Connected') }) @@ -427,7 +427,7 @@ describe('SetupModulesList', () => { }, } as any) const { getByText } = render(props) - const moduleSetup = getByText('View module setup instructions') + const moduleSetup = getByText('View setup instructions') fireEvent.click(moduleSetup) getByText('mockModuleSetupModal') }) diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModules/__tests__/SetupModulesMap.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesMap.test.tsx similarity index 100% rename from app/src/organisms/Devices/ProtocolRun/SetupModules/__tests__/SetupModulesMap.test.tsx rename to app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesMap.test.tsx diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModules/__tests__/UnMatchedModuleWarning.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/UnMatchedModuleWarning.test.tsx similarity index 100% rename from app/src/organisms/Devices/ProtocolRun/SetupModules/__tests__/UnMatchedModuleWarning.test.tsx rename to app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/UnMatchedModuleWarning.test.tsx diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModules/__tests__/utils.test.ts b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/utils.test.ts similarity index 100% rename from app/src/organisms/Devices/ProtocolRun/SetupModules/__tests__/utils.test.ts rename to app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/utils.test.ts diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModules/index.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/index.tsx similarity index 71% rename from app/src/organisms/Devices/ProtocolRun/SetupModules/index.tsx rename to app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/index.tsx index 63500b87608..edf9cb19ba0 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModules/index.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/index.tsx @@ -8,28 +8,37 @@ import { useHoverTooltip, PrimaryButton, } from '@opentrons/components' -import { useRunHasStarted, useUnmatchedModulesForProtocol } from '../../hooks' import { useToggleGroup } from '../../../../molecules/ToggleGroup/useToggleGroup' import { Tooltip } from '../../../../atoms/Tooltip' +import { useFeatureFlag } from '../../../../redux/config' +import { useRunHasStarted, useUnmatchedModulesForProtocol } from '../../hooks' import { SetupModulesMap } from './SetupModulesMap' import { SetupModulesList } from './SetupModulesList' +import { SetupFixtureList } from './SetupFixtureList' +import type { LoadedFixturesBySlot } from '@opentrons/api-client' -interface SetupModulesProps { +interface SetupModuleAndDeckProps { expandLabwarePositionCheckStep: () => void robotName: string runId: string + loadedFixturesBySlot: LoadedFixturesBySlot + hasModules: boolean } -export const SetupModules = ({ +export const SetupModuleAndDeck = ({ expandLabwarePositionCheckStep, robotName, runId, -}: SetupModulesProps): JSX.Element => { + loadedFixturesBySlot, + hasModules, +}: SetupModuleAndDeckProps): JSX.Element => { const { t } = useTranslation('protocol_setup') const [selectedValue, toggleGroup] = useToggleGroup( t('list_view'), t('map_view') ) + const enableDeckConfig = useFeatureFlag('enableDeckConfiguration') + const { missingModuleIds } = useUnmatchedModulesForProtocol(robotName, runId) const runHasStarted = useRunHasStarted(runId) const [targetProps, tooltipProps] = useHoverTooltip() @@ -38,7 +47,15 @@ export const SetupModules = ({ {toggleGroup} {selectedValue === t('list_view') ? ( - + <> + {hasModules ? ( + + ) : null} + {Object.keys(loadedFixturesBySlot).length > 0 && + enableDeckConfig ? ( + + ) : null} + ) : ( )} diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModules/utils.ts b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/utils.ts similarity index 64% rename from app/src/organisms/Devices/ProtocolRun/SetupModules/utils.ts rename to app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/utils.ts index 3936733de83..0b6713f0ff9 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModules/utils.ts +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/utils.ts @@ -4,7 +4,8 @@ import thermoModuleGen1 from '../../../../assets/images/thermocycler_closed.png' import heaterShakerModule from '../../../../assets/images/heater_shaker_module_transparent.png' import thermoModuleGen2 from '../../../../assets/images/thermocycler_gen_2_closed.png' import magneticBlockGen1 from '../../../../assets/images/magnetic_block_gen_1.png' -import type { ModuleModel } from '@opentrons/shared-data' +import stagingArea from '../../../../assets/images/staging_area_slot.png' +import type { FixtureLoadName, ModuleModel } from '@opentrons/shared-data' export function getModuleImage(model: ModuleModel): string { switch (model) { @@ -26,3 +27,23 @@ export function getModuleImage(model: ModuleModel): string { return 'Error: unknown module model' } } + +// TODO(jr, 10/4/23): add correct assets for wasteChute, trashBin, standardSlot +export function getFixtureImage(fixture: FixtureLoadName): string { + switch (fixture) { + case 'stagingArea': { + return stagingArea + } + case 'wasteChute': { + return stagingArea + } + case 'standardSlot': { + return stagingArea + } + case 'trashBin': { + return stagingArea + } + default: + return 'Error: unknown fixture' + } +} diff --git a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx index 2f5dd0b115c..2041543bd67 100644 --- a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx @@ -14,6 +14,7 @@ import noModulesProtocol from '@opentrons/shared-data/protocol/fixtures/4/simple import withModulesProtocol from '@opentrons/shared-data/protocol/fixtures/4/testModulesProtocol.json' import { i18n } from '../../../../i18n' +import { useFeatureFlag } from '../../../../redux/config' import { mockConnectedRobot } from '../../../../redux/discovery/__fixtures__' import { useMostRecentCompletedAnalysis } from '../../../LabwarePositionCheck/useMostRecentCompletedAnalysis' import { @@ -28,18 +29,19 @@ import { SetupLabware } from '../SetupLabware' import { SetupRobotCalibration } from '../SetupRobotCalibration' import { SetupLiquids } from '../SetupLiquids' import { ProtocolRunSetup } from '../ProtocolRunSetup' -import { SetupModules } from '../SetupModules' +import { SetupModuleAndDeck } from '../SetupModuleAndDeck' import { EmptySetupStep } from '../EmptySetupStep' jest.mock('@opentrons/api-client') jest.mock('../../hooks') jest.mock('../SetupLabware') jest.mock('../SetupRobotCalibration') -jest.mock('../SetupModules') +jest.mock('../SetupModuleAndDeck') jest.mock('../SetupLiquids') jest.mock('../EmptySetupStep') jest.mock('../../../LabwarePositionCheck/useMostRecentCompletedAnalysis') jest.mock('@opentrons/shared-data/js/helpers/parseProtocolData') +jest.mock('../../../../redux/config') const mockUseIsOT3 = useIsOT3 as jest.MockedFunction const mockUseMostRecentCompletedAnalysis = useMostRecentCompletedAnalysis as jest.MockedFunction< @@ -67,8 +69,8 @@ const mockSetupLabware = SetupLabware as jest.MockedFunction< const mockSetupRobotCalibration = SetupRobotCalibration as jest.MockedFunction< typeof SetupRobotCalibration > -const mockSetupModules = SetupModules as jest.MockedFunction< - typeof SetupModules +const mockSetupModuleAndDeck = SetupModuleAndDeck as jest.MockedFunction< + typeof SetupModuleAndDeck > const mockSetupLiquids = SetupLiquids as jest.MockedFunction< typeof SetupLiquids @@ -79,7 +81,9 @@ const mockProtocolHasLiquids = protocolHasLiquids as jest.MockedFunction< const mockEmptySetupStep = EmptySetupStep as jest.MockedFunction< typeof EmptySetupStep > - +const mockUseFeatureFlag = useFeatureFlag as jest.MockedFunction< + typeof useFeatureFlag +> const ROBOT_NAME = 'otie' const RUN_ID = '1' const MOCK_ROTOCOL_LIQUID_KEY = { liquids: [] } @@ -139,9 +143,12 @@ describe('ProtocolRunSetup', () => { }) ) .mockReturnValue(Mock SetupLabware) - when(mockSetupModules).mockReturnValue(
Mock SetupModules
) + when(mockSetupModuleAndDeck).mockReturnValue(
Mock SetupModules
) when(mockSetupLiquids).mockReturnValue(
Mock SetupLiquids
) when(mockEmptySetupStep).mockReturnValue(
Mock EmptySetupStep
) + when(mockUseFeatureFlag) + .calledWith('enableDeckConfiguration') + .mockReturnValue(false) }) afterEach(() => { resetAllWhenMocks() @@ -332,6 +339,37 @@ describe('ProtocolRunSetup', () => { 'Gather the following labware and full tip racks. To run your protocol without Labware Position Check, place and secure labware in their initial locations.' ) }) + + it('renders correct text contents for modules and fixtures', () => { + when(mockUseIsOT3).calledWith(ROBOT_NAME).mockReturnValue(true) + when(mockUseFeatureFlag) + .calledWith('enableDeckConfiguration') + .mockReturnValue(true) + when(mockUseMostRecentCompletedAnalysis) + .calledWith(RUN_ID) + .mockReturnValue({ + ...withModulesProtocol, + ...MOCK_ROTOCOL_LIQUID_KEY, + modules: [ + { + id: '1d57adf0-67ad-11ea-9f8b-3b50068bd62d:magneticModuleType', + location: { slot: '1' }, + model: 'magneticModuleV1', + }, + ], + } as any) + when(mockParseAllRequiredModuleModels).mockReturnValue([ + 'magneticModuleV1', + ]) + const { getByText } = render() + + getByText('STEP 2') + getByText('Modules & deck') + getByText( + 'Install the required modules and power them on. Install the required fixtures and review the deck configuration.' + ) + }) + it('renders view-only info message if run has started', async () => { when(mockUseRunHasStarted).calledWith(RUN_ID).mockReturnValue(true) diff --git a/app/src/organisms/ProtocolSetupModules/index.tsx b/app/src/organisms/ProtocolSetupModules/index.tsx index 66ced6c88e1..04eaf976b23 100644 --- a/app/src/organisms/ProtocolSetupModules/index.tsx +++ b/app/src/organisms/ProtocolSetupModules/index.tsx @@ -41,7 +41,7 @@ import { useRunCalibrationStatus, } from '../../organisms/Devices/hooks' import { ModuleInfo } from '../../organisms/Devices/ModuleInfo' -import { MultipleModulesModal } from '../../organisms/Devices/ProtocolRun/SetupModules/MultipleModulesModal' +import { MultipleModulesModal } from '../Devices/ProtocolRun/SetupModuleAndDeck/MultipleModulesModal' import { getProtocolModulesInfo } from '../../organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo' import { useMostRecentCompletedAnalysis } from '../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' import { ROBOT_MODEL_OT3, getLocalRobot } from '../../redux/discovery'