From 80c6e517798f3551691a209de49f6e9db23f0b0f Mon Sep 17 00:00:00 2001 From: ncdiehl11 Date: Tue, 2 Apr 2024 11:34:36 -0400 Subject: [PATCH 1/6] stash --- app/src/assets/localization/en/shared.json | 1 + .../ChooseProtocolSlideout/index.tsx | 357 ++++++++++++++++-- 2 files changed, 333 insertions(+), 25 deletions(-) diff --git a/app/src/assets/localization/en/shared.json b/app/src/assets/localization/en/shared.json index 8c8bed0a5af..adb939134f8 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_protocol": "Change protocol", "change_robot": "Change robot", "clear_data": "clear data", "close_robot_door": "Close the robot door before starting the run.", diff --git a/app/src/organisms/ChooseProtocolSlideout/index.tsx b/app/src/organisms/ChooseProtocolSlideout/index.tsx index b6d1d2805ff..b4ae330f3d5 100644 --- a/app/src/organisms/ChooseProtocolSlideout/index.tsx +++ b/app/src/organisms/ChooseProtocolSlideout/index.tsx @@ -12,24 +12,36 @@ import { Box, COLORS, DIRECTION_COLUMN, + DIRECTION_ROW, DISPLAY_BLOCK, + DropdownOption, Flex, Icon, + Link as LinkComponent, JUSTIFY_CENTER, + JUSTIFY_END, + JUSTIFY_FLEX_START, OVERFLOW_WRAP_ANYWHERE, PrimaryButton, ProtocolDeck, SIZE_1, SPACING, + SecondaryButton, StyledText, TYPOGRAPHY, + useHoverTooltip, } from '@opentrons/components' import { useLogger } from '../../logger' import { OPENTRONS_USB } from '../../redux/discovery' import { getStoredProtocols } from '../../redux/protocol-storage' import { appShellRequestor } from '../../redux/shell/remote' -import { Slideout } from '../../atoms/Slideout' +import { useFeatureFlag } from '../../redux/config' +import { MultiSlideout } from '../../atoms/Slideout/MultiSlideout' +import { Tooltip } from '../../atoms/Tooltip' +import { ToggleButton } from '../../atoms/buttons' +import { InputField } from '../../atoms/InputField' +import { DropdownMenu } from '../../atoms/MenuList/DropdownMenu' import { MiniCard } from '../../molecules/MiniCard' import { useTrackCreateProtocolRunEvent } from '../Devices/hooks' import { useCreateRunFromProtocol } from '../ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol' @@ -39,6 +51,7 @@ import { getAnalysisStatus } from '../ProtocolsLanding/utils' import type { Robot } from '../../redux/discovery/types' import type { StoredProtocolData } from '../../redux/protocol-storage' import type { State } from '../../redux/types' +import type { RunTimeParameter } from '@opentrons/shared-data' export const CARD_OUTLINE_BORDER_STYLE = css` border-style: ${BORDERS.styleSolid}; @@ -54,6 +67,60 @@ const _getFileBaseName = (filePath: string): string => { return filePath.split('/').reverse()[0] } +const mockRunTimeParameters: RunTimeParameter[] = [ + { + displayName: 'Dry Run', + value: false, + variableName: 'DRYRUN', + description: 'Is this a dry or wet run? Wet is true, dry is false', + type: 'boolean', + default: false, + }, + { + value: 4, + displayName: 'Columns of Samples', + variableName: 'COLUMNS', + description: 'How many columns do you want?', + type: 'int', + min: 1, + max: 14, + default: 4, + }, + { + value: 6.5, + displayName: 'EtoH Volume', + variableName: 'ETOH_VOLUME', + description: '70% ethanol volume', + type: 'float', + suffix: 'mL', + min: 1.5, + max: 10.0, + default: 6.5, + }, + { + value: 'none', + displayName: 'Default Module Offsets', + variableName: 'DEFAULT_OFFSETS', + description: 'default module offsets for temp, H-S, and none', + type: 'str', + choices: [ + { + displayName: 'No offsets', + value: 'none', + }, + { + displayName: 'temp offset', + value: '1', + }, + { + displayName: 'heater-shaker offset', + value: '2', + }, + ], + default: 'none', + }, +] + interface ChooseProtocolSlideoutProps { robot: Robot onCloseClick: () => void @@ -72,6 +139,27 @@ export function ChooseProtocolSlideoutComponent( selectedProtocol, setSelectedProtocol, ] = React.useState(null) + // todo (nd:04/01/2024) look at analysis instead of mock data for RTP + // const runTimeParameters = + // selectedProtocol?.mostRecentAnalysis?.runTimeParameters ?? [] + const [ + runTimeParametersOverrides, + setRunTimeParametersOverrides, + ] = React.useState([]) + React.useEffect(() => { + setRunTimeParametersOverrides( + selectedProtocol?.mostRecentAnalysis?.runTimeParameters ?? [] + ) + }, [selectedProtocol]) + const runTimeParametersFromAnalysis = + selectedProtocol?.mostRecentAnalysis?.runTimeParameters ?? [] + + const [currentPage, setCurrentPage] = React.useState(1) + const enableRunTimeParametersFF = useFeatureFlag('enableRunTimeParameters') + const [targetProps, tooltipProps] = useHoverTooltip() + + console.log(runTimeParametersOverrides) + const analysisStatus = getAnalysisStatus( false, selectedProtocol?.mostRecentAnalysis @@ -141,10 +229,218 @@ export function ChooseProtocolSlideoutComponent( logger.warn('failed to create protocol, no protocol selected') } } + + const isRestoreDefaultsLinkEnabled = + runTimeParametersOverrides?.some( + parameter => parameter.value !== parameter.default + ) ?? false + + const runTimeParametersInputs = + runTimeParametersOverrides?.map((runtimeParam, index) => { + if ('choices' in runtimeParam) { + const dropdownOptions = runtimeParam.choices.map(choice => { + return { name: choice.displayName, value: choice.value } + }) as DropdownOption[] + return ( + { + return choice.value === runtimeParam.value + }) ?? dropdownOptions[0] + } + onClick={choice => { + const clone = runTimeParametersOverrides.map((parameter, i) => { + if (i === index) { + return { + ...parameter, + value: + dropdownOptions.find(option => option.value === choice) + ?.value ?? parameter.default, + } + } + return parameter + }) + if (setRunTimeParametersOverrides != null) { + setRunTimeParametersOverrides(clone) + } + }} + title={runtimeParam.displayName} + caption={runtimeParam.description} + width="100%" + dropdownType="neutral" + /> + ) + } else if (runtimeParam.type === 'int' || runtimeParam.type === 'float') { + const value = runtimeParam.value as number + const id = `InputField_${runtimeParam.variableName}_${index.toString()}` + return ( + { + const clone = runTimeParametersOverrides.map((parameter, i) => { + if (i === index) { + return { + ...parameter, + value: + runtimeParam.type === 'int' + ? Math.round(e.target.valueAsNumber) + : e.target.valueAsNumber, + } + } + return parameter + }) + if (setRunTimeParametersOverrides != null) { + setRunTimeParametersOverrides(clone) + } + }} + /> + ) + } else if (runtimeParam.type === 'boolean') { + return ( + + + {runtimeParam.displayName} + + + { + const clone = runTimeParametersOverrides.map( + (parameter, i) => { + if (i === index) { + return { + ...parameter, + value: !parameter.value, + } + } + return parameter + } + ) + if (setRunTimeParametersOverrides != null) { + setRunTimeParametersOverrides(clone) + } + }} + height="0.813rem" + label={ + runtimeParam.value + ? t('protocol_details:on') + : t('protocol_details:off') + } + paddingTop={SPACING.spacing2} // manual alignment of SVG with value label + /> + + {runtimeParam.value + ? t('protocol_details:on') + : t('protocol_details:off')} + + + + {runtimeParam.description} + + + ) + } + }) ?? null + + const pageTwoBody = ( + + + { + const clone = runTimeParametersOverrides.map(parameter => ({ + ...parameter, + value: parameter.default, + })) + if (setRunTimeParametersOverrides != null) { + setRunTimeParametersOverrides(clone) + } + }} + paddingBottom={SPACING.spacing10} + {...targetProps} + > + {t('protocol_details:restore_defaults')} + + {!isRestoreDefaultsLinkEnabled && ( + + {t('protocol_details:no_custom_values')} + + )} + + + {runTimeParametersInputs} + + + ) + + const singlePageFooter = ( + + {isCreatingRun ? ( + + ) : ( + t('shared:proceed_to_setup') + )} + + ) + + const multiPageFooter = + currentPage === 1 ? ( + setCurrentPage(2)} + width="100%" + disabled={isCreatingRun || selectedProtocol == null} + > + {t('shared:continue_to_param')} + + ) : ( + + setCurrentPage(1)} width="51%"> + {t('shared:change_protocol')} + + + {isCreatingRun ? ( + + ) : ( + t('shared:confirm_values') + )} + + + ) + return ( - - - {isCreatingRun ? ( - - ) : ( - t('shared:proceed_to_setup') - )} - + {enableRunTimeParametersFF && runTimeParametersFromAnalysis.length > 0 + ? multiPageFooter + : singlePageFooter} } > {showSlideout ? ( - { - if (!isCreatingRun) { - resetCreateRun() - setSelectedProtocol(storedProtocol) - } - }} - robotName={robot.name} - {...{ selectedProtocol, runCreationError, runCreationErrorCode }} - /> + currentPage === 1 ? ( + { + if (!isCreatingRun) { + resetCreateRun() + setSelectedProtocol(storedProtocol) + } + }} + robotName={robot.name} + {...{ selectedProtocol, runCreationError, runCreationErrorCode }} + /> + ) : ( + pageTwoBody + ) ) : null} - + ) } @@ -225,7 +517,7 @@ function StoredProtocolList(props: StoredProtocolListProps): JSX.Element { runCreationErrorCode, robotName, } = props - const { t } = useTranslation(['device_details', 'shared']) + const { t } = useTranslation(['device_details', 'protocol_details', 'shared']) const storedProtocols = useSelector((state: State) => getStoredProtocols(state) ) @@ -401,3 +693,18 @@ function StoredProtocolList(props: StoredProtocolListProps): JSX.Element { ) } + +const ENABLED_LINK_CSS = css` + ${TYPOGRAPHY.linkPSemiBold} + cursor: pointer; +` + +const DISABLED_LINK_CSS = css` + ${TYPOGRAPHY.linkPSemiBold} + color: ${COLORS.grey40}; + cursor: default; + + &:hover { + color: ${COLORS.grey40}; + } +` From 3011baa3309d20fa962dd9d27ebdfeeb984b09d0 Mon Sep 17 00:00:00 2001 From: ncdiehl11 Date: Tue, 2 Apr 2024 15:13:21 -0400 Subject: [PATCH 2/6] handle range errors, adjust total steps number based on presence of RTP --- .../ChooseProtocolSlideout/index.tsx | 81 +++++-------------- 1 file changed, 22 insertions(+), 59 deletions(-) diff --git a/app/src/organisms/ChooseProtocolSlideout/index.tsx b/app/src/organisms/ChooseProtocolSlideout/index.tsx index b4ae330f3d5..db29fc9b29c 100644 --- a/app/src/organisms/ChooseProtocolSlideout/index.tsx +++ b/app/src/organisms/ChooseProtocolSlideout/index.tsx @@ -67,60 +67,6 @@ const _getFileBaseName = (filePath: string): string => { return filePath.split('/').reverse()[0] } -const mockRunTimeParameters: RunTimeParameter[] = [ - { - displayName: 'Dry Run', - value: false, - variableName: 'DRYRUN', - description: 'Is this a dry or wet run? Wet is true, dry is false', - type: 'boolean', - default: false, - }, - { - value: 4, - displayName: 'Columns of Samples', - variableName: 'COLUMNS', - description: 'How many columns do you want?', - type: 'int', - min: 1, - max: 14, - default: 4, - }, - { - value: 6.5, - displayName: 'EtoH Volume', - variableName: 'ETOH_VOLUME', - description: '70% ethanol volume', - type: 'float', - suffix: 'mL', - min: 1.5, - max: 10.0, - default: 6.5, - }, - { - value: 'none', - displayName: 'Default Module Offsets', - variableName: 'DEFAULT_OFFSETS', - description: 'default module offsets for temp, H-S, and none', - type: 'str', - choices: [ - { - displayName: 'No offsets', - value: 'none', - }, - { - displayName: 'temp offset', - value: '1', - }, - { - displayName: 'heater-shaker offset', - value: '2', - }, - ], - default: 'none', - }, -] - interface ChooseProtocolSlideoutProps { robot: Robot onCloseClick: () => void @@ -156,6 +102,9 @@ export function ChooseProtocolSlideoutComponent( const [currentPage, setCurrentPage] = React.useState(1) const enableRunTimeParametersFF = useFeatureFlag('enableRunTimeParameters') + const hasRunTimeParameters = + enableRunTimeParametersFF && runTimeParametersFromAnalysis.length > 0 + const [targetProps, tooltipProps] = useHoverTooltip() console.log(runTimeParametersOverrides) @@ -286,6 +235,22 @@ export function ChooseProtocolSlideoutComponent( tooltipText={runtimeParam.description} caption={`${runtimeParam.min}-${runtimeParam.max}`} id={id} + error={ + Number.isNaN(value) || + value < runtimeParam.min || + value > runtimeParam.max + ? t(`protocol_details:value_out_of_range`, { + min: + runtimeParam.type === 'int' + ? runtimeParam.min + : runtimeParam.min.toFixed(1), + max: + runtimeParam.type === 'int' + ? runtimeParam.max + : runtimeParam.max.toFixed(1), + }) + : null + } onChange={e => { const clone = runTimeParametersOverrides.map((parameter, i) => { if (i === index) { @@ -305,7 +270,7 @@ export function ChooseProtocolSlideoutComponent( }} /> ) - } else if (runtimeParam.type === 'boolean') { + } else if (runtimeParam.type === 'bool') { return ( - {enableRunTimeParametersFF && runTimeParametersFromAnalysis.length > 0 - ? multiPageFooter - : singlePageFooter} + {hasRunTimeParameters ? multiPageFooter : singlePageFooter} } > From 60b8de0683e33def15d6a74b41716799cfc6bea8 Mon Sep 17 00:00:00 2001 From: ncdiehl11 Date: Tue, 2 Apr 2024 15:50:48 -0400 Subject: [PATCH 3/6] rearrange imports, remove console log --- .../ChooseProtocolSlideout/index.tsx | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/app/src/organisms/ChooseProtocolSlideout/index.tsx b/app/src/organisms/ChooseProtocolSlideout/index.tsx index db29fc9b29c..31a7bc68eba 100644 --- a/app/src/organisms/ChooseProtocolSlideout/index.tsx +++ b/app/src/organisms/ChooseProtocolSlideout/index.tsx @@ -48,10 +48,10 @@ import { useCreateRunFromProtocol } from '../ChooseRobotToRunProtocolSlideout/us import { ApplyHistoricOffsets } from '../ApplyHistoricOffsets' import { useOffsetCandidatesForAnalysis } from '../ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' import { getAnalysisStatus } from '../ProtocolsLanding/utils' +import type { RunTimeParameter } from '@opentrons/shared-data' import type { Robot } from '../../redux/discovery/types' import type { StoredProtocolData } from '../../redux/protocol-storage' import type { State } from '../../redux/types' -import type { RunTimeParameter } from '@opentrons/shared-data' export const CARD_OUTLINE_BORDER_STYLE = css` border-style: ${BORDERS.styleSolid}; @@ -78,6 +78,8 @@ export function ChooseProtocolSlideoutComponent( const { t } = useTranslation(['device_details', 'shared']) const history = useHistory() const logger = useLogger(new URL('', import.meta.url).pathname) + const [targetProps, tooltipProps] = useHoverTooltip() + const { robot, showSlideout, onCloseClick } = props const { name } = robot @@ -85,13 +87,13 @@ export function ChooseProtocolSlideoutComponent( selectedProtocol, setSelectedProtocol, ] = React.useState(null) - // todo (nd:04/01/2024) look at analysis instead of mock data for RTP - // const runTimeParameters = - // selectedProtocol?.mostRecentAnalysis?.runTimeParameters ?? [] const [ runTimeParametersOverrides, setRunTimeParametersOverrides, ] = React.useState([]) + const [currentPage, setCurrentPage] = React.useState(1) + const enableRunTimeParametersFF = useFeatureFlag('enableRunTimeParameters') + React.useEffect(() => { setRunTimeParametersOverrides( selectedProtocol?.mostRecentAnalysis?.runTimeParameters ?? [] @@ -100,15 +102,9 @@ export function ChooseProtocolSlideoutComponent( const runTimeParametersFromAnalysis = selectedProtocol?.mostRecentAnalysis?.runTimeParameters ?? [] - const [currentPage, setCurrentPage] = React.useState(1) - const enableRunTimeParametersFF = useFeatureFlag('enableRunTimeParameters') const hasRunTimeParameters = enableRunTimeParametersFF && runTimeParametersFromAnalysis.length > 0 - const [targetProps, tooltipProps] = useHoverTooltip() - - console.log(runTimeParametersOverrides) - const analysisStatus = getAnalysisStatus( false, selectedProtocol?.mostRecentAnalysis @@ -165,7 +161,14 @@ export function ChooseProtocolSlideoutComponent( location, definitionUri, })) - : [] + : [], + runTimeParametersOverrides.reduce( + (acc, param) => + param.value !== param.default + ? { ...acc, [param.variableName]: param.value } + : acc, + {} + ) ) const handleProceed: React.MouseEventHandler = () => { if (selectedProtocol != null) { From c9acd7c9a6bb18a768a96b41395bc4f60e9d0b87 Mon Sep 17 00:00:00 2001 From: ncdiehl11 Date: Tue, 2 Apr 2024 16:18:56 -0400 Subject: [PATCH 4/6] remove unncessary null checks --- .../organisms/ChooseProtocolSlideout/index.tsx | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/app/src/organisms/ChooseProtocolSlideout/index.tsx b/app/src/organisms/ChooseProtocolSlideout/index.tsx index 31a7bc68eba..74995a9cc98 100644 --- a/app/src/organisms/ChooseProtocolSlideout/index.tsx +++ b/app/src/organisms/ChooseProtocolSlideout/index.tsx @@ -214,9 +214,7 @@ export function ChooseProtocolSlideoutComponent( } return parameter }) - if (setRunTimeParametersOverrides != null) { - setRunTimeParametersOverrides(clone) - } + setRunTimeParametersOverrides(clone) }} title={runtimeParam.displayName} caption={runtimeParam.description} @@ -267,9 +265,7 @@ export function ChooseProtocolSlideoutComponent( } return parameter }) - if (setRunTimeParametersOverrides != null) { - setRunTimeParametersOverrides(clone) - } + setRunTimeParametersOverrides(clone) }} /> ) @@ -305,9 +301,7 @@ export function ChooseProtocolSlideoutComponent( return parameter } ) - if (setRunTimeParametersOverrides != null) { - setRunTimeParametersOverrides(clone) - } + setRunTimeParametersOverrides(clone) }} height="0.813rem" label={ @@ -344,9 +338,7 @@ export function ChooseProtocolSlideoutComponent( ...parameter, value: parameter.default, })) - if (setRunTimeParametersOverrides != null) { - setRunTimeParametersOverrides(clone) - } + setRunTimeParametersOverrides(clone) }} paddingBottom={SPACING.spacing10} {...targetProps} From 803ac1491fab3c44043154daa419d3629e1eed27 Mon Sep 17 00:00:00 2001 From: ncdiehl11 Date: Tue, 2 Apr 2024 16:23:53 -0400 Subject: [PATCH 5/6] explicit Icon size --- app/src/organisms/ChooseProtocolSlideout/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/organisms/ChooseProtocolSlideout/index.tsx b/app/src/organisms/ChooseProtocolSlideout/index.tsx index 74995a9cc98..34e9e4facea 100644 --- a/app/src/organisms/ChooseProtocolSlideout/index.tsx +++ b/app/src/organisms/ChooseProtocolSlideout/index.tsx @@ -364,7 +364,7 @@ export function ChooseProtocolSlideoutComponent( width="100%" > {isCreatingRun ? ( - + ) : ( t('shared:proceed_to_setup') )} From b0d8bae1ce46d09f11e00a4f0c8e2eed8229fe30 Mon Sep 17 00:00:00 2001 From: ncdiehl11 Date: Tue, 2 Apr 2024 17:40:36 -0400 Subject: [PATCH 6/6] lint --- .../ChooseProtocolSlideout/index.tsx | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/app/src/organisms/ChooseProtocolSlideout/index.tsx b/app/src/organisms/ChooseProtocolSlideout/index.tsx index 34e9e4facea..859b1ac4cd9 100644 --- a/app/src/organisms/ChooseProtocolSlideout/index.tsx +++ b/app/src/organisms/ChooseProtocolSlideout/index.tsx @@ -24,7 +24,6 @@ import { OVERFLOW_WRAP_ANYWHERE, PrimaryButton, ProtocolDeck, - SIZE_1, SPACING, SecondaryButton, StyledText, @@ -225,6 +224,21 @@ export function ChooseProtocolSlideoutComponent( } else if (runtimeParam.type === 'int' || runtimeParam.type === 'float') { const value = runtimeParam.value as number const id = `InputField_${runtimeParam.variableName}_${index.toString()}` + const error = + Number.isNaN(value) || + value < runtimeParam.min || + value > runtimeParam.max + ? t(`protocol_details:value_out_of_range`, { + min: + runtimeParam.type === 'int' + ? runtimeParam.min + : runtimeParam.min.toFixed(1), + max: + runtimeParam.type === 'int' + ? runtimeParam.max + : runtimeParam.max.toFixed(1), + }) + : null return ( runtimeParam.max - ? t(`protocol_details:value_out_of_range`, { - min: - runtimeParam.type === 'int' - ? runtimeParam.min - : runtimeParam.min.toFixed(1), - max: - runtimeParam.type === 'int' - ? runtimeParam.max - : runtimeParam.max.toFixed(1), - }) - : null - } + error={error} onChange={e => { const clone = runTimeParametersOverrides.map((parameter, i) => { if (i === index) {