From 5efb48d99544879b94e02e402f36b5818c481b63 Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Tue, 2 Apr 2024 15:26:33 -0400 Subject: [PATCH] feat(app, shared-data): create odd boolean and choice selection screen (#14775) closes AUTH-123 --- .../localization/en/protocol_setup.json | 2 + .../ProtocolRunRunTimeParameters.tsx | 5 +- .../ProtocolSetupParameters/ChooseEnum.tsx | 87 +++++++++++++++++++ .../ViewOnlyParameters.tsx | 4 +- ....test.tsx => AnalysisFailedModal.test.tsx} | 0 .../__tests__/ChooseEnum.test.tsx | 79 +++++++++++++++++ .../ProtocolSetupParameters.test.tsx | 15 ++++ .../ProtocolSetupParameters/index.tsx | 72 ++++++++++++--- app/src/pages/ProtocolDetails/Parameters.tsx | 4 +- .../src/molecules/ParametersTable/index.tsx | 4 +- .../formatRunTimeParameterValue.test.ts | 14 +-- .../formatRunTimeParameterDefaultValue.ts | 36 ++++++++ .../js/helpers/formatRunTimeParameterValue.ts | 21 ++--- shared-data/js/helpers/index.ts | 1 + 14 files changed, 304 insertions(+), 40 deletions(-) create mode 100644 app/src/organisms/ProtocolSetupParameters/ChooseEnum.tsx rename app/src/organisms/ProtocolSetupParameters/__tests__/{AnalysisFailedModa.test.tsx => AnalysisFailedModal.test.tsx} (100%) create mode 100644 app/src/organisms/ProtocolSetupParameters/__tests__/ChooseEnum.test.tsx create mode 100644 shared-data/js/helpers/formatRunTimeParameterDefaultValue.ts diff --git a/app/src/assets/localization/en/protocol_setup.json b/app/src/assets/localization/en/protocol_setup.json index 371ce03a791..99b496a3479 100644 --- a/app/src/assets/localization/en/protocol_setup.json +++ b/app/src/assets/localization/en/protocol_setup.json @@ -38,6 +38,7 @@ "calibration_status": "calibration status", "calibration": "Calibration", "cancel_and_restart_to_edit": "Cancel the run and restart setup to edit", + "choose_enum": "Choose {{displayName}}", "closing": "Closing...", "complete_setup_before_proceeding": "complete setup before continuing run", "configure": "Configure", @@ -161,6 +162,7 @@ "must_have_labware_and_pip": "Protocol must load labware and a pipette", "n_a": "N/A", "name": "Name", + "no_custom_values": "No custom values specified", "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.", diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx index d16d7b8b8cb..af94400b80f 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' - +import { formatRunTimeParameterDefaultValue } from '@opentrons/shared-data' import { ALIGN_CENTER, BORDERS, @@ -17,7 +17,6 @@ import { useHoverTooltip, Icon, } from '@opentrons/components' -import { formatRunTimeParameterValue } from '@opentrons/shared-data' import { Banner } from '../../../atoms/Banner' import { Divider } from '../../../atoms/structure' @@ -151,7 +150,7 @@ const StyledTableRowComponent = ( - {formatRunTimeParameterValue(parameter, t)} + {formatRunTimeParameterDefaultValue(parameter, t)} {parameter.value !== parameter.default ? ( void + parameter: RunTimeParameter + setParameter: (value: boolean | string | number, variableName: string) => void + rawValue: number | string | boolean +} + +export function ChooseEnum({ + handleGoBack, + parameter, + setParameter, + rawValue, +}: ChooseEnumProps): JSX.Element { + const { makeSnackbar } = useToaster() + + const { t } = useTranslation(['protocol_setup', 'shared']) + if (parameter.type !== 'str') { + console.error( + `parameter type is expected to be a string for parameter ${parameter.displayName}` + ) + } + const options = parameter.type === 'str' ? parameter.choices : undefined + const handleOnClick = (newValue: string | number | boolean): void => { + setParameter(newValue, parameter.variableName) + } + const resetValueDisabled = parameter.default === rawValue + + return ( + <> + + resetValueDisabled + ? makeSnackbar(t('no_custom_values')) + : setParameter(parameter.default, parameter.variableName) + } + /> + + + {parameter.description} + + + {options?.map(option => { + return ( + handleOnClick(option.value)} + isSelected={option.value === rawValue} + /> + ) + })} + + + ) +} diff --git a/app/src/organisms/ProtocolSetupParameters/ViewOnlyParameters.tsx b/app/src/organisms/ProtocolSetupParameters/ViewOnlyParameters.tsx index e8aca7d8c9c..09dcaf26c47 100644 --- a/app/src/organisms/ProtocolSetupParameters/ViewOnlyParameters.tsx +++ b/app/src/organisms/ProtocolSetupParameters/ViewOnlyParameters.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' -import { formatRunTimeParameterValue } from '@opentrons/shared-data' +import { formatRunTimeParameterDefaultValue } from '@opentrons/shared-data' import { ALIGN_CENTER, BORDERS, @@ -94,7 +94,7 @@ export function ViewOnlyParameters({ gridGap={SPACING.spacing8} > - {formatRunTimeParameterValue(parameter, t)} + {formatRunTimeParameterDefaultValue(parameter, t)} {hasCustomValue ? ( ) => { + return renderWithProviders(, { + i18nInstance: i18n, + }) +} +describe('ChooseEnum', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + setParameter: vi.fn(), + handleGoBack: vi.fn(), + parameter: { + displayName: 'Default Module Offsets', + variableName: 'DEFAULT_OFFSETS', + value: 'none', + description: '', + type: 'str', + choices: [ + { + displayName: 'no offsets', + value: 'none', + }, + { + displayName: 'temp offset', + value: '1', + }, + { + displayName: 'heater-shaker offset', + value: '2', + }, + ], + default: 'none', + }, + rawValue: '1', + } + }) + it('renders the back icon and calls the prop', () => { + render(props) + fireEvent.click(screen.getAllByRole('button')[0]) + expect(props.handleGoBack).toHaveBeenCalled() + }) + it('calls the prop if reset default is clicked when the default has changed', () => { + render(props) + fireEvent.click(screen.getByText('Restore default values')) + expect(props.setParameter).toHaveBeenCalled() + }) + it('calls does not call prop if reset default is clicked when the default has not changed', () => { + props = { + ...props, + rawValue: 'none', + } + render(props) + fireEvent.click(screen.getByText('Restore default values')) + expect(props.setParameter).not.toHaveBeenCalled() + }) + it('should render the text and buttons for choice param', () => { + render(props) + screen.getByText('no offsets') + screen.getByText('temp offset') + screen.getByText('heater-shaker offset') + const notSelectedOption = screen.getByRole('label', { name: 'no offsets' }) + const selectedOption = screen.getByRole('label', { + name: 'temp offset', + }) + expect(notSelectedOption).toHaveStyle(`background-color: ${COLORS.blue40}`) + expect(selectedOption).toHaveStyle(`background-color: ${COLORS.blue60}`) + }) +}) diff --git a/app/src/organisms/ProtocolSetupParameters/__tests__/ProtocolSetupParameters.test.tsx b/app/src/organisms/ProtocolSetupParameters/__tests__/ProtocolSetupParameters.test.tsx index 4873745356c..1dc55314d59 100644 --- a/app/src/organisms/ProtocolSetupParameters/__tests__/ProtocolSetupParameters.test.tsx +++ b/app/src/organisms/ProtocolSetupParameters/__tests__/ProtocolSetupParameters.test.tsx @@ -6,12 +6,14 @@ import { useCreateRunMutation, useHost } from '@opentrons/react-api-client' import { i18n } from '../../../i18n' import { renderWithProviders } from '../../../__testing-utils__' import { ProtocolSetupParameters } from '..' +import { ChooseEnum } from '../ChooseEnum' 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('../ChooseEnum') vi.mock('@opentrons/react-api-client') vi.mock('../../LabwarePositionCheck/useMostRecentCompletedAnalysis') vi.mock('react-router-dom', async importOriginal => { @@ -39,6 +41,7 @@ describe('ProtocolSetupParameters', () => { labwareOffsets: [], runTimeParameters: mockRunTimeParameterData, } + vi.mocked(ChooseEnum).mockReturnValue(
mock ChooseEnum
) vi.mocked(useHost).mockReturnValue(MOCK_HOST_CONFIG) when(vi.mocked(useCreateRunMutation)) .calledWith(expect.anything()) @@ -52,6 +55,18 @@ describe('ProtocolSetupParameters', () => { screen.getByText('Dry Run') screen.getByText('a dry run description') }) + it('renders the ChooseEnum component when a str param is selected', () => { + render(props) + fireEvent.click(screen.getByText('Default Module Offsets')) + screen.getByText('mock ChooseEnum') + }) + it('renders the other setting when boolean param is selected', () => { + render(props) + screen.getByText('Off') + expect(screen.getAllByText('On')).toHaveLength(3) + fireEvent.click(screen.getByText('Dry Run')) + expect(screen.getAllByText('On')).toHaveLength(4) + }) it('renders the back icon and calls useHistory', () => { render(props) fireEvent.click(screen.getAllByRole('button')[0]) diff --git a/app/src/organisms/ProtocolSetupParameters/index.tsx b/app/src/organisms/ProtocolSetupParameters/index.tsx index a3cc0687b17..1312844b2ab 100644 --- a/app/src/organisms/ProtocolSetupParameters/index.tsx +++ b/app/src/organisms/ProtocolSetupParameters/index.tsx @@ -14,6 +14,7 @@ import { formatRunTimeParameterValue } from '@opentrons/shared-data' import { ProtocolSetupStep } from '../../pages/ProtocolSetup' import { ChildNavigation } from '../ChildNavigation' import { ResetValuesModal } from './ResetValuesModal' +import { ChooseEnum } from './ChooseEnum' import type { RunTimeParameter } from '@opentrons/shared-data' import type { LabwareOffsetCreateData } from '@opentrons/api-client' @@ -176,6 +177,10 @@ export function ProtocolSetupParameters({ const history = useHistory() const host = useHost() const queryClient = useQueryClient() + const [ + chooseValueScreen, + setChooseValueScreen, + ] = React.useState(null) const [resetValuesModal, showResetValuesModal] = React.useState( false ) @@ -187,6 +192,30 @@ export function ProtocolSetupParameters({ runTimeParametersOverrides, setRunTimeParametersOverrides, ] = React.useState(parameters) + + const updateParameters = ( + value: boolean | string | number, + variableName: string + ): void => { + const updatedParameters = parameters.map(parameter => { + if (parameter.variableName === variableName) { + return { ...parameter, value } + } + return parameter + }) + setRunTimeParametersOverrides(updatedParameters) + if (chooseValueScreen && chooseValueScreen.variableName === variableName) { + const updatedParameter = updatedParameters.find( + parameter => parameter.variableName === variableName + ) + if (updatedParameter != null) { + setChooseValueScreen(updatedParameter) + } + } + } + + // TODO(jr, 3/20/24): modify useCreateRunMutation to take in optional run time parameters + // newRunTimeParameters will be the param to plug in! const { createRun, isLoading } = useCreateRunMutation({ onSuccess: data => { queryClient @@ -199,17 +228,8 @@ export function ProtocolSetupParameters({ const handleConfirmValues = (): void => { createRun({ protocolId, labwareOffsets }) } - - return ( + let children = ( <> - {resetValuesModal ? ( - showResetValuesModal(false)} - /> - ) : null} - history.goBack()} @@ -230,14 +250,18 @@ export function ProtocolSetupParameters({ gridGap={SPACING.spacing8} paddingX={SPACING.spacing40} > - {parameters.map((parameter, index) => { + {runTimeParametersOverrides.map((parameter, index) => { return ( console.log('TODO: wire this up')} + onClickSetupStep={() => + parameter.type === 'bool' + ? updateParameters(!parameter.value, parameter.variableName) + : setChooseValueScreen(parameter) + } detail={formatRunTimeParameterValue(parameter, t)} description={parameter.description} fontSize="h4" @@ -248,4 +272,28 @@ export function ProtocolSetupParameters({
) + if (chooseValueScreen != null && chooseValueScreen.type === 'str') { + children = ( + setChooseValueScreen(null)} + parameter={chooseValueScreen} + setParameter={updateParameters} + rawValue={chooseValueScreen.value} + /> + ) + } + // TODO(jr, 4/1/24): add the int/float component + + return ( + <> + {resetValuesModal ? ( + showResetValuesModal(false)} + /> + ) : null} + {children} + + ) } diff --git a/app/src/pages/ProtocolDetails/Parameters.tsx b/app/src/pages/ProtocolDetails/Parameters.tsx index c43b56d7242..b8cbfa71155 100644 --- a/app/src/pages/ProtocolDetails/Parameters.tsx +++ b/app/src/pages/ProtocolDetails/Parameters.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' -import { formatRunTimeParameterValue } from '@opentrons/shared-data' +import { formatRunTimeParameterDefaultValue } from '@opentrons/shared-data' import { BORDERS, COLORS, @@ -118,7 +118,7 @@ export const Parameters = (props: { protocolId: string }): JSX.Element => { - {formatRunTimeParameterValue(parameter, t)} + {formatRunTimeParameterDefaultValue(parameter, t)} diff --git a/components/src/molecules/ParametersTable/index.tsx b/components/src/molecules/ParametersTable/index.tsx index 358b09c65c0..671646f19d0 100644 --- a/components/src/molecules/ParametersTable/index.tsx +++ b/components/src/molecules/ParametersTable/index.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import styled from 'styled-components' -import { formatRunTimeParameterValue } from '@opentrons/shared-data' +import { formatRunTimeParameterDefaultValue } from '@opentrons/shared-data' import { BORDERS } from '../../helix-design-system' import { SPACING, TYPOGRAPHY } from '../../ui-style-constants/index' import { StyledText } from '../../atoms/StyledText' @@ -69,7 +69,7 @@ export function ParametersTable({
- {formatRunTimeParameterValue(parameter, t)} + {formatRunTimeParameterDefaultValue(parameter, t)} diff --git a/shared-data/js/helpers/__tests__/formatRunTimeParameterValue.test.ts b/shared-data/js/helpers/__tests__/formatRunTimeParameterValue.test.ts index fec7bd7f4b2..a405d5845d3 100644 --- a/shared-data/js/helpers/__tests__/formatRunTimeParameterValue.test.ts +++ b/shared-data/js/helpers/__tests__/formatRunTimeParameterValue.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, vi } from 'vitest' -import { formatRunTimeParameterValue } from '../formatRunTimeParameterValue' +import { formatRunTimeParameterDefaultValue } from '../formatRunTimeParameterDefaultValue' import type { RunTimeParameter } from '../../types' @@ -9,7 +9,7 @@ const capitalizeFirstLetter = (str: string): string => { const mockTFunction = vi.fn(str => capitalizeFirstLetter(str)) -describe('utils-formatRunTimeParameterValue', () => { +describe('utils-formatRunTimeParameterDefaultValue', () => { it('should return value with suffix when type is int', () => { const mockData = { value: 6, @@ -21,7 +21,7 @@ describe('utils-formatRunTimeParameterValue', () => { max: 10, default: 6, } as RunTimeParameter - const result = formatRunTimeParameterValue(mockData, mockTFunction) + const result = formatRunTimeParameterDefaultValue(mockData, mockTFunction) expect(result).toEqual('6') }) @@ -37,7 +37,7 @@ describe('utils-formatRunTimeParameterValue', () => { max: 10.0, default: 6.5, } as RunTimeParameter - const result = formatRunTimeParameterValue(mockData, mockTFunction) + const result = formatRunTimeParameterDefaultValue(mockData, mockTFunction) expect(result).toEqual('6.5 mL') }) @@ -60,7 +60,7 @@ describe('utils-formatRunTimeParameterValue', () => { ], default: 'left', } as RunTimeParameter - const result = formatRunTimeParameterValue(mockData, mockTFunction) + const result = formatRunTimeParameterDefaultValue(mockData, mockTFunction) expect(result).toEqual('Left') }) @@ -73,7 +73,7 @@ describe('utils-formatRunTimeParameterValue', () => { type: 'bool', default: true, } as RunTimeParameter - const result = formatRunTimeParameterValue(mockData, mockTFunction) + const result = formatRunTimeParameterDefaultValue(mockData, mockTFunction) expect(result).toEqual('On') }) @@ -86,7 +86,7 @@ describe('utils-formatRunTimeParameterValue', () => { type: 'bool', default: false, } as RunTimeParameter - const result = formatRunTimeParameterValue(mockData, mockTFunction) + const result = formatRunTimeParameterDefaultValue(mockData, mockTFunction) expect(result).toEqual('Off') }) }) diff --git a/shared-data/js/helpers/formatRunTimeParameterDefaultValue.ts b/shared-data/js/helpers/formatRunTimeParameterDefaultValue.ts new file mode 100644 index 00000000000..78de4e78f02 --- /dev/null +++ b/shared-data/js/helpers/formatRunTimeParameterDefaultValue.ts @@ -0,0 +1,36 @@ +import type { RunTimeParameter } from '../types' + +export const formatRunTimeParameterDefaultValue = ( + runTimeParameter: RunTimeParameter, + t?: any +): string => { + const { type, default: defaultValue } = runTimeParameter + const suffix = + 'suffix' in runTimeParameter && runTimeParameter.suffix != null + ? runTimeParameter.suffix + : null + switch (type) { + case 'int': + case 'float': + return suffix !== null + ? `${defaultValue.toString()} ${suffix}` + : defaultValue.toString() + case 'bool': + if (t != null) { + return Boolean(defaultValue) ? t('on') : t('off') + } else { + return Boolean(defaultValue) ? 'On' : 'Off' + } + case 'str': + if ('choices' in runTimeParameter && runTimeParameter.choices != null) { + const choice = runTimeParameter.choices.find( + choice => choice.value === defaultValue + ) + if (choice != null) { + return choice.displayName + } + } + break + } + return '' +} diff --git a/shared-data/js/helpers/formatRunTimeParameterValue.ts b/shared-data/js/helpers/formatRunTimeParameterValue.ts index ed154bbcf8a..0aa0b72a194 100644 --- a/shared-data/js/helpers/formatRunTimeParameterValue.ts +++ b/shared-data/js/helpers/formatRunTimeParameterValue.ts @@ -1,10 +1,10 @@ -import { RunTimeParameter } from '../types' +import type { RunTimeParameter } from '../types' export const formatRunTimeParameterValue = ( runTimeParameter: RunTimeParameter, - t?: any + t: any ): string => { - const { type, default: defaultValue } = runTimeParameter + const { type, value } = runTimeParameter const suffix = 'suffix' in runTimeParameter && runTimeParameter.suffix != null ? runTimeParameter.suffix @@ -13,18 +13,15 @@ export const formatRunTimeParameterValue = ( case 'int': case 'float': return suffix !== null - ? `${defaultValue.toString()} ${suffix}` - : defaultValue.toString() - case 'bool': - if (t != null) { - return Boolean(defaultValue) ? t('on') : t('off') - } else { - return Boolean(defaultValue) ? 'On' : 'Off' - } + ? `${value.toString()} ${suffix}` + : value.toString() + case 'bool': { + return Boolean(value) ? t('on') : t('off') + } case 'str': if ('choices' in runTimeParameter && runTimeParameter.choices != null) { const choice = runTimeParameter.choices.find( - choice => choice.value === defaultValue + choice => choice.value === value ) if (choice != null) { return choice.displayName diff --git a/shared-data/js/helpers/index.ts b/shared-data/js/helpers/index.ts index 2d78f16ca1f..a65a83085de 100644 --- a/shared-data/js/helpers/index.ts +++ b/shared-data/js/helpers/index.ts @@ -28,6 +28,7 @@ export * from './getOccludedSlotCountForModule' export * from './labwareInference' export * from './getAddressableAreasInProtocol' export * from './getSimplestFlexDeckConfig' +export * from './formatRunTimeParameterDefaultValue' export * from './formatRunTimeParameterValue' export const getLabwareDefIsStandard = (def: LabwareDefinition2): boolean =>