Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 18 additions & 2 deletions app/frontend/javascript/multi-stamp/components/MultiStamp.vue
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,15 @@ import {
} from '@/javascript/shared/components/plateScanValidators.js'
import devourApi from '@/javascript/shared/devourApi.js'
import buildPlateObjs from '@/javascript/shared/plateHelpers.js'
import { handleFailedRequest, requestIsActive, requestsFromPlates } from '@/javascript/shared/requestHelpers.js'
import {
allWellsFromPlates,
handleFailedRequest,
requestIsActive,
requestsFromPlates,
} from '@/javascript/shared/requestHelpers.js'
import resources from '@/javascript/shared/resources.js'
import { transferPlatesToPlatesCreator } from '@/javascript/shared/transfersCreators.js'
import { transfersFromRequests } from '@/javascript/shared/transfersLayouts.js'
import { transfersFromRequests, transfersFromAllWells } from '@/javascript/shared/transfersLayouts.js'
import MultiStampTransfers from './MultiStampTransfers.vue'
import NullFilter from './NullFilter.vue'
import PlateSummary from './PlateSummary.vue'
Expand Down Expand Up @@ -150,6 +155,11 @@ export default {
required: false,
default: 'false',
},

// Flag to transfer all wells with aliquots, regardless of whether they have requests.
// Defaults to false.
// Also referenced as transfer-all-wells and transfer_all_wells
transferAllWells: { type: String, required: false, default: 'false' },
},
data() {
return {
Expand Down Expand Up @@ -226,7 +236,13 @@ export default {
}
return requestsWithPlatesArray
},
allWellsWithAliquots() {
return allWellsFromPlates(this.validPlates)
},
transfers() {
if (this.transferAllWells === 'true') {
return transfersFromAllWells(this.allWellsWithAliquots, this.transfersLayout)
}
return transfersFromRequests(this.requestsWithPlatesFiltered, this.transfersLayout)
},
validTransfers() {
Expand Down
23 changes: 22 additions & 1 deletion app/frontend/javascript/shared/requestHelpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,27 @@ const requestsFromPlates = function (plateObjs) {
return requestsArray
}

// Gets all wells with aliquots from an array of plateObjs, regardless of whether they have requests.
// Returns one object per occupied well, with request set to undefined.
const allWellsFromPlates = function (plateObjs) {
const wellsArray = []
for (let p = 0; p < plateObjs.length; p++) {
const plateObj = plateObjs[p]
const wells = plateObj.plate.wells
for (let w = 0; w < wells.length; w++) {
const well = wells[w]
if (well.aliquots.length > 0) {
wellsArray.push({
request: undefined,
well: well,
plateObj: plateObj,
})
}
}
}
return wellsArray
}

const handleFailedRequest = function (request) {
// generate an alert on the page
let title = 'Unexpected error'
Expand All @@ -50,4 +71,4 @@ const handleFailedRequest = function (request) {
})
}

export { handleFailedRequest, requestIsActive, requestIsLibraryCreation, requestsFromPlates }
export { allWellsFromPlates, handleFailedRequest, requestIsActive, requestIsLibraryCreation, requestsFromPlates }
65 changes: 64 additions & 1 deletion app/frontend/javascript/shared/requestHelpers.spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,69 @@
import { handleFailedRequest } from '@/javascript/shared/requestHelpers.js'
import { allWellsFromPlates, handleFailedRequest } from '@/javascript/shared/requestHelpers.js'
import eventBus from '@/javascript/shared/eventBus.js'

describe('allWellsFromPlates', () => {
const buildPlateObj = (index, wells) => ({
index,
state: 'valid',
plate: { uuid: `plate-uuid-${index}`, wells },
})

const buildWell = (position, aliquotCount = 0) => ({
uuid: `well-uuid-${position}`,
position: { name: position },
aliquots: Array.from({ length: aliquotCount }, (_, i) => ({ uuid: `aliquot-${position}-${i}` })),
})

it('returns an empty array when given no plates', () => {
expect(allWellsFromPlates([])).toEqual([])
})

it('returns an empty array when all wells are empty', () => {
const plateObj = buildPlateObj(0, [buildWell('A1'), buildWell('B1')])
expect(allWellsFromPlates([plateObj])).toEqual([])
})

it('returns only wells that have aliquots', () => {
const emptyWell = buildWell('A1', 0)
const occupiedWell = buildWell('B1', 1)
const plateObj = buildPlateObj(0, [emptyWell, occupiedWell])

const result = allWellsFromPlates([plateObj])

expect(result).toHaveLength(1)
expect(result[0].well).toBe(occupiedWell)
})

it('sets request to undefined for each returned entry', () => {
const plateObj = buildPlateObj(0, [buildWell('A1', 2)])

const result = allWellsFromPlates([plateObj])

expect(result[0].request).toBeUndefined()
})

it('includes the correct plateObj reference on each entry', () => {
const plateObj = buildPlateObj(0, [buildWell('A1', 1)])

const result = allWellsFromPlates([plateObj])

expect(result[0].plateObj).toBe(plateObj)
})

it('returns entries for all occupied wells across multiple plates', () => {
const plate0 = buildPlateObj(0, [buildWell('A1', 1), buildWell('B1', 0)])
const plate1 = buildPlateObj(1, [buildWell('A1', 0), buildWell('C1', 3)])

const result = allWellsFromPlates([plate0, plate1])

expect(result).toHaveLength(2)
expect(result[0].well.position.name).toBe('A1')
expect(result[0].plateObj).toBe(plate0)
expect(result[1].well.position.name).toBe('C1')
expect(result[1].plateObj).toBe(plate1)
})
})

describe('handleFailedRequest', () => {
it('emits a danger alert with a formatted message when response contains an array', () => {
const mockEmit = vi.spyOn(eventBus, '$emit')
Expand Down
2 changes: 1 addition & 1 deletion app/frontend/javascript/shared/transfersCreators.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const transferPlatesToPlatesCreator = function (transfers, extraParams = (_) =>
source_plate: transfers[i].plateObj.plate.uuid,
pool_index: transfers[i].plateObj.index + 1,
source_asset: transfers[i].well.uuid,
outer_request: transfers[i].request.uuid,
outer_request: transfers[i].request?.uuid ?? null,
new_target: { location: transfers[i].targetWell },
...extraParams(transfers[i]),
}
Expand Down
27 changes: 22 additions & 5 deletions app/frontend/javascript/shared/transfersLayouts.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,12 @@ const quadrantTransfers = function (requestsWithPlates) {
// duplicatedRequests array.
//
// Note: Indexes are calculated for 96 wells plates only.
const buildPlatesMatrix = function (requestsWithPlates, maxPlates, maxWellsPerPlate) {
const buildPlatesMatrix = function (requestsWithPlates, maxPlates, maxWellsPerPlate, transferAllWells = false) {
const platesMatrix = buildArray(maxPlates, () => new Array(maxWellsPerPlate))
const duplicatedRequests = []
for (let i = 0; i < requestsWithPlates.length; i++) {
const { request, well, plateObj } = requestsWithPlates[i]
if (request === undefined) {
if (request === undefined && !transferAllWells) {
continue
}
const wellIndex = nameToIndex(well.position.name, 8)
Expand Down Expand Up @@ -167,11 +167,12 @@ const buildSequentialTransfersArray = function (transferRequests) {
const transfers = new Array(transferRequests.length)
for (let i = 0; i < transferRequests.length; i++) {
const requestWithPlate = transferRequests[i]
const targetWell = indexToName(i, 8)
transfers[i] = {
request: requestWithPlate.request,
well: requestWithPlate.well,
plateObj: requestWithPlate.plateObj,
targetWell: indexToName(i, 8),
targetWell: targetWell,
}
}
return transfers
Expand Down Expand Up @@ -273,8 +274,8 @@ const buildSequentialTubesTransfersArray = function (transferRequests) {
// |C1|C2| | | | |P1D1|P2A1|P2D3
// +--+--+--~ +--+--+--~ +----+----+----~
// |D1| |D3 | |D2|D3 |P1C2|P2B2|
const sequentialTransfers = function (requestsWithPlates) {
const { platesMatrix, duplicatedRequests } = buildPlatesMatrix(requestsWithPlates, 10, 96)
const sequentialTransfers = function (requestsWithPlates, transferAllWells = false) {
const { platesMatrix, duplicatedRequests } = buildPlatesMatrix(requestsWithPlates, 10, 96, transferAllWells)
const transferRequests = platesMatrix.flat()
const validTransfers = buildSequentialTransfersArray(transferRequests)
const duplicatedTransfers = buildSequentialTransfersArray(duplicatedRequests)
Expand Down Expand Up @@ -337,6 +338,21 @@ const transfersFromRequests = function (requestsWithPlates, transfersLayout) {
return { valid: validTransfers, duplicated: duplicatedTransfers }
}

// Receives an array of allWellsWithAliquots and a transfer layout name (developed
// for 'sequential' only).
// Returns an object containing an array of valid transfers and an array of
// duplicated transfers.
// Throws an error if the transfers layout string is not mapped to a transfer
// function.
const transfersFromAllWells = function (allWellsWithAliquots, transfersLayout) {
const transferFunction = transferFunctions[transfersLayout]
if (transferFunction === undefined) {
throw `Invalid transfers layout name: ${transfersLayout}`
}
const { validTransfers, duplicatedTransfers } = transferFunction(allWellsWithAliquots, true)
return { valid: validTransfers, duplicated: duplicatedTransfers }
}

// Receives an array of potential transfers and a transfer layout name
// (valid options: 'sequentialtubes').
// Returns an object containing an array of valid transfers
Expand Down Expand Up @@ -367,6 +383,7 @@ const transfersForTubes = function (validTubes) {

export {
transfersFromRequests,
transfersFromAllWells,
transfersForTubes,
buildPlatesMatrix,
buildLibrarySplitPlatesMatrix,
Expand Down
33 changes: 33 additions & 0 deletions app/frontend/javascript/shared/transfersLayouts.spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
transfersFromRequests,
transfersFromAllWells,
buildPlatesMatrix,
buildLibrarySplitPlatesMatrix,
buildSequentialLibrarySplitTransfersArray,
Expand Down Expand Up @@ -281,4 +282,36 @@ describe('transfersLayouts.js', () => {
])
})
})

describe('#transfersFromAllWells', () => {
const allWells = [
{ request: undefined, well: well1, plateObj: plateObj1 },
{ request: undefined, well: well2, plateObj: plateObj2 },
]

it('throws an error if invalid layout is provided', () => {
expect(() => transfersFromAllWells(allWells, 'invalid')).toThrow('Invalid transfers layout name: invalid')
})

it('creates the correct transfers with sequential layout', () => {
const transfersResults = transfersFromAllWells(allWells, 'sequential')

expect(transfersResults.valid).toEqual([
{
request: undefined,
well: well1,
plateObj: plateObj1,
targetWell: 'A1',
},
{
request: undefined,
well: well2,
plateObj: plateObj2,
targetWell: 'B1',
},
])

expect(transfersResults.duplicated).toEqual([])
})
})
})
14 changes: 14 additions & 0 deletions app/models/concerns/presenters/state_input_no_submission.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# frozen_string_literal: true

module Presenters
# Include in presenters that suppress state changes under all circumstances
module StateInputNoSubmission
def control_state_change
# You cannot change the state
end

def default_state_change
# You cannot change the state
end
end
end
14 changes: 14 additions & 0 deletions app/models/concerns/presenters/stock_no_submission_behaviour.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# frozen_string_literal: true

# Include in a presenter which handles stock plates that don't require submissions
module Presenters::StockNoSubmissionBehaviour
extend ActiveSupport::Concern

include Presenters::StateInputNoSubmission

included { validates_with Validators::StockNoSubmissionStateValidator, if: :pending? }

def input_barcode
barcode
end
end
4 changes: 4 additions & 0 deletions app/models/labware_creators/multi_stamp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ def require_active_library_requests?
params&.fetch('require_active_library_requests', false)
end

def transfer_all_wells?
false
end

private

def create_labware!
Expand Down
16 changes: 16 additions & 0 deletions app/models/labware_creators/ten_stamp_all_wells.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# frozen_string_literal: true

module LabwareCreators
# A variant of TenStamp that transfers all wells containing aliquots,
# regardless of whether they have active requests.
# Developed for scRNA aggregation, where the XP and Input plates will not have an active submission
# at the time of transfer. Instead the submission will be made on the child Cherrypick plate.
class TenStampAllWells < TenStamp
# Flag to indicate that all wells with aliquots should be transferred, regardless of active requests.
# Defaulted to false in MultiStamp (super class of TenStamp), but overriden to true here.
# Passed through to the javascript for multi-stamping where it determines the behavior.
def transfer_all_wells?
true
end
end
end
23 changes: 23 additions & 0 deletions app/models/presenters/stock_plate_with_no_submission_presenter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

module Presenters
#
# Presenters::StockPlateWithNoSubmissionPresenter is used for stock plates
# which do not need a submission to continue.
# This is used for the scRNA Core pipeline, specifically for LRC GEM-X 5p CITE SUP Input plates.
#
class StockPlateWithNoSubmissionPresenter < PlatePresenter
include Presenters::StockNoSubmissionBehaviour
include Presenters::Statemachine::Permissive

validates_with Validators::SuboptimalValidator

def allow_new_submission?
true
end

def state
'passed'
end
end
end
24 changes: 24 additions & 0 deletions app/models/validators/stock_no_submission_state_validator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

module Validators
# Validator for stock plates that don't require submissions
# Same as StockStateValidator but skips checks for submissions
class StockNoSubmissionStateValidator < StockStateValidator
# rubocop:disable Metrics/MethodLength
def validate(presenter)
analyzer = Analyzer.new(presenter.labware)
if analyzer.no_samples?
presenter.errors.add(:plate, 'has no samples. Did the cherry-pick complete successfully?')
else
if analyzer.duplicates?
presenter.errors.add(:plate, "has multiple submissions on: #{analyzer.duplicates.to_sentence}")
end
if analyzer.empty_wells_with_requests?
presenter.errors.add(:plate, "has requests on empty wells: #{analyzer.empty_wells_with_requests.to_sentence}")
end
end
end

# rubocop:enable Metrics/MethodLength
end
end
3 changes: 2 additions & 1 deletion app/views/plate_creation/multi_stamp.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
data-target-columns="<%= @labware_creator.target_columns %>"
data-source-plates="<%= @labware_creator.source_plates %>"
data-acceptable-purposes="<%= @labware_creator.acceptable_purposes %>"
data-require-active-library-requests="<%= @labware_creator.require_active_library_requests? %>">
data-require-active-library-requests="<%= @labware_creator.require_active_library_requests? %>"
data-transfer-all-wells="<%= @labware_creator.transfer_all_wells? %>">
<div class="spinner-dark">Loading...</div>
</div>
Loading
Loading