diff --git a/protocol-designer/src/components/FileSidebar/FileSidebar.tsx b/protocol-designer/src/components/FileSidebar/FileSidebar.tsx index 11b8d21053d2..b083a17230fa 100644 --- a/protocol-designer/src/components/FileSidebar/FileSidebar.tsx +++ b/protocol-designer/src/components/FileSidebar/FileSidebar.tsx @@ -127,12 +127,53 @@ function getWarningContent({ } const pipettesDetails = pipettesWithoutStep - .map(pipette => `${pipette.mount} ${pipette.spec.displayName}`) + .map(pipette => + pipette.spec.channels === 96 + ? `${pipette.spec.displayName} pipette` + : `${pipette.mount} ${pipette.spec.displayName} pipette` + ) .join(' and ') - const modulesDetails = modulesWithoutStep - .map(moduleOnDeck => t(`modules:module_long_names.${moduleOnDeck.type}`)) - .join(' and ') + const unusedModuleCounts = modulesWithoutStep.reduce<{ + [key: string]: number + }>((acc, mod) => { + if (!(mod.type in acc)) { + return { ...acc, [mod.type]: 1 } + } else { + return { ...acc, [mod.type]: acc[mod.type] + 1 } + } + }, {}) + + const modulesDetails = Object.keys(unusedModuleCounts) + // sorting by module count + .sort((k1, k2) => { + if (unusedModuleCounts[k1] < unusedModuleCounts[k2]) { + return 1 + } else if (unusedModuleCounts[k1] > unusedModuleCounts[k2]) { + return -1 + } else { + return 0 + } + }) + .map(modType => + unusedModuleCounts[modType] === 1 + ? t(`modules:module_long_names.${modType}`) + : `${t(`modules:module_long_names.${modType}`)}s` + ) + // transform list of modules with counts to string + .reduce((acc, modName, index, arr) => { + if (arr.length > 2) { + if (index === arr.length - 1) { + return `${acc} and ${modName}` + } else { + return `${acc}${modName}, ` + } + } else if (arr.length === 2) { + return index === 0 ? `${modName} and ` : `${acc}${modName}` + } else { + return modName + } + }, '') if (pipettesWithoutStep.length && modulesWithoutStep.length) { return { diff --git a/protocol-designer/src/components/FileSidebar/__tests__/FileSidebar.test.tsx b/protocol-designer/src/components/FileSidebar/__tests__/FileSidebar.test.tsx index 8d0d38ec6cf3..11bda8345c6a 100644 --- a/protocol-designer/src/components/FileSidebar/__tests__/FileSidebar.test.tsx +++ b/protocol-designer/src/components/FileSidebar/__tests__/FileSidebar.test.tsx @@ -230,4 +230,58 @@ describe('FileSidebar', () => { 'One or more modules specified in your protocol in Slot(s) A1,B1 are not currently used in any step. In order to run this protocol you will need to power up and connect the modules to your robot.' ) }) + it('renders the formatted unused pipettes and modules warning sorted by count', () => { + vi.mocked(getInitialDeckSetup).mockReturnValue({ + modules: { + moduleId1: { + slot: 'A1', + moduleState: {} as any, + id: 'moduleId', + type: 'thermocyclerModuleType', + model: 'thermocyclerModuleV2', + }, + moduleId2: { + slot: 'C3', + moduleState: {} as any, + id: 'moduleId1', + type: 'temperatureModuleType', + model: 'temperatureModuleV2', + }, + moduleId3: { + slot: 'D3', + moduleState: {} as any, + id: 'moduleId2', + type: 'temperatureModuleType', + model: 'temperatureModuleV2', + }, + moduleId4: { + slot: 'C1', + moduleState: {} as any, + id: 'moduleId3', + type: 'heaterShakerModuleType', + model: 'heaterShakerModuleV1', + }, + }, + pipettes: { + pipetteId: { + mount: 'left', + name: 'p1000_96', + id: 'pipetteId', + tiprackLabwareDef: [fixtureTiprack300ul as LabwareDefinition2], + tiprackDefURI: ['mockDefUri'], + spec: { + displayName: 'mock display name', + channels: 96, + } as any, + }, + }, + additionalEquipmentOnDeck: {}, + labware: {}, + }) + render() + fireEvent.click(screen.getByRole('button', { name: 'Export' })) + screen.getByText( + 'The mock display name pipette and Temperature modules, Thermocycler module, and Heater-Shaker module in your protocol are not currently used in any step. In order to run this protocol you will need to attach this pipette as well as power up and connect the module to your robot.' + ) + }) })