Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DPL-110: Group arbitrary tubes #894

Merged
merged 38 commits into from
Nov 8, 2021
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
096ae89
Add page for new tube rack
sdjmchattie Oct 25, 2021
2203136
Add a top level button to create a new tube rack
sdjmchattie Oct 25, 2021
67801fc
Add JavaScript files for TubesToRack
sdjmchattie Oct 25, 2021
0900d2a
Set up usual Vue construct for new view
sdjmchattie Oct 25, 2021
4e7d56e
Port across new LabwareScan code from Cardinal
sdjmchattie Oct 25, 2021
4260441
Flesh out tube rack creation UI
sdjmchattie Oct 25, 2021
3e7dfe4
Specify the labware type in the LabwareScan component
sdjmchattie Oct 25, 2021
dd7a296
Fix linting issues
sdjmchattie Oct 25, 2021
8872aba
Add tests for labware type in messages and placeholders
sdjmchattie Oct 25, 2021
1a42bed
Change case of new tube rack button
sdjmchattie Oct 26, 2021
448ae5d
Merge branch 'develop' into DPL-110-group-arbitrary-tubes
sdjmchattie Oct 26, 2021
2cb25d7
Add message about missing UI for tube rack component
sdjmchattie Oct 26, 2021
624c674
Avoid repetition of the valid message in tube scan validators
sdjmchattie Oct 27, 2021
bc1f18b
Do some validation of scanned tubes
sdjmchattie Oct 27, 2021
29a99ed
Add validity for the Create button
sdjmchattie Oct 27, 2021
4be0eaf
Setup the inclusion of purposes for tubes from SS
sdjmchattie Oct 27, 2021
de67b4e
Refactor duplicated methods in ScanValidators
sdjmchattie Oct 27, 2021
cc6f064
Create a test for valid scan message
sdjmchattie Oct 27, 2021
fc16b90
Add validation for matching purposes across tubes
sdjmchattie Oct 27, 2021
9012a55
Add unit tests for checkMatchingPurposes
sdjmchattie Oct 27, 2021
d236b73
Force use of single quotes
sdjmchattie Oct 27, 2021
46d31c7
Merge branch 'develop' into DPL-110-group-arbitrary-tubes
sdjmchattie Oct 27, 2021
60f84e5
Add purpose to tube factory
sdjmchattie Oct 27, 2021
05ad666
Add first test for TubesToRack Vue component
sdjmchattie Oct 27, 2021
2f3ea7a
Add remaining tests for TubeToRack
sdjmchattie Oct 28, 2021
17b50cc
Start to provide the required creator class for tube racks
sdjmchattie Oct 28, 2021
2a6c504
Merge branch 'develop' into DPL-110-group-arbitrary-tubes
sdjmchattie Nov 2, 2021
0830dd4
Remove valid message from validators
sdjmchattie Nov 2, 2021
a2bcf07
Inline single export
sdjmchattie Nov 2, 2021
de89906
Define a tube rack by width and height
sdjmchattie Nov 2, 2021
94bc11b
Use shared method for position names in racks
sdjmchattie Nov 2, 2021
e431ec2
Show UNKNOWN purpose name when the tube has no purpose
sdjmchattie Nov 2, 2021
b0ce41b
Extract style to a defined class
sdjmchattie Nov 2, 2021
4c62165
Merge branch 'DPL-110-group-arbitrary-tubes' into DPL-110-submit-tube…
sdjmchattie Nov 3, 2021
116bc67
Remove button for creating a tube rack
sdjmchattie Nov 3, 2021
bc74e8e
Drop in a comment about the component being only partially complete
sdjmchattie Nov 3, 2021
86f496d
Remove new routing for tube racks
sdjmchattie Nov 8, 2021
4475640
Merge pull request #905 from sanger/DPL-110-submit-tube-rack
sdjmchattie Nov 8, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions app/assets/stylesheets/limber/screen.scss
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@ button#btnGroupInboxes, button#btnGroupRobots, button#btnGroupWip {
@extend .btn-block;
}

#btnNewTubeRack {
@extend .btn;
@extend .btn-primary;
@extend .btn-lg;
@extend .btn-block;
}

#btnPipelinesOverview {
@extend .btn;
@extend .btn-primary;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ describe('MultiStampTubes', () => {
wrapper.vm.updateTube(2, tube1)
const validator = wrapper.vm.scanValidation[0]
const validations = validator(tube1.labware)

expect(validations.valid).toEqual(true)
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ import devourApi from 'shared/devourApi'
import resources from 'shared/resources'
import { buildTubeObjs } from 'shared/tubeHelpers'
import { transfersForTubes } from 'shared/transfersLayouts'
import { checkDuplicates, validTubeScanMessage } from 'shared/components/tubeScanValidators'
import { checkDuplicates } from 'shared/components/tubeScanValidators'
import { validScanMessage } from 'shared/components/scanValidators'
import { indexToName } from 'shared/wellHelpers'

export default {
Expand Down Expand Up @@ -203,7 +204,7 @@ export default {
scanValidation() {
if (this.allowTubeDuplicates === 'true') {
return [
validTubeScanMessage
validScanMessage
]
}
const currTubes = this.tubes.map(tubeItem => tubeItem.labware)
Expand Down
1 change: 1 addition & 0 deletions app/javascript/packs/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ require('qc-information')
require('multi-stamp')
require('multi-stamp-tubes')
require('custom-tagged-plate')
require('tubes-to-rack')

// Load simple javscripts
// Tag animations rotates the displayed tag Id in wells with multiple tags
Expand Down
52 changes: 51 additions & 1 deletion app/javascript/shared/components/LabwareScan.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,19 @@ describe('LabwareScan', () => {
const nullTube = { data: [] }
const goodTube = jsonCollectionFactory('tube', [{ uuid: assetUuid }])

const wrapperFactoryPlate = function(api = mockApi()) {
return mount(LabwareScan, {
propsData: {
labwareType: 'plate',
label: 'My Plate',
description: 'Scan it in',
api: api.devour,
includes: '',
colourIndex: 3
},
localVue
})
}

const wrapperFactoryTube = function(api = mockApi()) {
return mount(LabwareScan, {
Expand Down Expand Up @@ -61,13 +74,50 @@ describe('LabwareScan', () => {
expect(wrapper.find('.text-muted').text()).toEqual('Scan it in')
})

it('renders the correct placeholder for a plate', () => {
const wrapper = wrapperFactoryPlate()

expect(wrapper.find('input').element.placeholder).toEqual('Scan plate')
})

it('renders the correct placeholder for a tube', () => {
const wrapper = wrapperFactoryTube()

expect(wrapper.find('input').element.placeholder).toEqual('Scan tube')
})

it('renders disabled if the disabled prop is set true', () => {
const wrapper = wrapperFactoryTubeDisabled()

expect(wrapper.find('input').element.disabled).toBe(true)
})

it('is invalid if it can not find a plate', async () => {
const api = mockApi()
api.mockGet('plates', {
filter: { barcode: 'not a barcode' },
include: '',
fields: { plates: 'labware_barcode,uuid,number_of_rows,number_of_columns' }
}, nullTube)
const wrapper = wrapperFactoryPlate(api)

wrapper.find('input').setValue('not a barcode')
await wrapper.find('input').trigger('change')

expect(wrapper.find('.wait-plate').exists()).toBe(true)

await flushPromises()

expect(wrapper.find('.invalid-feedback').text()).toEqual('Could not find plate')
expect(wrapper.emitted()).toEqual({
change: [
[{ state: 'searching', plate: null }],
[{ state: 'invalid', plate: undefined }]
]
})
})

it('is invalid if it can not find a tube', async () => {
const api = mockApi()
api.mockGet('tubes', {
filter: { barcode: 'not a barcode' },
Expand All @@ -83,7 +133,7 @@ describe('LabwareScan', () => {

await flushPromises()

expect(wrapper.find('.invalid-feedback').text()).toEqual('Could not find labware')
expect(wrapper.find('.invalid-feedback').text()).toEqual('Could not find tube')
expect(wrapper.emitted()).toEqual({
change: [
[{ state: 'searching', labware: null }],
Expand Down
13 changes: 8 additions & 5 deletions app/javascript/shared/components/LabwareScan.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
<div>
<b-container>
<b-row>
<b-col v-if="labwareType=='tube'"
<b-col
v-if="labwareType=='tube'"
cols="1"
>
<div :class="['pool-colours']">
<div :id="'well_index_' + colourIndex"
<div
:id="'well_index_' + colourIndex"
class="well"
>
<span :class="['aliquot', colourClass]" />
Expand All @@ -31,7 +33,7 @@
type="text"
:state="formState"
size="lg"
placeholder="Scan labware"
:placeholder="'Scan ' + labwareType"
:disabled="scanDisabled"
@change="lookupLabware"
/>
Expand All @@ -44,7 +46,8 @@

<script>

import { checkSize, aggregate } from './plateScanValidators'
import { checkSize } from './plateScanValidators'
import { aggregate } from './scanValidators'

// Incrementing counter to ensure all instances of LabwareScan
// have a unique id. Ensures labels correctly match up with
Expand Down Expand Up @@ -152,7 +155,7 @@ export default {
if (this.labware === null) {
return { state: 'empty', message: '' }
} else if (this.labware === undefined) {
return { state: 'invalid', message: 'Could not find labware' }
return { state: 'invalid', message: `Could not find ${this.labwareType}` }
} else {
const result = aggregate(this.computedValidators, this.labware)
return { state: boolToString[result.valid], message: result.message }
Expand Down
30 changes: 10 additions & 20 deletions app/javascript/shared/components/plateScanValidators.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@
//
// Validating multiple criteria
// Try and keep validators checking one thing only. It makes them easier to
// test and reuse. The aggregate validator allows you to combine multiple
// validators together.
// test and reuse. Multiple validators can be combined together using the aggregate
// function in scanValidators.js

import { validScanMessage } from './scanValidators'

// Returns a validator which ensures the plate is of a particular size.
// For example, to validate your typical 12*8 96 well plate: checkSize(12,8)
Expand All @@ -50,7 +52,7 @@ const checkSize = (cols, rows) => {
return { valid: false, message: `The plate should be ${cols}×${rows} wells in size` }
}
else {
return { valid: true, message: 'Great!' }
return validScanMessage()
}
}
}
Expand All @@ -69,7 +71,7 @@ const checkDuplicates = (plateList) => {
return { valid: false, message: 'Barcode has been scanned multiple times' }
}
else {
return { valid: true, message: 'Great!' }
return validScanMessage()
}
}
}
Expand All @@ -92,7 +94,7 @@ const checkExcess = (excessTransfers) => {
return { valid: false, message: 'Wells in excess: ' + excessWells.join(', ') }
}
else {
return { valid: true, message: 'Great!' }
return validScanMessage()
}
}
}
Expand All @@ -106,7 +108,7 @@ const checkState = (allowedStatesList) => {
if(!allowedStatesList.includes(plate.state)) {
return { valid: false, message: 'Plate must have a state of: ' + allowedStatesList.join(' or ') }
} else {
return { valid: true, message: 'Great!' }
return validScanMessage()
}
}
}
Expand All @@ -123,21 +125,9 @@ const checkQCableWalkingBy = (allowedWalkingByList) => {
if(!allowedWalkingByList.includes(qcable.lot.tag_layout_template.walking_by)) {
return { valid: false, message: 'QCable layout must have a walking by of: ' + allowedWalkingByList.join(' or ') }
} else {
return { valid: true, message: 'Great!' }
return validScanMessage()
}
}
}

// Receives an array of validators and calls them in the order they appear on
// the array.
// As a result, the smallest indexed failing validator will determine the
// error message and the remaining validators will be skipped.
// eg. aggregate([checkSize(12, 8), checkDuplicates(this.plates, 0)], plate)
// will return a validator which checks first the size of the plate, then duplications.
const aggregate = (validators, item) => {
return validators.reduce((aggregate, validator) => {
return aggregate.valid ? validator(item) : aggregate
}, { valid: true, message: 'Great!'})
}

export { checkSize, checkDuplicates, checkExcess, checkState, checkQCableWalkingBy, aggregate }
export { checkSize, checkDuplicates, checkExcess, checkState, checkQCableWalkingBy }
16 changes: 1 addition & 15 deletions app/javascript/shared/components/plateScanValidators.spec.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,4 @@
import { checkSize, checkDuplicates, checkExcess, checkState, checkQCableWalkingBy, aggregate } from 'shared/components/plateScanValidators'

describe('aggregate', () => {
const validFunction = (_) => { return { valid: true, message: 'Good' } }
const invalidFunction = (_) => { return { valid: false, message: 'Bad' } }

it('is valid if all functions are valid', () => {
expect(aggregate([validFunction, validFunction], {})).toEqual({ valid: true, message: 'Good' })
})

it('is invalid if any functions are invalid', () => {
expect(aggregate([validFunction, invalidFunction], {})).toEqual({ valid: false, message: 'Bad' })
expect(aggregate([invalidFunction, validFunction], {})).toEqual({ valid: false, message: 'Bad' })
})
})
import { checkSize, checkDuplicates, checkExcess, checkState, checkQCableWalkingBy } from 'shared/components/plateScanValidators'

describe('checkSize', () => {
it('is valid if the plate is the correct size', () => {
Expand Down
18 changes: 18 additions & 0 deletions app/javascript/shared/components/scanValidators.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Receives an array of validators and calls them in the order they appear on
// the array.
// As a result, the smallest indexed failing validator will determine the
// error message and the remaining validators will be skipped.
const aggregate = (validators, item) => {
return validators.reduce((aggregate, validator) => {
return aggregate.valid ? validator(item) : aggregate
}, validScanMessage())
}

// Return object for a valid scan
//
// Returns an object with a scan validation message
const validScanMessage = () => {
return { valid: true, message: 'Great!' }
}
sdjmchattie marked this conversation as resolved.
Show resolved Hide resolved

export { aggregate, validScanMessage }
21 changes: 21 additions & 0 deletions app/javascript/shared/components/scanValidators.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { aggregate, validScanMessage } from 'shared/components/scanValidators'

describe('aggregate', () => {
const validFunction = (_) => { return { valid: true, message: 'Good' } }
const invalidFunction = (_) => { return { valid: false, message: 'Bad' } }

it('is valid if all functions are valid', () => {
expect(aggregate([validFunction, validFunction], {})).toEqual({ valid: true, message: 'Good' })
})

it('is invalid if any functions are invalid', () => {
expect(aggregate([validFunction, invalidFunction], {})).toEqual({ valid: false, message: 'Bad' })
expect(aggregate([invalidFunction, validFunction], {})).toEqual({ valid: false, message: 'Bad' })
})
})

describe('validScanMessage', () => {
it('provides a valid scan result object', () => {
expect(validScanMessage()).toEqual({ valid: true, message: 'Great!' })
})
})
45 changes: 23 additions & 22 deletions app/javascript/shared/components/tubeScanValidators.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// provide custom validation.
//
// A validator is a function which:
// 1) Takes the result of the APi query (usually a tube) as a sole argument
// 1) Takes the result of the API query (usually a tube) as a sole argument
// 2) Returns javascript object with two properties:
// a) valid: A Boolean indicating if the tube is suitable or not
// b) message: A string which will be displayed to the user. Especially
Expand Down Expand Up @@ -35,9 +35,11 @@
//}
//
// Validating multiple criteria
// Try and keep validators checking one thing only. It makes them easier to
// test and reuse. The aggregate validator allows you to combine multiple
// validators together.
// Try and keep validators checking one thing only. It makes them easier to test
// and reuse. Multiple validators can be combined together using the aggregate
// function in scanValidators.js

import { validScanMessage } from './scanValidators'

// Returns a validator that ensures that the scanned item does not appear
// multiple times in the list (based on UUID).
Expand All @@ -53,16 +55,25 @@ const checkDuplicates = (tubeList) => {
return { valid: false, message: 'Barcode has been scanned multiple times' }
}
else {
return validTubeScanMessage()
return validScanMessage()
}
}
}

// Renders a valid Tube scan
//
// Returns an object with the right tube scan validation message
const validTubeScanMessage = () => {
return { valid: true, message: 'Great!' }
// Returns a validator that ensures the purpose namess of all the tubes match
// the name for the one provided. Typically the one provided should be the one
// of the purposes from the full set of tubes being validated.
const checkMatchingPurposes = (purpose) => {
return (tube) => {
if (tube && purpose && tube.purpose.name !== purpose.name) {
sdjmchattie marked this conversation as resolved.
Show resolved Hide resolved
return {
valid: false,
message: `Tube purpose '${tube.purpose.name}' doesn't match other tubes`
}
}

return validScanMessage()
}
}

// Returns a validator that ensures the tube has a state that matches to the
Expand All @@ -74,19 +85,9 @@ const checkState = (allowedStatesList) => {
if(!allowedStatesList.includes(tube.state)) {
return { valid: false, message: 'Tube must have a state of: ' + allowedStatesList.join(' or ') }
} else {
return { valid: true, message: 'Great!' }
return validScanMessage()
}
}
}

// Receives an array of validators and calls them in the order they appear on
// the array.
// As a result, the smallest indexed failing validator will determine the
// error message and the remaining validators will be skipped.
const aggregate = (validators, item) => {
return validators.reduce((aggregate, validator) => {
return aggregate.valid ? validator(item) : aggregate
}, { valid: true, message: 'Great!'})
}

export { checkDuplicates, checkState, aggregate, validTubeScanMessage }
export { checkDuplicates, checkMatchingPurposes, checkState }
Loading