diff --git a/api-client/src/deck_configuration/__stubs__/index.ts b/api-client/src/deck_configuration/__stubs__/index.ts index 2197c25baaa..1e23939c0ee 100644 --- a/api-client/src/deck_configuration/__stubs__/index.ts +++ b/api-client/src/deck_configuration/__stubs__/index.ts @@ -1,73 +1,61 @@ -import { v4 as uuidv4 } from 'uuid' - import { - STAGING_AREA_LOAD_NAME, - STANDARD_SLOT_LOAD_NAME, - TRASH_BIN_LOAD_NAME, - WASTE_CHUTE_LOAD_NAME, + SINGLE_LEFT_SLOT_FIXTURE, + SINGLE_CENTER_SLOT_FIXTURE, + SINGLE_RIGHT_SLOT_FIXTURE, + STAGING_AREA_RIGHT_SLOT_FIXTURE, + TRASH_BIN_ADAPTER_FIXTURE, + WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, } from '@opentrons/shared-data' -import type { Fixture } from '@opentrons/shared-data' +import type { DeckConfiguration } from '@opentrons/shared-data' -export const DECK_CONFIG_STUB: { [fixtureLocation: string]: Fixture } = { - cutoutA1: { - fixtureLocation: 'cutoutA1', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), +export const DECK_CONFIG_STUB: DeckConfiguration = [ + { + cutoutId: 'cutoutA1', + cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE, }, - cutoutB1: { - fixtureLocation: 'cutoutB1', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), + { + cutoutId: 'cutoutB1', + cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE, }, - cutoutC1: { - fixtureLocation: 'cutoutC1', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), + { + cutoutId: 'cutoutC1', + cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE, }, - cutoutD1: { - fixtureLocation: 'cutoutD1', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), + { + cutoutId: 'cutoutD1', + cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE, }, - cutoutA2: { - fixtureLocation: 'cutoutA2', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), + { + cutoutId: 'cutoutA2', + cutoutFixtureId: SINGLE_CENTER_SLOT_FIXTURE, }, - cutoutB2: { - fixtureLocation: 'cutoutB2', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), + { + cutoutId: 'cutoutB2', + cutoutFixtureId: SINGLE_CENTER_SLOT_FIXTURE, }, - cutoutC2: { - fixtureLocation: 'cutoutC2', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), + { + cutoutId: 'cutoutC2', + cutoutFixtureId: SINGLE_CENTER_SLOT_FIXTURE, }, - cutoutD2: { - fixtureLocation: 'cutoutD2', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), + { + cutoutId: 'cutoutD2', + cutoutFixtureId: SINGLE_CENTER_SLOT_FIXTURE, }, - cutoutA3: { - fixtureLocation: 'cutoutA3', - loadName: TRASH_BIN_LOAD_NAME, - fixtureId: uuidv4(), + { + cutoutId: 'cutoutA3', + cutoutFixtureId: TRASH_BIN_ADAPTER_FIXTURE, }, - cutoutB3: { - fixtureLocation: 'cutoutB3', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), + { + cutoutId: 'cutoutB3', + cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE, }, - cutoutC3: { - fixtureLocation: 'cutoutC3', - loadName: STAGING_AREA_LOAD_NAME, - fixtureId: uuidv4(), + { + cutoutId: 'cutoutC3', + cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, }, - cutoutD3: { - fixtureLocation: 'cutoutD3', - loadName: WASTE_CHUTE_LOAD_NAME, - fixtureId: uuidv4(), + { + cutoutId: 'cutoutD3', + cutoutFixtureId: WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, }, -} +] diff --git a/api-client/src/deck_configuration/createDeckConfiguration.ts b/api-client/src/deck_configuration/createDeckConfiguration.ts deleted file mode 100644 index 09a2f3b73d7..00000000000 --- a/api-client/src/deck_configuration/createDeckConfiguration.ts +++ /dev/null @@ -1,29 +0,0 @@ -// import { POST, request } from '../request' -import { DECK_CONFIG_STUB } from './__stubs__' - -import type { DeckConfiguration } from '@opentrons/shared-data' -// import type { ResponsePromise } from '../request' -import type { HostConfig } from '../types' - -// TODO(bh, 2023-09-26): uncomment and remove deck config stub when backend api is ready -// export function createDeckConfiguration( -// config: HostConfig, -// data: DeckConfiguration -// ): ResponsePromise { -// return request( -// POST, -// `/deck_configuration`, -// { data }, -// config -// ) -// } - -export function createDeckConfiguration( - config: HostConfig, - data: DeckConfiguration -): Promise<{ data: DeckConfiguration }> { - data.forEach(fixture => { - DECK_CONFIG_STUB[fixture.fixtureLocation] = fixture - }) - return Promise.resolve({ data: Object.values(DECK_CONFIG_STUB) }) -} diff --git a/api-client/src/deck_configuration/deleteDeckConfiguration.ts b/api-client/src/deck_configuration/deleteDeckConfiguration.ts deleted file mode 100644 index e3689f01559..00000000000 --- a/api-client/src/deck_configuration/deleteDeckConfiguration.ts +++ /dev/null @@ -1,30 +0,0 @@ -// import { DELETE, request } from '../request' -import { DECK_CONFIG_STUB } from './__stubs__' - -import type { Fixture } from '@opentrons/shared-data' -// import type { ResponsePromise } from '../request' -import type { EmptyResponse, HostConfig } from '../types' - -// TODO(bh, 2023-09-26): uncomment and remove deck config stub when backend api is ready -// export function deleteDeckConfiguration( -// config: HostConfig, -// data: Fixture -// ): ResponsePromise { -// const { fixtureLocation, ...rest } = data -// return request }>( -// DELETE, -// `/deck_configuration/${fixtureLocation}`, -// { data: rest }, -// config -// ) -// } - -export function deleteDeckConfiguration( - config: HostConfig, - data: Fixture -): Promise { - const { fixtureLocation } = data - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete DECK_CONFIG_STUB[fixtureLocation] - return Promise.resolve({ data: null }) -} diff --git a/api-client/src/deck_configuration/getDeckConfiguration.ts b/api-client/src/deck_configuration/getDeckConfiguration.ts index bc8c556e255..83f0734cbdb 100644 --- a/api-client/src/deck_configuration/getDeckConfiguration.ts +++ b/api-client/src/deck_configuration/getDeckConfiguration.ts @@ -15,5 +15,5 @@ import type { HostConfig } from '../types' export function getDeckConfiguration( config: HostConfig ): Promise<{ data: DeckConfiguration }> { - return Promise.resolve({ data: Object.values(DECK_CONFIG_STUB) }) + return Promise.resolve({ data: DECK_CONFIG_STUB }) } diff --git a/api-client/src/deck_configuration/index.ts b/api-client/src/deck_configuration/index.ts index c22cba0ae78..fd7ba31b0f0 100644 --- a/api-client/src/deck_configuration/index.ts +++ b/api-client/src/deck_configuration/index.ts @@ -1,4 +1,2 @@ -export { createDeckConfiguration } from './createDeckConfiguration' -export { deleteDeckConfiguration } from './deleteDeckConfiguration' export { getDeckConfiguration } from './getDeckConfiguration' export { updateDeckConfiguration } from './updateDeckConfiguration' diff --git a/api-client/src/deck_configuration/updateDeckConfiguration.ts b/api-client/src/deck_configuration/updateDeckConfiguration.ts index a02fb1af4b0..a61d56372a3 100644 --- a/api-client/src/deck_configuration/updateDeckConfiguration.ts +++ b/api-client/src/deck_configuration/updateDeckConfiguration.ts @@ -1,32 +1,25 @@ -import { v4 as uuidv4 } from 'uuid' - -// import { PATCH, request } from '../request' +// import { PUT, request } from '../request' import { DECK_CONFIG_STUB } from './__stubs__' -import type { Fixture } from '@opentrons/shared-data' +import type { DeckConfiguration } from '@opentrons/shared-data' // import type { ResponsePromise } from '../request' import type { HostConfig } from '../types' // TODO(bh, 2023-09-26): uncomment and remove deck config stub when backend api is ready // export function updateDeckConfiguration( // config: HostConfig, -// data: Omit -// ): ResponsePromise { -// const { fixtureLocation, ...rest } = data -// return request }>( -// PATCH, -// `/deck_configuration/${fixtureLocation}`, -// { data: rest }, -// config -// ) +// data: DeckConfiguration +// ): ResponsePromise { +// return request(PUT, '/deck_configuration', data, config) // } export function updateDeckConfiguration( config: HostConfig, - data: Omit -): Promise<{ data: Fixture }> { - const { fixtureLocation } = data - const fixtureId = uuidv4() - DECK_CONFIG_STUB[fixtureLocation] = { ...data, fixtureId } - return Promise.resolve({ data: DECK_CONFIG_STUB[fixtureLocation] }) + data: DeckConfiguration +): Promise<{ data: DeckConfiguration }> { + data.forEach((fixture, i) => { + DECK_CONFIG_STUB[i] = fixture + }) + + return Promise.resolve({ data: DECK_CONFIG_STUB }) } diff --git a/app/src/molecules/DeckThumbnail/__tests__/DeckThumbnail.test.tsx b/app/src/molecules/DeckThumbnail/__tests__/DeckThumbnail.test.tsx index 2c47a31532e..16b2555fda2 100644 --- a/app/src/molecules/DeckThumbnail/__tests__/DeckThumbnail.test.tsx +++ b/app/src/molecules/DeckThumbnail/__tests__/DeckThumbnail.test.tsx @@ -11,7 +11,6 @@ import ot3StandardDeckDef from '@opentrons/shared-data/deck/definitions/3/ot3_st import fixture_tiprack_300_ul from '@opentrons/shared-data/labware/fixtures/2/fixture_tiprack_300_ul.json' import { BaseDeck, - EXTENDED_DECK_CONFIG_FIXTURE, partialComponentPropsMatcher, renderWithProviders, } from '@opentrons/components' @@ -24,7 +23,7 @@ import { import { i18n } from '../../../i18n' import { useAttachedModules } from '../../../organisms/Devices/hooks' import { getStandardDeckViewLayerBlockList } from '../utils/getStandardDeckViewLayerBlockList' -import { getDeckConfigFromProtocolCommands } from '../../../resources/deck_configuration/utils' +import { getSimplestDeckConfigForProtocolCommands } from '../../../resources/deck_configuration/utils' import { getAttachedProtocolModuleMatches } from '../../../organisms/ProtocolSetupModulesAndDeck/utils' import { getProtocolModulesInfo } from '../../../organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo' import { getLabwareRenderInfo } from '../../../organisms/Devices/ProtocolRun/utils/getLabwareRenderInfo' @@ -66,8 +65,8 @@ const mockParseLabwareInfoByLiquidId = parseLabwareInfoByLiquidId as jest.Mocked const mockUseAttachedModules = useAttachedModules as jest.MockedFunction< typeof useAttachedModules > -const mockGetDeckConfigFromProtocolCommands = getDeckConfigFromProtocolCommands as jest.MockedFunction< - typeof getDeckConfigFromProtocolCommands +const mockGetSimplestDeckConfigForProtocolCommands = getSimplestDeckConfigForProtocolCommands as jest.MockedFunction< + typeof getSimplestDeckConfigForProtocolCommands > const mockGetLabwareRenderInfo = getLabwareRenderInfo as jest.MockedFunction< typeof getLabwareRenderInfo @@ -127,9 +126,11 @@ describe('DeckThumbnail', () => { mockUseAttachedModules.mockReturnValue( mockFetchModulesSuccessActionPayloadModules ) - when(mockGetDeckConfigFromProtocolCommands) + when(mockGetSimplestDeckConfigForProtocolCommands) .calledWith(commands) - .mockReturnValue(EXTENDED_DECK_CONFIG_FIXTURE) + .mockReturnValue([]) + // TODO(bh, 2023-11-13): mock the cutout config protocol spec + // .mockReturnValue(EXTENDED_DECK_CONFIG_FIXTURE) when(mockGetLabwareRenderInfo).mockReturnValue({}) when(mockGetProtocolModulesInfo) .calledWith(protocolAnalysis, ot2StandardDeckDef as any) @@ -239,9 +240,11 @@ describe('DeckThumbnail', () => { mockUseAttachedModules.mockReturnValue( mockFetchModulesSuccessActionPayloadModules ) - when(mockGetDeckConfigFromProtocolCommands) + when(mockGetSimplestDeckConfigForProtocolCommands) .calledWith(commands) - .mockReturnValue(EXTENDED_DECK_CONFIG_FIXTURE) + .mockReturnValue([]) + // TODO(bh, 2023-11-13): mock the cutout config protocol spec + // .mockReturnValue(EXTENDED_DECK_CONFIG_FIXTURE) when(mockGetLabwareRenderInfo).mockReturnValue({ [MOCK_300_UL_TIPRACK_ID]: { labwareDef: fixture_tiprack_300_ul as LabwareDefinition2, @@ -295,7 +298,6 @@ describe('DeckThumbnail', () => { deckLayerBlocklist: getStandardDeckViewLayerBlockList( FLEX_ROBOT_TYPE ), - deckConfig: EXTENDED_DECK_CONFIG_FIXTURE, labwareLocations: expect.anything(), moduleLocations: expect.anything(), }) diff --git a/app/src/molecules/DeckThumbnail/index.tsx b/app/src/molecules/DeckThumbnail/index.tsx index f4295fa8dd9..e75e244dca1 100644 --- a/app/src/molecules/DeckThumbnail/index.tsx +++ b/app/src/molecules/DeckThumbnail/index.tsx @@ -13,7 +13,7 @@ import { } from '@opentrons/api-client' import { getStandardDeckViewLayerBlockList } from './utils/getStandardDeckViewLayerBlockList' -import { getDeckConfigFromProtocolCommands } from '../../resources/deck_configuration/utils' +import { getSimplestDeckConfigForProtocolCommands } from '../../resources/deck_configuration/utils' import { getLabwareRenderInfo } from '../../organisms/Devices/ProtocolRun/utils/getLabwareRenderInfo' import { getProtocolModulesInfo } from '../../organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo' import { useAttachedModules } from '../../organisms/Devices/hooks' @@ -43,7 +43,7 @@ export function DeckThumbnail(props: DeckThumbnailProps): JSX.Element | null { protocolAnalysis.commands ) - const deckConfig = getDeckConfigFromProtocolCommands( + const deckConfig = getSimplestDeckConfigForProtocolCommands( protocolAnalysis.commands ) const liquids = protocolAnalysis.liquids diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx index 9fec31200e9..a33f55db2a2 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx @@ -14,13 +14,18 @@ import { SPACING, TYPOGRAPHY, } from '@opentrons/components' -import { useUpdateDeckConfigurationMutation } from '@opentrons/react-api-client' +import { + useDeckConfigurationQuery, + useUpdateDeckConfigurationMutation, +} from '@opentrons/react-api-client' import { getCutoutDisplayName, getFixtureDisplayName, - STAGING_AREA_LOAD_NAME, - TRASH_BIN_LOAD_NAME, - WASTE_CHUTE_LOAD_NAME, + STAGING_AREA_CUTOUTS, + STAGING_AREA_RIGHT_SLOT_FIXTURE, + TRASH_BIN_ADAPTER_FIXTURE, + WASTE_CHUTE_CUTOUT, + WASTE_CHUTE_FIXTURES, } from '@opentrons/shared-data' import { StyledText } from '../../atoms/text' @@ -30,24 +35,24 @@ import { Modal } from '../../molecules/Modal' import { LegacyModal } from '../../molecules/LegacyModal' import type { - Cutout, + CutoutConfig, + CutoutId, + CutoutFixtureId, DeckConfiguration, - Fixture, - FixtureLoadName, } from '@opentrons/shared-data' import type { ModalHeaderBaseProps } from '../../molecules/Modal/types' import type { LegacyModalProps } from '../../molecules/LegacyModal' interface AddFixtureModalProps { - fixtureLocation: Cutout + cutoutId: CutoutId setShowAddFixtureModal: (showAddFixtureModal: boolean) => void - setCurrentDeckConfig?: React.Dispatch> - providedFixtureOptions?: FixtureLoadName[] + setCurrentDeckConfig?: React.Dispatch> + providedFixtureOptions?: CutoutFixtureId[] isOnDevice?: boolean } export function AddFixtureModal({ - fixtureLocation, + cutoutId, setShowAddFixtureModal, setCurrentDeckConfig, providedFixtureOptions, @@ -55,10 +60,11 @@ export function AddFixtureModal({ }: AddFixtureModalProps): JSX.Element { const { t } = useTranslation('device_details') const { updateDeckConfiguration } = useUpdateDeckConfigurationMutation() + const deckConfig = useDeckConfigurationQuery()?.data ?? [] const modalHeader: ModalHeaderBaseProps = { title: t('add_to_slot', { - slotName: getCutoutDisplayName(fixtureLocation), + slotName: getCutoutDisplayName(cutoutId), }), hasExitIcon: true, onClick: () => setShowAddFixtureModal(false), @@ -66,7 +72,7 @@ export function AddFixtureModal({ const modalProps: LegacyModalProps = { title: t('add_to_slot', { - slotName: getCutoutDisplayName(fixtureLocation), + slotName: getCutoutDisplayName(cutoutId), }), onClose: () => setShowAddFixtureModal(false), closeOnOutsideClick: true, @@ -74,26 +80,22 @@ export function AddFixtureModal({ width: '23.125rem', } - const availableFixtures: FixtureLoadName[] = [TRASH_BIN_LOAD_NAME] - if ( - fixtureLocation === 'cutoutA3' || - fixtureLocation === 'cutoutB3' || - fixtureLocation === 'cutoutC3' - ) { - availableFixtures.push(STAGING_AREA_LOAD_NAME) + const availableFixtures: CutoutFixtureId[] = [TRASH_BIN_ADAPTER_FIXTURE] + if (STAGING_AREA_CUTOUTS.includes(cutoutId)) { + availableFixtures.push(STAGING_AREA_RIGHT_SLOT_FIXTURE) } - if (fixtureLocation === 'cutoutD3') { - availableFixtures.push(STAGING_AREA_LOAD_NAME, WASTE_CHUTE_LOAD_NAME) + if (cutoutId === WASTE_CHUTE_CUTOUT) { + availableFixtures.push(...WASTE_CHUTE_FIXTURES) } // For Touchscreen app - const handleTapAdd = (fixtureLoadName: FixtureLoadName): void => { + const handleTapAdd = (requiredFixtureId: CutoutFixtureId): void => { if (setCurrentDeckConfig != null) setCurrentDeckConfig( (prevDeckConfig: DeckConfiguration): DeckConfiguration => - prevDeckConfig.map((fixture: Fixture) => - fixture.fixtureLocation === fixtureLocation - ? { ...fixture, loadName: fixtureLoadName } + prevDeckConfig.map((fixture: CutoutConfig) => + fixture.cutoutId === cutoutId + ? { ...fixture, cutoutFixtureId: requiredFixtureId } : fixture ) ) @@ -104,11 +106,14 @@ export function AddFixtureModal({ // For Desktop app const fixtureOptions = providedFixtureOptions ?? availableFixtures - const handleClickAdd = (fixtureLoadName: FixtureLoadName): void => { - updateDeckConfiguration({ - fixtureLocation, - loadName: fixtureLoadName, - }) + const handleClickAdd = (requiredFixtureId: CutoutFixtureId): void => { + const newDeckConfig = deckConfig.map(fixture => + fixture.cutoutId === cutoutId + ? { ...fixture, cutoutFixtureId: requiredFixtureId } + : fixture + ) + + updateDeckConfiguration(newDeckConfig) setShowAddFixtureModal(false) } @@ -122,10 +127,10 @@ export function AddFixtureModal({ {t('add_to_slot_description')} - {fixtureOptions.map(fixture => ( - + {fixtureOptions.map(cutoutFixtureId => ( + @@ -166,18 +171,18 @@ export function AddFixtureModal({ } interface AddFixtureButtonProps { - fixtureLoadName: FixtureLoadName - handleClickAdd: (fixtureLoadName: FixtureLoadName) => void + cutoutFixtureId: CutoutFixtureId + handleClickAdd: (cutoutFixtureId: CutoutFixtureId) => void } function AddFixtureButton({ - fixtureLoadName, + cutoutFixtureId, handleClickAdd, }: AddFixtureButtonProps): JSX.Element { const { t } = useTranslation('device_details') return ( handleClickAdd(fixtureLoadName)} + onClick={() => handleClickAdd(cutoutFixtureId)} display="flex" justifyContent={JUSTIFY_SPACE_BETWEEN} flexDirection={DIRECTION_ROW} @@ -186,7 +191,7 @@ function AddFixtureButton({ css={FIXTURE_BUTTON_STYLE} > - {getFixtureDisplayName(fixtureLoadName)} + {getFixtureDisplayName(cutoutFixtureId)} {t('add')} diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/AddFixtureModal.test.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/AddFixtureModal.test.tsx index f20f887bfd4..e31de9bb461 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/AddFixtureModal.test.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/AddFixtureModal.test.tsx @@ -1,12 +1,17 @@ import * as React from 'react' import { renderWithProviders } from '@opentrons/components' -import { useUpdateDeckConfigurationMutation } from '@opentrons/react-api-client' -import { TRASH_BIN_LOAD_NAME } from '@opentrons/shared-data' +import { + useDeckConfigurationQuery, + useUpdateDeckConfigurationMutation, +} from '@opentrons/react-api-client' import { i18n } from '../../../i18n' import { AddFixtureModal } from '../AddFixtureModal' +import type { UseQueryResult } from 'react-query' +import type { DeckConfiguration } from '@opentrons/shared-data' + jest.mock('@opentrons/react-api-client') const mockSetShowAddFixtureModal = jest.fn() const mockUpdateDeckConfiguration = jest.fn() @@ -15,6 +20,9 @@ const mockSetCurrentDeckConfig = jest.fn() const mockUseUpdateDeckConfigurationMutation = useUpdateDeckConfigurationMutation as jest.MockedFunction< typeof useUpdateDeckConfigurationMutation > +const mockUseDeckConfigurationQuery = useDeckConfigurationQuery as jest.MockedFunction< + typeof useDeckConfigurationQuery +> const render = (props: React.ComponentProps) => { return renderWithProviders(, { @@ -27,7 +35,7 @@ describe('Touchscreen AddFixtureModal', () => { beforeEach(() => { props = { - fixtureLocation: 'cutoutD3', + cutoutId: 'cutoutD3', setShowAddFixtureModal: mockSetShowAddFixtureModal, setCurrentDeckConfig: mockSetCurrentDeckConfig, isOnDevice: true, @@ -35,6 +43,9 @@ describe('Touchscreen AddFixtureModal', () => { mockUseUpdateDeckConfigurationMutation.mockReturnValue({ updateDeckConfiguration: mockUpdateDeckConfiguration, } as any) + mockUseDeckConfigurationQuery.mockReturnValue(({ + data: [], + } as unknown) as UseQueryResult) }) it('should render text and buttons', () => { @@ -43,10 +54,10 @@ describe('Touchscreen AddFixtureModal', () => { getByText( 'Choose a fixture below to add to your deck configuration. It will be referenced during protocol analysis.' ) - getByText('Staging Area Slot') - getByText('Trash Bin') - getByText('Waste Chute') - expect(getAllByText('Add').length).toBe(3) + getByText('Staging area slot') + getByText('Trash bin') + getByText('Waste chute only') + expect(getAllByText('Add').length).toBe(6) }) it('should a mock function when tapping app button', () => { @@ -61,7 +72,7 @@ describe('Desktop AddFixtureModal', () => { beforeEach(() => { props = { - fixtureLocation: 'cutoutD3', + cutoutId: 'cutoutD3', setShowAddFixtureModal: mockSetShowAddFixtureModal, } mockUseUpdateDeckConfigurationMutation.mockReturnValue({ @@ -79,42 +90,39 @@ describe('Desktop AddFixtureModal', () => { getByText( 'Add this fixture to your deck configuration. It will be referenced during protocol analysis.' ) - getByText('Staging Area Slot') - getByText('Trash Bin') - getByText('Waste Chute') - expect(getAllByRole('button', { name: 'Add' }).length).toBe(3) + getByText('Staging area slot') + getByText('Trash bin') + getByText('Waste chute only') + expect(getAllByRole('button', { name: 'Add' }).length).toBe(6) }) it('should render text and buttons slot A1', () => { - props = { ...props, fixtureLocation: 'cutoutA1' } + props = { ...props, cutoutId: 'cutoutA1' } const [{ getByText, getByRole }] = render(props) getByText('Add to slot A1') getByText( 'Add this fixture to your deck configuration. It will be referenced during protocol analysis.' ) - getByText('Trash Bin') + getByText('Trash bin') getByRole('button', { name: 'Add' }) }) it('should render text and buttons slot B3', () => { - props = { ...props, fixtureLocation: 'cutoutB3' } + props = { ...props, cutoutId: 'cutoutB3' } const [{ getByText, getAllByRole }] = render(props) getByText('Add to slot B3') getByText( 'Add this fixture to your deck configuration. It will be referenced during protocol analysis.' ) - getByText('Staging Area Slot') - getByText('Trash Bin') + getByText('Staging area slot') + getByText('Trash bin') expect(getAllByRole('button', { name: 'Add' }).length).toBe(2) }) it('should call a mock function when clicking add button', () => { - props = { ...props, fixtureLocation: 'cutoutA1' } + props = { ...props, cutoutId: 'cutoutA1' } const [{ getByRole }] = render(props) getByRole('button', { name: 'Add' }).click() - expect(mockUpdateDeckConfiguration).toHaveBeenCalledWith({ - fixtureLocation: 'cutoutA1', - loadName: TRASH_BIN_LOAD_NAME, - }) + expect(mockUpdateDeckConfiguration).toHaveBeenCalled() }) }) diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx index c57cef374f8..e925a0d057f 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx @@ -24,7 +24,10 @@ import { import { getCutoutDisplayName, getFixtureDisplayName, - STANDARD_SLOT_LOAD_NAME, + SINGLE_RIGHT_CUTOUTS, + SINGLE_SLOT_FIXTURES, + SINGLE_LEFT_SLOT_FIXTURE, + SINGLE_RIGHT_SLOT_FIXTURE, } from '@opentrons/shared-data' import { StyledText } from '../../atoms/text' @@ -33,7 +36,7 @@ import { DeckFixtureSetupInstructionsModal } from './DeckFixtureSetupInstruction import { AddFixtureModal } from './AddFixtureModal' import { useRunStatuses } from '../Devices/hooks' -import type { Cutout } from '@opentrons/shared-data' +import type { CutoutId } from '@opentrons/shared-data' const RUN_REFETCH_INTERVAL = 5000 @@ -52,10 +55,9 @@ export function DeviceDetailsDeckConfiguration({ const [showAddFixtureModal, setShowAddFixtureModal] = React.useState( false ) - const [ - targetFixtureLocation, - setTargetFixtureLocation, - ] = React.useState(null) + const [targetCutoutId, setTargetCutoutId] = React.useState( + null + ) const deckConfig = useDeckConfigurationQuery().data ?? [] const { updateDeckConfiguration } = useUpdateDeckConfigurationMutation() @@ -65,29 +67,38 @@ export function DeviceDetailsDeckConfiguration({ }) const isMaintenanceRunExisting = maintenanceRunData?.data?.id != null - const handleClickAdd = (fixtureLocation: Cutout): void => { - setTargetFixtureLocation(fixtureLocation) + const handleClickAdd = (cutoutId: CutoutId): void => { + setTargetCutoutId(cutoutId) setShowAddFixtureModal(true) } - const handleClickRemove = (fixtureLocation: Cutout): void => { - console.log('remove fixtureLocation', fixtureLocation) - updateDeckConfiguration({ - fixtureLocation, - loadName: STANDARD_SLOT_LOAD_NAME, - }) + const handleClickRemove = (cutoutId: CutoutId): void => { + const isRightCutout = SINGLE_RIGHT_CUTOUTS.includes(cutoutId) + const singleSlotFixture = isRightCutout + ? SINGLE_RIGHT_SLOT_FIXTURE + : SINGLE_LEFT_SLOT_FIXTURE + + const newDeckConfig = deckConfig.map(fixture => + fixture.cutoutId === cutoutId + ? { ...fixture, cutoutFixtureId: singleSlotFixture } + : fixture + ) + + updateDeckConfiguration(newDeckConfig) } // do not show standard slot in fixture display list const fixtureDisplayList = deckConfig.filter( - fixture => fixture.loadName !== STANDARD_SLOT_LOAD_NAME + fixture => + fixture.cutoutFixtureId != null && + !SINGLE_SLOT_FIXTURES.includes(fixture.cutoutFixtureId) ) return ( <> - {showAddFixtureModal && targetFixtureLocation != null ? ( + {showAddFixtureModal && targetCutoutId != null ? ( ) : null} @@ -182,7 +193,7 @@ export function DeviceDetailsDeckConfiguration({ {fixtureDisplayList.map(fixture => { return ( - {getCutoutDisplayName(fixture.fixtureLocation)} + {getCutoutDisplayName(fixture.cutoutId)} - {getFixtureDisplayName(fixture.loadName)} + {getFixtureDisplayName(fixture.cutoutFixtureId)} ) diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunSetup.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunSetup.tsx index bf059c06930..aa45ca4191d 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunSetup.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunSetup.tsx @@ -1,10 +1,7 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' -import { - LoadedFixturesBySlot, - parseAllRequiredModuleModels, -} from '@opentrons/api-client' +import { parseAllRequiredModuleModels } from '@opentrons/api-client' import { Flex, ALIGN_CENTER, @@ -17,15 +14,11 @@ import { TYPOGRAPHY, Link, } from '@opentrons/components' -import { - STAGING_AREA_LOAD_NAME, - TRASH_BIN_LOAD_NAME, - WASTE_CHUTE_LOAD_NAME, -} from '@opentrons/shared-data' import { Line } from '../../../atoms/structure' import { StyledText } from '../../../atoms/text' import { InfoMessage } from '../../../molecules/InfoMessage' +import { getSimplestDeckConfigForProtocolCommands } from '../../../resources/deck_configuration/utils' import { useIsFlex, useRobot, @@ -76,52 +69,6 @@ export function ProtocolRunSetup({ const protocolData = robotProtocolAnalysis ?? storedProtocolAnalysis const modules = parseAllRequiredModuleModels(protocolData?.commands ?? []) - // TODO(Jr, 10/4/23): stubbed in the fixtures for now - delete IMMEDIATELY - // const loadedFixturesBySlot = parseInitialLoadedFixturesByCutout( - // protocolData?.commands ?? [] - // ) - - const STUBBED_LOAD_FIXTURE_BY_SLOT: LoadedFixturesBySlot = { - D3: { - id: 'stubbed_load_fixture', - commandType: 'loadFixture', - params: { - fixtureId: 'stubbedFixtureId', - loadName: WASTE_CHUTE_LOAD_NAME, - location: { cutout: 'cutoutD3' }, - }, - createdAt: 'fakeTimestamp', - startedAt: 'fakeTimestamp', - completedAt: 'fakeTimestamp', - status: 'succeeded', - }, - B3: { - id: 'stubbed_load_fixture_2', - commandType: 'loadFixture', - params: { - fixtureId: 'stubbedFixtureId_2', - loadName: STAGING_AREA_LOAD_NAME, - location: { cutout: 'cutoutB3' }, - }, - createdAt: 'fakeTimestamp', - startedAt: 'fakeTimestamp', - completedAt: 'fakeTimestamp', - status: 'succeeded', - }, - C3: { - id: 'stubbed_load_fixture_3', - commandType: 'loadFixture', - params: { - fixtureId: 'stubbedFixtureId_3', - loadName: TRASH_BIN_LOAD_NAME, - location: { cutout: 'cutoutC3' }, - }, - createdAt: 'fakeTimestamp', - startedAt: 'fakeTimestamp', - completedAt: 'fakeTimestamp', - status: 'succeeded', - }, - } const robot = useRobot(robotName) const calibrationStatusRobot = useRunCalibrationStatus(robotName, runId) const calibrationStatusModules = useModuleCalibrationStatus(robotName, runId) @@ -168,8 +115,12 @@ export function ProtocolRunSetup({ if (robot == null) return null const hasLiquids = protocolData != null && protocolData.liquids?.length > 0 const hasModules = protocolData != null && modules.length > 0 - const hasFixtures = - protocolData != null && Object.keys(STUBBED_LOAD_FIXTURE_BY_SLOT).length > 0 + + const protocolDeckConfig = getSimplestDeckConfigForProtocolCommands( + protocolData?.commands ?? [] + ) + + const hasFixtures = protocolDeckConfig.length > 0 let moduleDescription: string = t(`${MODULE_SETUP_KEY}_description`, { count: modules.length, @@ -213,8 +164,8 @@ export function ProtocolRunSetup({ expandLabwarePositionCheckStep={() => setExpandedStepKey(LPC_KEY)} robotName={robotName} runId={runId} - loadedFixturesBySlot={STUBBED_LOAD_FIXTURE_BY_SLOT} hasModules={hasModules} + commands={protocolData?.commands ?? []} /> ), description: moduleDescription, diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx index 0225ff8e575..cdaaddce611 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx @@ -17,7 +17,7 @@ import { } from '@opentrons/shared-data' import { getLabwareSetupItemGroups } from '../../../../pages/Protocols/utils' -import { getDeckConfigFromProtocolCommands } from '../../../../resources/deck_configuration/utils' +import { getSimplestDeckConfigForProtocolCommands } from '../../../../resources/deck_configuration/utils' import { getAttachedProtocolModuleMatches } from '../../../ProtocolSetupModulesAndDeck/utils' import { useAttachedModules } from '../../hooks' import { LabwareInfoOverlay } from '../LabwareInfoOverlay' @@ -107,7 +107,7 @@ export function SetupLabwareMap({ const { offDeckItems } = getLabwareSetupItemGroups(commands) - const deckConfig = getDeckConfigFromProtocolCommands( + const deckConfig = getSimplestDeckConfigForProtocolCommands( protocolAnalysis.commands ) diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLiquids/SetupLiquidsMap.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLiquids/SetupLiquidsMap.tsx index 3b718b29d52..0c6a4ac80b4 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLiquids/SetupLiquidsMap.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLiquids/SetupLiquidsMap.tsx @@ -25,7 +25,7 @@ import { LabwareInfoOverlay } from '../LabwareInfoOverlay' import { LiquidsLabwareDetailsModal } from './LiquidsLabwareDetailsModal' import { getWellFillFromLabwareId } from './utils' import { getLabwareRenderInfo } from '../utils/getLabwareRenderInfo' -import { getDeckConfigFromProtocolCommands } from '../../../../resources/deck_configuration/utils' +import { getSimplestDeckConfigForProtocolCommands } from '../../../../resources/deck_configuration/utils' import { getStandardDeckViewLayerBlockList } from '../utils/getStandardDeckViewLayerBlockList' import { getAttachedProtocolModuleMatches } from '../../../ProtocolSetupModulesAndDeck/utils' import { getProtocolModulesInfo } from '../utils/getProtocolModulesInfo' @@ -70,7 +70,7 @@ export function SetupLiquidsMap( const labwareByLiquidId = parseLabwareInfoByLiquidId( protocolAnalysis.commands ?? [] ) - const deckConfig = getDeckConfigFromProtocolCommands( + const deckConfig = getSimplestDeckConfigForProtocolCommands( protocolAnalysis.commands ) const deckLayerBlocklist = getStandardDeckViewLayerBlockList(robotType) diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLiquids/__tests__/SetupLiquidsMap.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLiquids/__tests__/SetupLiquidsMap.test.tsx index 98e51b6ba4d..e4786c8d522 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLiquids/__tests__/SetupLiquidsMap.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLiquids/__tests__/SetupLiquidsMap.test.tsx @@ -6,7 +6,6 @@ import { renderWithProviders, partialComponentPropsMatcher, LabwareRender, - EXTENDED_DECK_CONFIG_FIXTURE, } from '@opentrons/components' import fixture_tiprack_300_ul from '@opentrons/shared-data/labware/fixtures/2/fixture_tiprack_300_ul.json' @@ -31,7 +30,7 @@ import { getLabwareRenderInfo } from '../../utils/getLabwareRenderInfo' import { getStandardDeckViewLayerBlockList } from '../../utils/getStandardDeckViewLayerBlockList' import { getAttachedProtocolModuleMatches } from '../../../../ProtocolSetupModulesAndDeck/utils' import { getProtocolModulesInfo } from '../../utils/getProtocolModulesInfo' -import { getDeckConfigFromProtocolCommands } from '../../../../../resources/deck_configuration/utils' +import { getSimplestDeckConfigForProtocolCommands } from '../../../../../resources/deck_configuration/utils' import { mockProtocolModuleInfo } from '../../../../ProtocolSetupLabware/__fixtures__' import { mockFetchModulesSuccessActionPayloadModules } from '../../../../../redux/modules/__fixtures__' @@ -97,8 +96,8 @@ const mockGetAttachedProtocolModuleMatches = getAttachedProtocolModuleMatches as const mockGetProtocolModulesInfo = getProtocolModulesInfo as jest.MockedFunction< typeof getProtocolModulesInfo > -const mockGetDeckConfigFromProtocolCommands = getDeckConfigFromProtocolCommands as jest.MockedFunction< - typeof getDeckConfigFromProtocolCommands +const mockGetSimplestDeckConfigForProtocolCommands = getSimplestDeckConfigForProtocolCommands as jest.MockedFunction< + typeof getSimplestDeckConfigForProtocolCommands > const RUN_ID = '1' @@ -164,9 +163,10 @@ describe('SetupLiquidsMap', () => { when(mockGetLabwareRenderInfo) .calledWith(simpleAnalysisFileFixture as any, ot2StandardDeckDef as any) .mockReturnValue({}) - when(mockGetDeckConfigFromProtocolCommands) + when(mockGetSimplestDeckConfigForProtocolCommands) .calledWith(simpleAnalysisFileFixture.commands as RunTimeCommand[]) - .mockReturnValue(EXTENDED_DECK_CONFIG_FIXTURE) + // TODO(bh, 2023-11-13): mock the cutout config protocol spec + .mockReturnValue([]) when(mockGetRobotTypeFromLoadedLabware) .calledWith(simpleAnalysisFileFixture.labware as any) .mockReturnValue(FLEX_ROBOT_TYPE) @@ -335,7 +335,6 @@ describe('SetupLiquidsMap', () => { when(mockBaseDeck) .calledWith( partialComponentPropsMatcher({ - deckConfig: EXTENDED_DECK_CONFIG_FIXTURE, deckLayerBlocklist: getStandardDeckViewLayerBlockList( FLEX_ROBOT_TYPE ), diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal.tsx index c4960344fa3..10f3dfeb7e3 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal.tsx @@ -24,7 +24,9 @@ import { getCutoutDisplayName, getFixtureDisplayName, getModuleDisplayName, - STANDARD_SLOT_LOAD_NAME, + SINGLE_RIGHT_CUTOUTS, + SINGLE_LEFT_SLOT_FIXTURE, + SINGLE_RIGHT_SLOT_FIXTURE, } from '@opentrons/shared-data' import { Portal } from '../../../../App/portal' import { LegacyModal } from '../../../../molecules/LegacyModal' @@ -33,16 +35,16 @@ import { Modal } from '../../../../molecules/Modal' import { SmallButton } from '../../../../atoms/buttons/SmallButton' import type { - Cutout, - Fixture, - FixtureLoadName, + CutoutConfig, + CutoutId, + CutoutFixtureId, ModuleModel, } from '@opentrons/shared-data' interface LocationConflictModalProps { onCloseClick: () => void - cutout: Cutout - requiredFixture?: FixtureLoadName + cutoutId: CutoutId + requiredFixtureId?: CutoutFixtureId requiredModule?: ModuleModel isOnDevice?: boolean } @@ -52,33 +54,44 @@ export const LocationConflictModal = ( ): JSX.Element => { const { onCloseClick, - cutout, - requiredFixture, + cutoutId, + requiredFixtureId, requiredModule, isOnDevice = false, } = props const { t, i18n } = useTranslation(['protocol_setup', 'shared']) const deckConfig = useDeckConfigurationQuery().data ?? [] const { updateDeckConfiguration } = useUpdateDeckConfigurationMutation() - const deckConfigurationAtLocationLoadName = deckConfig.find( - (deckFixture: Fixture) => deckFixture.fixtureLocation === cutout - )?.loadName + const deckConfigurationAtLocationFixtureId = deckConfig.find( + (deckFixture: CutoutConfig) => deckFixture.cutoutId === cutoutId + )?.cutoutFixtureId const currentFixtureDisplayName = - deckConfigurationAtLocationLoadName != null - ? getFixtureDisplayName(deckConfigurationAtLocationLoadName) + deckConfigurationAtLocationFixtureId != null + ? getFixtureDisplayName(deckConfigurationAtLocationFixtureId) : '' const handleUpdateDeck = (): void => { - if (requiredFixture != null) { - updateDeckConfiguration({ - fixtureLocation: cutout, - loadName: requiredFixture, - }) + if (requiredFixtureId != null) { + const newRequiredFixtureDeckConfig = deckConfig.map(fixture => + fixture.cutoutId === cutoutId + ? { ...fixture, cutoutFixtureId: requiredFixtureId } + : fixture + ) + + updateDeckConfiguration(newRequiredFixtureDeckConfig) } else { - updateDeckConfiguration({ - fixtureLocation: cutout, - loadName: STANDARD_SLOT_LOAD_NAME, - }) + const isRightCutout = SINGLE_RIGHT_CUTOUTS.includes(cutoutId) + const singleSlotFixture = isRightCutout + ? SINGLE_RIGHT_SLOT_FIXTURE + : SINGLE_LEFT_SLOT_FIXTURE + + const newSingleSlotDeckConfig = deckConfig.map(fixture => + fixture.cutoutId === cutoutId + ? { ...fixture, cutoutFixtureId: singleSlotFixture } + : fixture + ) + + updateDeckConfiguration(newSingleSlotDeckConfig) } onCloseClick() } @@ -102,7 +115,7 @@ export const LocationConflictModal = ( i18nKey="deck_conflict_info" values={{ currentFixture: currentFixtureDisplayName, - cutout: getCutoutDisplayName(cutout), + cutout: getCutoutDisplayName(cutoutId), }} components={{ block: , @@ -115,7 +128,9 @@ export const LocationConflictModal = ( fontWeight={TYPOGRAPHY.fontWeightBold} paddingBottom={SPACING.spacing8} > - {t('slot_location', { slotName: getCutoutDisplayName(cutout) })} + {t('slot_location', { + slotName: getCutoutDisplayName(cutoutId), + })} - {requiredFixture != null && - getFixtureDisplayName(requiredFixture)} + {requiredFixtureId != null && + getFixtureDisplayName(requiredFixtureId)} {requiredModule != null && getModuleDisplayName(requiredModule)} @@ -199,7 +214,7 @@ export const LocationConflictModal = ( i18nKey="deck_conflict_info" values={{ currentFixture: currentFixtureDisplayName, - cutout: getCutoutDisplayName(cutout), + cutout: getCutoutDisplayName(cutoutId), }} components={{ block: , @@ -211,7 +226,9 @@ export const LocationConflictModal = ( fontSize={TYPOGRAPHY.fontSizeH4} fontWeight={TYPOGRAPHY.fontWeightBold} > - {t('slot_location', { slotName: getCutoutDisplayName(cutout) })} + {t('slot_location', { + slotName: getCutoutDisplayName(cutoutId), + })} - {requiredFixture != null && - getFixtureDisplayName(requiredFixture)} + {requiredFixtureId != null && + getFixtureDisplayName(requiredFixtureId)} {requiredModule != null && getModuleDisplayName(requiredModule)} diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/NotConfiguredModal.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/NotConfiguredModal.tsx index c568ef1fe95..dbed3152c1a 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/NotConfiguredModal.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/NotConfiguredModal.tsx @@ -1,6 +1,9 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' -import { useUpdateDeckConfigurationMutation } from '@opentrons/react-api-client/src/deck_configuration' +import { + useDeckConfigurationQuery, + useUpdateDeckConfigurationMutation, +} from '@opentrons/react-api-client/src/deck_configuration' import { Flex, DIRECTION_COLUMN, @@ -17,26 +20,30 @@ import { Portal } from '../../../../App/portal' import { LegacyModal } from '../../../../molecules/LegacyModal' import { StyledText } from '../../../../atoms/text' -import type { Cutout, FixtureLoadName } from '@opentrons/shared-data' +import type { CutoutFixtureId, CutoutId } from '@opentrons/shared-data' interface NotConfiguredModalProps { onCloseClick: () => void - requiredFixture: FixtureLoadName - cutout: Cutout + requiredFixtureId: CutoutFixtureId + cutoutId: CutoutId } export const NotConfiguredModal = ( props: NotConfiguredModalProps ): JSX.Element => { - const { onCloseClick, cutout, requiredFixture } = props + const { onCloseClick, cutoutId, requiredFixtureId } = props const { t, i18n } = useTranslation(['protocol_setup', 'shared']) const { updateDeckConfiguration } = useUpdateDeckConfigurationMutation() + const deckConfig = useDeckConfigurationQuery()?.data ?? [] const handleUpdateDeck = (): void => { - updateDeckConfiguration({ - fixtureLocation: cutout, - loadName: requiredFixture, - }) + const newDeckConfig = deckConfig.map(fixture => + fixture.cutoutId === cutoutId + ? { ...fixture, cutoutFixtureId: requiredFixtureId } + : fixture + ) + + updateDeckConfiguration(newDeckConfig) onCloseClick() } @@ -46,7 +53,7 @@ export const NotConfiguredModal = ( title={ {t('add_fixture', { - fixtureName: getFixtureDisplayName(requiredFixture), + fixtureName: getFixtureDisplayName(requiredFixtureId), })} } @@ -64,7 +71,7 @@ export const NotConfiguredModal = ( justifyContent={JUSTIFY_SPACE_BETWEEN} > - {getFixtureDisplayName(requiredFixture)} + {getFixtureDisplayName(requiredFixtureId)} {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 4817fc8ca09..80f720c51fe 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupFixtureList.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupFixtureList.tsx @@ -1,5 +1,4 @@ import * as React from 'react' -import map from 'lodash/map' import { css } from 'styled-components' import { useTranslation } from 'react-i18next' import { @@ -16,16 +15,10 @@ import { TYPOGRAPHY, } from '@opentrons/components' import { - FixtureLoadName, + SINGLE_SLOT_FIXTURES, + getCutoutDisplayName, getFixtureDisplayName, - LoadFixtureRunTimeCommand, } from '@opentrons/shared-data' -import { - useLoadedFixturesConfigStatus, - CONFIGURED, - CONFLICTING, - NOT_CONFIGURED, -} from '../../../../resources/deck_configuration/hooks' import { StyledText } from '../../../../atoms/text' import { StatusLabel } from '../../../../atoms/StatusLabel' import { TertiaryButton } from '../../../../atoms/buttons/TertiaryButton' @@ -33,15 +26,14 @@ import { LocationConflictModal } from './LocationConflictModal' import { NotConfiguredModal } from './NotConfiguredModal' import { getFixtureImage } from './utils' -import type { LoadedFixturesBySlot } from '@opentrons/api-client' -import type { Cutout } from '@opentrons/shared-data' +import type { CutoutConfigAndCompatibility } from '../../../../resources/deck_configuration/hooks' interface SetupFixtureListProps { - loadedFixturesBySlot: LoadedFixturesBySlot + deckConfigCompatibility: CutoutConfigAndCompatibility[] } export const SetupFixtureList = (props: SetupFixtureListProps): JSX.Element => { - const { loadedFixturesBySlot } = props + const { deckConfigCompatibility } = props const { t, i18n } = useTranslation('protocol_setup') return ( <> @@ -81,15 +73,11 @@ export const SetupFixtureList = (props: SetupFixtureListProps): JSX.Element => { gridGap={SPACING.spacing4} marginBottom={SPACING.spacing24} > - {map(loadedFixturesBySlot, ({ params, id }) => { - const { loadName, location } = params + {deckConfigCompatibility.map(cutoutConfigAndCompatibility => { return ( ) })} @@ -98,54 +86,43 @@ export const SetupFixtureList = (props: SetupFixtureListProps): JSX.Element => { ) } -interface FixtureListItemProps { - loadedFixtures: LoadFixtureRunTimeCommand[] - loadName: FixtureLoadName - cutout: Cutout - commandId: string -} +interface FixtureListItemProps extends CutoutConfigAndCompatibility {} export function FixtureListItem({ - loadedFixtures, - loadName, - cutout, - commandId, + cutoutId, + cutoutFixtureId, + compatibleCutoutFixtureIds, }: FixtureListItemProps): JSX.Element { const { t } = useTranslation('protocol_setup') - const configuration = useLoadedFixturesConfigStatus(loadedFixtures) - const configurationStatus = configuration.find( - config => config.id === commandId - )?.configurationStatus + const isCurrentFixtureCompatible = + cutoutFixtureId != null && + compatibleCutoutFixtureIds.includes(cutoutFixtureId) + const isConflictingFixtureConfigured = + cutoutFixtureId != null && !SINGLE_SLOT_FIXTURES.includes(cutoutFixtureId) let statusLabel - if ( - configurationStatus === CONFLICTING || - configurationStatus === NOT_CONFIGURED - ) { + if (!isCurrentFixtureCompatible) { statusLabel = ( ) - } else if (configurationStatus === CONFIGURED) { + } else { statusLabel = ( ) - // shouldn't run into this case - } else { - statusLabel = 'status label unknown' } const [ @@ -159,18 +136,18 @@ export function FixtureListItem({ return ( <> - {showNotConfiguredModal ? ( + {showNotConfiguredModal && cutoutFixtureId != null ? ( setShowNotConfiguredModal(false)} - cutout={cutout} - requiredFixture={loadName} + cutoutId={cutoutId} + requiredFixtureId={compatibleCutoutFixtureIds[0]} /> ) : null} - {showLocationConflictModal ? ( + {showLocationConflictModal && cutoutFixtureId != null ? ( setShowLocationConflictModal(false)} - cutout={cutout} - requiredFixture={loadName} + cutoutId={cutoutId} + requiredFixtureId={compatibleCutoutFixtureIds[0]} /> ) : null} - + {cutoutFixtureId != null ? ( + + ) : null} - {getFixtureDisplayName(loadName)} + {getFixtureDisplayName(cutoutFixtureId)} - {cutout} + {getCutoutDisplayName(cutoutId)} {statusLabel} - {configurationStatus !== CONFIGURED ? ( + {!isCurrentFixtureCompatible ? ( - configurationStatus === CONFLICTING + isConflictingFixtureConfigured ? setShowLocationConflictModal(true) : setShowNotConfiguredModal(true) } diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx index 0f52f417742..df50b6bf721 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx @@ -20,6 +20,8 @@ import { TOOLTIP_LEFT, } from '@opentrons/components' import { + FLEX_ROBOT_TYPE, + getDeckDefFromRobotType, getModuleType, HEATERSHAKER_MODULE_TYPE, HEATERSHAKER_MODULE_V1, @@ -33,6 +35,7 @@ import { TertiaryButton } from '../../../../atoms/buttons' import { StatusLabel } from '../../../../atoms/StatusLabel' import { StyledText } from '../../../../atoms/text' import { Tooltip } from '../../../../atoms/Tooltip' +import { getCutoutIdForSlotName } from '../../../../resources/deck_configuration/utils' import { useChainLiveCommands } from '../../../../resources/runs/hooks' import { ModuleSetupModal } from '../../../ModuleCard/ModuleSetupModal' import { ModuleWizardFlows } from '../../../ModuleWizardFlows' @@ -42,6 +45,7 @@ import { ModuleRenderInfoForProtocol, useIsFlex, useModuleRenderInfoForProtocolById, + useRobot, useUnmatchedModulesForProtocol, useRunCalibrationStatus, } from '../../hooks' @@ -50,7 +54,11 @@ import { MultipleModulesModal } from './MultipleModulesModal' import { UnMatchedModuleWarning } from './UnMatchedModuleWarning' import { getModuleImage } from './utils' -import type { Cutout, ModuleModel, Fixture } from '@opentrons/shared-data' +import type { + CutoutConfig, + DeckDefinition, + ModuleModel, +} from '@opentrons/shared-data' import type { AttachedModule } from '../../../../redux/modules/types' import type { ProtocolCalibrationStatus } from '../../hooks' @@ -72,6 +80,8 @@ export const SetupModulesList = (props: SetupModulesListProps): JSX.Element => { } = useUnmatchedModulesForProtocol(robotName, runId) const isFlex = useIsFlex(robotName) + const { robotModel } = useRobot(robotName) ?? {} + const deckDef = getDeckDefFromRobotType(robotModel ?? FLEX_ROBOT_TYPE) const calibrationStatus = useRunCalibrationStatus(robotName, runId) @@ -188,6 +198,7 @@ export const SetupModulesList = (props: SetupModulesListProps): JSX.Element => { isFlex={isFlex} calibrationStatus={calibrationStatus} conflictedFixture={conflictedFixture} + deckDef={deckDef} /> ) } @@ -205,7 +216,8 @@ interface ModulesListItemProps { heaterShakerModuleFromProtocol: ModuleRenderInfoForProtocol | null isFlex: boolean calibrationStatus: ProtocolCalibrationStatus - conflictedFixture?: Fixture + deckDef: DeckDefinition + conflictedFixture?: CutoutConfig } export function ModulesListItem({ @@ -217,6 +229,7 @@ export function ModulesListItem({ isFlex, calibrationStatus, conflictedFixture, + deckDef, }: ModulesListItemProps): JSX.Element { const { t } = useTranslation(['protocol_setup', 'module_wizard_flows']) const moduleConnectionStatus = @@ -346,13 +359,16 @@ export function ModulesListItem({ ) } + // convert slot name to cutout id + const cutoutIdForSlotName = getCutoutIdForSlotName(slotName, deckDef) + return ( <> - {showLocationConflictModal ? ( + {showLocationConflictModal && cutoutIdForSlotName != null ? ( setShowLocationConflictModal(false)} // TODO(bh, 2023-10-10): when module caddies are fixtures, narrow slotName to Cutout and remove type assertion - cutout={slotName as Cutout} + cutoutId={cutoutIdForSlotName} requiredModule={moduleModel} /> ) : null} diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesMap.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesMap.tsx index 6d24f7b6eee..d441a602e2a 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesMap.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesMap.tsx @@ -12,7 +12,7 @@ import { getRobotTypeFromLoadedLabware, } from '@opentrons/shared-data' -import { getDeckConfigFromProtocolCommands } from '../../../../resources/deck_configuration/utils' +import { getSimplestDeckConfigForProtocolCommands } from '../../../../resources/deck_configuration/utils' import { useMostRecentCompletedAnalysis } from '../../../LabwarePositionCheck/useMostRecentCompletedAnalysis' import { getAttachedProtocolModuleMatches } from '../../../ProtocolSetupModulesAndDeck/utils' import { ModuleInfo } from '../../ModuleInfo' @@ -64,7 +64,7 @@ export const SetupModulesMap = ({ ), })) - const deckConfig = getDeckConfigFromProtocolCommands( + const deckConfig = getSimplestDeckConfigForProtocolCommands( protocolAnalysis.commands ) @@ -77,7 +77,10 @@ export const SetupModulesMap = ({ > ({ + cutoutId, + cutoutFixtureId, + }))} deckLayerBlocklist={getStandardDeckViewLayerBlockList(robotType)} robotType={robotType} labwareLocations={[]} diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/LocationConflictModal.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/LocationConflictModal.test.tsx index a131b3c314a..06d2e1507c7 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/LocationConflictModal.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/LocationConflictModal.test.tsx @@ -2,10 +2,8 @@ import * as React from 'react' import { UseQueryResult } from 'react-query' import { renderWithProviders } from '@opentrons/components' import { - DeckConfiguration, - STAGING_AREA_LOAD_NAME, - Fixture, - TRASH_BIN_LOAD_NAME, + STAGING_AREA_RIGHT_SLOT_FIXTURE, + TRASH_BIN_ADAPTER_FIXTURE, } from '@opentrons/shared-data' import { useDeckConfigurationQuery, @@ -14,6 +12,8 @@ import { import { i18n } from '../../../../../i18n' import { LocationConflictModal } from '../LocationConflictModal' +import type { DeckConfiguration } from '@opentrons/shared-data' + jest.mock('@opentrons/react-api-client/src/deck_configuration') const mockUseDeckConfigurationQuery = useDeckConfigurationQuery as jest.MockedFunction< @@ -24,10 +24,9 @@ const mockUseUpdateDeckConfigurationMutation = useUpdateDeckConfigurationMutatio > const mockFixture = { - fixtureId: 'mockId', - fixtureLocation: 'cutoutB3', - loadName: STAGING_AREA_LOAD_NAME, -} as Fixture + cutoutId: 'cutoutB3', + cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, +} const render = (props: React.ComponentProps) => { return renderWithProviders(, { @@ -41,8 +40,8 @@ describe('LocationConflictModal', () => { beforeEach(() => { props = { onCloseClick: jest.fn(), - cutout: 'cutoutB3', - requiredFixture: TRASH_BIN_LOAD_NAME, + cutoutId: 'cutoutB3', + requiredFixtureId: TRASH_BIN_ADAPTER_FIXTURE, } mockUseDeckConfigurationQuery.mockReturnValue({ data: [mockFixture], @@ -57,8 +56,8 @@ describe('LocationConflictModal', () => { getByText('Slot B3') getByText('Protocol specifies') getByText('Currently configured') - getAllByText('Staging Area Slot') - getByText('Trash Bin') + getAllByText('Staging area slot') + getByText('Trash bin') getByRole('button', { name: 'Cancel' }).click() expect(props.onCloseClick).toHaveBeenCalled() getByRole('button', { name: 'Update deck' }).click() @@ -67,7 +66,7 @@ describe('LocationConflictModal', () => { it('should render the modal information for a module fixture conflict', () => { props = { onCloseClick: jest.fn(), - cutout: 'cutoutB3', + cutoutId: 'cutoutB3', requiredModule: 'heaterShakerModuleV1', } const { getByText, getByRole } = render(props) @@ -89,8 +88,8 @@ describe('LocationConflictModal', () => { getByText('Slot B3') getByText('Protocol specifies') getByText('Currently configured') - getAllByText('Staging Area Slot') - getByText('Trash Bin') + getAllByText('Staging area slot') + getByText('Trash bin') getByText('Cancel').click() expect(props.onCloseClick).toHaveBeenCalled() getByText('Confirm removal').click() diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/NotConfiguredModal.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/NotConfiguredModal.test.tsx index 6a000013101..f2d5fb5ca38 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/NotConfiguredModal.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/NotConfiguredModal.test.tsx @@ -1,15 +1,24 @@ 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 { TRASH_BIN_ADAPTER_FIXTURE } from '@opentrons/shared-data' +import { + useDeckConfigurationQuery, + useUpdateDeckConfigurationMutation, +} from '@opentrons/react-api-client/src/deck_configuration' import { i18n } from '../../../../../i18n' import { NotConfiguredModal } from '../NotConfiguredModal' +import type { UseQueryResult } from 'react-query' +import type { DeckConfiguration } from '@opentrons/shared-data' + jest.mock('@opentrons/react-api-client/src/deck_configuration') const mockUseUpdateDeckConfigurationMutation = useUpdateDeckConfigurationMutation as jest.MockedFunction< typeof useUpdateDeckConfigurationMutation > +const mockUseDeckConfigurationQuery = useDeckConfigurationQuery as jest.MockedFunction< + typeof useDeckConfigurationQuery +> const render = (props: React.ComponentProps) => { return renderWithProviders(, { @@ -23,20 +32,23 @@ describe('NotConfiguredModal', () => { beforeEach(() => { props = { onCloseClick: jest.fn(), - cutout: 'cutoutB3', - requiredFixture: TRASH_BIN_LOAD_NAME, + cutoutId: 'cutoutB3', + requiredFixtureId: TRASH_BIN_ADAPTER_FIXTURE, } mockUseUpdateDeckConfigurationMutation.mockReturnValue({ updateDeckConfiguration: mockUpdate, } as any) + mockUseDeckConfigurationQuery.mockReturnValue(({ + data: [], + } as unknown) as UseQueryResult) }) it('renders the correct text and button works as expected', () => { const { getByText, getByRole } = render(props) - getByText('Add Trash Bin to deck configuration') + 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') + 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 f35e647914b..d40128fffd6 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupFixtureList.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupFixtureList.test.tsx @@ -1,47 +1,34 @@ import * as React from 'react' import { renderWithProviders } from '@opentrons/components' -import { - LoadFixtureRunTimeCommand, - WASTE_CHUTE_LOAD_NAME, - WASTE_CHUTE_CUTOUT, -} from '@opentrons/shared-data' +import { STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE } from '@opentrons/shared-data' 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' + +import type { CutoutConfigAndCompatibility } from '../../../../../resources/deck_configuration/hooks' 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', - params: { - fixtureId: 'stubbedFixtureId', - loadName: WASTE_CHUTE_LOAD_NAME, - location: { cutout: 'cutoutD3' }, - }, - createdAt: 'fakeTimestamp', - startedAt: 'fakeTimestamp', - completedAt: 'fakeTimestamp', - status: 'succeeded', -} as LoadFixtureRunTimeCommand -const mockLoadedFixturesBySlot: LoadedFixturesBySlot = { - D3: mockLoadedFixture, -} +const mockDeckConfigCompatibility: CutoutConfigAndCompatibility[] = [ + { + cutoutId: 'cutoutD3', + cutoutFixtureId: STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, + requiredAddressableAreas: ['D4'], + compatibleCutoutFixtureIds: [ + STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, + ], + }, +] const render = (props: React.ComponentProps) => { return renderWithProviders(, { @@ -53,14 +40,8 @@ describe('SetupFixtureList', () => { let props: React.ComponentProps beforeEach(() => { props = { - loadedFixturesBySlot: mockLoadedFixturesBySlot, + deckConfigCompatibility: mockDeckConfigCompatibility, } - mockUseLoadedFixturesConfigStatus.mockReturnValue([ - { - ...mockLoadedFixture, - configurationStatus: 'configured', - }, - ]) mockLocationConflictModal.mockReturnValue(
mock location conflict modal
) @@ -72,33 +53,22 @@ describe('SetupFixtureList', () => { getByText('Fixture') getByText('Location') getByText('Status') - getByText('Waste Chute') + getByText('Waste chute with staging area slot') getByRole('button', { name: 'View setup instructions' }) - getByText(WASTE_CHUTE_CUTOUT) + getByText('D3') getByText('Configured') }) - it('should render the headers and a fixture with conflicted status', () => { - mockUseLoadedFixturesConfigStatus.mockReturnValue([ - { - ...mockLoadedFixture, - configurationStatus: 'conflicting', - }, - ]) - const { getByText, getByRole } = render(props)[0] - getByText('Location conflict') - getByRole('button', { name: 'Update deck' }).click() - getByText('mock location conflict modal') - }) - it('should render the headers and a fixture with not configured status and button', () => { - mockUseLoadedFixturesConfigStatus.mockReturnValue([ - { - ...mockLoadedFixture, - configurationStatus: 'not configured', - }, - ]) - const { getByText, getByRole } = render(props)[0] - getByText('Not configured') - getByRole('button', { name: 'Update deck' }).click() - getByText('mock not configured modal') - }) + // TODO(bh, 2023-11-14): implement test cases when example JSON protocol fixtures exist + // it('should render the headers and a fixture with conflicted status', () => { + // const { getByText, getByRole } = render(props)[0] + // getByText('Location conflict') + // getByRole('button', { name: 'Update deck' }).click() + // getByText('mock location conflict modal') + // }) + // it('should render the headers and a fixture with not configured status and button', () => { + // const { getByText, getByRole } = render(props)[0] + // getByText('Not configured') + // getByRole('button', { name: 'Update deck' }).click() + // getByText('mock not configured modal') + // }) }) diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesAndDeck.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesAndDeck.test.tsx index 4fd6e703904..07797b57c0d 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesAndDeck.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesAndDeck.test.tsx @@ -2,7 +2,6 @@ import * as React from 'react' import { fireEvent } from '@testing-library/react' import { when } from 'jest-when' import { renderWithProviders } from '@opentrons/components' -import { WASTE_CHUTE_LOAD_NAME } from '@opentrons/shared-data' import { i18n } from '../../../../../i18n' import { useIsFlex, @@ -15,23 +14,6 @@ import { SetupModulesList } from '../SetupModulesList' import { SetupModulesMap } from '../SetupModulesMap' import { SetupFixtureList } from '../SetupFixtureList' import { mockTemperatureModule } from '../../../../../redux/modules/__fixtures__' -import { LoadedFixturesBySlot } from '@opentrons/api-client' - -const mockLoadedFixturesBySlot: LoadedFixturesBySlot = { - D3: { - id: 'stubbed_load_fixture', - commandType: 'loadFixture', - params: { - fixtureId: 'stubbedFixtureId', - loadName: WASTE_CHUTE_LOAD_NAME, - location: { cutout: 'cutoutD3' }, - }, - createdAt: 'fakeTimestamp', - startedAt: 'fakeTimestamp', - completedAt: 'fakeTimestamp', - status: 'succeeded', - }, -} jest.mock('../../../hooks') jest.mock('../SetupModulesList') @@ -75,7 +57,7 @@ describe('SetupModuleAndDeck', () => { runId: MOCK_RUN_ID, expandLabwarePositionCheckStep: () => jest.fn(), hasModules: true, - loadedFixturesBySlot: {}, + commands: [], } mockSetupFixtureList.mockReturnValue(
Mock setup fixture list
) mockSetupModulesList.mockReturnValue(
Mock setup modules list
) @@ -141,7 +123,6 @@ describe('SetupModuleAndDeck', () => { it('should render the SetupModulesList and SetupFixtureList component when clicking List View for Flex', () => { when(mockUseIsFlex).calledWith(MOCK_ROBOT_NAME).mockReturnValue(true) - props.loadedFixturesBySlot = mockLoadedFixturesBySlot const { getByRole, getByText } = render(props) const button = getByRole('button', { name: 'List View' }) fireEvent.click(button) diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx index 4e2fc01c73c..efe07a3f6b0 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx @@ -2,6 +2,7 @@ import * as React from 'react' import { when, resetAllWhenMocks } from 'jest-when' import { fireEvent, waitFor } from '@testing-library/react' import { renderWithProviders } from '@opentrons/components' +import { STAGING_AREA_RIGHT_SLOT_FIXTURE } from '@opentrons/shared-data' import { i18n } from '../../../../../i18n' import { mockMagneticModule as mockMagneticModuleFixture, @@ -27,11 +28,7 @@ import { UnMatchedModuleWarning } from '../UnMatchedModuleWarning' import { SetupModulesList } from '../SetupModulesList' import { LocationConflictModal } from '../LocationConflictModal' -import { - ModuleModel, - ModuleType, - STAGING_AREA_LOAD_NAME, -} from '@opentrons/shared-data' +import type { ModuleModel, ModuleType } from '@opentrons/shared-data' jest.mock('@opentrons/react-api-client') jest.mock('../../../hooks') @@ -446,7 +443,7 @@ describe('SetupModulesList', () => { fireEvent.click(moduleSetup) getByText('mockModuleSetupModal') }) - it('shoulde render a magnetic block with a conflicted fixture', () => { + it('should render a magnetic block with a conflicted fixture', () => { when(mockUseIsFlex).calledWith(ROBOT_NAME).mockReturnValue(true) mockUseModuleRenderInfoForProtocolById.mockReturnValue({ [mockMagneticBlock.id]: { @@ -463,12 +460,11 @@ describe('SetupModulesList', () => { nestedLabwareDef: null, nestedLabwareId: null, protocolLoadOrder: 0, - slotName: '1', + slotName: 'B3', attachedModuleMatch: null, conflictedFixture: { - fixtureId: 'mockId', - fixtureLocation: '1', - loadName: STAGING_AREA_LOAD_NAME, + cutoutId: 'cutoutB3', + cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, }, }, } as any) diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/utils.test.ts b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/utils.test.ts index 96e1470f78f..2388de2b936 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/utils.test.ts +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/utils.test.ts @@ -44,15 +44,15 @@ describe('getModuleImage', () => { describe('getFixtureImage', () => { it('should render the staging area image', () => { - const result = getFixtureImage('stagingArea') + const result = getFixtureImage('stagingAreaRightSlot') expect(result).toEqual('staging_area_slot.png') }) it('should render the waste chute image', () => { - const result = getFixtureImage('wasteChute') + const result = getFixtureImage('wasteChuteRightAdapterNoCover') expect(result).toEqual('waste_chute.png') }) it('should render the trash binimage', () => { - const result = getFixtureImage('trashBin') + const result = getFixtureImage('trashBinAdapter') expect(result).toEqual('flex_trash_bin.png') }) // TODO(jr, 10/17/23): add rest of the test cases when we add the assets diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/index.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/index.tsx index 3ef1a8c7603..213b1bc84f5 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/index.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/index.tsx @@ -8,33 +8,37 @@ import { useHoverTooltip, PrimaryButton, } from '@opentrons/components' +import { FLEX_SINGLE_SLOT_ADDRESSABLE_AREAS } from '@opentrons/shared-data' + import { useToggleGroup } from '../../../../molecules/ToggleGroup/useToggleGroup' +import { useDeckConfigurationCompatibility } from '../../../../resources/deck_configuration/hooks' import { Tooltip } from '../../../../atoms/Tooltip' import { - useIsFlex, useRunHasStarted, useUnmatchedModulesForProtocol, useModuleCalibrationStatus, + useRobotType, } from '../../hooks' import { SetupModulesMap } from './SetupModulesMap' import { SetupModulesList } from './SetupModulesList' import { SetupFixtureList } from './SetupFixtureList' -import type { LoadedFixturesBySlot } from '@opentrons/api-client' + +import type { RunTimeCommand } from '@opentrons/shared-data' interface SetupModuleAndDeckProps { expandLabwarePositionCheckStep: () => void robotName: string runId: string - loadedFixturesBySlot: LoadedFixturesBySlot hasModules: boolean + commands: RunTimeCommand[] } export const SetupModuleAndDeck = ({ expandLabwarePositionCheckStep, robotName, runId, - loadedFixturesBySlot, hasModules, + commands, }: SetupModuleAndDeckProps): JSX.Element => { const { t } = useTranslation('protocol_setup') const [selectedValue, toggleGroup] = useToggleGroup( @@ -42,12 +46,28 @@ export const SetupModuleAndDeck = ({ t('map_view') ) - const isFlex = useIsFlex(robotName) + const robotType = useRobotType(robotName) const { missingModuleIds } = useUnmatchedModulesForProtocol(robotName, runId) const runHasStarted = useRunHasStarted(runId) const [targetProps, tooltipProps] = useHoverTooltip() const moduleCalibrationStatus = useModuleCalibrationStatus(robotName, runId) + const deckConfigCompatibility = useDeckConfigurationCompatibility( + robotType, + commands + ) + + const nonSingleSlotDeckConfigCompatibility = deckConfigCompatibility.filter( + ({ requiredAddressableAreas }) => + // required AA list includes a non-single-slot AA + !requiredAddressableAreas.every(aa => + FLEX_SINGLE_SLOT_ADDRESSABLE_AREAS.includes(aa) + ) + ) + // fixture includes at least 1 required AA + const requiredDeckConfigCompatibility = nonSingleSlotDeckConfigCompatibility.filter( + fixture => fixture.requiredAddressableAreas.length > 0 + ) return ( <> @@ -58,9 +78,9 @@ export const SetupModuleAndDeck = ({ {hasModules ? ( ) : null} - {Object.keys(loadedFixturesBySlot).length > 0 && isFlex ? ( - - ) : null} + ) : ( diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/utils.ts b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/utils.ts index 171a26199f4..ee5b72efee5 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/utils.ts +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/utils.ts @@ -1,3 +1,10 @@ +import { + SINGLE_SLOT_FIXTURES, + STAGING_AREA_RIGHT_SLOT_FIXTURE, + TRASH_BIN_ADAPTER_FIXTURE, + WASTE_CHUTE_FIXTURES, +} from '@opentrons/shared-data' + import magneticModule from '../../../../assets/images/magnetic_module_gen_2_transparent.png' import temperatureModule from '../../../../assets/images/temp_deck_gen_2_transparent.png' import thermoModuleGen1 from '../../../../assets/images/thermocycler_closed.png' @@ -9,7 +16,13 @@ import stagingArea from '../../../../assets/images/staging_area_slot.png' import wasteChute from '../../../../assets/images/waste_chute.png' // TODO(jr, 10/17/23): figure out if we need this asset, I'm stubbing it in for now // import wasteChuteStagingArea from '../../../../assets/images/waste_chute_with_staging_area.png' -import type { FixtureLoadName, ModuleModel } from '@opentrons/shared-data' + +import type { + CutoutFixtureId, + ModuleModel, + SingleSlotCutoutFixtureId, + WasteChuteCutoutFixtureId, +} from '@opentrons/shared-data' export function getModuleImage(model: ModuleModel): string { switch (model) { @@ -33,21 +46,21 @@ export function getModuleImage(model: ModuleModel): string { } // TODO(jr, 10/4/23): add correct assets for trashBin, standardSlot, wasteChuteAndStagingArea -export function getFixtureImage(fixture: FixtureLoadName): string { - switch (fixture) { - case 'stagingArea': { - return stagingArea - } - case 'wasteChute': { - return wasteChute - } - case 'standardSlot': { - return stagingArea - } - case 'trashBin': { - return trashBin - } - default: - return 'Error: unknown fixture' +export function getFixtureImage(fixture: CutoutFixtureId): string { + if (fixture === STAGING_AREA_RIGHT_SLOT_FIXTURE) { + return stagingArea + } else if ( + WASTE_CHUTE_FIXTURES.includes(fixture as WasteChuteCutoutFixtureId) + ) { + return wasteChute + } else if ( + // TODO(bh, 2023-11-13): this asset probably won't exist + SINGLE_SLOT_FIXTURES.includes(fixture as SingleSlotCutoutFixtureId) + ) { + return stagingArea + } else if (fixture === TRASH_BIN_ADAPTER_FIXTURE) { + return trashBin + } else { + return 'Error: unknown fixture' } } diff --git a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx index 63aafc70d1d..ac542cccca2 100644 --- a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx @@ -12,6 +12,7 @@ import withModulesProtocol from '@opentrons/shared-data/protocol/fixtures/4/test import { i18n } from '../../../../i18n' import { mockConnectedRobot } from '../../../../redux/discovery/__fixtures__' +import { getSimplestDeckConfigForProtocolCommands } from '../../../../resources/deck_configuration/utils' import { useMostRecentCompletedAnalysis } from '../../../LabwarePositionCheck/useMostRecentCompletedAnalysis' import { useIsFlex, @@ -39,6 +40,7 @@ jest.mock('../EmptySetupStep') jest.mock('../../../LabwarePositionCheck/useMostRecentCompletedAnalysis') jest.mock('@opentrons/shared-data/js/helpers/parseProtocolData') jest.mock('../../../../redux/config') +jest.mock('../../../../resources/deck_configuration/utils') const mockUseIsFlex = useIsFlex as jest.MockedFunction const mockUseMostRecentCompletedAnalysis = useMostRecentCompletedAnalysis as jest.MockedFunction< @@ -78,6 +80,10 @@ const mockSetupLiquids = SetupLiquids as jest.MockedFunction< const mockEmptySetupStep = EmptySetupStep as jest.MockedFunction< typeof EmptySetupStep > +const mockGetSimplestDeckConfigForProtocolCommands = getSimplestDeckConfigForProtocolCommands as jest.MockedFunction< + typeof getSimplestDeckConfigForProtocolCommands +> + const ROBOT_NAME = 'otie' const RUN_ID = '1' const MOCK_ROTOCOL_LIQUID_KEY = { liquids: [] } @@ -140,6 +146,7 @@ describe('ProtocolRunSetup', () => { when(mockSetupModuleAndDeck).mockReturnValue(
Mock SetupModules
) when(mockSetupLiquids).mockReturnValue(
Mock SetupLiquids
) when(mockEmptySetupStep).mockReturnValue(
Mock EmptySetupStep
) + when(mockGetSimplestDeckConfigForProtocolCommands).mockReturnValue([]) }) afterEach(() => { resetAllWhenMocks() diff --git a/app/src/organisms/Devices/hooks/__tests__/useModuleRenderInfoForProtocolById.test.tsx b/app/src/organisms/Devices/hooks/__tests__/useModuleRenderInfoForProtocolById.test.tsx index 17d70702dad..b1e92228117 100644 --- a/app/src/organisms/Devices/hooks/__tests__/useModuleRenderInfoForProtocolById.test.tsx +++ b/app/src/organisms/Devices/hooks/__tests__/useModuleRenderInfoForProtocolById.test.tsx @@ -2,13 +2,7 @@ import { renderHook } from '@testing-library/react-hooks' import { when, resetAllWhenMocks } from 'jest-when' import { UseQueryResult } from 'react-query' -import { - getDeckDefFromRobotType, - getRobotTypeFromLoadedLabware, - FLEX_ROBOT_TYPE, - STAGING_AREA_LOAD_NAME, -} from '@opentrons/shared-data' -import standardDeckDef from '@opentrons/shared-data/deck/definitions/3/ot3_standard.json' +import { STAGING_AREA_RIGHT_SLOT_FIXTURE } from '@opentrons/shared-data' import _heaterShakerCommandsWithResultsKey from '@opentrons/shared-data/protocol/fixtures/6/heaterShakerCommandsWithResultsKey.json' import { useMostRecentCompletedAnalysis } from '../../../LabwarePositionCheck/useMostRecentCompletedAnalysis' import { useDeckConfigurationQuery } from '@opentrons/react-api-client/src/deck_configuration' @@ -27,16 +21,14 @@ import { } from '..' import type { + CutoutConfig, DeckConfiguration, - DeckDefinition, - Fixture, ModuleModel, ModuleType, ProtocolAnalysisOutput, } from '@opentrons/shared-data' jest.mock('@opentrons/react-api-client/src/deck_configuration') -jest.mock('@opentrons/shared-data/js/helpers') jest.mock('../../ProtocolRun/utils/getProtocolModulesInfo') jest.mock('../useAttachedModules') jest.mock('../useProtocolDetailsForRun') @@ -49,12 +41,6 @@ const mockGetProtocolModulesInfo = getProtocolModulesInfo as jest.MockedFunction const mockUseAttachedModules = useAttachedModules as jest.MockedFunction< typeof useAttachedModules > -const mockGetDeckDefFromRobotType = getDeckDefFromRobotType as jest.MockedFunction< - typeof getDeckDefFromRobotType -> -const mockGetRobotTypeFromLoadedLabware = getRobotTypeFromLoadedLabware as jest.MockedFunction< - typeof getRobotTypeFromLoadedLabware -> const mockUseStoredProtocolAnalysis = useStoredProtocolAnalysis as jest.MockedFunction< typeof useStoredProtocolAnalysis > @@ -68,7 +54,15 @@ const heaterShakerCommandsWithResultsKey = (_heaterShakerCommandsWithResultsKey const PROTOCOL_DETAILS = { displayName: 'fake protocol', - protocolData: heaterShakerCommandsWithResultsKey, + protocolData: { + ...heaterShakerCommandsWithResultsKey, + labware: [ + { + displayName: 'Trash', + definitionId: 'opentrons/opentrons_1_trash_3200ml_fixed/1', + }, + ], + }, protocolKey: 'fakeProtocolKey', } @@ -132,11 +126,10 @@ const TEMPERATURE_MODULE_INFO = { slotName: 'D1', } -const mockFixture = { - fixtureId: 'mockId', - fixtureLocation: 'cutoutD1', - loadName: STAGING_AREA_LOAD_NAME, -} as Fixture +const mockFixture: CutoutConfig = { + cutoutId: 'cutoutD1', + cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, +} describe('useModuleRenderInfoForProtocolById hook', () => { beforeEach(() => { @@ -150,22 +143,16 @@ describe('useModuleRenderInfoForProtocolById hook', () => { mockTemperatureModuleGen2, mockThermocycler, ]) - when(mockGetDeckDefFromRobotType) - .calledWith(FLEX_ROBOT_TYPE) - .mockReturnValue((standardDeckDef as unknown) as DeckDefinition) - when(mockGetRobotTypeFromLoadedLabware).mockReturnValue(FLEX_ROBOT_TYPE) when(mockUseStoredProtocolAnalysis) .calledWith('1') .mockReturnValue((PROTOCOL_DETAILS as unknown) as ProtocolAnalysisOutput) when(mockUseMostRecentCompletedAnalysis) .calledWith('1') .mockReturnValue(PROTOCOL_DETAILS.protocolData as any) - when(mockGetProtocolModulesInfo) - .calledWith( - heaterShakerCommandsWithResultsKey, - (standardDeckDef as unknown) as DeckDefinition - ) - .mockReturnValue([TEMPERATURE_MODULE_INFO, MAGNETIC_MODULE_INFO]) + mockGetProtocolModulesInfo.mockReturnValue([ + TEMPERATURE_MODULE_INFO, + MAGNETIC_MODULE_INFO, + ]) }) afterEach(() => { diff --git a/app/src/organisms/Devices/hooks/useModuleRenderInfoForProtocolById.ts b/app/src/organisms/Devices/hooks/useModuleRenderInfoForProtocolById.ts index 3c5d9404f74..317b6ed9209 100644 --- a/app/src/organisms/Devices/hooks/useModuleRenderInfoForProtocolById.ts +++ b/app/src/organisms/Devices/hooks/useModuleRenderInfoForProtocolById.ts @@ -1,23 +1,24 @@ import { checkModuleCompatibility, - Fixture, getDeckDefFromRobotType, getRobotTypeFromLoadedLabware, - STANDARD_SLOT_LOAD_NAME, + SINGLE_SLOT_FIXTURES, } from '@opentrons/shared-data' import { useDeckConfigurationQuery } from '@opentrons/react-api-client/src/deck_configuration' +import { getCutoutIdForSlotName } from '../../../resources/deck_configuration/utils' import { getProtocolModulesInfo } from '../ProtocolRun/utils/getProtocolModulesInfo' import { useMostRecentCompletedAnalysis } from '../../LabwarePositionCheck/useMostRecentCompletedAnalysis' import { useAttachedModules } from './useAttachedModules' import { useStoredProtocolAnalysis } from './useStoredProtocolAnalysis' +import type { CutoutConfig } from '@opentrons/shared-data' import type { AttachedModule } from '../../../redux/modules/types' import type { ProtocolModuleInfo } from '../ProtocolRun/utils/getProtocolModulesInfo' export interface ModuleRenderInfoForProtocol extends ProtocolModuleInfo { attachedModuleMatch: AttachedModule | null - conflictedFixture?: Fixture + conflictedFixture?: CutoutConfig } export interface ModuleRenderInfoById { @@ -54,26 +55,31 @@ export function useModuleRenderInfoForProtocolById( protocolMod.moduleDef.model ) && !matchedAmod.find(m => m === attachedMod) ) ?? null + + const cutoutIdForSlotName = getCutoutIdForSlotName( + protocolMod.slotName, + deckDef + ) + + const conflictedFixture = deckConfig?.find( + fixture => + fixture.cutoutId === cutoutIdForSlotName && + fixture.cutoutFixtureId != null && + !SINGLE_SLOT_FIXTURES.includes(fixture.cutoutFixtureId) + ) + if (compatibleAttachedModule !== null) { matchedAmod = [...matchedAmod, compatibleAttachedModule] return { ...protocolMod, attachedModuleMatch: compatibleAttachedModule, - conflictedFixture: deckConfig?.find( - fixture => - fixture.fixtureLocation === protocolMod.slotName && - fixture.loadName !== STANDARD_SLOT_LOAD_NAME - ), + conflictedFixture, } } return { ...protocolMod, attachedModuleMatch: null, - conflictedFixture: deckConfig?.find( - fixture => - fixture.fixtureLocation === protocolMod.slotName && - fixture.loadName !== STANDARD_SLOT_LOAD_NAME - ), + conflictedFixture, } } ) diff --git a/app/src/organisms/ProtocolDetails/RobotConfigurationDetails.tsx b/app/src/organisms/ProtocolDetails/RobotConfigurationDetails.tsx index 6b87d735279..e4da0e001c7 100644 --- a/app/src/organisms/ProtocolDetails/RobotConfigurationDetails.tsx +++ b/app/src/organisms/ProtocolDetails/RobotConfigurationDetails.tsx @@ -13,10 +13,12 @@ import { TYPOGRAPHY, } from '@opentrons/components' import { + getCutoutDisplayName, getFixtureDisplayName, getModuleDisplayName, getModuleType, getPipetteNameSpecs, + SINGLE_SLOT_FIXTURES, THERMOCYCLER_MODULE_TYPE, } from '@opentrons/shared-data' @@ -28,17 +30,18 @@ import { getSlotsForThermocycler } from './utils' import type { LoadModuleRunTimeCommand, - LoadFixtureRunTimeCommand, PipetteName, RobotType, + SingleSlotCutoutFixtureId, } from '@opentrons/shared-data' +import type { CutoutConfigProtocolSpec } from '../../resources/deck_configuration/utils' interface RobotConfigurationDetailsProps { leftMountPipetteName: PipetteName | null rightMountPipetteName: PipetteName | null extensionInstrumentName: string | null requiredModuleDetails: LoadModuleRunTimeCommand[] - requiredFixtureDetails: LoadFixtureRunTimeCommand[] + requiredFixtureDetails: CutoutConfigProtocolSpec[] isLoading: boolean robotType: RobotType | null } @@ -110,6 +113,14 @@ export const RobotConfigurationDetails = ( emptyText ) + // filter out single slot fixtures + const nonStandardRequiredFixtureDetails = requiredFixtureDetails.filter( + fixture => + !SINGLE_SLOT_FIXTURES.includes( + fixture.cutoutFixtureId as SingleSlotCutoutFixtureId + ) + ) + return ( ) })} - {requiredFixtureDetails.map((fixture, index) => { + {nonStandardRequiredFixtureDetails.map((fixture, index) => { return ( - {getFixtureDisplayName(fixture.params.loadName)} + {getFixtureDisplayName(fixture.cutoutFixtureId)} } /> diff --git a/app/src/organisms/ProtocolDetails/index.tsx b/app/src/organisms/ProtocolDetails/index.tsx index 9b017166cb9..66917dcc1bd 100644 --- a/app/src/organisms/ProtocolDetails/index.tsx +++ b/app/src/organisms/ProtocolDetails/index.tsx @@ -37,12 +37,8 @@ import { parseInitialLoadedLabwareBySlot, parseInitialLoadedLabwareByModuleId, parseInitialLoadedLabwareByAdapter, - parseInitialLoadedFixturesByCutout, } from '@opentrons/api-client' -import { - WASTE_CHUTE_LOAD_NAME, - getGripperDisplayName, -} from '@opentrons/shared-data' +import { getGripperDisplayName } from '@opentrons/shared-data' import { Portal } from '../../App/portal' import { Divider } from '../../atoms/structure' @@ -58,6 +54,7 @@ import { analyzeProtocol, } from '../../redux/protocol-storage' import { useFeatureFlag } from '../../redux/config' +import { getSimplestDeckConfigForProtocolCommands } from '../../resources/deck_configuration/utils' import { ChooseRobotToRunProtocolSlideout } from '../ChooseRobotToRunProtocolSlideout' import { SendProtocolToOT3Slideout } from '../SendProtocolToOT3Slideout' import { ProtocolAnalysisFailure } from '../ProtocolAnalysisFailure' @@ -72,11 +69,7 @@ import { ProtocolLabwareDetails } from './ProtocolLabwareDetails' import { ProtocolLiquidsDetails } from './ProtocolLiquidsDetails' import { RobotConfigurationDetails } from './RobotConfigurationDetails' -import type { - JsonConfig, - LoadFixtureRunTimeCommand, - PythonConfig, -} from '@opentrons/shared-data' +import type { JsonConfig, PythonConfig } from '@opentrons/shared-data' import type { StoredProtocolData } from '../../redux/protocol-storage' import type { State, Dispatch } from '../../redux/types' @@ -237,29 +230,9 @@ export function ProtocolDetails( ? map(parseInitialLoadedModulesBySlot(mostRecentAnalysis.commands)) : [] - // TODO: IMMEDIATELY remove stubbed fixture as soon as PE supports loadFixture - const STUBBED_LOAD_FIXTURE: LoadFixtureRunTimeCommand = { - id: 'stubbed_load_fixture', - commandType: 'loadFixture', - params: { - fixtureId: 'stubbedFixtureId', - loadName: WASTE_CHUTE_LOAD_NAME, - location: { cutout: 'cutoutD3' }, - }, - createdAt: 'fakeTimestamp', - startedAt: 'fakeTimestamp', - completedAt: 'fakeTimestamp', - status: 'succeeded', - } - const requiredFixtureDetails = - mostRecentAnalysis?.commands != null - ? [ - ...map( - parseInitialLoadedFixturesByCutout(mostRecentAnalysis.commands) - ), - STUBBED_LOAD_FIXTURE, - ] - : [] + const requiredFixtureDetails = getSimplestDeckConfigForProtocolCommands( + mostRecentAnalysis?.commands ?? [] + ) const requiredLabwareDetails = mostRecentAnalysis != null diff --git a/app/src/organisms/ProtocolSetupDeckConfiguration/__tests__/ProtocolSetupDeckConfiguration.test.tsx b/app/src/organisms/ProtocolSetupDeckConfiguration/__tests__/ProtocolSetupDeckConfiguration.test.tsx index d453c00ded7..b8382ac1ef2 100644 --- a/app/src/organisms/ProtocolSetupDeckConfiguration/__tests__/ProtocolSetupDeckConfiguration.test.tsx +++ b/app/src/organisms/ProtocolSetupDeckConfiguration/__tests__/ProtocolSetupDeckConfiguration.test.tsx @@ -1,23 +1,19 @@ import * as React from 'react' import { when, resetAllWhenMocks } from 'jest-when' -import { renderWithProviders, DeckConfigurator } from '@opentrons/components' -import { - useUpdateDeckConfigurationMutation, - useCreateDeckConfigurationMutation, -} from '@opentrons/react-api-client' +import { renderWithProviders, BaseDeck } from '@opentrons/components' +import { useUpdateDeckConfigurationMutation } from '@opentrons/react-api-client' import { i18n } from '../../../i18n' import { useMostRecentCompletedAnalysis } from '../../LabwarePositionCheck/useMostRecentCompletedAnalysis' import { ProtocolSetupDeckConfiguration } from '..' -jest.mock('@opentrons/components/src/hardware-sim/DeckConfigurator/index') +jest.mock('@opentrons/components/src/hardware-sim/BaseDeck/index') jest.mock('@opentrons/react-api-client') jest.mock('../../LabwarePositionCheck/useMostRecentCompletedAnalysis') const mockSetSetupScreen = jest.fn() const mockUpdateDeckConfiguration = jest.fn() -const mockCreateDeckConfiguration = jest.fn() const PROTOCOL_DETAILS = { displayName: 'fake protocol', protocolData: [], @@ -25,18 +21,13 @@ const PROTOCOL_DETAILS = { robotType: 'OT-3 Standard' as const, } -const mockDeckConfigurator = DeckConfigurator as jest.MockedFunction< - typeof DeckConfigurator -> const mockUseMostRecentCompletedAnalysis = useMostRecentCompletedAnalysis as jest.MockedFunction< typeof useMostRecentCompletedAnalysis > const mockUseUpdateDeckConfigurationMutation = useUpdateDeckConfigurationMutation as jest.MockedFunction< typeof useUpdateDeckConfigurationMutation > -const mockUseCreateDeckConfigurationMutation = useCreateDeckConfigurationMutation as jest.MockedFunction< - typeof useCreateDeckConfigurationMutation -> +const mockBaseDeck = BaseDeck as jest.MockedFunction const render = ( props: React.ComponentProps @@ -51,21 +42,18 @@ describe('ProtocolSetupDeckConfiguration', () => { beforeEach(() => { props = { - fixtureLocation: 'cutoutD3', + cutoutId: 'cutoutD3', runId: 'mockRunId', setSetupScreen: mockSetSetupScreen, providedFixtureOptions: [], } - mockDeckConfigurator.mockReturnValue(
mock DeckConfigurator
) + mockBaseDeck.mockReturnValue(
mock BaseDeck
) when(mockUseMostRecentCompletedAnalysis) .calledWith('mockRunId') .mockReturnValue(PROTOCOL_DETAILS.protocolData as any) mockUseUpdateDeckConfigurationMutation.mockReturnValue({ updateDeckConfiguration: mockUpdateDeckConfiguration, } as any) - mockUseCreateDeckConfigurationMutation.mockReturnValue({ - createDeckConfiguration: mockCreateDeckConfiguration, - } as any) }) afterEach(() => { @@ -75,7 +63,7 @@ describe('ProtocolSetupDeckConfiguration', () => { it('should render text, button, and DeckConfigurator', () => { const [{ getByText }] = render(props) getByText('Deck configuration') - getByText('mock DeckConfigurator') + getByText('mock BaseDeck') getByText('Confirm') }) @@ -88,6 +76,6 @@ describe('ProtocolSetupDeckConfiguration', () => { it('should call a mock function when tapping confirm button', () => { const [{ getByText }] = render(props) getByText('Confirm').click() - expect(mockCreateDeckConfiguration).toHaveBeenCalled() + expect(mockUpdateDeckConfiguration).toHaveBeenCalled() }) }) diff --git a/app/src/organisms/ProtocolSetupDeckConfiguration/index.tsx b/app/src/organisms/ProtocolSetupDeckConfiguration/index.tsx index bb5638ac06a..435498c53d9 100644 --- a/app/src/organisms/ProtocolSetupDeckConfiguration/index.tsx +++ b/app/src/organisms/ProtocolSetupDeckConfiguration/index.tsx @@ -2,39 +2,38 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import { - DeckConfigurator, + BaseDeck, DIRECTION_COLUMN, Flex, JUSTIFY_CENTER, SPACING, } from '@opentrons/components' -import { useCreateDeckConfigurationMutation } from '@opentrons/react-api-client' -import { WASTE_CHUTE_LOAD_NAME } from '@opentrons/shared-data' +import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' +import { useUpdateDeckConfigurationMutation } from '@opentrons/react-api-client' import { ChildNavigation } from '../ChildNavigation' import { AddFixtureModal } from '../DeviceDetailsDeckConfiguration/AddFixtureModal' import { DeckConfigurationDiscardChangesModal } from '../DeviceDetailsDeckConfiguration/DeckConfigurationDiscardChangesModal' import { useMostRecentCompletedAnalysis } from '../LabwarePositionCheck/useMostRecentCompletedAnalysis' +import { getSimplestDeckConfigForProtocolCommands } from '../../resources/deck_configuration/utils' import { Portal } from '../../App/portal' import type { - Cutout, + CutoutFixtureId, + CutoutId, DeckConfiguration, - Fixture, - FixtureLoadName, - LoadFixtureRunTimeCommand, } from '@opentrons/shared-data' import type { SetupScreens } from '../../pages/OnDeviceDisplay/ProtocolSetup' interface ProtocolSetupDeckConfigurationProps { - fixtureLocation: Cutout + cutoutId: CutoutId | null runId: string setSetupScreen: React.Dispatch> - providedFixtureOptions: FixtureLoadName[] + providedFixtureOptions: CutoutFixtureId[] } export function ProtocolSetupDeckConfiguration({ - fixtureLocation, + cutoutId, runId, setSetupScreen, providedFixtureOptions, @@ -51,46 +50,19 @@ export function ProtocolSetupDeckConfiguration({ ] = React.useState(false) const mostRecentAnalysis = useMostRecentCompletedAnalysis(runId) - const STUBBED_LOAD_FIXTURE: LoadFixtureRunTimeCommand = { - id: 'stubbed_load_fixture', - commandType: 'loadFixture', - params: { - fixtureId: 'stubbedFixtureId', - loadName: WASTE_CHUTE_LOAD_NAME, - location: { cutout: 'cutoutD3' }, - }, - createdAt: 'fakeTimestamp', - startedAt: 'fakeTimestamp', - completedAt: 'fakeTimestamp', - status: 'succeeded', - } - - const requiredFixtureDetails = - mostRecentAnalysis?.commands != null - ? [ - // parseInitialLoadedFixturesByCutout(mostRecentAnalysis.commands), - STUBBED_LOAD_FIXTURE, - ] - : [] - const deckConfig = - (requiredFixtureDetails.map( - (fixture): Fixture | false => - fixture.params.fixtureId != null && { - fixtureId: fixture.params.fixtureId, - fixtureLocation: fixture.params.location.cutout, - loadName: fixture.params.loadName, - } - ) as DeckConfiguration) ?? [] + const simplestDeckConfig = getSimplestDeckConfigForProtocolCommands( + mostRecentAnalysis?.commands ?? [] + ).map(({ cutoutId, cutoutFixtureId }) => ({ cutoutId, cutoutFixtureId })) const [ currentDeckConfig, setCurrentDeckConfig, - ] = React.useState(deckConfig) + ] = React.useState(simplestDeckConfig) - const { createDeckConfiguration } = useCreateDeckConfigurationMutation() + const { updateDeckConfiguration } = useUpdateDeckConfigurationMutation() const handleClickConfirm = (): void => { - createDeckConfiguration(currentDeckConfig) + updateDeckConfiguration(currentDeckConfig) setSetupScreen('modules') } @@ -102,9 +74,9 @@ export function ProtocolSetupDeckConfiguration({ setShowConfirmationModal={setShowDiscardChangeModal} /> ) : null} - {showConfigurationModal && fixtureLocation != null ? ( + {showConfigurationModal && cutoutId != null ? ( - {/* DeckConfigurator will be replaced by BaseDeck when RAUT-793 is ready */} - {}} - handleClickRemove={() => {}} +
diff --git a/app/src/organisms/ProtocolSetupLabware/LabwareMapViewModal.tsx b/app/src/organisms/ProtocolSetupLabware/LabwareMapViewModal.tsx index b56629a6876..13bfb49a3b2 100644 --- a/app/src/organisms/ProtocolSetupLabware/LabwareMapViewModal.tsx +++ b/app/src/organisms/ProtocolSetupLabware/LabwareMapViewModal.tsx @@ -5,7 +5,7 @@ import { BaseDeck } from '@opentrons/components' import { FLEX_ROBOT_TYPE, THERMOCYCLER_MODULE_V1 } from '@opentrons/shared-data' import { Modal } from '../../molecules/Modal' -import { getDeckConfigFromProtocolCommands } from '../../resources/deck_configuration/utils' +import { getSimplestDeckConfigForProtocolCommands } from '../../resources/deck_configuration/utils' import { getStandardDeckViewLayerBlockList } from '../Devices/ProtocolRun/utils/getStandardDeckViewLayerBlockList' import { getLabwareRenderInfo } from '../Devices/ProtocolRun/utils/getLabwareRenderInfo' import { AttachedProtocolModuleMatch } from '../ProtocolSetupModulesAndDeck/utils' @@ -42,7 +42,7 @@ export function LabwareMapViewModal( mostRecentAnalysis, } = props const { t } = useTranslation('protocol_setup') - const deckConfig = getDeckConfigFromProtocolCommands( + const deckConfig = getSimplestDeckConfigForProtocolCommands( mostRecentAnalysis?.commands ?? [] ) const labwareRenderInfo = diff --git a/app/src/organisms/ProtocolSetupLabware/__tests__/LabwareMapViewModal.test.tsx b/app/src/organisms/ProtocolSetupLabware/__tests__/LabwareMapViewModal.test.tsx index 5c9bf19d6ed..5b7d63a5277 100644 --- a/app/src/organisms/ProtocolSetupLabware/__tests__/LabwareMapViewModal.test.tsx +++ b/app/src/organisms/ProtocolSetupLabware/__tests__/LabwareMapViewModal.test.tsx @@ -10,7 +10,7 @@ import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' import deckDefFixture from '@opentrons/shared-data/deck/fixtures/3/deckExample.json' import fixture_tiprack_300_ul from '@opentrons/shared-data/labware/fixtures/2/fixture_tiprack_300_ul.json' import { i18n } from '../../../i18n' -import { getDeckConfigFromProtocolCommands } from '../../../resources/deck_configuration/utils' +import { getSimplestDeckConfigForProtocolCommands } from '../../../resources/deck_configuration/utils' import { getLabwareRenderInfo } from '../../Devices/ProtocolRun/utils/getLabwareRenderInfo' import { getStandardDeckViewLayerBlockList } from '../../Devices/ProtocolRun/utils/getStandardDeckViewLayerBlockList' import { mockProtocolModuleInfo } from '../__fixtures__' @@ -32,8 +32,8 @@ jest.mock('../../../redux/config') const mockGetLabwareRenderInfo = getLabwareRenderInfo as jest.MockedFunction< typeof getLabwareRenderInfo > -const mockGetDeckConfigFromProtocolCommands = getDeckConfigFromProtocolCommands as jest.MockedFunction< - typeof getDeckConfigFromProtocolCommands +const mockGetSimplestDeckConfigForProtocolCommands = getSimplestDeckConfigForProtocolCommands as jest.MockedFunction< + typeof getSimplestDeckConfigForProtocolCommands > const mockBaseDeck = BaseDeck as jest.MockedFunction @@ -53,7 +53,7 @@ const render = (props: React.ComponentProps) => { describe('LabwareMapViewModal', () => { beforeEach(() => { mockGetLabwareRenderInfo.mockReturnValue({}) - mockGetDeckConfigFromProtocolCommands.mockReturnValue([]) + mockGetSimplestDeckConfigForProtocolCommands.mockReturnValue([]) }) afterEach(() => { diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/FixtureTable.tsx b/app/src/organisms/ProtocolSetupModulesAndDeck/FixtureTable.tsx index ef77aae7f2a..26287055b76 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/FixtureTable.tsx +++ b/app/src/organisms/ProtocolSetupModulesAndDeck/FixtureTable.tsx @@ -14,73 +14,68 @@ import { TYPOGRAPHY, } from '@opentrons/components' import { + FLEX_SINGLE_SLOT_ADDRESSABLE_AREAS, getCutoutDisplayName, getFixtureDisplayName, - WASTE_CHUTE_LOAD_NAME, + SINGLE_SLOT_FIXTURES, } from '@opentrons/shared-data' -// import { parseInitialLoadedFixturesByCutout } from '@opentrons/api-client' -import { - CONFIGURED, - CONFLICTING, - NOT_CONFIGURED, - useLoadedFixturesConfigStatus, -} from '../../resources/deck_configuration/hooks' +import { useDeckConfigurationCompatibility } from '../../resources/deck_configuration/hooks' import { LocationConflictModal } from '../Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal' import { StyledText } from '../../atoms/text' import { Chip } from '../../atoms/Chip' +import { getSimplestDeckConfigForProtocolCommands } from '../../resources/deck_configuration/utils' import type { CompletedProtocolAnalysis, - Cutout, - FixtureLoadName, - LoadFixtureRunTimeCommand, + CutoutFixtureId, + CutoutId, + RobotType, } from '@opentrons/shared-data' import type { SetupScreens } from '../../pages/OnDeviceDisplay/ProtocolSetup' interface FixtureTableProps { + robotType: RobotType mostRecentAnalysis: CompletedProtocolAnalysis | null setSetupScreen: React.Dispatch> - setFixtureLocation: (fixtureLocation: Cutout) => void - setProvidedFixtureOptions: (providedFixtureOptions: FixtureLoadName[]) => void + setCutoutId: (cutoutId: CutoutId) => void + setProvidedFixtureOptions: (providedFixtureOptions: CutoutFixtureId[]) => void } export function FixtureTable({ + robotType, mostRecentAnalysis, setSetupScreen, - setFixtureLocation, + setCutoutId, setProvidedFixtureOptions, -}: FixtureTableProps): JSX.Element { +}: FixtureTableProps): JSX.Element | null { const { t, i18n } = useTranslation('protocol_setup') - const STUBBED_LOAD_FIXTURE: LoadFixtureRunTimeCommand = { - id: 'stubbed_load_fixture', - commandType: 'loadFixture', - params: { - fixtureId: 'stubbedFixtureId', - loadName: WASTE_CHUTE_LOAD_NAME, - location: { cutout: 'cutoutD3' }, - }, - createdAt: 'fakeTimestamp', - startedAt: 'fakeTimestamp', - completedAt: 'fakeTimestamp', - status: 'succeeded', - } const [ showLocationConflictModal, setShowLocationConflictModal, ] = React.useState(false) - const requiredFixtureDetails = - mostRecentAnalysis?.commands != null - ? [ - // parseInitialLoadedFixturesByCutout(mostRecentAnalysis.commands), - STUBBED_LOAD_FIXTURE, - ] - : [] + const requiredFixtureDetails = getSimplestDeckConfigForProtocolCommands( + mostRecentAnalysis?.commands ?? [] + ) + const deckConfigCompatibility = useDeckConfigurationCompatibility( + robotType, + mostRecentAnalysis?.commands ?? [] + ) - const configurations = useLoadedFixturesConfigStatus(requiredFixtureDetails) + const nonSingleSlotDeckConfigCompatibility = deckConfigCompatibility.filter( + ({ requiredAddressableAreas }) => + // required AA list includes a non-single-slot AA + !requiredAddressableAreas.every(aa => + FLEX_SINGLE_SLOT_ADDRESSABLE_AREAS.includes(aa) + ) + ) + // fixture includes at least 1 required AA + const requiredDeckConfigCompatibility = nonSingleSlotDeckConfigCompatibility.filter( + fixture => fixture.requiredAddressableAreas.length > 0 + ) - return ( + return requiredDeckConfigCompatibility.length > 0 ? ( {t('location')} {t('status')} - {requiredFixtureDetails.map((fixture, index) => { - const configurationStatus = configurations.find( - configuration => configuration.id === fixture.id - )?.configurationStatus - - const statusNotReady = - configurationStatus === CONFLICTING || - configurationStatus === NOT_CONFIGURED + {requiredDeckConfigCompatibility.map( + ({ cutoutId, cutoutFixtureId, compatibleCutoutFixtureIds }, index) => { + const isCurrentFixtureCompatible = + cutoutFixtureId != null && + compatibleCutoutFixtureIds.includes(cutoutFixtureId) - let chipLabel: JSX.Element - let handleClick - if (statusNotReady) { - chipLabel = ( - <> - - - - ) - handleClick = - configurationStatus === CONFLICTING + let chipLabel: JSX.Element + let handleClick + if (!isCurrentFixtureCompatible) { + const isConflictingFixtureConfigured = + cutoutFixtureId != null && + !SINGLE_SLOT_FIXTURES.includes(cutoutFixtureId) + chipLabel = ( + <> + + + + ) + handleClick = isConflictingFixtureConfigured ? () => setShowLocationConflictModal(true) : () => { - setFixtureLocation(fixture.params.location.cutout) - setProvidedFixtureOptions([fixture.params.loadName]) + setCutoutId(cutoutId) + setProvidedFixtureOptions(compatibleCutoutFixtureIds) setSetupScreen('deck configuration') } - } else if (configurationStatus === CONFIGURED) { - chipLabel = ( - - ) - // TODO(jr, 10/17/23): wire this up - // handleClick = () => setShowNotConfiguredModal(true) - - // shouldn't run into this case - } else { - chipLabel =
status label unknown
- } - - return ( - - {showLocationConflictModal ? ( - setShowLocationConflictModal(false)} - cutout={fixture.params.location.cutout} - requiredFixture={fixture.params.loadName} - isOnDevice={true} + } else { + chipLabel = ( + - ) : null} - - - - {getFixtureDisplayName(fixture.params.loadName)} - - - - + {showLocationConflictModal ? ( + setShowLocationConflictModal(false)} + cutoutId={cutoutId} + requiredFixtureId={compatibleCutoutFixtureIds[0]} + isOnDevice={true} /> - + ) : null} - {chipLabel} + + + {cutoutFixtureId != null + ? getFixtureDisplayName(cutoutFixtureId) + : null} + + + + + + + {chipLabel} + - - - ) - })} +
+ ) + } + )}
- ) + ) : null } diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/ModulesAndDeckMapViewModal.tsx b/app/src/organisms/ProtocolSetupModulesAndDeck/ModulesAndDeckMapViewModal.tsx index 5045795d771..d432f9d8bba 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/ModulesAndDeckMapViewModal.tsx +++ b/app/src/organisms/ProtocolSetupModulesAndDeck/ModulesAndDeckMapViewModal.tsx @@ -6,7 +6,7 @@ import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' import { Modal } from '../../molecules/Modal' import { ModuleInfo } from '../Devices/ModuleInfo' -import { getDeckConfigFromProtocolCommands } from '../../resources/deck_configuration/utils' +import { getSimplestDeckConfigForProtocolCommands } from '../../resources/deck_configuration/utils' import { getStandardDeckViewLayerBlockList } from '../Devices/ProtocolRun/utils/getStandardDeckViewLayerBlockList' import type { CompletedProtocolAnalysis } from '@opentrons/shared-data' @@ -36,7 +36,7 @@ export function ModulesAndDeckMapViewModal({ if (protocolAnalysis == null) return null - const deckConfig = getDeckConfigFromProtocolCommands( + const deckConfig = getSimplestDeckConfigForProtocolCommands( protocolAnalysis.commands ) diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/FixtureTable.test.tsx b/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/FixtureTable.test.tsx index 4d7e18cbcea..c24bbbe2537 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/FixtureTable.test.tsx +++ b/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/FixtureTable.test.tsx @@ -1,55 +1,27 @@ import * as React from 'react' import { renderWithProviders } from '@opentrons/components' import { - STAGING_AREA_LOAD_NAME, - WASTE_CHUTE_LOAD_NAME, + FLEX_ROBOT_TYPE, + STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, } from '@opentrons/shared-data' import { i18n } from '../../../i18n' -import { useLoadedFixturesConfigStatus } from '../../../resources/deck_configuration/hooks' +import { useDeckConfigurationCompatibility } from '../../../resources/deck_configuration/hooks' import { LocationConflictModal } from '../../Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal' import { FixtureTable } from '../FixtureTable' -import type { LoadFixtureRunTimeCommand } from '@opentrons/shared-data' jest.mock('../../../resources/deck_configuration/hooks') jest.mock('../../Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal') -const mockUseLoadedFixturesConfigStatus = useLoadedFixturesConfigStatus as jest.MockedFunction< - typeof useLoadedFixturesConfigStatus -> const mockLocationConflictModal = LocationConflictModal as jest.MockedFunction< typeof LocationConflictModal > -const mockLoadedFixture = { - id: 'stubbed_load_fixture', - commandType: 'loadFixture', - params: { - fixtureId: 'stubbedFixtureId', - loadName: WASTE_CHUTE_LOAD_NAME, - location: { cutout: 'cutoutD3' }, - }, - createdAt: 'fakeTimestamp', - startedAt: 'fakeTimestamp', - completedAt: 'fakeTimestamp', - status: 'succeeded', -} as LoadFixtureRunTimeCommand - -const mockLoadedStagingAreaFixture = { - id: 'stubbed_load_fixture_2', - commandType: 'loadFixture', - params: { - fixtureId: 'stubbedFixtureId', - loadName: STAGING_AREA_LOAD_NAME, - location: { cutout: 'cutoutD3' }, - }, - createdAt: 'fakeTimestamp', - startedAt: 'fakeTimestamp', - completedAt: 'fakeTimestamp', - status: 'succeeded', -} as LoadFixtureRunTimeCommand +const mockUseDeckConfigurationCompatibility = useDeckConfigurationCompatibility as jest.MockedFunction< + typeof useDeckConfigurationCompatibility +> const mockSetSetupScreen = jest.fn() -const mockSetFixtureLocation = jest.fn() +const mockSetCutoutId = jest.fn() const mockSetProvidedFixtureOptions = jest.fn() const render = (props: React.ComponentProps) => { @@ -63,16 +35,24 @@ describe('FixtureTable', () => { beforeEach(() => { props = { mostRecentAnalysis: [] as any, + robotType: FLEX_ROBOT_TYPE, setSetupScreen: mockSetSetupScreen, - setFixtureLocation: mockSetFixtureLocation, + setCutoutId: mockSetCutoutId, setProvidedFixtureOptions: mockSetProvidedFixtureOptions, } - mockUseLoadedFixturesConfigStatus.mockReturnValue([ - { ...mockLoadedFixture, configurationStatus: 'configured' }, - ]) mockLocationConflictModal.mockReturnValue(
mock location conflict modal
) + mockUseDeckConfigurationCompatibility.mockReturnValue([ + { + cutoutId: 'cutoutD3', + cutoutFixtureId: STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, + requiredAddressableAreas: ['D4'], + compatibleCutoutFixtureIds: [ + STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, + ], + }, + ]) }) it('should render table header and contents', () => { @@ -84,46 +64,39 @@ describe('FixtureTable', () => { it('should render the current status - configured', () => { props = { ...props, - mostRecentAnalysis: { commands: [mockLoadedFixture] } as any, + // TODO(bh, 2023-11-13): mock load labware etc commands + mostRecentAnalysis: { commands: [] } as any, } const [{ getByText }] = render(props) getByText('Configured') }) - it('should render the current status - not configured', () => { - mockUseLoadedFixturesConfigStatus.mockReturnValue([ - { ...mockLoadedFixture, configurationStatus: 'not configured' }, - ]) - props = { - ...props, - mostRecentAnalysis: { commands: [mockLoadedStagingAreaFixture] } as any, - } - const [{ getByText }] = render(props) - getByText('Not configured') - }) - it('should render the current status - conflicting', () => { - mockUseLoadedFixturesConfigStatus.mockReturnValue([ - { ...mockLoadedFixture, configurationStatus: 'conflicting' }, - ]) - props = { - ...props, - mostRecentAnalysis: { commands: [mockLoadedStagingAreaFixture] } as any, - } - const [{ getByText, getAllByText }] = render(props) - getByText('Location conflict').click() - getAllByText('mock location conflict modal') - }) - it('should call a mock function when tapping not configured row', () => { - mockUseLoadedFixturesConfigStatus.mockReturnValue([ - { ...mockLoadedFixture, configurationStatus: 'not configured' }, - ]) - props = { - ...props, - mostRecentAnalysis: { commands: [mockLoadedStagingAreaFixture] } as any, - } - const [{ getByText }] = render(props) - getByText('Not configured').click() - expect(mockSetFixtureLocation).toHaveBeenCalledWith('cutoutD3') - expect(mockSetSetupScreen).toHaveBeenCalledWith('deck configuration') - expect(mockSetProvidedFixtureOptions).toHaveBeenCalledWith(['wasteChute']) - }) + // TODO(bh, 2023-11-14): implement test cases when example JSON protocol fixtures exist + // it('should render the current status - not configured', () => { + // props = { + // ...props, + // mostRecentAnalysis: { commands: [] } as any, + // } + // const [{ getByText }] = render(props) + // getByText('Not configured') + // }) + // it('should render the current status - conflicting', () => { + // props = { + // ...props, + // mostRecentAnalysis: { commands: [] } as any, + // } + // const [{ getByText, getAllByText }] = render(props) + // getByText('Location conflict').click() + // getAllByText('mock location conflict modal') + // }) + // it('should call a mock function when tapping not configured row', () => { + // props = { + // ...props, + // mostRecentAnalysis: { commands: [] } as any, + // } + // const [{ getByText }] = render(props) + // getByText('Not configured').click() + // expect(mockSetCutoutId).toHaveBeenCalledWith('cutoutD3') + // expect(mockSetSetupScreen).toHaveBeenCalledWith('deck configuration') + // expect(mockSetProvidedFixtureOptions).toHaveBeenCalledWith(['wasteChute']) + // }) }) diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ModulesAndDeckMapViewModal.test.tsx b/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ModulesAndDeckMapViewModal.test.tsx index 1882f9947cc..343a692a4f4 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ModulesAndDeckMapViewModal.test.tsx +++ b/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ModulesAndDeckMapViewModal.test.tsx @@ -1,14 +1,10 @@ import * as React from 'react' import { when, resetAllWhenMocks } from 'jest-when' -import { - renderWithProviders, - BaseDeck, - EXTENDED_DECK_CONFIG_FIXTURE, -} from '@opentrons/components' +import { renderWithProviders, BaseDeck } from '@opentrons/components' import { i18n } from '../../../i18n' -import { getDeckConfigFromProtocolCommands } from '../../../resources/deck_configuration/utils' +import { getSimplestDeckConfigForProtocolCommands } from '../../../resources/deck_configuration/utils' import { ModulesAndDeckMapViewModal } from '../ModulesAndDeckMapViewModal' jest.mock('@opentrons/components/src/hardware-sim/BaseDeck') @@ -92,8 +88,8 @@ const render = ( } const mockBaseDeck = BaseDeck as jest.MockedFunction -const mockGetDeckConfigFromProtocolCommands = getDeckConfigFromProtocolCommands as jest.MockedFunction< - typeof getDeckConfigFromProtocolCommands +const mockGetSimplestDeckConfigForProtocolCommands = getSimplestDeckConfigForProtocolCommands as jest.MockedFunction< + typeof getSimplestDeckConfigForProtocolCommands > describe('ModulesAndDeckMapViewModal', () => { @@ -106,8 +102,9 @@ describe('ModulesAndDeckMapViewModal', () => { runId: mockRunId, protocolAnalysis: PROTOCOL_ANALYSIS, } - when(mockGetDeckConfigFromProtocolCommands).mockReturnValue( - EXTENDED_DECK_CONFIG_FIXTURE + when(mockGetSimplestDeckConfigForProtocolCommands).mockReturnValue( + // TODO(bh, 2023-11-13): mock cutout config protocol spec + [] ) mockBaseDeck.mockReturnValue(
mock BaseDeck
) }) diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ProtocolSetupModulesAndDeck.test.tsx b/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ProtocolSetupModulesAndDeck.test.tsx index d5dd9c2d787..03bc2b1a49a 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ProtocolSetupModulesAndDeck.test.tsx +++ b/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ProtocolSetupModulesAndDeck.test.tsx @@ -7,12 +7,10 @@ import { MemoryRouter } from 'react-router-dom' import { renderWithProviders } from '@opentrons/components' import { useDeckConfigurationQuery } from '@opentrons/react-api-client' import { - DeckConfiguration, - Fixture, getDeckDefFromRobotType, - STAGING_AREA_LOAD_NAME, + WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, } from '@opentrons/shared-data' -import ot3StandardDeckDef from '@opentrons/shared-data/deck/definitions/3/ot3_standard.json' +import ot3StandardDeckDef from '@opentrons/shared-data/deck/definitions/4/ot3_standard.json' import { i18n } from '../../../i18n' import { useChainLiveCommands } from '../../../resources/runs/hooks' @@ -38,6 +36,8 @@ import { FixtureTable } from '../FixtureTable' import { ModulesAndDeckMapViewModal } from '../ModulesAndDeckMapViewModal' import { ProtocolSetupModulesAndDeck } from '..' +import type { CutoutConfig, DeckConfiguration } from '@opentrons/shared-data' + jest.mock('@opentrons/react-api-client') jest.mock('../../../resources/runs/hooks') jest.mock('@opentrons/shared-data/js/helpers') @@ -103,7 +103,7 @@ const mockModulesAndDeckMapViewModal = ModulesAndDeckMapViewModal as jest.Mocked const ROBOT_NAME = 'otie' const RUN_ID = '1' const mockSetSetupScreen = jest.fn() -const mockSetFixtureLocation = jest.fn() +const mockSetCutoutId = jest.fn() const mockSetProvidedFixtureOptions = jest.fn() const calibratedMockApiHeaterShaker = { @@ -118,11 +118,10 @@ const calibratedMockApiHeaterShaker = { last_modified: '2023-06-01T14:42:20.131798+00:00', }, } -const mockFixture = { - fixtureId: 'mockId', - fixtureLocation: '10' as any, - loadName: STAGING_AREA_LOAD_NAME, -} as Fixture +const mockFixture: CutoutConfig = { + cutoutId: 'cutoutD3', + cutoutFixtureId: WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, +} const render = () => { return renderWithProviders( @@ -130,7 +129,7 @@ const render = () => { , @@ -363,6 +362,7 @@ describe('ProtocolSetupModulesAndDeck', () => { { ...mockProtocolModuleInfo[0], attachedModuleMatch: calibratedMockApiHeaterShaker, + slotName: 'D3', }, ]) const [{ getByText }] = render() diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/index.tsx b/app/src/organisms/ProtocolSetupModulesAndDeck/index.tsx index 5a58f031275..394f3fb68f8 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/index.tsx +++ b/app/src/organisms/ProtocolSetupModulesAndDeck/index.tsx @@ -16,11 +16,12 @@ import { } from '@opentrons/components' import { useDeckConfigurationQuery } from '@opentrons/react-api-client' import { + FLEX_ROBOT_TYPE, getDeckDefFromRobotType, getModuleDisplayName, getModuleType, NON_CONNECTING_MODULE_TYPES, - STANDARD_SLOT_LOAD_NAME, + SINGLE_SLOT_FIXTURES, TC_MODULE_LOCATION_OT3, THERMOCYCLER_MODULE_TYPE, } from '@opentrons/shared-data' @@ -38,7 +39,8 @@ import { import { MultipleModulesModal } from '../Devices/ProtocolRun/SetupModuleAndDeck/MultipleModulesModal' import { getProtocolModulesInfo } from '../../organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo' import { useMostRecentCompletedAnalysis } from '../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' -import { ROBOT_MODEL_OT3, getLocalRobot } from '../../redux/discovery' +import { getLocalRobot } from '../../redux/discovery' +import { getCutoutIdForSlotName } from '../../resources/deck_configuration/utils' import { useChainLiveCommands } from '../../resources/runs/hooks' import { getModulePrepCommands, @@ -57,7 +59,11 @@ import { FixtureTable } from './FixtureTable' import { ModulesAndDeckMapViewModal } from './ModulesAndDeckMapViewModal' import type { CommandData } from '@opentrons/api-client' -import type { Cutout, Fixture, FixtureLoadName } from '@opentrons/shared-data' +import type { + CutoutConfig, + CutoutId, + CutoutFixtureId, +} from '@opentrons/shared-data' import type { SetupScreens } from '../../pages/OnDeviceDisplay/ProtocolSetup' import type { ProtocolCalibrationStatus } from '../../organisms/Devices/hooks' import type { AttachedProtocolModuleMatch } from './utils' @@ -75,7 +81,7 @@ interface RenderModuleStatusProps { commands: ModulePrepCommandsType[], continuePastCommandFailure: boolean ) => Promise - conflictedFixture?: Fixture + conflictedFixture?: CutoutConfig } function RenderModuleStatus({ @@ -186,7 +192,7 @@ interface RowModuleProps { ) => Promise prepCommandErrorMessage: string setPrepCommandErrorMessage: React.Dispatch> - conflictedFixture?: Fixture + conflictedFixture?: CutoutConfig } function RowModule({ @@ -229,7 +235,7 @@ function RowModule({ {showLocationConflictModal && conflictedFixture != null ? ( setShowLocationConflictModal(false)} - cutout={conflictedFixture.fixtureLocation} + cutoutId={conflictedFixture.cutoutId} requiredModule={module.moduleDef.model} isOnDevice={true} /> @@ -302,8 +308,8 @@ function RowModule({ interface ProtocolSetupModulesAndDeckProps { runId: string setSetupScreen: React.Dispatch> - setFixtureLocation: (fixtureLocation: Cutout) => void - setProvidedFixtureOptions: (providedFixtureOptions: FixtureLoadName[]) => void + setCutoutId: (cutoutId: CutoutId) => void + setProvidedFixtureOptions: (providedFixtureOptions: CutoutFixtureId[]) => void } /** @@ -312,7 +318,7 @@ interface ProtocolSetupModulesAndDeckProps { export function ProtocolSetupModulesAndDeck({ runId, setSetupScreen, - setFixtureLocation, + setCutoutId, setProvidedFixtureOptions, }: ProtocolSetupModulesAndDeckProps): JSX.Element { const { i18n, t } = useTranslation('protocol_setup') @@ -337,7 +343,7 @@ export function ProtocolSetupModulesAndDeck({ const { data: deckConfig } = useDeckConfigurationQuery() const mostRecentAnalysis = useMostRecentCompletedAnalysis(runId) - const deckDef = getDeckDefFromRobotType(ROBOT_MODEL_OT3) + const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE) const attachedModules = useAttachedModules({ @@ -401,6 +407,7 @@ export function ProtocolSetupModulesAndDeck({ flexDirection={DIRECTION_COLUMN} gridGap={SPACING.spacing24} marginTop="7.75rem" + marginBottom={SPACING.spacing80} > {isModuleMismatch && !clearModuleMismatchBanner ? ( otherModule.moduleDef.model === module.moduleDef.model ) + + const cutoutIdForSlotName = getCutoutIdForSlotName( + module.slotName, + deckDef + ) + return ( - fixture.fixtureLocation === module.slotName && - fixture.loadName !== STANDARD_SLOT_LOAD_NAME + fixture.cutoutId === cutoutIdForSlotName && + fixture.cutoutFixtureId != null && + !SINGLE_SLOT_FIXTURES.includes(fixture.cutoutFixtureId) )} /> ) })}
diff --git a/app/src/pages/DeckConfiguration/__tests__/DeckConfiguration.test.tsx b/app/src/pages/DeckConfiguration/__tests__/DeckConfiguration.test.tsx index d9e7123ab68..7f24c1e91fc 100644 --- a/app/src/pages/DeckConfiguration/__tests__/DeckConfiguration.test.tsx +++ b/app/src/pages/DeckConfiguration/__tests__/DeckConfiguration.test.tsx @@ -5,9 +5,9 @@ import { when, resetAllWhenMocks } from 'jest-when' import { DeckConfigurator, renderWithProviders } from '@opentrons/components' import { useDeckConfigurationQuery, - useCreateDeckConfigurationMutation, + useUpdateDeckConfigurationMutation, } from '@opentrons/react-api-client' -import { TRASH_BIN_LOAD_NAME } from '@opentrons/shared-data' +import { TRASH_BIN_ADAPTER_FIXTURE } from '@opentrons/shared-data' import { i18n } from '../../../i18n' import { DeckFixtureSetupInstructionsModal } from '../../../organisms/DeviceDetailsDeckConfiguration/DeckFixtureSetupInstructionsModal' @@ -17,7 +17,7 @@ import { DeckConfigurationEditor } from '..' import type { UseQueryResult } from 'react-query' import type { DeckConfiguration } from '@opentrons/shared-data' -const mockCreateDeckConfiguration = jest.fn() +const mockUpdateDeckConfiguration = jest.fn() const mockGoBack = jest.fn() jest.mock('react-router-dom', () => { const reactRouterDom = jest.requireActual('react-router-dom') @@ -29,9 +29,8 @@ jest.mock('react-router-dom', () => { const mockDeckConfig = [ { - fixtureId: 'mockFixtureIdC3', - fixtureLocation: 'cutoutC3', - loadName: TRASH_BIN_LOAD_NAME, + cutoutId: 'cutoutC3', + cutoutFixtureId: TRASH_BIN_ADAPTER_FIXTURE, }, ] @@ -56,8 +55,8 @@ const mockUseDeckConfigurationQuery = useDeckConfigurationQuery as jest.MockedFu const mockDeckConfigurationDiscardChangesModal = DeckConfigurationDiscardChangesModal as jest.MockedFunction< typeof DeckConfigurationDiscardChangesModal > -const mockUseCreateDeckConfigurationMutation = useCreateDeckConfigurationMutation as jest.MockedFunction< - typeof useCreateDeckConfigurationMutation +const mockUseUpdateDeckConfigurationMutation = useUpdateDeckConfigurationMutation as jest.MockedFunction< + typeof useUpdateDeckConfigurationMutation > const render = () => { @@ -83,8 +82,8 @@ describe('DeckConfigurationEditor', () => { mockDeckConfigurationDiscardChangesModal.mockReturnValue(
mock DeckConfigurationDiscardChangesModal
) - when(mockUseCreateDeckConfigurationMutation).mockReturnValue({ - createDeckConfiguration: mockCreateDeckConfiguration, + when(mockUseUpdateDeckConfigurationMutation).mockReturnValue({ + updateDeckConfiguration: mockUpdateDeckConfiguration, } as any) }) diff --git a/app/src/pages/DeckConfiguration/index.tsx b/app/src/pages/DeckConfiguration/index.tsx index 8f1e84f8068..6ea37eb40d6 100644 --- a/app/src/pages/DeckConfiguration/index.tsx +++ b/app/src/pages/DeckConfiguration/index.tsx @@ -12,9 +12,13 @@ import { } from '@opentrons/components' import { useDeckConfigurationQuery, - useCreateDeckConfigurationMutation, + useUpdateDeckConfigurationMutation, } from '@opentrons/react-api-client' -import { STANDARD_SLOT_LOAD_NAME } from '@opentrons/shared-data' +import { + SINGLE_RIGHT_CUTOUTS, + SINGLE_LEFT_SLOT_FIXTURE, + SINGLE_RIGHT_SLOT_FIXTURE, +} from '@opentrons/shared-data' import { SmallButton } from '../../atoms/buttons' import { ChildNavigation } from '../../organisms/ChildNavigation' @@ -23,7 +27,7 @@ import { DeckFixtureSetupInstructionsModal } from '../../organisms/DeviceDetails import { DeckConfigurationDiscardChangesModal } from '../../organisms/DeviceDetailsDeckConfiguration/DeckConfigurationDiscardChangesModal' import { Portal } from '../../App/portal' -import type { Cutout, DeckConfiguration } from '@opentrons/shared-data' +import type { CutoutId, DeckConfiguration } from '@opentrons/shared-data' export function DeckConfigurationEditor(): JSX.Element { const { t, i18n } = useTranslation([ @@ -40,42 +44,45 @@ export function DeckConfigurationEditor(): JSX.Element { showConfigurationModal, setShowConfigurationModal, ] = React.useState(false) - const [ - targetFixtureLocation, - setTargetFixtureLocation, - ] = React.useState(null) + const [targetCutoutId, setTargetCutoutId] = React.useState( + null + ) const [ showDiscardChangeModal, setShowDiscardChangeModal, ] = React.useState(false) const deckConfig = useDeckConfigurationQuery().data ?? [] - const { createDeckConfiguration } = useCreateDeckConfigurationMutation() + const { updateDeckConfiguration } = useUpdateDeckConfigurationMutation() const [ currentDeckConfig, setCurrentDeckConfig, ] = React.useState(deckConfig) - const handleClickAdd = (fixtureLocation: Cutout): void => { - setTargetFixtureLocation(fixtureLocation) + const handleClickAdd = (cutoutId: CutoutId): void => { + setTargetCutoutId(cutoutId) setShowConfigurationModal(true) } - const handleClickRemove = (fixtureLocation: Cutout): void => { + const handleClickRemove = (cutoutId: CutoutId): void => { setCurrentDeckConfig(prevDeckConfig => prevDeckConfig.map(fixture => - fixture.fixtureLocation === fixtureLocation - ? { ...fixture, loadName: STANDARD_SLOT_LOAD_NAME } + fixture.cutoutId === cutoutId + ? { + ...fixture, + cutoutFixtureId: SINGLE_RIGHT_CUTOUTS.includes(cutoutId) + ? SINGLE_RIGHT_SLOT_FIXTURE + : SINGLE_LEFT_SLOT_FIXTURE, + } : fixture ) ) - createDeckConfiguration(currentDeckConfig) } const handleClickConfirm = (): void => { if (!isEqual(deckConfig, currentDeckConfig)) { - createDeckConfiguration(currentDeckConfig) + updateDeckConfiguration(currentDeckConfig) } history.goBack() } @@ -114,9 +121,9 @@ export function DeckConfigurationEditor(): JSX.Element { isOnDevice /> ) : null} - {showConfigurationModal && targetFixtureLocation != null ? ( + {showConfigurationModal && targetCutoutId != null ? ( { } else if (protocolHardware.hardwareType === 'module') { return getModuleDisplayName(protocolHardware.moduleModel) } else { - return getFixtureDisplayName(protocolHardware.fixtureName) + return getFixtureDisplayName(protocolHardware.cutoutFixtureId) } } diff --git a/app/src/pages/OnDeviceDisplay/ProtocolDetails/__tests__/Hardware.test.tsx b/app/src/pages/OnDeviceDisplay/ProtocolDetails/__tests__/Hardware.test.tsx index c2f3fab61c5..e70ebc86dde 100644 --- a/app/src/pages/OnDeviceDisplay/ProtocolDetails/__tests__/Hardware.test.tsx +++ b/app/src/pages/OnDeviceDisplay/ProtocolDetails/__tests__/Hardware.test.tsx @@ -1,8 +1,8 @@ import * as React from 'react' import { when, resetAllWhenMocks } from 'jest-when' import { - STAGING_AREA_LOAD_NAME, - WASTE_CHUTE_LOAD_NAME, + STAGING_AREA_RIGHT_SLOT_FIXTURE, + WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, WASTE_CHUTE_CUTOUT, } from '@opentrons/shared-data' import { renderWithProviders } from '@opentrons/components' @@ -62,13 +62,13 @@ describe('Hardware', () => { }, { hardwareType: 'fixture', - fixtureName: WASTE_CHUTE_LOAD_NAME, + cutoutFixtureId: WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, location: { cutout: WASTE_CHUTE_CUTOUT }, hasSlotConflict: false, }, { hardwareType: 'fixture', - fixtureName: STAGING_AREA_LOAD_NAME, + cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, location: { cutout: 'cutoutB3' }, hasSlotConflict: false, }, @@ -93,7 +93,7 @@ describe('Hardware', () => { }) getByRole('row', { name: '1 Heater-Shaker Module GEN1' }) getByRole('row', { name: '3 Temperature Module GEN2' }) - getByRole('row', { name: 'D3 Waste Chute' }) - getByRole('row', { name: 'B3 Staging Area Slot' }) + getByRole('row', { name: 'D3 Waste chute only' }) + getByRole('row', { name: 'B3 Staging area slot' }) }) }) diff --git a/app/src/pages/OnDeviceDisplay/ProtocolSetup/__tests__/ProtocolSetup.test.tsx b/app/src/pages/OnDeviceDisplay/ProtocolSetup/__tests__/ProtocolSetup.test.tsx index 32358722ecc..eff166f2617 100644 --- a/app/src/pages/OnDeviceDisplay/ProtocolSetup/__tests__/ProtocolSetup.test.tsx +++ b/app/src/pages/OnDeviceDisplay/ProtocolSetup/__tests__/ProtocolSetup.test.tsx @@ -18,7 +18,7 @@ import { mockHeaterShaker } from '../../../../redux/modules/__fixtures__' import { FLEX_ROBOT_TYPE, getDeckDefFromRobotType, - STAGING_AREA_LOAD_NAME, + STAGING_AREA_RIGHT_SLOT_FIXTURE, } from '@opentrons/shared-data' import ot3StandardDeckDef from '@opentrons/shared-data/deck/definitions/3/ot3_standard.json' @@ -46,6 +46,7 @@ import { useRunStatus, } from '../../../../organisms/RunTimeControl/hooks' import { useIsHeaterShakerInProtocol } from '../../../../organisms/ModuleCard/hooks' +import { useDeckConfigurationCompatibility } from '../../../../resources/deck_configuration/hooks' import { ConfirmAttachedModal } from '../ConfirmAttachedModal' import { ProtocolSetup } from '..' @@ -53,7 +54,6 @@ import type { UseQueryResult } from 'react-query' import type { DeckConfiguration, CompletedProtocolAnalysis, - Fixture, } from '@opentrons/shared-data' // Mock IntersectionObserver @@ -88,6 +88,7 @@ jest.mock('../../../../organisms/ModuleCard/hooks') jest.mock('../../../../redux/discovery/selectors') jest.mock('../ConfirmAttachedModal') jest.mock('../../../../organisms/ToasterOven') +jest.mock('../../../../resources/deck_configuration/hooks') const mockGetDeckDefFromRobotType = getDeckDefFromRobotType as jest.MockedFunction< typeof getDeckDefFromRobotType @@ -163,6 +164,9 @@ const mockUseModuleCalibrationStatus = useModuleCalibrationStatus as jest.Mocked const mockGetLocalRobot = getLocalRobot as jest.MockedFunction< typeof getLocalRobot > +const mockUseDeckConfigurationCompatibility = useDeckConfigurationCompatibility as jest.MockedFunction< + typeof useDeckConfigurationCompatibility +> const render = (path = '/') => { return renderWithProviders( @@ -230,10 +234,9 @@ const mockDoorStatus = { }, } const mockFixture = { - fixtureId: 'mockId', - fixtureLocation: 'cutoutD1', - loadName: STAGING_AREA_LOAD_NAME, -} as Fixture + cutoutId: 'cutoutD1', + cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, +} const MOCK_MAKE_SNACKBAR = jest.fn() @@ -332,6 +335,7 @@ describe('ProtocolSetup', () => { .mockReturnValue(({ makeSnackbar: MOCK_MAKE_SNACKBAR, } as unknown) as any) + when(mockUseDeckConfigurationCompatibility).mockReturnValue([]) }) afterEach(() => { diff --git a/app/src/pages/OnDeviceDisplay/ProtocolSetup/index.tsx b/app/src/pages/OnDeviceDisplay/ProtocolSetup/index.tsx index d8732a22457..ea3210279e6 100644 --- a/app/src/pages/OnDeviceDisplay/ProtocolSetup/index.tsx +++ b/app/src/pages/OnDeviceDisplay/ProtocolSetup/index.tsx @@ -81,7 +81,7 @@ import { ConfirmAttachedModal } from './ConfirmAttachedModal' import { getLatestCurrentOffsets } from '../../../organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/utils' import { CloseButton, PlayButton } from './Buttons' -import type { Cutout, FixtureLoadName } from '@opentrons/shared-data' +import type { CutoutFixtureId, CutoutId } from '@opentrons/shared-data' import type { OnDeviceRouteParams } from '../../../App/types' import type { ProtocolHardware, ProtocolFixture } from '../../Protocols/hooks' import type { ProtocolModuleInfo } from '../../../organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo' @@ -463,7 +463,7 @@ function PrepareToRun({ const missingFixturesText = missingFixtures.length === 1 ? `${t('missing')} ${getFixtureDisplayName( - missingFixtures[0].fixtureName + missingFixtures[0].cutoutFixtureId )}` : t('multiple_fixtures_missing', { count: missingFixtures.length }) @@ -691,11 +691,9 @@ export function ProtocolSetup(): JSX.Element { handleProceedToRunClick, !configBypassHeaterShakerAttachmentConfirmation ) - const [fixtureLocation, setFixtureLocation] = React.useState( - '' as Cutout - ) + const [cutoutId, setCutoutId] = React.useState(null) const [providedFixtureOptions, setProvidedFixtureOptions] = React.useState< - FixtureLoadName[] + CutoutFixtureId[] >([]) // orchestrate setup subpages/components @@ -719,7 +717,7 @@ export function ProtocolSetup(): JSX.Element { ), @@ -731,7 +729,7 @@ export function ProtocolSetup(): JSX.Element { ), 'deck configuration': ( - fixture.fixtureLocation === location.slotName && - fixture.loadName !== STANDARD_SLOT_LOAD_NAME - ), + hasSlotConflict: + deckConfig?.find( + ({ cutoutId, cutoutFixtureId }) => + cutoutId === location.slotName && + cutoutFixtureId != null && + !SINGLE_SLOT_FIXTURES.includes(cutoutFixtureId) + ) != null, } } ) @@ -128,48 +139,37 @@ export const useRequiredProtocolHardwareFromAnalysis = ( }) ) - // TODO(jr, 10/2/23): IMMEDIATELY delete the stubs when api supports - // loadFixture - // const requiredFixture: ProtocolFixture[] = analysis.commands - // .filter( - // (command): command is LoadFixtureRunTimeCommand => - // command.commandType === 'loadFixture' - // ) - // .map(({ params }) => { - // return { - // hardwareType: 'fixture', - // fixtureName: params.loadName, - // location: params.location, - // } - // }) - const STUBBED_FIXTURES: ProtocolFixture[] = [ - { - hardwareType: 'fixture', - fixtureName: 'wasteChute', - location: { cutout: 'cutoutD3' }, - hasSlotConflict: false, - }, - { - hardwareType: 'fixture', - fixtureName: 'standardSlot', - location: { cutout: 'cutoutC3' }, - hasSlotConflict: false, - }, - { - hardwareType: 'fixture', - fixtureName: 'stagingArea', - location: { cutout: 'cutoutB3' }, - hasSlotConflict: false, - }, - ] + const nonSingleSlotDeckConfigCompatibility = deckConfigCompatibility.filter( + ({ requiredAddressableAreas }) => + // required AA list includes a non-single-slot AA + !requiredAddressableAreas.every(aa => + FLEX_SINGLE_SLOT_ADDRESSABLE_AREAS.includes(aa) + ) + ) + // fixture includes at least 1 required AA + const requiredDeckConfigCompatibility = nonSingleSlotDeckConfigCompatibility.filter( + fixture => fixture.requiredAddressableAreas.length > 0 + ) + + const requiredFixtures = requiredDeckConfigCompatibility.map( + ({ cutoutFixtureId, cutoutId, compatibleCutoutFixtureIds }) => ({ + hardwareType: 'fixture' as const, + cutoutFixtureId, + location: { cutout: cutoutId }, + hasSlotConflict: + cutoutFixtureId != null && + !SINGLE_SLOT_FIXTURES.includes(cutoutFixtureId) + ? compatibleCutoutFixtureIds.includes(cutoutFixtureId) + : false, + }) + ) return { requiredProtocolHardware: [ ...requiredPipettes, ...requiredModules, ...requiredGripper, - // ...requiredFixture, - ...STUBBED_FIXTURES, + ...requiredFixtures, ], isLoading: isLoadingInstruments || isLoadingModules, } @@ -245,10 +245,10 @@ const useMissingProtocolHardwareFromRequiredProtocolHardware = ( return !hardware.connected } else { // fixtures - return !deckConfig?.find( - fixture => - hardware.location.cutout === fixture.fixtureLocation && - hardware.fixtureName === fixture.loadName + return !deckConfig?.some( + ({ cutoutId, cutoutFixtureId }) => + hardware.location.cutout === cutoutId && + hardware.cutoutFixtureId === cutoutFixtureId ) } }), diff --git a/app/src/resources/deck_configuration/__tests__/hooks.test.ts b/app/src/resources/deck_configuration/__tests__/hooks.test.ts index 2ee9eb57add..5a37005074e 100644 --- a/app/src/resources/deck_configuration/__tests__/hooks.test.ts +++ b/app/src/resources/deck_configuration/__tests__/hooks.test.ts @@ -1,26 +1,16 @@ import { when, resetAllWhenMocks } from 'jest-when' -import { v4 as uuidv4 } from 'uuid' import { useDeckConfigurationQuery } from '@opentrons/react-api-client' import { - STAGING_AREA_LOAD_NAME, - STANDARD_SLOT_LOAD_NAME, - TRASH_BIN_LOAD_NAME, - WASTE_CHUTE_LOAD_NAME, + SINGLE_LEFT_SLOT_FIXTURE, + SINGLE_RIGHT_SLOT_FIXTURE, + STAGING_AREA_RIGHT_SLOT_FIXTURE, + TRASH_BIN_ADAPTER_FIXTURE, + WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE, } from '@opentrons/shared-data' -import { - CONFIGURED, - CONFLICTING, - NOT_CONFIGURED, - useLoadedFixturesConfigStatus, -} from '../hooks' - import type { UseQueryResult } from 'react-query' -import type { - DeckConfiguration, - LoadFixtureRunTimeCommand, -} from '@opentrons/shared-data' +import type { DeckConfiguration } from '@opentrons/shared-data' jest.mock('@opentrons/react-api-client') @@ -30,76 +20,40 @@ const mockUseDeckConfigurationQuery = useDeckConfigurationQuery as jest.MockedFu const MOCK_DECK_CONFIG: DeckConfiguration = [ { - fixtureLocation: 'cutoutA1', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), + cutoutId: 'cutoutA1', + cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE, }, { - fixtureLocation: 'cutoutB1', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), + cutoutId: 'cutoutB1', + cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE, }, { - fixtureLocation: 'cutoutC1', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), + cutoutId: 'cutoutC1', + cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE, }, { - fixtureLocation: 'cutoutD1', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), + cutoutId: 'cutoutD1', + cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE, }, { - fixtureLocation: 'cutoutA3', - loadName: TRASH_BIN_LOAD_NAME, - fixtureId: uuidv4(), + cutoutId: 'cutoutA3', + cutoutFixtureId: TRASH_BIN_ADAPTER_FIXTURE, }, { - fixtureLocation: 'cutoutB3', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), + cutoutId: 'cutoutB3', + cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE, }, { - fixtureLocation: 'cutoutC3', - loadName: STAGING_AREA_LOAD_NAME, - fixtureId: uuidv4(), + cutoutId: 'cutoutC3', + cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, }, { - fixtureLocation: 'cutoutD3', - loadName: WASTE_CHUTE_LOAD_NAME, - fixtureId: uuidv4(), + cutoutId: 'cutoutD3', + cutoutFixtureId: WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE, }, ] -const WASTE_CHUTE_LOADED_FIXTURE: LoadFixtureRunTimeCommand = { - id: 'stubbed_load_fixture', - commandType: 'loadFixture', - params: { - fixtureId: 'stubbedFixtureId', - loadName: WASTE_CHUTE_LOAD_NAME, - location: { cutout: 'cutoutD3' }, - }, - createdAt: 'fakeTimestamp', - startedAt: 'fakeTimestamp', - completedAt: 'fakeTimestamp', - status: 'succeeded', -} - -const STAGING_AREA_LOADED_FIXTURE: LoadFixtureRunTimeCommand = { - id: 'stubbed_load_fixture', - commandType: 'loadFixture', - params: { - fixtureId: 'stubbedFixtureId', - loadName: STAGING_AREA_LOAD_NAME, - location: { cutout: 'cutoutD3' }, - }, - createdAt: 'fakeTimestamp', - startedAt: 'fakeTimestamp', - completedAt: 'fakeTimestamp', - status: 'succeeded', -} - -describe('useLoadedFixturesConfigStatus', () => { +describe('useDeckConfigurationCompatibility', () => { beforeEach(() => { when(mockUseDeckConfigurationQuery) .calledWith() @@ -109,34 +63,5 @@ describe('useLoadedFixturesConfigStatus', () => { }) afterEach(() => resetAllWhenMocks()) - it('returns configured status if fixture is configured at location', () => { - const loadedFixturesConfigStatus = useLoadedFixturesConfigStatus([ - WASTE_CHUTE_LOADED_FIXTURE, - ]) - expect(loadedFixturesConfigStatus).toEqual([ - { ...WASTE_CHUTE_LOADED_FIXTURE, configurationStatus: CONFIGURED }, - ]) - }) - it('returns conflicted status if fixture is conflicted at location', () => { - const loadedFixturesConfigStatus = useLoadedFixturesConfigStatus([ - STAGING_AREA_LOADED_FIXTURE, - ]) - expect(loadedFixturesConfigStatus).toEqual([ - { ...STAGING_AREA_LOADED_FIXTURE, configurationStatus: CONFLICTING }, - ]) - }) - it('returns not configured status if fixture is not configured at location', () => { - when(mockUseDeckConfigurationQuery) - .calledWith() - .mockReturnValue({ - data: MOCK_DECK_CONFIG.slice(0, -1), - } as UseQueryResult) - - const loadedFixturesConfigStatus = useLoadedFixturesConfigStatus([ - WASTE_CHUTE_LOADED_FIXTURE, - ]) - expect(loadedFixturesConfigStatus).toEqual([ - { ...WASTE_CHUTE_LOADED_FIXTURE, configurationStatus: NOT_CONFIGURED }, - ]) - }) + it('returns configured status if fixture is configured at location', () => {}) }) diff --git a/app/src/resources/deck_configuration/hooks.ts b/app/src/resources/deck_configuration/hooks.ts index 967d46b5119..9d6dc3d8793 100644 --- a/app/src/resources/deck_configuration/hooks.ts +++ b/app/src/resources/deck_configuration/hooks.ts @@ -1,48 +1,60 @@ +import { parseAllAddressableAreas } from '@opentrons/api-client' import { useDeckConfigurationQuery } from '@opentrons/react-api-client' -import { STANDARD_SLOT_LOAD_NAME } from '@opentrons/shared-data' - -import type { Fixture, LoadFixtureRunTimeCommand } from '@opentrons/shared-data' - -export const CONFIGURED = 'configured' -export const CONFLICTING = 'conflicting' -export const NOT_CONFIGURED = 'not configured' - -type LoadedFixtureConfigurationStatus = - | typeof CONFIGURED - | typeof CONFLICTING - | typeof NOT_CONFIGURED - -type LoadedFixtureConfiguration = LoadFixtureRunTimeCommand & { - configurationStatus: LoadedFixtureConfigurationStatus +import { + FLEX_ROBOT_TYPE, + getDeckDefFromRobotTypeV4, +} from '@opentrons/shared-data' + +import { + getCutoutFixturesForCutoutId, + getCutoutIdForAddressableArea, +} from './utils' + +import type { + CutoutFixtureId, + RobotType, + RunTimeCommand, +} from '@opentrons/shared-data' +import type { CutoutConfigProtocolSpec } from './utils' + +export interface CutoutConfigAndCompatibility extends CutoutConfigProtocolSpec { + compatibleCutoutFixtureIds: CutoutFixtureId[] } - -export function useLoadedFixturesConfigStatus( - loadedFixtures: LoadFixtureRunTimeCommand[] -): LoadedFixtureConfiguration[] { +export function useDeckConfigurationCompatibility( + robotType: RobotType, + protocolCommands: RunTimeCommand[] +): CutoutConfigAndCompatibility[] { const deckConfig = useDeckConfigurationQuery().data ?? [] - - return loadedFixtures.map(loadedFixture => { - const deckConfigurationAtLocation = deckConfig.find( - (deckFixture: Fixture) => - deckFixture.fixtureLocation === loadedFixture.params.location.cutout - ) - - let configurationStatus: LoadedFixtureConfigurationStatus = NOT_CONFIGURED - if ( - deckConfigurationAtLocation != null && - 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 !== STANDARD_SLOT_LOAD_NAME - ) { - configurationStatus = CONFLICTING - } - - return { ...loadedFixture, configurationStatus } - }) + if (robotType !== FLEX_ROBOT_TYPE) return [] + const deckDef = getDeckDefFromRobotTypeV4(robotType) + const allAddressableAreas = parseAllAddressableAreas(protocolCommands) + return deckConfig.reduce( + (acc, { cutoutId, cutoutFixtureId }) => { + const fixturesThatMountToCutoutId = getCutoutFixturesForCutoutId( + cutoutId, + deckDef.cutoutFixtures + ) + const requiredAddressableAreasForCutoutId = allAddressableAreas.filter( + aa => + getCutoutIdForAddressableArea(aa, fixturesThatMountToCutoutId) === + cutoutId + ) + return [ + ...acc, + { + cutoutId, + cutoutFixtureId: cutoutFixtureId, + requiredAddressableAreas: requiredAddressableAreasForCutoutId, + compatibleCutoutFixtureIds: fixturesThatMountToCutoutId + .filter(cf => + requiredAddressableAreasForCutoutId.every(aa => + cf.providesAddressableAreas[cutoutId].includes(aa) + ) + ) + .map(cf => cf.id), + }, + ] + }, + [] + ) } diff --git a/app/src/resources/deck_configuration/types.ts b/app/src/resources/deck_configuration/types.ts new file mode 100644 index 00000000000..2929de72deb --- /dev/null +++ b/app/src/resources/deck_configuration/types.ts @@ -0,0 +1,11 @@ +import type { + CutoutId, + CutoutFixtureId, + AddressableAreaName, +} from '@opentrons/shared-data' + +export interface CutoutConfig { + cutoutId: CutoutId + cutoutFixtureId: CutoutFixtureId + requiredAddressableAreas: AddressableAreaName[] +} diff --git a/app/src/resources/deck_configuration/utils.ts b/app/src/resources/deck_configuration/utils.ts index 36150586b09..2075306b09e 100644 --- a/app/src/resources/deck_configuration/utils.ts +++ b/app/src/resources/deck_configuration/utils.ts @@ -1,27 +1,26 @@ import { parseAllAddressableAreas } from '@opentrons/api-client' import { FLEX_ROBOT_TYPE, + getAddressableAreaFromSlotId, getDeckDefFromRobotTypeV4, } from '@opentrons/shared-data' import type { CutoutId, - DeckConfiguration, RunTimeCommand, - Cutout, CutoutFixtureId, CutoutFixture, AddressableAreaName, - FixtureLoadName, + DeckDefinition, } from '@opentrons/shared-data' -interface CutoutConfig { +export interface CutoutConfigProtocolSpec { cutoutId: CutoutId - cutoutFixtureId: CutoutFixtureId + cutoutFixtureId: CutoutFixtureId | null requiredAddressableAreas: AddressableAreaName[] } -export const FLEX_SIMPLEST_DECK_CONFIG: CutoutConfig[] = [ +export const FLEX_SIMPLEST_DECK_CONFIG: CutoutConfigProtocolSpec[] = [ { cutoutId: 'cutoutA1', cutoutFixtureId: 'singleLeftSlot', @@ -86,95 +85,81 @@ export const FLEX_SIMPLEST_DECK_CONFIG: CutoutConfig[] = [ export function getSimplestDeckConfigForProtocolCommands( protocolAnalysisCommands: RunTimeCommand[] -): CutoutConfig[] { +): CutoutConfigProtocolSpec[] { // TODO(BC, 2023-11-06): abstract out the robot type const deckDef = getDeckDefFromRobotTypeV4(FLEX_ROBOT_TYPE) const addressableAreas = parseAllAddressableAreas(protocolAnalysisCommands) - const simplestDeckConfig = addressableAreas.reduce( - (acc, addressableArea) => { - const cutoutFixturesForAddressableArea = getCutoutFixturesForAddressableAreas( - [addressableArea], - deckDef.cutoutFixtures + const simplestDeckConfig = addressableAreas.reduce< + CutoutConfigProtocolSpec[] + >((acc, addressableArea) => { + const cutoutFixturesForAddressableArea = getCutoutFixturesForAddressableAreas( + [addressableArea], + deckDef.cutoutFixtures + ) + const cutoutIdForAddressableArea = getCutoutIdForAddressableArea( + addressableArea, + cutoutFixturesForAddressableArea + ) + const cutoutFixturesForCutoutId = + cutoutIdForAddressableArea != null + ? getCutoutFixturesForCutoutId( + cutoutIdForAddressableArea, + deckDef.cutoutFixtures + ) + : null + + const existingCutoutConfig = acc.find( + cutoutConfig => cutoutConfig.cutoutId === cutoutIdForAddressableArea + ) + + if ( + existingCutoutConfig != null && + cutoutFixturesForCutoutId != null && + cutoutIdForAddressableArea != null + ) { + const indexOfExistingFixture = cutoutFixturesForCutoutId.findIndex( + ({ id }) => id === existingCutoutConfig.cutoutFixtureId ) - const cutoutIdForAddressableArea = getCutoutIdForAddressableArea( - addressableArea, - cutoutFixturesForAddressableArea + const accIndex = acc.findIndex( + ({ cutoutId }) => cutoutId === cutoutIdForAddressableArea ) - const cutoutFixturesForCutoutId = - cutoutIdForAddressableArea != null - ? getCutoutFixturesForCutoutId( - cutoutIdForAddressableArea, - deckDef.cutoutFixtures - ) - : null - - const existingCutoutConfig = acc.find( - cutoutConfig => cutoutConfig.cutoutId === cutoutIdForAddressableArea + const previousRequiredAAs = acc[accIndex]?.requiredAddressableAreas + const allNextRequiredAddressableAreas = previousRequiredAAs.includes( + addressableArea + ) + ? previousRequiredAAs + : [...previousRequiredAAs, addressableArea] + const nextCompatibleCutoutFixture = getSimplestFixtureForAddressableAreas( + cutoutIdForAddressableArea, + allNextRequiredAddressableAreas, + cutoutFixturesForCutoutId + ) + const indexOfCurrentFixture = cutoutFixturesForCutoutId.findIndex( + ({ id }) => id === nextCompatibleCutoutFixture?.id ) if ( - existingCutoutConfig != null && - cutoutFixturesForCutoutId != null && - cutoutIdForAddressableArea != null + nextCompatibleCutoutFixture != null && + indexOfCurrentFixture > indexOfExistingFixture ) { - const indexOfExistingFixture = cutoutFixturesForCutoutId.findIndex( - ({ id }) => id === existingCutoutConfig.cutoutFixtureId - ) - const accIndex = acc.findIndex( - ({ cutoutId }) => cutoutId === cutoutIdForAddressableArea - ) - const previousRequiredAAs = acc[accIndex]?.requiredAddressableAreas - const allNextRequiredAddressableAreas = previousRequiredAAs.includes( - addressableArea - ) - ? previousRequiredAAs - : [...previousRequiredAAs, addressableArea] - const nextCompatibleCutoutFixture = getSimplestFixtureForAddressableAreas( - cutoutIdForAddressableArea, - allNextRequiredAddressableAreas, - cutoutFixturesForCutoutId - ) - const indexOfCurrentFixture = cutoutFixturesForCutoutId.findIndex( - ({ id }) => id === nextCompatibleCutoutFixture?.id - ) - - if ( - nextCompatibleCutoutFixture != null && - indexOfCurrentFixture > indexOfExistingFixture - ) { - return [ - ...acc.slice(0, accIndex), - { - cutoutId: cutoutIdForAddressableArea, - cutoutFixtureId: nextCompatibleCutoutFixture.id, - requiredAddressableAreas: allNextRequiredAddressableAreas, - }, - ...acc.slice(accIndex + 1), - ] - } + return [ + ...acc.slice(0, accIndex), + { + cutoutId: cutoutIdForAddressableArea, + cutoutFixtureId: nextCompatibleCutoutFixture.id, + requiredAddressableAreas: allNextRequiredAddressableAreas, + }, + ...acc.slice(accIndex + 1), + ] } - return acc - }, - FLEX_SIMPLEST_DECK_CONFIG - ) + } + return acc + }, FLEX_SIMPLEST_DECK_CONFIG) return simplestDeckConfig } -// TODO(BC, 11/7/23): remove this function in favor of getSimplestDeckConfigForProtocolCommands -export function getDeckConfigFromProtocolCommands( - commands: RunTimeCommand[] -): DeckConfiguration { - return getSimplestDeckConfigForProtocolCommands(commands).map( - ({ cutoutId, cutoutFixtureId }) => ({ - fixtureId: cutoutFixtureId, - fixtureLocation: cutoutId as Cutout, - loadName: cutoutFixtureId as FixtureLoadName, - }) - ) -} - export function getCutoutFixturesForAddressableAreas( addressableAreas: AddressableAreaName[], cutoutFixtures: CutoutFixture[] @@ -195,6 +180,22 @@ export function getCutoutFixturesForCutoutId( ) } +export function getCutoutIdForSlotName( + slotName: string, + deckDef: DeckDefinition +): CutoutId | null { + const addressableArea = getAddressableAreaFromSlotId(slotName, deckDef) + const cutoutIdForSlotName = + addressableArea != null + ? getCutoutIdForAddressableArea( + addressableArea.id, + deckDef.cutoutFixtures + ) + : null + + return cutoutIdForSlotName +} + export function getCutoutIdForAddressableArea( addressableArea: AddressableAreaName, cutoutFixtures: CutoutFixture[] diff --git a/components/src/hardware-sim/BaseDeck/BaseDeck.tsx b/components/src/hardware-sim/BaseDeck/BaseDeck.tsx index 28d66051393..0760fc10fa8 100644 --- a/components/src/hardware-sim/BaseDeck/BaseDeck.tsx +++ b/components/src/hardware-sim/BaseDeck/BaseDeck.tsx @@ -46,7 +46,7 @@ import type { WellFill } from '../Labware' interface BaseDeckProps { robotType: RobotType - labwareLocations: Array<{ + labwareLocations?: Array<{ labwareLocation: LabwareLocation definition: LabwareDefinition2 wellFill?: WellFill @@ -54,7 +54,7 @@ interface BaseDeckProps { labwareChildren?: React.ReactNode onLabwareClick?: () => void }> - moduleLocations: Array<{ + moduleLocations?: Array<{ moduleModel: ModuleModel moduleLocation: ModuleLocation nestedLabwareDef?: LabwareDefinition2 | null @@ -76,8 +76,8 @@ interface BaseDeckProps { export function BaseDeck(props: BaseDeckProps): JSX.Element { const { robotType, - moduleLocations, - labwareLocations, + moduleLocations = [], + labwareLocations = [], lightFill = COLORS.light1, darkFill = COLORS.darkGreyEnabled, deckLayerBlocklist = [], @@ -92,20 +92,20 @@ export function BaseDeck(props: BaseDeckProps): JSX.Element { const singleSlotFixtures = deckConfig.filter(fixture => SINGLE_SLOT_FIXTURES.includes( - fixture.fixtureId as SingleSlotCutoutFixtureId + fixture.cutoutFixtureId as SingleSlotCutoutFixtureId ) ) const stagingAreaFixtures = deckConfig.filter( - fixture => fixture.fixtureId === STAGING_AREA_RIGHT_SLOT_FIXTURE + fixture => fixture.cutoutFixtureId === STAGING_AREA_RIGHT_SLOT_FIXTURE ) const trashBinFixtures = deckConfig.filter( - fixture => fixture.fixtureId === TRASH_BIN_ADAPTER_FIXTURE + fixture => fixture.cutoutFixtureId === TRASH_BIN_ADAPTER_FIXTURE ) const wasteChuteFixtures = deckConfig.filter( fixture => WASTE_CHUTE_FIXTURES.includes( - fixture.fixtureId as WasteChuteCutoutFixtureId - ) && fixture.fixtureLocation === WASTE_CHUTE_CUTOUT + fixture.cutoutFixtureId as WasteChuteCutoutFixtureId + ) && fixture.cutoutId === WASTE_CHUTE_CUTOUT ) return ( @@ -121,8 +121,8 @@ export function BaseDeck(props: BaseDeckProps): JSX.Element { <> {singleSlotFixtures.map(fixture => ( ( ))} {trashBinFixtures.map(fixture => ( - + ))} {wasteChuteFixtures.map(fixture => ( void - handleClickRemove: (fixtureLocation: Cutout) => void + handleClickAdd: (cutoutId: CutoutId) => void + handleClickRemove: (cutoutId: CutoutId) => void lightFill?: string darkFill?: string readOnly?: boolean @@ -45,7 +45,7 @@ export function DeckConfigurator(props: DeckConfiguratorProps): JSX.Element { const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE) // restrict configuration to certain locations - const configurableFixtureLocations: Cutout[] = [ + const configurableFixtureLocations: CutoutId[] = [ 'cutoutA1', 'cutoutB1', 'cutoutC1', @@ -55,23 +55,26 @@ export function DeckConfigurator(props: DeckConfiguratorProps): JSX.Element { 'cutoutC3', 'cutoutD3', ] - const configurableDeckConfig = deckConfig.filter(fixture => - configurableFixtureLocations.includes(fixture.fixtureLocation) + const configurableDeckConfig = deckConfig.filter(({ cutoutId }) => + configurableFixtureLocations.includes(cutoutId) ) const stagingAreaFixtures = configurableDeckConfig.filter( - fixture => fixture.loadName === STAGING_AREA_LOAD_NAME + ({ cutoutFixtureId }) => cutoutFixtureId === STAGING_AREA_RIGHT_SLOT_FIXTURE ) const wasteChuteFixtures = configurableDeckConfig.filter( - fixture => fixture.loadName === WASTE_CHUTE_LOAD_NAME + ({ cutoutFixtureId }) => + cutoutFixtureId != null && WASTE_CHUTE_FIXTURES.includes(cutoutFixtureId) ) const emptyFixtures = readOnly ? [] : configurableDeckConfig.filter( - fixture => fixture.loadName === STANDARD_SLOT_LOAD_NAME + ({ cutoutFixtureId }) => + cutoutFixtureId != null && + SINGLE_SLOT_FIXTURES.includes(cutoutFixtureId) ) const trashBinFixtures = configurableDeckConfig.filter( - fixture => fixture.loadName === TRASH_BIN_LOAD_NAME + ({ cutoutFixtureId }) => cutoutFixtureId === TRASH_BIN_ADAPTER_FIXTURE ) return ( @@ -80,47 +83,46 @@ export function DeckConfigurator(props: DeckConfiguratorProps): JSX.Element { // eslint-disable-next-line @typescript-eslint/restrict-template-expressions viewBox={`${deckDef.cornerOffsetFromOrigin[0]} ${deckDef.cornerOffsetFromOrigin[1]} ${deckDef.dimensions[0]} ${deckDef.dimensions[1]}`} > - {/* TODO(bh, 2023-10-18): migrate to v4 deck def cutouts */} - {deckDef.locations.cutouts.map(slotDef => ( + {deckDef.locations.cutouts.map(cutout => ( ))} - {stagingAreaFixtures.map(fixture => ( + {stagingAreaFixtures.map(({ cutoutId }) => ( ))} - {emptyFixtures.map(fixture => ( + {emptyFixtures.map(({ cutoutId }) => ( ))} - {wasteChuteFixtures.map(fixture => ( + {wasteChuteFixtures.map(({ cutoutId }) => ( ))} - {trashBinFixtures.map(fixture => ( + {trashBinFixtures.map(({ cutoutId }) => ( ))} diff --git a/protocol-designer/src/components/DeckSetup/constants.ts b/protocol-designer/src/components/DeckSetup/constants.ts index 8d2a436291c..2037126b253 100644 --- a/protocol-designer/src/components/DeckSetup/constants.ts +++ b/protocol-designer/src/components/DeckSetup/constants.ts @@ -1,26 +1,3 @@ -import { STANDARD_SLOT_LOAD_NAME } from '@opentrons/shared-data' - -const cutouts = [ - 'A1', - 'A2', - 'A3', - 'B1', - 'B2', - 'B3', - 'C1', - 'C2', - 'C3', - 'D1', - 'D2', - 'D3', -] - -export const DEFAULT_SLOTS = cutouts.map((cutout, index) => ({ - fixtureId: (index + 1).toString(), - fixtureLocation: cutout, - loadName: STANDARD_SLOT_LOAD_NAME, -})) - export const VIEWBOX_MIN_X = -64 export const VIEWBOX_MIN_Y = -10 export const VIEWBOX_WIDTH = 520 diff --git a/protocol-designer/src/components/DeckSetup/index.tsx b/protocol-designer/src/components/DeckSetup/index.tsx index ed443435e00..3db220a1858 100644 --- a/protocol-designer/src/components/DeckSetup/index.tsx +++ b/protocol-designer/src/components/DeckSetup/index.tsx @@ -23,6 +23,7 @@ import { } from '@opentrons/step-generation' import { FLEX_ROBOT_TYPE, + FLEX_STAGING_AREA_SLOT_ADDRESSABLE_AREAS, getAddressableAreaFromSlotId, getDeckDefFromRobotType, getLabwareHasQuirk, @@ -33,11 +34,10 @@ import { inferModuleOrientationFromXCoordinate, isAddressableAreaStandardSlot, OT2_ROBOT_TYPE, - STAGING_AREA_LOAD_NAME, THERMOCYCLER_MODULE_TYPE, - TRASH_BIN_LOAD_NAME, + TRASH_BIN_ADAPTER_FIXTURE, + WASTE_CHUTE_ADDRESSABLE_AREAS, WASTE_CHUTE_CUTOUT, - WASTE_CHUTE_LOAD_NAME, } from '@opentrons/shared-data' import { FLEX_TRASH_DEF_URI, OT_2_TRASH_DEF_URI } from '../../constants' import { selectors as labwareDefSelectors } from '../../labware-defs' @@ -528,23 +528,28 @@ export const DeckSetup = (): JSX.Element => { const trashBinFixtures = [ { - fixtureId: trash?.id, - fixtureLocation: + cutoutId: trash?.slot != null ? getCutoutIdForAddressableArea( trash?.slot as AddressableAreaName, deckDef.cutoutFixtures ) : null, - loadName: TRASH_BIN_LOAD_NAME, + cutoutFixtureId: TRASH_BIN_ADAPTER_FIXTURE, }, ] const wasteChuteFixtures = Object.values( activeDeckSetup.additionalEquipmentOnDeck - ).filter(aE => aE.name === WASTE_CHUTE_LOAD_NAME) + ).filter(aE => + WASTE_CHUTE_ADDRESSABLE_AREAS.includes(aE.name as AddressableAreaName) + ) const stagingAreaFixtures: AdditionalEquipmentEntity[] = Object.values( activeDeckSetup.additionalEquipmentOnDeck - ).filter(aE => aE.name === STAGING_AREA_LOAD_NAME) + ).filter(aE => + FLEX_STAGING_AREA_SLOT_ADDRESSABLE_AREAS.includes( + aE.name as AddressableAreaName + ) + ) const filteredAddressableAreas = deckDef.locations.addressableAreas.filter( aa => isAddressableAreaStandardSlot(aa.id, deckDef) @@ -593,11 +598,11 @@ export const DeckSetup = (): JSX.Element => { /> ))} {trash != null - ? trashBinFixtures.map(fixture => - fixture.fixtureLocation != null ? ( - + ? trashBinFixtures.map(({ cutoutId }) => + cutoutId != null ? ( + { diff --git a/protocol-designer/src/components/modals/CreateFileWizard/StagingAreaTile.tsx b/protocol-designer/src/components/modals/CreateFileWizard/StagingAreaTile.tsx index dc552a72d63..423b0c93689 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/StagingAreaTile.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/StagingAreaTile.tsx @@ -13,16 +13,16 @@ import { } from '@opentrons/components' import { OT2_ROBOT_TYPE, + SINGLE_RIGHT_SLOT_FIXTURE, STAGING_AREA_CUTOUTS, - STAGING_AREA_LOAD_NAME, - STANDARD_SLOT_LOAD_NAME, + STAGING_AREA_RIGHT_SLOT_FIXTURE, } from '@opentrons/shared-data' import { i18n } from '../../../localization' import { getEnableDeckModification } from '../../../feature-flags/selectors' import { GoBack } from './GoBack' import { HandleEnter } from './HandleEnter' -import type { DeckConfiguration } from '@opentrons/shared-data' +import type { DeckConfiguration, CutoutId } from '@opentrons/shared-data' import type { WizardTileProps } from './types' export function StagingAreaTile(props: WizardTileProps): JSX.Element | null { @@ -30,36 +30,36 @@ export function StagingAreaTile(props: WizardTileProps): JSX.Element | null { const isOt2 = values.fields.robotType === OT2_ROBOT_TYPE const deckConfigurationFF = useSelector(getEnableDeckModification) const stagingAreaItems = values.additionalEquipment.filter(equipment => - equipment.includes(STAGING_AREA_LOAD_NAME) + // TODO(bc, 11/14/2023): refactor the additional items field to include a cutoutId + // and a cutoutFixtureId so that we don't have to string parse here to generate them + equipment.includes('stagingArea') ) - const savedStagingAreaSlots = stagingAreaItems.flatMap(item => { - const [loadName, fixtureLocation] = item.split('_') - const fixtureId = `id_${fixtureLocation}` - return [ - { - fixtureId, - fixtureLocation, - loadName, - }, - ] as DeckConfiguration - }) + const savedStagingAreaSlots: DeckConfiguration = stagingAreaItems.flatMap( + item => { + // TODO(bc, 11/14/2023): refactor the additional items field to include a cutoutId + // and a cutoutFixtureId so that we don't have to string parse here to generate them + const cutoutId = item.split('_')[1] as CutoutId + return [ + { + cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, + cutoutId, + }, + ] + } + ) - // NOTE: fixtureId doesn't matter since we don't create - // the entity until you complete the create file wizard via createDeckFixture action - // fixtureId here is only needed to visually add to the deck configurator const STANDARD_EMPTY_SLOTS: DeckConfiguration = STAGING_AREA_CUTOUTS.map( - fixtureLocation => ({ - fixtureId: `id_${fixtureLocation}`, - fixtureLocation, - loadName: STANDARD_SLOT_LOAD_NAME, + cutoutId => ({ + cutoutId, + cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE, }) ) STANDARD_EMPTY_SLOTS.forEach(emptySlot => { if ( !savedStagingAreaSlots.some( - slot => slot.fixtureLocation === emptySlot.fixtureLocation + ({ cutoutId }) => cutoutId === emptySlot.cutoutId ) ) { savedStagingAreaSlots.push(emptySlot) @@ -78,12 +78,12 @@ export function StagingAreaTile(props: WizardTileProps): JSX.Element | null { return null } - const handleClickAdd = (fixtureLocation: string): void => { + const handleClickAdd = (cutoutId: string): void => { const modifiedSlots: DeckConfiguration = updatedSlots.map(slot => { - if (slot.fixtureLocation === fixtureLocation) { + if (slot.cutoutId === cutoutId) { return { ...slot, - loadName: STAGING_AREA_LOAD_NAME, + cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, } } return slot @@ -91,16 +91,16 @@ export function StagingAreaTile(props: WizardTileProps): JSX.Element | null { setUpdatedSlots(modifiedSlots) setFieldValue('additionalEquipment', [ ...values.additionalEquipment, - `${STAGING_AREA_LOAD_NAME}_${fixtureLocation}`, + `stagingArea_${cutoutId}`, ]) } - const handleClickRemove = (fixtureLocation: string): void => { + const handleClickRemove = (cutoutId: string): void => { const modifiedSlots: DeckConfiguration = updatedSlots.map(slot => { - if (slot.fixtureLocation === fixtureLocation) { + if (slot.cutoutId === cutoutId) { return { ...slot, - loadName: STANDARD_SLOT_LOAD_NAME, + cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE, } } return slot @@ -108,10 +108,7 @@ export function StagingAreaTile(props: WizardTileProps): JSX.Element | null { setUpdatedSlots(modifiedSlots) setFieldValue( 'additionalEquipment', - without( - values.additionalEquipment, - `${STAGING_AREA_LOAD_NAME}_${fixtureLocation}` - ) + without(values.additionalEquipment, `stagingArea_${cutoutId}`) ) } diff --git a/protocol-designer/src/components/modules/StagingAreasModal.tsx b/protocol-designer/src/components/modules/StagingAreasModal.tsx index 79725aa5147..a5fd5a258ec 100644 --- a/protocol-designer/src/components/modules/StagingAreasModal.tsx +++ b/protocol-designer/src/components/modules/StagingAreasModal.tsx @@ -17,11 +17,11 @@ import { DeckConfigurator, } from '@opentrons/components' import { - Cutout, + CutoutId, DeckConfiguration, - STAGING_AREA_LOAD_NAME, + SINGLE_RIGHT_SLOT_FIXTURE, STAGING_AREA_CUTOUTS, - STANDARD_SLOT_LOAD_NAME, + STAGING_AREA_RIGHT_SLOT_FIXTURE, } from '@opentrons/shared-data' import { i18n } from '../../localization' import { @@ -56,27 +56,27 @@ const StagingAreasModalComponent = ( ? false : areSlotsEmpty.includes(false) - const mappedStagingAreas = stagingAreas.flatMap(area => { - return [ - { - fixtureId: area.id, - fixtureLocation: area.location ?? '', - loadName: STAGING_AREA_LOAD_NAME, - }, - ] as DeckConfiguration + const mappedStagingAreas: DeckConfiguration = stagingAreas.flatMap(area => { + return area.location != null + ? [ + { + cutoutId: area.location as CutoutId, + cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, + }, + ] + : [] }) const STANDARD_EMPTY_SLOTS: DeckConfiguration = STAGING_AREA_CUTOUTS.map( - fixtureLocation => ({ - fixtureId: `id_${fixtureLocation}`, - fixtureLocation: fixtureLocation as Cutout, - loadName: STANDARD_SLOT_LOAD_NAME, + cutoutId => ({ + cutoutId, + cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE, }) ) STANDARD_EMPTY_SLOTS.forEach(emptySlot => { if ( !mappedStagingAreas.some( - slot => slot.fixtureLocation === emptySlot.fixtureLocation + ({ cutoutId }) => cutoutId === emptySlot.cutoutId ) ) { mappedStagingAreas.push(emptySlot) @@ -89,34 +89,31 @@ const StagingAreasModalComponent = ( selectableSlots ) - const handleClickAdd = (fixtureLocation: string): void => { + const handleClickAdd = (cutoutId: string): void => { const modifiedSlots: DeckConfiguration = updatedSlots.map(slot => { - if (slot.fixtureLocation === fixtureLocation) { + if (slot.cutoutId === cutoutId) { return { ...slot, - loadName: STAGING_AREA_LOAD_NAME, + cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, } } return slot }) setUpdatedSlots(modifiedSlots) - const updatedSelectedSlots = [...values.selectedSlots, fixtureLocation] + const updatedSelectedSlots = [...values.selectedSlots, cutoutId] setFieldValue('selectedSlots', updatedSelectedSlots) } - const handleClickRemove = (fixtureLocation: string): void => { + const handleClickRemove = (cutoutId: string): void => { const modifiedSlots: DeckConfiguration = updatedSlots.map(slot => { - if (slot.fixtureLocation === fixtureLocation) { - return { - ...slot, - loadName: STANDARD_SLOT_LOAD_NAME, - } + if (slot.cutoutId === cutoutId) { + return { ...slot, loadName: SINGLE_RIGHT_SLOT_FIXTURE } } return slot }) setUpdatedSlots(modifiedSlots) const updatedSelectedSlots = values.selectedSlots.filter( - item => item !== fixtureLocation + item => item !== cutoutId ) setFieldValue('selectedSlots', updatedSelectedSlots) } diff --git a/react-api-client/src/deck_configuration/__tests__/useCreateDeckConfigurationMutation.test.tsx b/react-api-client/src/deck_configuration/__tests__/useCreateDeckConfigurationMutation.test.tsx deleted file mode 100644 index 200b05f9208..00000000000 --- a/react-api-client/src/deck_configuration/__tests__/useCreateDeckConfigurationMutation.test.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import * as React from 'react' -import { when, resetAllWhenMocks } from 'jest-when' -import { QueryClient, QueryClientProvider } from 'react-query' -import { renderHook } from '@testing-library/react-hooks' - -import { createDeckConfiguration } from '@opentrons/api-client' -// import { -// TRASH_BIN_LOAD_NAME, -// WASTE_CHUTE_LOAD_NAME, -// WASTE_CHUTE_CUTOUT, -// } from '@opentrons/shared-data' - -import { useHost } from '../../api' -import { useCreateDeckConfigurationMutation } from '..' - -import type { HostConfig } from '@opentrons/api-client' -// import type { DeckConfiguration } from '@opentrons/shared-data' - -jest.mock('@opentrons/api-client') -jest.mock('../../api/useHost') - -const mockCreateDeckConfiguration = createDeckConfiguration as jest.MockedFunction< - typeof createDeckConfiguration -> -const mockUseHost = useHost as jest.MockedFunction - -// const mockDeckConfiguration = [ -// { -// fixtureId: 'mockFixtureWasteChuteId', -// fixtureLocation: 'cutoutD3', -// loadName: WASTE_CHUTE_LOAD_NAME, -// }, -// ] as DeckConfiguration - -const HOST_CONFIG: HostConfig = { hostname: 'localhost' } - -describe('useCreateDeckConfigurationMutation hook', () => { - let wrapper: React.FunctionComponent<{}> - - beforeEach(() => { - const queryClient = new QueryClient() - const clientProvider: React.FunctionComponent<{}> = ({ children }) => ( - {children} - ) - - wrapper = clientProvider - }) - - afterEach(() => { - resetAllWhenMocks() - }) - - it('should return no data when calling createDeckConfiguration if the request fails', async () => { - when(mockUseHost).calledWith().mockReturnValue(HOST_CONFIG) - when(mockCreateDeckConfiguration) - .calledWith(HOST_CONFIG, []) - .mockRejectedValue('oh no') - - const { result, waitFor } = renderHook( - () => useCreateDeckConfigurationMutation(), - { - wrapper, - } - ) - expect(result.current.data).toBeUndefined() - result.current.createDeckConfiguration([]) - await waitFor(() => { - return result.current.status !== 'loading' - }) - expect(result.current.data).toBeUndefined() - }) - // ToDo (kk:10/25/2023) this part will be update when backend is ready - // it('should create a run when calling createDeckConfiguration callback with DeckConfiguration', async () => { - // when(useHost).calledWith().mockReturnValue(HOST_CONFIG) - // when(mockCreateDeckConfiguration) - // .calledWith(HOST_CONFIG, mockDeckConfiguration) - // .mockResolvedValue({ - // data: mockCreateDeckConfiguration, - // } as Response) - - // const { result, waitFor } = renderHook(useCreateDeckConfigurationMutation, { - // wrapper, - // }) - // act(() => result.current.createDeckConfiguration(mockDeckConfiguration)) - - // await waitFor(() => result.current.data != null) - // expect(result.current.data).toEqual(mockDeckConfiguration) - // }) -}) diff --git a/react-api-client/src/deck_configuration/index.ts b/react-api-client/src/deck_configuration/index.ts index b6237d14c30..063a5b0fe82 100644 --- a/react-api-client/src/deck_configuration/index.ts +++ b/react-api-client/src/deck_configuration/index.ts @@ -1,3 +1,2 @@ -export { useCreateDeckConfigurationMutation } from './useCreateDeckConfigurationMutation' export { useDeckConfigurationQuery } from './useDeckConfigurationQuery' export { useUpdateDeckConfigurationMutation } from './useUpdateDeckConfigurationMutation' diff --git a/react-api-client/src/deck_configuration/useCreateDeckConfigurationMutation.ts b/react-api-client/src/deck_configuration/useCreateDeckConfigurationMutation.ts deleted file mode 100644 index ad898e8c13b..00000000000 --- a/react-api-client/src/deck_configuration/useCreateDeckConfigurationMutation.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { useMutation, useQueryClient } from 'react-query' -import { createDeckConfiguration } from '@opentrons/api-client' -import { useHost } from '../api' - -import type { - UseMutateFunction, - UseMutationOptions, - UseMutationResult, -} from 'react-query' -import type { AxiosError } from 'axios' -import type { DeckConfiguration } from '@opentrons/shared-data' -import type { ErrorResponse, HostConfig } from '@opentrons/api-client' - -const DECK_CONFIGURATION = 'deck_configuration' - -export type UseCreateDeckConfigurationMutationResult = UseMutationResult< - DeckConfiguration, - AxiosError, - DeckConfiguration -> & { - createDeckConfiguration: UseMutateFunction< - DeckConfiguration, - AxiosError, - DeckConfiguration - > -} - -export type UseCreateDeckConfigurationMutationOptions = UseMutationOptions< - DeckConfiguration, - AxiosError, - DeckConfiguration -> - -export function useCreateDeckConfigurationMutation( - options: UseCreateDeckConfigurationMutationOptions = {} -): UseCreateDeckConfigurationMutationResult { - const host = useHost() - const queryClient = useQueryClient() - - const mutation = useMutation< - DeckConfiguration, - AxiosError, - DeckConfiguration - >( - [host, DECK_CONFIGURATION], - (deckConfiguration: DeckConfiguration) => - createDeckConfiguration(host as HostConfig, deckConfiguration).then( - response => { - queryClient - .invalidateQueries([host, DECK_CONFIGURATION]) - .catch((error: Error) => { - throw error - }) - return response.data - } - ), - options - ) - return { - ...mutation, - createDeckConfiguration: mutation.mutate, - } -} diff --git a/react-api-client/src/deck_configuration/useUpdateDeckConfigurationMutation.ts b/react-api-client/src/deck_configuration/useUpdateDeckConfigurationMutation.ts index 13dc7e0a155..f90bfdd2b49 100644 --- a/react-api-client/src/deck_configuration/useUpdateDeckConfigurationMutation.ts +++ b/react-api-client/src/deck_configuration/useUpdateDeckConfigurationMutation.ts @@ -12,24 +12,24 @@ import { useHost } from '../api' import type { AxiosError } from 'axios' import type { ErrorResponse, HostConfig } from '@opentrons/api-client' -import type { Fixture } from '@opentrons/shared-data' +import type { DeckConfiguration } from '@opentrons/shared-data' export type UseUpdateDeckConfigurationMutationResult = UseMutationResult< - Omit, + DeckConfiguration, AxiosError, - Omit + DeckConfiguration > & { updateDeckConfiguration: UseMutateFunction< - Omit, + DeckConfiguration, AxiosError, - Omit + DeckConfiguration > } export type UseUpdateDeckConfigurationMutationOptions = UseMutationOptions< - Omit, + DeckConfiguration, AxiosError, - Omit + DeckConfiguration > export function useUpdateDeckConfigurationMutation( @@ -39,12 +39,12 @@ export function useUpdateDeckConfigurationMutation( const queryClient = useQueryClient() const mutation = useMutation< - Omit, + DeckConfiguration, AxiosError, - Omit + DeckConfiguration >( [host, 'deck_configuration'], - (fixture: Omit) => + (fixture: DeckConfiguration) => updateDeckConfiguration(host as HostConfig, fixture).then(response => { queryClient .invalidateQueries([host, 'deck_configuration']) diff --git a/shared-data/command/types/setup.ts b/shared-data/command/types/setup.ts index f8fb78752e5..73156d26377 100644 --- a/shared-data/command/types/setup.ts +++ b/shared-data/command/types/setup.ts @@ -5,7 +5,6 @@ import type { LabwareOffset, PipetteName, ModuleModel, - FixtureLoadName, Cutout, } from '../../js' @@ -158,6 +157,6 @@ interface LoadLiquidResult { interface LoadFixtureParams { location: { cutout: Cutout } - loadName: FixtureLoadName + loadName: string fixtureId?: string } diff --git a/shared-data/deck/types/schemaV4.ts b/shared-data/deck/types/schemaV4.ts index ecf6bb51d26..9f4d6045fc4 100644 --- a/shared-data/deck/types/schemaV4.ts +++ b/shared-data/deck/types/schemaV4.ts @@ -64,6 +64,8 @@ export type SingleSlotCutoutFixtureId = | 'singleCenterSlot' | 'singleRightSlot' +export type StagingAreaRightSlotFixtureId = 'stagingAreaRightSlot' + export type TrashBinAdapterCutoutFixtureId = 'trashBinAdapter' export type WasteChuteCutoutFixtureId = @@ -74,6 +76,7 @@ export type WasteChuteCutoutFixtureId = export type CutoutFixtureId = | SingleSlotCutoutFixtureId + | StagingAreaRightSlotFixtureId | TrashBinAdapterCutoutFixtureId | WasteChuteCutoutFixtureId | 'stagingAreaRightSlot' diff --git a/shared-data/js/constants.ts b/shared-data/js/constants.ts index b31a67b958d..368f23c0a59 100644 --- a/shared-data/js/constants.ts +++ b/shared-data/js/constants.ts @@ -1,5 +1,5 @@ -import { AddressableAreaName } from '.' -import type { Cutout, ModuleType } from './types' +import type { CutoutFixtureId, CutoutId, AddressableAreaName } from '../deck' +import type { ModuleType } from './types' // constants for dealing with robot coordinate system (eg in labwareTools) export const SLOT_LENGTH_MM = 127.76 // along X axis in robot coordinate system @@ -184,18 +184,35 @@ export const TC_MODULE_LOCATION_OT3: 'A1+B1' = 'A1+B1' export const WEIGHT_OF_96_CHANNEL: '~10kg' = '~10kg' -export const STAGING_AREA_CUTOUTS: Cutout[] = [ +export const SINGLE_LEFT_CUTOUTS: CutoutId[] = [ + 'cutoutA1', + 'cutoutB1', + 'cutoutC1', + 'cutoutD1', +] + +export const SINGLE_CENTER_CUTOUTS: CutoutId[] = [ + 'cutoutA2', + 'cutoutB2', + 'cutoutC2', + 'cutoutD2', +] + +export const SINGLE_RIGHT_CUTOUTS: CutoutId[] = [ + 'cutoutA3', + 'cutoutB3', + 'cutoutC3', + 'cutoutD3', +] + +export const STAGING_AREA_CUTOUTS: CutoutId[] = [ 'cutoutA3', 'cutoutB3', 'cutoutC3', 'cutoutD3', ] -export const WASTE_CHUTE_CUTOUT: 'cutoutD3' = 'cutoutD3' -export const STAGING_AREA_LOAD_NAME = 'stagingArea' -export const STANDARD_SLOT_LOAD_NAME = 'standardSlot' -export const TRASH_BIN_LOAD_NAME = 'trashBin' -export const WASTE_CHUTE_LOAD_NAME = 'wasteChute' +export const WASTE_CHUTE_CUTOUT: 'cutoutD3' = 'cutoutD3' export const A1_ADDRESSABLE_AREA: 'A1' = 'A1' export const A2_ADDRESSABLE_AREA: 'A2' = 'A2' @@ -238,7 +255,7 @@ export const NINETY_SIX_CHANNEL_WASTE_CHUTE_ADDRESSABLE_AREA: '96ChannelWasteChu export const GRIPPER_WASTE_CHUTE_ADDRESSABLE_AREA: 'gripperWasteChute' = 'gripperWasteChute' -export const FLEX_SINGLE_SLOT_ADDRESSABLE_AREAS = [ +export const FLEX_SINGLE_SLOT_ADDRESSABLE_AREAS: AddressableAreaName[] = [ A1_ADDRESSABLE_AREA, A2_ADDRESSABLE_AREA, A3_ADDRESSABLE_AREA, @@ -253,7 +270,7 @@ export const FLEX_SINGLE_SLOT_ADDRESSABLE_AREAS = [ D3_ADDRESSABLE_AREA, ] -export const FLEX_STAGING_AREA_SLOT_ADDRESSABLE_AREAS = [ +export const FLEX_STAGING_AREA_SLOT_ADDRESSABLE_AREAS: AddressableAreaName[] = [ A4_ADDRESSABLE_AREA, B4_ADDRESSABLE_AREA, C4_ADDRESSABLE_AREA, @@ -295,13 +312,13 @@ export const STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE: ' export const STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE: 'stagingAreaSlotWithWasteChuteRightAdapterNoCover' = 'stagingAreaSlotWithWasteChuteRightAdapterNoCover' -export const SINGLE_SLOT_FIXTURES = [ +export const SINGLE_SLOT_FIXTURES: CutoutFixtureId[] = [ SINGLE_LEFT_SLOT_FIXTURE, SINGLE_CENTER_SLOT_FIXTURE, SINGLE_RIGHT_SLOT_FIXTURE, ] -export const WASTE_CHUTE_FIXTURES = [ +export const WASTE_CHUTE_FIXTURES: CutoutFixtureId[] = [ WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE, WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE, diff --git a/shared-data/js/fixtures.ts b/shared-data/js/fixtures.ts index 39ae5b9fe8d..f5549408af7 100644 --- a/shared-data/js/fixtures.ts +++ b/shared-data/js/fixtures.ts @@ -1,15 +1,18 @@ import { FLEX_ROBOT_TYPE, - STAGING_AREA_LOAD_NAME, - TRASH_BIN_LOAD_NAME, - WASTE_CHUTE_LOAD_NAME, + STAGING_AREA_RIGHT_SLOT_FIXTURE, + TRASH_BIN_ADAPTER_FIXTURE, + WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE, + WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, + STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE, + STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, } from './constants' +import type { CutoutFixtureId } from '../deck' import type { AddressableArea, CoordinateTuple, Cutout, DeckDefinition, - FixtureLoadName, OT2Cutout, } from './types' @@ -93,13 +96,27 @@ export function getAddressableAreaFromSlotId( ) } -export function getFixtureDisplayName(loadName: FixtureLoadName): string { - if (loadName === STAGING_AREA_LOAD_NAME) { - return 'Staging Area Slot' - } else if (loadName === TRASH_BIN_LOAD_NAME) { - return 'Trash Bin' - } else if (loadName === WASTE_CHUTE_LOAD_NAME) { - return 'Waste Chute' +export function getFixtureDisplayName( + cutoutFixtureId: CutoutFixtureId | null +): string { + if (cutoutFixtureId === STAGING_AREA_RIGHT_SLOT_FIXTURE) { + return 'Staging area slot' + } else if (cutoutFixtureId === TRASH_BIN_ADAPTER_FIXTURE) { + return 'Trash bin' + } else if (cutoutFixtureId === WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE) { + return 'Waste chute only' + } else if (cutoutFixtureId === WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE) { + return 'Waste chute only covered' + } else if ( + cutoutFixtureId === + STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE + ) { + return 'Waste chute with staging area slot' + } else if ( + cutoutFixtureId === + STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE + ) { + return 'Waste chute with staging area slot covered' } else { return 'Slot' } diff --git a/shared-data/js/types.ts b/shared-data/js/types.ts index 79e84d2af33..c295bd8977a 100644 --- a/shared-data/js/types.ts +++ b/shared-data/js/types.ts @@ -24,10 +24,6 @@ import { GRIPPER_V1_2, EXTENSION, MAGNETIC_BLOCK_V1, - STAGING_AREA_LOAD_NAME, - STANDARD_SLOT_LOAD_NAME, - TRASH_BIN_LOAD_NAME, - WASTE_CHUTE_LOAD_NAME, } from './constants' import type { INode } from 'svgson' import type { RunTimeCommand, LabwareLocation } from '../command/types' @@ -232,18 +228,6 @@ export type ModuleModelWithLegacy = | typeof MAGDECK | typeof TEMPDECK -export type FixtureLoadName = - | typeof STAGING_AREA_LOAD_NAME - | typeof STANDARD_SLOT_LOAD_NAME - | typeof TRASH_BIN_LOAD_NAME - | typeof WASTE_CHUTE_LOAD_NAME - -export interface DeckOffset { - x: number - y: number - z: number -} - export interface Dimensions { xDimension: number yDimension: number @@ -254,13 +238,6 @@ export interface DeckRobot { model: RobotType } -export interface DeckFixture { - id: string - slot: string - labware: string - displayName: string -} - export type CoordinateTuple = [number, number, number] export type UnitDirection = 1 | -1 @@ -307,29 +284,11 @@ export interface AddressableArea { matingSurfaceUnitVector?: UnitVectorTuple } -export interface DeckLocations { - orderedSlots: DeckSlot[] - calibrationPoints: DeckCalibrationPoint[] - fixtures: DeckFixture[] - addressableAreas: AddressableArea[] -} - export interface DeckMetadata { displayName: string tags: string[] } -export interface DeckDefinitionV3 { - otId: string - cornerOffsetFromOrigin: CoordinateTuple - dimensions: CoordinateTuple - robot: DeckRobot - cutoutFixtures: CutoutFixture[] - locations: DeckLocations - metadata: DeckMetadata - layers: INode[] -} - export interface DeckCutout { id: string position: CoordinateTuple @@ -338,13 +297,12 @@ export interface DeckCutout { export interface LegacyFixture { id: string - // TODO: is this cutout location? slot: string labware: string displayName: string } -export interface DeckLocationsV4 { +export interface DeckLocations { addressableAreas: AddressableArea[] calibrationPoints: DeckCalibrationPoint[] cutouts: DeckCutout[] @@ -356,7 +314,7 @@ export interface DeckDefinition { cornerOffsetFromOrigin: CoordinateTuple dimensions: CoordinateTuple robot: DeckRobot - locations: DeckLocationsV4 + locations: DeckLocations metadata: DeckMetadata cutoutFixtures: CutoutFixture[] } @@ -633,10 +591,9 @@ export type FlexSlot = | 'C4' | 'D4' -export interface Fixture { - fixtureId: string - fixtureLocation: Cutout - loadName: FixtureLoadName +export interface CutoutConfig { + cutoutId: CutoutId + cutoutFixtureId: CutoutFixtureId | null } -export type DeckConfiguration = Fixture[] +export type DeckConfiguration = CutoutConfig[] diff --git a/shared-data/protocol/types/schemaV7/command/setup.ts b/shared-data/protocol/types/schemaV7/command/setup.ts index f0d3ff0b0da..e6048ef58c9 100644 --- a/shared-data/protocol/types/schemaV7/command/setup.ts +++ b/shared-data/protocol/types/schemaV7/command/setup.ts @@ -5,7 +5,6 @@ import type { LabwareOffset, PipetteName, ModuleModel, - FixtureLoadName, Cutout, } from '../../../../js' @@ -154,6 +153,6 @@ interface LoadLiquidResult { interface LoadFixtureParams { location: { cutout: Cutout } - loadName: FixtureLoadName + loadName: string fixtureId?: string }