From be4c595e010ccbef6ccf9e59422771bd46f128c5 Mon Sep 17 00:00:00 2001 From: Swaraj Patil Date: Wed, 24 Jun 2026 23:34:09 -0400 Subject: [PATCH 1/4] Migrate DIANN loadpage conditionalPanels to server-side shinyjs visibility --- R/constants.R | 28 +++ R/loadpage-server-rendering.R | 220 ++++++++++++++++++ R/module-loadpage-server.R | 3 + R/module-loadpage-ui.R | 79 ++++--- .../testthat/test-loadpage-server-rendering.R | 109 +++++++++ 5 files changed, 406 insertions(+), 33 deletions(-) create mode 100644 R/loadpage-server-rendering.R create mode 100644 tests/testthat/test-loadpage-server-rendering.R diff --git a/R/constants.R b/R/constants.R index 5305820..d601baa 100644 --- a/R/constants.R +++ b/R/constants.R @@ -61,6 +61,34 @@ CONSTANTS_STATMODEL = list( plot_type_qq_plot = "QQPlots" # QQPlots — matches MSstats::groupComparisonQCPlots(type = "QQPlots") ) +NAMESPACE_LOADPAGE = list( + # Cross-module public IDs (read from outside the loadpage module). + bio = "BIO", + dda_dia = "DDA_DIA", + filetype = "filetype", + proceed1 = "proceed1", + # DIANN-cluster IDs migrated to server-side show/hide in Phase 1. + big_file_diann = "big_file_diann", + big_diann_calculate_anomaly_scores = "big_diann_calculate_anomaly_scores", + big_diann_run_order_file = "big_diann_run_order_file", + diann_2plus = "diann_2plus", + intensity_column = "intensity_column", + q_val = "q_val", + q_cutoff = "q_cutoff", + mbr = "MBR", + diann_calculate_anomaly_scores = "diann_calculate_anomaly_scores", + diann_run_order_file = "diann_run_order_file", + # Visibility container IDs introduced by the Phase 1 migration. + diann_lf_options_panel = "diann_lf_options_panel", + diann_intensity_column_panel = "diann_intensity_column_panel", + qval_filter_panel = "qval_filter_panel", + qval_cutoff_panel = "qval_cutoff_panel", + qval_mbr_panel = "qval_mbr_panel", + diann_anomaly_panel = "diann_anomaly_panel", + diann_anomaly_run_order_panel = "diann_anomaly_run_order_panel", + big_diann_anomaly_run_order_panel = "big_diann_anomaly_run_order_panel" +) + NAMESPACE_EXPDES = list( sidebar_controls = "sidebar_controls", protein_select = "protein_select", diff --git a/R/loadpage-server-rendering.R b/R/loadpage-server-rendering.R new file mode 100644 index 0000000..a564b01 --- /dev/null +++ b/R/loadpage-server-rendering.R @@ -0,0 +1,220 @@ +# ============================================================================ +# Loadpage — server-side visibility predicates and observer registration +# ============================================================================ +# +# Phase 1 scope: DIANN converter cluster only. These predicates and the +# observer-registration helper replace the JavaScript `conditionalPanel` +# expressions that previously gated four DIANN-related UI sections: +# +# 1. DIANN LType options block (diann_2plus toggle + intensity-column override) +# 2. Q-value filter block (shared with Skyline / Spectronaut) including the +# DIANN-specific MBR sub-panel +# 3. DIANN regular-path anomaly-scoring toggle + run-order fileInput +# 4. DIANN big-file-path anomaly-scoring run-order fileInput +# +# All four panels migrate to `shinyjs::hidden(div(id = ns(...), ...))` in the +# UI plus an `observe({ shinyjs::toggle(...) })` block here. Reason: panels +# contain inputs whose state must persist across visibility flips (an +# intensity-column text, a Q-value numeric, a run-order file, etc.). `renderUI` +# would rebuild the inputs at defaults whenever the driving reactive changed, +# which is a behavior change vs the original `conditionalPanel` (which kept +# the DOM mounted) and would also cause `getData(input)` in R/utils.R to read +# `NULL` for hidden inputs at proceed time. +# +# All helpers are internal (`@noRd`); kept pure (no Shiny reactivity) so they +# can be exercised with truth-table tests. + + +#' Should the DIANN LType options block be visible? +#' +#' Mirrors: +#' filetype == 'diann' && DDA_DIA == 'LType' && !big_file_diann +#' +#' @param filetype value of `input$filetype` +#' @param dda_dia value of `input$DDA_DIA` +#' @param big_file_diann value of `input$big_file_diann` +#' @noRd +loadpage_show_diann_lf_options <- function(filetype, dda_dia, big_file_diann) { + isTRUE(filetype == "diann") && + isTRUE(dda_dia == "LType") && + !isTRUE(big_file_diann) +} + +#' Should the DIANN intensity-column override be visible? +#' +#' Shown only when the user has indicated DIANN < 2.0 by leaving the +#' `diann_2plus` checkbox unchecked. Parent panel +#' (`loadpage_show_diann_lf_options`) must already be visible — when it is +#' hidden the CSS cascade hides this sub-panel regardless of this predicate. +#' +#' @param diann_2plus value of `input$diann_2plus` +#' @noRd +loadpage_show_diann_intensity_column <- function(diann_2plus) { + !isTRUE(diann_2plus) +} + +#' Should the Q-value filter section be visible? +#' +#' Shared across Skyline, Spectronaut, and DIANN (regular path). +#' +#' Mirrors: +#' filetype == 'sky' || filetype == 'spec' || +#' (filetype == 'diann' && !big_file_diann) +#' +#' @param filetype value of `input$filetype` +#' @param big_file_diann value of `input$big_file_diann` +#' @noRd +loadpage_show_qval_filter <- function(filetype, big_file_diann) { + isTRUE(filetype == "sky") || + isTRUE(filetype == "spec") || + (isTRUE(filetype == "diann") && !isTRUE(big_file_diann)) +} + +#' Should the Q-value cutoff + MBR sub-section be visible? +#' +#' Inside the Q-value filter section; gated by the `q_val` checkbox. +#' +#' @param q_val value of `input$q_val` +#' @noRd +loadpage_show_qval_cutoff <- function(q_val) { + isTRUE(q_val) +} + +#' Should the DIANN MBR checkbox be visible? +#' +#' Only relevant for DIANN once the user has enabled Q-value filtering. +#' +#' @param q_val value of `input$q_val` +#' @param filetype value of `input$filetype` +#' @noRd +loadpage_show_diann_mbr <- function(q_val, filetype) { + isTRUE(q_val) && isTRUE(filetype == "diann") +} + +#' Should the DIANN regular-path anomaly-scoring section be visible? +#' +#' Mirrors: +#' filetype == 'diann' && !big_file_diann +#' +#' @param filetype value of `input$filetype` +#' @param big_file_diann value of `input$big_file_diann` +#' @noRd +loadpage_show_diann_anomaly <- function(filetype, big_file_diann) { + isTRUE(filetype == "diann") && !isTRUE(big_file_diann) +} + +#' Should the DIANN regular-path anomaly run-order fileInput be visible? +#' +#' Inside the regular-path anomaly section; gated by the anomaly checkbox. +#' +#' @param diann_calculate_anomaly_scores value of `input$diann_calculate_anomaly_scores` +#' @noRd +loadpage_show_diann_anomaly_run_order <- function(diann_calculate_anomaly_scores) { + isTRUE(diann_calculate_anomaly_scores) +} + +#' Should the DIANN big-file-path anomaly run-order fileInput be visible? +#' +#' Inside the big-file annotation block (itself only rendered when +#' `big_file_diann` is TRUE and `is_web_server` is FALSE); gated by the +#' big-file anomaly checkbox. +#' +#' @param big_diann_calculate_anomaly_scores value of +#' `input$big_diann_calculate_anomaly_scores` +#' @noRd +loadpage_show_big_diann_anomaly_run_order <- function(big_diann_calculate_anomaly_scores) { + isTRUE(big_diann_calculate_anomaly_scores) +} + + +#' Register DIANN-cluster visibility observers inside the loadpage module +#' +#' Adds one `observe({ shinyjs::toggle(...) })` block per migrated panel. +#' Call once from `loadpageServer`'s `moduleServer` scope so the observers +#' inherit the module session and `shinyjs` uses raw (unnamespaced) IDs. +#' +#' @param input the Shiny module's `input` object +#' @param session the Shiny module's `session` (unused for now but kept for +#' future-proofing — `shinyjs::toggle` already uses the current reactive +#' domain) +#' @noRd +register_diann_visibility_observers <- function(input, session) { + observe({ + shinyjs::toggle( + NAMESPACE_LOADPAGE$diann_lf_options_panel, + condition = loadpage_show_diann_lf_options( + input[[NAMESPACE_LOADPAGE$filetype]], + input[[NAMESPACE_LOADPAGE$dda_dia]], + input[[NAMESPACE_LOADPAGE$big_file_diann]] + ) + ) + }) + + observe({ + shinyjs::toggle( + NAMESPACE_LOADPAGE$diann_intensity_column_panel, + condition = loadpage_show_diann_intensity_column( + input[[NAMESPACE_LOADPAGE$diann_2plus]] + ) + ) + }) + + observe({ + shinyjs::toggle( + NAMESPACE_LOADPAGE$qval_filter_panel, + condition = loadpage_show_qval_filter( + input[[NAMESPACE_LOADPAGE$filetype]], + input[[NAMESPACE_LOADPAGE$big_file_diann]] + ) + ) + }) + + observe({ + shinyjs::toggle( + NAMESPACE_LOADPAGE$qval_cutoff_panel, + condition = loadpage_show_qval_cutoff( + input[[NAMESPACE_LOADPAGE$q_val]] + ) + ) + }) + + observe({ + shinyjs::toggle( + NAMESPACE_LOADPAGE$qval_mbr_panel, + condition = loadpage_show_diann_mbr( + input[[NAMESPACE_LOADPAGE$q_val]], + input[[NAMESPACE_LOADPAGE$filetype]] + ) + ) + }) + + observe({ + shinyjs::toggle( + NAMESPACE_LOADPAGE$diann_anomaly_panel, + condition = loadpage_show_diann_anomaly( + input[[NAMESPACE_LOADPAGE$filetype]], + input[[NAMESPACE_LOADPAGE$big_file_diann]] + ) + ) + }) + + observe({ + shinyjs::toggle( + NAMESPACE_LOADPAGE$diann_anomaly_run_order_panel, + condition = loadpage_show_diann_anomaly_run_order( + input[[NAMESPACE_LOADPAGE$diann_calculate_anomaly_scores]] + ) + ) + }) + + observe({ + shinyjs::toggle( + NAMESPACE_LOADPAGE$big_diann_anomaly_run_order_panel, + condition = loadpage_show_big_diann_anomaly_run_order( + input[[NAMESPACE_LOADPAGE$big_diann_calculate_anomaly_scores]] + ) + ) + }) + + invisible(NULL) +} diff --git a/R/module-loadpage-server.R b/R/module-loadpage-server.R index 4c00602..18271f4 100644 --- a/R/module-loadpage-server.R +++ b/R/module-loadpage-server.R @@ -18,6 +18,9 @@ loadpageServer <- function(id, parent_session, is_web_server = FALSE, app_templa condition_metadata <- reactiveVal(NULL) + # DIANN-cluster visibility: observers replace the JS conditionalPanels + register_diann_visibility_observers(input, session) + # == shinyFiles LOGIC FOR LOCAL FILE BROWSER ================================= # Define volumes for the file selection. if (!is_web_server) { diff --git a/R/module-loadpage-ui.R b/R/module-loadpage-ui.R index e9f9292..e482b50 100644 --- a/R/module-loadpage-ui.R +++ b/R/module-loadpage-ui.R @@ -407,16 +407,21 @@ create_diann_large_annotation_ui <- function(ns, calculate_anomaly_def = FALSE) div("Carries Ms1ProfileCorr, Evidence, RT, and Predicted.RT through the out-of-memory steps, then engineers DeltaRT = RT - Predicted.RT in-memory after collect and fits MSstatsConvert::MSstatsAnomalyScores on c(Ms1ProfileCorr, Evidence, DeltaRT). Requires a run order CSV.", class = "icon-tooltip")), value = calculate_anomaly_def), - conditionalPanel( - condition = sprintf("input['%s']", ns("big_diann_calculate_anomaly_scores")), - fileInput(ns("big_diann_run_order_file"), + # Big-file-path anomaly run-order fileInput (visibility driven) + # server-side. Kept inside this helper (called from the diann_options_ui + # renderUI) so the fileInput is mounted whenever the parent big-file UI + # is. The matching observer in register_diann_visibility_observers + # toggles it on `big_diann_calculate_anomaly_scores`. + shinyjs::hidden(div( + id = ns(NAMESPACE_LOADPAGE$big_diann_anomaly_run_order_panel), + fileInput(ns(NAMESPACE_LOADPAGE$big_diann_run_order_file), label = h5("Upload Run Order File", class = "icon-wrapper", icon("question-circle", lib = "font-awesome"), div("CSV with two columns: 'Run' (sequence name matching the converter output) and 'Order' (chronological run number, e.g. 1, 2, 3...).", class = "icon-tooltip")), multiple = FALSE, accept = c(".csv")) - ) + )) ) } @@ -774,20 +779,21 @@ create_label_free_options <- function(ns) { create_quality_filtering_options(ns) ), - # DIANN specific options - conditionalPanel( - condition = "input['loadpage-filetype'] == 'diann' && input['loadpage-DDA_DIA'] == 'LType' && !input['loadpage-big_file_diann']", - checkboxInput(ns("diann_2plus"), "DIANN 2.0+", value = FALSE), - conditionalPanel( - condition = "!input['loadpage-diann_2plus']", - textInput(ns("intensity_column"), + # DIANN specific options — visibility driven server-side + # (R/loadpage-server-rendering.R::register_diann_visibility_observers). + shinyjs::hidden(div( + id = ns(NAMESPACE_LOADPAGE$diann_lf_options_panel), + checkboxInput(ns(NAMESPACE_LOADPAGE$diann_2plus), "DIANN 2.0+", value = FALSE), + shinyjs::hidden(div( + id = ns(NAMESPACE_LOADPAGE$diann_intensity_column_panel), + textInput(ns(NAMESPACE_LOADPAGE$intensity_column), h5("Intensity Column Name", class = "icon-wrapper", icon("question-circle", lib = "font-awesome"), div("Enter the column name containing intensity values for DIANN versions prior to 2.0", class = "icon-tooltip")), value = "FragmentQuantCorrected") - ), + )), uiOutput(ns("diann_turnover_ui")) - ) + )) ) } @@ -795,18 +801,22 @@ create_label_free_options <- function(ns) { #' @noRd create_quality_filtering_options <- function(ns) { tagList( - conditionalPanel( - condition = "input['loadpage-filetype'] == 'sky' || input['loadpage-filetype'] == 'spec'|| (input['loadpage-filetype'] == 'diann' && !input['loadpage-big_file_diann'])", - checkboxInput(ns("q_val"), "Filter with Q-value"), - conditionalPanel( - condition = "input['loadpage-q_val']", - conditionalPanel( - condition = "input['loadpage-filetype'] == 'diann'", - checkboxInput(ns("MBR"), "MBR Enabled", value = FALSE) - ), - numericInput(ns("q_cutoff"), "Q-value cutoff", 0.01, 0, 1, 0.01) - ) - ), + # Q-value filter (Skyline / Spectronaut / DIANN regular) — visibility + # driven server-side. MBR is a DIANN-only sub-checkbox inside the cutoff + # block. State must persist across visibility flips, so we use nested + # hidden divs and observers, not renderUI. + shinyjs::hidden(div( + id = ns(NAMESPACE_LOADPAGE$qval_filter_panel), + checkboxInput(ns(NAMESPACE_LOADPAGE$q_val), "Filter with Q-value"), + shinyjs::hidden(div( + id = ns(NAMESPACE_LOADPAGE$qval_cutoff_panel), + shinyjs::hidden(div( + id = ns(NAMESPACE_LOADPAGE$qval_mbr_panel), + checkboxInput(ns(NAMESPACE_LOADPAGE$mbr), "MBR Enabled", value = FALSE) + )), + numericInput(ns(NAMESPACE_LOADPAGE$q_cutoff), "Q-value cutoff", 0.01, 0, 1, 0.01) + )) + )), conditionalPanel( condition = "input['loadpage-filetype'] == 'spec'", @@ -838,9 +848,12 @@ create_quality_filtering_options <- function(ns) { # DIANNtoMSstatsFormat when calculateAnomalyScores = TRUE) can do # temporal feature engineering on Ms1ProfileCorr, Evidence, and # DeltaRT. - conditionalPanel( - condition = "input['loadpage-filetype'] == 'diann' && !input['loadpage-big_file_diann']", - checkboxInput(ns("diann_calculate_anomaly_scores"), + # DIANN regular-path anomaly scoring — visibility driven server-side. + # The run-order fileInput must stay mounted across checkbox toggles or + # the uploaded file would be lost. + shinyjs::hidden(div( + id = ns(NAMESPACE_LOADPAGE$diann_anomaly_panel), + checkboxInput(ns(NAMESPACE_LOADPAGE$diann_calculate_anomaly_scores), label = tags$span( "Calculate Anomaly Scores", class = "icon-wrapper", @@ -849,15 +862,15 @@ create_quality_filtering_options <- function(ns) { class = "icon-tooltip") ), value = FALSE), - conditionalPanel( - condition = "input['loadpage-diann_calculate_anomaly_scores']", - fileInput(ns("diann_run_order_file"), + shinyjs::hidden(div( + id = ns(NAMESPACE_LOADPAGE$diann_anomaly_run_order_panel), + fileInput(ns(NAMESPACE_LOADPAGE$diann_run_order_file), label = h5("Upload Run Order File", class = "icon-wrapper", icon("question-circle", lib = "font-awesome"), div("CSV with two columns: 'Run' (sequence name matching the DIANN report's Run column) and 'Order' (chronological run number, e.g. 1, 2, 3...).", class = "icon-tooltip")), multiple = FALSE, accept = c(".csv")) - ) - ), + )) + )), conditionalPanel( condition = "input['loadpage-filetype'] == 'open'", diff --git a/tests/testthat/test-loadpage-server-rendering.R b/tests/testthat/test-loadpage-server-rendering.R new file mode 100644 index 0000000..c96edca --- /dev/null +++ b/tests/testthat/test-loadpage-server-rendering.R @@ -0,0 +1,109 @@ +# Truth-table tests for the DIANN-cluster visibility predicates extracted in +# Phase 1 of the loadpage refactor. Each predicate is a pure transform of a +# few Shiny input values — these tests pin the JS-condition semantics that +# previously lived in conditionalPanel(condition = "...") expressions in +# module-loadpage-ui.R. + +test_that("loadpage_show_diann_lf_options is TRUE only when diann + LType + small-file", { + expect_true( + MSstatsShiny:::loadpage_show_diann_lf_options("diann", "LType", FALSE) + ) + expect_true( + MSstatsShiny:::loadpage_show_diann_lf_options("diann", "LType", NULL) + ) + + # wrong converter + expect_false( + MSstatsShiny:::loadpage_show_diann_lf_options("sky", "LType", FALSE) + ) + expect_false( + MSstatsShiny:::loadpage_show_diann_lf_options("spec", "LType", FALSE) + ) + # wrong label type + expect_false( + MSstatsShiny:::loadpage_show_diann_lf_options("diann", "TMT", FALSE) + ) + # big-file mode active + expect_false( + MSstatsShiny:::loadpage_show_diann_lf_options("diann", "LType", TRUE) + ) +}) + +test_that("loadpage_show_diann_intensity_column inverts diann_2plus", { + expect_true(MSstatsShiny:::loadpage_show_diann_intensity_column(FALSE)) + expect_true(MSstatsShiny:::loadpage_show_diann_intensity_column(NULL)) + expect_false(MSstatsShiny:::loadpage_show_diann_intensity_column(TRUE)) +}) + +test_that("loadpage_show_qval_filter is TRUE for sky/spec and diann-small-file", { + expect_true(MSstatsShiny:::loadpage_show_qval_filter("sky", FALSE)) + expect_true(MSstatsShiny:::loadpage_show_qval_filter("sky", TRUE)) # sky doesn't read big_file_diann + expect_true(MSstatsShiny:::loadpage_show_qval_filter("spec", FALSE)) + expect_true(MSstatsShiny:::loadpage_show_qval_filter("spec", TRUE)) # spec ignores big_file_diann + + expect_true(MSstatsShiny:::loadpage_show_qval_filter("diann", FALSE)) + expect_false(MSstatsShiny:::loadpage_show_qval_filter("diann", TRUE)) + + expect_false(MSstatsShiny:::loadpage_show_qval_filter("maxq", FALSE)) + expect_false(MSstatsShiny:::loadpage_show_qval_filter("PD", FALSE)) + expect_false(MSstatsShiny:::loadpage_show_qval_filter("open", FALSE)) + expect_false(MSstatsShiny:::loadpage_show_qval_filter(NULL, FALSE)) +}) + +test_that("loadpage_show_qval_cutoff is the q_val checkbox", { + expect_true(MSstatsShiny:::loadpage_show_qval_cutoff(TRUE)) + expect_false(MSstatsShiny:::loadpage_show_qval_cutoff(FALSE)) + expect_false(MSstatsShiny:::loadpage_show_qval_cutoff(NULL)) +}) + +test_that("loadpage_show_diann_mbr requires both q_val and diann", { + expect_true(MSstatsShiny:::loadpage_show_diann_mbr(TRUE, "diann")) + + expect_false(MSstatsShiny:::loadpage_show_diann_mbr(FALSE, "diann")) + expect_false(MSstatsShiny:::loadpage_show_diann_mbr(NULL, "diann")) + expect_false(MSstatsShiny:::loadpage_show_diann_mbr(TRUE, "sky")) + expect_false(MSstatsShiny:::loadpage_show_diann_mbr(TRUE, "spec")) + expect_false(MSstatsShiny:::loadpage_show_diann_mbr(TRUE, NULL)) +}) + +test_that("loadpage_show_diann_anomaly is TRUE for diann + small-file only", { + expect_true(MSstatsShiny:::loadpage_show_diann_anomaly("diann", FALSE)) + expect_true(MSstatsShiny:::loadpage_show_diann_anomaly("diann", NULL)) + + expect_false(MSstatsShiny:::loadpage_show_diann_anomaly("diann", TRUE)) + expect_false(MSstatsShiny:::loadpage_show_diann_anomaly("sky", FALSE)) + expect_false(MSstatsShiny:::loadpage_show_diann_anomaly("spec", FALSE)) + expect_false(MSstatsShiny:::loadpage_show_diann_anomaly(NULL, FALSE)) +}) + +test_that("loadpage_show_diann_anomaly_run_order is the anomaly checkbox", { + expect_true(MSstatsShiny:::loadpage_show_diann_anomaly_run_order(TRUE)) + expect_false(MSstatsShiny:::loadpage_show_diann_anomaly_run_order(FALSE)) + expect_false(MSstatsShiny:::loadpage_show_diann_anomaly_run_order(NULL)) +}) + +test_that("loadpage_show_big_diann_anomaly_run_order is the big-file anomaly checkbox", { + expect_true(MSstatsShiny:::loadpage_show_big_diann_anomaly_run_order(TRUE)) + expect_false(MSstatsShiny:::loadpage_show_big_diann_anomaly_run_order(FALSE)) + expect_false(MSstatsShiny:::loadpage_show_big_diann_anomaly_run_order(NULL)) +}) + +test_that("NAMESPACE_LOADPAGE retains literal string values (no renames in Phase 1)", { + expect_equal(NAMESPACE_LOADPAGE$bio, "BIO") + expect_equal(NAMESPACE_LOADPAGE$dda_dia, "DDA_DIA") + expect_equal(NAMESPACE_LOADPAGE$filetype, "filetype") + expect_equal(NAMESPACE_LOADPAGE$proceed1, "proceed1") + expect_equal(NAMESPACE_LOADPAGE$q_val, "q_val") + expect_equal(NAMESPACE_LOADPAGE$q_cutoff, "q_cutoff") + expect_equal(NAMESPACE_LOADPAGE$mbr, "MBR") + expect_equal(NAMESPACE_LOADPAGE$intensity_column, "intensity_column") + expect_equal(NAMESPACE_LOADPAGE$diann_2plus, "diann_2plus") + expect_equal(NAMESPACE_LOADPAGE$diann_calculate_anomaly_scores, + "diann_calculate_anomaly_scores") + expect_equal(NAMESPACE_LOADPAGE$diann_run_order_file, "diann_run_order_file") + expect_equal(NAMESPACE_LOADPAGE$big_file_diann, "big_file_diann") + expect_equal(NAMESPACE_LOADPAGE$big_diann_calculate_anomaly_scores, + "big_diann_calculate_anomaly_scores") + expect_equal(NAMESPACE_LOADPAGE$big_diann_run_order_file, + "big_diann_run_order_file") +}) From c21f5f62dc8cab1eeeda2f3a5f49e769aca9346c Mon Sep 17 00:00:00 2001 From: Swaraj Patil Date: Thu, 25 Jun 2026 12:01:11 -0400 Subject: [PATCH 2/4] loadpage: - move converter panels to server-side show/hide - split the server file into smaller helpers --- R/constants.R | 35 +- R/loadpage-server-data-loaders.R | 177 ++++ R/loadpage-server-preview.R | 143 +++ R/loadpage-server-proceed-validation.R | 127 +++ R/loadpage-server-rendering.R | 867 ++++++++++++++++-- R/loadpage-server-summary.R | 235 +++++ R/module-loadpage-server.R | 819 ++--------------- R/module-loadpage-ui.R | 340 ++++--- .../testthat/test-loadpage-server-rendering.R | 386 ++++++++ tests/testthat/test-module-loadpage-ui.R | 217 ++++- 10 files changed, 2299 insertions(+), 1047 deletions(-) create mode 100644 R/loadpage-server-data-loaders.R create mode 100644 R/loadpage-server-preview.R create mode 100644 R/loadpage-server-proceed-validation.R create mode 100644 R/loadpage-server-summary.R diff --git a/R/constants.R b/R/constants.R index d601baa..e215aae 100644 --- a/R/constants.R +++ b/R/constants.R @@ -78,7 +78,13 @@ NAMESPACE_LOADPAGE = list( mbr = "MBR", diann_calculate_anomaly_scores = "diann_calculate_anomaly_scores", diann_run_order_file = "diann_run_order_file", - # Visibility container IDs introduced by the Phase 1 migration. + # Driver IDs introduced (i.e. centralized) in Phase 2. + big_file_spec = "big_file_spec", + label_free_type = "LabelFreeType", + calculate_anomaly_scores = "calculate_anomaly_scores", + m_score = "m_score", + which_proteinid = "which.proteinid", + # Phase 1 container IDs (visibility divs). diann_lf_options_panel = "diann_lf_options_panel", diann_intensity_column_panel = "diann_intensity_column_panel", qval_filter_panel = "qval_filter_panel", @@ -86,7 +92,32 @@ NAMESPACE_LOADPAGE = list( qval_mbr_panel = "qval_mbr_panel", diann_anomaly_panel = "diann_anomaly_panel", diann_anomaly_run_order_panel = "diann_anomaly_run_order_panel", - big_diann_anomaly_run_order_panel = "big_diann_anomaly_run_order_panel" + big_diann_anomaly_run_order_panel = "big_diann_anomaly_run_order_panel", + # Phase 2 container IDs (visibility divs introduced by the broader sweep). + sample_dda_description_panel = "sample_dda_description_panel", + sample_dia_description_panel = "sample_dia_description_panel", + sample_srm_prm_description_panel = "sample_srm_prm_description_panel", + label_free_type_selection_panel = "label_free_type_selection_panel", + standard_quant_upload_panel = "standard_quant_upload_panel", + standard_annot_upload_panel = "standard_annot_upload_panel", + msstats_regular_upload_panel = "msstats_regular_upload_panel", + msstats_ptm_upload_panel = "msstats_ptm_upload_panel", + skyline_upload_panel = "skyline_upload_panel", + ptm_fragpipe_upload_panel = "ptm_fragpipe_upload_panel", + maxquant_upload_panel = "maxquant_upload_panel", + ptm_uploads_panel = "ptm_uploads_panel", + ptm_maxquant_pgroup_panel = "ptm_maxquant_pgroup_panel", + ptm_metamorpheus_extras_panel = "ptm_metamorpheus_extras_panel", + ptm_fasta_id_column_panel = "ptm_fasta_id_column_panel", + ptm_mod_id_maxq_panel = "ptm_mod_id_maxq_panel", + ptm_mod_id_pd_panel = "ptm_mod_id_pd_panel", + ptm_mod_id_spec_panel = "ptm_mod_id_spec_panel", + dia_umpire_upload_panel = "dia_umpire_upload_panel", + label_free_options_panel = "label_free_options_panel", + openswath_mscore_panel = "openswath_mscore_panel", + openswath_mscore_cutoff_panel = "openswath_mscore_cutoff_panel", + # Phase 2 renderUI slot — the TMT which.proteinid duplicate-ns()-id case. + tmt_options_ui = "tmt_options_ui" ) NAMESPACE_EXPDES = list( diff --git a/R/loadpage-server-data-loaders.R b/R/loadpage-server-data-loaders.R new file mode 100644 index 0000000..03fa770 --- /dev/null +++ b/R/loadpage-server-data-loaders.R @@ -0,0 +1,177 @@ +# ============================================================================ +# Loadpage — data-loading reactives + download MSstats handler + summaries +# ============================================================================ +# +# Extracted from R/module-loadpage-server.R by the Phase 2 server split. +# Pure cut-and-paste: no behavior change, no reactivity timing change, no +# input-ID renames. Owns: +# - 11 single-file wrapper reactives (`get_annot`, `get_annot1/2/3`, +# `get_evidence`, `get_evidence2`, `get_global`, `get_proteinGroups`, +# `get_proteinGroups2`, `get_FragSummary`, `get_peptideSummary`, +# `get_protSummary`, `get_maxq_ptm_sites`) +# - the lynchpin `get_data` eventReactive (triggered on `proceed1`) +# - the download_msstats_format downloadHandler + its enable/disable +# observers +# - `get_data_code` (triggered on `calculate`) +# - `get_summary1`, `get_summary2` (triggered on `proceed1`) +# +# Returns a named list of reactives the summary helper and the orchestrator +# (for the public return value) read. + + +#' Register the loadpage data-loading reactives + download handler. +#' +#' @param input the Shiny module's `input` object +#' @param output the Shiny module's `output` object +#' @param session the Shiny module's `session` +#' @return named list with `get_data`, `get_annot`, `get_summary1`, +#' `get_summary2`, `get_data_code` (and the other single-file +#' wrappers if the orchestrator or any future helper needs +#' them) +#' @noRd +register_loadpage_data_loaders <- function(input, output, session) { + + get_annot <- eventReactive(input$proceed1, { + getAnnot(input) + }) + + get_annot1 <- reactive({ + getAnnot1(input) + }) + + get_annot2 <- reactive({ + getAnnot2(input) + }) + + get_annot3 <- reactive({ + getAnnot3(input) + }) + + get_evidence <- reactive({ + getEvidence(input) + }) + + get_evidence2 <- reactive({ + getEvidence2(input) + }) + + get_global <- reactive({ + getGlobal(input) + }) + + get_proteinGroups <- reactive({ + getProteinGroups(input) + }) + + get_proteinGroups2 <- reactive({ + getProteinGroups2(input) + }) + + get_FragSummary <- reactive({ + getFragSummary(input) + }) + + get_peptideSummary <- reactive({ + getPeptideSummary(input) + }) + + get_protSummary <- reactive({ + getProtSummary(input) + }) + + get_maxq_ptm_sites <- reactive({ + getMaxqPtmSites(input) + }) + + get_data <- eventReactive(input$proceed1, { + tryCatch( + getData(input), + error = function(e) { + tryCatch(remove_modal_spinner(), error = function(e2) NULL) + showNotification( + paste("Failed to load data:", conditionMessage(e)), + type = "error", duration = 12) + NULL + } + ) + }) + + observeEvent(input$proceed1, { + shinyjs::disable("download_msstats_format") + }) + + observeEvent(get_data(), { + req(get_data()) + shinyjs::enable("download_msstats_format") + }) + + output$download_msstats_format <- downloadHandler( + filename = function() { + data <- get_data() + if (inherits(data, "data.frame")) { + paste0("MSstats_format-", Sys.Date(), ".csv") + } else { + paste0("MSstats_format-", Sys.Date(), ".zip") + } + }, + content = function(file) { + tryCatch({ + data <- get_data() + if (inherits(data, "data.frame")) { + data.table::fwrite(data, file) + } else { + tmp_dir <- tempfile("msstats_format_") + dir.create(tmp_dir) + on.exit(unlink(tmp_dir, recursive = TRUE), add = TRUE) + tmp_files <- character() + for (nm in names(data)) { + tbl <- data[[nm]] + if (is.null(tbl)) next + if (NROW(tbl) == 0L) next + tmp_path <- file.path(tmp_dir, paste0(nm, ".csv")) + data.table::fwrite(tbl, tmp_path) + tmp_files <- c(tmp_files, tmp_path) + } + if (length(tmp_files) == 0L) { + stop("No non-empty tables available to export.") + } + utils::zip(zipfile = file, files = tmp_files, flags = "-j") + } + }, error = function(e) { + writeLines(paste("Failed to export MSstats format:", conditionMessage(e)), file) + }) + } + ) + + get_data_code <- eventReactive(input$calculate, { + getDataCode(input) + }) + + get_summary1 <- eventReactive(input$proceed1, { + getSummary1(input, get_data(), get_annot()) + }) + + get_summary2 <- eventReactive(input$proceed1, { + getSummary2(input, get_data()) + }) + + list( + get_annot = get_annot, + get_annot1 = get_annot1, + get_annot2 = get_annot2, + get_annot3 = get_annot3, + get_evidence = get_evidence, + get_evidence2 = get_evidence2, + get_global = get_global, + get_proteinGroups = get_proteinGroups, + get_proteinGroups2 = get_proteinGroups2, + get_FragSummary = get_FragSummary, + get_peptideSummary = get_peptideSummary, + get_protSummary = get_protSummary, + get_maxq_ptm_sites = get_maxq_ptm_sites, + get_data = get_data, + get_data_code = get_data_code, + get_summary1 = get_summary1, + get_summary2 = get_summary2 + ) +} diff --git a/R/loadpage-server-preview.R b/R/loadpage-server-preview.R new file mode 100644 index 0000000..bac4d58 --- /dev/null +++ b/R/loadpage-server-preview.R @@ -0,0 +1,143 @@ +# ============================================================================ +# Loadpage — preview data + DIANN auto-detection + Metamorpheus mod-ID UI +# ============================================================================ +# +# Extracted from R/module-loadpage-server.R by the Phase 2 server split. +# Pure cut-and-paste: no behavior change, no reactivity timing change, no +# input-ID renames. Owns: +# - `preview_data` reactiveVal (first 100 rows of the selected file) +# - `last_detected_diann_format` reactiveVal +# - `main_data_file` reactive (filetype → fileInput reactive) +# - the preview-fetch observer +# - the DIANN 2.0+ auto-toggle observer +# - the manual-override mismatch warning observeEvent +# - `output$mod_id_meta_ui` renderUI (depends on preview_data) +# - `output$mod_id_meta_other_input` renderUI +# +# Returns the `preview_data` reactive (invisibly) so future helpers can read +# it if needed. + + +#' Register the loadpage preview cluster. +#' +#' @param input the Shiny module's `input` object +#' @param output the Shiny module's `output` object +#' @param session the Shiny module's `session` +#' @return `preview_data` reactiveVal (invisibly) +#' @noRd +register_loadpage_preview <- function(input, output, session) { + preview_data <- reactiveVal(NULL) + last_detected_diann_format <- reactiveVal(NULL) + + # Determine the main data file based on current selections + # TODO: Add preview mappings for remaining PTM file types (PD, spec, sky, maxq) + # once preview-based UI features are extended beyond Metamorpheus. + main_data_file <- reactive({ + req(input$filetype) + if (input$BIO == "PTM") { + switch(input$filetype, + "meta" = input$ptm_input, + # TODO: "maxq" = input$ptm_input, + # TODO: "PD" = input$ptm_input, + # TODO: "spec" = input$ptm_input, + # TODO: "sky" = input$ptm_input, + # TODO: "phil" = input$ptmdata, + # TODO: "msstats" = input$msstatsptmdata, + NULL + ) + } else { + switch(input$filetype, + # TODO: Map remaining non-PTM file types when preview features are needed + "prog" =, "PD" =, "open" =, "openms" =, "spmin" =, "phil" =, "meta" = input$data, + "msstats" = input$msstatsdata, + "sky" = input$skylinedata, + "spec" = input$specdata, + "diann" = input$dianndata, + "maxq" = input$evidence, + NULL + ) + } + }) + + # Read first 100 rows for preview-based UI features. + # Supported: Metamorpheus PTM (modification ID dropdown), DIANN (version auto-detection). + # TODO: Extend to other input formats (Spectronaut, MaxQuant) as needed. + observe({ + should_preview <- (isTRUE(input$filetype == "meta") && isTRUE(input$BIO == "PTM")) || + (isTRUE(input$filetype == "diann") && isTRUE(input$BIO != "PTM")) + if (should_preview) { + file_info <- main_data_file() + if (!is.null(file_info)) { + # Reset DIANN detection tracker so a new file re-triggers the notification + last_detected_diann_format(NULL) + preview <- .read_preview(file_info$datapath, file_info$name) + if (is.null(preview)) { + showNotification("Could not preview file. Please verify the file format.", + type = "warning", duration = 5) + } + preview_data(preview) + } else { + preview_data(NULL) + } + } else { + preview_data(NULL) + } + }) + + # Auto-toggle DIANN 2.0+ checkbox based on detected file format + observe({ + req(input$filetype == "diann", input$BIO != "PTM") + preview <- preview_data() + if (is.null(preview)) return() + + is_2plus <- .is_diann_2plus(preview) + previous <- last_detected_diann_format() + # Only update and notify when the detected state actually changes + if (is.null(previous) || previous != is_2plus) { + updateCheckboxInput(session, "diann_2plus", value = is_2plus) + if (is_2plus) { + showNotification("Detected DIANN 2.0+ format (per-fragment columns).", + type = "message", duration = 5) + } else { + showNotification("Detected DIANN 1.x format (legacy fragment column).", + type = "message", duration = 5) + } + last_detected_diann_format(is_2plus) + } + }) + + # Warn user if they manually set DIANN 2.0+ checkbox to a value that conflicts with detected format + observeEvent(input$diann_2plus, { + req(input$filetype == "diann", input$BIO != "PTM") + preview <- preview_data() + if (is.null(preview)) return() + detected_2plus <- .is_diann_2plus(preview) + if (isTRUE(input$diann_2plus) != detected_2plus) { + showNotification( + paste0("Warning: You've ", + if (isTRUE(input$diann_2plus)) "checked" else "unchecked", + " DIANN 2.0+, but the uploaded file appears to be ", + if (detected_2plus) "DIANN 2.0+ format" else "DIANN 1.x format", + ". This mismatch may cause upload to fail."), + type = "warning", duration = 10) + } + }, ignoreInit = TRUE) + + # ========= METAMORPHEUS PTM: Dynamic modification ID dropdown ========= + output$mod_id_meta_ui <- renderUI({ + ns <- session$ns + req(input$filetype == "meta", input$BIO == "PTM") + mods <- .extract_mod_ids_from_preview(preview_data()) + create_meta_mod_id_selector(ns, mods) + }) + + # Show manual text input when "Other" is selected (replaces conditionalPanel) + output$mod_id_meta_other_input <- renderUI({ + req(input$mod_id_meta_select == "__other__") + textInput(session$ns("mod_id_meta_custom"), + label = h5("Enter modification ID (e.g. [Common Biological:Phosphorylation on S])"), + value = "") + }) + + invisible(preview_data) +} diff --git a/R/loadpage-server-proceed-validation.R b/R/loadpage-server-proceed-validation.R new file mode 100644 index 0000000..374dfcb --- /dev/null +++ b/R/loadpage-server-proceed-validation.R @@ -0,0 +1,127 @@ +# ============================================================================ +# Loadpage — `proceed1` button enable/disable cascade +# ============================================================================ +# +# Extracted from R/module-loadpage-server.R by the Phase 2 server split. +# Pure cut-and-paste: the deeply nested `observe()` block that gates the +# Upload Data button against the active (BIO, DDA_DIA, filetype, file-upload +# state) combination is preserved verbatim. The two big-file path reactives +# (`local_big_file_path`, `local_big_diann_path`) originate in the +# shinyFiles block — they stay in the orchestrator and are passed in as +# function arguments here. + + +#' Register the `proceed1` enable cascade. +#' +#' @param input the Shiny module's `input` object +#' @param session the Shiny module's `session` (used implicitly +#' by `enable` / `disable` via the current +#' reactive domain) +#' @param local_big_file_path reactive returning the local path of the +#' Spectronaut big-file selection (NULL when not +#' in big-file mode or on the web server) +#' @param local_big_diann_path reactive returning the local path of the +#' DIANN big-file selection +#' @noRd +register_loadpage_proceed_validation <- function(input, session, + local_big_file_path, + local_big_diann_path) { + observe({ + disable("proceed1") + if (((input$BIO == "Protein") || (input$BIO == "Peptide"))) { + if (input$DDA_DIA == "LType") { + if ((!is.null(input$filetype) && length(input$filetype) > 0)) { + if (input$filetype == "sample") { + if (!is.null(input$LabelFreeType)) { + enable("proceed1") + } + } else if (input$filetype == "msstats") { + if (!is.null(input$msstatsdata)) { + enable("proceed1") + } + } else if (input$filetype == "sky") { + if (!is.null(input$skylinedata)) { + enable("proceed1") + } + } else if (input$filetype == "maxq") { + if (!is.null(input$evidence) && !is.null(input$pGroup)) { # && !is.null(input$annot1) + enable("proceed1") + } + } else if (input$filetype == "prog" || input$filetype == "PD" || input$filetype == "open" || input$filetype == "phil" || input$filetype == "meta") { + if (!is.null(input$data)) { + enable("proceed1") + } + } else if (input$filetype == "openms") { + if (!is.null(input$data)) { + enable("proceed1") + } + } else if (input$filetype == "spec") { + spec_regular_file_ok <- !isTRUE(input$big_file_spec) && !is.null(input$specdata) + spec_big_file_ok <- isTRUE(input$big_file_spec) && length(local_big_file_path()) > 0 + if (spec_regular_file_ok || spec_big_file_ok) { + enable("proceed1") + } + } else if (input$filetype == "ump") { + if (!is.null(input$fragSummary) && !is.null(input$peptideSummary) && !is.null(input$protSummary)) { #&& !is.null(input$annot2) + enable("proceed1") + } + } else if (input$filetype == "diann") { + diann_regular_file_ok <- !isTRUE(input$big_file_diann) && !is.null(input$dianndata) + diann_big_file_ok <- isTRUE(input$big_file_diann) && length(local_big_diann_path()) > 0 + if (diann_regular_file_ok || diann_big_file_ok) { + enable("proceed1") + } + } + } + } else if (input$DDA_DIA == "TMT") { + if ((!is.null(input$filetype) && length(input$filetype) > 0)) { + if (input$filetype == "sample" || input$filetype == "msstats") { + enable("proceed1") + } + if (input$filetype == "maxq") { + if (!is.null(input$evidence) && !is.null(input$pGroup)) { # && !is.null(input$annot1) + enable("proceed1") + } + } else if (input$filetype == "PD") { + if (!is.null(input$data)) { + enable("proceed1") + } + } else if (input$filetype == "openms") { + if (!is.null(input$data)) { + enable("proceed1") + } + } else if (input$filetype == "spmin" || input$filetype == "phil") { + if (!is.null(input$data)) { + enable("proceed1") + } + } + } + } + + } + else if ((input$BIO == "PTM")) { + if (input$DDA_DIA == "LType" || input$DDA_DIA == "TMT") { + if ((!is.null(input$filetype) && length(input$filetype) > 0)) { + if (input$filetype == "sample") { + enable("proceed1") + } else if (input$filetype == "msstats") { + if (!is.null(input$msstatsptmdata)) { + enable("proceed1") + } + } else if (input$filetype == "sky" || input$filetype == "maxq" || input$filetype == "spec" || input$filetype == "PD" || input$filetype == "meta") { + if (!is.null(input$ptm_input) && !is.null(input$fasta)) { # && !is.null(input$ptm_annot) + enable("proceed1") + } + } + else if (input$filetype == "phil") { + if (!is.null(input$ptmdata)) { # && !is.null(input$annotation) + enable("proceed1") + } + } + } + } + } + }) + + invisible(NULL) +} diff --git a/R/loadpage-server-rendering.R b/R/loadpage-server-rendering.R index a564b01..9ad9af5 100644 --- a/R/loadpage-server-rendering.R +++ b/R/loadpage-server-rendering.R @@ -2,37 +2,46 @@ # Loadpage — server-side visibility predicates and observer registration # ============================================================================ # -# Phase 1 scope: DIANN converter cluster only. These predicates and the -# observer-registration helper replace the JavaScript `conditionalPanel` -# expressions that previously gated four DIANN-related UI sections: +# Phase 1 (DIANN cluster) and Phase 2 (the rest) of the loadpage refactor that +# moved conditional UI off `conditionalPanel` and onto server-side +# `shinyjs::show/hide`. All container divs in `R/module-loadpage-ui.R` are +# mounted unconditionally via `shinyjs::hidden(div(id = ns(...), ...))`; +# `register_loadpage_visibility_observers` below installs one +# `observe({ shinyjs::toggle(...) })` per container, driven by a pure +# predicate. # -# 1. DIANN LType options block (diann_2plus toggle + intensity-column override) -# 2. Q-value filter block (shared with Skyline / Spectronaut) including the -# DIANN-specific MBR sub-panel -# 3. DIANN regular-path anomaly-scoring toggle + run-order fileInput -# 4. DIANN big-file-path anomaly-scoring run-order fileInput +# Why show/hide (the default), not renderUI: +# - panels contain inputs whose values must persist across visibility flips, +# - `R/utils.R::getData` / `getDataCode` read many of those input IDs by +# literal string at `input$proceed1`, and would see NULL on a destroyed- +# and-rebuilt input. # -# All four panels migrate to `shinyjs::hidden(div(id = ns(...), ...))` in the -# UI plus an `observe({ shinyjs::toggle(...) })` block here. Reason: panels -# contain inputs whose state must persist across visibility flips (an -# intensity-column text, a Q-value numeric, a run-order file, etc.). `renderUI` -# would rebuild the inputs at defaults whenever the driving reactive changed, -# which is a behavior change vs the original `conditionalPanel` (which kept -# the DOM mounted) and would also cause `getData(input)` in R/utils.R to read -# `NULL` for hidden inputs at proceed time. +# The one renderUI exception is the TMT `which.proteinid` text field: two +# pre-existing `conditionalPanel`s both declared `ns("which.proteinid")` with +# different defaults (one for PD, one for MaxQuant). Mounting both as hidden +# divs would deterministically collide on a single ns() id. The exception is +# implemented as `output[[tmt_options_ui]] <- renderUI({...})` below; the +# rebuild always preserves the user's current value via `isolate()` and only +# falls back to the converter-appropriate default on the very first build +# (when no user value exists yet). # -# All helpers are internal (`@noRd`); kept pure (no Shiny reactivity) so they -# can be exercised with truth-table tests. +# Two `conditionalPanel`s are intentionally NOT migrated and remain in the UI +# file (Spectronaut regular-path anomaly checkbox + nested run-order +# fileInput, see the carveout comments in `R/module-loadpage-ui.R`). They +# share `ns("calculate_anomaly_scores")` / `ns("run_order_file")` with the +# big-file Spectronaut helper that the pre-existing +# `output$spectronaut_options_ui` renderUI emits, and routing them to +# renderUI would lose the user's uploaded run-order CSV (fileInput state +# cannot be re-seeded on rebuild). +# +# All helpers are internal (`@noRd`); predicates are pure (no Shiny +# reactivity) so they can be exercised with truth-table tests. -#' Should the DIANN LType options block be visible? -#' -#' Mirrors: -#' filetype == 'diann' && DDA_DIA == 'LType' && !big_file_diann -#' -#' @param filetype value of `input$filetype` -#' @param dda_dia value of `input$DDA_DIA` -#' @param big_file_diann value of `input$big_file_diann` +# ---------------------------------------------------------------------------- +# Phase 1 predicates (DIANN cluster). Unchanged from the Phase 1 commit. +# ---------------------------------------------------------------------------- + #' @noRd loadpage_show_diann_lf_options <- function(filetype, dda_dia, big_file_diann) { isTRUE(filetype == "diann") && @@ -40,29 +49,11 @@ loadpage_show_diann_lf_options <- function(filetype, dda_dia, big_file_diann) { !isTRUE(big_file_diann) } -#' Should the DIANN intensity-column override be visible? -#' -#' Shown only when the user has indicated DIANN < 2.0 by leaving the -#' `diann_2plus` checkbox unchecked. Parent panel -#' (`loadpage_show_diann_lf_options`) must already be visible — when it is -#' hidden the CSS cascade hides this sub-panel regardless of this predicate. -#' -#' @param diann_2plus value of `input$diann_2plus` #' @noRd loadpage_show_diann_intensity_column <- function(diann_2plus) { !isTRUE(diann_2plus) } -#' Should the Q-value filter section be visible? -#' -#' Shared across Skyline, Spectronaut, and DIANN (regular path). -#' -#' Mirrors: -#' filetype == 'sky' || filetype == 'spec' || -#' (filetype == 'diann' && !big_file_diann) -#' -#' @param filetype value of `input$filetype` -#' @param big_file_diann value of `input$big_file_diann` #' @noRd loadpage_show_qval_filter <- function(filetype, big_file_diann) { isTRUE(filetype == "sky") || @@ -70,75 +61,271 @@ loadpage_show_qval_filter <- function(filetype, big_file_diann) { (isTRUE(filetype == "diann") && !isTRUE(big_file_diann)) } -#' Should the Q-value cutoff + MBR sub-section be visible? -#' -#' Inside the Q-value filter section; gated by the `q_val` checkbox. -#' -#' @param q_val value of `input$q_val` #' @noRd loadpage_show_qval_cutoff <- function(q_val) { isTRUE(q_val) } -#' Should the DIANN MBR checkbox be visible? -#' -#' Only relevant for DIANN once the user has enabled Q-value filtering. -#' -#' @param q_val value of `input$q_val` -#' @param filetype value of `input$filetype` #' @noRd loadpage_show_diann_mbr <- function(q_val, filetype) { isTRUE(q_val) && isTRUE(filetype == "diann") } -#' Should the DIANN regular-path anomaly-scoring section be visible? -#' -#' Mirrors: -#' filetype == 'diann' && !big_file_diann -#' -#' @param filetype value of `input$filetype` -#' @param big_file_diann value of `input$big_file_diann` #' @noRd loadpage_show_diann_anomaly <- function(filetype, big_file_diann) { isTRUE(filetype == "diann") && !isTRUE(big_file_diann) } -#' Should the DIANN regular-path anomaly run-order fileInput be visible? -#' -#' Inside the regular-path anomaly section; gated by the anomaly checkbox. -#' -#' @param diann_calculate_anomaly_scores value of `input$diann_calculate_anomaly_scores` #' @noRd loadpage_show_diann_anomaly_run_order <- function(diann_calculate_anomaly_scores) { isTRUE(diann_calculate_anomaly_scores) } -#' Should the DIANN big-file-path anomaly run-order fileInput be visible? -#' -#' Inside the big-file annotation block (itself only rendered when -#' `big_file_diann` is TRUE and `is_web_server` is FALSE); gated by the -#' big-file anomaly checkbox. -#' -#' @param big_diann_calculate_anomaly_scores value of -#' `input$big_diann_calculate_anomaly_scores` #' @noRd loadpage_show_big_diann_anomaly_run_order <- function(big_diann_calculate_anomaly_scores) { isTRUE(big_diann_calculate_anomaly_scores) } -#' Register DIANN-cluster visibility observers inside the loadpage module +# ---------------------------------------------------------------------------- +# Phase 2 predicates (everything else). Each mirrors a previous +# `conditionalPanel(condition = "...")` JS expression including the full +# ancestor chain for nested cases. +# ---------------------------------------------------------------------------- + +#' Sample dataset description (parameterized for DDA / DIA / SRM_PRM). +#' @noRd +loadpage_show_sample_dataset_description <- function(filetype, label_free_type, mode) { + isTRUE(filetype == "sample") && isTRUE(label_free_type == mode) +} + +#' The LabelFreeType radio (DDA / DIA / SRM_PRM picker) is itself shown only +#' for the sample-dataset label-free workflow. +#' @noRd +loadpage_show_label_free_type_selection <- function(bio, filetype, dda_dia) { + !isTRUE(bio == "PTM") && + isTRUE(filetype == "sample") && + isTRUE(dda_dia == "LType") +} + +#' Generic `data` fileInput section (used by non-PTM 10col / prog / PD / +#' open / openms / spmin / phil / meta converters). +#' @noRd +loadpage_show_standard_quant_upload <- function(filetype, bio) { + if (isTRUE(bio == "PTM")) return(FALSE) + isTRUE(filetype %in% c("10col", "prog", "PD", "open", "openms", + "spmin", "phil", "meta")) +} + +#' Generic `annot` fileInput section (Skyline / Progenesis / PD / +#' OpenSWATH / SpectroMine / FragPipe / Metamorpheus always, Spectronaut and +#' DIANN only outside their big-file paths). +#' @noRd +loadpage_show_standard_annot_upload <- function(filetype, bio, big_file_spec, big_file_diann) { + if (isTRUE(bio == "PTM")) return(FALSE) + if (isTRUE(filetype %in% c("sky", "prog", "PD", "open", "spmin", "phil", "meta"))) return(TRUE) + if (isTRUE(filetype == "spec") && !isTRUE(big_file_spec)) return(TRUE) + if (isTRUE(filetype == "diann") && !isTRUE(big_file_diann)) return(TRUE) + FALSE +} + +#' Pre-formatted MSstats CSV upload — label-free path only. +#' @noRd +loadpage_show_msstats_regular_upload <- function(filetype, bio, dda_dia) { + isTRUE(filetype == "msstats") && + !isTRUE(bio == "PTM") && + !isTRUE(dda_dia == "TMT") +} + +#' Pre-formatted MSstatsPTM CSV upload — PTM path only. (The original JS +#' condition had a redundant `|| (BIO=='PTM' && DDA_DIA=='TMT')` clause that +#' was tautologically true whenever `BIO=='PTM'`; it collapses away here.) +#' @noRd +loadpage_show_msstats_ptm_upload <- function(filetype, bio) { + isTRUE(filetype == "msstats") && isTRUE(bio == "PTM") +} + +#' @noRd +loadpage_show_skyline_upload <- function(filetype, bio) { + isTRUE(filetype == "sky") && !isTRUE(bio == "PTM") +} + +#' @noRd +loadpage_show_ptm_fragpipe_upload <- function(filetype, bio) { + isTRUE(filetype == "phil") && isTRUE(bio == "PTM") +} + +#' @noRd +loadpage_show_maxquant_upload <- function(filetype, bio, dda_dia) { + isTRUE(filetype == "maxq") && + !isTRUE(bio == "PTM") && + isTRUE(dda_dia %in% c("TMT", "LType")) +} + +#' Shared PTM uploads block (PTM Input / Annot / FASTA / Unmod Protein). +#' Original JS had a redundant `|| (BIO=='PTM' && DDA_DIA=='TMT')` term — +#' collapses to `BIO=='PTM' && filetype ∈ ...`. +#' @noRd +loadpage_show_ptm_uploads <- function(filetype, bio) { + isTRUE(bio == "PTM") && + isTRUE(filetype %in% c("maxq", "PD", "spec", "sky", "meta")) +} + +#' MaxQuant-specific PTM `proteinGroups.txt` upload. +#' @noRd +loadpage_show_ptm_maxquant_pgroup <- function(filetype, bio) { + isTRUE(filetype == "maxq") && isTRUE(bio == "PTM") +} + +#' Metamorpheus-specific PTM extras (`ptm_protein_annot` + dynamic mod-ID +#' selector). +#' @noRd +loadpage_show_ptm_metamorpheus_extras <- function(filetype, bio) { + isTRUE(filetype == "meta") && isTRUE(bio == "PTM") +} + +#' FASTA-column-name text input (same gate as `loadpage_show_ptm_uploads`). +#' @noRd +loadpage_show_ptm_fasta_id_column <- function(filetype, bio) { + loadpage_show_ptm_uploads(filetype, bio) +} + +#' @noRd +loadpage_show_ptm_mod_id_maxq <- function(filetype, bio) { + isTRUE(filetype == "maxq") && isTRUE(bio == "PTM") +} + +#' @noRd +loadpage_show_ptm_mod_id_pd <- function(filetype, bio) { + isTRUE(filetype == "PD") && isTRUE(bio == "PTM") +} + +#' @noRd +loadpage_show_ptm_mod_id_spec <- function(filetype, bio) { + isTRUE(filetype == "spec") && isTRUE(bio == "PTM") +} + +#' @noRd +loadpage_show_dia_umpire_upload <- function(filetype) { + isTRUE(filetype == "ump") +} + +#' Label-free options block (`unique_peptides`, `remove`). Suppressed for +#' the sample-data and big-file workflows. +#' @noRd +loadpage_show_label_free_options <- function(filetype, dda_dia, big_file_spec, big_file_diann) { + if (is.null(filetype) || !nzchar(filetype)) return(FALSE) + isTRUE(dda_dia == "LType") && + !isTRUE(filetype == "sample") && + !(isTRUE(filetype == "spec") && isTRUE(big_file_spec)) && + !(isTRUE(filetype == "diann") && isTRUE(big_file_diann)) +} + +#' OpenSWATH M-score filter section (parent of `mscore_cutoff`). +#' @noRd +loadpage_show_openswath_mscore <- function(filetype) { + isTRUE(filetype == "open") +} + +#' M-score numeric cutoff — nested under the M-score section. The predicate +#' AND-includes the parent's `filetype == 'open'` clause, NOT only the +#' immediate `m_score` driver, so the cutoff hides when the converter +#' changes even if the checkbox happens to still be TRUE. +#' @noRd +loadpage_show_openswath_mscore_cutoff <- function(filetype, m_score) { + isTRUE(filetype == "open") && isTRUE(m_score) +} + +#' TMT options section visibility — used inside the renderUI below to decide +#' whether to emit any UI at all. +#' @noRd +loadpage_show_tmt_options <- function(filetype, dda_dia) { + isTRUE(dda_dia == "TMT") && isTRUE(filetype %in% c("PD", "maxq")) +} + +#' Default `which.proteinid` value for the active TMT converter. Returns +#' NULL when neither PD nor MaxQuant is selected. +#' @noRd +loadpage_default_proteinid_for_filetype <- function(filetype) { + if (isTRUE(filetype == "PD")) return("Protein.Accessions") + if (isTRUE(filetype == "maxq")) return("Proteins") + NULL +} + +#' Compute the seed value for the TMT `which.proteinid` renderUI on a rebuild. +#' +#' Distinguishes "user never changed the default" from "user typed a custom +#' value" so converter switches apply the new converter's default in the +#' former case and carry the typed value in the latter. Pure — no Shiny +#' reactivity; the caller passes in `isolate(input[[which_proteinid]])` and +#' the previous filetype from a reactiveVal tracker. +#' +#' Rules (in order): +#' 1. `preserved_value` is NULL — first build, or rebuild after leaving TMT +#' entirely (the renderUI returned NULL, textInput unmounted) → apply +#' the incoming converter's default. +#' 2. Converter has NOT changed (`outgoing_filetype == incoming_filetype`) +#' → carry `preserved_value` verbatim. Covers re-renders that fire +#' without an actual converter switch. +#' 3. Converter changed AND `preserved_value` equals the OUTGOING +#' converter's default → user never typed → apply the incoming default. +#' 4. Converter changed AND `preserved_value` differs from the outgoing +#' default → user typed a custom value → carry it. +#' +#' Edge case: `outgoing_filetype` is NULL but `preserved_value` is non-NULL +#' (unusual race / pre-fill). Conservative: carry `preserved_value` rather +#' than clobber it with the incoming default. +#' +#' @param incoming_filetype the current `input$filetype` (PD or maxq) +#' @param outgoing_filetype the filetype from the previous renderUI call, +#' tracked in a reactiveVal; NULL on first build +#' @param preserved_value `isolate(input[[which_proteinid]])` — the value +#' of the textInput from the previous build, NULL when unmounted +#' @noRd +loadpage_seed_proteinid <- function(incoming_filetype, + outgoing_filetype, + preserved_value) { + incoming_default <- loadpage_default_proteinid_for_filetype(incoming_filetype) + + # Rule 1: first build / textInput was unmounted. + if (is.null(preserved_value)) { + return(incoming_default) + } + + # Rule 2: no converter change. + if (!is.null(outgoing_filetype) && + isTRUE(outgoing_filetype == incoming_filetype)) { + return(preserved_value) + } + + # Rule 3: converter switch with the outgoing default — user never typed. + outgoing_default <- loadpage_default_proteinid_for_filetype(outgoing_filetype) + if (!is.null(outgoing_default) && + isTRUE(preserved_value == outgoing_default)) { + return(incoming_default) + } + + # Rule 4 (and unknown-outgoing edge case): user typed something → carry. + preserved_value +} + + +# ---------------------------------------------------------------------------- +# Unified registration helper. +# ---------------------------------------------------------------------------- + +#' Register every loadpage visibility observer (Phase 1 + Phase 2) plus the +#' single TMT `which.proteinid` renderUI exception. #' -#' Adds one `observe({ shinyjs::toggle(...) })` block per migrated panel. -#' Call once from `loadpageServer`'s `moduleServer` scope so the observers -#' inherit the module session and `shinyjs` uses raw (unnamespaced) IDs. +#' Replaces Phase 1's `register_diann_visibility_observers`. Call once from +#' `loadpageServer`'s `moduleServer` scope. #' #' @param input the Shiny module's `input` object -#' @param session the Shiny module's `session` (unused for now but kept for -#' future-proofing — `shinyjs::toggle` already uses the current reactive -#' domain) +#' @param output the Shiny module's `output` object (for the TMT renderUI) +#' @param session the Shiny module's `session` (for `session$ns`) #' @noRd -register_diann_visibility_observers <- function(input, session) { +register_loadpage_visibility_observers <- function(input, output, session) { + # --- Phase 1: DIANN cluster ------------------------------------------------ observe({ shinyjs::toggle( NAMESPACE_LOADPAGE$diann_lf_options_panel, @@ -149,7 +336,6 @@ register_diann_visibility_observers <- function(input, session) { ) ) }) - observe({ shinyjs::toggle( NAMESPACE_LOADPAGE$diann_intensity_column_panel, @@ -158,7 +344,6 @@ register_diann_visibility_observers <- function(input, session) { ) ) }) - observe({ shinyjs::toggle( NAMESPACE_LOADPAGE$qval_filter_panel, @@ -168,7 +353,6 @@ register_diann_visibility_observers <- function(input, session) { ) ) }) - observe({ shinyjs::toggle( NAMESPACE_LOADPAGE$qval_cutoff_panel, @@ -177,7 +361,6 @@ register_diann_visibility_observers <- function(input, session) { ) ) }) - observe({ shinyjs::toggle( NAMESPACE_LOADPAGE$qval_mbr_panel, @@ -187,7 +370,6 @@ register_diann_visibility_observers <- function(input, session) { ) ) }) - observe({ shinyjs::toggle( NAMESPACE_LOADPAGE$diann_anomaly_panel, @@ -197,7 +379,6 @@ register_diann_visibility_observers <- function(input, session) { ) ) }) - observe({ shinyjs::toggle( NAMESPACE_LOADPAGE$diann_anomaly_run_order_panel, @@ -206,7 +387,6 @@ register_diann_visibility_observers <- function(input, session) { ) ) }) - observe({ shinyjs::toggle( NAMESPACE_LOADPAGE$big_diann_anomaly_run_order_panel, @@ -216,5 +396,512 @@ register_diann_visibility_observers <- function(input, session) { ) }) + # --- Phase 2: sample-dataset descriptions + LabelFreeType picker ---------- + observe({ + shinyjs::toggle( + NAMESPACE_LOADPAGE$sample_dda_description_panel, + condition = loadpage_show_sample_dataset_description( + input[[NAMESPACE_LOADPAGE$filetype]], + input[[NAMESPACE_LOADPAGE$label_free_type]], + "DDA" + ) + ) + }) + observe({ + shinyjs::toggle( + NAMESPACE_LOADPAGE$sample_dia_description_panel, + condition = loadpage_show_sample_dataset_description( + input[[NAMESPACE_LOADPAGE$filetype]], + input[[NAMESPACE_LOADPAGE$label_free_type]], + "DIA" + ) + ) + }) + observe({ + shinyjs::toggle( + NAMESPACE_LOADPAGE$sample_srm_prm_description_panel, + condition = loadpage_show_sample_dataset_description( + input[[NAMESPACE_LOADPAGE$filetype]], + input[[NAMESPACE_LOADPAGE$label_free_type]], + "SRM_PRM" + ) + ) + }) + observe({ + shinyjs::toggle( + NAMESPACE_LOADPAGE$label_free_type_selection_panel, + condition = loadpage_show_label_free_type_selection( + input[[NAMESPACE_LOADPAGE$bio]], + input[[NAMESPACE_LOADPAGE$filetype]], + input[[NAMESPACE_LOADPAGE$dda_dia]] + ) + ) + }) + + # --- Phase 2: non-PTM converter uploads ------------------------------------ + observe({ + shinyjs::toggle( + NAMESPACE_LOADPAGE$standard_quant_upload_panel, + condition = loadpage_show_standard_quant_upload( + input[[NAMESPACE_LOADPAGE$filetype]], + input[[NAMESPACE_LOADPAGE$bio]] + ) + ) + }) + observe({ + shinyjs::toggle( + NAMESPACE_LOADPAGE$standard_annot_upload_panel, + condition = loadpage_show_standard_annot_upload( + input[[NAMESPACE_LOADPAGE$filetype]], + input[[NAMESPACE_LOADPAGE$bio]], + input[[NAMESPACE_LOADPAGE$big_file_spec]], + input[[NAMESPACE_LOADPAGE$big_file_diann]] + ) + ) + }) + observe({ + shinyjs::toggle( + NAMESPACE_LOADPAGE$msstats_regular_upload_panel, + condition = loadpage_show_msstats_regular_upload( + input[[NAMESPACE_LOADPAGE$filetype]], + input[[NAMESPACE_LOADPAGE$bio]], + input[[NAMESPACE_LOADPAGE$dda_dia]] + ) + ) + }) + observe({ + shinyjs::toggle( + NAMESPACE_LOADPAGE$msstats_ptm_upload_panel, + condition = loadpage_show_msstats_ptm_upload( + input[[NAMESPACE_LOADPAGE$filetype]], + input[[NAMESPACE_LOADPAGE$bio]] + ) + ) + }) + observe({ + shinyjs::toggle( + NAMESPACE_LOADPAGE$skyline_upload_panel, + condition = loadpage_show_skyline_upload( + input[[NAMESPACE_LOADPAGE$filetype]], + input[[NAMESPACE_LOADPAGE$bio]] + ) + ) + }) + observe({ + shinyjs::toggle( + NAMESPACE_LOADPAGE$ptm_fragpipe_upload_panel, + condition = loadpage_show_ptm_fragpipe_upload( + input[[NAMESPACE_LOADPAGE$filetype]], + input[[NAMESPACE_LOADPAGE$bio]] + ) + ) + }) + observe({ + shinyjs::toggle( + NAMESPACE_LOADPAGE$maxquant_upload_panel, + condition = loadpage_show_maxquant_upload( + input[[NAMESPACE_LOADPAGE$filetype]], + input[[NAMESPACE_LOADPAGE$bio]], + input[[NAMESPACE_LOADPAGE$dda_dia]] + ) + ) + }) + + # --- Phase 2: PTM converter cluster --------------------------------------- + observe({ + shinyjs::toggle( + NAMESPACE_LOADPAGE$ptm_uploads_panel, + condition = loadpage_show_ptm_uploads( + input[[NAMESPACE_LOADPAGE$filetype]], + input[[NAMESPACE_LOADPAGE$bio]] + ) + ) + }) + observe({ + shinyjs::toggle( + NAMESPACE_LOADPAGE$ptm_maxquant_pgroup_panel, + condition = loadpage_show_ptm_maxquant_pgroup( + input[[NAMESPACE_LOADPAGE$filetype]], + input[[NAMESPACE_LOADPAGE$bio]] + ) + ) + }) + observe({ + shinyjs::toggle( + NAMESPACE_LOADPAGE$ptm_metamorpheus_extras_panel, + condition = loadpage_show_ptm_metamorpheus_extras( + input[[NAMESPACE_LOADPAGE$filetype]], + input[[NAMESPACE_LOADPAGE$bio]] + ) + ) + }) + observe({ + shinyjs::toggle( + NAMESPACE_LOADPAGE$ptm_fasta_id_column_panel, + condition = loadpage_show_ptm_fasta_id_column( + input[[NAMESPACE_LOADPAGE$filetype]], + input[[NAMESPACE_LOADPAGE$bio]] + ) + ) + }) + observe({ + shinyjs::toggle( + NAMESPACE_LOADPAGE$ptm_mod_id_maxq_panel, + condition = loadpage_show_ptm_mod_id_maxq( + input[[NAMESPACE_LOADPAGE$filetype]], + input[[NAMESPACE_LOADPAGE$bio]] + ) + ) + }) + observe({ + shinyjs::toggle( + NAMESPACE_LOADPAGE$ptm_mod_id_pd_panel, + condition = loadpage_show_ptm_mod_id_pd( + input[[NAMESPACE_LOADPAGE$filetype]], + input[[NAMESPACE_LOADPAGE$bio]] + ) + ) + }) + observe({ + shinyjs::toggle( + NAMESPACE_LOADPAGE$ptm_mod_id_spec_panel, + condition = loadpage_show_ptm_mod_id_spec( + input[[NAMESPACE_LOADPAGE$filetype]], + input[[NAMESPACE_LOADPAGE$bio]] + ) + ) + }) + + # --- Phase 2: DIA-Umpire + label-free options + OpenSWATH M-score --------- + observe({ + shinyjs::toggle( + NAMESPACE_LOADPAGE$dia_umpire_upload_panel, + condition = loadpage_show_dia_umpire_upload( + input[[NAMESPACE_LOADPAGE$filetype]] + ) + ) + }) + observe({ + shinyjs::toggle( + NAMESPACE_LOADPAGE$label_free_options_panel, + condition = loadpage_show_label_free_options( + input[[NAMESPACE_LOADPAGE$filetype]], + input[[NAMESPACE_LOADPAGE$dda_dia]], + input[[NAMESPACE_LOADPAGE$big_file_spec]], + input[[NAMESPACE_LOADPAGE$big_file_diann]] + ) + ) + }) + observe({ + shinyjs::toggle( + NAMESPACE_LOADPAGE$openswath_mscore_panel, + condition = loadpage_show_openswath_mscore( + input[[NAMESPACE_LOADPAGE$filetype]] + ) + ) + }) + observe({ + shinyjs::toggle( + NAMESPACE_LOADPAGE$openswath_mscore_cutoff_panel, + condition = loadpage_show_openswath_mscore_cutoff( + input[[NAMESPACE_LOADPAGE$filetype]], + input[[NAMESPACE_LOADPAGE$m_score]] + ) + ) + }) + + # --- Phase 2: TMT which.proteinid renderUI (the duplicate-ns()-id case) ---- + # + # Two `conditionalPanel`s previously declared the same ns("which.proteinid") + # with different defaults (PD: "Protein.Accessions", MaxQuant: "Proteins"). + # Mounting both as hidden divs would collide deterministically. The fix: + # one uiOutput slot driven by a renderUI that emits the textInput. The seed + # logic must distinguish "user is still on the converter's default" (in + # which case switching converter should apply the new converter's default) + # from "user typed a custom value" (in which case the typed value carries + # across the switch). The reactiveVal `last_tmt_filetype` below holds the + # filetype from the previous renderUI evaluation so the seeding helper can + # compare `preserved` against the OUTGOING converter's default. + last_tmt_filetype <- reactiveVal(NULL) + + output[[NAMESPACE_LOADPAGE$tmt_options_ui]] <- renderUI({ + filetype <- input[[NAMESPACE_LOADPAGE$filetype]] + dda_dia <- input[[NAMESPACE_LOADPAGE$dda_dia]] + if (!loadpage_show_tmt_options(filetype, dda_dia)) return(NULL) + + preserved <- isolate(input[[NAMESPACE_LOADPAGE$which_proteinid]]) + outgoing <- isolate(last_tmt_filetype()) + + seed_value <- loadpage_seed_proteinid( + incoming_filetype = filetype, + outgoing_filetype = outgoing, + preserved_value = preserved + ) + + # Update the tracker for the NEXT evaluation. Set after computing the + # seed so this evaluation still sees the previous filetype as `outgoing`. + last_tmt_filetype(filetype) + + tagList( + h4("Select the options for pre-processing"), + textInput(session$ns(NAMESPACE_LOADPAGE$which_proteinid), + label = h5("Enter the column name corresponding to the protein name"), + value = seed_value) + ) + }) + + invisible(NULL) +} + + +# ============================================================================ +# Pre-Phase-1 converter renderUIs + file-type availability observer. +# +# Moved from R/module-loadpage-server.R by the Phase 2 server split. Pure +# cut-and-paste: no behavior change, no reactivity timing change. These +# renderUIs were in the orchestrator before; they're co-located here with +# the other UI-rendering helpers for navigability. Reads `is_web_server` +# and `app_template` from the outer module, so the registration helper +# below takes them as args. +# +# Includes the file-type availability observer (the radio-disable + +# `runjs` opacity block, originally at module-loadpage-server.R:347-400). +# It is UI state, not predicate-driven visibility, so it stays a plain +# observer with no corresponding `loadpage_show_*` predicate. +# ============================================================================ + + +#' Register the pre-existing Spectronaut/DIANN converter renderUIs + the +#' Metamorpheus mod-ID renderUI's wrappers were moved into +#' `register_loadpage_preview` since they depend on the preview reactive. +#' What stays here is everything else that doesn't need preview_data. +#' +#' @param input the Shiny module's `input` object +#' @param output the Shiny module's `output` object +#' @param session the Shiny module's `session` (for `session$ns`) +#' @param is_web_server TRUE if running as the web app (`launch_MSstatsShiny()`), +#' FALSE for local-server / shinyFiles mode +#' @param app_template reactive returning the selected template name; may be +#' NULL when the orchestrator passes no template +#' @noRd +register_loadpage_converter_ui <- function(input, output, session, + is_web_server = FALSE, + app_template = NULL) { + + output$spectronaut_header_ui <- renderUI({ + req(input$filetype == 'spec', input$BIO != 'PTM') + create_spectronaut_header() + }) + + output$spectronaut_file_selection_ui <- renderUI({ + req(input$filetype == 'spec', input$BIO != 'PTM') + + ui_elements <- tagList() + + if (!is_web_server) { + ui_elements <- tagList(ui_elements, create_spectronaut_mode_selector(session$ns, isTRUE(input$big_file_spec))) + + if (isTRUE(input$big_file_spec)) { + ui_elements <- tagList(ui_elements, create_spectronaut_large_file_ui(session$ns)) + } else { + ui_elements <- tagList(ui_elements, create_spectronaut_standard_ui(session$ns)) + } + } else { + ui_elements <- tagList(ui_elements, create_spectronaut_standard_ui(session$ns)) + } + + ui_elements + }) + + output$spectronaut_intensity_ui <- renderUI({ + req(input$filetype == 'spec', input$BIO != 'PTM') + + default_intensity <- if (!is.null(app_template) && + app_template() == TEMPLATES$protein_turnover) { + "FG.MS1Quantity" + } else { + "F.NormalizedPeakArea" + } + + textInput(session$ns("spec_intensity_col"), + label = h5("Intensity column", + class = "icon-wrapper", + icon("question-circle", lib = "font-awesome"), + div("Spectronaut export column to use as the intensity measure (e.g. F.NormalizedPeakArea, F.PeakArea, FG.MS1Quantity). Leave at the default unless you have a specific reason to override it.", + class = "icon-tooltip")), + value = default_intensity) + }) + + output$spectronaut_turnover_ui <- renderUI({ + req(input$filetype == 'spec', input$BIO != 'PTM') + req(!is.null(app_template) && app_template() == TEMPLATES$protein_turnover) + + ns <- session$ns + tagList( + tags$hr(), + h4("Protein Turnover Options"), + textInput(ns("spec_peptide_seq_col"), + "Peptide sequence column", + value = "FG.LabeledSequence"), + textInput(ns("spec_heavy_labels"), + "Heavy labels (comma-separated)", + value = "L[Leu6]") + ) + }) + + output$diann_turnover_ui <- renderUI({ + req(input$filetype == 'diann', input$DDA_DIA == 'LType') + req(!is.null(app_template) && app_template() == TEMPLATES$protein_turnover) + + ns <- session$ns + textInput(ns("diann_labeled_aa"), + h5("SILAC-labeled amino acids", class = "icon-wrapper", + icon("question-circle", lib = "font-awesome"), + div("Comma-separated single-letter codes of SILAC-labeled amino acids (e.g. K for lysine, or K,R for lysine and arginine).", class = "icon-tooltip")), + value = "K") + }) + + output$diann_header_ui <- renderUI({ + req(input$filetype == 'diann', input$BIO != 'PTM') + create_diann_header() + }) + + output$diann_file_selection_ui <- renderUI({ + req(input$filetype == 'diann', input$BIO != 'PTM') + + ui_elements <- tagList() + + if (!is_web_server) { + ui_elements <- tagList(ui_elements, create_diann_mode_selector(session$ns, isTRUE(input$big_file_diann))) + + if (isTRUE(input$big_file_diann)) { + ui_elements <- tagList(ui_elements, create_diann_large_file_ui(session$ns)) + } else { + ui_elements <- tagList(ui_elements, create_diann_standard_ui(session$ns)) + } + } else { + ui_elements <- tagList(ui_elements, create_diann_standard_ui(session$ns)) + } + + ui_elements + }) + + output$diann_options_ui <- renderUI({ + req(input$filetype == 'diann', input$BIO != 'PTM') + + if (!is_web_server && isTRUE(input$big_file_diann)) { + mbr_def <- if (is.null(input$big_diann_MBR)) TRUE else input$big_diann_MBR + quantcol_def <- if (is.null(input$big_diann_quantification_column) || + !nzchar(input$big_diann_quantification_column)) { + "FragmentQuantCorrected" + } else { + input$big_diann_quantification_column + } + global_qv_def <- if (is.null(input$big_diann_global_qvalue_cutoff)) 0.01 else input$big_diann_global_qvalue_cutoff + qv_def <- if (is.null(input$big_diann_qvalue_cutoff)) 0.01 else input$big_diann_qvalue_cutoff + pg_qv_def <- if (is.null(input$big_diann_pg_qvalue_cutoff)) 0.01 else input$big_diann_pg_qvalue_cutoff + + max_feature_def <- if (is.null(input$big_diann_max_feature_count)) 100 else input$big_diann_max_feature_count + unique_peps_def <- if (is.null(input$big_diann_filter_unique_peptides)) FALSE else input$big_diann_filter_unique_peptides + agg_psms_def <- if (is.null(input$big_diann_aggregate_psms)) FALSE else input$big_diann_aggregate_psms + few_obs_def <- if (is.null(input$big_diann_filter_few_obs)) FALSE else input$big_diann_filter_few_obs + backend_def <- if (is.null(input$big_diann_backend) || !nzchar(input$big_diann_backend)) "arrow" else input$big_diann_backend + calculate_anomaly_def <- if (is.null(input$big_diann_calculate_anomaly_scores)) FALSE else input$big_diann_calculate_anomaly_scores + + tagList( + create_diann_large_filter_options(session$ns, mbr_def, quantcol_def, + global_qv_def, qv_def, pg_qv_def), + create_diann_large_bottom_ui(session$ns, max_feature_def, + unique_peps_def, agg_psms_def, few_obs_def, + backend_def), + create_diann_large_annotation_ui(session$ns, calculate_anomaly_def) + ) + } else { + NULL + } + }) + + output$spectronaut_options_ui <- renderUI({ + req(input$filetype == 'spec', input$BIO != 'PTM') + + if (!is_web_server && isTRUE(input$big_file_spec)) { + qval_def <- if (is.null(input$filter_by_qvalue)) TRUE else input$filter_by_qvalue + excluded_def <- if (is.null(input$filter_by_excluded)) FALSE else input$filter_by_excluded + identified_def <- if (is.null(input$filter_by_identified)) FALSE else input$filter_by_identified + cutoff_def <- if (is.null(input$qvalue_cutoff)) 0.01 else input$qvalue_cutoff + + max_feature_def <- if (is.null(input$max_feature_count)) 20 else input$max_feature_count + unique_peps_def <- if (is.null(input$filter_unique_peptides)) FALSE else input$filter_unique_peptides + agg_psms_def <- if (is.null(input$aggregate_psms)) FALSE else input$aggregate_psms + few_obs_def <- if (is.null(input$filter_few_obs)) FALSE else input$filter_few_obs + calculate_anomaly_def <- if (is.null(input$calculate_anomaly_scores)) FALSE else input$calculate_anomaly_scores + + tagList( + create_spectronaut_large_filter_options(session$ns, excluded_def, identified_def, qval_def), + if (qval_def) create_spectronaut_qvalue_cutoff_ui(session$ns, cutoff_def), + create_spectronaut_large_bottom_ui(session$ns, max_feature_def, unique_peps_def, agg_psms_def, few_obs_def), + create_spectronaut_large_annotation_ui(session$ns, calculate_anomaly_def) + ) + } else { + NULL + } + }) + + # File-type availability — disable converter radios that don't fit the + # current (BIO, DDA_DIA) combo, and dim them via the `runjs` opacity hack. + # UI state only, not predicate-driven visibility (no `loadpage_show_*` + # predicate). Moved from module-loadpage-server.R verbatim except for the + # debug `print()` lines that the original carried. + observe({ + if ((input$BIO == "Protein" || input$BIO == "Peptide") && input$DDA_DIA == "LType") { + runjs("$('[type=radio][name=loadpage-filetype]:disabled').parent().parent().parent().find('div.radio').css('opacity', 1)") + enable("filetype") + disable(selector = "[type=radio][value=spmin]") + runjs("$.each($('[type=radio][name=loadpage-filetype]:disabled'), function(_, e){ $(e).parent().parent().css('opacity', 0.4) })") + + } else if ((input$BIO == "Protein" || input$BIO == "Peptide") && input$DDA_DIA == "TMT") { + runjs("$('[type=radio][name=loadpage-filetype]:disabled').parent().parent().parent().find('div.radio').css('opacity', 1)") + enable("filetype") + disable(selector = "[type=radio][value=sky]") + disable(selector = "[type=radio][value=prog]") + disable(selector = "[type=radio][value=spec]") + disable(selector = "[type=radio][value=open]") + disable(selector = "[type=radio][value=ump]") + disable(selector = "[type=radio][value=diann]") + disable(selector = "[type=radio][value=meta]") + runjs("$.each($('[type=radio][name=loadpage-filetype]:disabled'), function(_, e){ $(e).parent().parent().css('opacity', 0.4) })") + + } else if (input$BIO == "PTM" && input$DDA_DIA == "LType") { + runjs("$('[type=radio][name=loadpage-filetype]:disabled').parent().parent().parent().find('div.radio').css('opacity', 1)") + enable("filetype") + # disable(selector = "[type=radio][value=sky]") + disable(selector = "[type=radio][value=prog]") + disable(selector = "[type=radio][value=PD]") + disable(selector = "[type=radio][value=openms]") + disable(selector = "[type=radio][value=spmin]") + disable(selector = "[type=radio][value=open]") + disable(selector = "[type=radio][value=ump]") + disable(selector = "[type=radio][value=phil]") + disable(selector = "[type=radio][value=diann]") + + runjs("$.each($('[type=radio][name=loadpage-filetype]:disabled'), function(_, e){ $(e).parent().parent().css('opacity', 0.4) })") + } else if (input$BIO == "PTM" && input$DDA_DIA == "TMT") { + runjs("$('[type=radio][name=loadpage-filetype]:disabled').parent().parent().parent().find('div.radio').css('opacity', 1)") + enable("filetype") + disable(selector = "[type=radio][value=prog]") + disable(selector = "[type=radio][value=openms]") + disable(selector = "[type=radio][value=spec]") + disable(selector = "[type=radio][value=open]") + disable(selector = "[type=radio][value=ump]") + disable(selector = "[type=radio][value=spmin]") + disable(selector = "[type=radio][value=diann]") + disable(selector = "[type=radio][value=sky]") + disable(selector = "[type=radio][value=meta]") + + runjs("$.each($('[type=radio][name=loadpage-filetype]:disabled'), function(_, e){ $(e).parent().parent().css('opacity', 0.4) })") + } + }) + invisible(NULL) } + diff --git a/R/loadpage-server-summary.R b/R/loadpage-server-summary.R new file mode 100644 index 0000000..bffcad8 --- /dev/null +++ b/R/loadpage-server-summary.R @@ -0,0 +1,235 @@ +# ============================================================================ +# Loadpage — condition_metadata DT editor + post-`proceed1` onclick handler +# ============================================================================ +# +# Extracted from R/module-loadpage-server.R by the Phase 2 server split. +# Pure cut-and-paste. Owns: +# - the condition_metadata DT cell-edit observeEvent +# - `output$condition_metadata_table` renderDT +# - the `onclick("proceed1", { ... })` block (post-proceed setup, +# condition_metadata initialization per template, summary outputs, and +# the nested `onclick("proceed2", ...)` that flips the parent tabset) +# +# The orchestrator owns the `condition_metadata` reactiveVal because the +# summary helper and the rest of the page (via the public return value) +# both share it. It is passed in as `condition_metadata` below. The +# data-loading reactives the summary helper reads +# (`get_data`, `get_annot`, `get_summary1`, `get_summary2`) come in via +# `data_reactives` from `register_loadpage_data_loaders()`. + + +#' Register the loadpage post-`proceed1` summary cluster. +#' +#' @param input the Shiny module's `input` object +#' @param output the Shiny module's `output` object +#' @param session the Shiny module's `session` +#' @param parent_session the parent module's session (for the tablist +#' switch in `onclick("proceed2")`) +#' @param app_template reactive (or NULL) returning the active template +#' @param data_reactives named list from `register_loadpage_data_loaders`; +#' the helper consumes `get_data`, `get_annot`, +#' `get_summary1`, `get_summary2` +#' @param condition_metadata `reactiveVal` owned by the orchestrator +#' @noRd +register_loadpage_summary <- function(input, output, session, parent_session, + app_template = NULL, + data_reactives, + condition_metadata) { + + get_data <- data_reactives$get_data + get_annot <- data_reactives$get_annot + get_summary1 <- data_reactives$get_summary1 + get_summary2 <- data_reactives$get_summary2 + + # Handle edits to the condition metadata DT table + observeEvent(input$condition_metadata_table_cell_edit, { + info <- input$condition_metadata_table_cell_edit + current <- condition_metadata() + if (is.null(current)) return() + if (info$col == 1) { + value_col <- if ("TimeVal" %in% colnames(current)) "TimeVal" else "DoseVal" + current[[value_col]][info$row] <- info$value + condition_metadata(current) + } else if (info$col == 2 && "DrugName" %in% colnames(current)) { + current[["DrugName"]][info$row] <- as.character(info$value) + condition_metadata(current) + } else if (info$col == 3 && "DoseUnit" %in% colnames(current)) { + current[["DoseUnit"]][info$row] <- as.character(info$value) + condition_metadata(current) + } + }) + + # Render the editable condition metadata table + output$condition_metadata_table <- DT::renderDT({ + req(!is.null(condition_metadata())) + meta <- condition_metadata() + caption_text <- "Click any cell to edit. Cells showing '?' could not be + parsed and must be filled in before running analysis." + DT::datatable( + meta, + editable = list(target = "cell", disable = list(columns = c(0))), + rownames = FALSE, + selection = "none", + options = list(dom = 't', paging = FALSE), + caption = caption_text + ) + }) + + onclick("proceed1", { + get_data() + get_annot() + shinyjs::show("summary_tables") + + condition_metadata(NULL) + # Initialize condition metadata for protein turnover and chemoproteomics templates + if (!is.null(app_template) && app_template() == TEMPLATES$protein_turnover) { + tryCatch({ + data <- get_data() + if (!is.null(data) && "Condition" %in% colnames(data)) { + conditions <- unique(as.character(data$Condition)) + time_vals <- as.character(autofill_condition_value(conditions)) + time_vals[is.na(time_vals) | time_vals == "NA"] <- "?" + meta_df <- data.frame(Condition = conditions, + TimeVal = time_vals, + stringsAsFactors = FALSE) + condition_metadata(meta_df) + } + }, error = function(e) {}) + } else if (!is.null(app_template) && app_template() == TEMPLATES$chemoproteomics) { + tryCatch({ + data <- get_data() + if (!is.null(data) && "Condition" %in% colnames(data)) { + conditions <- unique(as.character(data$Condition)) + is_ctrl <- grepl("^(dmso|control|vehicle)$", tolower(trimws(conditions))) + parsed_drug <- parse_drug_name_from_conditions(conditions) + dose_vals <- as.character(autofill_condition_value(conditions)) + dose_vals[is.na(dose_vals) | dose_vals == "NA"] <- "?" + meta_df <- data.frame(Condition = conditions, + DoseVal = dose_vals, + DrugName = ifelse(is_ctrl, conditions, parsed_drug), + DoseUnit = parse_dose_unit_from_conditions(conditions), + stringsAsFactors = FALSE) + condition_metadata(meta_df) + } + }, error = function(e) { + condition_metadata(NULL) + showNotification( + paste("Could not initialize condition metadata:", conditionMessage(e)), + type = "warning", + duration = 6 + ) + }) + } + + ### outputs ### + get_summary <- reactive({ + if (is.null(get_data())) { + return(NULL) + } + data1 <- get_data() + data_summary <- describe(data1) + }) + + output$template <- downloadHandler( + filename = "extdata/templateannotation.csv", + + content = function(file) { + file.copy("extdata/templateannotation.csv", file) + }, + contentType = "csv" + ) + + output$template1 <- downloadHandler( + filename = function() { + paste("extdata/templateevidence", "txt", sep = ".") + }, + + content = function(file) { + file.copy("extdata/templateevidence.txt", file) + }, + contentType = "txt" + ) + + output$summary <- renderTable( + { + head(get_data()) + }, bordered = TRUE + ) + output$summary_ptm <- renderTable( + { + head(get_data()$PTM) + }, bordered = TRUE + ) + output$summary_prot <- renderTable( + { + head(get_data()$PROTEIN) + }, bordered = TRUE + ) + + + output$summary1 <- renderTable( + { + req(get_data()) + get_summary1() + + }, colnames = FALSE, bordered = TRUE + ) + + output$summary2 <- renderTable( + { + req(get_data()) + get_summary2() + + }, colnames = FALSE, bordered = TRUE, align = 'lr' + ) + + onclick("proceed2", { + updateTabsetPanel(session = parent_session, inputId = "tablist", + selected = "DataProcessing") + }) + output$summary_tables <- renderUI({ + ns <- session$ns + is_turnover <- !is.null(app_template) && app_template() == TEMPLATES$protein_turnover + is_chemo <- !is.null(app_template) && app_template() == TEMPLATES$chemoproteomics + tagList( + tags$head( + tags$style(HTML('#loadpage-proceed2{background-color:orange}')) + ), + actionButton(inputId = ns("proceed2"), label = "Next step"), + if (is_turnover) tagList( + tags$hr(), + h4("Condition time points"), + p("Time values are auto-filled from condition names. Correct any values as needed before running the analysis."), + DT::dataTableOutput(ns("condition_metadata_table")), + tags$br() + ) else if (is_chemo) tagList( + tags$hr(), + h4("Condition doses"), + p("Dose values are auto-filled from condition names. Correct any values as needed before running the analysis."), + DT::dataTableOutput(ns("condition_metadata_table")), + tags$br() + ), + h4("Summary of experimental design"), + tableOutput(ns('summary1')), + tags$br(), + h4("Summary of dataset"), + tableOutput(ns("summary2")), + tags$br(), + conditionalPanel(condition = "input['loadpage-BIO'] !== 'PTM'", + h4("Top 6 rows of the dataset"), + div(style = "overflow-x: auto;", tableOutput(ns("summary"))) + ), + conditionalPanel(condition = "input['loadpage-BIO'] == 'PTM'", + h4("Top 6 rows of the PTM dataset"), + div(style = "overflow-x: auto;", tableOutput(ns("summary_ptm"))), + tags$br(), + h4("Top 6 rows of the unmodified protein dataset"), + div(style = "overflow-x: auto;", tableOutput(ns("summary_prot"))) + ) + ) + }) + + }) + + invisible(NULL) +} diff --git a/R/module-loadpage-server.R b/R/module-loadpage-server.R index 18271f4..61e343a 100644 --- a/R/module-loadpage-server.R +++ b/R/module-loadpage-server.R @@ -1,37 +1,49 @@ #' Loadpage Server module for data selection and upload server. #' -#' This function sets up the loadpage server where it consists of several, -#' options for users to select and upload files. +#' This function sets up the loadpage server where it consists of several +#' options for users to select and upload files. After the Phase 2 split, +#' the orchestrator below keeps only: +#' +#' \itemize{ +#' \item the module signature and `condition_metadata` reactiveVal, +#' \item the shinyFiles browser block (it produces the +#' `local_big_file_path` / `local_big_diann_path` reactives that the +#' proceed-validation helper consumes, so it must remain co-located +#' with the module's reactive scope), +#' \item the five helper registrations in order, +#' \item the final public `return(list(input, getData, +#' getConditionMetadata))`. +#' } +#' +#' Each helper lives in its own file (`R/loadpage-server-*.R`); see those +#' files for the moved code blocks. #' #' @param id namespace prefix for the module #' @param parent_session session of the main calling module #' @param is_web_server boolean indicating if the app is running on a web server +#' @param app_template reactive (or NULL) returning the selected template #' #' @return input object with user selected options #' #' @export #' @examples #' NA -#' +#' loadpageServer <- function(id, parent_session, is_web_server = FALSE, app_template = NULL) { moduleServer(id, function(input, output, session) { condition_metadata <- reactiveVal(NULL) - # DIANN-cluster visibility: observers replace the JS conditionalPanels - register_diann_visibility_observers(input, session) - - # == shinyFiles LOGIC FOR LOCAL FILE BROWSER ================================= - # Define volumes for the file selection. + # == shinyFiles LOGIC FOR LOCAL FILE BROWSER =============================== + # Stays in the orchestrator because the two `local_big_*_path` reactives + # it produces are consumed by `register_loadpage_proceed_validation()` + # below; lifting them into a helper would require an extra round-trip. if (!is_web_server) { volumes <- shinyFiles::getVolumes()() - # Server-side logic for the shinyFiles buttons (Spectronaut + DIANN) - shinyFiles::shinyFileChoose(input, "big_file_browse", roots = volumes, session = session) + shinyFiles::shinyFileChoose(input, "big_file_browse", roots = volumes, session = session) shinyFiles::shinyFileChoose(input, "big_diann_browse", roots = volumes, session = session) - # Reactive to parse and store the full file information (path, name, etc.) - # This is efficient because parseFilePaths is only called once. local_file_info <- reactive({ req(is.list(input$big_file_browse)) shinyFiles::parseFilePaths(volumes, input$big_file_browse) @@ -42,7 +54,6 @@ loadpageServer <- function(id, parent_session, is_web_server = FALSE, app_templa shinyFiles::parseFilePaths(volumes, input$big_diann_browse) }) - # Reactive to get just the full datapath, for use in backend processing. local_big_file_path <- reactive({ path_info <- local_file_info() if (nrow(path_info) > 0) path_info$datapath else NULL @@ -53,7 +64,6 @@ loadpageServer <- function(id, parent_session, is_web_server = FALSE, app_templa if (nrow(path_info) > 0) path_info$datapath else NULL }) - # Render just the filename for user feedback in the UI. output$specdata_big_path <- renderPrint({ req(nrow(local_file_info()) > 0) cat(local_file_info()$name) @@ -63,765 +73,48 @@ loadpageServer <- function(id, parent_session, is_web_server = FALSE, app_templa req(nrow(local_diann_file_info()) > 0) cat(local_diann_file_info()$name) }) - } - else { - local_big_file_path <- reactive({ NULL }) + } else { + local_big_file_path <- reactive({ NULL }) local_big_diann_path <- reactive({ NULL }) } - # ============ PREVIEW DATA: Read first 100 rows on file upload ============ - preview_data <- reactiveVal(NULL) - - # Determine the main data file based on current selections - # TODO: Add preview mappings for remaining PTM file types (PD, spec, sky, maxq) - # once preview-based UI features are extended beyond Metamorpheus. - main_data_file <- reactive({ - req(input$filetype) - if (input$BIO == "PTM") { - switch(input$filetype, - "meta" = input$ptm_input, - # TODO: "maxq" = input$ptm_input, - # TODO: "PD" = input$ptm_input, - # TODO: "spec" = input$ptm_input, - # TODO: "sky" = input$ptm_input, - # TODO: "phil" = input$ptmdata, - # TODO: "msstats" = input$msstatsptmdata, - NULL - ) - } else { - switch(input$filetype, - # TODO: Map remaining non-PTM file types when preview features are needed - "prog" =, "PD" =, "open" =, "openms" =, "spmin" =, "phil" =, "meta" = input$data, - "msstats" = input$msstatsdata, - "sky" = input$skylinedata, - "spec" = input$specdata, - "diann" = input$dianndata, - "maxq" = input$evidence, - NULL - ) - } - }) - - # Read first 100 rows for preview-based UI features. - # Supported: Metamorpheus PTM (modification ID dropdown), DIANN (version auto-detection). - # TODO: Extend to other input formats (Spectronaut, MaxQuant) as needed. - observe({ - should_preview <- (isTRUE(input$filetype == "meta") && isTRUE(input$BIO == "PTM")) || - (isTRUE(input$filetype == "diann") && isTRUE(input$BIO != "PTM")) - if (should_preview) { - file_info <- main_data_file() - if (!is.null(file_info)) { - # Reset DIANN detection tracker so a new file re-triggers the notification - last_detected_diann_format(NULL) - preview <- .read_preview(file_info$datapath, file_info$name) - if (is.null(preview)) { - showNotification("Could not preview file. Please verify the file format.", - type = "warning", duration = 5) - } - preview_data(preview) - } else { - preview_data(NULL) - } - } else { - preview_data(NULL) - } - }) - - # Track last detected DIANN format to avoid redundant notifications - last_detected_diann_format <- reactiveVal(NULL) - - # Auto-toggle DIANN 2.0+ checkbox based on detected file format - observe({ - req(input$filetype == "diann", input$BIO != "PTM") - preview <- preview_data() - if (is.null(preview)) return() - - is_2plus <- .is_diann_2plus(preview) - previous <- last_detected_diann_format() - # Only update and notify when the detected state actually changes - if (is.null(previous) || previous != is_2plus) { - updateCheckboxInput(session, "diann_2plus", value = is_2plus) - if (is_2plus) { - showNotification("Detected DIANN 2.0+ format (per-fragment columns).", - type = "message", duration = 5) - } else { - showNotification("Detected DIANN 1.x format (legacy fragment column).", - type = "message", duration = 5) - } - last_detected_diann_format(is_2plus) - } - }) - - # Warn user if they manually set DIANN 2.0+ checkbox to a value that conflicts with detected format - observeEvent(input$diann_2plus, { - req(input$filetype == "diann", input$BIO != "PTM") - preview <- preview_data() - if (is.null(preview)) return() - detected_2plus <- .is_diann_2plus(preview) - if (isTRUE(input$diann_2plus) != detected_2plus) { - showNotification( - paste0("Warning: You've ", - if (isTRUE(input$diann_2plus)) "checked" else "unchecked", - " DIANN 2.0+, but the uploaded file appears to be ", - if (detected_2plus) "DIANN 2.0+ format" else "DIANN 1.x format", - ". This mismatch may cause upload to fail."), - type = "warning", duration = 10) - } - }, ignoreInit = TRUE) - - # ========= METAMORPHEUS PTM: Dynamic modification ID dropdown ========= - output$mod_id_meta_ui <- renderUI({ - ns <- session$ns - req(input$filetype == "meta", input$BIO == "PTM") - mods <- .extract_mod_ids_from_preview(preview_data()) - create_meta_mod_id_selector(ns, mods) - }) - - # Show manual text input when "Other" is selected (replaces conditionalPanel) - output$mod_id_meta_other_input <- renderUI({ - req(input$mod_id_meta_select == "__other__") - textInput(session$ns("mod_id_meta_custom"), - label = h5("Enter modification ID (e.g. [Common Biological:Phosphorylation on S])"), - value = "") - }) - - output$spectronaut_header_ui <- renderUI({ - req(input$filetype == 'spec', input$BIO != 'PTM') - create_spectronaut_header() - }) - - output$spectronaut_file_selection_ui <- renderUI({ - req(input$filetype == 'spec', input$BIO != 'PTM') - - ui_elements <- tagList() - - if (!is_web_server) { - ui_elements <- tagList(ui_elements, create_spectronaut_mode_selector(session$ns, isTRUE(input$big_file_spec))) - - if (isTRUE(input$big_file_spec)) { - ui_elements <- tagList(ui_elements, create_spectronaut_large_file_ui(session$ns)) - } else { - ui_elements <- tagList(ui_elements, create_spectronaut_standard_ui(session$ns)) - } - } else { - ui_elements <- tagList(ui_elements, create_spectronaut_standard_ui(session$ns)) - } - - ui_elements - }) - - output$spectronaut_intensity_ui <- renderUI({ - req(input$filetype == 'spec', input$BIO != 'PTM') - - default_intensity <- if (!is.null(app_template) && - app_template() == TEMPLATES$protein_turnover) { - "FG.MS1Quantity" - } else { - "F.NormalizedPeakArea" - } - - textInput(session$ns("spec_intensity_col"), - label = h5("Intensity column", - class = "icon-wrapper", - icon("question-circle", lib = "font-awesome"), - div("Spectronaut export column to use as the intensity measure (e.g. F.NormalizedPeakArea, F.PeakArea, FG.MS1Quantity). Leave at the default unless you have a specific reason to override it.", - class = "icon-tooltip")), - value = default_intensity) - }) - - output$spectronaut_turnover_ui <- renderUI({ - req(input$filetype == 'spec', input$BIO != 'PTM') - req(!is.null(app_template) && app_template() == TEMPLATES$protein_turnover) - - ns <- session$ns - tagList( - tags$hr(), - h4("Protein Turnover Options"), - textInput(ns("spec_peptide_seq_col"), - "Peptide sequence column", - value = "FG.LabeledSequence"), - textInput(ns("spec_heavy_labels"), - "Heavy labels (comma-separated)", - value = "L[Leu6]") - ) - }) - - output$diann_turnover_ui <- renderUI({ - req(input$filetype == 'diann', input$DDA_DIA == 'LType') - req(!is.null(app_template) && app_template() == TEMPLATES$protein_turnover) - - ns <- session$ns - textInput(ns("diann_labeled_aa"), - h5("SILAC-labeled amino acids", class = "icon-wrapper", - icon("question-circle", lib = "font-awesome"), - div("Comma-separated single-letter codes of SILAC-labeled amino acids (e.g. K for lysine, or K,R for lysine and arginine).", class = "icon-tooltip")), - value = "K") - }) - - output$diann_header_ui <- renderUI({ - req(input$filetype == 'diann', input$BIO != 'PTM') - create_diann_header() - }) - - output$diann_file_selection_ui <- renderUI({ - req(input$filetype == 'diann', input$BIO != 'PTM') - - ui_elements <- tagList() - - if (!is_web_server) { - ui_elements <- tagList(ui_elements, create_diann_mode_selector(session$ns, isTRUE(input$big_file_diann))) - - if (isTRUE(input$big_file_diann)) { - ui_elements <- tagList(ui_elements, create_diann_large_file_ui(session$ns)) - } else { - ui_elements <- tagList(ui_elements, create_diann_standard_ui(session$ns)) - } - } else { - ui_elements <- tagList(ui_elements, create_diann_standard_ui(session$ns)) - } - - ui_elements - }) - - output$diann_options_ui <- renderUI({ - req(input$filetype == 'diann', input$BIO != 'PTM') - - if (!is_web_server && isTRUE(input$big_file_diann)) { - mbr_def <- if (is.null(input$big_diann_MBR)) TRUE else input$big_diann_MBR - quantcol_def <- if (is.null(input$big_diann_quantification_column) || - !nzchar(input$big_diann_quantification_column)) { - "FragmentQuantCorrected" - } else { - input$big_diann_quantification_column - } - global_qv_def <- if (is.null(input$big_diann_global_qvalue_cutoff)) 0.01 else input$big_diann_global_qvalue_cutoff - qv_def <- if (is.null(input$big_diann_qvalue_cutoff)) 0.01 else input$big_diann_qvalue_cutoff - pg_qv_def <- if (is.null(input$big_diann_pg_qvalue_cutoff)) 0.01 else input$big_diann_pg_qvalue_cutoff - - max_feature_def <- if (is.null(input$big_diann_max_feature_count)) 100 else input$big_diann_max_feature_count - unique_peps_def <- if (is.null(input$big_diann_filter_unique_peptides)) FALSE else input$big_diann_filter_unique_peptides - agg_psms_def <- if (is.null(input$big_diann_aggregate_psms)) FALSE else input$big_diann_aggregate_psms - few_obs_def <- if (is.null(input$big_diann_filter_few_obs)) FALSE else input$big_diann_filter_few_obs - backend_def <- if (is.null(input$big_diann_backend) || !nzchar(input$big_diann_backend)) "arrow" else input$big_diann_backend - calculate_anomaly_def <- if (is.null(input$big_diann_calculate_anomaly_scores)) FALSE else input$big_diann_calculate_anomaly_scores - - tagList( - create_diann_large_filter_options(session$ns, mbr_def, quantcol_def, - global_qv_def, qv_def, pg_qv_def), - create_diann_large_bottom_ui(session$ns, max_feature_def, - unique_peps_def, agg_psms_def, few_obs_def, - backend_def), - create_diann_large_annotation_ui(session$ns, calculate_anomaly_def) - ) - } else { - NULL - } - }) - - output$spectronaut_options_ui <- renderUI({ - req(input$filetype == 'spec', input$BIO != 'PTM') - - if (!is_web_server && isTRUE(input$big_file_spec)) { - qval_def <- if (is.null(input$filter_by_qvalue)) TRUE else input$filter_by_qvalue - excluded_def <- if (is.null(input$filter_by_excluded)) FALSE else input$filter_by_excluded - identified_def <- if (is.null(input$filter_by_identified)) FALSE else input$filter_by_identified - cutoff_def <- if (is.null(input$qvalue_cutoff)) 0.01 else input$qvalue_cutoff - - max_feature_def <- if (is.null(input$max_feature_count)) 20 else input$max_feature_count - unique_peps_def <- if (is.null(input$filter_unique_peptides)) FALSE else input$filter_unique_peptides - agg_psms_def <- if (is.null(input$aggregate_psms)) FALSE else input$aggregate_psms - few_obs_def <- if (is.null(input$filter_few_obs)) FALSE else input$filter_few_obs - calculate_anomaly_def <- if (is.null(input$calculate_anomaly_scores)) FALSE else input$calculate_anomaly_scores - - tagList( - create_spectronaut_large_filter_options(session$ns, excluded_def, identified_def, qval_def), - if (qval_def) create_spectronaut_qvalue_cutoff_ui(session$ns, cutoff_def), - create_spectronaut_large_bottom_ui(session$ns, max_feature_def, unique_peps_def, agg_psms_def, few_obs_def), - create_spectronaut_large_annotation_ui(session$ns, calculate_anomaly_def) - ) - } else { - NULL - } - }) - - # toggle ui (DDA DIA SRM) - observe({ - print("bio") - - print(input$BIO) - if((input$BIO == "Protein" || input$BIO == "Peptide") && input$DDA_DIA == "LType"){ - runjs("$('[type=radio][name=loadpage-filetype]:disabled').parent().parent().parent().find('div.radio').css('opacity', 1)") - enable("filetype") - disable(selector = "[type=radio][value=spmin]") - runjs("$.each($('[type=radio][name=loadpage-filetype]:disabled'), function(_, e){ $(e).parent().parent().css('opacity', 0.4) })") - - } else if ((input$BIO == "Protein" || input$BIO == "Peptide") && input$DDA_DIA == "TMT"){ - runjs("$('[type=radio][name=loadpage-filetype]:disabled').parent().parent().parent().find('div.radio').css('opacity', 1)") - enable("filetype") - disable(selector = "[type=radio][value=sky]") - disable(selector = "[type=radio][value=prog]") - disable(selector = "[type=radio][value=spec]") - disable(selector = "[type=radio][value=open]") - disable(selector = "[type=radio][value=ump]") - disable(selector = "[type=radio][value=diann]") - disable(selector = "[type=radio][value=meta]") - runjs("$.each($('[type=radio][name=loadpage-filetype]:disabled'), function(_, e){ $(e).parent().parent().css('opacity', 0.4) })") - - } else if (input$BIO == "PTM" && input$DDA_DIA == "LType"){ - runjs("$('[type=radio][name=loadpage-filetype]:disabled').parent().parent().parent().find('div.radio').css('opacity', 1)") - enable("filetype") - # disable(selector = "[type=radio][value=sky]") - disable(selector = "[type=radio][value=prog]") - disable(selector = "[type=radio][value=PD]") - disable(selector = "[type=radio][value=openms]") - disable(selector = "[type=radio][value=spmin]") - disable(selector = "[type=radio][value=open]") - disable(selector = "[type=radio][value=ump]") - disable(selector = "[type=radio][value=phil]") - disable(selector = "[type=radio][value=diann]") - - runjs("$.each($('[type=radio][name=loadpage-filetype]:disabled'), function(_, e){ $(e).parent().parent().css('opacity', 0.4) })") - }else if (input$BIO == "PTM" && input$DDA_DIA == "TMT"){ - runjs("$('[type=radio][name=loadpage-filetype]:disabled').parent().parent().parent().find('div.radio').css('opacity', 1)") - enable("filetype") - disable(selector = "[type=radio][value=prog]") - disable(selector = "[type=radio][value=openms]") - disable(selector = "[type=radio][value=spec]") - disable(selector = "[type=radio][value=open]") - disable(selector = "[type=radio][value=ump]") - disable(selector = "[type=radio][value=spmin]") - disable(selector = "[type=radio][value=diann]") - disable(selector = "[type=radio][value=sky]") - disable(selector = "[type=radio][value=meta]") - - runjs("$.each($('[type=radio][name=loadpage-filetype]:disabled'), function(_, e){ $(e).parent().parent().css('opacity', 0.4) })") - } - - }) - - # observeEvent(input$filetype,{ - # enable("proceed1") - # }) - - observe({ - disable("proceed1") - if(((input$BIO == "Protein") || (input$BIO == "Peptide"))) { - if(input$DDA_DIA == "LType") { - if ((!is.null(input$filetype) && length(input$filetype) > 0)) { - if (input$filetype == "sample") { - if(!is.null(input$LabelFreeType)) { - enable("proceed1") - } - } else if (input$filetype == "msstats") { - if(!is.null(input$msstatsdata)) { - enable("proceed1") - } - } else if (input$filetype == "sky") { - if(!is.null(input$skylinedata)) { - enable("proceed1") - } - } else if (input$filetype == "maxq") { - if(!is.null(input$evidence) && !is.null(input$pGroup)) { # && !is.null(input$annot1) - enable("proceed1") - } - } else if (input$filetype == "prog" || input$filetype == "PD" || input$filetype == "open" || input$filetype == "phil" || input$filetype == "meta") { - if(!is.null(input$data)) { - enable("proceed1") - } - } else if (input$filetype == "openms") { - if(!is.null(input$data)) { - enable("proceed1") - } - } else if (input$filetype == "spec") { - spec_regular_file_ok <- !isTRUE(input$big_file_spec) && !is.null(input$specdata) - spec_big_file_ok <- isTRUE(input$big_file_spec) && length(local_big_file_path()) > 0 - if(spec_regular_file_ok || spec_big_file_ok) { - enable("proceed1") - } - } else if (input$filetype == "ump") { - if(!is.null(input$fragSummary) && !is.null(input$peptideSummary) && !is.null(input$protSummary)) { #&& !is.null(input$annot2) - enable("proceed1") - } - } else if (input$filetype == "diann") { - diann_regular_file_ok <- !isTRUE(input$big_file_diann) && !is.null(input$dianndata) - diann_big_file_ok <- isTRUE(input$big_file_diann) && length(local_big_diann_path()) > 0 - if(diann_regular_file_ok || diann_big_file_ok) { - enable("proceed1") - } - } - } - } else if (input$DDA_DIA == "TMT") { - if ((!is.null(input$filetype) && length(input$filetype) > 0)) { - if(input$filetype == "sample" || input$filetype == "msstats") { - enable("proceed1") - } - if (input$filetype == "maxq") { - if(!is.null(input$evidence) && !is.null(input$pGroup)) { # && !is.null(input$annot1) - enable("proceed1") - } - } else if (input$filetype == "PD") { - if(!is.null(input$data)) { - enable("proceed1") - } - } else if (input$filetype == "openms") { - if(!is.null(input$data)) { - enable("proceed1") - } - } else if (input$filetype == "spmin" || input$filetype == "phil") { - if(!is.null(input$data)) { - enable("proceed1") - } - } - } - } - - } - else if ((input$BIO == "PTM")) { - if (input$DDA_DIA == "LType" || input$DDA_DIA == "TMT") { - if ((!is.null(input$filetype) && length(input$filetype) > 0)) { - if (input$filetype == "sample") { - enable("proceed1") - } else if (input$filetype == "msstats") { - if(!is.null(input$msstatsptmdata)) { - enable("proceed1") - } - } else if (input$filetype == "sky" || input$filetype == "maxq" || input$filetype == "spec" || input$filetype == "PD" || input$filetype == "meta") { - if(!is.null(input$ptm_input) && !is.null(input$fasta)) { # && !is.null(input$ptm_annot) - enable("proceed1") - } - } - else if (input$filetype == "phil") { - if(!is.null(input$ptmdata)) { # && !is.null(input$annotation) - enable("proceed1") - } - } - } - } - } - }) - - get_annot = eventReactive(input$proceed1, { - getAnnot(input) - }) - + # == HELPER REGISTRATION (5 helpers, all in R/loadpage-server-*.R) ========= + # + # Order matters only insofar as Shiny reactivity is set up at module-mount + # time. We follow the file's original top-to-bottom layout: preview -> + # visibility -> converter UI -> proceed validation -> data loaders -> + # summary. The visibility + converter helpers are independent of the + # data-loaders' return value; only the summary helper consumes it. - get_annot1 = reactive({ - getAnnot1(input) - }) + register_loadpage_preview(input, output, session) - get_annot2 = reactive({ - getAnnot2(input) - }) + register_loadpage_visibility_observers(input, output, session) - get_annot3 = reactive({ - getAnnot3(input) - }) - - get_evidence = reactive({ - getEvidence(input) - }) - - get_evidence2 = reactive({ - getEvidence2(input) - }) - - get_global = reactive({ - getGlobal(input) - }) - - get_proteinGroups = reactive({ - getProteinGroups(input) - }) - - get_proteinGroups2 = reactive({ - getProteinGroups2(input) - }) - - get_FragSummary = reactive({ - getFragSummary(input) - }) - - get_peptideSummary = reactive({ - getPeptideSummary(input) - }) - - get_protSummary = reactive({ - getProtSummary(input) - }) - - get_maxq_ptm_sites = reactive({ - getMaxqPtmSites(input) - }) - - get_data = eventReactive(input$proceed1, { - tryCatch( - getData(input), - error = function(e) { - tryCatch(remove_modal_spinner(), error = function(e2) NULL) - showNotification( - paste("Failed to load data:", conditionMessage(e)), - type = "error", duration = 12) - NULL - } - ) - }) - - observeEvent(input$proceed1, { - shinyjs::disable("download_msstats_format") - }) - - observeEvent(get_data(), { - req(get_data()) - shinyjs::enable("download_msstats_format") - }) - - output$download_msstats_format = downloadHandler( - filename = function() { - data <- get_data() - if (inherits(data, "data.frame")) { - paste0("MSstats_format-", Sys.Date(), ".csv") - } else { - paste0("MSstats_format-", Sys.Date(), ".zip") - } - }, - content = function(file) { - tryCatch({ - data <- get_data() - if (inherits(data, "data.frame")) { - data.table::fwrite(data, file) - } else { - tmp_dir <- tempfile("msstats_format_") - dir.create(tmp_dir) - on.exit(unlink(tmp_dir, recursive = TRUE), add = TRUE) - tmp_files <- character() - for (nm in names(data)) { - tbl <- data[[nm]] - if (is.null(tbl)) next - if (NROW(tbl) == 0L) next - tmp_path <- file.path(tmp_dir, paste0(nm, ".csv")) - data.table::fwrite(tbl, tmp_path) - tmp_files <- c(tmp_files, tmp_path) - } - if (length(tmp_files) == 0L) { - stop("No non-empty tables available to export.") - } - utils::zip(zipfile = file, files = tmp_files, flags = "-j") - } - }, error = function(e) { - writeLines(paste("Failed to export MSstats format:", conditionMessage(e)), file) - }) - } + register_loadpage_converter_ui( + input, output, session, + is_web_server = is_web_server, + app_template = app_template ) + register_loadpage_proceed_validation( + input, session, + local_big_file_path = local_big_file_path, + local_big_diann_path = local_big_diann_path + ) - get_data_code = eventReactive(input$calculate, { - getDataCode(input) - }) - - get_summary1 = eventReactive(input$proceed1, { - getSummary1(input,get_data(),get_annot()) - }) - - get_summary2 = eventReactive(input$proceed1, { - getSummary2(input,get_data()) - }) - - # Handle edits to the condition metadata DT table - observeEvent(input$condition_metadata_table_cell_edit, { - info <- input$condition_metadata_table_cell_edit - current <- condition_metadata() - if (is.null(current)) return() - if (info$col == 1) { - value_col <- if ("TimeVal" %in% colnames(current)) "TimeVal" else "DoseVal" - current[[value_col]][info$row] <- info$value - condition_metadata(current) - } else if (info$col == 2 && "DrugName" %in% colnames(current)) { - current[["DrugName"]][info$row] <- as.character(info$value) - condition_metadata(current) - } else if (info$col == 3 && "DoseUnit" %in% colnames(current)) { - current[["DoseUnit"]][info$row] <- as.character(info$value) - condition_metadata(current) - } - }) - - # Render the editable condition metadata table - output$condition_metadata_table <- DT::renderDT({ - req(!is.null(condition_metadata())) - meta <- condition_metadata() - caption_text <- "Click any cell to edit. Cells showing '?' could not be - parsed and must be filled in before running analysis." - DT::datatable( - meta, - editable = list(target = "cell", disable = list(columns = c(0))), - rownames = FALSE, - selection = "none", - options = list(dom = 't', paging = FALSE), - caption = caption_text - ) - }) - - onclick("proceed1", { - get_data() - get_annot() - shinyjs::show("summary_tables") - - condition_metadata(NULL) - # Initialize condition metadata for protein turnover and chemoproteomics templates - if (!is.null(app_template) && app_template() == TEMPLATES$protein_turnover) { - tryCatch({ - data <- get_data() - if (!is.null(data) && "Condition" %in% colnames(data)) { - conditions <- unique(as.character(data$Condition)) - time_vals <- as.character(autofill_condition_value(conditions)) - time_vals[is.na(time_vals) | time_vals == "NA"] <- "?" - meta_df <- data.frame(Condition = conditions, - TimeVal = time_vals, - stringsAsFactors = FALSE) - condition_metadata(meta_df) - } - }, error = function(e) {}) - } else if (!is.null(app_template) && app_template() == TEMPLATES$chemoproteomics) { - tryCatch({ - data <- get_data() - if (!is.null(data) && "Condition" %in% colnames(data)) { - conditions <- unique(as.character(data$Condition)) - is_ctrl <- grepl("^(dmso|control|vehicle)$", tolower(trimws(conditions))) - parsed_drug <- parse_drug_name_from_conditions(conditions) - dose_vals <- as.character(autofill_condition_value(conditions)) - dose_vals[is.na(dose_vals) | dose_vals == "NA"] <- "?" - meta_df <- data.frame(Condition = conditions, - DoseVal = dose_vals, - DrugName = ifelse(is_ctrl, conditions, parsed_drug), - DoseUnit = parse_dose_unit_from_conditions(conditions), - stringsAsFactors = FALSE) - condition_metadata(meta_df) - } - }, error = function(e) { - condition_metadata(NULL) - showNotification( - paste("Could not initialize condition metadata:", conditionMessage(e)), - type = "warning", - duration = 6 - ) - }) - } - - ### outputs ### - get_summary = reactive({ - if(is.null(get_data())) { - return(NULL) - } - data1 = get_data() - data_summary = describe(data1) - }) - - output$template = downloadHandler( - filename = "extdata/templateannotation.csv", - - content = function(file) { - file.copy("extdata/templateannotation.csv", file) - }, - contentType = "csv" - ) - - output$template1 = downloadHandler( - filename = function() { - paste("extdata/templateevidence", "txt", sep = ".") - }, - - content = function(file) { - file.copy("extdata/templateevidence.txt", file) - }, - contentType = "txt" - ) - - output$summary = renderTable( - { - head(get_data()) - }, bordered = TRUE - ) - output$summary_ptm = renderTable( - { - head(get_data()$PTM) - }, bordered = TRUE - ) - output$summary_prot = renderTable( - { - head(get_data()$PROTEIN) - }, bordered = TRUE - ) - - - output$summary1 = renderTable( - { - req(get_data()) - get_summary1() - - }, colnames = FALSE, bordered = TRUE - ) - - output$summary2 = renderTable( - { - req(get_data()) - get_summary2() - - }, colnames = FALSE, bordered = TRUE, align='lr' - ) + data_reactives <- register_loadpage_data_loaders(input, output, session) - onclick("proceed2", { - updateTabsetPanel(session = parent_session, inputId = "tablist", - selected = "DataProcessing") - }) - output$summary_tables = renderUI({ - ns <- session$ns - is_turnover <- !is.null(app_template) && app_template() == TEMPLATES$protein_turnover - is_chemo <- !is.null(app_template) && app_template() == TEMPLATES$chemoproteomics - tagList( - tags$head( - tags$style(HTML('#loadpage-proceed2{background-color:orange}')) - ), - actionButton(inputId = ns("proceed2"), label = "Next step"), - if (is_turnover) tagList( - tags$hr(), - h4("Condition time points"), - p("Time values are auto-filled from condition names. Correct any values as needed before running the analysis."), - DT::dataTableOutput(ns("condition_metadata_table")), - tags$br() - ) else if (is_chemo) tagList( - tags$hr(), - h4("Condition doses"), - p("Dose values are auto-filled from condition names. Correct any values as needed before running the analysis."), - DT::dataTableOutput(ns("condition_metadata_table")), - tags$br() - ), - h4("Summary of experimental design"), - tableOutput(ns('summary1')), - tags$br(), - h4("Summary of dataset"), - tableOutput(ns("summary2")), - tags$br(), - conditionalPanel(condition = "input['loadpage-BIO'] !== 'PTM'", - h4("Top 6 rows of the dataset"), - div(style = "overflow-x: auto;", tableOutput(ns("summary"))) - ), - conditionalPanel(condition = "input['loadpage-BIO'] == 'PTM'", - h4("Top 6 rows of the PTM dataset"), - div(style = "overflow-x: auto;", tableOutput(ns("summary_ptm"))), - tags$br(), - h4("Top 6 rows of the unmodified protein dataset"), - div(style = "overflow-x: auto;", tableOutput(ns("summary_prot"))) - ) - ) - }) - - }) - return( - list( - input = input, - getData = get_data, - getConditionMetadata = condition_metadata - ) + register_loadpage_summary( + input, output, session, parent_session, + app_template = app_template, + data_reactives = data_reactives, + condition_metadata = condition_metadata ) - }) + return(list( + input = input, + getData = data_reactives$get_data, + getConditionMetadata = condition_metadata + )) + }) } diff --git a/R/module-loadpage-ui.R b/R/module-loadpage-ui.R index e482b50..4f635f4 100644 --- a/R/module-loadpage-ui.R +++ b/R/module-loadpage-ui.R @@ -25,7 +25,7 @@ loadpageUI <- function(id) { tags$br(), # Conditional sample dataset descriptions - create_sample_dataset_descriptions(), + create_sample_dataset_descriptions(ns), tags$br(), @@ -89,30 +89,34 @@ create_header_content <- function() { } #' Create conditional descriptions for sample datasets +#' Visibility is driven server-side by +#' `register_loadpage_visibility_observers` (see +#' `R/loadpage-server-rendering.R`); each description sits in a hidden div +#' that the observer toggles on `filetype == 'sample' && LabelFreeType == `. #' @noRd -create_sample_dataset_descriptions <- function() { +create_sample_dataset_descriptions <- function(ns) { tagList( - conditionalPanel( - condition = "input['loadpage-filetype'] == 'sample' && input['loadpage-LabelFreeType'] == 'DDA'", + shinyjs::hidden(div( + id = ns(NAMESPACE_LOADPAGE$sample_dda_description_panel), p("The sample dataset for DDA acquisition is taken from the publication ", a("Choi, M. et al. ABRF Proteome Informatics Research Group (iPRG) 2015 Study: Detection of Differentially Abundant Proteins in Label-Free Quantitative LC MS/MS Experiments. Journal of Proteome Research 16.2 (2016): 945-957. ", href = "https://pubs.acs.org/doi/10.1021/acs.jproteome.6b00881", target = "_blank")) - ), - conditionalPanel( - condition = "input['loadpage-filetype'] == 'sample' && input['loadpage-LabelFreeType'] == 'DIA'", + )), + shinyjs::hidden(div( + id = ns(NAMESPACE_LOADPAGE$sample_dia_description_panel), p("The sample dataset for DIA acquisition is taken from the publication ", - a("Selevsek, N. et al. Reproducible and Consistent Quantification of the Saccharomyces Cerevisiae Proteome by SWATH-Mass Spectrometry. Molecular & Cellular Proteomics: MCP 14.3 (2015): 739-749. ", - href = "http://www.mcponline.org/content/14/3/739.long", + a("Selevsek, N. et al. Reproducible and Consistent Quantification of the Saccharomyces Cerevisiae Proteome by SWATH-Mass Spectrometry. Molecular & Cellular Proteomics: MCP 14.3 (2015): 739-749. ", + href = "http://www.mcponline.org/content/14/3/739.long", target="_blank")) - ), - conditionalPanel( - condition = "input['loadpage-filetype'] == 'sample' && input['loadpage-LabelFreeType'] == 'SRM_PRM'", + )), + shinyjs::hidden(div( + id = ns(NAMESPACE_LOADPAGE$sample_srm_prm_description_panel), p("The sample dataset for SRM/PRM acquisition is taken from the publication ", - a("Picotti, P. et al. Full dynamic range proteome analysis of S. cerevisiae by targeted proteomics. Cell (2009), 138, 795-806.", - href = "http://www.cell.com/cell/fulltext/S0092-8674(09)00715-6", + a("Picotti, P. et al. Full dynamic range proteome analysis of S. cerevisiae by targeted proteomics. Cell (2009), 138, 795-806.", + href = "http://www.cell.com/cell/fulltext/S0092-8674(09)00715-6", target="_blank")) - ) + )) ) } @@ -164,19 +168,19 @@ create_main_selection_controls <- function(ns) { ) } -#' Create label-free type selection +#' Create label-free type selection (visibility driven server-side). #' @noRd create_label_free_type_selection <- function(ns) { - conditionalPanel( - condition="input['loadpage-BIO'] != 'PTM' && input['loadpage-filetype'] == 'sample' && input['loadpage-DDA_DIA'] == 'LType'", - radioButtons(ns("LabelFreeType"), - label = h4("4. Type of Label-Free type", class = "icon-wrapper", + shinyjs::hidden(div( + id = ns(NAMESPACE_LOADPAGE$label_free_type_selection_panel), + radioButtons(ns(NAMESPACE_LOADPAGE$label_free_type), + label = h4("4. Type of Label-Free type", class = "icon-wrapper", icon("question-circle", lib = "font-awesome"), div("Choose the spectral processing tool used to process your data", class = "icon-tooltip")), choices = c("DDA" = "DDA", "DIA" ="DIA", "SRM/PRM" ="SRM_PRM"), selected = character(0) ) - ) + )) } #' Create all file upload sections @@ -215,61 +219,61 @@ create_file_upload_sections <- function(ns) { ) } -#' Create standard quantification file uploads +#' Create standard quantification file uploads (visibility driven server-side). #' @noRd create_standard_uploads <- function(ns) { - conditionalPanel( - condition = "(input['loadpage-filetype'] =='10col' || input['loadpage-filetype'] =='prog' || input['loadpage-filetype'] =='PD' || input['loadpage-filetype'] =='open'|| - input['loadpage-filetype'] =='openms' || input['loadpage-filetype'] =='spmin' || input['loadpage-filetype'] == 'phil' || input['loadpage-filetype'] == 'meta') && input['loadpage-BIO'] != 'PTM'", + shinyjs::hidden(div( + id = ns(NAMESPACE_LOADPAGE$standard_quant_upload_panel), h4("4. Upload quantification dataset"), fileInput(ns('data'), "", multiple = FALSE, accept = NULL) - ) + )) } -#' Create standard annotation file uploads +#' Create standard annotation file uploads (visibility driven server-side). #' @noRd create_standard_annotation_uploads <- function(ns) { - conditionalPanel( - condition = "(input['loadpage-filetype'] == 'sky' || input['loadpage-filetype'] == 'prog' || input['loadpage-filetype'] == 'PD' || (input['loadpage-filetype'] == 'spec' && !input['loadpage-big_file_spec']) || input['loadpage-filetype'] == 'open'|| input['loadpage-filetype'] =='spmin' || input['loadpage-filetype'] == 'phil' || (input['loadpage-filetype'] == 'diann' && !input['loadpage-big_file_diann']) || input['loadpage-filetype'] == 'meta') && input['loadpage-BIO'] != 'PTM'", - h4("5. Upload annotation File", class = "icon-wrapper", + shinyjs::hidden(div( + id = ns(NAMESPACE_LOADPAGE$standard_annot_upload_panel), + h4("5. Upload annotation File", class = "icon-wrapper", icon("question-circle", lib = "font-awesome"), div("Upload manually created annotation file. This file maps MS runs to experiment metadata (i.e. conditions, bioreplicates). Please see Help tab for information on creating this file.", class = "icon-tooltip")), fileInput(ns('annot'), "", multiple = FALSE, accept = c(".csv")) - ) + )) } -#' Create MSstats format file uploads +#' Create MSstats format file uploads (visibility driven server-side). #' @noRd create_msstats_uploads <- function(ns) { tagList( # Regular MSstats format - conditionalPanel( - condition = "input['loadpage-filetype'] == 'msstats' && (input['loadpage-BIO'] != 'PTM' && (input['loadpage-BIO'] != 'PTM' && input['loadpage-DDA_DIA'] != 'TMT'))", + shinyjs::hidden(div( + id = ns(NAMESPACE_LOADPAGE$msstats_regular_upload_panel), h4("4. Upload data in MSstats Format"), fileInput(ns('msstatsdata'), "", multiple = FALSE, accept = NULL) - ), - - # PTM MSstats format - conditionalPanel( - condition = "input['loadpage-filetype'] == 'msstats' && (input['loadpage-BIO'] == 'PTM' || (input['loadpage-BIO'] == 'PTM' && input['loadpage-DDA_DIA'] == 'TMT'))", + )), + + # PTM MSstats format. (The original JS condition had a redundant TMT + # clause that collapsed to `BIO=='PTM'`; the server predicate folds it.) + shinyjs::hidden(div( + id = ns(NAMESPACE_LOADPAGE$msstats_ptm_upload_panel), h4("4. Upload PTM data in MSstats Format"), fileInput(ns('msstatsptmdata'), "", multiple = FALSE, accept = NULL), h4("5. (Optional) Upload unmodified data in MSstats Format"), fileInput(ns('unmod'), "", multiple = FALSE, accept = NULL), tags$br() - ) + )) ) } -#' Create Skyline file uploads +#' Create Skyline file uploads (visibility driven server-side). #' @noRd create_skyline_uploads <- function(ns) { - conditionalPanel( - condition = "input['loadpage-filetype'] == 'sky' && input['loadpage-BIO'] != 'PTM'", + shinyjs::hidden(div( + id = ns(NAMESPACE_LOADPAGE$skyline_upload_panel), h4("4. Upload MSstats report from Skyline"), fileInput(ns('skylinedata'), "", multiple = FALSE, accept = NULL) - ) + )) } #' Create DIANN file uploads @@ -410,7 +414,7 @@ create_diann_large_annotation_ui <- function(ns, calculate_anomaly_def = FALSE) # Big-file-path anomaly run-order fileInput (visibility driven) # server-side. Kept inside this helper (called from the diann_options_ui # renderUI) so the fileInput is mounted whenever the parent big-file UI - # is. The matching observer in register_diann_visibility_observers + # is. The matching observer in register_loadpage_visibility_observers # toggles it on `big_diann_calculate_anomaly_scores`. shinyjs::hidden(div( id = ns(NAMESPACE_LOADPAGE$big_diann_anomaly_run_order_panel), @@ -495,6 +499,18 @@ create_spectronaut_large_bottom_ui <- function(ns, max_feature_def = 20, unique_ #' Create Spectronaut large file annotation override + anomaly UI #' +#' Note: this helper is invoked from `output$spectronaut_options_ui` +#' (server-side renderUI) only when `big_file_spec == TRUE` and +#' `is_web_server == FALSE`. It re-declares `ns("calculate_anomaly_scores")` +#' and `ns("run_order_file")` — the same ns() ids declared statically in +#' `create_quality_filtering_options` for the Spectronaut REGULAR path. +#' The regular-path pair is the documented Phase 2 carveout (stays as +#' `conditionalPanel`, see the comment in `create_quality_filtering_options`): +#' migrating either side to a permanently-mounted hidden div would create a +#' duplicate-ns()-id collision, and renderUI is ruled out because +#' `run_order_file` is a fileInput whose uploaded value cannot survive a +#' rebuild. The big-file conditionalPanel below stays for the same reason. +#' #' @noRd create_spectronaut_large_annotation_ui <- function(ns, calculate_anomaly_def = FALSE) { tagList( @@ -514,6 +530,8 @@ create_spectronaut_large_annotation_ui <- function(ns, calculate_anomaly_def = F div("Runs the same anomaly scoring pipeline as the regular Spectronaut path: the converter carries FG.ShapeQualityScore (MS2)/(MS1) and EGDeltaRT through the out-of-memory steps, then MSstatsConvert::MSstatsAnomalyScores fits the isolation-forest model on the collected data and adds an AnomalyScores column. Requires a run order CSV.", class = "icon-tooltip")), value = calculate_anomaly_def), + # CARVEOUT (big-file side): same duplicate-ns() rationale as the + # regular-path carveout below. Stays as conditionalPanel. conditionalPanel( condition = sprintf("input['%s']", ns("calculate_anomaly_scores")), fileInput(ns("run_order_file"), @@ -527,60 +545,60 @@ create_spectronaut_large_annotation_ui <- function(ns, calculate_anomaly_def = F ) } -#' Create PTM FragPipe uploads +#' Create PTM FragPipe uploads (visibility driven server-side). #' @noRd create_ptm_fragpipe_uploads <- function(ns) { - conditionalPanel( - condition = "input['loadpage-filetype'] == 'phil' && input['loadpage-BIO'] == 'PTM'", + shinyjs::hidden(div( + id = ns(NAMESPACE_LOADPAGE$ptm_fragpipe_upload_panel), h4("4. Upload PTM msstats dataset"), fileInput(ns('ptmdata'), "", multiple = FALSE, accept = NULL), - + h4("5. Upload PTM annotation file"), fileInput(ns('annotation'), "", multiple = FALSE, accept = c(".csv")), - + h4("6. Upload global profiling msstats dataset (optional)"), fileInput(ns('globaldata'), "", multiple = FALSE, accept = NULL), - + h4("7. Upload global profiling annotation file (optional)"), fileInput(ns('globalannotation'), "", multiple = FALSE, accept = c(".csv")), - + h4("Select the options for pre-processing"), - textInput(ns("mod_id_col"), - h5("Please enter the name of the modification id column", class = "icon-wrapper", + textInput(ns("mod_id_col"), + h5("Please enter the name of the modification id column", class = "icon-wrapper", icon("question-circle", lib = "font-awesome"), div("Only part of the string is required. For example if your mod id column is named 'STY.1221.12' you only need to enter 'STY' here.", class = "icon-tooltip")), value = "STY"), - - textInput(ns("localization_cutoff"), - h5("Please enter the localization_cutoff", class = "icon-wrapper", + + textInput(ns("localization_cutoff"), + h5("Please enter the localization_cutoff", class = "icon-wrapper", icon("question-circle", lib = "font-awesome"), div("The probability cutoff used to determine if a modification should be marked or not. If a site cannot be localized it may be dropped depending on the option below.", class = "icon-tooltip")), value = ".75"), - - radioButtons(ns("remove_unlocalized_peptides"), - h5("Remove unlocalized peptides", class = "icon-wrapper", + + radioButtons(ns("remove_unlocalized_peptides"), + h5("Remove unlocalized peptides", class = "icon-wrapper", icon("question-circle", lib = "font-awesome"), div("Should peptides without all sites localized be kept or removed.", class = "icon-tooltip")), c(Yes=TRUE, No=FALSE), inline=TRUE) - ) + )) } -#' Create MaxQuant file uploads +#' Create MaxQuant file uploads (visibility driven server-side). #' @noRd create_maxquant_uploads <- function(ns) { - conditionalPanel( - condition = "input['loadpage-filetype'] == 'maxq' && input['loadpage-BIO'] != 'PTM' && (input['loadpage-DDA_DIA'] == 'TMT' || input['loadpage-DDA_DIA'] == 'LType')", + shinyjs::hidden(div( + id = ns(NAMESPACE_LOADPAGE$maxquant_upload_panel), h4("4. Upload evidence.txt File"), fileInput(ns('evidence'), "", multiple = FALSE, accept = NULL), - + h4("5. Upload proteinGroups.txt File"), fileInput(ns('pGroup'), "", multiple = FALSE, accept = NULL), - - h4("6. Upload annotation File", class = "icon-wrapper", + + h4("6. Upload annotation File", class = "icon-wrapper", icon("question-circle", lib = "font-awesome"), div("Upload manually created annotation file. This file maps MS runs to experiment metadata (i.e. conditions, bioreplicates). Please see Help tab for information on creating this file.", class = "icon-tooltip")), fileInput(ns('annot1'), "", multiple = FALSE, accept = c(".csv")) - ) + )) } #' Create modification ID selector UI for Metamorpheus PTM @@ -617,39 +635,42 @@ create_meta_mod_id_selector <- function(ns, mod_choices = character(0)) { } } -#' Create PTM file uploads (for MaxQuant, PD, Spectronaut, Skyline) +#' Create PTM file uploads (for MaxQuant, PD, Spectronaut, Skyline, Metamorpheus). +#' Visibility driven server-side. Redundant TMT clauses in the original JS +#' conditions collapse away (`BIO=='PTM' || (BIO=='PTM' && DDA_DIA=='TMT')` +#' is just `BIO=='PTM'`). #' @noRd create_ptm_uploads <- function(ns) { tagList( - conditionalPanel( - condition = "(input['loadpage-filetype'] == 'maxq' || input['loadpage-filetype'] == 'PD' || input['loadpage-filetype'] == 'spec' || input['loadpage-filetype'] == 'sky' || input['loadpage-filetype'] == 'meta') && (input['loadpage-BIO'] == 'PTM' || (input['loadpage-BIO'] == 'PTM' && input['loadpage-DDA_DIA'] == 'TMT'))", + shinyjs::hidden(div( + id = ns(NAMESPACE_LOADPAGE$ptm_uploads_panel), h4("4. Upload PTM Input File"), fileInput(ns('ptm_input'), "", multiple = FALSE, accept = NULL), - - h4("5. Upload annotation File", class = "icon-wrapper", + + h4("5. Upload annotation File", class = "icon-wrapper", icon("question-circle", lib = "font-awesome"), div("Upload manually created annotation file. This file maps MS runs to experiment metadata (i.e. conditions, bioreplicates). Please see Help tab for information on creating this file.", class = "icon-tooltip")), fileInput(ns('ptm_annot'), "", multiple = FALSE, accept = c(".csv")), - - h4("6. Upload fasta File", class = "icon-wrapper", + + h4("6. Upload fasta File", class = "icon-wrapper", icon("question-circle", lib = "font-awesome"), div("Upload FASTA file. This file allows us to identify where in the protein sequence a modification occurs.", class = "icon-tooltip")), fileInput(ns('fasta'), "", multiple = FALSE), - + h4("7. (Recommended) Upload Unmodified Protein Input File"), fileInput(ns('ptm_protein_input'), "", multiple = FALSE, accept = NULL) - ), - + )), + # MaxQuant specific PTM - conditionalPanel( - condition = "(input['loadpage-filetype'] == 'maxq') && (input['loadpage-BIO'] == 'PTM' || (input['loadpage-BIO'] == 'PTM' && input['loadpage-DDA_DIA'] == 'TMT'))", + shinyjs::hidden(div( + id = ns(NAMESPACE_LOADPAGE$ptm_maxquant_pgroup_panel), h4("8. (Optional) Upload Unmodified Protein proteinGroups.txt File"), fileInput(ns('ptm_pgroup'), "", multiple = FALSE, accept = NULL) - ), + )), # Metamorpheus specific PTM - conditionalPanel( - condition = "(input['loadpage-filetype'] == 'meta') && (input['loadpage-BIO'] == 'PTM')", + shinyjs::hidden(div( + id = ns(NAMESPACE_LOADPAGE$ptm_metamorpheus_extras_panel), h4("8. (Recommended) Upload Unmodified Protein Annotation File"), fileInput( ns("ptm_protein_annot"), @@ -659,71 +680,74 @@ create_ptm_uploads <- function(ns) { ), uiOutput(ns("mod_id_meta_ui")) - ), - + )), + # PTM modification labels create_ptm_modification_labels(ns), - + # FASTA file column name - conditionalPanel( - condition = "(input['loadpage-filetype'] == 'maxq' || input['loadpage-filetype'] == 'PD' || input['loadpage-filetype'] == 'spec' || input['loadpage-filetype'] == 'sky' || input['loadpage-filetype'] == 'meta') && (input['loadpage-BIO'] == 'PTM' || (input['loadpage-BIO'] == 'PTM' && input['loadpage-DDA_DIA'] == 'TMT'))", - h4("FASTA file column name", class = "icon-wrapper", + shinyjs::hidden(div( + id = ns(NAMESPACE_LOADPAGE$ptm_fasta_id_column_panel), + h4("FASTA file column name", class = "icon-wrapper", icon("question-circle", lib = "font-awesome"), div("Name of column in FASTA file that matches with Protein name column in input. It is critical the values in both columns match so that the modfication can be identified.", class = "icon-tooltip")), textInput(ns("fasta_id_column"), "", value="uniprot_iso") - ) + )) ) } -#' Create PTM modification label inputs +#' Create PTM modification label inputs (visibility driven server-side). +#' These three panels are mutually exclusive at runtime — one per converter. +#' The original JS conditions had a redundant `|| (BIO=='PTM' && DDA_DIA=='TMT')` +#' clause that the server predicates fold away to `BIO=='PTM' && filetype==`. #' @noRd create_ptm_modification_labels <- function(ns) { tagList( - conditionalPanel( - condition = "(input['loadpage-filetype'] == 'maxq') && (input['loadpage-BIO'] == 'PTM' || (input['loadpage-BIO'] == 'PTM' && input['loadpage-DDA_DIA'] == 'TMT'))", - h4("Modification Label", class = "icon-wrapper", + shinyjs::hidden(div( + id = ns(NAMESPACE_LOADPAGE$ptm_mod_id_maxq_panel), + h4("Modification Label", class = "icon-wrapper", icon("question-circle", lib = "font-awesome"), div("Indicate if experiment was processed using TMT labeling", class = "icon-tooltip")), textInput(ns("mod_id_maxq"), "", value="\\(Phospho \\(STY\\)\\)") - ), - - conditionalPanel( - condition = "(input['loadpage-filetype'] == 'PD') && (input['loadpage-BIO'] == 'PTM' || (input['loadpage-BIO'] == 'PTM' && input['loadpage-DDA_DIA'] == 'TMT'))", - h4("Modification Label", class = "icon-wrapper", + )), + + shinyjs::hidden(div( + id = ns(NAMESPACE_LOADPAGE$ptm_mod_id_pd_panel), + h4("Modification Label", class = "icon-wrapper", icon("question-circle", lib = "font-awesome"), div("Indicate if experiment was processed using TMT labeling", class = "icon-tooltip")), textInput(ns("mod_id_pd"), "", value="\\(Phospho\\)") - ), - - conditionalPanel( - condition = "(input['loadpage-filetype'] == 'spec') && (input['loadpage-BIO'] == 'PTM' || (input['loadpage-BIO'] == 'PTM' && input['loadpage-DDA_DIA'] == 'TMT'))", - h4("Modification Label", class = "icon-wrapper", + )), + + shinyjs::hidden(div( + id = ns(NAMESPACE_LOADPAGE$ptm_mod_id_spec_panel), + h4("Modification Label", class = "icon-wrapper", icon("question-circle", lib = "font-awesome"), div("Indicate if experiment was processed using TMT labeling", class = "icon-tooltip")), textInput(ns("mod_id_spec"), "", value="\\[Phospho \\(STY\\)\\]") - ) + )) ) } -#' Create DIA-Umpire file uploads +#' Create DIA-Umpire file uploads (visibility driven server-side). #' @noRd create_ump_uploads <- function(ns) { - conditionalPanel( - condition = "input['loadpage-filetype'] == 'ump'", + shinyjs::hidden(div( + id = ns(NAMESPACE_LOADPAGE$dia_umpire_upload_panel), h4("4. Upload FragSummary.xls File"), fileInput(ns('fragSummary'), "", multiple = FALSE, accept = NULL), - + h4("5. Upload PeptideSummary.xls File"), fileInput(ns('peptideSummary'), "", multiple = FALSE, accept = NULL), - + h4("6. Upload ProtSummary.xls File"), fileInput(ns('protSummary'), "", multiple = FALSE, accept = NULL), - - h4("7. Upload Annotation File", class = "icon-wrapper", + + h4("7. Upload Annotation File", class = "icon-wrapper", icon("question-circle", lib = "font-awesome"), div("Upload manually created annotation file. This file maps MS runs to experiment metadata (i.e. conditions, bioreplicates). Please see Help tab for information on creating this file.", class = "icon-tooltip")), fileInput(ns('annot2'), "", multiple = FALSE, accept = c(".csv")) - ) + )) } #' Create processing options @@ -740,47 +764,47 @@ create_processing_options <- function(ns) { ) } -#' Create TMT processing options +#' Create TMT processing options (rendered server-side). +#' +#' The previous code declared `ns("which.proteinid")` in two mutually +#' exclusive `conditionalPanel`s with different defaults (PD -> +#' "Protein.Accessions", MaxQuant -> "Proteins"). Mounting both as hidden +#' divs would collide on a single ns() id. The single +#' `output[[tmt_options_ui]]` renderUI in R/loadpage-server-rendering.R +#' replaces both panels — it emits one textInput with the converter- +#' appropriate default on first build and carries the user's typed value +#' across filetype flips via isolate(). #' @noRd create_tmt_options <- function(ns) { - tagList( - conditionalPanel( - condition = "input['loadpage-filetype'] && input['loadpage-DDA_DIA'] == 'TMT' && input['loadpage-filetype'] == 'PD'", - h4("Select the options for pre-processing"), - textInput(ns("which.proteinid"), - h5("Protein Name Column", class = "icon-wrapper", - icon("question-circle", lib = "font-awesome"), - div("Enter the column in your data containing protein names", class = "icon-tooltip")), - value = "Protein.Accessions") - ), - - conditionalPanel( - condition = "input['loadpage-filetype'] && input['loadpage-DDA_DIA'] == 'TMT' && input['loadpage-filetype'] == 'maxq'", - h4("Select the options for pre-processing"), - textInput(ns("which.proteinid"), - h5("Protein Name Column", class = "icon-wrapper", - icon("question-circle", lib = "font-awesome"), - div("Enter the column in your data containing protein names", class = "icon-tooltip")), - value = "Proteins") - ) - ) + uiOutput(ns(NAMESPACE_LOADPAGE$tmt_options_ui)) } -#' Create label-free processing options +#' Create label-free processing options (visibility driven server-side). +#' +#' The inner `create_quality_filtering_options(ns)` helper still contains +#' two `conditionalPanel`s for the Spectronaut regular-path anomaly +#' checkbox + nested run-order fileInput. Those are NOT migrated to +#' show/hide because they share `ns("calculate_anomaly_scores")` / +#' `ns("run_order_file")` with the big-file Spectronaut helper that +#' `output$spectronaut_options_ui` emits. Mounting both as hidden divs +#' would collide on a duplicate ns() id; routing to renderUI would lose +#' the user's uploaded run-order CSV (fileInput state cannot be re-seeded +#' on rebuild). See the matching comment in +#' `create_quality_filtering_options` below. #' @noRd create_label_free_options <- function(ns) { tagList( - conditionalPanel( - condition = "input['loadpage-filetype'] && input['loadpage-DDA_DIA'] == 'LType' && input['loadpage-filetype'] != 'sample' && (input['loadpage-filetype'] != 'spec' || !input['loadpage-big_file_spec']) && (input['loadpage-filetype'] != 'diann' || !input['loadpage-big_file_diann'])", + shinyjs::hidden(div( + id = ns(NAMESPACE_LOADPAGE$label_free_options_panel), h4("Select the options for pre-processing"), checkboxInput(ns("unique_peptides"), "Use unique peptides", value = TRUE), checkboxInput(ns("remove"), "Remove proteins with 1 feature", value = FALSE), # Quality filtering options create_quality_filtering_options(ns) - ), - + )), + # DIANN specific options — visibility driven server-side - # (R/loadpage-server-rendering.R::register_diann_visibility_observers). + # (R/loadpage-server-rendering.R::register_loadpage_visibility_observers). shinyjs::hidden(div( id = ns(NAMESPACE_LOADPAGE$diann_lf_options_panel), checkboxInput(ns(NAMESPACE_LOADPAGE$diann_2plus), "DIANN 2.0+", value = FALSE), @@ -818,6 +842,19 @@ create_quality_filtering_options <- function(ns) { )) )), + # === Spectronaut regular-path anomaly checkbox + nested run-order === + # CARVEOUT: these two `conditionalPanel`s are intentionally NOT migrated + # to server-side show/hide. `ns("calculate_anomaly_scores")` and + # `ns("run_order_file")` are ALSO declared by the big-file Spectronaut + # helper (`create_spectronaut_large_annotation_ui`) that + # `output$spectronaut_options_ui` emits when `big_file_spec == TRUE`. + # Migrating them to a permanently-mounted hidden div would create a + # duplicate-ns()-id collision with the big-file path. renderUI is also + # ruled out because `run_order_file` is a fileInput whose uploaded value + # cannot survive a rebuild. Today's `conditionalPanel` keeps the static + # tree at `display:none` whenever the big-file path is active, leaving + # only the renderUI'd big-file copy mounted — which is what we want. + # See R/loadpage-server-rendering.R for the broader carveout note. conditionalPanel( condition = "input['loadpage-filetype'] == 'spec'", checkboxInput(ns("calculate_anomaly_scores"), @@ -829,6 +866,8 @@ create_quality_filtering_options <- function(ns) { class = "icon-tooltip") ), value = FALSE), + # CARVEOUT (nested): same reason — `ns("run_order_file")` collides with + # the big-file copy. Stay as conditionalPanel. conditionalPanel( condition = "input['loadpage-calculate_anomaly_scores']", fileInput(ns("run_order_file"), @@ -872,13 +911,18 @@ create_quality_filtering_options <- function(ns) { )) )), - conditionalPanel( - condition = "input['loadpage-filetype'] == 'open'", - checkboxInput(ns("m_score"), "Filter with M-score"), - conditionalPanel( - condition = "input['loadpage-m_score']", + # OpenSWATH M-score filter — visibility driven server-side. The nested + # cutoff numeric must stay mounted across `m_score` toggles to preserve + # the user's value; predicate AND-includes the parent `filetype=='open'` + # so swapping converter hides the inner div even if `m_score` is still + # TRUE. + shinyjs::hidden(div( + id = ns(NAMESPACE_LOADPAGE$openswath_mscore_panel), + checkboxInput(ns(NAMESPACE_LOADPAGE$m_score), "Filter with M-score"), + shinyjs::hidden(div( + id = ns(NAMESPACE_LOADPAGE$openswath_mscore_cutoff_panel), numericInput(ns("m_cutoff"), "M-score cutoff", 0.01, 0, 1, 0.01) - ) - ) + )) + )) ) } diff --git a/tests/testthat/test-loadpage-server-rendering.R b/tests/testthat/test-loadpage-server-rendering.R index c96edca..aa455cf 100644 --- a/tests/testthat/test-loadpage-server-rendering.R +++ b/tests/testthat/test-loadpage-server-rendering.R @@ -107,3 +107,389 @@ test_that("NAMESPACE_LOADPAGE retains literal string values (no renames in Phase expect_equal(NAMESPACE_LOADPAGE$big_diann_run_order_file, "big_diann_run_order_file") }) + + +# ============================================================================ +# Phase 2 predicate truth tables + namespace assertions +# ============================================================================ + +test_that("loadpage_show_sample_dataset_description matches one mode at a time", { + for (mode in c("DDA", "DIA", "SRM_PRM")) { + # Active mode positive + expect_true(MSstatsShiny:::loadpage_show_sample_dataset_description("sample", mode, mode), + info = paste("active mode", mode)) + # Wrong filetype always FALSE + for (ft in c("diann", "sky", "spec", "maxq", "msstats", NULL)) { + expect_false(MSstatsShiny:::loadpage_show_sample_dataset_description(ft, mode, mode), + info = paste("ft", ft %||% "NULL", "mode", mode)) + } + # Wrong LabelFreeType FALSE + other_modes <- setdiff(c("DDA", "DIA", "SRM_PRM"), mode) + for (other in other_modes) { + expect_false(MSstatsShiny:::loadpage_show_sample_dataset_description("sample", other, mode), + info = paste("target", mode, "actual", other)) + } + # NULL LabelFreeType FALSE + expect_false(MSstatsShiny:::loadpage_show_sample_dataset_description("sample", NULL, mode)) + } +}) + +test_that("loadpage_show_label_free_type_selection requires non-PTM + sample + LType", { + expect_true(MSstatsShiny:::loadpage_show_label_free_type_selection("Protein", "sample", "LType")) + expect_true(MSstatsShiny:::loadpage_show_label_free_type_selection("Peptide", "sample", "LType")) + + # NULL bio behaves like "not PTM" by design — the original JS condition + # `input['loadpage-BIO'] != 'PTM'` is TRUE for unset BIO (in JS, + # `null != 'PTM'` is true), so the original `conditionalPanel` did show + # the LabelFreeType selector at startup once `filetype=='sample'` and + # `DDA_DIA=='LType'` were selected even if BIO was still untouched. The + # predicate mirrors that behavior; do not regress it. + expect_true(MSstatsShiny:::loadpage_show_label_free_type_selection(NULL, "sample", "LType")) + + expect_false(MSstatsShiny:::loadpage_show_label_free_type_selection("PTM", "sample", "LType")) + expect_false(MSstatsShiny:::loadpage_show_label_free_type_selection("Protein", "diann", "LType")) + expect_false(MSstatsShiny:::loadpage_show_label_free_type_selection("Protein", "sample", "TMT")) +}) + +test_that("loadpage_show_standard_quant_upload covers non-PTM converters only", { + for (ft in c("10col", "prog", "PD", "open", "openms", "spmin", "phil", "meta")) { + expect_true(MSstatsShiny:::loadpage_show_standard_quant_upload(ft, "Protein"), + info = paste("ft", ft)) + expect_true(MSstatsShiny:::loadpage_show_standard_quant_upload(ft, "Peptide"), + info = paste("ft Peptide", ft)) + expect_false(MSstatsShiny:::loadpage_show_standard_quant_upload(ft, "PTM"), + info = paste("ft", ft, "PTM")) + } + for (ft in c("diann", "sky", "spec", "maxq", "ump", "msstats", "sample", NULL)) { + expect_false(MSstatsShiny:::loadpage_show_standard_quant_upload(ft, "Protein"), + info = paste("excluded ft", ft %||% "NULL")) + } +}) + +test_that("loadpage_show_standard_annot_upload — Spectronaut/DIANN gated by big-file", { + expect_false(MSstatsShiny:::loadpage_show_standard_annot_upload("spec", "Protein", FALSE, FALSE) == FALSE) + expect_true(MSstatsShiny:::loadpage_show_standard_annot_upload("spec", "Protein", FALSE, FALSE)) + expect_false(MSstatsShiny:::loadpage_show_standard_annot_upload("spec", "Protein", TRUE, FALSE)) + expect_true(MSstatsShiny:::loadpage_show_standard_annot_upload("diann", "Protein", FALSE, FALSE)) + expect_false(MSstatsShiny:::loadpage_show_standard_annot_upload("diann","Protein", FALSE, TRUE)) + + for (ft in c("sky", "prog", "PD", "open", "spmin", "phil", "meta")) { + expect_true(MSstatsShiny:::loadpage_show_standard_annot_upload(ft, "Protein", FALSE, FALSE), + info = paste("ft", ft)) + expect_false(MSstatsShiny:::loadpage_show_standard_annot_upload(ft, "PTM", FALSE, FALSE), + info = paste("ft", ft, "PTM")) + } + expect_false(MSstatsShiny:::loadpage_show_standard_annot_upload("maxq", "Protein", FALSE, FALSE)) + expect_false(MSstatsShiny:::loadpage_show_standard_annot_upload(NULL, "Protein", FALSE, FALSE)) +}) + +test_that("loadpage_show_msstats_regular_upload is non-PTM label-free only", { + expect_true(MSstatsShiny:::loadpage_show_msstats_regular_upload("msstats", "Protein", "LType")) + expect_true(MSstatsShiny:::loadpage_show_msstats_regular_upload("msstats", "Peptide", "LType")) + expect_false(MSstatsShiny:::loadpage_show_msstats_regular_upload("msstats", "Protein", "TMT")) + expect_false(MSstatsShiny:::loadpage_show_msstats_regular_upload("msstats", "PTM", "LType")) + expect_false(MSstatsShiny:::loadpage_show_msstats_regular_upload("diann", "Protein", "LType")) +}) + +test_that("loadpage_show_msstats_ptm_upload is PTM only (collapsed TMT clause)", { + expect_true(MSstatsShiny:::loadpage_show_msstats_ptm_upload("msstats", "PTM")) + expect_false(MSstatsShiny:::loadpage_show_msstats_ptm_upload("msstats", "Protein")) + expect_false(MSstatsShiny:::loadpage_show_msstats_ptm_upload("diann", "PTM")) +}) + +test_that("loadpage_show_skyline_upload is non-PTM Skyline", { + expect_true(MSstatsShiny:::loadpage_show_skyline_upload("sky", "Protein")) + expect_true(MSstatsShiny:::loadpage_show_skyline_upload("sky", "Peptide")) + expect_false(MSstatsShiny:::loadpage_show_skyline_upload("sky", "PTM")) + expect_false(MSstatsShiny:::loadpage_show_skyline_upload("diann", "Protein")) +}) + +test_that("loadpage_show_ptm_fragpipe_upload is PTM FragPipe only", { + expect_true(MSstatsShiny:::loadpage_show_ptm_fragpipe_upload("phil", "PTM")) + expect_false(MSstatsShiny:::loadpage_show_ptm_fragpipe_upload("phil", "Protein")) + expect_false(MSstatsShiny:::loadpage_show_ptm_fragpipe_upload("maxq", "PTM")) +}) + +test_that("loadpage_show_maxquant_upload is non-PTM MaxQuant under TMT or LType", { + for (dd in c("TMT", "LType")) { + expect_true(MSstatsShiny:::loadpage_show_maxquant_upload("maxq", "Protein", dd), + info = paste("dd", dd)) + expect_false(MSstatsShiny:::loadpage_show_maxquant_upload("maxq", "PTM", dd), + info = paste("dd", dd, "PTM")) + } + expect_false(MSstatsShiny:::loadpage_show_maxquant_upload("sky", "Protein", "TMT")) + expect_false(MSstatsShiny:::loadpage_show_maxquant_upload("maxq", "Protein", NULL)) +}) + +test_that("loadpage_show_ptm_uploads collapses the redundant TMT clause", { + for (ft in c("maxq", "PD", "spec", "sky", "meta")) { + expect_true(MSstatsShiny:::loadpage_show_ptm_uploads(ft, "PTM"), + info = paste("ft", ft)) + expect_false(MSstatsShiny:::loadpage_show_ptm_uploads(ft, "Protein"), + info = paste("ft", ft, "Protein")) + } + expect_false(MSstatsShiny:::loadpage_show_ptm_uploads("diann", "PTM")) + expect_false(MSstatsShiny:::loadpage_show_ptm_uploads("phil", "PTM")) # phil has its own uploader + expect_false(MSstatsShiny:::loadpage_show_ptm_uploads(NULL, "PTM")) +}) + +test_that("loadpage_show_ptm_maxquant_pgroup is MaxQuant PTM only", { + expect_true(MSstatsShiny:::loadpage_show_ptm_maxquant_pgroup("maxq", "PTM")) + expect_false(MSstatsShiny:::loadpage_show_ptm_maxquant_pgroup("maxq", "Protein")) + expect_false(MSstatsShiny:::loadpage_show_ptm_maxquant_pgroup("PD", "PTM")) +}) + +test_that("loadpage_show_ptm_metamorpheus_extras is Metamorpheus PTM only", { + expect_true(MSstatsShiny:::loadpage_show_ptm_metamorpheus_extras("meta", "PTM")) + expect_false(MSstatsShiny:::loadpage_show_ptm_metamorpheus_extras("meta", "Protein")) + expect_false(MSstatsShiny:::loadpage_show_ptm_metamorpheus_extras("maxq", "PTM")) +}) + +test_that("loadpage_show_ptm_fasta_id_column matches ptm_uploads gate exactly", { + for (ft in c("maxq", "PD", "spec", "sky", "meta", "diann", "phil", NULL)) { + for (bio in c("PTM", "Protein", "Peptide")) { + expect_equal( + MSstatsShiny:::loadpage_show_ptm_fasta_id_column(ft, bio), + MSstatsShiny:::loadpage_show_ptm_uploads(ft, bio), + info = paste("ft", ft %||% "NULL", "bio", bio) + ) + } + } +}) + +test_that("loadpage_show_ptm_mod_id_maxq / pd / spec gate on PTM + the matching filetype", { + expect_true(MSstatsShiny:::loadpage_show_ptm_mod_id_maxq("maxq", "PTM")) + expect_false(MSstatsShiny:::loadpage_show_ptm_mod_id_maxq("maxq", "Protein")) + expect_false(MSstatsShiny:::loadpage_show_ptm_mod_id_maxq("PD", "PTM")) + + expect_true(MSstatsShiny:::loadpage_show_ptm_mod_id_pd("PD", "PTM")) + expect_false(MSstatsShiny:::loadpage_show_ptm_mod_id_pd("PD", "Protein")) + expect_false(MSstatsShiny:::loadpage_show_ptm_mod_id_pd("maxq", "PTM")) + + expect_true(MSstatsShiny:::loadpage_show_ptm_mod_id_spec("spec", "PTM")) + expect_false(MSstatsShiny:::loadpage_show_ptm_mod_id_spec("spec", "Protein")) + expect_false(MSstatsShiny:::loadpage_show_ptm_mod_id_spec("PD", "PTM")) +}) + +test_that("loadpage_show_dia_umpire_upload is just filetype == 'ump'", { + expect_true(MSstatsShiny:::loadpage_show_dia_umpire_upload("ump")) + expect_false(MSstatsShiny:::loadpage_show_dia_umpire_upload("diann")) + expect_false(MSstatsShiny:::loadpage_show_dia_umpire_upload(NULL)) +}) + +test_that("loadpage_show_label_free_options excludes sample + big-file paths", { + # baseline label-free converter visible + expect_true(MSstatsShiny:::loadpage_show_label_free_options("sky", "LType", FALSE, FALSE)) + expect_true(MSstatsShiny:::loadpage_show_label_free_options("maxq", "LType", FALSE, FALSE)) + # sample excluded + expect_false(MSstatsShiny:::loadpage_show_label_free_options("sample", "LType", FALSE, FALSE)) + # TMT excluded + expect_false(MSstatsShiny:::loadpage_show_label_free_options("maxq", "TMT", FALSE, FALSE)) + # NULL filetype excluded + expect_false(MSstatsShiny:::loadpage_show_label_free_options(NULL, "LType", FALSE, FALSE)) + # big-file Spectronaut excluded; small-file allowed + expect_false(MSstatsShiny:::loadpage_show_label_free_options("spec", "LType", TRUE, FALSE)) + expect_true(MSstatsShiny:::loadpage_show_label_free_options("spec", "LType", FALSE, FALSE)) + # big-file DIANN excluded; small-file allowed + expect_false(MSstatsShiny:::loadpage_show_label_free_options("diann", "LType", FALSE, TRUE)) + expect_true(MSstatsShiny:::loadpage_show_label_free_options("diann", "LType", FALSE, FALSE)) +}) + +test_that("loadpage_show_openswath_mscore is filetype == 'open'", { + expect_true(MSstatsShiny:::loadpage_show_openswath_mscore("open")) + expect_false(MSstatsShiny:::loadpage_show_openswath_mscore("sky")) + expect_false(MSstatsShiny:::loadpage_show_openswath_mscore(NULL)) +}) + +test_that("loadpage_show_openswath_mscore_cutoff gates on full ancestor chain (regression check)", { + # Both clauses TRUE → visible + expect_true(MSstatsShiny:::loadpage_show_openswath_mscore_cutoff("open", TRUE)) + + # m_score TRUE but filetype wrong → HIDDEN (the regression a naive + # immediate-driver-only predicate would introduce) + expect_false(MSstatsShiny:::loadpage_show_openswath_mscore_cutoff("sky", TRUE)) + expect_false(MSstatsShiny:::loadpage_show_openswath_mscore_cutoff("diann", TRUE)) + expect_false(MSstatsShiny:::loadpage_show_openswath_mscore_cutoff(NULL, TRUE)) + + # filetype right but m_score off → hidden + expect_false(MSstatsShiny:::loadpage_show_openswath_mscore_cutoff("open", FALSE)) + expect_false(MSstatsShiny:::loadpage_show_openswath_mscore_cutoff("open", NULL)) +}) + +test_that("loadpage_show_tmt_options gates on DDA_DIA == 'TMT' AND filetype in {PD, maxq}", { + expect_true(MSstatsShiny:::loadpage_show_tmt_options("PD", "TMT")) + expect_true(MSstatsShiny:::loadpage_show_tmt_options("maxq", "TMT")) + + expect_false(MSstatsShiny:::loadpage_show_tmt_options("PD", "LType")) + expect_false(MSstatsShiny:::loadpage_show_tmt_options("maxq", "LType")) + expect_false(MSstatsShiny:::loadpage_show_tmt_options("sky", "TMT")) + expect_false(MSstatsShiny:::loadpage_show_tmt_options(NULL, "TMT")) +}) + +test_that("loadpage_default_proteinid_for_filetype picks the right default per converter", { + expect_equal(MSstatsShiny:::loadpage_default_proteinid_for_filetype("PD"), "Protein.Accessions") + expect_equal(MSstatsShiny:::loadpage_default_proteinid_for_filetype("maxq"), "Proteins") + expect_null(MSstatsShiny:::loadpage_default_proteinid_for_filetype("sky")) + expect_null(MSstatsShiny:::loadpage_default_proteinid_for_filetype(NULL)) +}) + + +# ---------------------------------------------------------------------------- +# `loadpage_seed_proteinid` — the typed-vs-default seeding rule. +# Pins the four cases that drive the TMT renderUI's seed value across +# converter switches. Failing any of these would re-introduce the +# "default-vs-typed" regression where switching PD <-> MaxQuant either +# clobbered a custom value or failed to update the per-converter default. +# ---------------------------------------------------------------------------- + +test_that("loadpage_seed_proteinid: first build applies incoming default", { + expect_equal(MSstatsShiny:::loadpage_seed_proteinid("PD", NULL, NULL), + "Protein.Accessions") + expect_equal(MSstatsShiny:::loadpage_seed_proteinid("maxq", NULL, NULL), + "Proteins") +}) + +test_that("loadpage_seed_proteinid: same-converter rebuild carries the current value", { + # No converter switch — keep whatever is currently in the textInput, whether + # default or typed. Covers renderUI re-evaluations triggered by deps that + # don't actually change the filetype. + expect_equal(MSstatsShiny:::loadpage_seed_proteinid("PD", "PD", "Protein.Accessions"), + "Protein.Accessions") + expect_equal(MSstatsShiny:::loadpage_seed_proteinid("PD", "PD", "MyProtCol"), + "MyProtCol") + expect_equal(MSstatsShiny:::loadpage_seed_proteinid("maxq", "maxq", "Proteins"), + "Proteins") + expect_equal(MSstatsShiny:::loadpage_seed_proteinid("maxq", "maxq", "Anything"), + "Anything") +}) + +test_that("loadpage_seed_proteinid: switch from outgoing-default applies incoming default", { + # The default-vs-typed distinction: preserved equals outgoing's default → + # user never typed → apply incoming default. This was the broken case where + # PD -> MaxQuant stayed on "Protein.Accessions". + expect_equal(MSstatsShiny:::loadpage_seed_proteinid("maxq", "PD", "Protein.Accessions"), + "Proteins") + expect_equal(MSstatsShiny:::loadpage_seed_proteinid("PD", "maxq", "Proteins"), + "Protein.Accessions") +}) + +test_that("loadpage_seed_proteinid: switch with custom typed value carries it across", { + # preserved differs from outgoing's default → user typed → carry verbatim, + # do NOT clobber with the incoming default. + expect_equal(MSstatsShiny:::loadpage_seed_proteinid("maxq", "PD", "MyProtCol"), + "MyProtCol") + expect_equal(MSstatsShiny:::loadpage_seed_proteinid("PD", "maxq", "MyProtCol"), + "MyProtCol") + expect_equal(MSstatsShiny:::loadpage_seed_proteinid("PD", "maxq", "AnotherCol"), + "AnotherCol") +}) + +test_that("loadpage_seed_proteinid: NULL preserved (post-unmount) re-applies incoming default", { + # When the user leaves TMT entirely the renderUI returns NULL, the textInput + # unmounts, and `input$which.proteinid` becomes NULL. On re-entry to TMT, + # `preserved` is NULL but the tracker `last_tmt_filetype` may still hold + # the previous filetype. Rule 1 (first-build) takes precedence and applies + # the incoming converter's default. Typed values do NOT survive a full TMT + # exit-and-return — this is the renderUI rebuild cost we accepted for the + # duplicate-ns() carveout. + expect_equal(MSstatsShiny:::loadpage_seed_proteinid("PD", "PD", NULL), + "Protein.Accessions") + expect_equal(MSstatsShiny:::loadpage_seed_proteinid("maxq", "PD", NULL), + "Proteins") + expect_equal(MSstatsShiny:::loadpage_seed_proteinid("PD", "maxq", NULL), + "Protein.Accessions") +}) + +test_that("loadpage_seed_proteinid: NULL outgoing with non-NULL preserved carries the value", { + # Conservative edge case: if outgoing is somehow NULL but preserved is set + # (the tracker never ran but the input has a value — unusual race / pre-fill + # / restoration), we cannot compare against an outgoing default. Carry the + # preserved value rather than clobber it. + expect_equal(MSstatsShiny:::loadpage_seed_proteinid("PD", NULL, "Protein.Accessions"), + "Protein.Accessions") + expect_equal(MSstatsShiny:::loadpage_seed_proteinid("maxq", NULL, "MyProtCol"), + "MyProtCol") +}) + +test_that("loadpage_seed_proteinid: full acceptance-test sequence (PD <-> MaxQuant)", { + # Threads the reactiveVal tracker by hand through the 6-step acceptance test + # from the PR brief. Each step asserts the seed value AND the tracker update + # the renderUI performs after computing the seed. + last_filetype <- NULL + + # 1. TMT + PD → "Protein.Accessions" (first build) + seed1 <- MSstatsShiny:::loadpage_seed_proteinid("PD", last_filetype, NULL) + expect_equal(seed1, "Protein.Accessions") + last_filetype <- "PD" + + # 2. switch to MaxQuant without typing → "Proteins" + seed2 <- MSstatsShiny:::loadpage_seed_proteinid("maxq", last_filetype, seed1) + expect_equal(seed2, "Proteins") + last_filetype <- "maxq" + + # 3. back to PD without typing → "Protein.Accessions" + seed3 <- MSstatsShiny:::loadpage_seed_proteinid("PD", last_filetype, seed2) + expect_equal(seed3, "Protein.Accessions") + last_filetype <- "PD" + + # 4. type "MyProtCol" under PD, switch to MaxQuant → "MyProtCol" (carried) + # (the user typing is simulated by passing "MyProtCol" as `preserved`.) + seed4 <- MSstatsShiny:::loadpage_seed_proteinid("maxq", last_filetype, "MyProtCol") + expect_equal(seed4, "MyProtCol") + last_filetype <- "maxq" + + # 5. back to PD → "MyProtCol" + seed5 <- MSstatsShiny:::loadpage_seed_proteinid("PD", last_filetype, seed4) + expect_equal(seed5, "MyProtCol") + last_filetype <- "PD" + + # 6. restart app, TMT + PD → "Protein.Accessions" + # Fresh session: tracker and preserved both reset to NULL. + fresh_seed <- MSstatsShiny:::loadpage_seed_proteinid("PD", NULL, NULL) + expect_equal(fresh_seed, "Protein.Accessions") +}) + + +# ---------------------------------------------------------------------------- +# Phase 2 namespace assertions — every new container/driver id matches the +# literal string `R/utils.R` (or the UI ns(...) wrappers) read. +# ---------------------------------------------------------------------------- + +test_that("NAMESPACE_LOADPAGE — Phase 2 driver IDs preserve literal values", { + expect_equal(NAMESPACE_LOADPAGE$label_free_type, "LabelFreeType") + expect_equal(NAMESPACE_LOADPAGE$big_file_spec, "big_file_spec") + expect_equal(NAMESPACE_LOADPAGE$calculate_anomaly_scores, "calculate_anomaly_scores") + expect_equal(NAMESPACE_LOADPAGE$m_score, "m_score") + expect_equal(NAMESPACE_LOADPAGE$which_proteinid, "which.proteinid") +}) + +test_that("NAMESPACE_LOADPAGE — Phase 2 container IDs match their UI div IDs", { + # Sample / LabelFreeType + expect_equal(NAMESPACE_LOADPAGE$sample_dda_description_panel, "sample_dda_description_panel") + expect_equal(NAMESPACE_LOADPAGE$sample_dia_description_panel, "sample_dia_description_panel") + expect_equal(NAMESPACE_LOADPAGE$sample_srm_prm_description_panel, "sample_srm_prm_description_panel") + expect_equal(NAMESPACE_LOADPAGE$label_free_type_selection_panel, "label_free_type_selection_panel") + # Converter uploads + expect_equal(NAMESPACE_LOADPAGE$standard_quant_upload_panel, "standard_quant_upload_panel") + expect_equal(NAMESPACE_LOADPAGE$standard_annot_upload_panel, "standard_annot_upload_panel") + expect_equal(NAMESPACE_LOADPAGE$msstats_regular_upload_panel, "msstats_regular_upload_panel") + expect_equal(NAMESPACE_LOADPAGE$msstats_ptm_upload_panel, "msstats_ptm_upload_panel") + expect_equal(NAMESPACE_LOADPAGE$skyline_upload_panel, "skyline_upload_panel") + expect_equal(NAMESPACE_LOADPAGE$ptm_fragpipe_upload_panel, "ptm_fragpipe_upload_panel") + expect_equal(NAMESPACE_LOADPAGE$maxquant_upload_panel, "maxquant_upload_panel") + expect_equal(NAMESPACE_LOADPAGE$dia_umpire_upload_panel, "dia_umpire_upload_panel") + # PTM cluster + expect_equal(NAMESPACE_LOADPAGE$ptm_uploads_panel, "ptm_uploads_panel") + expect_equal(NAMESPACE_LOADPAGE$ptm_maxquant_pgroup_panel, "ptm_maxquant_pgroup_panel") + expect_equal(NAMESPACE_LOADPAGE$ptm_metamorpheus_extras_panel, "ptm_metamorpheus_extras_panel") + expect_equal(NAMESPACE_LOADPAGE$ptm_fasta_id_column_panel, "ptm_fasta_id_column_panel") + expect_equal(NAMESPACE_LOADPAGE$ptm_mod_id_maxq_panel, "ptm_mod_id_maxq_panel") + expect_equal(NAMESPACE_LOADPAGE$ptm_mod_id_pd_panel, "ptm_mod_id_pd_panel") + expect_equal(NAMESPACE_LOADPAGE$ptm_mod_id_spec_panel, "ptm_mod_id_spec_panel") + # Options + OpenSWATH + expect_equal(NAMESPACE_LOADPAGE$label_free_options_panel, "label_free_options_panel") + expect_equal(NAMESPACE_LOADPAGE$openswath_mscore_panel, "openswath_mscore_panel") + expect_equal(NAMESPACE_LOADPAGE$openswath_mscore_cutoff_panel, "openswath_mscore_cutoff_panel") + # TMT renderUI slot + expect_equal(NAMESPACE_LOADPAGE$tmt_options_ui, "tmt_options_ui") +}) diff --git a/tests/testthat/test-module-loadpage-ui.R b/tests/testthat/test-module-loadpage-ui.R index d606d82..ee5d24e 100644 --- a/tests/testthat/test-module-loadpage-ui.R +++ b/tests/testthat/test-module-loadpage-ui.R @@ -60,27 +60,108 @@ test_that("loadpageUI contains all required radio button choices", { } }) -test_that("loadpageUI includes required conditional panels for different workflows", { - # Test that key conditional panels exist for different analysis types +test_that("loadpageUI mounts hidden visibility containers for migrated workflows", { + # The Phase 1 + Phase 2 refactor moved conditional UI off `conditionalPanel` + # and onto server-side `shinyjs::show/hide`. Each migrated panel is now + # wrapped in `shinyjs::hidden(div(id = ns(NAMESPACE_LOADPAGE$), ...))`, + # so the static UI contains the namespaced container divs (mounted, hidden) + # in place of the old JS condition strings. The driver inputs / file inputs + # inside live alongside, ready for the server's toggle observers. result <- loadpageUI("test") html_output <- as.character(result) - - # Check for conditional panel conditions that handle different workflows - # Note: HTML entities encode single quotes as ' - expected_conditions <- c( - "input['loadpage-filetype'] == 'sample'", # Sample data panels - "input['loadpage-BIO'] != 'PTM'", # Non-PTM workflows - "input['loadpage-filetype'] == 'maxq'", # MaxQuant workflow - "input['loadpage-DDA_DIA'] == 'TMT'", # TMT labeling - "input['loadpage-filetype'] == 'sky'" # Skyline workflow + + expected_panel_ids <- c( + # Sample dataset descriptions (Phase 2 — 3 mutually exclusive panels) + "test-sample_dda_description_panel", + "test-sample_dia_description_panel", + "test-sample_srm_prm_description_panel", + # LabelFreeType selector (Phase 2) + "test-label_free_type_selection_panel", + # Non-PTM uploads (Phase 2) + "test-standard_quant_upload_panel", + "test-standard_annot_upload_panel", + "test-msstats_regular_upload_panel", + "test-skyline_upload_panel", + "test-maxquant_upload_panel", + "test-dia_umpire_upload_panel", + # PTM cluster (Phase 2) + "test-msstats_ptm_upload_panel", + "test-ptm_fragpipe_upload_panel", + "test-ptm_uploads_panel", + "test-ptm_maxquant_pgroup_panel", + "test-ptm_metamorpheus_extras_panel", + "test-ptm_fasta_id_column_panel", + "test-ptm_mod_id_maxq_panel", + "test-ptm_mod_id_pd_panel", + "test-ptm_mod_id_spec_panel", + # Label-free options + OpenSWATH (Phase 2) + "test-label_free_options_panel", + "test-openswath_mscore_panel", + "test-openswath_mscore_cutoff_panel", + # Phase 1 DIANN panels (already mounted as hidden divs) + "test-diann_lf_options_panel", + "test-diann_intensity_column_panel", + "test-qval_filter_panel", + "test-qval_cutoff_panel", + "test-qval_mbr_panel", + "test-diann_anomaly_panel", + "test-diann_anomaly_run_order_panel" ) - - for(condition in expected_conditions) { - expect_true(grepl(condition, html_output, fixed = TRUE), - info = paste("Missing conditional panel for:", condition)) + for (id in expected_panel_ids) { + expect_true( + grepl(paste0('id="', id, '"'), html_output, fixed = TRUE), + info = paste("Missing hidden visibility container div id:", id) + ) } }) +test_that("loadpageUI exposes the TMT renderUI slot in place of duplicate-id panels", { + # The two pre-existing TMT `conditionalPanel`s both declared + # `ns("which.proteinid")` with different per-converter defaults — mounting + # both as hidden divs would collide on a single ns() id. Phase 2 consolidated + # them into a single `output[[tmt_options_ui]]` renderUI; the static UI + # exposes a `uiOutput(ns("tmt_options_ui"))` slot instead. + result <- loadpageUI("test") + html_output <- as.character(result) + expect_true(grepl('id="test-tmt_options_ui"', html_output, fixed = TRUE), + info = "TMT options uiOutput slot not found in rendered UI") + # And the static UI must NOT contain the literal `which.proteinid` input + # node, since it is rendered server-side now. + expect_false(grepl('id="test-which.proteinid"', html_output, fixed = TRUE), + info = paste("Static UI must not mount a `which.proteinid` input;", + "it is emitted server-side via the tmt_options_ui", + "renderUI. A static occurrence would re-introduce", + "the duplicate-ns()-id collision Phase 2 fixed.")) +}) + +test_that("Spectronaut anomaly carveout panels remain as conditionalPanels", { + # `calculate_anomaly_scores` and `run_order_file` are intentionally NOT + # migrated — they are also declared in the big-file Spectronaut helper + # emitted by `output$spectronaut_options_ui`. Mounting the regular-path + # copies as hidden divs would create a duplicate-ns()-id collision; routing + # them to renderUI would lose the user's uploaded run-order CSV (fileInput + # state cannot be re-seeded on rebuild). The two carveout conditionalPanels + # MUST remain in the static UI. See `create_quality_filtering_options` and + # `create_spectronaut_large_annotation_ui` for the matching code comments. + options <- create_quality_filtering_options(NS("test")) + options_html <- as.character(options) + + # Outer carveout: filetype == 'spec' + expect_true(grepl("input['loadpage-filetype'] == 'spec'", + options_html, fixed = TRUE), + info = "Spectronaut regular-path anomaly conditionalPanel must remain") + # Nested carveout: calculate_anomaly_scores + expect_true(grepl("input['loadpage-calculate_anomaly_scores']", + options_html, fixed = TRUE), + info = "Spectronaut anomaly run-order nested conditionalPanel must remain") + # And the two carveout inputs MUST still be declared (with the same ns() + # ids the big-file helper uses — the documented collision pair). + expect_true(grepl("test-calculate_anomaly_scores", options_html, fixed = TRUE), + info = "calculate_anomaly_scores checkbox missing from carveout panel") + expect_true(grepl("test-run_order_file", options_html, fixed = TRUE), + info = "run_order_file fileInput missing from carveout panel") +}) + test_that("loadpageUI properly handles file input elements and validation", { # Test that file inputs are properly configured result <- loadpageUI("test") @@ -132,19 +213,34 @@ test_that("create_header_content includes required elements", { }) # Tests for create_sample_dataset_descriptions() -test_that("create_sample_dataset_descriptions creates conditional panels", { - descriptions <- create_sample_dataset_descriptions() +test_that("create_sample_dataset_descriptions creates hidden divs with namespaced container IDs", { + # Phase 2: the helper now requires `ns` and returns three hidden divs (not + # conditionalPanels). Visibility is toggled server-side by + # `register_loadpage_visibility_observers` on the + # `filetype == 'sample' && LabelFreeType == ` predicate. + descriptions <- create_sample_dataset_descriptions(NS("test")) descriptions_html <- as.character(descriptions) - - # Check for conditional panels - expect_true(grepl("shiny-panel-conditional", descriptions_html)) - - # Check for specific dataset references + + # Three hidden container divs, one per LabelFreeType mode + expect_true(grepl('id="test-sample_dda_description_panel"', + descriptions_html, fixed = TRUE), + info = "DDA description hidden container missing") + expect_true(grepl('id="test-sample_dia_description_panel"', + descriptions_html, fixed = TRUE), + info = "DIA description hidden container missing") + expect_true(grepl('id="test-sample_srm_prm_description_panel"', + descriptions_html, fixed = TRUE), + info = "SRM/PRM description hidden container missing") + + # And none of them should still be conditionalPanels. + expect_false(grepl("shiny-panel-conditional", descriptions_html, fixed = TRUE), + info = paste("Sample-dataset descriptions must be hidden divs,", + "not conditionalPanels")) + + # The publication content must be preserved verbatim expect_true(grepl("DDA acquisition", descriptions_html)) expect_true(grepl("DIA acquisition", descriptions_html)) expect_true(grepl("SRM/PRM acquisition", descriptions_html)) - - # Check for publication links expect_true(grepl("Choi, M. et al", descriptions_html)) expect_true(grepl("Selevsek, N. et al", descriptions_html)) expect_true(grepl("Picotti, P. et al", descriptions_html)) @@ -209,35 +305,48 @@ test_that("create_main_selection_controls creates proper radio buttons", { }) # Tests for create_label_free_type_selection() -test_that("create_label_free_type_selection creates conditional panel", { +test_that("create_label_free_type_selection wraps the LabelFreeType radio in a hidden container", { + # Phase 2: the conditionalPanel was replaced with + # `shinyjs::hidden(div(id = ns(NAMESPACE_LOADPAGE$label_free_type_selection_panel), ...))`. + # The BIO / filetype / DDA_DIA gating is now in + # `loadpage_show_label_free_type_selection()` (server-side). selection <- create_label_free_type_selection(NS("test")) selection_html <- as.character(selection) - - expect_true(grepl("shiny-panel-conditional", selection_html)) + + expect_true(grepl('id="test-label_free_type_selection_panel"', selection_html, fixed = TRUE), + info = "Hidden container div missing") + expect_false(grepl("shiny-panel-conditional", selection_html, fixed = TRUE), + info = "LabelFreeType selector should no longer be a conditionalPanel") + # Contents preserved expect_true(grepl("Type of Label-Free type", selection_html)) expect_true(grepl("DDA", selection_html)) expect_true(grepl("DIA", selection_html)) expect_true(grepl("SRM/PRM", selection_html)) - - # Check conditional logic - expect_true(grepl("loadpage-BIO", selection_html)) - expect_true(grepl("loadpage-filetype", selection_html)) + # The LabelFreeType radio input ID must remain literal (no renames). + expect_true(grepl("test-LabelFreeType", selection_html, fixed = TRUE), + info = paste("LabelFreeType radio input ID missing or renamed;", + "Phase 2 explicitly forbids input-ID renames")) }) # Tests for create_standard_uploads() -test_that("create_standard_uploads creates file input with conditions", { +test_that("create_standard_uploads wraps the data fileInput in a hidden container", { + # Phase 2: the conditionalPanel JS condition is gone; the panel is now a + # hidden div with the data fileInput mounted inside. The list of converter + # filetypes that should show this panel is encoded in + # `loadpage_show_standard_quant_upload()` (truth-tabled in + # test-loadpage-server-rendering.R). uploads <- create_standard_uploads(NS("test")) uploads_html <- as.character(uploads) - + + expect_true(grepl('id="test-standard_quant_upload_panel"', uploads_html, fixed = TRUE), + info = "Hidden container div missing") + expect_false(grepl("shiny-panel-conditional", uploads_html, fixed = TRUE), + info = "Standard quant upload should no longer be a conditionalPanel") + # Contents preserved — header text + the fileInput expect_true(grepl("Upload quantification dataset", uploads_html)) expect_true(grepl("shiny-input-file", uploads_html)) - expect_true(grepl("test-data", uploads_html)) - - # Check conditional logic for multiple file types - expect_true(grepl("loadpage-filetype", uploads_html)) - expect_true(grepl("prog", uploads_html)) - expect_true(grepl("PD", uploads_html)) - expect_true(grepl("phil", uploads_html)) + expect_true(grepl("test-data", uploads_html, fixed = TRUE), + info = "`data` fileInput input ID missing or renamed") }) # Tests for create_msstats_uploads() @@ -451,13 +560,33 @@ test_that("DIANN large-file helper functions create correct UI elements", { expect_true(grepl("test-big_diann_run_order_file", annot_html)) }) -test_that("DIANN regular-path condition strings hide controls in big-file mode", { +test_that("DIANN big-file gating now lives in the server predicate, not a JS condition", { + # Phase 1 migrated `loadpage_show_qval_filter` and + # `loadpage_show_diann_anomaly` (both AND-include `!big_file_diann`); Phase 2 + # migrated `loadpage_show_standard_annot_upload` (same big-file gate for the + # DIANN branch). After both phases, the JS-encoded `loadpage-big_file_diann` + # condition string is no longer emitted statically — the gating is enforced + # by the corresponding server `observe({ shinyjs::toggle(..., condition = ...) })` + # blocks. What we DO want to assert statically is that the gated containers + # are present (mounted hidden) so the observers have something to toggle, + # and that the JS condition string is in fact gone. result <- loadpageUI("test") html_output <- as.character(result) - # Annotation upload is gated on !big_file_diann for DIANN - expect_true(grepl("loadpage-big_file_diann", html_output, fixed = TRUE), - info = "DIANN large-file gating condition is missing from rendered UI") + for (panel_id in c("test-standard_annot_upload_panel", + "test-qval_filter_panel", + "test-diann_anomaly_panel")) { + expect_true( + grepl(paste0('id="', panel_id, '"'), html_output, fixed = TRUE), + info = paste("Big-file-gated panel container missing:", panel_id) + ) + } + expect_false( + grepl("loadpage-big_file_diann", html_output, fixed = TRUE), + info = paste("Static UI should no longer encode a `big_file_diann` JS", + "condition string; gating moved to server predicates in", + "R/loadpage-server-rendering.R") + ) }) test_that("Spectronaut helper functions create correct UI elements", { From 6c8f9b409b4b664ab119690a93ad6438b54659b6 Mon Sep 17 00:00:00 2001 From: Swaraj Patil Date: Thu, 25 Jun 2026 14:08:13 -0400 Subject: [PATCH 3/4] Fix the helper-count docs (5 -> 6) in loadpage server --- R/module-loadpage-server.R | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/R/module-loadpage-server.R b/R/module-loadpage-server.R index 61e343a..21ef1f5 100644 --- a/R/module-loadpage-server.R +++ b/R/module-loadpage-server.R @@ -10,7 +10,13 @@ #' `local_big_file_path` / `local_big_diann_path` reactives that the #' proceed-validation helper consumes, so it must remain co-located #' with the module's reactive scope), -#' \item the five helper registrations in order, +#' \item the six helper registrations in order +#' (`register_loadpage_preview`, +#' `register_loadpage_visibility_observers`, +#' `register_loadpage_converter_ui`, +#' `register_loadpage_proceed_validation`, +#' `register_loadpage_data_loaders`, +#' `register_loadpage_summary`), #' \item the final public `return(list(input, getData, #' getConditionMetadata))`. #' } @@ -78,7 +84,7 @@ loadpageServer <- function(id, parent_session, is_web_server = FALSE, app_templa local_big_diann_path <- reactive({ NULL }) } - # == HELPER REGISTRATION (5 helpers, all in R/loadpage-server-*.R) ========= + # == HELPER REGISTRATION (6 helpers, all in R/loadpage-server-*.R) ========= # # Order matters only insofar as Shiny reactivity is set up at module-mount # time. We follow the file's original top-to-bottom layout: preview -> From 9d43d9fe247f143e466517658bcecb51d58c0051 Mon Sep 17 00:00:00 2001 From: Swaraj Patil Date: Sat, 27 Jun 2026 13:22:05 -0400 Subject: [PATCH 4/4] Loadpage refactor: address review comments (rename, move summaries, comment cleanup) --- R/constants.R | 6 - ...loadpage-server-converter-options-panel.R} | 109 +++++------------- R/loadpage-server-data-loaders.R | 39 +------ R/loadpage-server-preview.R | 19 +-- R/loadpage-server-proceed-validation.R | 12 +- R/loadpage-server-summary.R | 32 ++--- R/module-loadpage-ui.R | 8 +- tests/testthat/test-module-loadpage-ui.R | 2 +- 8 files changed, 51 insertions(+), 176 deletions(-) rename R/{loadpage-server-rendering.R => loadpage-server-converter-options-panel.R} (86%) diff --git a/R/constants.R b/R/constants.R index e215aae..f1843ba 100644 --- a/R/constants.R +++ b/R/constants.R @@ -62,12 +62,10 @@ CONSTANTS_STATMODEL = list( ) NAMESPACE_LOADPAGE = list( - # Cross-module public IDs (read from outside the loadpage module). bio = "BIO", dda_dia = "DDA_DIA", filetype = "filetype", proceed1 = "proceed1", - # DIANN-cluster IDs migrated to server-side show/hide in Phase 1. big_file_diann = "big_file_diann", big_diann_calculate_anomaly_scores = "big_diann_calculate_anomaly_scores", big_diann_run_order_file = "big_diann_run_order_file", @@ -78,13 +76,11 @@ NAMESPACE_LOADPAGE = list( mbr = "MBR", diann_calculate_anomaly_scores = "diann_calculate_anomaly_scores", diann_run_order_file = "diann_run_order_file", - # Driver IDs introduced (i.e. centralized) in Phase 2. big_file_spec = "big_file_spec", label_free_type = "LabelFreeType", calculate_anomaly_scores = "calculate_anomaly_scores", m_score = "m_score", which_proteinid = "which.proteinid", - # Phase 1 container IDs (visibility divs). diann_lf_options_panel = "diann_lf_options_panel", diann_intensity_column_panel = "diann_intensity_column_panel", qval_filter_panel = "qval_filter_panel", @@ -93,7 +89,6 @@ NAMESPACE_LOADPAGE = list( diann_anomaly_panel = "diann_anomaly_panel", diann_anomaly_run_order_panel = "diann_anomaly_run_order_panel", big_diann_anomaly_run_order_panel = "big_diann_anomaly_run_order_panel", - # Phase 2 container IDs (visibility divs introduced by the broader sweep). sample_dda_description_panel = "sample_dda_description_panel", sample_dia_description_panel = "sample_dia_description_panel", sample_srm_prm_description_panel = "sample_srm_prm_description_panel", @@ -116,7 +111,6 @@ NAMESPACE_LOADPAGE = list( label_free_options_panel = "label_free_options_panel", openswath_mscore_panel = "openswath_mscore_panel", openswath_mscore_cutoff_panel = "openswath_mscore_cutoff_panel", - # Phase 2 renderUI slot — the TMT which.proteinid duplicate-ns()-id case. tmt_options_ui = "tmt_options_ui" ) diff --git a/R/loadpage-server-rendering.R b/R/loadpage-server-converter-options-panel.R similarity index 86% rename from R/loadpage-server-rendering.R rename to R/loadpage-server-converter-options-panel.R index 9ad9af5..04a0c6e 100644 --- a/R/loadpage-server-rendering.R +++ b/R/loadpage-server-converter-options-panel.R @@ -1,45 +1,8 @@ -# ============================================================================ -# Loadpage — server-side visibility predicates and observer registration -# ============================================================================ -# -# Phase 1 (DIANN cluster) and Phase 2 (the rest) of the loadpage refactor that -# moved conditional UI off `conditionalPanel` and onto server-side -# `shinyjs::show/hide`. All container divs in `R/module-loadpage-ui.R` are -# mounted unconditionally via `shinyjs::hidden(div(id = ns(...), ...))`; -# `register_loadpage_visibility_observers` below installs one -# `observe({ shinyjs::toggle(...) })` per container, driven by a pure -# predicate. -# -# Why show/hide (the default), not renderUI: -# - panels contain inputs whose values must persist across visibility flips, -# - `R/utils.R::getData` / `getDataCode` read many of those input IDs by -# literal string at `input$proceed1`, and would see NULL on a destroyed- -# and-rebuilt input. -# -# The one renderUI exception is the TMT `which.proteinid` text field: two -# pre-existing `conditionalPanel`s both declared `ns("which.proteinid")` with -# different defaults (one for PD, one for MaxQuant). Mounting both as hidden -# divs would deterministically collide on a single ns() id. The exception is -# implemented as `output[[tmt_options_ui]] <- renderUI({...})` below; the -# rebuild always preserves the user's current value via `isolate()` and only -# falls back to the converter-appropriate default on the very first build -# (when no user value exists yet). -# -# Two `conditionalPanel`s are intentionally NOT migrated and remain in the UI -# file (Spectronaut regular-path anomaly checkbox + nested run-order -# fileInput, see the carveout comments in `R/module-loadpage-ui.R`). They -# share `ns("calculate_anomaly_scores")` / `ns("run_order_file")` with the -# big-file Spectronaut helper that the pre-existing -# `output$spectronaut_options_ui` renderUI emits, and routing them to -# renderUI would lose the user's uploaded run-order CSV (fileInput state -# cannot be re-seeded on rebuild). -# -# All helpers are internal (`@noRd`); predicates are pure (no Shiny -# reactivity) so they can be exercised with truth-table tests. +# Loadpage converter-options panel: pure visibility predicates + their server-side show/hide observers, the TMT which.proteinid renderUI, and the Spectronaut/DIANN converter renderUIs. # ---------------------------------------------------------------------------- -# Phase 1 predicates (DIANN cluster). Unchanged from the Phase 1 commit. +# DIANN-cluster visibility predicates. # ---------------------------------------------------------------------------- #' @noRd @@ -88,9 +51,8 @@ loadpage_show_big_diann_anomaly_run_order <- function(big_diann_calculate_anomal # ---------------------------------------------------------------------------- -# Phase 2 predicates (everything else). Each mirrors a previous -# `conditionalPanel(condition = "...")` JS expression including the full -# ancestor chain for nested cases. +# Visibility predicates for the remaining converter and upload panels. Each +# encodes the full ancestor chain so a nested panel hides when an ancestor does. # ---------------------------------------------------------------------------- #' Sample dataset description (parameterized for DDA / DIA / SRM_PRM). @@ -137,9 +99,9 @@ loadpage_show_msstats_regular_upload <- function(filetype, bio, dda_dia) { !isTRUE(dda_dia == "TMT") } -#' Pre-formatted MSstatsPTM CSV upload — PTM path only. (The original JS -#' condition had a redundant `|| (BIO=='PTM' && DDA_DIA=='TMT')` clause that -#' was tautologically true whenever `BIO=='PTM'`; it collapses away here.) +#' Pre-formatted MSstatsPTM CSV upload — PTM path only. (A +#' `|| (BIO=='PTM' && DDA_DIA=='TMT')` term would be redundant — tautological +#' whenever `BIO=='PTM'` — so the predicate omits it.) #' @noRd loadpage_show_msstats_ptm_upload <- function(filetype, bio) { isTRUE(filetype == "msstats") && isTRUE(bio == "PTM") @@ -163,8 +125,8 @@ loadpage_show_maxquant_upload <- function(filetype, bio, dda_dia) { } #' Shared PTM uploads block (PTM Input / Annot / FASTA / Unmod Protein). -#' Original JS had a redundant `|| (BIO=='PTM' && DDA_DIA=='TMT')` term — -#' collapses to `BIO=='PTM' && filetype ∈ ...`. +#' A `|| (BIO=='PTM' && DDA_DIA=='TMT')` term would be redundant (tautological +#' whenever `BIO=='PTM'`), so the predicate is `BIO=='PTM' && filetype ∈ ...`. #' @noRd loadpage_show_ptm_uploads <- function(filetype, bio) { isTRUE(bio == "PTM") && @@ -314,18 +276,22 @@ loadpage_seed_proteinid <- function(incoming_filetype, # Unified registration helper. # ---------------------------------------------------------------------------- -#' Register every loadpage visibility observer (Phase 1 + Phase 2) plus the -#' single TMT `which.proteinid` renderUI exception. +#' Register every loadpage visibility observer plus the single TMT +#' `which.proteinid` renderUI exception. Call once from `loadpageServer`'s +#' `moduleServer` scope. #' -#' Replaces Phase 1's `register_diann_visibility_observers`. Call once from -#' `loadpageServer`'s `moduleServer` scope. +#' Panels are shown/hidden with `shinyjs` rather than rebuilt with `renderUI`: +#' `getData` / `getDataCode` read input IDs by literal string, so a `renderUI` +#' rebuild would destroy a hidden input and feed `getData` NULL at proceed +#' time, and would also reset values the user typed and drop uploaded files. +#' show/hide keeps the inputs mounted so their state is preserved. #' #' @param input the Shiny module's `input` object #' @param output the Shiny module's `output` object (for the TMT renderUI) #' @param session the Shiny module's `session` (for `session$ns`) #' @noRd register_loadpage_visibility_observers <- function(input, output, session) { - # --- Phase 1: DIANN cluster ------------------------------------------------ + # --- DIANN cluster --------------------------------------------------------- observe({ shinyjs::toggle( NAMESPACE_LOADPAGE$diann_lf_options_panel, @@ -396,7 +362,7 @@ register_loadpage_visibility_observers <- function(input, output, session) { ) }) - # --- Phase 2: sample-dataset descriptions + LabelFreeType picker ---------- + # --- Sample-dataset descriptions + LabelFreeType picker ------------------- observe({ shinyjs::toggle( NAMESPACE_LOADPAGE$sample_dda_description_panel, @@ -438,7 +404,7 @@ register_loadpage_visibility_observers <- function(input, output, session) { ) }) - # --- Phase 2: non-PTM converter uploads ------------------------------------ + # --- Non-PTM converter uploads --------------------------------------------- observe({ shinyjs::toggle( NAMESPACE_LOADPAGE$standard_quant_upload_panel, @@ -507,7 +473,7 @@ register_loadpage_visibility_observers <- function(input, output, session) { ) }) - # --- Phase 2: PTM converter cluster --------------------------------------- + # --- PTM converter cluster ------------------------------------------------- observe({ shinyjs::toggle( NAMESPACE_LOADPAGE$ptm_uploads_panel, @@ -572,7 +538,7 @@ register_loadpage_visibility_observers <- function(input, output, session) { ) }) - # --- Phase 2: DIA-Umpire + label-free options + OpenSWATH M-score --------- + # --- DIA-Umpire + label-free options + OpenSWATH M-score ------------------- observe({ shinyjs::toggle( NAMESPACE_LOADPAGE$dia_umpire_upload_panel, @@ -610,7 +576,7 @@ register_loadpage_visibility_observers <- function(input, output, session) { ) }) - # --- Phase 2: TMT which.proteinid renderUI (the duplicate-ns()-id case) ---- + # --- TMT which.proteinid renderUI (the duplicate-ns()-id case) ------------- # # Two `conditionalPanel`s previously declared the same ns("which.proteinid") # with different defaults (PD: "Protein.Accessions", MaxQuant: "Proteins"). @@ -654,27 +620,11 @@ register_loadpage_visibility_observers <- function(input, output, session) { } -# ============================================================================ -# Pre-Phase-1 converter renderUIs + file-type availability observer. -# -# Moved from R/module-loadpage-server.R by the Phase 2 server split. Pure -# cut-and-paste: no behavior change, no reactivity timing change. These -# renderUIs were in the orchestrator before; they're co-located here with -# the other UI-rendering helpers for navigability. Reads `is_web_server` -# and `app_template` from the outer module, so the registration helper -# below takes them as args. -# -# Includes the file-type availability observer (the radio-disable + -# `runjs` opacity block, originally at module-loadpage-server.R:347-400). -# It is UI state, not predicate-driven visibility, so it stays a plain -# observer with no corresponding `loadpage_show_*` predicate. -# ============================================================================ - - -#' Register the pre-existing Spectronaut/DIANN converter renderUIs + the -#' Metamorpheus mod-ID renderUI's wrappers were moved into -#' `register_loadpage_preview` since they depend on the preview reactive. -#' What stays here is everything else that doesn't need preview_data. +# Spectronaut/DIANN converter renderUIs + the file-type availability observer (radio-disable + opacity; UI state, so no `loadpage_show_*` predicate). + + +#' Register the Spectronaut and DIANN converter renderUIs and the file-type +#' availability observer. #' #' @param input the Shiny module's `input` object #' @param output the Shiny module's `output` object @@ -850,8 +800,7 @@ register_loadpage_converter_ui <- function(input, output, session, # File-type availability — disable converter radios that don't fit the # current (BIO, DDA_DIA) combo, and dim them via the `runjs` opacity hack. # UI state only, not predicate-driven visibility (no `loadpage_show_*` - # predicate). Moved from module-loadpage-server.R verbatim except for the - # debug `print()` lines that the original carried. + # predicate). observe({ if ((input$BIO == "Protein" || input$BIO == "Peptide") && input$DDA_DIA == "LType") { runjs("$('[type=radio][name=loadpage-filetype]:disabled').parent().parent().parent().find('div.radio').css('opacity', 1)") diff --git a/R/loadpage-server-data-loaders.R b/R/loadpage-server-data-loaders.R index 03fa770..d69aca7 100644 --- a/R/loadpage-server-data-loaders.R +++ b/R/loadpage-server-data-loaders.R @@ -1,22 +1,4 @@ -# ============================================================================ -# Loadpage — data-loading reactives + download MSstats handler + summaries -# ============================================================================ -# -# Extracted from R/module-loadpage-server.R by the Phase 2 server split. -# Pure cut-and-paste: no behavior change, no reactivity timing change, no -# input-ID renames. Owns: -# - 11 single-file wrapper reactives (`get_annot`, `get_annot1/2/3`, -# `get_evidence`, `get_evidence2`, `get_global`, `get_proteinGroups`, -# `get_proteinGroups2`, `get_FragSummary`, `get_peptideSummary`, -# `get_protSummary`, `get_maxq_ptm_sites`) -# - the lynchpin `get_data` eventReactive (triggered on `proceed1`) -# - the download_msstats_format downloadHandler + its enable/disable -# observers -# - `get_data_code` (triggered on `calculate`) -# - `get_summary1`, `get_summary2` (triggered on `proceed1`) -# -# Returns a named list of reactives the summary helper and the orchestrator -# (for the public return value) read. +# Loadpage data-loading reactives + the MSstats-format download handler. #' Register the loadpage data-loading reactives + download handler. @@ -24,10 +6,9 @@ #' @param input the Shiny module's `input` object #' @param output the Shiny module's `output` object #' @param session the Shiny module's `session` -#' @return named list with `get_data`, `get_annot`, `get_summary1`, -#' `get_summary2`, `get_data_code` (and the other single-file -#' wrappers if the orchestrator or any future helper needs -#' them) +#' @return named list with `get_data`, `get_annot`, `get_data_code` +#' (and the other single-file wrappers if the orchestrator or +#' any future helper needs them) #' @noRd register_loadpage_data_loaders <- function(input, output, session) { @@ -147,14 +128,6 @@ register_loadpage_data_loaders <- function(input, output, session) { getDataCode(input) }) - get_summary1 <- eventReactive(input$proceed1, { - getSummary1(input, get_data(), get_annot()) - }) - - get_summary2 <- eventReactive(input$proceed1, { - getSummary2(input, get_data()) - }) - list( get_annot = get_annot, get_annot1 = get_annot1, @@ -170,8 +143,6 @@ register_loadpage_data_loaders <- function(input, output, session) { get_protSummary = get_protSummary, get_maxq_ptm_sites = get_maxq_ptm_sites, get_data = get_data, - get_data_code = get_data_code, - get_summary1 = get_summary1, - get_summary2 = get_summary2 + get_data_code = get_data_code ) } diff --git a/R/loadpage-server-preview.R b/R/loadpage-server-preview.R index bac4d58..bcf5144 100644 --- a/R/loadpage-server-preview.R +++ b/R/loadpage-server-preview.R @@ -1,21 +1,4 @@ -# ============================================================================ -# Loadpage — preview data + DIANN auto-detection + Metamorpheus mod-ID UI -# ============================================================================ -# -# Extracted from R/module-loadpage-server.R by the Phase 2 server split. -# Pure cut-and-paste: no behavior change, no reactivity timing change, no -# input-ID renames. Owns: -# - `preview_data` reactiveVal (first 100 rows of the selected file) -# - `last_detected_diann_format` reactiveVal -# - `main_data_file` reactive (filetype → fileInput reactive) -# - the preview-fetch observer -# - the DIANN 2.0+ auto-toggle observer -# - the manual-override mismatch warning observeEvent -# - `output$mod_id_meta_ui` renderUI (depends on preview_data) -# - `output$mod_id_meta_other_input` renderUI -# -# Returns the `preview_data` reactive (invisibly) so future helpers can read -# it if needed. +# Loadpage preview cluster: first-100-row preview, DIANN version auto-detection, and the Metamorpheus modification-ID UI. #' Register the loadpage preview cluster. diff --git a/R/loadpage-server-proceed-validation.R b/R/loadpage-server-proceed-validation.R index 374dfcb..283cc3a 100644 --- a/R/loadpage-server-proceed-validation.R +++ b/R/loadpage-server-proceed-validation.R @@ -1,14 +1,4 @@ -# ============================================================================ -# Loadpage — `proceed1` button enable/disable cascade -# ============================================================================ -# -# Extracted from R/module-loadpage-server.R by the Phase 2 server split. -# Pure cut-and-paste: the deeply nested `observe()` block that gates the -# Upload Data button against the active (BIO, DDA_DIA, filetype, file-upload -# state) combination is preserved verbatim. The two big-file path reactives -# (`local_big_file_path`, `local_big_diann_path`) originate in the -# shinyFiles block — they stay in the orchestrator and are passed in as -# function arguments here. +# Loadpage `proceed1` (Upload Data) button enable/disable cascade. #' Register the `proceed1` enable cascade. diff --git a/R/loadpage-server-summary.R b/R/loadpage-server-summary.R index bffcad8..6945eb6 100644 --- a/R/loadpage-server-summary.R +++ b/R/loadpage-server-summary.R @@ -1,21 +1,4 @@ -# ============================================================================ -# Loadpage — condition_metadata DT editor + post-`proceed1` onclick handler -# ============================================================================ -# -# Extracted from R/module-loadpage-server.R by the Phase 2 server split. -# Pure cut-and-paste. Owns: -# - the condition_metadata DT cell-edit observeEvent -# - `output$condition_metadata_table` renderDT -# - the `onclick("proceed1", { ... })` block (post-proceed setup, -# condition_metadata initialization per template, summary outputs, and -# the nested `onclick("proceed2", ...)` that flips the parent tabset) -# -# The orchestrator owns the `condition_metadata` reactiveVal because the -# summary helper and the rest of the page (via the public return value) -# both share it. It is passed in as `condition_metadata` below. The -# data-loading reactives the summary helper reads -# (`get_data`, `get_annot`, `get_summary1`, `get_summary2`) come in via -# `data_reactives` from `register_loadpage_data_loaders()`. +# Loadpage condition-metadata DT editor + the post-`proceed1` summary outputs and experimental-design summary statistics (number of conditions, number of replicates, etc.). #' Register the loadpage post-`proceed1` summary cluster. @@ -27,8 +10,7 @@ #' switch in `onclick("proceed2")`) #' @param app_template reactive (or NULL) returning the active template #' @param data_reactives named list from `register_loadpage_data_loaders`; -#' the helper consumes `get_data`, `get_annot`, -#' `get_summary1`, `get_summary2` +#' the helper consumes `get_data`, `get_annot` #' @param condition_metadata `reactiveVal` owned by the orchestrator #' @noRd register_loadpage_summary <- function(input, output, session, parent_session, @@ -38,8 +20,14 @@ register_loadpage_summary <- function(input, output, session, parent_session, get_data <- data_reactives$get_data get_annot <- data_reactives$get_annot - get_summary1 <- data_reactives$get_summary1 - get_summary2 <- data_reactives$get_summary2 + + get_summary1 <- eventReactive(input$proceed1, { + getSummary1(input, get_data(), get_annot()) + }) + + get_summary2 <- eventReactive(input$proceed1, { + getSummary2(input, get_data()) + }) # Handle edits to the condition metadata DT table observeEvent(input$condition_metadata_table_cell_edit, { diff --git a/R/module-loadpage-ui.R b/R/module-loadpage-ui.R index 4f635f4..924c3ab 100644 --- a/R/module-loadpage-ui.R +++ b/R/module-loadpage-ui.R @@ -91,7 +91,7 @@ create_header_content <- function() { #' Create conditional descriptions for sample datasets #' Visibility is driven server-side by #' `register_loadpage_visibility_observers` (see -#' `R/loadpage-server-rendering.R`); each description sits in a hidden div +#' `R/loadpage-server-converter-options-panel.R`); each description sits in a hidden div #' that the observer toggles on `filetype == 'sample' && LabelFreeType == `. #' @noRd create_sample_dataset_descriptions <- function(ns) { @@ -770,7 +770,7 @@ create_processing_options <- function(ns) { #' exclusive `conditionalPanel`s with different defaults (PD -> #' "Protein.Accessions", MaxQuant -> "Proteins"). Mounting both as hidden #' divs would collide on a single ns() id. The single -#' `output[[tmt_options_ui]]` renderUI in R/loadpage-server-rendering.R +#' `output[[tmt_options_ui]]` renderUI in R/loadpage-server-converter-options-panel.R #' replaces both panels — it emits one textInput with the converter- #' appropriate default on first build and carries the user's typed value #' across filetype flips via isolate(). @@ -804,7 +804,7 @@ create_label_free_options <- function(ns) { )), # DIANN specific options — visibility driven server-side - # (R/loadpage-server-rendering.R::register_loadpage_visibility_observers). + # (R/loadpage-server-converter-options-panel.R::register_loadpage_visibility_observers). shinyjs::hidden(div( id = ns(NAMESPACE_LOADPAGE$diann_lf_options_panel), checkboxInput(ns(NAMESPACE_LOADPAGE$diann_2plus), "DIANN 2.0+", value = FALSE), @@ -854,7 +854,7 @@ create_quality_filtering_options <- function(ns) { # cannot survive a rebuild. Today's `conditionalPanel` keeps the static # tree at `display:none` whenever the big-file path is active, leaving # only the renderUI'd big-file copy mounted — which is what we want. - # See R/loadpage-server-rendering.R for the broader carveout note. + # See R/loadpage-server-converter-options-panel.R for the broader carveout note. conditionalPanel( condition = "input['loadpage-filetype'] == 'spec'", checkboxInput(ns("calculate_anomaly_scores"), diff --git a/tests/testthat/test-module-loadpage-ui.R b/tests/testthat/test-module-loadpage-ui.R index ee5d24e..80afab0 100644 --- a/tests/testthat/test-module-loadpage-ui.R +++ b/tests/testthat/test-module-loadpage-ui.R @@ -585,7 +585,7 @@ test_that("DIANN big-file gating now lives in the server predicate, not a JS con grepl("loadpage-big_file_diann", html_output, fixed = TRUE), info = paste("Static UI should no longer encode a `big_file_diann` JS", "condition string; gating moved to server predicates in", - "R/loadpage-server-rendering.R") + "R/loadpage-server-converter-options-panel.R") ) })