Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
340081b
add RNA Seq branch
wendyyang Feb 9, 2026
4fd4858
Y25-682 - Add RNAseq Workflow Branch
sabrine33 Feb 9, 2026
c46ec1a
run prettier and rubocop
sabrine33 Feb 9, 2026
095bf26
check in changes
wendyyang Feb 10, 2026
923a394
support processed labware state
sabrine33 Feb 16, 2026
e536e43
save changes
wendyyang Feb 17, 2026
24e80f1
update the template name
wendyyang Feb 20, 2026
5250430
update the config file
wendyyang Feb 20, 2026
7dbbb93
update the presenter
wendyyang Feb 20, 2026
188f07a
revert autodetect_project/study flag
wendyyang Feb 20, 2026
b8cad20
save changes
wendyyang Feb 23, 2026
2ed3a51
add submissionWhenPassed statemachine to presenter
wendyyang Feb 23, 2026
df20716
fix linting
wendyyang Feb 25, 2026
5bfe0a3
remove comment out codes
wendyyang Feb 25, 2026
eadf66e
refactor StockPlateWithSubmissionPresenter by inherit SubmissionPlate…
wendyyang Mar 2, 2026
0bd96d3
uodate the request options value
wendyyang Mar 2, 2026
0198c20
disable submission when request is active
wendyyang Mar 2, 2026
7f712f7
update disable workflow condition
wendyyang Mar 3, 2026
db42a10
remove submission as defined within Y26-004
sabrine33 Mar 3, 2026
7a50cb5
update request pending condition
wendyyang Mar 3, 2026
f814dd3
Merge branch '2665-y26-004---lcm-triomics---choose-workflow-emseq-rna…
wendyyang Mar 3, 2026
aa4a47a
disable the button for the same request type only
wendyyang Mar 4, 2026
c789c0f
revert the changes as request_as_source is private method
wendyyang Mar 5, 2026
efb0ea2
update th request type mapping
wendyyang Mar 5, 2026
ae948e2
Quite functional code made by Github Copilot. Adds a field to the cho…
KatyTaylor Mar 5, 2026
0275c62
correct config
sabrine33 Mar 6, 2026
0bf6c59
run rubocop
sabrine33 Mar 6, 2026
555a0ce
fix LCMT RNA Preamp children and use finish robot term
sabrine33 Mar 9, 2026
7dafc07
update robot start button accordingly
sabrine33 Mar 11, 2026
fa98236
remove hard coded to find request type
wendyyang Mar 11, 2026
cb3d4a1
fix linting
wendyyang Mar 11, 2026
3941fd1
fix linting
wendyyang Mar 11, 2026
0cf66ae
update the method name
wendyyang Mar 11, 2026
ff31dac
resolve conflics
wendyyang Mar 11, 2026
b6266a4
add tests
wendyyang Mar 15, 2026
47e30e8
fix the bug
wendyyang Mar 15, 2026
c495ab3
fix the linting
wendyyang Mar 15, 2026
949f126
add more tests
wendyyang Mar 16, 2026
633edf7
fix linting
wendyyang Mar 16, 2026
3febc5d
Merge pull request #2731 from sanger/2665-y26-004---lcm-triomics---ch…
wendyyang Mar 16, 2026
d844c3e
Merge pull request #2700 from sanger/Y25-682
sabrine33 Mar 17, 2026
2b44a29
set project uuid on order creation, rather than submission
KatyTaylor Mar 19, 2026
76165ee
rubocop
KatyTaylor Mar 20, 2026
6e3bcea
erb lint
KatyTaylor Mar 20, 2026
c06da7d
undo refactor as not necessary any more
KatyTaylor Mar 20, 2026
e7e626f
Merge branch 'develop' into Triomic_Epic
wendyyang Mar 24, 2026
ceb005f
merge epic branch in
wendyyang Mar 31, 2026
f90cdd7
fix linting
wendyyang Mar 31, 2026
3d6ef3f
fix the test
wendyyang Mar 31, 2026
295d8c4
fix statuses in bed verification, after UAT feedback
KatyTaylor Mar 31, 2026
4edf2af
Merge pull request #2767 from sanger/bed_verification_tweak
wendyyang Mar 31, 2026
49c34b1
move javascript to js file
wendyyang Mar 31, 2026
3671146
fix the linting
wendyyang Mar 31, 2026
06706ca
add js tests
wendyyang Apr 1, 2026
e83791d
correct bed verification positions after UAT feedback
KatyTaylor Apr 7, 2026
211eda5
Merge pull request #2776 from sanger/bed_verification_tweak
KatyTaylor Apr 7, 2026
2a7e491
add the style
wendyyang Apr 7, 2026
a02ef33
add validation of project input
wendyyang Apr 7, 2026
4097347
fix linting
wendyyang Apr 7, 2026
034b0e6
add condition for showing project input field
wendyyang Apr 8, 2026
527ecf4
add condition to show project input field
wendyyang Apr 8, 2026
d978d03
remove redundant codes and fix linting
wendyyang Apr 8, 2026
6a8dbcc
refactor the javascript
wendyyang Apr 8, 2026
1f87624
fix linting
wendyyang Apr 8, 2026
e68b3e0
fix test
wendyyang Apr 9, 2026
fc906c4
add CollectionProjectBehaviour to PermissiveSubmissionPlatePresenter
wendyyang Apr 9, 2026
e3fa4ae
Merge branch 'refs/heads/Triomic_Epic' into Y26-005-LCM-Triomics-assi…
wendyyang Apr 9, 2026
255ffef
add more option to trigger project option
wendyyang Apr 9, 2026
60140ed
tidy up the codes
wendyyang Apr 9, 2026
20c18aa
fix linting
wendyyang Apr 9, 2026
fd4a931
Merge pull request #2732 from sanger/Y26-005-LCM-Triomics-assign-Proj…
wendyyang Apr 10, 2026
9e0998c
revert 'fix' LCMT RNA Preamp bed layout
sabrine33 Apr 14, 2026
71a0575
Merge pull request #2792 from sanger/Y25-682
sabrine33 Apr 14, 2026
5344f26
resolve conflicts
sabrine33 Apr 15, 2026
99762fc
Merge branch 'Triomic_Epic' of github.com:sanger/limber into Triomic_…
sabrine33 Apr 16, 2026
7f76a36
Merge branch 'develop' into Triomic_Epic
wendyyang Apr 17, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def sequencescape_submission_parameters
params
.expect(sequencescape_submission: [
:template_uuid,
:supplied_project_uuid,
:labware_barcode, {
request_options: {}, assets: [], asset_groups: {}, extra_barcodes: []
}
Expand Down
14 changes: 14 additions & 0 deletions app/controllers/plates_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@
# fail_wells => Updates the state of individual wells when failing
# Note: Finds plates via the v2 api
class PlatesController < LabwareController
# AJAX endpoint to search for a Project by id using Sequencescape API
def find_project_by_id
project_id = params[:project_id]
begin
project = Sequencescape::Api::V2::Project.find!(project_id)
# Ensure we return both id and uuid for clarity
render json: { found: true,
project: { id: project.first.id, uuid: project.first.uuid, name: project.first.name } }
rescue JsonApiClient::Errors::NotFound
render json: { found: false, error: 'Project not found' }, status: :not_found
rescue StandardError => e
render json: { found: false, error: e.message }, status: :internal_server_error
end
end
before_action :check_for_current_user!, only: %i[update fail_wells] # rubocop:todo Rails/LexicallyScopedActionFilter

def fail_wells # rubocop:todo Metrics/AbcSize
Expand Down
113 changes: 113 additions & 0 deletions app/frontend/entrypoints/pages/choose_workflow.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,116 @@
import { disableEnterKeySubmit } from '@/javascript/lib/disable_enter_key_submit.js'

disableEnterKeySubmit('#choose_workflow_card', '#submission_forms')

const globalInput = document.getElementById('project_cost_code_global')
const resultDiv = document.getElementById('project_search_result')

if (globalInput && resultDiv) {
let foundProjectId = null
let validatedProjectCode = null

const injectProjectFields = function (form, projectCode, projectUuid) {
const hiddenProjectCode = form.querySelector('.project-cost-code-hidden')
const hiddenProjectUuid = form.querySelector('.supplied-project-uuid-hidden')

if (hiddenProjectCode) {
hiddenProjectCode.value = projectCode
}

if (hiddenProjectUuid) {
hiddenProjectUuid.value = projectUuid || ''
}
}

const showError = function (message) {
foundProjectId = null
validatedProjectCode = null
resultDiv.innerHTML = `<div class="alert alert-danger">${message}</div>`
globalInput.classList.add('is-invalid')
}

const showSuccess = function (project) {
resultDiv.textContent = `Project found: ${project.name || project.uuid}`
globalInput.classList.remove('is-invalid')
}

const lookupProject = async function (projectCode) {
resultDiv.textContent = 'Searching...'
globalInput.classList.remove('is-invalid')

try {
const response = await fetch(`/plates/find_project_by_id?project_id=${encodeURIComponent(projectCode)}`)
const data = await response.json()

if (!response.ok || !data.found) {
throw new Error(data.error || 'Project not found')
}

foundProjectId = data.project.uuid
validatedProjectCode = projectCode
showSuccess(data.project)
return true
} catch (error) {
showError(error.message || 'Project not found')
return false
}
}

const triggerLookup = async function ({ requireValue = false } = {}) {
const projectCode = globalInput.value.trim()

if (!projectCode) {
if (requireValue) showError('Project / Cost Code is required')
return
}
if (validatedProjectCode === projectCode && foundProjectId) return
await lookupProject(projectCode)
}

globalInput.addEventListener('input', function () {
foundProjectId = null
validatedProjectCode = null
resultDiv.textContent = ''
globalInput.classList.remove('is-invalid')
})

globalInput.addEventListener('keydown', async function (event) {
if (event.key !== 'Enter') return
event.preventDefault()
await triggerLookup({ requireValue: true })
})

globalInput.addEventListener('blur', function () {
triggerLookup()
})

document.querySelectorAll('#submission_forms form').forEach(function (form) {
form.addEventListener('submit', async function (event) {
const projectCode = globalInput.value.trim()

if (!projectCode) {
event.preventDefault()
showError('Project / Cost Code is required')
globalInput.focus()
return
}

if (validatedProjectCode === projectCode && foundProjectId) {
injectProjectFields(form, projectCode, foundProjectId)
return
}

event.preventDefault()

const projectFound = await lookupProject(projectCode)

if (!projectFound) {
globalInput.focus()
return
}

injectProjectFields(form, projectCode, foundProjectId)
form.submit()
})
})
}
148 changes: 148 additions & 0 deletions app/frontend/entrypoints/pages/choose_workflow.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
describe('choose_workflow submission form project fields', () => {
const waitForCondition = async (predicate, timeoutMs = 1000, intervalMs = 10) => {
const start = Date.now()

while (Date.now() - start < timeoutMs) {
if (predicate()) return
await new Promise((resolve) => setTimeout(resolve, intervalMs))
}

throw new Error('Timed out waiting for async workflow update')
}

beforeEach(() => {
document.body.innerHTML = `
<div id="choose_workflow_card">
<input id="project_cost_code_global" type="text" />
<div id="project_search_result"></div>
<div id="submission_forms">
<form>
<input type="hidden" class="project-cost-code-hidden" />
<input type="hidden" class="supplied-project-uuid-hidden" />
</form>
</div>
</div>
`
})

afterEach(() => {
vi.unstubAllGlobals()
})

it('sets hidden project fields when the form is submitted', async () => {
vi.resetModules()

const projectInput = document.getElementById('project_cost_code_global')
const resultDiv = document.getElementById('project_search_result')
const form = document.querySelector('#submission_forms form')
const hiddenProjectCode = form.querySelector('.project-cost-code-hidden')
const hiddenProjectUuid = form.querySelector('.supplied-project-uuid-hidden')

vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue({
found: true,
project: { uuid: 'project-uuid-123', name: 'Example Project' },
}),
}),
)

await import('./choose_workflow.js')

projectInput.value = '12345'
projectInput.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
await waitForCondition(() => resultDiv.textContent === 'Project found: Example Project')

form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }))

expect(hiddenProjectCode.value).toBe('12345')
expect(hiddenProjectUuid.value).toBe('project-uuid-123')
})

it('validates and submits the form when project is entered without pressing enter first', async () => {
vi.resetModules()

const projectInput = document.getElementById('project_cost_code_global')
const resultDiv = document.getElementById('project_search_result')
const form = document.querySelector('#submission_forms form')
const hiddenProjectCode = form.querySelector('.project-cost-code-hidden')
const hiddenProjectUuid = form.querySelector('.supplied-project-uuid-hidden')

form.submit = vi.fn()

vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue({
found: true,
project: { uuid: 'project-uuid-456', name: 'Validated Project' },
}),
}),
)

await import('./choose_workflow.js')

projectInput.value = '67890'
form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }))

await waitForCondition(() => resultDiv.textContent === 'Project found: Validated Project')
await waitForCondition(() => form.submit.mock.calls.length === 1)

expect(hiddenProjectCode.value).toBe('67890')
expect(hiddenProjectUuid.value).toBe('project-uuid-456')
})

it('sets project uuid hidden field to empty string when no project is found', async () => {
vi.resetModules()

const projectInput = document.getElementById('project_cost_code_global')
const resultDiv = document.getElementById('project_search_result')
const form = document.querySelector('#submission_forms form')
const hiddenProjectCode = form.querySelector('.project-cost-code-hidden')
const hiddenProjectUuid = form.querySelector('.supplied-project-uuid-hidden')

vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue({
ok: false,
json: vi.fn().mockResolvedValue({
found: false,
error: 'Project not found',
}),
}),
)

await import('./choose_workflow.js')

projectInput.value = '99999'
projectInput.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
await waitForCondition(() => resultDiv.textContent === 'Project not found')

form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }))

expect(hiddenProjectCode.value).toBe('')
expect(hiddenProjectUuid.value).toBe('')
})

it('shows an error and does not submit when project code is blank', async () => {
vi.resetModules()

const projectInput = document.getElementById('project_cost_code_global')
const resultDiv = document.getElementById('project_search_result')
const form = document.querySelector('#submission_forms form')

form.submit = vi.fn()

await import('./choose_workflow.js')

projectInput.value = ' '
form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }))

expect(resultDiv.textContent).toContain('Project / Cost Code is required')
expect(projectInput.classList.contains('is-invalid')).toBe(true)
expect(form.submit).not.toHaveBeenCalled()
})
})
1 change: 1 addition & 0 deletions app/frontend/stylesheets/limber/_labware-mixins.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
background: none;
}
.started,
.processed,
.process_1,
.process_2 {
background-color: $info;
Expand Down
2 changes: 2 additions & 0 deletions app/frontend/stylesheets/limber/screen.scss
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,7 @@ dl#samples-information {
@extend .list-group-item-warning;
}
li.state-started,
li.state-processed,
li.state-process_1,
li.state-process_2 {
@extend .list-group-item-info;
Expand Down Expand Up @@ -413,6 +414,7 @@ dl#samples-information {
@extend.bg-warning;
}
.state-badge.started,
.state-badge.processed,
.state-badge.process_1,
.state-badge.process_2 {
@extend.bg-info;
Expand Down
3 changes: 3 additions & 0 deletions app/integrations/sequencescape/api/v2/submission_template.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@

# submission template resource
class Sequencescape::Api::V2::SubmissionTemplate < Sequencescape::Api::V2::Base
def self.find_by(uuid:)
where(uuid:).first
end
end
10 changes: 10 additions & 0 deletions app/models/concerns/presenters/collect_project_behaviour.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# frozen_string_literal: true

# Include in a presenter which needs to collect a project from the user before allowing submission creation
module Presenters::CollectProjectBehaviour
def collect_project?
return false if purpose_config[:presenter_class].is_a?(String)

purpose_config.dig(:presenter_class, :args, :collect_project) || false
end
end
9 changes: 9 additions & 0 deletions app/models/concerns/presenters/statemachine/permissive.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ module Permissive # rubocop:todo Style/Documentation
include DoesNotAllowLibraryPassing
end

state :processed do
include StateAllowsChildCreation
include DoesNotAllowLibraryPassing

def sidebar_partial
'default'
end
end

state :processed_1 do
include StateAllowsChildCreation
include DoesNotAllowLibraryPassing
Expand Down
17 changes: 17 additions & 0 deletions app/models/presenters/pcr_permissive_presenter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

module Presenters
#
# Combines PCR rendering (tagged aliquots) with permissive plate creation behavior
#
class PcrPermissivePresenter < StandardPresenter
include Presenters::Statemachine::Permissive

# Use the tagged aliquot partial from PcrPresenter
self.aliquot_partial = 'tagged_aliquot'

# Include the same validators as PermissivePresenter
validates_with Validators::SuboptimalValidator
validates_with Validators::ActiveRequestValidator
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ module Presenters
class PermissiveSubmissionPlatePresenter < PlatePresenter
include Presenters::Statemachine::PermissiveSubmission
include Presenters::SubmissionBehaviour
include Presenters::CollectProjectBehaviour

validates_with Validators::SuboptimalValidator
validates_with Validators::ActiveRequestValidator
Expand Down
4 changes: 4 additions & 0 deletions app/models/presenters/plate_presenter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,10 @@ def display_manual_transfer_button?
can_be_enabled?(purpose_config.dig(:manual_transfer, :states))
end

def disable_button_for_submission?(_submission)
false
end

private

def libraries_passable?
Expand Down
Loading
Loading