Skip to content

Commit

Permalink
Merge pull request #1463 from sanger/dpl-962-enhance-tube-arraying-sc…
Browse files Browse the repository at this point in the history
…reen

DPL-962 added tube summary panel to tube arraying screen
  • Loading branch information
andrewsparkes authored Nov 23, 2023
2 parents 5c2b31a + 6d7313f commit 721e82c
Show file tree
Hide file tree
Showing 3 changed files with 269 additions and 0 deletions.
19 changes: 19 additions & 0 deletions app/javascript/multi-stamp-tubes/components/MultiStampTubes.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
:wells="targetWells"
/>
</b-card>
<b-card bg-variant="dark" text-variant="white">
<lb-tube-array-summary :tubes="tubes" />
</b-card>
</lb-main-content>
<lb-sidebar>
<b-card header="Scan tubes" header-tag="h3">
Expand Down Expand Up @@ -54,6 +57,7 @@ import { transferTubesCreator } from 'shared/transfersCreators'
import Plate from 'shared/components/Plate'
import LabwareScan from 'shared/components/LabwareScan'
import LoadingModal from 'shared/components/LoadingModal'
import TubeArraySummary from './TubeArraySummary'
import devourApi from 'shared/devourApi'
import resources from 'shared/resources'
import { buildTubeObjs } from 'shared/tubeHelpers'
Expand All @@ -62,10 +66,25 @@ import { checkDuplicates } from 'shared/components/tubeScanValidators'
import { validScanMessage } from 'shared/components/scanValidators'
import { indexToName } from 'shared/wellHelpers'
// Multistamp tubes is used in Cardinal and scRNA pipelines to record the transfers of samples from
// tubes to a plate.
//
// In the Lab there are three steps to this process:
// 1. The Lab user arrays the tubes in an (untracked) tube rack. This component is responsible for tracking that
// arraying of tubes into the rack, by scanning each into a position. Limber LIMS records that arrangement
// of tubes as transfers into the wells of a new child plate (we do not model the rack).
// 2. The Lab user can then download from LIMS a printed version of that arrangement of tubes to paper, and print
// a label for the child plate.
// 3. The Lab user takes the paper printout, the rack of tubes, and the labeled child plate to the fume hood and
// manually transfers the samples from the tubes to the plate according to the plan. The printout is their
// checklist. Once done they click the Manual Transfer button in LIMS to action the transfers of samples into
// the child plate.
export default {
name: 'MultiStampTubes',
components: {
'lb-plate': Plate,
'lb-tube-array-summary': TubeArraySummary,
'lb-labware-scan': LabwareScan,
'lb-loading-modal': LoadingModal,
'lb-multi-stamp-tubes-transfers': MultiStampTubesTransfers,
Expand Down
147 changes: 147 additions & 0 deletions app/javascript/multi-stamp-tubes/components/TubeArraySummary.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// Import the component being tested
import { mount } from '@vue/test-utils'
import TubeArraySummary from './TubeArraySummary.vue'

// create an extended `Vue` constructor
import localVue from 'test_support/base_vue'

describe('TubeArraySummary', () => {
var emptyTubes = []
for (let i = 0; i < 96; i++) {
emptyTubes.push({ index: i, labware: null, state: 'empty' })
}

var fullSetOfTubes = []
for (let i = 0; i < 96; i++) {
var machine_barcode = (1000000000000 + i).toString()
var human_barcode = 'NT' + (1000 + i).toString() + 'G'
fullSetOfTubes.push({
index: i,
labware: { labware_barcode: { human_barcode: human_barcode, machine_barcode: machine_barcode } },
state: 'valid',
})
}

var mixtureOfTubesWithDuplicates = []
for (let i = 0; i < 96; i++) {
switch (true) {
case i < 6:
var human_barcode1 = 'NT1001G'
var machine_barcode1 = '1000000000001'
mixtureOfTubesWithDuplicates.push({
index: i.toString(),
labware: { labware_barcode: { human_barcode: human_barcode1, machine_barcode: machine_barcode1 } },
state: 'valid',
})
break
case i < 12:
var human_barcode2 = 'NT1002H'
var machine_barcode2 = '1000000000002'
mixtureOfTubesWithDuplicates.push({
index: i.toString(),
labware: { labware_barcode: { human_barcode: human_barcode2, machine_barcode: machine_barcode2 } },
state: 'valid',
})
break
case i < 18:
var human_barcode3 = 'NT1003I'
var machine_barcode3 = '1000000000003'
mixtureOfTubesWithDuplicates.push({
index: i.toString(),
labware: { labware_barcode: { human_barcode: human_barcode3, machine_barcode: machine_barcode3 } },
state: 'valid',
})
break
default:
mixtureOfTubesWithDuplicates.push({ index: i.toString(), labware: null, state: 'empty' })
break
}
}

const wrapperTubeArraySummaryEmpty = function () {
return mount(TubeArraySummary, {
propsData: {
tubes: emptyTubes,
},
localVue,
})
}

const wrapperTubeArraySummaryWithDuplicates = function () {
return mount(TubeArraySummary, {
propsData: {
tubes: mixtureOfTubesWithDuplicates,
},
localVue,
})
}

const wrapperTubeArraySummaryFull = function () {
return mount(TubeArraySummary, {
propsData: {
tubes: fullSetOfTubes,
},
localVue,
})
}
it('renders the provided caption', () => {
const wrapper = wrapperTubeArraySummaryWithDuplicates()

expect(wrapper.find('caption').text()).toEqual('Summary of scanned tubes')
})

it('renders the provided tubes summary headers', () => {
const wrapper = wrapperTubeArraySummaryWithDuplicates()

expect(wrapper.find('#header_human_barcode').text()).toEqual('Human Barcode')
expect(wrapper.find('#header_machine_barcode').text()).toEqual('Machine Barcode')
expect(wrapper.find('#header_replicates').text()).toEqual('Replicates')
})

it('renders the provided tubes summary rows', () => {
const wrapper = wrapperTubeArraySummaryWithDuplicates()

// row 1
expect(wrapper.find('#row_human_barcode_index_0').text()).toEqual('NT1001G')
expect(wrapper.find('#row_machine_barcode_index_0').text()).toEqual('1000000000001')
expect(wrapper.find('#row_replicates_index_0').text()).toEqual('6')

// row 2
expect(wrapper.find('#row_human_barcode_index_1').text()).toEqual('NT1002H')
expect(wrapper.find('#row_machine_barcode_index_1').text()).toEqual('1000000000002')
expect(wrapper.find('#row_replicates_index_1').text()).toEqual('6')

// row 3
expect(wrapper.find('#row_human_barcode_index_2').text()).toEqual('NT1003I')
expect(wrapper.find('#row_machine_barcode_index_2').text()).toEqual('1000000000003')
expect(wrapper.find('#row_replicates_index_2').text()).toEqual('6')

// row 4
expect(wrapper.find('#row_human_barcode_index_3').text()).toEqual('Empty')
expect(wrapper.find('#row_machine_barcode_index_3').text()).toEqual('Empty')
expect(wrapper.find('#row_replicates_index_3').text()).toEqual('78')
})

it('only renders a row for the empty positions when there are no tubes', () => {
const wrapper = wrapperTubeArraySummaryEmpty()

// row 1
expect(wrapper.find('#row_human_barcode_index_0').text()).toEqual('Empty')
expect(wrapper.find('#row_machine_barcode_index_0').text()).toEqual('Empty')
expect(wrapper.find('#row_replicates_index_0').text()).toEqual('96')
})

it('does not render a row for empty positions when there are none', () => {
const wrapper = wrapperTubeArraySummaryFull()

// row 1
expect(wrapper.find('#row_human_barcode_index_0').text()).toEqual('NT1000G')
expect(wrapper.find('#row_machine_barcode_index_0').text()).toEqual('1000000000000')
expect(wrapper.find('#row_replicates_index_0').text()).toEqual('1')

// row 96
expect(wrapper.find('#row_human_barcode_index_95').text()).toEqual('NT1095G')
expect(wrapper.find('#row_machine_barcode_index_95').text()).toEqual('1000000000095')
expect(wrapper.find('#row_replicates_index_95').text()).toEqual('1')
})
})
103 changes: 103 additions & 0 deletions app/javascript/multi-stamp-tubes/components/TubeArraySummary.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<template>
<table id="tube_scan_summary" :class="['plate-view', 'pool-colours']">
<caption>
{{
'Summary of scanned tubes'
}}
</caption>
<thead>
<tr>
<th class="first-col" />
<th id="header_human_barcode" class="headingcell">Human Barcode</th>
<th id="header_machine_barcode" class="headingcell">Machine Barcode</th>
<th id="header_replicates" class="headingcell">Replicates</th>
</tr>
</thead>
<tbody>
<tr v-for="(value, machine_barcode, rowIndex) in tubesDict" :key="rowIndex">
<th class="first-col">
{{ (rowIndex + 1).toString() + '.' }}
</th>
<td :id="`row_human_barcode_index_${rowIndex}`" class="summarycell">
{{ value.human_barcode }}
</td>
<td :id="`row_machine_barcode_index_${rowIndex}`" class="summarycell">
{{ machine_barcode }}
</td>
<td :id="`row_replicates_index_${rowIndex}`" class="replicate_cell">
{{ value.replicates }}
</td>
</tr>
</tbody>
</table>
</template>

<script>
export default {
name: 'TubeArraySummary',
props: {
// See parent component MultiStampTubes for more details about the Lab process.
// This prop is the array of tube objects from the parent component, it represents the tubes
// scanned into the MultiStampTubes arraying component. It gets updated every time there is
// a change to the tubes in the parent component.
//
// Each tube object contains the following:
// - index: the index of the tube in the array, related to the position in the tube rack
// - labware: the labware object scanned into the parent screen, null if position is empty, here
// we are most interested in the barcodes of the labware
// - state: the state of the tube at this index, or 'empty' if nothing scanned yet
tubes: {
type: Array,
required: true,
},
},
computed: {
// Create a dictionary of tube barcodes and their numbers of replicates for use
// in the summary table
tubesDict() {
var summary_dict = {}
this.tubes.forEach(function (tube) {
var tube_machine_barcode = 'Empty'
var tube_human_barcode = 'Empty'
// extract the labware barcodes where the labware has been scanned
if (tube.labware != null) {
tube_machine_barcode = tube.labware.labware_barcode.machine_barcode
tube_human_barcode = tube.labware.labware_barcode.human_barcode
}
// build up the dictionary of summary table replicates by barcode (and empties)
if (tube_machine_barcode in summary_dict) {
summary_dict[tube_machine_barcode]['replicates'] += 1
} else {
summary_dict[tube_machine_barcode] = {
replicates: 1,
human_barcode: tube_human_barcode,
}
}
})
return summary_dict
},
},
}
</script>

<style scoped lang="scss">
.headingcell {
margin-top: 2px;
text-align: left;
padding: 4px;
border: 1px #343a40 solid;
border-radius: 2px;
}
.summarycell {
margin-top: 2px;
text-align: left;
padding: 4px;
}
.replicate_cell {
margin-top: 2px;
text-align: right;
padding: 4px;
}
</style>

0 comments on commit 721e82c

Please sign in to comment.