diff --git a/app/src/assets/localization/en/shared.json b/app/src/assets/localization/en/shared.json
index fe3c9177c5c..8c8bed0a5af 100644
--- a/app/src/assets/localization/en/shared.json
+++ b/app/src/assets/localization/en/shared.json
@@ -6,6 +6,7 @@
"before_you_begin": "Before you begin",
"browse": "browse",
"cancel": "cancel",
+ "change_robot": "Change robot",
"clear_data": "clear data",
"close_robot_door": "Close the robot door before starting the run.",
"close": "close",
@@ -13,8 +14,10 @@
"confirm_placement": "Confirm placement",
"confirm_position": "Confirm position",
"confirm_terminate": "This will immediately stop the activity begun on a computer. You, or another user, may lose progress or see an error in the Opentrons App.",
+ "confirm_values": "Confirm values",
"confirm": "Confirm",
"continue_activity": "Continue activity",
+ "continue_to_param": "Continue to parameters",
"continue": "continue",
"delete": "Delete",
"did_pipette_pick_up_tip": "Did pipette pick up tip successfully?",
diff --git a/app/src/atoms/Slideout/MultiSlideout.tsx b/app/src/atoms/Slideout/MultiSlideout.tsx
new file mode 100644
index 00000000000..71ce02f6de6
--- /dev/null
+++ b/app/src/atoms/Slideout/MultiSlideout.tsx
@@ -0,0 +1,37 @@
+import * as React from 'react'
+import { Slideout } from './index'
+
+interface MultiSlideoutProps {
+ title: string | React.ReactElement
+ children: React.ReactNode
+ onCloseClick: () => void
+ currentStep: number
+ maxSteps: number
+ // isExpanded is for collapse and expand animation
+ isExpanded?: boolean
+ footer?: React.ReactNode
+}
+
+export const MultiSlideout = (props: MultiSlideoutProps): JSX.Element => {
+ const {
+ isExpanded,
+ title,
+ onCloseClick,
+ children,
+ footer,
+ maxSteps,
+ currentStep,
+ } = props
+
+ return (
+
+ {children}
+
+ )
+}
diff --git a/app/src/atoms/Slideout/index.tsx b/app/src/atoms/Slideout/index.tsx
index 57d20e1de50..03fcc0c9723 100644
--- a/app/src/atoms/Slideout/index.tsx
+++ b/app/src/atoms/Slideout/index.tsx
@@ -19,14 +19,20 @@ import {
import { Divider } from '../structure'
import { StyledText } from '../text'
+import { useTranslation } from 'react-i18next'
+export interface MultiSlideoutSpecs {
+ currentStep: number
+ maxSteps: number
+}
export interface SlideoutProps {
title: string | React.ReactElement
children: React.ReactNode
- onCloseClick: () => unknown
+ onCloseClick: () => void
// isExpanded is for collapse and expand animation
isExpanded?: boolean
footer?: React.ReactNode
+ multiSlideoutSpecs?: MultiSlideoutSpecs
}
const SHARED_STYLE = css`
@@ -108,10 +114,17 @@ const CLOSE_ICON_STYLE = css`
`
export const Slideout = (props: SlideoutProps): JSX.Element => {
- const { isExpanded, title, onCloseClick, children, footer } = props
+ const {
+ isExpanded,
+ title,
+ onCloseClick,
+ children,
+ footer,
+ multiSlideoutSpecs,
+ } = props
+ const { t } = useTranslation('shared')
const slideOutRef = React.useRef(null)
const [isReachedBottom, setIsReachedBottom] = React.useState(false)
-
const hasBeenExpanded = React.useRef(isExpanded ?? false)
const handleScroll = (): void => {
if (slideOutRef.current == null) return
@@ -166,6 +179,19 @@ export const Slideout = (props: SlideoutProps): JSX.Element => {
flexDirection={DIRECTION_COLUMN}
justifyContent={JUSTIFY_SPACE_BETWEEN}
>
+ {multiSlideoutSpecs == null ? null : (
+
+ {t('step', {
+ current: multiSlideoutSpecs.currentStep,
+ max: multiSlideoutSpecs.maxSteps,
+ })}
+
+ )}
{typeof title === 'string' ? (
{
expect(vi.mocked(startDiscovery)).toHaveBeenCalled()
expect(dispatch).toHaveBeenCalledWith({ type: 'mockStartDiscovery' })
})
+ it('renders the multi slideout page 1', () => {
+ render({
+ onCloseClick: vi.fn(),
+ isExpanded: true,
+ isSelectedRobotOnDifferentSoftwareVersion: false,
+ selectedRobot: null,
+ setSelectedRobot: mockSetSelectedRobot,
+ title: 'choose robot slideout title',
+ robotType: 'OT-2 Standard',
+ multiSlideout: { currentPage: 1 },
+ })
+ screen.getByText('Step 1 / 2')
+ })
+ it('renders the multi slideout page 2', () => {
+ render({
+ onCloseClick: vi.fn(),
+ isExpanded: true,
+ isSelectedRobotOnDifferentSoftwareVersion: false,
+ selectedRobot: null,
+ setSelectedRobot: mockSetSelectedRobot,
+ title: 'choose robot slideout title',
+ robotType: 'OT-2 Standard',
+ multiSlideout: { currentPage: 2 },
+ })
+ screen.getByText('Step 2 / 2')
+ })
it('defaults to first available robot and allows an available robot to be selected', () => {
vi.mocked(getConnectableRobots).mockReturnValue([
{ ...mockConnectableRobot, name: 'otherRobot', ip: 'otherIp' },
diff --git a/app/src/organisms/ChooseRobotSlideout/index.tsx b/app/src/organisms/ChooseRobotSlideout/index.tsx
index f9c9c37730c..5c768bdb12f 100644
--- a/app/src/organisms/ChooseRobotSlideout/index.tsx
+++ b/app/src/organisms/ChooseRobotSlideout/index.tsx
@@ -35,6 +35,7 @@ import {
} from '../../redux/discovery'
import { Banner } from '../../atoms/Banner'
import { Slideout } from '../../atoms/Slideout'
+import { MultiSlideout } from '../../atoms/Slideout/MultiSlideout'
import { StyledText } from '../../atoms/text'
import { AvailableRobotOption } from './AvailableRobotOption'
@@ -90,6 +91,7 @@ interface ChooseRobotSlideoutProps
isAnalysisError?: boolean
isAnalysisStale?: boolean
showIdleOnly?: boolean
+ multiSlideout?: { currentPage: number }
}
export function ChooseRobotSlideout(
@@ -112,6 +114,7 @@ export function ChooseRobotSlideout(
setSelectedRobot,
robotType,
showIdleOnly = false,
+ multiSlideout,
} = props
const dispatch = useDispatch()
const isScanning = useSelector((state: State) => getScanning(state))
@@ -171,145 +174,157 @@ export function ChooseRobotSlideout(
const unavailableCount =
unhealthyReachableRobots.length + unreachableRobots.length
- return (
-
-
- {isAnalysisError ? (
- {t('protocol_failed_app_analysis')}
- ) : null}
- {isAnalysisStale ? (
- {t('protocol_outdated_app_analysis')}
- ) : null}
-
- {isScanning ? (
-
-
- {t('app_settings:searching')}
-
-
-
- ) : (
- dispatch(startDiscovery())}
- textTransform={TYPOGRAPHY.textTransformCapitalize}
- role="button"
- css={TYPOGRAPHY.linkPSemiBold}
+ const pageOneBody = (
+
+ {isAnalysisError ? (
+ {t('protocol_failed_app_analysis')}
+ ) : null}
+ {isAnalysisStale ? (
+ {t('protocol_outdated_app_analysis')}
+ ) : null}
+
+ {isScanning ? (
+
+
- {t('shared:refresh')}
-
- )}
-
- {!isScanning && healthyReachableRobots.length === 0 ? (
-
-
-
- {t('no_available_robots_found')}
+ {t('app_settings:searching')}
+
) : (
- healthyReachableRobots.map(robot => {
- const isSelected =
- selectedRobot != null && selectedRobot.ip === robot.ip
- return (
-
- {
- if (!isCreatingRun) {
- resetCreateRun?.()
- setSelectedRobot(robot)
- }
- }}
- isError={runCreationError != null}
- isSelected={isSelected}
- isSelectedRobotOnDifferentSoftwareVersion={
- isSelectedRobotOnDifferentSoftwareVersion
- }
- showIdleOnly={showIdleOnly}
- registerRobotBusyStatus={registerRobotBusyStatus}
- />
- {runCreationError != null && isSelected && (
-
- {runCreationErrorCode === 409 ? (
-
- ),
- }}
- />
- ) : (
- runCreationError
- )}
-
- )}
-
- )
- })
- )}
- {!isScanning && unavailableCount > 0 ? (
- dispatch(startDiscovery())}
+ textTransform={TYPOGRAPHY.textTransformCapitalize}
+ role="button"
+ css={TYPOGRAPHY.linkPSemiBold}
>
-
- {showIdleOnly
- ? t('unavailable_or_busy_robot_not_listed', {
- count: unavailableCount + reducerBusyCount,
- })
- : t('unavailable_robot_not_listed', {
- count: unavailableCount,
- })}
-
-
- {t('view_unavailable_robots')}
-
-
- ) : null}
+ {t('shared:refresh')}
+
+ )}
+ {!isScanning && healthyReachableRobots.length === 0 ? (
+
+
+
+ {t('no_available_robots_found')}
+
+
+ ) : (
+ healthyReachableRobots.map(robot => {
+ const isSelected =
+ selectedRobot != null && selectedRobot.ip === robot.ip
+ return (
+
+ {
+ if (!isCreatingRun) {
+ resetCreateRun?.()
+ setSelectedRobot(robot)
+ }
+ }}
+ isError={runCreationError != null}
+ isSelected={isSelected}
+ isSelectedRobotOnDifferentSoftwareVersion={
+ isSelectedRobotOnDifferentSoftwareVersion
+ }
+ showIdleOnly={showIdleOnly}
+ registerRobotBusyStatus={registerRobotBusyStatus}
+ />
+ {runCreationError != null && isSelected && (
+
+ {runCreationErrorCode === 409 ? (
+
+ ),
+ }}
+ />
+ ) : (
+ runCreationError
+ )}
+
+ )}
+
+ )
+ })
+ )}
+ {!isScanning && unavailableCount > 0 ? (
+
+
+ {showIdleOnly
+ ? t('unavailable_or_busy_robot_not_listed', {
+ count: unavailableCount + reducerBusyCount,
+ })
+ : t('unavailable_robot_not_listed', {
+ count: unavailableCount,
+ })}
+
+
+ {t('view_unavailable_robots')}
+
+
+ ) : null}
+
+ )
+
+ const pageTwoBody = TODO
+
+ return multiSlideout != null ? (
+
+ {multiSlideout.currentPage === 1 ? pageOneBody : pageTwoBody}
+
+ ) : (
+
+ {pageOneBody}
)
}
diff --git a/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx b/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx
index 70b54a106ce..bf2cf102d76 100644
--- a/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx
+++ b/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx
@@ -183,16 +183,17 @@ describe('ChooseRobotToRunProtocolSlideout', () => {
showSlideout: true,
})
const proceedButton = screen.getByRole('button', {
- name: 'Proceed to setup',
+ name: 'Continue to parameters',
})
- expect(proceedButton).not.toBeDisabled()
+
const otherRobot = screen.getByText('otherRobot')
fireEvent.click(otherRobot) // unselect default robot
- expect(proceedButton).not.toBeDisabled()
const mockRobot = screen.getByText('opentrons-robot-name')
fireEvent.click(mockRobot)
- expect(proceedButton).not.toBeDisabled()
fireEvent.click(proceedButton)
+ const confirm = screen.getByRole('button', { name: 'Confirm values' })
+ expect(confirm).not.toBeDisabled()
+ fireEvent.click(confirm)
expect(mockCreateRunFromProtocolSource).toHaveBeenCalledWith({
files: [expect.any(File)],
protocolKey: storedProtocolDataFixture.protocolKey,
@@ -211,7 +212,7 @@ describe('ChooseRobotToRunProtocolSlideout', () => {
showSlideout: true,
})
const proceedButton = screen.getByRole('button', {
- name: 'Proceed to setup',
+ name: 'Continue to parameters',
})
expect(proceedButton).toBeDisabled()
screen.getByText(
@@ -235,15 +236,17 @@ describe('ChooseRobotToRunProtocolSlideout', () => {
showSlideout: true,
})
const proceedButton = screen.getByRole('button', {
- name: 'Proceed to setup',
+ name: 'Continue to parameters',
})
fireEvent.click(proceedButton)
+ fireEvent.click(screen.getByRole('button', { name: 'Confirm values' }))
expect(mockCreateRunFromProtocolSource).toHaveBeenCalledWith({
files: [expect.any(File)],
protocolKey: storedProtocolDataFixture.protocolKey,
})
expect(mockTrackCreateProtocolRunEvent).toHaveBeenCalled()
- expect(screen.getByText('run creation error')).toBeInTheDocument()
+ // TODO( jr, 3.13.24): fix this when page 2 is completed of the multislideout
+ // expect(screen.getByText('run creation error')).toBeInTheDocument()
})
it('renders error state when run creation error code is 409', () => {
@@ -260,20 +263,22 @@ describe('ChooseRobotToRunProtocolSlideout', () => {
showSlideout: true,
})
const proceedButton = screen.getByRole('button', {
- name: 'Proceed to setup',
+ name: 'Continue to parameters',
})
+ const link = screen.getByRole('link', { name: 'Go to Robot' })
+ fireEvent.click(link)
+ expect(link.getAttribute('href')).toEqual('/devices/opentrons-robot-name')
fireEvent.click(proceedButton)
+ fireEvent.click(screen.getByRole('button', { name: 'Confirm values' }))
expect(mockCreateRunFromProtocolSource).toHaveBeenCalledWith({
files: [expect.any(File)],
protocolKey: storedProtocolDataFixture.protocolKey,
})
expect(mockTrackCreateProtocolRunEvent).toHaveBeenCalled()
- screen.getByText(
- 'This robot is busy and can’t run this protocol right now.'
- )
- const link = screen.getByRole('link', { name: 'Go to Robot' })
- fireEvent.click(link)
- expect(link.getAttribute('href')).toEqual('/devices/opentrons-robot-name')
+ // TODO( jr, 3.13.24): fix this when page 2 is completed of the multislideout
+ // screen.getByText(
+ // 'This robot is busy and can’t run this protocol right now.'
+ // )
})
it('renders apply historic offsets as determinate if candidates available', () => {
@@ -311,9 +316,10 @@ describe('ChooseRobotToRunProtocolSlideout', () => {
)
expect(screen.getByRole('checkbox')).toBeChecked()
const proceedButton = screen.getByRole('button', {
- name: 'Proceed to setup',
+ name: 'Continue to parameters',
})
fireEvent.click(proceedButton)
+ fireEvent.click(screen.getByRole('button', { name: 'Confirm values' }))
expect(mockCreateRunFromProtocolSource).toHaveBeenCalledWith({
files: [expect.any(File)],
protocolKey: storedProtocolDataFixture.protocolKey,
@@ -350,9 +356,10 @@ describe('ChooseRobotToRunProtocolSlideout', () => {
expect(screen.getByRole('checkbox')).toBeChecked()
const proceedButton = screen.getByRole('button', {
- name: 'Proceed to setup',
+ name: 'Continue to parameters',
})
fireEvent.click(proceedButton)
+ fireEvent.click(screen.getByRole('button', { name: 'Confirm values' }))
expect(vi.mocked(useCreateRunFromProtocol)).nthCalledWith(
2,
expect.any(Object),
diff --git a/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx b/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx
index fab0fbcd756..dbb5f7196b1 100644
--- a/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx
+++ b/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx
@@ -10,6 +10,9 @@ import {
DIRECTION_COLUMN,
SIZE_1,
PrimaryButton,
+ DIRECTION_ROW,
+ SecondaryButton,
+ SPACING,
} from '@opentrons/components'
import { getRobotUpdateDisplayInfo } from '../../redux/robot-update'
@@ -50,7 +53,7 @@ export function ChooseRobotToRunProtocolSlideoutComponent(
srcFiles,
mostRecentAnalysis,
} = storedProtocolData
-
+ const [currentPage, setCurrentPage] = React.useState(1)
const [selectedRobot, setSelectedRobot] = React.useState(null)
const { trackCreateProtocolRunEvent } = useTrackCreateProtocolRunEvent(
storedProtocolData,
@@ -142,6 +145,7 @@ export function ChooseRobotToRunProtocolSlideoutComponent(
return (
-
-
- {isCreatingRun ? (
-
- ) : (
- t('shared:proceed_to_setup')
- )}
-
+ {currentPage === 1 ? (
+ <>
+
+ setCurrentPage(2)}
+ width="100%"
+ disabled={
+ isCreatingRun ||
+ selectedRobot == null ||
+ isSelectedRobotOnDifferentSoftwareVersion
+ }
+ >
+ {t('shared:continue_to_param')}
+
+ >
+ ) : (
+
+ setCurrentPage(1)} width="50%">
+ {t('shared:change_robot')}
+
+
+ {isCreatingRun ? (
+
+ ) : (
+ t('shared:confirm_values')
+ )}
+
+
+ )}
}
selectedRobot={selectedRobot}