From bb1f1ce56c7ab93527d5a05bfb2ca17dd7c43a28 Mon Sep 17 00:00:00 2001 From: Brian Arthur Cooper Date: Thu, 16 May 2024 19:43:26 -0400 Subject: [PATCH] refactor(app): make on device display deck configurator updates live (#15202) Instead of the previous method of staging a batch of changes to deck configuration before commiting them with the confirm button, to avoid synchronization issues, make the ODD deck configurator behave like the desktop version. all updates immediately permeate to the robot's source of truth Re: https://opentrons.atlassian.net/browse/RQA-2669 --- app/src/assets/localization/en/shared.json | 1 + .../AddFixtureModal.stories.tsx | 2 +- .../AddFixtureModal.tsx | 42 ++---- .../__tests__/AddFixtureModal.test.tsx | 9 +- .../DeviceDetailsDeckConfiguration.test.tsx | 31 +++- .../DeviceDetailsDeckConfiguration/index.tsx | 94 ++---------- .../ProtocolSetupDeckConfiguration.test.tsx | 20 +-- .../ProtocolSetupDeckConfiguration/index.tsx | 37 +++-- .../__tests__/DeckConfiguration.test.tsx | 34 ++--- app/src/pages/DeckConfiguration/index.tsx | 134 +++--------------- .../{hooks.ts => hooks.tsx} | 97 +++++++++++++ 11 files changed, 202 insertions(+), 299 deletions(-) rename app/src/resources/deck_configuration/{hooks.ts => hooks.tsx} (53%) diff --git a/app/src/assets/localization/en/shared.json b/app/src/assets/localization/en/shared.json index 5613508b242..899adfc5a31 100644 --- a/app/src/assets/localization/en/shared.json +++ b/app/src/assets/localization/en/shared.json @@ -63,6 +63,7 @@ "robot_is_busy": "Robot is busy", "robot_is_reachable_but_not_responding": "This robot's API server is not responding correctly to requests at IP address {{hostname}}", "robot_was_seen_but_is_unreachable": "This robot has been seen recently, but is currently not reachable at IP address {{hostname}}", + "save": "save", "something_went_wrong": "something went wrong", "sort_by": "Sort by", "stand_back_robot_is_in_motion": "Stand back, robot is in motion", diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.stories.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.stories.tsx index 034a18c1e77..de7c062f9d1 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.stories.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.stories.tsx @@ -26,6 +26,6 @@ const Template: Story> = args => ( export const Default = Template.bind({}) Default.args = { fixtureLocation: 'cutoutD3', - setShowAddFixtureModal: () => {}, + closeModal: () => {}, isOnDevice: true, } diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx index 47ec3077d61..c7452bd4ebc 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx @@ -54,15 +54,13 @@ import type { CutoutConfig, CutoutId, CutoutFixtureId, - DeckConfiguration, } from '@opentrons/shared-data' import type { ModalHeaderBaseProps } from '../../molecules/Modal/types' import type { LegacyModalProps } from '../../molecules/LegacyModal' interface AddFixtureModalProps { cutoutId: CutoutId - setShowAddFixtureModal: (showAddFixtureModal: boolean) => void - setCurrentDeckConfig?: React.Dispatch> + closeModal: () => void providedFixtureOptions?: CutoutFixtureId[] isOnDevice?: boolean } @@ -75,8 +73,7 @@ type OptionStage = export function AddFixtureModal({ cutoutId, - setShowAddFixtureModal, - setCurrentDeckConfig, + closeModal, providedFixtureOptions, isOnDevice = false, }: AddFixtureModalProps): JSX.Element { @@ -109,18 +106,14 @@ export function AddFixtureModal({ slotName: getCutoutDisplayName(cutoutId), }), hasExitIcon: providedFixtureOptions == null, - onClick: () => { - setShowAddFixtureModal(false) - }, + onClick: closeModal, } const modalProps: LegacyModalProps = { title: t('add_to_slot', { slotName: getCutoutDisplayName(cutoutId), }), - onClose: () => { - setShowAddFixtureModal(false) - }, + onClose: closeModal, closeOnOutsideClick: true, childrenPadding: SPACING.spacing24, width: '26.75rem', @@ -289,22 +282,7 @@ export function AddFixtureModal({ ) } - const handleAddODD = (addedCutoutConfigs: CutoutConfig[]): void => { - if (setCurrentDeckConfig != null) - setCurrentDeckConfig( - (prevDeckConfig: DeckConfiguration): DeckConfiguration => - prevDeckConfig.map((fixture: CutoutConfig) => { - const replacementCutoutConfig = addedCutoutConfigs.find( - c => c.cutoutId === fixture.cutoutId - ) - return replacementCutoutConfig ?? fixture - }) - ) - - setShowAddFixtureModal(false) - } - - const handleAddDesktop = (addedCutoutConfigs: CutoutConfig[]): void => { + const handleAddFixture = (addedCutoutConfigs: CutoutConfig[]): void => { const newDeckConfig = deckConfig.map(fixture => { const replacementCutoutConfig = addedCutoutConfigs.find( c => c.cutoutId === fixture.cutoutId @@ -313,7 +291,7 @@ export function AddFixtureModal({ }) updateDeckConfiguration(newDeckConfig) - setShowAddFixtureModal(false) + closeModal() } const fixtureOptions = availableOptions.map(cutoutConfigs => ( @@ -327,9 +305,7 @@ export function AddFixtureModal({ )} buttonText={t('add')} onClickHandler={() => { - isOnDevice - ? handleAddODD(cutoutConfigs) - : handleAddDesktop(cutoutConfigs) + handleAddFixture(cutoutConfigs) }} isOnDevice={isOnDevice} /> @@ -341,9 +317,7 @@ export function AddFixtureModal({ - providedFixtureOptions != null - ? null - : setShowAddFixtureModal(false) + providedFixtureOptions != null ? null : closeModal() } > diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/AddFixtureModal.test.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/AddFixtureModal.test.tsx index 38cc283f8e9..17ae8511513 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/AddFixtureModal.test.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/AddFixtureModal.test.tsx @@ -23,9 +23,8 @@ import type { Modules } from '@opentrons/api-client' vi.mock('@opentrons/react-api-client') vi.mock('../../../resources/deck_configuration') -const mockSetShowAddFixtureModal = vi.fn() +const mockCloseModal = vi.fn() const mockUpdateDeckConfiguration = vi.fn() -const mockSetCurrentDeckConfig = vi.fn() const render = (props: React.ComponentProps) => { return renderWithProviders(, { @@ -39,8 +38,7 @@ describe('Touchscreen AddFixtureModal', () => { beforeEach(() => { props = { cutoutId: 'cutoutD3', - setShowAddFixtureModal: mockSetShowAddFixtureModal, - setCurrentDeckConfig: mockSetCurrentDeckConfig, + closeModal: mockCloseModal, isOnDevice: true, } vi.mocked(useUpdateDeckConfigurationMutation).mockReturnValue({ @@ -69,7 +67,6 @@ describe('Touchscreen AddFixtureModal', () => { render(props) fireEvent.click(screen.getAllByText('Select options')[1]) fireEvent.click(screen.getAllByText('Add')[0]) - expect(mockSetCurrentDeckConfig).toHaveBeenCalled() }) it('when fixture options are provided, should only render those options', () => { @@ -96,7 +93,7 @@ describe('Desktop AddFixtureModal', () => { beforeEach(() => { props = { cutoutId: 'cutoutD3', - setShowAddFixtureModal: mockSetShowAddFixtureModal, + closeModal: mockCloseModal, } vi.mocked(useUpdateDeckConfigurationMutation).mockReturnValue({ updateDeckConfiguration: mockUpdateDeckConfiguration, diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/DeviceDetailsDeckConfiguration.test.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/DeviceDetailsDeckConfiguration.test.tsx index e6d048bcf52..27b6bbad2ba 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/DeviceDetailsDeckConfiguration.test.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/DeviceDetailsDeckConfiguration.test.tsx @@ -3,6 +3,10 @@ import { fireEvent, screen } from '@testing-library/react' import { when } from 'vitest-when' import { describe, it, beforeEach, vi, afterEach } from 'vitest' +import { + DeckConfiguration, + TRASH_BIN_ADAPTER_FIXTURE, +} from '@opentrons/shared-data' import { DeckConfigurator } from '@opentrons/components' import { useModulesQuery, @@ -16,8 +20,12 @@ import { DeckFixtureSetupInstructionsModal } from '../DeckFixtureSetupInstructio import { useIsEstopNotDisengaged } from '../../../resources/devices/hooks/useIsEstopNotDisengaged' import { DeviceDetailsDeckConfiguration } from '../' import { useNotifyCurrentMaintenanceRun } from '../../../resources/maintenance_runs' -import { useNotifyDeckConfigurationQuery } from '../../../resources/deck_configuration' +import { + useDeckConfigurationEditingTools, + useNotifyDeckConfigurationQuery, +} from '../../../resources/deck_configuration' +import type { UseQueryResult } from 'react-query' import type { MaintenanceRun } from '@opentrons/api-client' import type * as OpentronsComponents from '@opentrons/components' @@ -35,6 +43,12 @@ vi.mock('../../../resources/maintenance_runs') vi.mock('../../../resources/devices/hooks/useIsEstopNotDisengaged') vi.mock('../../../resources/deck_configuration') +const mockDeckConfig = [ + { + cutoutId: 'cutoutC3', + cutoutFixtureId: TRASH_BIN_ADAPTER_FIXTURE, + }, +] const ROBOT_NAME = 'otie' const mockUpdateDeckConfiguration = vi.fn() const RUN_STATUSES = { @@ -63,9 +77,6 @@ describe('DeviceDetailsDeckConfiguration', () => { robotName: ROBOT_NAME, } vi.mocked(useModulesQuery).mockReturnValue({ data: { data: [] } } as any) - vi.mocked(useNotifyDeckConfigurationQuery).mockReturnValue({ - data: [], - } as any) vi.mocked(useUpdateDeckConfigurationMutation).mockReturnValue({ updateDeckConfiguration: mockUpdateDeckConfiguration, } as any) @@ -83,6 +94,14 @@ describe('DeviceDetailsDeckConfiguration', () => { .calledWith(ROBOT_NAME) .thenReturn(false) when(vi.mocked(useIsRobotViewable)).calledWith(ROBOT_NAME).thenReturn(true) + vi.mocked(useNotifyDeckConfigurationQuery).mockReturnValue({ + data: mockDeckConfig, + } as UseQueryResult) + vi.mocked(useDeckConfigurationEditingTools).mockReturnValue({ + addFixtureToCutout: vi.fn(), + removeFixtureFromCutout: vi.fn(), + addFixtureModal: null, + }) }) afterEach(() => { @@ -132,7 +151,9 @@ describe('DeviceDetailsDeckConfiguration', () => { }) it('should render no deck fixtures, if deck configs are not set', () => { - vi.mocked(useNotifyDeckConfigurationQuery).mockReturnValue([] as any) + vi.mocked(useNotifyDeckConfigurationQuery).mockReturnValue({ + data: [], + } as any) render(props) screen.getByText('No deck fixtures') }) diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx index b6f62c8c08a..d28df33bf69 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx @@ -19,19 +19,11 @@ import { StyledText, TYPOGRAPHY, } from '@opentrons/components' -import { - useModulesQuery, - useUpdateDeckConfigurationMutation, -} from '@opentrons/react-api-client' +import { useModulesQuery } from '@opentrons/react-api-client' import { getCutoutDisplayName, getFixtureDisplayName, - SINGLE_RIGHT_CUTOUTS, SINGLE_SLOT_FIXTURES, - SINGLE_LEFT_SLOT_FIXTURE, - SINGLE_RIGHT_SLOT_FIXTURE, - SINGLE_CENTER_SLOT_FIXTURE, - SINGLE_LEFT_CUTOUTS, getDeckDefFromRobotType, FLEX_ROBOT_TYPE, } from '@opentrons/shared-data' @@ -39,12 +31,14 @@ import { import { useNotifyCurrentMaintenanceRun } from '../../resources/maintenance_runs' import { Banner } from '../../atoms/Banner' import { DeckFixtureSetupInstructionsModal } from './DeckFixtureSetupInstructionsModal' -import { AddFixtureModal } from './AddFixtureModal' import { useIsRobotViewable, useRunStatuses } from '../Devices/hooks' import { useIsEstopNotDisengaged } from '../../resources/devices/hooks/useIsEstopNotDisengaged' -import { useNotifyDeckConfigurationQuery } from '../../resources/deck_configuration' +import { + useDeckConfigurationEditingTools, + useNotifyDeckConfigurationQuery, +} from '../../resources/deck_configuration' -import type { CutoutFixtureId, CutoutId } from '@opentrons/shared-data' +import type { CutoutId } from '@opentrons/shared-data' const DECK_CONFIG_REFETCH_INTERVAL = 5000 const RUN_REFETCH_INTERVAL = 5000 @@ -65,12 +59,6 @@ export function DeviceDetailsDeckConfiguration({ showSetupInstructionsModal, setShowSetupInstructionsModal, ] = React.useState(false) - const [showAddFixtureModal, setShowAddFixtureModal] = React.useState( - false - ) - const [targetCutoutId, setTargetCutoutId] = React.useState( - null - ) const { data: modulesData } = useModulesQuery() const deckConfig = @@ -78,7 +66,6 @@ export function DeviceDetailsDeckConfiguration({ refetchInterval: DECK_CONFIG_REFETCH_INTERVAL, }).data ?? [] const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE) - const { updateDeckConfiguration } = useUpdateDeckConfigurationMutation() const { isRunRunning } = useRunStatuses() const { data: maintenanceRunData } = useNotifyCurrentMaintenanceRun({ refetchInterval: RUN_REFETCH_INTERVAL, @@ -87,59 +74,11 @@ export function DeviceDetailsDeckConfiguration({ const isMaintenanceRunExisting = maintenanceRunData?.data?.id != null const isRobotViewable = useIsRobotViewable(robotName) - const handleClickAdd = (cutoutId: CutoutId): void => { - setTargetCutoutId(cutoutId) - setShowAddFixtureModal(true) - } - - const handleClickRemove = ( - cutoutId: CutoutId, - cutoutFixtureId: CutoutFixtureId - ): void => { - let replacementFixtureId: CutoutFixtureId = SINGLE_CENTER_SLOT_FIXTURE - if (SINGLE_RIGHT_CUTOUTS.includes(cutoutId)) { - replacementFixtureId = SINGLE_RIGHT_SLOT_FIXTURE - } else if (SINGLE_LEFT_CUTOUTS.includes(cutoutId)) { - replacementFixtureId = SINGLE_LEFT_SLOT_FIXTURE - } - - const fixtureGroup = - deckDef.cutoutFixtures.find(cf => cf.id === cutoutFixtureId) - ?.fixtureGroup ?? {} - - let newDeckConfig = deckConfig - if (cutoutId in fixtureGroup) { - const groupMap = - fixtureGroup[cutoutId]?.find(group => - Object.entries(group).every(([cId, cfId]) => - deckConfig.find( - config => - config.cutoutId === cId && config.cutoutFixtureId === cfId - ) - ) - ) ?? {} - newDeckConfig = deckConfig.map(cutoutConfig => - cutoutConfig.cutoutId in groupMap - ? { - ...cutoutConfig, - cutoutFixtureId: replacementFixtureId, - opentronsModuleSerialNumber: undefined, - } - : cutoutConfig - ) - } else { - newDeckConfig = deckConfig.map(cutoutConfig => - cutoutConfig.cutoutId === cutoutId - ? { - ...cutoutConfig, - cutoutFixtureId: replacementFixtureId, - opentronsModuleSerialNumber: undefined, - } - : cutoutConfig - ) - } - updateDeckConfiguration(newDeckConfig) - } + const { + addFixtureToCutout, + removeFixtureFromCutout, + addFixtureModal, + } = useDeckConfigurationEditingTools(false) // do not show standard slot in fixture display list const { displayList: fixtureDisplayList } = deckConfig.reduce<{ @@ -199,12 +138,7 @@ export function DeviceDetailsDeckConfiguration({ return ( <> - {showAddFixtureModal && targetCutoutId != null ? ( - - ) : null} + {addFixtureModal} {showSetupInstructionsModal ? ( cutoutId) } deckConfig={deckConfig} - handleClickAdd={handleClickAdd} - handleClickRemove={handleClickRemove} + handleClickAdd={addFixtureToCutout} + handleClickRemove={removeFixtureFromCutout} /> { when(vi.mocked(useMostRecentCompletedAnalysis)) .calledWith('mockRunId') .thenReturn(PROTOCOL_DETAILS.protocolData) - vi.mocked(useUpdateDeckConfigurationMutation).mockReturnValue({ - updateDeckConfiguration: mockUpdateDeckConfiguration, - } as any) vi.mocked(useNotifyDeckConfigurationQuery).mockReturnValue(({ data: [], } as unknown) as UseQueryResult) + vi.mocked(useUpdateDeckConfigurationMutation).mockReturnValue({ + updateDeckConfiguration: vi.fn(), + } as any) vi.mocked(useModulesQuery).mockReturnValue(({ data: { data: [] }, } as unknown) as UseQueryResult) @@ -88,18 +87,11 @@ describe('ProtocolSetupDeckConfiguration', () => { render(props) screen.getByText('Deck configuration') screen.getByText('mock BaseDeck') - screen.getByText('Confirm') - }) - - it('should call a mock function when tapping the back button', () => { - render(props) - fireEvent.click(screen.getByTestId('ChildNavigation_Back_Button')) - expect(mockSetSetupScreen).toHaveBeenCalledWith('modules') + screen.getByText('Save') }) it('should call a mock function when tapping confirm button', () => { render(props) - fireEvent.click(screen.getByText('Confirm')) - expect(mockUpdateDeckConfiguration).toHaveBeenCalled() + fireEvent.click(screen.getByText('Save')) }) }) diff --git a/app/src/organisms/ProtocolSetupDeckConfiguration/index.tsx b/app/src/organisms/ProtocolSetupDeckConfiguration/index.tsx index c3c65e6318f..7f6f78bc343 100644 --- a/app/src/organisms/ProtocolSetupDeckConfiguration/index.tsx +++ b/app/src/organisms/ProtocolSetupDeckConfiguration/index.tsx @@ -15,9 +15,9 @@ import { MAGNETIC_BLOCK_V1_FIXTURE, MODULE_FIXTURES_BY_MODEL, STAGING_AREA_SLOT_WITH_MAGNETIC_BLOCK_V1_FIXTURE, + THERMOCYCLER_V2_REAR_FIXTURE, getSimplestDeckConfigForProtocol, } from '@opentrons/shared-data' -import { useUpdateDeckConfigurationMutation } from '@opentrons/react-api-client' import { ChildNavigation } from '../ChildNavigation' import { AddFixtureModal } from '../DeviceDetailsDeckConfiguration/AddFixtureModal' @@ -29,7 +29,6 @@ import { useNotifyDeckConfigurationQuery } from '../../resources/deck_configurat import type { CutoutFixtureId, CutoutId, - DeckConfiguration, ModuleModel, } from '@opentrons/shared-data' import type { ModuleOnDeck } from '@opentrons/components' @@ -48,7 +47,11 @@ export function ProtocolSetupDeckConfiguration({ setSetupScreen, providedFixtureOptions, }: ProtocolSetupDeckConfigurationProps): JSX.Element { - const { t } = useTranslation(['protocol_setup', 'devices_landing', 'shared']) + const { i18n, t } = useTranslation([ + 'protocol_setup', + 'devices_landing', + 'shared', + ]) const [ showConfigurationModal, @@ -66,28 +69,28 @@ export function ProtocolSetupDeckConfiguration({ mostRecentAnalysis ).map(({ cutoutId, cutoutFixtureId }) => ({ cutoutId, cutoutFixtureId })) - const targetDeckConfig = simplestDeckConfig.find( + const targetCutoutConfig = simplestDeckConfig.find( deck => deck.cutoutId === cutoutId ) const mergedDeckConfig = deckConfig.map(config => - targetDeckConfig != null && config.cutoutId === targetDeckConfig.cutoutId - ? targetDeckConfig + targetCutoutConfig != null && + config.cutoutId === targetCutoutConfig.cutoutId + ? targetCutoutConfig : config ) - const [ - currentDeckConfig, - setCurrentDeckConfig, - ] = React.useState(mergedDeckConfig) - const modulesOnDeck = currentDeckConfig.reduce( + const modulesOnDeck = mergedDeckConfig.reduce( (acc, cutoutConfig) => { const matchingFixtureIdsAndModel = Object.entries( MODULE_FIXTURES_BY_MODEL ).find(([_moduleModel, moduleFixtureIds]) => moduleFixtureIds.includes(cutoutConfig.cutoutFixtureId) ) - if (matchingFixtureIdsAndModel != null) { + if ( + matchingFixtureIdsAndModel != null && + cutoutConfig.cutoutFixtureId !== THERMOCYCLER_V2_REAR_FIXTURE + ) { const [matchingModel] = matchingFixtureIdsAndModel return [ ...acc, @@ -117,9 +120,7 @@ export function ProtocolSetupDeckConfiguration({ [] ) - const { updateDeckConfiguration } = useUpdateDeckConfigurationMutation() const handleClickConfirm = (): void => { - updateDeckConfiguration(currentDeckConfig) setSetupScreen('modules') } @@ -135,9 +136,8 @@ export function ProtocolSetupDeckConfiguration({ {showConfigurationModal && cutoutId != null ? ( setShowConfigurationModal(false)} providedFixtureOptions={providedFixtureOptions} - setCurrentDeckConfig={setCurrentDeckConfig} isOnDevice /> ) : null} @@ -147,8 +147,7 @@ export function ProtocolSetupDeckConfiguration({ setSetupScreen('modules')} - buttonText={t('shared:confirm')} + buttonText={i18n.format(t('shared:save'), 'capitalize')} onClickButton={handleClickConfirm} /> diff --git a/app/src/pages/DeckConfiguration/__tests__/DeckConfiguration.test.tsx b/app/src/pages/DeckConfiguration/__tests__/DeckConfiguration.test.tsx index 9efe3bc0aa2..d0fab1277b0 100644 --- a/app/src/pages/DeckConfiguration/__tests__/DeckConfiguration.test.tsx +++ b/app/src/pages/DeckConfiguration/__tests__/DeckConfiguration.test.tsx @@ -12,7 +12,10 @@ import { TRASH_BIN_ADAPTER_FIXTURE } from '@opentrons/shared-data' import { i18n } from '../../../i18n' import { DeckFixtureSetupInstructionsModal } from '../../../organisms/DeviceDetailsDeckConfiguration/DeckFixtureSetupInstructionsModal' import { DeckConfigurationEditor } from '..' -import { useNotifyDeckConfigurationQuery } from '../../../resources/deck_configuration' +import { + useNotifyDeckConfigurationQuery, + useDeckConfigurationEditingTools, +} from '../../../resources/deck_configuration' import type { UseQueryResult } from 'react-query' import type { DeckConfiguration } from '@opentrons/shared-data' @@ -71,13 +74,18 @@ describe('DeckConfigurationEditor', () => { vi.mocked(useUpdateDeckConfigurationMutation).mockReturnValue({ updateDeckConfiguration: mockUpdateDeckConfiguration, } as any) + vi.mocked(useDeckConfigurationEditingTools).mockReturnValue({ + addFixtureToCutout: vi.fn(), + removeFixtureFromCutout: vi.fn(), + addFixtureModal: null, + }) }) it('should render text, button and DeckConfigurator', () => { render() screen.getByText('Deck configuration') screen.getByText('Setup Instructions') - screen.getByText('Confirm') + screen.getByText('Save') expect(vi.mocked(DeckConfigurator)).toHaveBeenCalled() }) @@ -86,26 +94,4 @@ describe('DeckConfigurationEditor', () => { fireEvent.click(screen.getByText('Setup Instructions')) expect(vi.mocked(DeckFixtureSetupInstructionsModal)).toHaveBeenCalled() }) - - it('should call a mock function when tapping confirm', () => { - // (kk:10/26/2023) - // Once get approval, I will be able to update this case - // render() - // screen.getByText('Confirm').click() - // expect(mockUpdateDeckConfiguration).toHaveBeenCalled() - }) - - it('should call a mock function when tapping back button if there is no change', () => { - render() - fireEvent.click(screen.getByTestId('ChildNavigation_Back_Button')) - expect(mockGoBack).toHaveBeenCalled() - }) - - it('should render modal when tapping back button if there is a change', () => { - // (kk:10/26/2023) - // Once get approval, I will be able to update this case - // render() - // screen.getByTestId('ChildNavigation_Back_Button').click() - // expect(mockGoBack).toHaveBeenCalled() - }) }) diff --git a/app/src/pages/DeckConfiguration/index.tsx b/app/src/pages/DeckConfiguration/index.tsx index 2c90d5249b8..2ed8530bcbd 100644 --- a/app/src/pages/DeckConfiguration/index.tsx +++ b/app/src/pages/DeckConfiguration/index.tsx @@ -2,7 +2,6 @@ import * as React from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' import { useHistory } from 'react-router-dom' -import isEqual from 'lodash/isEqual' import { DeckConfigurator, @@ -11,30 +10,16 @@ import { JUSTIFY_CENTER, JUSTIFY_SPACE_AROUND, } from '@opentrons/components' -import { useUpdateDeckConfigurationMutation } from '@opentrons/react-api-client' -import { - SINGLE_RIGHT_CUTOUTS, - SINGLE_LEFT_SLOT_FIXTURE, - SINGLE_RIGHT_SLOT_FIXTURE, - SINGLE_LEFT_CUTOUTS, - SINGLE_CENTER_SLOT_FIXTURE, - getDeckDefFromRobotType, - FLEX_ROBOT_TYPE, -} from '@opentrons/shared-data' import { SmallButton } from '../../atoms/buttons' import { ChildNavigation } from '../../organisms/ChildNavigation' -import { AddFixtureModal } from '../../organisms/DeviceDetailsDeckConfiguration/AddFixtureModal' import { DeckFixtureSetupInstructionsModal } from '../../organisms/DeviceDetailsDeckConfiguration/DeckFixtureSetupInstructionsModal' import { DeckConfigurationDiscardChangesModal } from '../../organisms/DeviceDetailsDeckConfiguration/DeckConfigurationDiscardChangesModal' import { getTopPortalEl } from '../../App/portal' -import { useNotifyDeckConfigurationQuery } from '../../resources/deck_configuration' - -import type { - CutoutFixtureId, - CutoutId, - DeckConfiguration, -} from '@opentrons/shared-data' +import { + useDeckConfigurationEditingTools, + useNotifyDeckConfigurationQuery, +} from '../../resources/deck_configuration' export function DeckConfigurationEditor(): JSX.Element { const { t, i18n } = useTranslation([ @@ -47,96 +32,25 @@ export function DeckConfigurationEditor(): JSX.Element { showSetupInstructionsModal, setShowSetupInstructionsModal, ] = React.useState(false) - const [ - showConfigurationModal, - setShowConfigurationModal, - ] = React.useState(false) - const [targetCutoutId, setTargetCutoutId] = React.useState( - null - ) + + const isOnDevice = true + const { + addFixtureToCutout, + removeFixtureFromCutout, + addFixtureModal, + } = useDeckConfigurationEditingTools(isOnDevice) + const [ showDiscardChangeModal, setShowDiscardChangeModal, ] = React.useState(false) - const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE) const deckConfig = useNotifyDeckConfigurationQuery().data ?? [] - const { updateDeckConfiguration } = useUpdateDeckConfigurationMutation() - - const [ - currentDeckConfig, - setCurrentDeckConfig, - ] = React.useState(deckConfig) - - const handleClickAdd = (cutoutId: CutoutId): void => { - setTargetCutoutId(cutoutId) - setShowConfigurationModal(true) - } - - const handleClickRemove = ( - cutoutId: CutoutId, - cutoutFixtureId: CutoutFixtureId - ): void => { - let replacementFixtureId: CutoutFixtureId = SINGLE_CENTER_SLOT_FIXTURE - if (SINGLE_RIGHT_CUTOUTS.includes(cutoutId)) { - replacementFixtureId = SINGLE_RIGHT_SLOT_FIXTURE - } else if (SINGLE_LEFT_CUTOUTS.includes(cutoutId)) { - replacementFixtureId = SINGLE_LEFT_SLOT_FIXTURE - } - - const fixtureGroup = - deckDef.cutoutFixtures.find(cf => cf.id === cutoutFixtureId) - ?.fixtureGroup ?? {} - - let newDeckConfig = currentDeckConfig - if (cutoutId in fixtureGroup) { - const groupMap = - fixtureGroup[cutoutId]?.find(group => - Object.entries(group).every(([cId, cfId]) => - currentDeckConfig.find( - config => - config.cutoutId === cId && config.cutoutFixtureId === cfId - ) - ) - ) ?? {} - newDeckConfig = currentDeckConfig.map(cutoutConfig => - cutoutConfig.cutoutId in groupMap - ? { - ...cutoutConfig, - cutoutFixtureId: replacementFixtureId, - opentronsModuleSerialNumber: undefined, - } - : cutoutConfig - ) - } else { - newDeckConfig = currentDeckConfig.map(cutoutConfig => - cutoutConfig.cutoutId === cutoutId - ? { - ...cutoutConfig, - cutoutFixtureId: replacementFixtureId, - opentronsModuleSerialNumber: undefined, - } - : cutoutConfig - ) - } - setCurrentDeckConfig(newDeckConfig) - } const handleClickConfirm = (): void => { - if (!isEqual(deckConfig, currentDeckConfig)) { - updateDeckConfiguration(currentDeckConfig) - } history.goBack() } - const handleClickBack = (): void => { - if (!isEqual(deckConfig, currentDeckConfig)) { - setShowDiscardChangeModal(true) - } else { - history.goBack() - } - } - const secondaryButtonProps: React.ComponentProps = { onClick: () => setShowSetupInstructionsModal(true), buttonText: i18n.format(t('setup_instructions'), 'titleCase'), @@ -145,10 +59,6 @@ export function DeckConfigurationEditor(): JSX.Element { iconPlacement: 'startIcon', } - React.useEffect(() => { - setCurrentDeckConfig(deckConfig) - }, [deckConfig]) - return ( <> {createPortal( @@ -161,17 +71,10 @@ export function DeckConfigurationEditor(): JSX.Element { {showSetupInstructionsModal ? ( - ) : null} - {showConfigurationModal && targetCutoutId != null ? ( - ) : null} + {addFixtureModal} , getTopPortalEl() )} @@ -181,16 +84,15 @@ export function DeckConfigurationEditor(): JSX.Element { > diff --git a/app/src/resources/deck_configuration/hooks.ts b/app/src/resources/deck_configuration/hooks.tsx similarity index 53% rename from app/src/resources/deck_configuration/hooks.ts rename to app/src/resources/deck_configuration/hooks.tsx index ed48b705c5c..a3bf96173c3 100644 --- a/app/src/resources/deck_configuration/hooks.ts +++ b/app/src/resources/deck_configuration/hooks.tsx @@ -1,3 +1,4 @@ +import * as React from 'react' import { getInitialAndMovedLabwareInSlots } from '@opentrons/components' import { FLEX_ROBOT_TYPE, @@ -6,6 +7,11 @@ import { getCutoutIdForAddressableArea, getDeckDefFromRobotType, getLabwareDisplayName, + SINGLE_CENTER_SLOT_FIXTURE, + SINGLE_LEFT_CUTOUTS, + SINGLE_LEFT_SLOT_FIXTURE, + SINGLE_RIGHT_CUTOUTS, + SINGLE_RIGHT_SLOT_FIXTURE, SINGLE_SLOT_FIXTURES, } from '@opentrons/shared-data' @@ -13,11 +19,14 @@ import type { CompletedProtocolAnalysis, CutoutConfigProtocolSpec, CutoutFixtureId, + CutoutId, ProtocolAnalysisOutput, RobotType, } from '@opentrons/shared-data' import { useNotifyDeckConfigurationQuery } from './useNotifyDeckConfigurationQuery' +import { AddFixtureModal } from '../../organisms/DeviceDetailsDeckConfiguration/AddFixtureModal' +import { useUpdateDeckConfigurationMutation } from '@opentrons/react-api-client' const DECK_CONFIG_REFETCH_INTERVAL = 5000 @@ -100,3 +109,91 @@ export function useDeckConfigurationCompatibility( [] ) } + +interface DeckConfigurationEditingTools { + addFixtureToCutout: (cutoutId: CutoutId) => void + removeFixtureFromCutout: ( + cutoutId: CutoutId, + cutoutFixtureId: CutoutFixtureId + ) => void + addFixtureModal: React.ReactNode +} +export function useDeckConfigurationEditingTools( + isOnDevice: boolean +): DeckConfigurationEditingTools { + const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE) + const deckConfig = + useNotifyDeckConfigurationQuery({ + refetchInterval: DECK_CONFIG_REFETCH_INTERVAL, + }).data ?? [] + const { updateDeckConfiguration } = useUpdateDeckConfigurationMutation() + const [targetCutoutId, setTargetCutoutId] = React.useState( + null + ) + + const addFixtureToCutout = (cutoutId: CutoutId): void => { + setTargetCutoutId(cutoutId) + } + + const removeFixtureFromCutout = ( + cutoutId: CutoutId, + cutoutFixtureId: CutoutFixtureId + ): void => { + let replacementFixtureId: CutoutFixtureId = SINGLE_CENTER_SLOT_FIXTURE + if (SINGLE_RIGHT_CUTOUTS.includes(cutoutId)) { + replacementFixtureId = SINGLE_RIGHT_SLOT_FIXTURE + } else if (SINGLE_LEFT_CUTOUTS.includes(cutoutId)) { + replacementFixtureId = SINGLE_LEFT_SLOT_FIXTURE + } + + const fixtureGroup = + deckDef.cutoutFixtures.find(cf => cf.id === cutoutFixtureId) + ?.fixtureGroup ?? {} + + let newDeckConfig = deckConfig + if (cutoutId in fixtureGroup) { + const groupMap = + fixtureGroup[cutoutId]?.find(group => + Object.entries(group).every(([cId, cfId]) => + deckConfig.find( + config => + config.cutoutId === cId && config.cutoutFixtureId === cfId + ) + ) + ) ?? {} + newDeckConfig = deckConfig.map(cutoutConfig => + cutoutConfig.cutoutId in groupMap + ? { + ...cutoutConfig, + cutoutFixtureId: replacementFixtureId, + opentronsModuleSerialNumber: undefined, + } + : cutoutConfig + ) + } else { + newDeckConfig = deckConfig.map(cutoutConfig => + cutoutConfig.cutoutId === cutoutId + ? { + ...cutoutConfig, + cutoutFixtureId: replacementFixtureId, + opentronsModuleSerialNumber: undefined, + } + : cutoutConfig + ) + } + updateDeckConfiguration(newDeckConfig) + } + + return { + addFixtureToCutout, + removeFixtureFromCutout, + addFixtureModal: + targetCutoutId != null ? ( + setTargetCutoutId(null)} + isOnDevice={isOnDevice} + /> + ) : null, + } +}