From 97bf5f324ebb56f3313981563dd5dcafad1bdc5a Mon Sep 17 00:00:00 2001 From: Brent Hagen Date: Fri, 26 Apr 2024 12:10:46 -0400 Subject: [PATCH] feat(app): create hooks to anonymize instrument display names (#14978) OEM mode requires removing "Flex" from pipette and gripper names in the ODD. these hooks wrap the shared-data helpers used to get instrument display names and remove "Flex" when in OEM mode on the ODD. closes PLAT-261 --- app/src/LocalizationProvider.tsx | 11 +- .../AttachedInstrumentMountItem.tsx | 46 +++---- .../ProtocolInstrumentMountItem.tsx | 15 ++- .../ProtocolSetupInstruments.test.tsx | 3 + .../__tests__/InstrumentDetail.test.tsx | 25 ++-- app/src/pages/InstrumentDetail/index.tsx | 25 ++-- .../__tests__/InstrumentsDashboard.test.tsx | 2 +- app/src/pages/ProtocolDetails/Hardware.tsx | 113 +++++++++++------- app/src/pages/Protocols/hooks/index.ts | 2 +- app/src/redux/config/selectors.ts | 2 +- app/src/resources/instruments/hooks.ts | 62 ++++++++++ app/src/resources/robot-settings/hooks.ts | 22 ++++ shared-data/js/gripper.ts | 4 +- shared-data/js/pipettes.ts | 2 +- 14 files changed, 222 insertions(+), 112 deletions(-) create mode 100644 app/src/resources/instruments/hooks.ts create mode 100644 app/src/resources/robot-settings/hooks.ts diff --git a/app/src/LocalizationProvider.tsx b/app/src/LocalizationProvider.tsx index 4b9e10a1f8d..e2a30c95cd7 100644 --- a/app/src/LocalizationProvider.tsx +++ b/app/src/LocalizationProvider.tsx @@ -2,13 +2,10 @@ import * as React from 'react' import { I18nextProvider } from 'react-i18next' import reduce from 'lodash/reduce' -import { useRobotSettingsQuery } from '@opentrons/react-api-client' - import { resources } from './assets/localization' +import { useIsOEMMode } from './resources/robot-settings/hooks' import { i18n, i18nCb, i18nConfig } from './i18n' -import type { RobotSettingsField } from '@opentrons/api-client' - export interface OnDeviceLocalizationProviderProps { children?: React.ReactNode } @@ -20,11 +17,7 @@ const ANONYMOUS_RESOURCE = 'anonymous' export function OnDeviceLocalizationProvider( props: OnDeviceLocalizationProviderProps ): JSX.Element | null { - const { settings } = useRobotSettingsQuery().data ?? {} - const oemModeSetting = (settings ?? []).find( - (setting: RobotSettingsField) => setting?.id === 'enableOEMMode' - ) - const isOEMMode = oemModeSetting?.value ?? false + const isOEMMode = useIsOEMMode() // iterate through language resources, nested files, substitute anonymous file for branded file for OEM mode const anonResources = reduce( diff --git a/app/src/organisms/InstrumentMountItem/AttachedInstrumentMountItem.tsx b/app/src/organisms/InstrumentMountItem/AttachedInstrumentMountItem.tsx index 84abd47809d..042502275be 100644 --- a/app/src/organisms/InstrumentMountItem/AttachedInstrumentMountItem.tsx +++ b/app/src/organisms/InstrumentMountItem/AttachedInstrumentMountItem.tsx @@ -1,22 +1,23 @@ import * as React from 'react' import { useHistory } from 'react-router-dom' +import { SINGLE_MOUNT_PIPETTES } from '@opentrons/shared-data' + import { - getGripperDisplayName, - getPipetteModelSpecs, - GripperModel, - PipetteModel, - SINGLE_MOUNT_PIPETTES, -} from '@opentrons/shared-data' + useGripperDisplayName, + usePipetteModelSpecs, +} from '../../resources/instruments/hooks' import { ChoosePipette } from '../PipetteWizardFlows/ChoosePipette' import { FLOWS } from '../PipetteWizardFlows/constants' -import { PipetteWizardFlows } from '../PipetteWizardFlows' -import { GripperWizardFlows } from '../GripperWizardFlows' import { GRIPPER_FLOW_TYPES } from '../GripperWizardFlows/constants' import { LabeledMount } from './LabeledMount' + import type { InstrumentData } from '@opentrons/api-client' +import type { GripperModel, PipetteModel } from '@opentrons/shared-data' import type { Mount } from '../../redux/pipettes/types' import type { SelectablePipettes } from '../PipetteWizardFlows/types' +import type { GripperWizardFlows } from '../GripperWizardFlows' +import type { PipetteWizardFlows } from '../PipetteWizardFlows' interface AttachedInstrumentMountItemProps { mount: Mount | 'extension' @@ -62,22 +63,27 @@ export function AttachedInstrumentMountItem( history.push(`/instruments/${mount}`) } } - let displayName - if (attachedInstrument != null && attachedInstrument.ok) { - displayName = - attachedInstrument?.mount !== 'extension' - ? getPipetteModelSpecs( - attachedInstrument?.instrumentModel as PipetteModel - )?.displayName - : getGripperDisplayName( - attachedInstrument?.instrumentModel as GripperModel - ) - } + + const instrumentModel = attachedInstrument?.ok + ? attachedInstrument.instrumentModel + : null + + const pipetteDisplayName = + usePipetteModelSpecs(instrumentModel as PipetteModel)?.displayName ?? null + const gripperDisplayName = useGripperDisplayName( + instrumentModel as GripperModel + ) + + const displayName = + attachedInstrument?.ok && attachedInstrument?.mount === 'extension' + ? gripperDisplayName + : pipetteDisplayName + return ( <> {showChoosePipetteModal ? ( diff --git a/app/src/organisms/InstrumentMountItem/ProtocolInstrumentMountItem.tsx b/app/src/organisms/InstrumentMountItem/ProtocolInstrumentMountItem.tsx index a350e13f6b9..80e160437e8 100644 --- a/app/src/organisms/InstrumentMountItem/ProtocolInstrumentMountItem.tsx +++ b/app/src/organisms/InstrumentMountItem/ProtocolInstrumentMountItem.tsx @@ -14,8 +14,6 @@ import { JUSTIFY_FLEX_START, } from '@opentrons/components' import { - getGripperDisplayName, - getPipetteNameSpecs, NINETY_SIX_CHANNEL, PipetteName, SINGLE_MOUNT_PIPETTES, @@ -23,6 +21,10 @@ import { } from '@opentrons/shared-data' import { SmallButton } from '../../atoms/buttons' +import { + useGripperDisplayName, + usePipetteNameSpecs, +} from '../../resources/instruments/hooks' import { FLOWS } from '../PipetteWizardFlows/constants' import { PipetteWizardFlows } from '../PipetteWizardFlows' import { GripperWizardFlows } from '../GripperWizardFlows' @@ -97,6 +99,11 @@ export function ProtocolInstrumentMountItem( attachedInstrument != null && attachedInstrument.ok && attachedInstrument?.data?.calibratedOffset?.last_modified != null + + const gripperDisplayName = useGripperDisplayName(speccedName as GripperModel) + const pipetteDisplayName = usePipetteNameSpecs(speccedName as PipetteName) + ?.displayName + return ( <> @@ -117,9 +124,7 @@ export function ProtocolInstrumentMountItem( )} - {mount === 'extension' - ? getGripperDisplayName(speccedName as GripperModel) - : getPipetteNameSpecs(speccedName as PipetteName)?.displayName} + {mount === 'extension' ? gripperDisplayName : pipetteDisplayName} diff --git a/app/src/organisms/ProtocolSetupInstruments/__tests__/ProtocolSetupInstruments.test.tsx b/app/src/organisms/ProtocolSetupInstruments/__tests__/ProtocolSetupInstruments.test.tsx index 7ab8f2b97b1..bcfc0ecf1d6 100644 --- a/app/src/organisms/ProtocolSetupInstruments/__tests__/ProtocolSetupInstruments.test.tsx +++ b/app/src/organisms/ProtocolSetupInstruments/__tests__/ProtocolSetupInstruments.test.tsx @@ -12,6 +12,7 @@ import { import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' import { useMostRecentCompletedAnalysis } from '../../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' +import { useIsOEMMode } from '../../../resources/robot-settings/hooks' import { mockRecentAnalysis } from '../__fixtures__' import { ProtocolSetupInstruments } from '..' @@ -19,6 +20,7 @@ vi.mock('@opentrons/react-api-client') vi.mock( '../../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' ) +vi.mock('../../../resources/robot-settings/hooks') const mockGripperData = { instrumentModel: 'gripper_v1', @@ -71,6 +73,7 @@ describe('ProtocolSetupInstruments', () => { data: [mockLeftPipetteData, mockRightPipetteData, mockGripperData], }, } as any) + vi.mocked(useIsOEMMode).mockReturnValue(false) }) afterEach(() => { vi.resetAllMocks() diff --git a/app/src/pages/InstrumentDetail/__tests__/InstrumentDetail.test.tsx b/app/src/pages/InstrumentDetail/__tests__/InstrumentDetail.test.tsx index 56f1c4a11b3..0743afedbe1 100644 --- a/app/src/pages/InstrumentDetail/__tests__/InstrumentDetail.test.tsx +++ b/app/src/pages/InstrumentDetail/__tests__/InstrumentDetail.test.tsx @@ -4,30 +4,24 @@ import { useParams } from 'react-router-dom' import { useInstrumentsQuery } from '@opentrons/react-api-client' import { renderWithProviders } from '../../../__testing-utils__' -import { - getPipetteModelSpecs, - getGripperDisplayName, -} from '@opentrons/shared-data' import { i18n } from '../../../i18n' import { InstrumentDetail } from '../../../pages/InstrumentDetail' +import { + useGripperDisplayName, + usePipetteModelSpecs, +} from '../../../resources/instruments/hooks' +import { useIsOEMMode } from '../../../resources/robot-settings/hooks' import type { Instruments } from '@opentrons/api-client' -import type * as SharedData from '@opentrons/shared-data' vi.mock('@opentrons/react-api-client') -vi.mock('@opentrons/shared-data', async importOriginal => { - const actual = await importOriginal() - return { - ...actual, - getPipetteModelSpecs: vi.fn(), - getGripperDisplayName: vi.fn(), - } -}) vi.mock('react-router-dom', () => ({ useParams: vi.fn(), useHistory: vi.fn(), })) +vi.mock('../../../resources/instruments/hooks') +vi.mock('../../../resources/robot-settings/hooks') const render = () => { return renderWithProviders(, { @@ -97,11 +91,12 @@ describe('InstrumentDetail', () => { vi.mocked(useInstrumentsQuery).mockReturnValue({ data: mockInstrumentsQuery, } as any) - vi.mocked(getPipetteModelSpecs).mockReturnValue({ + vi.mocked(usePipetteModelSpecs).mockReturnValue({ displayName: 'mockPipette', } as any) - vi.mocked(getGripperDisplayName).mockReturnValue('mockGripper') + vi.mocked(useGripperDisplayName).mockReturnValue('mockGripper') vi.mocked(useParams).mockReturnValue({ mount: 'left' }) + vi.mocked(useIsOEMMode).mockReturnValue(false) }) afterEach(() => { diff --git a/app/src/pages/InstrumentDetail/index.tsx b/app/src/pages/InstrumentDetail/index.tsx index 6bf5ddc8433..cececd01703 100644 --- a/app/src/pages/InstrumentDetail/index.tsx +++ b/app/src/pages/InstrumentDetail/index.tsx @@ -2,12 +2,6 @@ import * as React from 'react' import { useParams } from 'react-router-dom' import styled from 'styled-components' -import { - getGripperDisplayName, - getPipetteModelSpecs, - GripperModel, - PipetteModel, -} from '@opentrons/shared-data' import { useInstrumentsQuery, useHost } from '@opentrons/react-api-client' import { Icon, @@ -21,11 +15,16 @@ import { } from '@opentrons/components' import { BackButton } from '../../atoms/buttons/BackButton' +import { ODD_FOCUS_VISIBLE } from '../../atoms/buttons/constants' import { InstrumentInfo } from '../../organisms/InstrumentInfo' import { handleInstrumentDetailOverflowMenu } from '../../pages/InstrumentDetail/InstrumentDetailOverflowMenu' -import { ODD_FOCUS_VISIBLE } from '../../atoms/buttons/constants' +import { + useGripperDisplayName, + usePipetteModelSpecs, +} from '../../resources/instruments/hooks' import type { GripperData, PipetteData } from '@opentrons/api-client' +import type { GripperModel, PipetteModel } from '@opentrons/shared-data' export const InstrumentDetail = (): JSX.Element => { const host = useHost() @@ -36,11 +35,15 @@ export const InstrumentDetail = (): JSX.Element => { (i): i is PipetteData | GripperData => i.ok && i.mount === mount ) ?? null + const pipetteDisplayName = usePipetteModelSpecs( + instrument?.instrumentModel as PipetteModel + )?.displayName + const gripperDisplayName = useGripperDisplayName( + instrument?.instrumentModel as GripperModel + ) + const displayName = - instrument?.mount !== 'extension' - ? getPipetteModelSpecs(instrument?.instrumentModel as PipetteModel) - ?.displayName - : getGripperDisplayName(instrument?.instrumentModel as GripperModel) + instrument?.mount !== 'extension' ? pipetteDisplayName : gripperDisplayName return ( <> diff --git a/app/src/pages/InstrumentsDashboard/__tests__/InstrumentsDashboard.test.tsx b/app/src/pages/InstrumentsDashboard/__tests__/InstrumentsDashboard.test.tsx index d816731eea1..06e040bbe39 100644 --- a/app/src/pages/InstrumentsDashboard/__tests__/InstrumentsDashboard.test.tsx +++ b/app/src/pages/InstrumentsDashboard/__tests__/InstrumentsDashboard.test.tsx @@ -14,7 +14,7 @@ import { InstrumentDetail } from '../../../pages/InstrumentDetail' import type * as ReactApiClient from '@opentrons/react-api-client' const mockGripperData = { - instrumentModel: 'gripper_v1', + instrumentModel: 'gripperV1', instrumentType: 'gripper', mount: 'extension', serialNumber: 'ghi789', diff --git a/app/src/pages/ProtocolDetails/Hardware.tsx b/app/src/pages/ProtocolDetails/Hardware.tsx index da6d1f2633c..c59c24e7118 100644 --- a/app/src/pages/ProtocolDetails/Hardware.tsx +++ b/app/src/pages/ProtocolDetails/Hardware.tsx @@ -14,18 +14,21 @@ import { WRAP, } from '@opentrons/components' import { - GRIPPER_V1, getCutoutDisplayName, - getGripperDisplayName, getModuleDisplayName, getModuleType, - getPipetteNameSpecs, getFixtureDisplayName, + GRIPPER_V1_2, } from '@opentrons/shared-data' + +import { + useGripperDisplayName, + usePipetteNameSpecs, +} from '../../resources/instruments/hooks' import { useRequiredProtocolHardware } from '../Protocols/hooks' import { EmptySection } from './EmptySection' -import type { ProtocolHardware } from '../Protocols/hooks' +import type { ProtocolHardware, ProtocolPipette } from '../Protocols/hooks' import type { TFunction } from 'i18next' const Table = styled('table')` @@ -75,11 +78,19 @@ const getHardwareLocation = ( } } -const getHardwareName = (protocolHardware: ProtocolHardware): string => { +// convert to anon + +const useHardwareName = (protocolHardware: ProtocolHardware): string => { + const gripperDisplayName = useGripperDisplayName(GRIPPER_V1_2) + + const pipetteDisplayName = + usePipetteNameSpecs((protocolHardware as ProtocolPipette).pipetteName) + ?.displayName ?? '' + if (protocolHardware.hardwareType === 'gripper') { - return getGripperDisplayName(GRIPPER_V1) + return gripperDisplayName } else if (protocolHardware.hardwareType === 'pipette') { - return getPipetteNameSpecs(protocolHardware.pipetteName)?.displayName ?? '' + return pipetteDisplayName } else if (protocolHardware.hardwareType === 'module') { return getModuleDisplayName(protocolHardware.moduleModel) } else { @@ -87,6 +98,54 @@ const getHardwareName = (protocolHardware: ProtocolHardware): string => { } } +function HardwareItem({ + hardware, +}: { + hardware: ProtocolHardware +}): JSX.Element { + const { t, i18n } = useTranslation('protocol_details') + + const hardwareName = useHardwareName(hardware) + + let location: JSX.Element = ( + + {i18n.format(getHardwareLocation(hardware, t), 'titleCase')} + + ) + if (hardware.hardwareType === 'module') { + location = + } else if (hardware.hardwareType === 'fixture') { + location = ( + + ) + } + return ( + + + {location} + + + + {hardware.hardwareType === 'module' && ( + + + + )} + {hardwareName} + + + + ) +} + export const Hardware = (props: { protocolId: string }): JSX.Element => { const { requiredProtocolHardware } = useRequiredProtocolHardware( props.protocolId @@ -123,45 +182,7 @@ export const Hardware = (props: { protocolId: string }): JSX.Element => { {requiredProtocolHardware.map((hardware, id) => { - let location: JSX.Element = ( - - {i18n.format(getHardwareLocation(hardware, t), 'titleCase')} - - ) - if (hardware.hardwareType === 'module') { - location = - } else if (hardware.hardwareType === 'fixture') { - location = ( - - ) - } - return ( - - - {location} - - - - {hardware.hardwareType === 'module' && ( - - - - )} - {getHardwareName(hardware)} - - - - ) + return })} diff --git a/app/src/pages/Protocols/hooks/index.ts b/app/src/pages/Protocols/hooks/index.ts index 7335b9482ea..bcdb9793e69 100644 --- a/app/src/pages/Protocols/hooks/index.ts +++ b/app/src/pages/Protocols/hooks/index.ts @@ -31,7 +31,7 @@ import type { } from '@opentrons/shared-data' import type { LabwareSetupItem } from '../utils' -interface ProtocolPipette { +export interface ProtocolPipette { hardwareType: 'pipette' pipetteName: PipetteName mount: 'left' | 'right' diff --git a/app/src/redux/config/selectors.ts b/app/src/redux/config/selectors.ts index 4d32befab43..3e630973e51 100644 --- a/app/src/redux/config/selectors.ts +++ b/app/src/redux/config/selectors.ts @@ -78,7 +78,7 @@ export const getUpdateChannelOptions = (state: State): SelectOption[] => { export const getIsOnDevice: (state: State) => boolean = createSelector( getConfig, - config => config?.isOnDevice ?? false + config => !!(config?.isOnDevice ?? false) ) export const getProtocolsDesktopSortKey: ( diff --git a/app/src/resources/instruments/hooks.ts b/app/src/resources/instruments/hooks.ts new file mode 100644 index 00000000000..31a40f5fdd0 --- /dev/null +++ b/app/src/resources/instruments/hooks.ts @@ -0,0 +1,62 @@ +import { + getGripperDisplayName, + getPipetteModelSpecs, + getPipetteNameSpecs, + GRIPPER_MODELS, +} from '@opentrons/shared-data' +import { useIsOEMMode } from '../robot-settings/hooks' + +import type { + GripperModel, + PipetteModel, + PipetteModelSpecs, + PipetteName, + PipetteNameSpecs, +} from '@opentrons/shared-data' + +export function usePipetteNameSpecs( + name: PipetteName +): PipetteNameSpecs | null { + const isOEMMode = useIsOEMMode() + const pipetteNameSpecs = getPipetteNameSpecs(name) + + if (pipetteNameSpecs == null) return null + + const brandedDisplayName = pipetteNameSpecs.displayName + const anonymizedDisplayName = pipetteNameSpecs.displayName.replace( + 'Flex ', + '' + ) + + const displayName = isOEMMode ? anonymizedDisplayName : brandedDisplayName + + return { ...pipetteNameSpecs, displayName } +} + +export function usePipetteModelSpecs( + model: PipetteModel +): PipetteModelSpecs | null { + const modelSpecificFields = getPipetteModelSpecs(model) + const pipetteNameSpecs = usePipetteNameSpecs( + modelSpecificFields?.name as PipetteName + ) + + if (modelSpecificFields == null || pipetteNameSpecs == null) return null + + return { ...modelSpecificFields, displayName: pipetteNameSpecs.displayName } +} + +export function useGripperDisplayName(gripperModel: GripperModel): string { + const isOEMMode = useIsOEMMode() + + let brandedDisplayName = '' + + // check to only call display name helper for a gripper model + if (GRIPPER_MODELS.includes(gripperModel)) { + brandedDisplayName = getGripperDisplayName(gripperModel) + } + + const anonymizedDisplayName = brandedDisplayName.replace('Flex ', '') + + return isOEMMode ? anonymizedDisplayName : brandedDisplayName +} diff --git a/app/src/resources/robot-settings/hooks.ts b/app/src/resources/robot-settings/hooks.ts new file mode 100644 index 00000000000..a548b154b56 --- /dev/null +++ b/app/src/resources/robot-settings/hooks.ts @@ -0,0 +1,22 @@ +import { useSelector } from 'react-redux' +import { useRobotSettingsQuery } from '@opentrons/react-api-client' +import { getIsOnDevice } from '../../redux/config' + +import type { RobotSettingsField } from '@opentrons/api-client' + +/** + * a hook to tell the ODD that the robot is in OEM mode + * limit to ODD, since some instrument name hooks will be common to both ODD and desktop + * @returns boolean + */ +export function useIsOEMMode(): boolean { + const { settings } = useRobotSettingsQuery().data ?? {} + const isOnDevice = useSelector(getIsOnDevice) + + const oemModeSetting = + (settings ?? []).find( + (setting: RobotSettingsField) => setting?.id === 'enableOEMMode' + )?.value ?? false + + return oemModeSetting && isOnDevice +} diff --git a/shared-data/js/gripper.ts b/shared-data/js/gripper.ts index 3a2714e30c1..15c1d3f7f7b 100644 --- a/shared-data/js/gripper.ts +++ b/shared-data/js/gripper.ts @@ -18,9 +18,9 @@ export const getGripperDef = ( return gripperV1_2 as GripperDefinition default: console.warn( - `Could not find a gripper with model ${gripperModel}, falling back to most recent definition: ${GRIPPER_V1_1}` + `Could not find a gripper with model ${gripperModel}, falling back to most recent definition: ${GRIPPER_V1_2}` ) - return gripperV1_1 as GripperDefinition + return gripperV1_2 as GripperDefinition } } diff --git a/shared-data/js/pipettes.ts b/shared-data/js/pipettes.ts index 19a78bd1424..0901d10ae42 100644 --- a/shared-data/js/pipettes.ts +++ b/shared-data/js/pipettes.ts @@ -60,7 +60,7 @@ export function getPipetteNameSpecs( // NOTE: this should NEVER be used in PD, which is model-agnostic export function getPipetteModelSpecs( model: PipetteModel -): PipetteModelSpecs | null | undefined { +): PipetteModelSpecs | null { const modelSpecificFields = pipetteModelSpecs.config[model] const modelFields = modelSpecificFields &&