diff --git a/app/src/assets/localization/en/protocol_setup.json b/app/src/assets/localization/en/protocol_setup.json index 047e56426ac..c4aed3c3847 100644 --- a/app/src/assets/localization/en/protocol_setup.json +++ b/app/src/assets/localization/en/protocol_setup.json @@ -2,6 +2,8 @@ "96_mount": "left + right mount", "adapter_slot_location_module": "Slot {{slotName}}, {{adapterName}} on {{moduleName}}", "adapter_slot_location": "Slot {{slotName}}, {{adapterName}}", + "add_fixture_to_deck": "Add this fixture to your deck configuration. It will be referenced during protocol analysis.", + "add_fixture": "Add {{fixtureName}} to deck configuration", "additional_labware": "{{count}} additional labware", "additional_off_deck_labware": "Additional Off-Deck Labware", "attach_gripper_failure_reason": "Attach the required gripper to continue", diff --git a/app/src/assets/localization/en/shared.json b/app/src/assets/localization/en/shared.json index 057d4aca41c..fe3c9177c5c 100644 --- a/app/src/assets/localization/en/shared.json +++ b/app/src/assets/localization/en/shared.json @@ -1,5 +1,6 @@ { "a_software_update_is_available": "A software update is available for this robot. Update to run protocols.", + "add": "add", "alphabetical": "Alphabetical", "back": "Back", "before_you_begin": "Before you begin", diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/NotConfiguredModal.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/NotConfiguredModal.tsx new file mode 100644 index 00000000000..be95deb66ee --- /dev/null +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/NotConfiguredModal.tsx @@ -0,0 +1,76 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { useUpdateDeckConfigurationMutation } from '@opentrons/react-api-client/src/deck_configuration' +import { + Flex, + DIRECTION_COLUMN, + TYPOGRAPHY, + SPACING, + JUSTIFY_SPACE_BETWEEN, + COLORS, + BORDERS, + ALIGN_CENTER, +} from '@opentrons/components' +import { FixtureLoadName, getFixtureDisplayName } from '@opentrons/shared-data' +import { TertiaryButton } from '../../../../atoms/buttons/TertiaryButton' +import { Portal } from '../../../../App/portal' +import { LegacyModal } from '../../../../molecules/LegacyModal' +import { StyledText } from '../../../../atoms/text' + +interface NotConfiguredModalProps { + onCloseClick: () => void + requiredFixture: FixtureLoadName + cutout: string +} + +export const NotConfiguredModal = ( + props: NotConfiguredModalProps +): JSX.Element => { + const { onCloseClick, cutout, requiredFixture } = props + const { t, i18n } = useTranslation(['protocol_setup', 'shared']) + const { updateDeckConfiguration } = useUpdateDeckConfigurationMutation() + + const handleUpdateDeck = (): void => { + updateDeckConfiguration({ + fixtureLocation: cutout, + loadName: requiredFixture, + }) + onCloseClick() + } + + return ( + + + {t('add_fixture', { + fixtureName: getFixtureDisplayName(requiredFixture), + })} + + } + onClose={onCloseClick} + width="27.75rem" + > + + {t('add_fixture_to_deck')} + + + + {getFixtureDisplayName(requiredFixture)} + + + {i18n.format(t('add'), 'capitalize')} + + + + + + + ) +} diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupFixtureList.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupFixtureList.tsx index 3005891a175..f54e5d3ce39 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupFixtureList.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupFixtureList.tsx @@ -30,6 +30,7 @@ import { StyledText } from '../../../../atoms/text' import { StatusLabel } from '../../../../atoms/StatusLabel' import { TertiaryButton } from '../../../../atoms/buttons/TertiaryButton' import { LocationConflictModal } from './LocationConflictModal' +import { NotConfiguredModal } from './NotConfiguredModal' import { getFixtureImage } from './utils' import type { LoadedFixturesBySlot } from '@opentrons/api-client' @@ -151,9 +152,20 @@ export function FixtureListItem({ showLocationConflictModal, setShowLocationConflictModal, ] = React.useState(false) + const [ + showNotConfiguredModal, + setShowNotConfiguredModal, + ] = React.useState(false) return ( <> + {showNotConfiguredModal ? ( + setShowNotConfiguredModal(false)} + cutout={cutout} + requiredFixture={loadName} + /> + ) : null} {showLocationConflictModal ? ( setShowLocationConflictModal(false)} @@ -217,7 +229,7 @@ export function FixtureListItem({ onClick={() => configurationStatus === CONFLICTING ? setShowLocationConflictModal(true) - : console.log('wire this up') + : setShowNotConfiguredModal(true) } > diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/NotConfiguredModal.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/NotConfiguredModal.test.tsx new file mode 100644 index 00000000000..d18bee369a8 --- /dev/null +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/NotConfiguredModal.test.tsx @@ -0,0 +1,43 @@ +import * as React from 'react' +import { renderWithProviders } from '@opentrons/components' +import { TRASH_BIN_LOAD_NAME } from '@opentrons/shared-data' +import { useUpdateDeckConfigurationMutation } from '@opentrons/react-api-client/src/deck_configuration' +import { i18n } from '../../../../../i18n' +import { NotConfiguredModal } from '../NotConfiguredModal' + +jest.mock('@opentrons/react-api-client/src/deck_configuration') + +const mockUseUpdateDeckConfigurationMutation = useUpdateDeckConfigurationMutation as jest.MockedFunction< + typeof useUpdateDeckConfigurationMutation +> + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +describe('NotConfiguredModal', () => { + let props: React.ComponentProps + const mockUpdate = jest.fn() + beforeEach(() => { + props = { + onCloseClick: jest.fn(), + cutout: 'B3', + requiredFixture: TRASH_BIN_LOAD_NAME, + } + mockUseUpdateDeckConfigurationMutation.mockReturnValue({ + updateDeckConfiguration: mockUpdate, + } as any) + }) + it('renders the correct text and button works as expected', () => { + const { getByText, getByRole } = render(props) + getByText('Add Trash Bin to deck configuration') + getByText( + 'Add this fixture to your deck configuration. It will be referenced during protocol analysis.' + ) + getByText('Trash Bin') + getByRole('button', { name: 'Add' }).click() + expect(mockUpdate).toHaveBeenCalled() + }) +}) diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupFixtureList.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupFixtureList.test.tsx index cf6eb1b4e43..8da24a61675 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupFixtureList.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupFixtureList.test.tsx @@ -8,17 +8,23 @@ import { import { i18n } from '../../../../../i18n' import { useLoadedFixturesConfigStatus } from '../../../../../resources/deck_configuration/hooks' import { SetupFixtureList } from '../SetupFixtureList' +import { NotConfiguredModal } from '../NotConfiguredModal' import { LocationConflictModal } from '../LocationConflictModal' import type { LoadedFixturesBySlot } from '@opentrons/api-client' jest.mock('../../../../../resources/deck_configuration/hooks') jest.mock('../LocationConflictModal') +jest.mock('../NotConfiguredModal') + const mockUseLoadedFixturesConfigStatus = useLoadedFixturesConfigStatus as jest.MockedFunction< typeof useLoadedFixturesConfigStatus > const mockLocationConflictModal = LocationConflictModal as jest.MockedFunction< typeof LocationConflictModal > +const mockNotConfiguredModal = NotConfiguredModal as jest.MockedFunction< + typeof NotConfiguredModal +> const mockLoadedFixture = { id: 'stubbed_load_fixture', commandType: 'loadFixture', @@ -58,6 +64,7 @@ describe('SetupFixtureList', () => { mockLocationConflictModal.mockReturnValue(
mock location conflict modal
) + mockNotConfiguredModal.mockReturnValue(
mock not configured modal
) }) it('should render the headers and a fixture with configured status', () => { @@ -92,6 +99,6 @@ describe('SetupFixtureList', () => { const { getByText, getByRole } = render(props)[0] getByText('Not configured') getByRole('button', { name: 'Update deck' }).click() - // TODO(Jr, 10/5/23): add test coverage for button + getByText('mock not configured modal') }) }) diff --git a/app/src/resources/deck_configuration/hooks.ts b/app/src/resources/deck_configuration/hooks.ts index 8388cfab084..29b853b6704 100644 --- a/app/src/resources/deck_configuration/hooks.ts +++ b/app/src/resources/deck_configuration/hooks.ts @@ -1,6 +1,10 @@ import { useDeckConfigurationQuery } from '@opentrons/react-api-client' -import type { Fixture, LoadFixtureRunTimeCommand } from '@opentrons/shared-data' +import { + Fixture, + LoadFixtureRunTimeCommand, + STANDARD_SLOT_LOAD_NAME, +} from '@opentrons/shared-data' export const CONFIGURED = 'configured' export const CONFLICTING = 'conflicting' @@ -32,9 +36,12 @@ export function useLoadedFixturesConfigStatus( deckConfigurationAtLocation.loadName === loadedFixture.params.loadName ) { configurationStatus = CONFIGURED + // special casing this for now until we know what the backend will give us. It is only + // conflicting if the current deck configuration fixture is not the desired or standard slot } else if ( deckConfigurationAtLocation != null && - deckConfigurationAtLocation.loadName !== loadedFixture.params.loadName + deckConfigurationAtLocation.loadName !== loadedFixture.params.loadName && + deckConfigurationAtLocation.loadName !== STANDARD_SLOT_LOAD_NAME ) { configurationStatus = CONFLICTING }