Skip to content

Commit

Permalink
feat(app): view-only run time parameters in odd (#14701)
Browse files Browse the repository at this point in the history
  • Loading branch information
jerader authored Mar 21, 2024
1 parent 6b652c2 commit 0a11394
Show file tree
Hide file tree
Showing 12 changed files with 358 additions and 65 deletions.
2 changes: 2 additions & 0 deletions app/src/assets/localization/en/protocol_setup.json
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@
"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 hardware used in this protocol",
"no_parameters_specified": "No parameters specified",
"no_tiprack_loaded": "Protocol must load a tip rack",
"no_tiprack_used": "Protocol must pick up a tip",
"no_usb_connection_required": "No USB connection required",
Expand Down Expand Up @@ -218,6 +219,7 @@
"required_tip_racks_title": "Required Tip Length Calibrations",
"reset_parameter_values_body": "This will discard any changes you have made. All parameters will have their default values.",
"reset_parameter_values": "Reset parameter values?",
"reset_setup": "Restart setup to edit",
"reset_values": "Reset values",
"resolve": "Resolve",
"restore_default": "Restore default values",
Expand Down
7 changes: 7 additions & 0 deletions app/src/atoms/Chip/Chip.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ export default {
},
defaultValue: 'basic',
},
hasIcon: {
control: {
type: 'boolean',
},
defaultValue: true,
},
},
component: Chip,
parameters: touchScreenViewport,
Expand All @@ -38,4 +44,5 @@ export const ChipComponent = Template.bind({})
ChipComponent.args = {
type: 'basic',
text: 'Chip component',
hasIcon: true,
}
9 changes: 9 additions & 0 deletions app/src/atoms/Chip/__tests__/Chip.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -183,4 +183,13 @@ describe('Chip', () => {
const icon = screen.getByLabelText('icon_mockInfo')
expect(icon).toHaveStyle(`color: ${COLORS.blue60}`)
})
it('renders no icon when hasIcon is false', () => {
props = {
text: 'mockInfo',
hasIcon: false,
type: 'info',
}
render(props)
expect(screen.queryByText('icon_mockInfo')).not.toBeInTheDocument()
})
})
22 changes: 13 additions & 9 deletions app/src/atoms/Chip/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ interface ChipProps extends StyleProps {
text: string
/** name constant of the text color and the icon color to display */
type: ChipType
/** has icon */
hasIcon?: boolean
}

const CHIP_PROPS_BY_TYPE: Record<
Expand Down Expand Up @@ -82,13 +84,15 @@ const CHIP_PROPS_BY_TYPE: Record<
},
}

export function Chip({
background,
iconName,
type,
text,
...styleProps
}: ChipProps): JSX.Element {
export function Chip(props: ChipProps): JSX.Element {
const {
background,
iconName,
type,
text,
hasIcon = true,
...styleProps
} = props
const backgroundColor =
background === false && type !== 'basic'
? COLORS.transparent
Expand All @@ -107,15 +111,15 @@ export function Chip({
data-testid={`Chip_${type}`}
{...styleProps}
>
{type !== 'basic' && (
{type !== 'basic' && hasIcon ? (
<Icon
name={icon}
color={CHIP_PROPS_BY_TYPE[type].iconColor}
aria-label={`icon_${text}`}
size="1.5rem"
data-testid="RenderResult_icon"
/>
)}
) : null}
<StyledText
fontSize={TYPOGRAPHY.fontSize22}
lineHeight={TYPOGRAPHY.lineHeight28}
Expand Down
135 changes: 135 additions & 0 deletions app/src/organisms/ProtocolSetupParameters/ViewOnlyParameters.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import {
ALIGN_CENTER,
BORDERS,
COLORS,
DIRECTION_COLUMN,
DIRECTION_ROW,
Flex,
SPACING,
TYPOGRAPHY,
} from '@opentrons/components'
import { useMostRecentCompletedAnalysis } from '../LabwarePositionCheck/useMostRecentCompletedAnalysis'
import { ChildNavigation } from '../ChildNavigation'
import { StyledText } from '../../atoms/text'
import { Chip } from '../../atoms/Chip'
import { useToaster } from '../ToasterOven'
import { mockData } from './index'

import type { RunTimeParameter } from '@opentrons/shared-data'
import type { SetupScreens } from '../../pages/ProtocolSetup'

export interface ViewOnlyParametersProps {
runId: string
setSetupScreen: React.Dispatch<React.SetStateAction<SetupScreens>>
}

export function ViewOnlyParameters({
runId,
setSetupScreen,
}: ViewOnlyParametersProps): JSX.Element {
const { t, i18n } = useTranslation('protocol_setup')
const { makeSnackbar } = useToaster()
const mostRecentAnalysis = useMostRecentCompletedAnalysis(runId)
const handleOnClick = (): void => {
makeSnackbar(t('reset_setup'))
}

// TODO(jr, 3/18/24): remove mockData
const parameters = mostRecentAnalysis?.runTimeParameters ?? mockData

const getDefault = (parameter: RunTimeParameter): string => {
const { type, default: defaultValue } = parameter
const suffix =
'suffix' in parameter && parameter.suffix != null ? parameter.suffix : ''
switch (type) {
case 'int':
case 'float':
return `${defaultValue.toString()} ${suffix}`
case 'boolean':
return Boolean(defaultValue)
? i18n.format(t('on'), 'capitalize')
: i18n.format(t('off'), 'capitalize')
case 'str':
if ('choices' in parameter && parameter.choices != null) {
const choice = parameter.choices.find(
choice => choice.value === defaultValue
)
if (choice != null) {
return choice.displayName
}
}
break
}
return ''
}

return (
<>
<ChildNavigation
header={t('parameters')}
onClickBack={() => setSetupScreen('prepare to run')}
inlineNotification={{
type: 'neutral',
heading: t('values_are_view_only'),
}}
/>
<Flex
marginTop="7.75rem"
flexDirection={DIRECTION_COLUMN}
gridGap={SPACING.spacing8}
paddingX={SPACING.spacing8}
>
<Flex
gridGap={SPACING.spacing8}
color={COLORS.grey60}
fontSize={TYPOGRAPHY.fontSize20}
fontWeight={TYPOGRAPHY.fontWeightSemiBold}
lineHeight={TYPOGRAPHY.lineHeight24}
>
<StyledText paddingLeft={SPACING.spacing16} width="50%">
{t('name')}
</StyledText>
<StyledText>{t('value')}</StyledText>
</Flex>
{parameters.map((parameter, index) => {
// TODO(jr, 3/20/24): plug in the info if the
// parameter changed from the default
const hasCustomValue = true
return (
<Flex
onClick={handleOnClick}
key={`${parameter.displayName}_${index}`}
alignItems={ALIGN_CENTER}
backgroundColor={COLORS.grey35}
borderRadius={BORDERS.borderRadius8}
padding={`${SPACING.spacing16} ${SPACING.spacing24}`}
gridGap={SPACING.spacing24}
>
<StyledText
width="48%"
as="p"
fontWeight={TYPOGRAPHY.fontWeightSemiBold}
>
{parameter.displayName}
</StyledText>
<Flex
alignItems={ALIGN_CENTER}
flexDirection={DIRECTION_ROW}
gridGap={SPACING.spacing8}
>
<StyledText as="p" maxWidth="15rem" color={COLORS.grey60}>
{getDefault(parameter)}
</StyledText>
{hasCustomValue ? (
<Chip type="success" text={t('updated')} hasIcon={false} />
) : null}
</Flex>
</Flex>
)
})}
</Flex>
</>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ import * as React from 'react'
import { when } from 'vitest-when'
import { it, describe, beforeEach, vi, expect } from 'vitest'
import { fireEvent, screen } from '@testing-library/react'
import { useCreateRunMutation, useHost } from '@opentrons/react-api-client'
import { i18n } from '../../../i18n'
import { renderWithProviders } from '../../../__testing-utils__'
import { ProtocolSetupParameters } from '..'
import { useMostRecentCompletedAnalysis } from '../../LabwarePositionCheck/useMostRecentCompletedAnalysis'
import { mockRunTimeParameterData } from '../../../pages/ProtocolDetails/fixtures'
import type * as ReactRouterDom from 'react-router-dom'
import type { HostConfig } from '@opentrons/api-client'

const mockGoBack = vi.fn()

vi.mock('@opentrons/react-api-client')
vi.mock('../../LabwarePositionCheck/useMostRecentCompletedAnalysis')
vi.mock('react-router-dom', async importOriginal => {
const reactRouterDom = await importOriginal<typeof ReactRouterDom>()
Expand All @@ -18,8 +21,8 @@ vi.mock('react-router-dom', async importOriginal => {
useHistory: () => ({ goBack: mockGoBack } as any),
}
})

const RUN_ID = 'mockId'
const MOCK_HOST_CONFIG: HostConfig = { hostname: 'MOCK_HOST' }
const mockCreateRun = vi.fn()
const render = (
props: React.ComponentProps<typeof ProtocolSetupParameters>
) => {
Expand All @@ -32,14 +35,14 @@ describe('ProtocolSetupParameters', () => {

beforeEach(() => {
props = {
runId: 'mockId',
setSetupScreen: vi.fn(),
protocolId: 'mockId',
labwareOffsets: [],
runTimeParameters: mockRunTimeParameterData,
}
when(vi.mocked(useMostRecentCompletedAnalysis))
.calledWith(RUN_ID)
.thenReturn({
runTimeParameters: mockRunTimeParameterData,
} as any)
vi.mocked(useHost).mockReturnValue(MOCK_HOST_CONFIG)
when(vi.mocked(useCreateRunMutation))
.calledWith(expect.anything())
.thenReturn({ createRun: mockCreateRun } as any)
})
it('renders the parameters labels and mock data', () => {
render(props)
Expand All @@ -54,10 +57,10 @@ describe('ProtocolSetupParameters', () => {
fireEvent.click(screen.getAllByRole('button')[0])
expect(mockGoBack).toHaveBeenCalled()
})
it('renders the confirm values button and clicking on it calls correct stuff', () => {
it('renders the confirm values button and clicking on it creates a run', () => {
render(props)
fireEvent.click(screen.getByRole('button', { name: 'Confirm values' }))
expect(props.setSetupScreen).toHaveBeenCalled()
expect(mockCreateRun).toHaveBeenCalled()
})
it('renders the reset values modal', () => {
render(props)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import * as React from 'react'
import { when } from 'vitest-when'
import { it, describe, beforeEach, vi, expect } from 'vitest'
import { fireEvent, screen } from '@testing-library/react'
import { i18n } from '../../../i18n'
import { renderWithProviders } from '../../../__testing-utils__'
import { useMostRecentCompletedAnalysis } from '../../LabwarePositionCheck/useMostRecentCompletedAnalysis'
import { useToaster } from '../../ToasterOven'
import { mockRunTimeParameterData } from '../../../pages/ProtocolDetails/fixtures'
import { ViewOnlyParameters } from '../ViewOnlyParameters'

vi.mock('../../LabwarePositionCheck/useMostRecentCompletedAnalysis')
vi.mock('../../ToasterOven')
const RUN_ID = 'mockId'
const render = (props: React.ComponentProps<typeof ViewOnlyParameters>) => {
return renderWithProviders(<ViewOnlyParameters {...props} />, {
i18nInstance: i18n,
})
}
const mockMakeSnackBar = vi.fn()
describe('ViewOnlyParameters', () => {
let props: React.ComponentProps<typeof ViewOnlyParameters>

beforeEach(() => {
props = {
runId: 'mockId',
setSetupScreen: vi.fn(),
}
when(vi.mocked(useMostRecentCompletedAnalysis))
.calledWith(RUN_ID)
.thenReturn({
runTimeParameters: mockRunTimeParameterData,
} as any)
when(useToaster)
.calledWith()
.thenReturn(({
makeSnackbar: mockMakeSnackBar,
} as unknown) as any)
})
it('renders the parameters labels and mock data', () => {
render(props)
screen.getByText('Parameters')
screen.getByText('Values are view-only')
screen.getByText('Name')
screen.getByText('Value')
screen.getByText('Dry Run')
screen.getByText('6.5')
screen.getByText('Use Gripper')
screen.getByText('Default Module Offsets')
screen.getByText('Columns of Samples')
screen.getByText('4 mL')
})
it('renders the snackbar from clicking on an item', () => {
render(props)
fireEvent.click(screen.getByText('4 mL'))
expect(mockMakeSnackBar).toBeCalledWith('Restart setup to edit')
})
it('renders the back icon and calls the prop', () => {
render(props)
fireEvent.click(screen.getAllByRole('button')[0])
expect(props.setSetupScreen).toHaveBeenCalled()
})
// TODO(jr, 3/20/24):test the update chip when
// custom value boolean is wired up
})
Loading

0 comments on commit 0a11394

Please sign in to comment.