diff --git a/R/constants.R b/R/constants.R index 5305820..8c9bbfd 100644 --- a/R/constants.R +++ b/R/constants.R @@ -61,6 +61,58 @@ CONSTANTS_STATMODEL = list( plot_type_qq_plot = "QQPlots" # QQPlots — matches MSstats::groupComparisonQCPlots(type = "QQPlots") ) +NAMESPACE_LOADPAGE = list( + bio = "BIO", + dda_dia = "DDA_DIA", + filetype = "filetype", + proceed1 = "proceed1", + 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", + big_file_spec = "big_file_spec", + label_free_type = "LabelFreeType", + calculate_anomaly_scores = "calculate_anomaly_scores", + m_score = "m_score", + which_proteinid = "which.proteinid", + 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", + 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", + summary_nonptm_panel = "summary_nonptm_panel", + summary_ptm_panel = "summary_ptm_panel", + tmt_options_ui = "tmt_options_ui" +) + NAMESPACE_EXPDES = list( sidebar_controls = "sidebar_controls", protein_select = "protein_select", diff --git a/R/loadpage-server-converter-options-panel.R b/R/loadpage-server-converter-options-panel.R new file mode 100644 index 0000000..6199bfa --- /dev/null +++ b/R/loadpage-server-converter-options-panel.R @@ -0,0 +1,916 @@ +# Loadpage converter-options panel: pure visibility predicates + their server-side show/hide observers, the TMT which.proteinid renderUI, and the Spectronaut/DIANN converter renderUIs. + + +# ---------------------------------------------------------------------------- +# DIANN-cluster visibility predicates. +# ---------------------------------------------------------------------------- + +#' @noRd +loadpage_show_diann_lf_options <- function(filetype, dda_dia, big_file_diann) { + isTRUE(filetype == "diann") && + isTRUE(dda_dia == "LType") && + !isTRUE(big_file_diann) +} + +#' @noRd +loadpage_show_diann_intensity_column <- function(diann_2plus) { + !isTRUE(diann_2plus) +} + +#' @noRd +loadpage_show_diann_mbr <- function(q_val, filetype) { + isTRUE(q_val) && isTRUE(filetype == "diann") +} + + +# ---------------------------------------------------------------------------- +# Shared / cross-converter visibility predicates. These gate panels shown under +# more than one converter — e.g. the q-value filter applies to Skyline, +# Spectronaut, and small-file DIANN — so they live here rather than under any +# single converter's section. +# ---------------------------------------------------------------------------- + +#' @noRd +loadpage_show_qval_filter <- function(filetype, big_file_diann) { + isTRUE(filetype == "sky") || + isTRUE(filetype == "spec") || + (isTRUE(filetype == "diann") && !isTRUE(big_file_diann)) +} + +#' @noRd +loadpage_show_qval_cutoff <- function(q_val) { + isTRUE(q_val) +} + + +# ---------------------------------------------------------------------------- +# 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). Composes +#' on the picker's full ancestor condition +#' (`loadpage_show_sample_dataset_label_free_type_selector`, i.e. +#' `BIO != 'PTM' && filetype == 'sample' && DDA_DIA == 'LType'`) AND a matching +#' LabelFreeType, so a description can never outlive the picker: when BIO becomes +#' PTM or DDA_DIA leaves LType the picker hides and the description hides too. +#' @param filetype the active `input$filetype` +#' @param label_free_type the active `input$LabelFreeType` +#' @param label_free_mode this description's mode ("DDA" / "DIA" / "SRM_PRM") +#' @param bio the active `input$BIO` (picker ancestor gate) +#' @param dda_dia the active `input$DDA_DIA` (picker ancestor gate) +#' @noRd +loadpage_show_sample_dataset_description <- function(filetype, label_free_type, + label_free_mode, bio, dda_dia) { + loadpage_show_sample_dataset_label_free_type_selector(bio, filetype, dda_dia) && + isTRUE(label_free_type == label_free_mode) +} + +#' The LabelFreeType radio (DDA / DIA / SRM_PRM picker) is itself shown only +#' for the sample-dataset label-free workflow. +#' @noRd +loadpage_show_sample_dataset_label_free_type_selector <- 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_label_free_upload <- function(filetype, bio, dda_dia) { + isTRUE(filetype == "msstats") && + !isTRUE(bio == "PTM") && + !isTRUE(dda_dia == "TMT") +} + +#' 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") +} + +#' @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). +#' 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") && + 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 +} + + +# ---------------------------------------------------------------------------- +# Post-proceed1 summary-table visibility predicates (BIO-driven). These gate +# the dataset-preview panels rendered after `proceed1`: the standard summary for +# non-PTM data, and the PTM + unmodified-protein summaries for PTM data. NULL +# bio reads as non-PTM, mirroring the original `input['loadpage-BIO'] !== 'PTM'` +# JS condition. +# ---------------------------------------------------------------------------- + +#' @noRd +loadpage_show_nonptm_summary <- function(bio) { + !isTRUE(bio == "PTM") +} + +#' @noRd +loadpage_show_ptm_summary <- function(bio) { + isTRUE(bio == "PTM") +} + + +# ---------------------------------------------------------------------------- +# Unified registration helper. +# ---------------------------------------------------------------------------- + +#' Register every loadpage visibility observer plus the single TMT +#' `which.proteinid` renderUI exception. 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) { + # --- DIANN cluster --------------------------------------------------------- + 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]] + ) + ) + }) + + # --- 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", + input[[NAMESPACE_LOADPAGE$bio]], + input[[NAMESPACE_LOADPAGE$dda_dia]] + ) + ) + }) + 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", + input[[NAMESPACE_LOADPAGE$bio]], + input[[NAMESPACE_LOADPAGE$dda_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", + input[[NAMESPACE_LOADPAGE$bio]], + input[[NAMESPACE_LOADPAGE$dda_dia]] + ) + ) + }) + observe({ + shinyjs::toggle( + NAMESPACE_LOADPAGE$label_free_type_selection_panel, + condition = loadpage_show_sample_dataset_label_free_type_selector( + input[[NAMESPACE_LOADPAGE$bio]], + input[[NAMESPACE_LOADPAGE$filetype]], + input[[NAMESPACE_LOADPAGE$dda_dia]] + ) + ) + }) + + # --- 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_label_free_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]] + ) + ) + }) + + # --- 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]] + ) + ) + }) + + # --- 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]] + ) + ) + }) + + # --- Post-proceed1 summary tables (BIO-driven) ----------------------------- + # Unlike the static converter panels above, these two divs are emitted by the + # `summary_tables` renderUI in `register_loadpage_summary`, which (re)builds + # only when `proceed1` fires — so they do not exist at the app's first flush. + # Each observer therefore also takes a dependency on `proceed1`, so it + # re-fires once the renderUI has (re)mounted the divs and applies the correct + # initial visibility. (Shiny applies output values before custom messages + # within a response, so the toggle lands after the divs are inserted.) BIO is + # read the same way as the other BIO-driven predicates. + observe({ + input[[NAMESPACE_LOADPAGE$proceed1]] + shinyjs::toggle( + NAMESPACE_LOADPAGE$summary_nonptm_panel, + condition = loadpage_show_nonptm_summary( + input[[NAMESPACE_LOADPAGE$bio]] + ) + ) + }) + observe({ + input[[NAMESPACE_LOADPAGE$proceed1]] + shinyjs::toggle( + NAMESPACE_LOADPAGE$summary_ptm_panel, + condition = loadpage_show_ptm_summary( + input[[NAMESPACE_LOADPAGE$bio]] + ) + ) + }) + + # --- 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) +} + + +# 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 +#' @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 + } + }) + + # Spectronaut regular-path anomaly UI (renderUI, not show/hide): mounts only + # when the regular Spectronaut path is active, so its `calculate_anomaly_scores` + # / `run_order_file` ids never coexist with the big-file copy emitted by + # `spectronaut_options_ui` above. Reads the checkbox NON-isolated so the + # renderUI re-runs on toggle and emits the run-order fileInput only when + # ticked; the fileInput is rebuilt fresh (upload dropped) on any rebuild — + # converter switch, big_file toggle, or checkbox toggle (accepted tradeoff). + output$spectronaut_anomaly_ui <- renderUI({ + req(input$filetype == 'spec', !isTRUE(input$big_file_spec)) + create_spectronaut_anomaly_ui( + session$ns, isTRUE(input$calculate_anomaly_scores) + ) + }) + + # DIANN regular-path anomaly UI (renderUI, migrated from Phase 1 show/hide for + # consistency with the Spectronaut regular path). DIANN's regular ids + # (diann_*) differ from the big-file ids (big_diann_*), so there is no + # collision — this migration is purely for uniformity. Same pattern: reads the + # checkbox NON-isolated so the run-order fileInput is emitted when ticked and + # dropped on any rebuild. + output$diann_anomaly_ui <- renderUI({ + req(input$filetype == 'diann', !isTRUE(input$big_file_diann)) + create_diann_anomaly_ui( + session$ns, isTRUE(input$diann_calculate_anomaly_scores) + ) + }) + + # 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). + 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-data-loaders.R b/R/loadpage-server-data-loaders.R new file mode 100644 index 0000000..d69aca7 --- /dev/null +++ b/R/loadpage-server-data-loaders.R @@ -0,0 +1,148 @@ +# Loadpage data-loading reactives + the MSstats-format download handler. + + +#' 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_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) + }) + + 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 + ) +} diff --git a/R/loadpage-server-preview.R b/R/loadpage-server-preview.R new file mode 100644 index 0000000..bcf5144 --- /dev/null +++ b/R/loadpage-server-preview.R @@ -0,0 +1,126 @@ +# Loadpage preview cluster: first-100-row preview, DIANN version auto-detection, and the Metamorpheus modification-ID UI. + + +#' 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..283cc3a --- /dev/null +++ b/R/loadpage-server-proceed-validation.R @@ -0,0 +1,117 @@ +# Loadpage `proceed1` (Upload Data) button enable/disable cascade. + + +#' 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-summary.R b/R/loadpage-server-summary.R new file mode 100644 index 0000000..1460a0b --- /dev/null +++ b/R/loadpage-server-summary.R @@ -0,0 +1,203 @@ +# 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. +#' +#' @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` +#' @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 <- 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$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(), + shinyjs::hidden(div(id = ns(NAMESPACE_LOADPAGE$summary_nonptm_panel), + h4("Top 6 rows of the dataset"), + div(style = "overflow-x: auto;", tableOutput(ns("summary"))) + )), + shinyjs::hidden(div(id = ns(NAMESPACE_LOADPAGE$summary_ptm_panel), + 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 4c00602..21ef1f5 100644 --- a/R/module-loadpage-server.R +++ b/R/module-loadpage-server.R @@ -1,34 +1,55 @@ #' 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 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))`. +#' } +#' +#' 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) - # == 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) @@ -39,7 +60,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 @@ -50,7 +70,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) @@ -60,765 +79,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 (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 -> + # 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 e9f9292..41ae2cc 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-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() { +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,60 @@ 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. + 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 @@ -407,16 +410,19 @@ 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, emitted by this helper (called + # from the diann_options_ui renderUI) only when the checkbox is ticked — + # the same renderUI-gated pattern as the other three anomaly spots. The + # upload is dropped on a big_file / converter / checkbox toggle (accepted). + if (isTRUE(calculate_anomaly_def)) { + 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")) - ) + } ) } @@ -490,6 +496,15 @@ create_spectronaut_large_bottom_ui <- function(ns, max_feature_def = 20, unique_ #' Create Spectronaut large file annotation override + anomaly UI #' +#' Invoked from `output$spectronaut_options_ui` (server-side renderUI) only +#' when `big_file_spec == TRUE` and `is_web_server == FALSE`. It declares +#' `ns("calculate_anomaly_scores")` / `ns("run_order_file")` — the same ids the +#' regular Spectronaut path emits from `output$spectronaut_anomaly_ui`. Both +#' copies are renderUI-gated on mutually exclusive `big_file_spec` states, so +#' they never coexist in the DOM and the shared ns() ids never collide. The +#' run-order fileInput is emitted only when the checkbox is ticked (no +#' conditionalPanel), so its upload is dropped on any rebuild or checkbox toggle. +#' #' @noRd create_spectronaut_large_annotation_ui <- function(ns, calculate_anomaly_def = FALSE) { tagList( @@ -509,8 +524,9 @@ 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), - conditionalPanel( - condition = sprintf("input['%s']", ns("calculate_anomaly_scores")), + # Run-order fileInput emitted only when the checkbox is ticked (renderUI- + # gated, no conditionalPanel); the upload is dropped on any rebuild. + if (isTRUE(calculate_anomaly_def)) { fileInput(ns("run_order_file"), label = h5("Upload Run Order File", class = "icon-wrapper", @@ -518,64 +534,125 @@ create_spectronaut_large_annotation_ui <- function(ns, calculate_anomaly_def = F 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")) - ) + } ) } -#' Create PTM FragPipe uploads +#' Create the DIANN regular-path anomaly UI: the Calculate Anomaly Scores +#' checkbox and, when it is ticked, the nested run-order fileInput. Emitted by +#' `output$diann_anomaly_ui` (renderUI) so it mounts only on the regular DIANN +#' path (filetype == 'diann' && !big_file_diann) — the same renderUI-gated +#' pattern as `create_spectronaut_anomaly_ui`. The run-order upload is dropped +#' on a converter switch, big_file toggle, or checkbox toggle (accepted +#' tradeoff). Uses the `diann_*` ns() ids (distinct from the big-file +#' `big_diann_*` ids), so there is no cross-path collision. +#' @noRd +create_diann_anomaly_ui <- function(ns, calculate_anomaly_def = FALSE) { + tagList( + checkboxInput(ns(NAMESPACE_LOADPAGE$diann_calculate_anomaly_scores), + label = tags$span( + "Calculate Anomaly Scores", + class = "icon-wrapper", + icon("question-circle", lib = "font-awesome"), + div("Engineers DeltaRT = RT - Predicted.RT in the raw DIANN report, then calls MSstatsConvert::MSstatsAnomalyScores via DIANNtoMSstatsFormat with quality_metrics c(Ms1ProfileCorr, Evidence, DeltaRT) and temporal directions c(mean_decrease, mean_decrease, dispersion_increase). Requires a run order CSV.", + class = "icon-tooltip") + ), + value = calculate_anomaly_def), + if (isTRUE(calculate_anomaly_def)) { + 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")) + } + ) +} + +#' Create the Spectronaut regular-path anomaly UI: the Calculate Anomaly Scores +#' checkbox + its nested run-order fileInput. Emitted by +#' `output$spectronaut_anomaly_ui` (renderUI) so it mounts only on the regular +#' path (filetype == 'spec' && !big_file_spec); the big-file copy comes from +#' `create_spectronaut_large_annotation_ui`. renderUI gating keeps the two from +#' coexisting, so their shared `ns("calculate_anomaly_scores")` / +#' `ns("run_order_file")` ids never collide. The run-order fileInput is emitted +#' only when the checkbox is ticked (no conditionalPanel), so its upload is +#' dropped on a converter switch, big_file toggle, or checkbox toggle (accepted). +#' @noRd +create_spectronaut_anomaly_ui <- function(ns, calculate_anomaly_def = FALSE) { + tagList( + checkboxInput(ns("calculate_anomaly_scores"), + label = tags$span( + "Calculate Anomaly Scores", + class = "icon-wrapper", + icon("question-circle", lib = "font-awesome"), + div("Calculate anomaly scores for each feature based on a random forest model. This requires a CSV file containing the order of your MS runs.", + class = "icon-tooltip") + ), + value = calculate_anomaly_def), + if (isTRUE(calculate_anomaly_def)) { + fileInput(ns("run_order_file"), + label = h5("Upload Run Order File", class = "icon-wrapper", + icon("question-circle", lib = "font-awesome"), + div("The run order file should be a CSV with two columns: 'Run' and 'Order'. 'Run' contains the sequence name, and 'Order' contains the chronological run number (e.g., 1, 2, 3...).", class = "icon-tooltip")), + multiple = FALSE, accept = c(".csv")) + } + ) +} + +#' 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 @@ -612,39 +689,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"), @@ -654,71 +734,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 @@ -735,59 +818,49 @@ 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-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(). #' @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). #' @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 - 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-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), + 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,77 +868,51 @@ 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) - ) - ), - - conditionalPanel( - condition = "input['loadpage-filetype'] == 'spec'", - checkboxInput(ns("calculate_anomaly_scores"), - label = tags$span( - "Calculate Anomaly Scores", - class = "icon-wrapper", - icon("question-circle", lib = "font-awesome"), - div("Calculate anomaly scores for each feature based on a random forest model. This requires a CSV file containing the order of your MS runs.", - class = "icon-tooltip") - ), - value = FALSE), - conditionalPanel( - condition = "input['loadpage-calculate_anomaly_scores']", - fileInput(ns("run_order_file"), - label = h5("Upload Run Order File", class = "icon-wrapper", - icon("question-circle", lib = "font-awesome"), - div("The run order file should be a CSV with two columns: 'Run' and 'Order'. 'Run' contains the sequence name, and 'Order' contains the chronological run number (e.g., 1, 2, 3...).", class = "icon-tooltip")), - multiple = FALSE, accept = c(".csv")) - ) - ), - - # DIANN anomaly scoring (regular path). - # - # DIANN reports do not ship a DeltaRT column; it's engineered as - # RT - Predicted.RT before the converter runs. The user supplies a - # run-order CSV (same shape as the Spectronaut path) so - # MSstatsConvert::MSstatsAnomalyScores (invoked internally by - # 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"), - label = tags$span( - "Calculate Anomaly Scores", - class = "icon-wrapper", - icon("question-circle", lib = "font-awesome"), - div("Engineers DeltaRT = RT - Predicted.RT in the raw DIANN report, then calls MSstatsConvert::MSstatsAnomalyScores via DIANNtoMSstatsFormat with quality_metrics c(Ms1ProfileCorr, Evidence, DeltaRT) and temporal directions c(mean_decrease, mean_decrease, dispersion_increase). Requires a run order CSV.", - class = "icon-tooltip") - ), - value = FALSE), - conditionalPanel( - condition = "input['loadpage-diann_calculate_anomaly_scores']", - fileInput(ns("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'", - checkboxInput(ns("m_score"), "Filter with M-score"), - conditionalPanel( - condition = "input['loadpage-m_score']", + # 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) + )) + )), + + # Spectronaut regular-path anomaly scoring (Calculate Anomaly Scores + # checkbox + nested run-order fileInput). Emitted server-side by + # `output$spectronaut_anomaly_ui` so it mounts only on the regular path + # (filetype == 'spec' && !big_file_spec); the big-file path emits its own + # copy from `output$spectronaut_options_ui`. renderUI keeps the two copies + # from coexisting, so their shared `ns("calculate_anomaly_scores")` / + # `ns("run_order_file")` ids never collide. + uiOutput(ns("spectronaut_anomaly_ui")), + + # DIANN regular-path anomaly scoring. Emitted server-side by + # `output$diann_anomaly_ui` (renderUI) so it mounts only on the regular + # DIANN path (filetype == 'diann' && !big_file_diann) — the same renderUI + # pattern as the Spectronaut regular anomaly UI above. The run-order + # fileInput is emitted by the renderUI when the checkbox is ticked. + uiOutput(ns("diann_anomaly_ui")), + + # 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 new file mode 100644 index 0000000..41fad40 --- /dev/null +++ b/tests/testthat/test-loadpage-server-rendering.R @@ -0,0 +1,519 @@ +# 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("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") +}) + + +# ============================================================================ +# Phase 2 predicate truth tables + namespace assertions +# ============================================================================ + +test_that("loadpage_show_sample_dataset_description matches one mode at a time", { + # All cases below hold the picker VISIBLE (bio = "Protein", dda_dia = "LType") + # so they isolate the filetype/LabelFreeType logic; the picker-hidden orphan + # case is covered by the regression test below. + for (mode in c("DDA", "DIA", "SRM_PRM")) { + # Active mode positive + expect_true(MSstatsShiny:::loadpage_show_sample_dataset_description("sample", mode, mode, "Protein", "LType"), + 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, "Protein", "LType"), + 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, "Protein", "LType"), + info = paste("target", mode, "actual", other)) + } + # NULL LabelFreeType FALSE + expect_false(MSstatsShiny:::loadpage_show_sample_dataset_description("sample", NULL, mode, "Protein", "LType")) + } +}) + +test_that("loadpage_show_sample_dataset_description hides with the picker (orphaned-description regression)", { + # CodeRabbit orphan: filetype stays 'sample' and a LabelFreeType value is + # still selected, but the picker hides because BIO == 'PTM' or DDA_DIA leaves + # 'LType'. The description MUST hide with the picker — it now composes on + # loadpage_show_sample_dataset_label_free_type_selector, so a visible + # description implies a visible picker by construction. + + # Positive control: picker visible -> description shows. + expect_true(MSstatsShiny:::loadpage_show_sample_dataset_description( + "sample", "DDA", "DDA", "Protein", "LType")) + + # BIO flipped to PTM -> picker hidden -> description hidden (was the orphan). + expect_false(MSstatsShiny:::loadpage_show_sample_dataset_description( + "sample", "DDA", "DDA", "PTM", "LType")) + # DDA_DIA left LType (e.g. TMT) -> picker hidden -> description hidden. + expect_false(MSstatsShiny:::loadpage_show_sample_dataset_description( + "sample", "DDA", "DDA", "Protein", "TMT")) +}) + +test_that("loadpage_show_sample_dataset_label_free_type_selector requires non-PTM + sample + LType", { + expect_true(MSstatsShiny:::loadpage_show_sample_dataset_label_free_type_selector("Protein", "sample", "LType")) + expect_true(MSstatsShiny:::loadpage_show_sample_dataset_label_free_type_selector("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_sample_dataset_label_free_type_selector(NULL, "sample", "LType")) + + expect_false(MSstatsShiny:::loadpage_show_sample_dataset_label_free_type_selector("PTM", "sample", "LType")) + expect_false(MSstatsShiny:::loadpage_show_sample_dataset_label_free_type_selector("Protein", "diann", "LType")) + expect_false(MSstatsShiny:::loadpage_show_sample_dataset_label_free_type_selector("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_label_free_upload is non-PTM label-free only", { + expect_true(MSstatsShiny:::loadpage_show_msstats_label_free_upload("msstats", "Protein", "LType")) + expect_true(MSstatsShiny:::loadpage_show_msstats_label_free_upload("msstats", "Peptide", "LType")) + expect_false(MSstatsShiny:::loadpage_show_msstats_label_free_upload("msstats", "Protein", "TMT")) + expect_false(MSstatsShiny:::loadpage_show_msstats_label_free_upload("msstats", "PTM", "LType")) + expect_false(MSstatsShiny:::loadpage_show_msstats_label_free_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") +}) + + +# ---------------------------------------------------------------------------- +# Post-proceed1 summary-table predicates (BIO-driven). NULL bio behaves like +# "not PTM" — mirroring the original `input['loadpage-BIO'] !== 'PTM'` JS — so +# the non-PTM summary shows and the PTM summary hides when BIO is unset. +# ---------------------------------------------------------------------------- + +test_that("loadpage_show_nonptm_summary is TRUE for non-PTM (incl. NULL), FALSE for PTM", { + expect_true(MSstatsShiny:::loadpage_show_nonptm_summary("Protein")) + expect_true(MSstatsShiny:::loadpage_show_nonptm_summary("Peptide")) + expect_true(MSstatsShiny:::loadpage_show_nonptm_summary(NULL)) + expect_false(MSstatsShiny:::loadpage_show_nonptm_summary("PTM")) +}) + +test_that("loadpage_show_ptm_summary is TRUE only for PTM (NULL -> FALSE)", { + expect_true(MSstatsShiny:::loadpage_show_ptm_summary("PTM")) + expect_false(MSstatsShiny:::loadpage_show_ptm_summary("Protein")) + expect_false(MSstatsShiny:::loadpage_show_ptm_summary("Peptide")) + expect_false(MSstatsShiny:::loadpage_show_ptm_summary(NULL)) +}) + + +# ---------------------------------------------------------------------------- +# 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") + # Post-proceed1 summary tables + expect_equal(NAMESPACE_LOADPAGE$summary_nonptm_panel, "summary_nonptm_panel") + expect_equal(NAMESPACE_LOADPAGE$summary_ptm_panel, "summary_ptm_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..977aa75 100644 --- a/tests/testthat/test-module-loadpage-ui.R +++ b/tests/testthat/test-module-loadpage-ui.R @@ -60,27 +60,158 @@ 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" ) - - 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 regular-path anomaly UI is a server-rendered slot, not a static conditionalPanel", { + # `calculate_anomaly_scores` + `run_order_file` are also declared by the + # big-file Spectronaut helper (`create_spectronaut_large_annotation_ui`, + # emitted by `output$spectronaut_options_ui`). To keep the two copies from + # colliding on a shared ns() id, the regular path now emits them from + # `output$spectronaut_anomaly_ui` (renderUI), which mounts only on the + # regular path (filetype == 'spec' && !big_file_spec). The static + # quality-filtering options therefore expose only the renderUI slot. + options <- create_quality_filtering_options(NS("test")) + options_html <- as.character(options) + + # The renderUI slot is present... + expect_true(grepl('id="test-spectronaut_anomaly_ui"', options_html, fixed = TRUE), + info = "spectronaut_anomaly_ui renderUI slot missing") + # ...and the regular-path anomaly inputs are NOT mounted statically (they are + # emitted server-side), so there is no duplicate-ns()-id with the big-file copy. + expect_false(grepl("test-calculate_anomaly_scores", options_html, fixed = TRUE), + info = "calculate_anomaly_scores must be server-rendered, not static") + expect_false(grepl("test-run_order_file", options_html, fixed = TRUE), + info = "run_order_file must be server-rendered, not static") + # The old regular-path anomaly conditionalPanel JS condition must be gone. + expect_false(grepl("input['loadpage-filetype'] == 'spec'", + options_html, fixed = TRUE), + info = "regular-path anomaly conditionalPanel should be gone") +}) + +test_that("create_spectronaut_anomaly_ui emits the checkbox and (only when ticked) the run-order fileInput", { + # Default (unticked): checkbox present, run-order fileInput ABSENT. The + # fileInput is emitted by the renderUI only when the checkbox is ticked (no + # conditionalPanel) — that renderUI gating is what keeps the regular and + # big-file copies (same ns() ids) from coexisting in the DOM. + html <- as.character(create_spectronaut_anomaly_ui(NS("test"))) + expect_true(grepl("test-calculate_anomaly_scores", html, fixed = TRUE)) + expect_true(grepl("Calculate Anomaly Scores", html, fixed = TRUE)) + expect_false(grepl("test-run_order_file", html, fixed = TRUE), + info = "run-order fileInput must be absent when the checkbox is unticked") + expect_false(grepl("data-display-if", html, fixed = TRUE), + info = "no conditionalPanel — the nesting is renderUI-gated now") + + # Ticked: checkbox pre-checked (seed) + run-order fileInput present. Ids are + # the SAME literals the big-file helper uses (no rename). + checked <- as.character(create_spectronaut_anomaly_ui(NS("test"), TRUE)) + expect_true(grepl("test-run_order_file", checked, fixed = TRUE)) + expect_true(grepl("checked", checked, fixed = TRUE)) +}) + +test_that("create_diann_anomaly_ui emits the checkbox and (only when ticked) the run-order fileInput", { + # DIANN regular path, migrated from Phase 1 show/hide to the same renderUI- + # gated pattern as the Spectronaut regular helper. Uses the diann_* ids + # (unchanged, distinct from the big-file big_diann_* ids). + html <- as.character(create_diann_anomaly_ui(NS("test"))) + expect_true(grepl("test-diann_calculate_anomaly_scores", html, fixed = TRUE)) + expect_true(grepl("Calculate Anomaly Scores", html, fixed = TRUE)) + expect_false(grepl("test-diann_run_order_file", html, fixed = TRUE), + info = "run-order fileInput must be absent when the checkbox is unticked") + expect_false(grepl("data-display-if", html, fixed = TRUE), + info = "no conditionalPanel — the nesting is renderUI-gated now") + + checked <- as.character(create_diann_anomaly_ui(NS("test"), TRUE)) + expect_true(grepl("test-diann_run_order_file", checked, fixed = TRUE)) + expect_true(grepl("checked", checked, fixed = TRUE)) +}) + +test_that("create_spectronaut_large_annotation_ui gates the run-order fileInput on the checkbox (renderUI, no conditionalPanel)", { + # Big-file Spectronaut path: the run-order fileInput is emitted only when the + # checkbox is ticked (was a conditionalPanel before). Shares the regular + # path's ns() ids; renderUI mounting on mutually exclusive big_file_spec keeps + # them from colliding. + html <- as.character(create_spectronaut_large_annotation_ui(NS("test"))) + expect_true(grepl("test-big_spec_annotation", html, fixed = TRUE)) + expect_true(grepl("test-calculate_anomaly_scores", html, fixed = TRUE)) + expect_false(grepl("test-run_order_file", html, fixed = TRUE), + info = "run-order fileInput must be absent when the checkbox is unticked") + expect_false(grepl("data-display-if", html, fixed = TRUE), + info = "no conditionalPanel — the nesting is renderUI-gated now") + + checked <- as.character(create_spectronaut_large_annotation_ui(NS("test"), TRUE)) + expect_true(grepl("test-run_order_file", checked, fixed = TRUE)) +}) + test_that("loadpageUI properly handles file input elements and validation", { # Test that file inputs are properly configured result <- loadpageUI("test") @@ -132,19 +263,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 +355,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_sample_dataset_label_free_type_selector()` (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() @@ -330,9 +489,14 @@ test_that("create_quality_filtering_options creates filtering controls", { expect_true(grepl("Q-value cutoff", options_html)) expect_true(grepl("M-score cutoff", options_html)) expect_true(grepl("MBR Enabled", options_html)) - # Regular DIANN anomaly scoring controls (parallel to Spectronaut's). - expect_true(grepl("test-diann_calculate_anomaly_scores", options_html)) - expect_true(grepl("test-diann_run_order_file", options_html)) + # DIANN regular-path anomaly UI is now a server-rendered slot (renderUI), + # parallel to Spectronaut's — the inputs are emitted server-side, not static. + expect_true(grepl('id="test-diann_anomaly_ui"', options_html, fixed = TRUE), + info = "diann_anomaly_ui renderUI slot missing") + expect_false(grepl("test-diann_calculate_anomaly_scores", options_html, fixed = TRUE), + info = "diann_calculate_anomaly_scores must be server-rendered, not static") + expect_false(grepl("test-diann_run_order_file", options_html, fixed = TRUE), + info = "diann_run_order_file must be server-rendered, not static") }) # Test order preservation in main selection controls @@ -441,23 +605,46 @@ test_that("DIANN large-file helper functions create correct UI elements", { expect_true(grepl("Backend", bottom_html)) expect_true(grepl("arrow", bottom_html)) - # Annotation + anomaly UI + # Annotation + anomaly UI. The run-order fileInput is now renderUI-gated on + # the checkbox (no show/hide panel): absent by default, present only when + # calculate_anomaly_def = TRUE. annot_ui <- create_diann_large_annotation_ui(NS("test")) annot_html <- as.character(annot_ui) expect_true(grepl("Annotation file", annot_html)) expect_true(grepl("test-big_diann_annotation", annot_html)) expect_true(grepl("Calculate Anomaly Scores", annot_html)) expect_true(grepl("test-big_diann_calculate_anomaly_scores", annot_html)) - expect_true(grepl("test-big_diann_run_order_file", annot_html)) + expect_false(grepl("test-big_diann_run_order_file", annot_html, fixed = TRUE), + info = "run-order fileInput must be absent when the checkbox is unticked") + annot_html_checked <- as.character(create_diann_large_annotation_ui(NS("test"), TRUE)) + expect_true(grepl("test-big_diann_run_order_file", annot_html_checked, fixed = TRUE), + info = "run-order fileInput must appear when calculate_anomaly_def = TRUE") }) -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", { + # The DIANN big-file gate (`!big_file_diann`) lives in server-side visibility + # code, not a JS `conditionalPanel` condition: `loadpage_show_qval_filter` / + # `loadpage_show_standard_annot_upload` drive show/hide panels, and the DIANN + # regular-path anomaly UI is now a renderUI slot (`diann_anomaly_ui`). So the + # JS-encoded `loadpage-big_file_diann` string is not emitted statically. We + # assert the gated containers / slots are present and the JS string is 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_ui")) { + 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-converter-options-panel.R") + ) }) test_that("Spectronaut helper functions create correct UI elements", {