From 6a3272d755f414e9239b5a7a00f65f1425d56acb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerardo=20J=2E=20Rodr=C3=ADguez?= <68994823+Gero1999@users.noreply.github.com> Date: Wed, 15 Apr 2026 13:25:31 +0000 Subject: [PATCH 01/20] refactor: detect unit changes by comparing PPSTRESU vs PPORRESU Replace the default flag filter in the save handler with a value-based comparison. Rows where PPSTRESU differs from PPORRESU are treated as changed, regardless of whether the change came from a user edit or an automatic simplification (e.g. volume units). This ensures the volume unit simplification from tab_data.R is captured in session$userData$units_table() and subsequently included in the exported settings and R script. Refs #1190 Co-authored-by: Ona --- inst/shiny/modules/tab_nca/units_table.R | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/inst/shiny/modules/tab_nca/units_table.R b/inst/shiny/modules/tab_nca/units_table.R index f0ada8f0c..f2ecc6a4d 100644 --- a/inst/shiny/modules/tab_nca/units_table.R +++ b/inst/shiny/modules/tab_nca/units_table.R @@ -193,7 +193,8 @@ units_table_server <- function(id, mydata) { log_trace("Applying custom units specification.") modal_units_table() %>% - dplyr::filter(!default) %>% + dplyr::filter(PPSTRESU != PPORRESU) %>% + dplyr::select(-any_of("default")) %>% session$userData$units_table() # Close the modal message window for the user From f750289e5fa1672a13524fd505ef38f80f0e0e87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerardo=20J=2E=20Rodr=C3=ADguez?= <68994823+Gero1999@users.noreply.github.com> Date: Wed, 15 Apr 2026 13:25:50 +0000 Subject: [PATCH 02/20] refactor: remove default flag from units table modal open logic The modal no longer adds a default column when building the table. Custom units are merged by matching on all columns except PPSTRESU and conversion_factor, without needing a tracking flag. Refs #1190 Co-authored-by: Ona --- inst/shiny/modules/tab_nca/units_table.R | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/inst/shiny/modules/tab_nca/units_table.R b/inst/shiny/modules/tab_nca/units_table.R index f2ecc6a4d..7b8b2f833 100644 --- a/inst/shiny/modules/tab_nca/units_table.R +++ b/inst/shiny/modules/tab_nca/units_table.R @@ -26,13 +26,12 @@ units_table_server <- function(id, mydata) { modal_units_table <- reactiveVal(NULL) observeEvent(input$open_units_table, { - default_units <- mydata()$units %>% - dplyr::mutate(default = TRUE) + default_units <- mydata()$units if (!is.null(session$userData$units_table())) { - custom_units <- dplyr::mutate(session$userData$units_table(), default = FALSE) + custom_units <- session$userData$units_table() by_cols <- intersect(names(default_units), names(custom_units)) - by_cols <- setdiff(by_cols, c("PPSTRESU", "conversion_factor", "default")) + by_cols <- setdiff(by_cols, c("PPSTRESU", "conversion_factor")) dplyr::rows_update( default_units, custom_units, From dc649586d17511464aae37a8b1e3e3c914c754ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerardo=20J=2E=20Rodr=C3=ADguez?= <68994823+Gero1999@users.noreply.github.com> Date: Wed, 15 Apr 2026 13:26:09 +0000 Subject: [PATCH 03/20] refactor: remove default column from reactable and edit handler The default column is no longer needed in the reactable definition (was hidden) or in the edit handler (was set to FALSE on edit). Change detection is now purely value-based. Refs #1190 Co-authored-by: Ona --- inst/shiny/modules/tab_nca/units_table.R | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/inst/shiny/modules/tab_nca/units_table.R b/inst/shiny/modules/tab_nca/units_table.R index 7b8b2f833..676ca6fd2 100644 --- a/inst/shiny/modules/tab_nca/units_table.R +++ b/inst/shiny/modules/tab_nca/units_table.R @@ -101,8 +101,7 @@ units_table_server <- function(id, mydata) { PPORRESU = colDef(name = "Default Unit"), PPSTRESU = colDef(name = "Custom Unit"), conversion_factor = colDef(name = "Conversion Factor"), - is_hidden = colDef(show = FALSE), - default = colDef(show = FALSE) + is_hidden = colDef(show = FALSE) ), pagination = FALSE, filterable = TRUE, @@ -155,7 +154,6 @@ units_table_server <- function(id, mydata) { ) } - modal_units_table[info$row, "default"] <- FALSE modal_units_table[info$row, "conversion_factor"] <- conversion_factor_value } From be27b41e863ed2ec14c1aa4890d04bda43a3fe73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerardo=20J=2E=20Rodr=C3=ADguez?= <68994823+Gero1999@users.noreply.github.com> Date: Wed, 15 Apr 2026 13:26:33 +0000 Subject: [PATCH 04/20] refactor: remove default flag filtering from settings export The units table stored in session$userData$units_table() now only contains rows where PPSTRESU != PPORRESU, so the export paths in nca_setup.R (settings download) and zip-utils.R (ZIP export) no longer need to filter by the default flag. Refs #1190 Co-authored-by: Ona --- inst/shiny/functions/zip-utils.R | 7 ++----- inst/shiny/modules/tab_nca/nca_setup.R | 5 ----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/inst/shiny/functions/zip-utils.R b/inst/shiny/functions/zip-utils.R index 8943307a6..d4981bb3a 100644 --- a/inst/shiny/functions/zip-utils.R +++ b/inst/shiny/functions/zip-utils.R @@ -523,11 +523,8 @@ prepare_export_files <- function(target_dir, .export_settings <- function(target_dir, session) { settings_list <- session$userData$settings() - if (!is.null(settings_list$units)) { - settings_list$units <- settings_list$units %>% - dplyr::filter(!default) %>% - dplyr::select(-default) - } + # units table from session$userData$units_table() already contains + # only changed rows (PPSTRESU != PPORRESU), no filtering needed. settings_list$ratio_table <- session$userData$ratio_table() diff --git a/inst/shiny/modules/tab_nca/nca_setup.R b/inst/shiny/modules/tab_nca/nca_setup.R index 83cd1e2ce..3dcc0da74 100644 --- a/inst/shiny/modules/tab_nca/nca_setup.R +++ b/inst/shiny/modules/tab_nca/nca_setup.R @@ -228,11 +228,6 @@ nca_setup_server <- function(id, data, adnca_data, extra_group_vars, settings_ov }, content = function(con) { export_settings <- final_settings() - if (!is.null(export_settings$units)) { - export_settings$units <- export_settings$units %>% - filter(!default) %>% - select(-default) - } export_settings$ratio_table <- ratio_table() payload <- list( settings = export_settings, From 339b5595b33cf33893c81b2a87d23c8cabe4d33b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerardo=20J=2E=20Rodr=C3=ADguez?= <68994823+Gero1999@users.noreply.github.com> Date: Wed, 15 Apr 2026 13:27:05 +0000 Subject: [PATCH 05/20] refactor: remove default flag from global units_table observer The observer that syncs the local modal_units_table with the global session$userData$units_table no longer uses the default column. Merging is done by matching on all columns except PPSTRESU and conversion_factor. Refs #1190 Co-authored-by: Ona --- inst/shiny/modules/tab_nca/units_table.R | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/inst/shiny/modules/tab_nca/units_table.R b/inst/shiny/modules/tab_nca/units_table.R index 676ca6fd2..c24d9ea7a 100644 --- a/inst/shiny/modules/tab_nca/units_table.R +++ b/inst/shiny/modules/tab_nca/units_table.R @@ -200,12 +200,11 @@ units_table_server <- function(id, mydata) { #' Update local `modal_units_table()` if the global value changes. observeEvent(session$userData$units_table(), { - default_units <- mydata()$units %>% - dplyr::mutate(default = TRUE) + default_units <- mydata()$units - custom_units <- dplyr::mutate(session$userData$units_table(), default = FALSE) + custom_units <- session$userData$units_table() by_cols <- intersect(names(default_units), names(custom_units)) - by_cols <- setdiff(by_cols, c("PPSTRESU", "conversion_factor", "default")) + by_cols <- setdiff(by_cols, c("PPSTRESU", "conversion_factor")) dplyr::rows_update( default_units, custom_units, From aa406cdc30967c08b814ad7fdd921faa57904d20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerardo=20J=2E=20Rodr=C3=ADguez?= <68994823+Gero1999@users.noreply.github.com> Date: Wed, 15 Apr 2026 13:27:34 +0000 Subject: [PATCH 06/20] cleanup: remove residual default column guards Remove the select(-any_of("default")) guard from tab_nca.R and the save handler in units_table.R. The default column no longer exists in the units table, so these guards are unnecessary. Refs #1190 Co-authored-by: Ona --- inst/shiny/modules/tab_nca.R | 4 +--- inst/shiny/modules/tab_nca/units_table.R | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/inst/shiny/modules/tab_nca.R b/inst/shiny/modules/tab_nca.R index bd33e9972..b232a2e03 100644 --- a/inst/shiny/modules/tab_nca.R +++ b/inst/shiny/modules/tab_nca.R @@ -136,9 +136,7 @@ tab_nca_server <- function(id, pknca_data, extra_group_vars, settings_override) # Update units table processed_pknca_data <- processed_pknca_data() if (!is.null(session$userData$units_table())) { - custom_units <- select( - session$userData$units_table(), -any_of("default") - ) + custom_units <- session$userData$units_table() by_cols <- intersect(names(processed_pknca_data$units), names(custom_units)) by_cols <- setdiff(by_cols, c("PPSTRESU", "conversion_factor")) processed_pknca_data$units <- rows_update( diff --git a/inst/shiny/modules/tab_nca/units_table.R b/inst/shiny/modules/tab_nca/units_table.R index c24d9ea7a..8e27faab5 100644 --- a/inst/shiny/modules/tab_nca/units_table.R +++ b/inst/shiny/modules/tab_nca/units_table.R @@ -191,7 +191,6 @@ units_table_server <- function(id, mydata) { log_trace("Applying custom units specification.") modal_units_table() %>% dplyr::filter(PPSTRESU != PPORRESU) %>% - dplyr::select(-any_of("default")) %>% session$userData$units_table() # Close the modal message window for the user From b66eec9e7d1be772b56b6710fe94032bb1bef378 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerardo=20J=2E=20Rodr=C3=ADguez?= <68994823+Gero1999@users.noreply.github.com> Date: Wed, 15 Apr 2026 13:28:21 +0000 Subject: [PATCH 07/20] chore: bump version to 0.1.0.9150 and update NEWS.md Refs #1190 Co-authored-by: Ona --- DESCRIPTION | 2 +- NEWS.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index 2a9bba8ff..5e0652702 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: aNCA Title: (Pre-)Clinical NCA in a Dynamic Shiny App -Version: 0.1.0.9149 +Version: 0.1.0.9150 Authors@R: c( person("Ercan", "Suekuer", email = "ercan.suekuer@roche.com", role = "aut", comment = c(ORCID = "0009-0001-1626-1526")), diff --git a/NEWS.md b/NEWS.md index bc5c285ad..6b638569b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -57,6 +57,7 @@ * Settings file now outputs and time duplicate exclusions and processes them automatically upon settings upload (#1195) ## Bugs fixed +* R script and settings export now include volume unit simplifications. Unit change detection uses value comparison (`PPSTRESU != PPORRESU`) instead of an edit-tracking flag, so automatic simplifications (e.g. `mg*L/mL` → `mg`) are captured alongside user edits (#1190) * SASS compilation moved from runtime (`app.R`) to a `data-raw/compile_css.R` script, fixing startup crashes on read-only deployments (#1107) * ZIP folder with results will now include the exploration tab outputs: individual plots, mean plots (#794) * Updated TMAX label from Time of CMAX to Time of CMAX Observation (#787) From 4b4de94829e214c65a55ed435889274792fe74fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerardo=20J=2E=20Rodr=C3=ADguez?= <68994823+Gero1999@users.noreply.github.com> Date: Wed, 15 Apr 2026 13:47:33 +0000 Subject: [PATCH 08/20] fix: auto-populate units_table from simplified PKNCAdata units When pknca_data() is created, any units where PPSTRESU differs from PPORRESU (e.g. volume unit simplification) are automatically stored in session$userData$units_table(). This ensures the settings export and R script include these changes even if the user never opens the Units modal. Previously, session$userData$units_table() stayed NULL until the modal was opened and saved, causing settings to export units: ~. Refs #1190 Co-authored-by: Ona --- inst/shiny/modules/tab_nca.R | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/inst/shiny/modules/tab_nca.R b/inst/shiny/modules/tab_nca.R index b232a2e03..24a737534 100644 --- a/inst/shiny/modules/tab_nca.R +++ b/inst/shiny/modules/tab_nca.R @@ -84,6 +84,18 @@ tab_nca_server <- function(id, pknca_data, extra_group_vars, settings_override) #' should respect the units, regardless of location. session$userData$units_table <- reactiveVal(NULL) + # Auto-populate units_table with any units that differ from their defaults + # (e.g. volume unit simplification from tab_data.R). This ensures the + # settings export and R script include these changes even if the user + # never opens the Units modal. + observeEvent(pknca_data(), { + units <- pknca_data()$units + changed <- units[units$PPSTRESU != units$PPORRESU, , drop = FALSE] + if (nrow(changed) > 0) { + session$userData$units_table(changed) + } + }) + adnca_data <- reactive(pknca_data()$conc$data) # #' NCA Setup module From 5b9b9132c83966053327bb9cb3cc3065cbae8129 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerardo=20J=2E=20Rodr=C3=ADguez?= <68994823+Gero1999@users.noreply.github.com> Date: Wed, 15 Apr 2026 13:51:21 +0000 Subject: [PATCH 09/20] fix: exclude NA rows from units change detection PKNCA can produce units rows where PPORRESU and PPSTRESU are both NA (parameters with no derivable units). The comparison NA != NA returns NA, which R treats as truthy in subsetting, causing these rows to leak into the saved units table as .na.character entries. Add explicit is.na() guards in both the auto-populate observer and the modal save handler. Refs #1190 Co-authored-by: Ona --- inst/shiny/modules/tab_nca.R | 4 +++- inst/shiny/modules/tab_nca/units_table.R | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/inst/shiny/modules/tab_nca.R b/inst/shiny/modules/tab_nca.R index 24a737534..96e98a415 100644 --- a/inst/shiny/modules/tab_nca.R +++ b/inst/shiny/modules/tab_nca.R @@ -90,7 +90,9 @@ tab_nca_server <- function(id, pknca_data, extra_group_vars, settings_override) # never opens the Units modal. observeEvent(pknca_data(), { units <- pknca_data()$units - changed <- units[units$PPSTRESU != units$PPORRESU, , drop = FALSE] + differs <- !is.na(units$PPSTRESU) & !is.na(units$PPORRESU) & + units$PPSTRESU != units$PPORRESU + changed <- units[differs, , drop = FALSE] if (nrow(changed) > 0) { session$userData$units_table(changed) } diff --git a/inst/shiny/modules/tab_nca/units_table.R b/inst/shiny/modules/tab_nca/units_table.R index 8e27faab5..cc7873e7f 100644 --- a/inst/shiny/modules/tab_nca/units_table.R +++ b/inst/shiny/modules/tab_nca/units_table.R @@ -190,7 +190,7 @@ units_table_server <- function(id, mydata) { log_trace("Applying custom units specification.") modal_units_table() %>% - dplyr::filter(PPSTRESU != PPORRESU) %>% + dplyr::filter(!is.na(PPSTRESU), !is.na(PPORRESU), PPSTRESU != PPORRESU) %>% session$userData$units_table() # Close the modal message window for the user From e372eb1fdb4095ccb10837c56f84a2319cab6013 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerardo=20J=2E=20Rodr=C3=ADguez?= <68994823+Gero1999@users.noreply.github.com> Date: Wed, 15 Apr 2026 16:12:48 +0200 Subject: [PATCH 10/20] address if the user loads new data with no unit diffs after previously having diffs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- inst/shiny/modules/tab_nca.R | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/inst/shiny/modules/tab_nca.R b/inst/shiny/modules/tab_nca.R index 96e98a415..2dd70a91d 100644 --- a/inst/shiny/modules/tab_nca.R +++ b/inst/shiny/modules/tab_nca.R @@ -84,20 +84,20 @@ tab_nca_server <- function(id, pknca_data, extra_group_vars, settings_override) #' should respect the units, regardless of location. session$userData$units_table <- reactiveVal(NULL) - # Auto-populate units_table with any units that differ from their defaults - # (e.g. volume unit simplification from tab_data.R). This ensures the - # settings export and R script include these changes even if the user - # never opens the Units modal. + # Keep units_table synchronized with the current dataset. Store the full + # units table so downstream code never receives a partial units table, and + # clear it when no units are available to avoid stale values persisting + # across uploads. observeEvent(pknca_data(), { - units <- pknca_data()$units - differs <- !is.na(units$PPSTRESU) & !is.na(units$PPORRESU) & - units$PPSTRESU != units$PPORRESU - changed <- units[differs, , drop = FALSE] - if (nrow(changed) > 0) { - session$userData$units_table(changed) + current_pknca_data <- pknca_data() + + if (is.null(current_pknca_data) || is.null(current_pknca_data$units)) { + session$userData$units_table(NULL) + return() } - }) + session$userData$units_table(current_pknca_data$units) + }, ignoreNULL = FALSE) adnca_data <- reactive(pknca_data()$conc$data) # #' NCA Setup module From f4f71d2f81e3ca9c54ee40894d403d7d33277e46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerardo=20J=2E=20Rodr=C3=ADguez?= <68994823+Gero1999@users.noreply.github.com> Date: Mon, 20 Apr 2026 08:23:09 +0000 Subject: [PATCH 11/20] fix: store full units table internally, filter to changed rows on export MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit session$userData$units_table() stores the full units table because downstream code (nca_results.R) replaces res$data$units with it and joins it to results — both require the complete table. The settings YAML and ZIP exports now filter to only rows where PPSTRESU != PPORRESU, preserving the compact settings format from #900. This applies at both export points: settings download (nca_setup.R) and ZIP export (zip-utils.R). The modal save handler now stores the full modal table instead of filtering, consistent with the auto-populate observer. Refs #1190 Co-authored-by: Ona --- inst/shiny/functions/zip-utils.R | 7 +++++-- inst/shiny/modules/tab_nca/nca_setup.R | 5 +++++ inst/shiny/modules/tab_nca/units_table.R | 4 +--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/inst/shiny/functions/zip-utils.R b/inst/shiny/functions/zip-utils.R index d4981bb3a..86328ddfa 100644 --- a/inst/shiny/functions/zip-utils.R +++ b/inst/shiny/functions/zip-utils.R @@ -523,8 +523,11 @@ prepare_export_files <- function(target_dir, .export_settings <- function(target_dir, session) { settings_list <- session$userData$settings() - # units table from session$userData$units_table() already contains - # only changed rows (PPSTRESU != PPORRESU), no filtering needed. + # Only export units that differ from defaults (PPSTRESU != PPORRESU) + if (!is.null(settings_list$units)) { + settings_list$units <- settings_list$units %>% + dplyr::filter(!is.na(PPSTRESU), !is.na(PPORRESU), PPSTRESU != PPORRESU) + } settings_list$ratio_table <- session$userData$ratio_table() diff --git a/inst/shiny/modules/tab_nca/nca_setup.R b/inst/shiny/modules/tab_nca/nca_setup.R index 3dcc0da74..a1e37be0d 100644 --- a/inst/shiny/modules/tab_nca/nca_setup.R +++ b/inst/shiny/modules/tab_nca/nca_setup.R @@ -228,6 +228,11 @@ nca_setup_server <- function(id, data, adnca_data, extra_group_vars, settings_ov }, content = function(con) { export_settings <- final_settings() + # Only export units that differ from defaults (PPSTRESU != PPORRESU) + if (!is.null(export_settings$units)) { + export_settings$units <- export_settings$units %>% + filter(!is.na(PPSTRESU), !is.na(PPORRESU), PPSTRESU != PPORRESU) + } export_settings$ratio_table <- ratio_table() payload <- list( settings = export_settings, diff --git a/inst/shiny/modules/tab_nca/units_table.R b/inst/shiny/modules/tab_nca/units_table.R index cc7873e7f..f4e51d2ac 100644 --- a/inst/shiny/modules/tab_nca/units_table.R +++ b/inst/shiny/modules/tab_nca/units_table.R @@ -189,9 +189,7 @@ units_table_server <- function(id, mydata) { } log_trace("Applying custom units specification.") - modal_units_table() %>% - dplyr::filter(!is.na(PPSTRESU), !is.na(PPORRESU), PPSTRESU != PPORRESU) %>% - session$userData$units_table() + session$userData$units_table(modal_units_table()) # Close the modal message window for the user removeModal() From d8b6875aa179941e6d4dfd070eb9d5ef91ccc85f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerardo=20J=2E=20Rodr=C3=ADguez?= <68994823+Gero1999@users.noreply.github.com> Date: Mon, 20 Apr 2026 08:26:20 +0000 Subject: [PATCH 12/20] fix: merge imported units into full data-derived table on settings upload The exported settings YAML only contains changed rows (PPSTRESU != PPORRESU). On import, these partial rows must be merged into the full data-derived units table, because downstream code (nca_results.R) expects the complete table. Uses rows_update to overlay the imported changes onto the base units, matching by all columns except PPSTRESU and conversion_factor. Waits for processed_pknca_data() to be available before merging. Refs #1190 Co-authored-by: Ona --- inst/shiny/modules/tab_nca/nca_setup.R | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/inst/shiny/modules/tab_nca/nca_setup.R b/inst/shiny/modules/tab_nca/nca_setup.R index a1e37be0d..87d11c563 100644 --- a/inst/shiny/modules/tab_nca/nca_setup.R +++ b/inst/shiny/modules/tab_nca/nca_setup.R @@ -166,7 +166,23 @@ nca_setup_server <- function(id, data, adnca_data, extra_group_vars, settings_ov has_full_units <- all(c("PPORRESU", "conversion_factor") %in% names(imported_units)) if (has_full_units) { - session$userData$units_table(imported_units) + # Imported settings contain only changed rows (PPSTRESU != PPORRESU). + # Merge them into the full data-derived units table so downstream code + # that expects the complete table (e.g. nca_results.R) works correctly. + observe({ + req(processed_pknca_data()) + data_units <- processed_pknca_data()$units + by_cols <- intersect(names(data_units), names(imported_units)) + by_cols <- setdiff(by_cols, c("PPSTRESU", "conversion_factor")) + merged <- rows_update( + data_units, + imported_units, + by = by_cols, + unmatched = "ignore" + ) + session$userData$units_table(merged) + }) %>% + bindEvent(processed_pknca_data(), once = TRUE) } else { # Defaults-only format: wait for data-derived units, then resolve. observe({ From 9d9f9c4d3912d78fc5b976a05307a1a8f8b35fd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerardo=20J=2E=20Rodr=C3=ADguez?= <68994823+Gero1999@users.noreply.github.com> Date: Wed, 22 Apr 2026 15:21:02 +0200 Subject: [PATCH 13/20] Bump version to 0.1.0.9153 --- DESCRIPTION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index 965be5db2..ba18af36f 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: aNCA Title: (Pre-)Clinical NCA in a Dynamic Shiny App -Version: 0.1.0.9152 +Version: 0.1.0.9153 Authors@R: c( person("Ercan", "Suekuer", email = "ercan.suekuer@roche.com", role = "aut", comment = c(ORCID = "0009-0001-1626-1526")), From 8b537928b909580f8d1e21b7b48f007ea2768a50 Mon Sep 17 00:00:00 2001 From: Gero1999 Date: Mon, 27 Apr 2026 07:38:44 +0000 Subject: [PATCH 14/20] refactor: reduce cyclomatic complexity of tab_nca_server Extract four helpers from the NCA calculation reactive: - .has_no_valid_parameters: parameter validation check - .notify_invalid_parameters: auto-replay vs manual notification - .run_nca_calculation: pipeline with warning capture - .finalize_nca_run: modal cleanup and auto-replay state reset Reduces cyclomatic complexity from 18 to below 15. Co-authored-by: Ona --- inst/shiny/modules/tab_nca.R | 215 +++++++++++++++++++---------------- 1 file changed, 120 insertions(+), 95 deletions(-) diff --git a/inst/shiny/modules/tab_nca.R b/inst/shiny/modules/tab_nca.R index bc77156c5..8541ec3d4 100644 --- a/inst/shiny/modules/tab_nca.R +++ b/inst/shiny/modules/tab_nca.R @@ -169,30 +169,10 @@ tab_nca_server <- function(id, pknca_data, extra_group_vars, settings_override, res_nca <- reactive({ req(processed_pknca_data()) - if (all(!unlist(processed_pknca_data()$intervals[sapply(processed_pknca_data()$intervals, - is.logical)]))) { + if (.has_no_valid_parameters(processed_pknca_data())) { log_error("Invalid parameters") - if (auto_nca_running()) { - auto_nca_running(FALSE) - session$userData$auto_replay_active <- FALSE - shiny::removeModal() - showNotification( - paste( - "Session restored but NCA could not be auto-run:", - "no suitable parameters for this dataset.", - "Please adjust settings and run NCA manually." - ), - type = "warning", duration = 10 - ) - } else { - showNotification( - paste( - "No suitable parameters selected for NCA calculation.", - "Please go back and select parameters suitable for the data." - ), - type = "error", duration = NULL - ) - } + .notify_invalid_parameters(auto_nca_running(), session) + auto_nca_running(FALSE) return(NULL) } @@ -203,87 +183,22 @@ tab_nca_server <- function(id, pknca_data, extra_group_vars, settings_override, log_info("Calculating NCA results...") tryCatch({ - # Create env for storing PKNCA run warnings, so that warning messages can be appended - # from within warning handler without bleeding to global env. - pknca_warn_env <- new.env() - pknca_warn_env$warnings <- c() - - # Update units table - processed_pknca_data <- processed_pknca_data() - if (!is.null(session$userData$units_table())) { - custom_units <- session$userData$units_table() - by_cols <- intersect(names(processed_pknca_data$units), names(custom_units)) - by_cols <- setdiff(by_cols, c("PPSTRESU", "conversion_factor")) - processed_pknca_data$units <- rows_update( - processed_pknca_data$units, - custom_units, - by = by_cols, - unmatched = "ignore" - ) - } - - #' Calculate results - res <- withCallingHandlers({ - processed_pknca_data %>% - # Check if there are exclusions that contains a filled reason - check_valid_pknca_data() %>% - # Perform PKNCA parameter calculations - PKNCA_calculate_nca( - blq_rule = settings()$data_imputation$blq_imputation_rule - ) %>% - # Add bioavailability results if requested - add_f_to_pknca_results(settings()$bioavailability) %>% - # Apply standard CDISC names - mutate( - PPTESTCD = translate_terms(PPTESTCD, "PKNCA", "PPTESTCD") - ) %>% - # Apply flag rules to mark results in the `exclude` column - PKNCA_hl_rules_exclusion( - rules = isolate(settings()$flags) %>% - purrr::keep(\(x) x$is.checked) %>% - purrr::map(\(x) x$threshold) - ) %>% - # Add parameter ratio calculations - calculate_table_ratios(ratio_table = ratio_table()) %>% - # Keep only parameters requested by the user - remove_pp_not_requested() - }, - warning = function(w) { - parsed <- .parse_pknca_warning(w) - if (!is.null(parsed)) { - log_warn("Warning during NCA calculation: {conditionMessage(w)}") - pknca_warn_env$warnings <- append(pknca_warn_env$warnings, parsed) - } - invokeRestart("muffleWarning") - }) - - # Display unique warnings thrown by PKNCA run. - purrr::walk(unique(pknca_warn_env$warnings), function(w) { - w_message <- paste0("PKNCA run produced a warning: ", w) - log_warn(w_message) - showNotification(w_message, type = "warning", duration = 5) - }) + res <- .run_nca_calculation( + processed_pknca_data(), settings, ratio_table, + session$userData$units_table() + ) updateTabsetPanel(session, "nca_navset", selected = "Results") - log_success("NCA results calculated.") res }, error = function(e) { - log_error("Error calculating NCA results:\n{conditionMessage(e)}") + log_error("Error calculating NCA results:\n", conditionMessage(e)) showNotification(.parse_pknca_error(e), type = "error", duration = NULL) NULL }, finally = { - if (auto_nca_running()) { - auto_nca_running(FALSE) - session$userData$auto_replay_active <- FALSE - # Dismiss the "Restoring session..." popup - shiny::removeModal() - log_success("Auto-replay: session restored with NCA results.") - } else { - # Delay the removal of loading modal to give it enough time to render - later::later(~shiny::removeModal(session = session), delay = 0.5) - } + .finalize_nca_run(auto_nca_running(), session) + auto_nca_running(FALSE) }) }) %>% bindEvent(input$run_nca) @@ -344,6 +259,116 @@ tab_nca_server <- function(id, pknca_data, extra_group_vars, settings_override, }) } +#' Check whether the PKNCAdata intervals contain any selected (TRUE) parameters. +#' @param pknca_data A PKNCAdata object. +#' @returns TRUE if no valid parameters are selected. +#' @noRd +.has_no_valid_parameters <- function(pknca_data) { + all(!unlist(pknca_data$intervals[sapply(pknca_data$intervals, is.logical)])) +} + +#' Show the appropriate notification when NCA has no valid parameters. +#' @param is_auto_replay Logical, whether this is an auto-replay run. +#' @param session Shiny session object. +#' @noRd +.notify_invalid_parameters <- function(is_auto_replay, session) { + if (is_auto_replay) { + session$userData$auto_replay_active <- FALSE + shiny::removeModal() + showNotification( + paste( + "Session restored but NCA could not be auto-run:", + "no suitable parameters for this dataset.", + "Please adjust settings and run NCA manually." + ), + type = "warning", duration = 10 + ) + } else { + showNotification( + paste( + "No suitable parameters selected for NCA calculation.", + "Please go back and select parameters suitable for the data." + ), + type = "error", duration = NULL + ) + } +} + +#' Run the NCA calculation pipeline with warning capture. +#' @param processed_pknca_data A processed PKNCAdata object. +#' @param settings Reactive returning the current settings list. +#' @param ratio_table Reactive returning the ratio calculations table. +#' @param custom_units Optional custom units table (or NULL). +#' @returns NCA results data frame. +#' @noRd +.run_nca_calculation <- function(processed_pknca_data, settings, ratio_table, + custom_units) { + pknca_warn_env <- new.env() + pknca_warn_env$warnings <- c() + + # Apply custom units if available + if (!is.null(custom_units)) { + by_cols <- intersect(names(processed_pknca_data$units), names(custom_units)) + by_cols <- setdiff(by_cols, c("PPSTRESU", "conversion_factor")) + processed_pknca_data$units <- rows_update( + processed_pknca_data$units, + custom_units, + by = by_cols, + unmatched = "ignore" + ) + } + + res <- withCallingHandlers({ + processed_pknca_data %>% + check_valid_pknca_data() %>% + PKNCA_calculate_nca( + blq_rule = settings()$data_imputation$blq_imputation_rule + ) %>% + add_f_to_pknca_results(settings()$bioavailability) %>% + mutate( + PPTESTCD = translate_terms(PPTESTCD, "PKNCA", "PPTESTCD") + ) %>% + PKNCA_hl_rules_exclusion( + rules = isolate(settings()$flags) %>% + purrr::keep(\(x) x$is.checked) %>% + purrr::map(\(x) x$threshold) + ) %>% + calculate_table_ratios(ratio_table = ratio_table()) %>% + remove_pp_not_requested() + }, + warning = function(w) { + parsed <- .parse_pknca_warning(w) + if (!is.null(parsed)) { + log_warn("Warning during NCA calculation: ", conditionMessage(w)) + pknca_warn_env$warnings <- append(pknca_warn_env$warnings, parsed) + } + invokeRestart("muffleWarning") + }) + + # Display unique warnings thrown by PKNCA run. + purrr::walk(unique(pknca_warn_env$warnings), function(w) { + w_message <- paste0("PKNCA run produced a warning: ", w) + log_warn(w_message) + showNotification(w_message, type = "warning", duration = 5) + }) + + res +} + +#' Clean up after NCA run (dismiss modals, reset auto-replay state). +#' @param is_auto_replay Logical, whether this is an auto-replay run. +#' @param session Shiny session object. +#' @noRd +.finalize_nca_run <- function(is_auto_replay, session) { + if (is_auto_replay) { + session$userData$auto_replay_active <- FALSE + shiny::removeModal() + log_success("Auto-replay: session restored with NCA results.") + } else { + later::later(~shiny::removeModal(session = session), delay = 0.5) + } +} + #' #' Parses error from results calculation pipeline into a html-formatted message ready #' to be displayed in the interface. From 840102eba43c6b1d7b6fe94af1b1f87714324774 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerardo=20J=2E=20Rodr=C3=ADguez?= <68994823+Gero1999@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:01:05 +0000 Subject: [PATCH 15/20] revert: remove auto-replay system, keep only units-table changes The auto-replay/session-restore feature (auto-NCA-run, tab navigation, filter import) was unrelated to #1190 and introduced a race condition that caused the 'Restoring session...' modal to hang indefinitely. Reverts tab_data.R, app.R, and tab_nca.R to main. Only the two units-related changes in tab_nca.R are preserved: - Auto-populate units_table from pknca_data on data load - Remove select(-any_of('default')) guard (default column removed) Co-authored-by: Ona --- inst/shiny/app.R | 29 +-- inst/shiny/modules/tab_data.R | 381 ++++++++-------------------------- inst/shiny/modules/tab_nca.R | 250 +++++++--------------- 3 files changed, 163 insertions(+), 497 deletions(-) diff --git a/inst/shiny/app.R b/inst/shiny/app.R index 55e5c4c96..2bca93439 100644 --- a/inst/shiny/app.R +++ b/inst/shiny/app.R @@ -16,7 +16,7 @@ require(shinyjs) require(shinyjqui) require(shinyWidgets) require(stats) - +require(stringi) require(tidyr) require(tools) require(utils) @@ -210,37 +210,12 @@ server <- function(input, output, session) { tab_data_outputs$extra_group_vars ) - # Auto-replay: navigate to saved tab once data processing completes. - observeEvent(tab_data_outputs$auto_replay_ready(), { - req(tab_data_outputs$auto_replay_ready()) - target_tab <- session$userData$auto_replay_target_tab %||% "" - valid_tabs <- c("data", "exploration", "nca", "tlg") - - if (target_tab %in% valid_tabs && target_tab != "data") { - log_info("Auto-replay: navigating to saved tab '", target_tab, "'.") - js <- sprintf( - "document.querySelector(`a[data-value='%s']`).click();", - target_tab - ) - shinyjs::runjs(js) - } - - # Dismiss loading popup unless NCA auto-run will handle it - nca_ran <- isTRUE(session$userData$auto_replay_nca_ran) - if (target_tab != "nca" || !nca_ran) { - session$userData$auto_replay_active <- FALSE - shiny::removeModal() - log_success("Auto-replay: session restored.") - } - }) - # NCA ---- tab_nca_outputs <- tab_nca_server( "nca", tab_data_outputs$pknca_data, tab_data_outputs$extra_group_vars, - tab_data_outputs$settings_override, - auto_replay_ready = tab_data_outputs$auto_replay_ready + tab_data_outputs$settings_override ) # TLG diff --git a/inst/shiny/modules/tab_data.R b/inst/shiny/modules/tab_data.R index 311068e43..614c93b2e 100644 --- a/inst/shiny/modules/tab_data.R +++ b/inst/shiny/modules/tab_data.R @@ -1,258 +1,20 @@ -#' Abort auto-replay, dismiss loading popup, and show a warning. -#' @param auto_replay ReactiveVal to reset. -#' @param message Warning message to display. -#' @param log_msg Message for the log. -#' @keywords internal -#' @noRd -.abort_auto_replay <- function(auto_replay, message, log_msg, session) { - auto_replay(FALSE) - session$userData$auto_replay_active <- FALSE - shiny::removeModal() - log_warn(log_msg) - showNotification(message, type = "warning", duration = 10) -} - -#' Set up step navigation observers (next/prev/restart buttons). -#' @param input Shiny input. -#' @param session Shiny session. -#' @param data_step ReactiveVal for current step. -#' @param trigger_mapping_submit ReactiveVal to trigger mapping. -#' @param steps Character vector of step IDs. -#' @param step_labels Character vector of step display labels. -#' @keywords internal -#' @noRd -.setup_step_navigation <- function(input, session, data_step, - trigger_mapping_submit, - steps, step_labels) { - observe({ - current <- data_step() - if (current == steps[1]) { - shinyjs::disable("prev_step") - } else { - shinyjs::enable("prev_step") - } - }) - - observeEvent(input$restart, { - log_info("Application restarting...") - session$reload() - }) - - observeEvent(input$next_step, { - shinyjs::disable("next_step") - current_step <- isolate(data_step()) - if (current_step %in% c("upload", "filtering")) { - idx <- match(current_step, steps) - data_step(steps[idx + 1]) - updateTabsetPanel( - session, "data_navset", selected = step_labels[idx + 1] - ) - } else if (current_step == "mapping") { - trigger_mapping_submit(trigger_mapping_submit() + 1) - } else if (current_step == "preview") { - shinyjs::runjs( - "document.querySelector(`a[data-value='exploration']`).click();" - ) - } - }) - - observe({ - data_step() - shinyjs::enable("next_step") - }) - - observeEvent(input$prev_step, { - current <- data_step() - idx <- match(current, steps) - if (!is.na(idx) && idx > 1) { - data_step(steps[idx - 1]) - } - updateTabsetPanel( - session, "data_navset", selected = step_labels[idx - 1] - ) - }) -} - - -#' Start auto-replay: store target tab, show loading popup, and -#' trigger mapping submission after a delay. Aborts if any column -#' mappings were skipped. -#' @param override List from settings_override (must contain `tab`, -#' `nca_ran`, `filters`). -#' @param auto_replay ReactiveVal tracking auto-replay state. -#' @param data_step ReactiveVal for current step. -#' @param trigger_mapping_submit ReactiveVal to trigger mapping. -#' @param session Shiny session. -#' @keywords internal -#' @noRd -.start_auto_replay <- function(override, auto_replay, data_step, - trigger_mapping_submit, session) { - auto_replay(TRUE) - # auto_replay_active is set by the priority-10 observer in - # tab_data_server, which fires before this function runs. - session$userData$auto_replay_target_tab <- override$tab %||% "" - session$userData$auto_replay_nca_ran <- isTRUE(override$nca_ran) - has_filters <- !is.null(override$filters) && length(override$filters) > 0 - session$userData$auto_replay_filter_pending <- has_filters - log_info("Auto-replay: settings detected, will auto-advance.") - loading_popup("Restoring session...") - - shinyjs::delay(500, { - skipped <- session$userData$mapping_skipped %||% character(0) - if (length(skipped) > 0) { - .abort_auto_replay( - auto_replay, - paste( - "Session restore stopped: some column mappings could", - "not be applied. Please review and continue manually." - ), - "Auto-replay aborted: partial mapping failure.", - session - ) - data_step("mapping") - updateTabsetPanel(session, "data_navset", selected = "Mapping") - } else { - trigger_mapping_submit(trigger_mapping_submit() + 1) - } - }) -} - -#' Set up auto-replay observers for restoring a session from settings. -#' Wires up the pipeline: settings upload -> mapping -> filtering -> -#' preview -> signal ready. Includes a 15s safety timeout. -#' @param uploaded_data List of reactives from data_upload_server. -#' @param auto_replay ReactiveVal tracking auto-replay state. -#' @param data_step ReactiveVal for current step. -#' @param trigger_mapping_submit ReactiveVal to trigger mapping. -#' @param processed_data Reactive for filtered data. -#' @param pknca_data Reactive for PKNCA data object. -#' @param session Shiny session. -#' @returns ReactiveVal that signals when auto-replay is complete. -#' @keywords internal -#' @noRd -.setup_auto_replay <- function(uploaded_data, auto_replay, data_step, - trigger_mapping_submit, - processed_data, pknca_data, session) { - # Step 1: Detect settings upload and trigger mapping submission. - observeEvent(uploaded_data$settings_override(), { - override <- uploaded_data$settings_override() - if (is.null(override) || is.null(override$mapping)) return() - .start_auto_replay( - override, auto_replay, data_step, trigger_mapping_submit, session - ) - }) - - # Safety timeout: abort if pipeline doesn't complete within 15s. - # The delay callback checks auto_replay() itself, so no early return - # needed — the timer is harmless if auto-replay finishes first. - observeEvent(trigger_mapping_submit(), { - shinyjs::delay(15000, { - if (auto_replay()) { - .abort_auto_replay( - auto_replay, - paste( - "Session restore stopped: the data pipeline did not", - "complete. Please review and continue manually." - ), - "Auto-replay aborted: pipeline did not complete in time.", - session - ) - } - }) - }, ignoreInit = TRUE) - - # Step 2 (safety net) is handled by the adnca_mapped observer in - # tab_data_server, which also handles the normal advance to filtering. - - # Step 3: After filtering completes, advance to preview. - # When filters are imported, processed_data (filtered_data in - # data_filtering.R) fires exactly twice: - # 1. Triggered by raw_adnca_data() change via bindEvent — unfiltered. - # 2. Triggered by auto-submit clicking submit_filters — filtered. - # The pending flag skips fire 1 so we only advance on fire 2. - # This invariant holds because filtered_data is bound to - # (input$submit_filters, raw_adnca_data()) and auto-replay only - # clicks submit once. If that bindEvent changes, this logic must - # be revisited. - observeEvent(processed_data(), { - if (!auto_replay()) return() - if (isTRUE(session$userData$auto_replay_filter_pending)) { - log_trace("Auto-replay: skipping first processed_data fire (pre-filter).") - session$userData$auto_replay_filter_pending <- FALSE - return() - } - data_step("preview") - updateTabsetPanel(session, "data_navset", selected = "Preview") - }) - - # Step 4: Signal that data processing is complete. - auto_replay_ready <- reactiveVal(FALSE) - observeEvent(pknca_data(), { - if (!auto_replay()) return() - if (is.null(pknca_data())) { - auto_replay(FALSE) - session$userData$auto_replay_active <- FALSE - shiny::removeModal() - return() - } - auto_replay(FALSE) - # Keep auto_replay_active TRUE until NCA auto-run completes (cleared - # in tab_nca_server) so intermediate notifications stay suppressed. - auto_replay_ready(TRUE) - }) - - auto_replay_ready -} - -# TODO: Remove once PKNCA simplifies volume units in PPORRESU by default. -#' Simplify volume-type units in a PKNCA units table. -#' @param units A data.frame with PPTESTCD, PPORRESU, PPSTRESU, -#' conversion_factor columns. -#' @returns The units data.frame with simplified volume units. -#' @keywords internal -#' @noRd -.simplify_volume_units <- function(units) { - vol_params <- metadata_nca_parameters$PKNCA[ - metadata_nca_parameters$unit_type == "volume" - ] - is_vol <- units$PPTESTCD %in% vol_params - - new_ppstresu <- ifelse( - is_vol, - sapply(units$PPSTRESU, function(x) { - simplify_unit(x, as_character = TRUE) - }), - units$PPSTRESU - ) - # Only accept changes producing simple units - new_ppstresu <- ifelse( - nchar(new_ppstresu) < 3, new_ppstresu, units$PPSTRESU - ) - - units$PPSTRESU <- new_ppstresu - units$conversion_factor <- ifelse( - is_vol, - get_conversion_factor(units$PPORRESU, units$PPSTRESU), - units$conversion_factor - ) - units -} - -#' Data tab module (upload, mapping, filtering, preview). +#' Module handling pre-processing of data. #' -#' Handles the data pre-processing pipeline: upload raw ADNCA data, -#' map columns, apply filters, and create a PKNCAdata object. When -#' settings are uploaded alongside data, auto-replay restores the -#' previous session state. +#' @details +#' Handles user upload or dummy data, filtering, mapping and reviewing. The general pipeline for +#' the module is: +#' 1. Upload raw adnca data (or return a dummy dataset) +#' 2. Filter the raw data based on user input +#' 3. Process the data based on column mapping specifications +#' The module also allows the user to review the data after performing filtering and mapping - +#' the processed data will go further into the analysis pipeline. #' -#' @param id Module ID. -#' -#' @returns List containing: -#' - `pknca_data`: reactive PKNCAdata object for analysis. -#' - `adnca_raw`: reactive raw uploaded ADNCA data. -#' - `extra_group_vars`: reactive grouping variables from column mapping. -#' - `settings_override`: reactive uploaded settings (or NULL). -#' - `auto_replay_ready`: ReactiveVal signalling auto-replay completion. +#' @returns list containing: +#' - pknca_data: reactive PKNCAdata object for analysis +#' - adnca_raw: reactive raw uploaded ADNCA data (or dummy data) +#' - extra_group_vars: reactive grouping variables from column mapping +#' - settings_override: reactive uploaded settings (or NULL) + tab_data_ui <- function(id) { ns <- NS(id) @@ -319,25 +81,49 @@ tab_data_server <- function(id) { steps <- c("upload", "mapping", "filtering", "preview") step_labels <- c("Upload", "Mapping", "Filtering", "Preview") data_step <- reactiveVal("upload") + observe({ + current <- data_step() + if (current == steps[1]) { + shinyjs::disable("prev_step") + } else { + shinyjs::enable("prev_step") + } + }) + observeEvent(input$restart, { + log_info("Application restarting...") + session$reload() + }) + observeEvent(input$next_step, { + shinyjs::disable("next_step") # Disable button on click + current_step <- isolate(data_step()) + if (current_step %in% c("upload", "filtering")) { + idx <- match(current_step, steps) + data_step(steps[idx + 1]) + updateTabsetPanel(session, "data_navset", selected = step_labels[idx + 1]) + } else if (current_step == "mapping") { + trigger_mapping_submit(trigger_mapping_submit() + 1) + } else if (current_step == "preview") { + shinyjs::runjs("document.querySelector(`a[data-value='exploration']`).click();") + } + }) - auto_replay <- reactiveVal(FALSE) + # enable next step after progression + observe({ + data_step() + shinyjs::enable("next_step") + }) - .setup_step_navigation( - input, session, data_step, trigger_mapping_submit, steps, step_labels - ) + observeEvent(input$prev_step, { + current <- data_step() + idx <- match(current, steps) + if (!is.na(idx) && idx > 1) { + data_step(steps[idx - 1]) + } + updateTabsetPanel(session, "data_navset", selected = step_labels[idx - 1]) + }) #' Load raw ADNCA data uploaded_data <- data_upload_server("raw_data") - # Set auto_replay_active early so child modules (e.g. data_filtering) - # can check it during the same flush cycle. Uses priority = 10 to - # run before default-priority observers in child modules. - observeEvent(uploaded_data$settings_override(), { - override <- uploaded_data$settings_override() - if (!is.null(override) && !is.null(override$mapping)) { - session$userData$auto_replay_active <- TRUE - } - }, priority = 10) - # Call the column mapping module imported_mapping <- reactive({ override <- uploaded_data$settings_override() @@ -355,28 +141,8 @@ tab_data_server <- function(id) { #' Reactive value for the processed dataset adnca_mapped <- column_mapping$processed_data - # Advance to filtering after mapping completes. - # Also acts as auto-replay step 2 safety net: if mappings were - # skipped, abort auto-replay and stay on mapping. observeEvent(adnca_mapped(), { req(adnca_mapped()) - if (auto_replay()) { - skipped <- session$userData$mapping_skipped %||% character(0) - if (length(skipped) > 0) { - .abort_auto_replay( - auto_replay, - paste( - "Session restore stopped: some column mappings could", - "not be applied. Please review and continue manually." - ), - "Auto-replay aborted: partial mapping failure.", - session - ) - data_step("mapping") - updateTabsetPanel(session, "data_navset", selected = "Mapping") - return() - } - } data_step("filtering") updateTabsetPanel(session, "data_navset", selected = "Filtering") }) @@ -414,24 +180,49 @@ tab_data_server <- function(id) { ) # Use raw data + mapping to create a PKNCA object + #' Initializes PKNCA::PKNCAdata object from raw adnca data pknca_data <- reactive({ req(processed_data()) log_trace("Creating PKNCA::data object.") tryCatch({ + #' Create data object pknca_object <- PKNCA_create_data_object( adnca_data = uploaded_data$adnca_raw(), mapping = column_mapping$mapping(), applied_filters = filtering_result$applied_filters(), time_duplicate_rows = column_mapping$time_duplicate_rows() ) - pknca_object$units <- .simplify_volume_units(pknca_object$units) + ############################################################################################ + # TODO: Until PKNCA manages to simplify by default in PPORRESU its volume units, + # this is implemented here via hardcoding in PPSTRESU + pknca_object$units <- pknca_object$units %>% + mutate( + PPSTRESU = { + new_ppstresu <- ifelse( + PPTESTCD %in% metadata_nca_parameters$PKNCA[ + metadata_nca_parameters$unit_type == "volume" + ], + sapply(PPSTRESU, function(x) simplify_unit(x, as_character = TRUE)), + PPSTRESU + ) + # Only accept changes producing simple units + ifelse(nchar(new_ppstresu) < 3, new_ppstresu, .[["PPSTRESU"]]) + }, + conversion_factor = ifelse( + PPTESTCD %in% metadata_nca_parameters$PKNCA[ + metadata_nca_parameters$unit_type == "volume" + ], + get_conversion_factor(PPORRESU, PPSTRESU), + conversion_factor + ) + ) + ############################################################################################ log_success("PKNCA data object created.") + #' Enable related tabs and update the curent view if data is created succesfully. purrr::walk(c("nca", "exploration", "tlg"), function(tab) { - shinyjs::enable( - selector = paste0("#page li a[data-value=", tab, "]") - ) + shinyjs::enable(selector = paste0("#page li a[data-value=", tab, "]")) }) pknca_object @@ -443,17 +234,11 @@ tab_data_server <- function(id) { }) %>% bindEvent(processed_data()) - auto_replay_ready <- .setup_auto_replay( - uploaded_data, auto_replay, data_step, trigger_mapping_submit, - processed_data, pknca_data, session - ) - list( pknca_data = pknca_data, adnca_raw = uploaded_data$adnca_raw, extra_group_vars = extra_group_vars, - settings_override = uploaded_data$settings_override, - auto_replay_ready = auto_replay_ready + settings_override = uploaded_data$settings_override ) }) } diff --git a/inst/shiny/modules/tab_nca.R b/inst/shiny/modules/tab_nca.R index 974495737..3a8720814 100644 --- a/inst/shiny/modules/tab_nca.R +++ b/inst/shiny/modules/tab_nca.R @@ -12,14 +12,12 @@ #' and display, including modules like `nca_results.R`, `parameter_datasets.R`, #' `descriptive_statistics.R` and `additional_analysis.R` #' -#' @param id Module ID. -#' @param pknca_data Reactive PKNCAdata object from the data tab. -#' @param extra_group_vars Reactive with additional grouping variable names. -#' @param settings_override Reactive with uploaded settings list (or NULL). -#' @param auto_replay_ready ReactiveVal that signals when auto-replay -#' data processing is complete and NCA can be auto-triggered. #' -#' @returns List with `res_nca` and `processed_pknca_data` reactives. +#' @param id ID of the module. +#' @param adnca_data Raw ADNCA data uploaded by the user, with any mapping and filters applied. +#' @param extra_group_vars Column name(s) of the additional variable options for input widgets +#' +#' @returns `res_nca` reactive with results data object. tab_nca_ui <- function(id) { ns <- NS(id) @@ -77,8 +75,7 @@ tab_nca_ui <- function(id) { ) } -tab_nca_server <- function(id, pknca_data, extra_group_vars, settings_override, - auto_replay_ready) { +tab_nca_server <- function(id, pknca_data, extra_group_vars, settings_override) { moduleServer(id, function(input, output, session) { ns <- session$ns @@ -101,6 +98,7 @@ tab_nca_server <- function(id, pknca_data, extra_group_vars, settings_override, session$userData$units_table(current_pknca_data$units) }, ignoreNULL = FALSE) + adnca_data <- reactive(pknca_data()$conc$data) # #' NCA Setup module @@ -127,78 +125,97 @@ tab_nca_server <- function(id, pknca_data, extra_group_vars, settings_override, reactive(slope_rules()), columns = NULL) - # Auto-replay: trigger NCA run once settings are applied and data is ready. - # Debounces processed_pknca_data to wait for the full settings cascade - # (analyte → pcspec → profile → parameters → data object) to settle. - auto_nca_pending <- reactiveVal(FALSE) - auto_nca_running <- reactiveVal(FALSE) - pknca_data_debounced <- processed_pknca_data %>% debounce(1000) - - observeEvent(auto_replay_ready(), { - req(auto_replay_ready()) - target <- session$userData$auto_replay_target_tab %||% "" - nca_ran <- isTRUE(session$userData$auto_replay_nca_ran) - if (target == "nca" && nca_ran) { - auto_nca_pending(TRUE) - # Safety: if NCA auto-run doesn't trigger within 10s, dismiss popup - shinyjs::delay(10000, { - if (auto_nca_pending()) { - auto_nca_pending(FALSE) - session$userData$auto_replay_active <- FALSE - shiny::removeModal() - log_warn("Auto-replay: NCA auto-run timed out.") - showNotification( - "Session restored but NCA could not be auto-run. Please run NCA manually.", - type = "warning", duration = 10 - ) - } - }) - } - }) - - # Trigger NCA once the debounced data object has settled. - observeEvent(pknca_data_debounced(), { - if (!auto_nca_pending()) return() - auto_nca_pending(FALSE) - log_info("Auto-replay: triggering NCA calculation.") - auto_nca_running(TRUE) - shinyjs::click("run_nca") - }) - #' Triggers NCA analysis, creating res_nca reactive res_nca <- reactive({ req(processed_pknca_data()) - if (.has_no_valid_parameters(processed_pknca_data())) { + if (all(!unlist(processed_pknca_data()$intervals[sapply(processed_pknca_data()$intervals, + is.logical)]))) { log_error("Invalid parameters") - .notify_invalid_parameters(auto_nca_running(), session) - auto_nca_running(FALSE) + showNotification("No suitable parameters selected for NCA calculation. + Please go back and select parameters suitable for the data.", + type = "error", duration = NULL) return(NULL) } - if (!auto_nca_running()) { - loading_popup("Calculating NCA results...") - } + loading_popup("Calculating NCA results...") log_info("Calculating NCA results...") tryCatch({ - res <- .run_nca_calculation( - processed_pknca_data(), settings, ratio_table, - session$userData$units_table() - ) + # Create env for storing PKNCA run warnings, so that warning messages can be appended + # from within warning handler without bleeding to global env. + pknca_warn_env <- new.env() + pknca_warn_env$warnings <- c() + + # Update units table + processed_pknca_data <- processed_pknca_data() + if (!is.null(session$userData$units_table())) { + custom_units <- session$userData$units_table() + by_cols <- intersect(names(processed_pknca_data$units), names(custom_units)) + by_cols <- setdiff(by_cols, c("PPSTRESU", "conversion_factor")) + processed_pknca_data$units <- rows_update( + processed_pknca_data$units, + custom_units, + by = by_cols, + unmatched = "ignore" + ) + } + + #' Calculate results + res <- withCallingHandlers({ + processed_pknca_data %>% + # Check if there are exclusions that contains a filled reason + check_valid_pknca_data() %>% + # Perform PKNCA parameter calculations + PKNCA_calculate_nca( + blq_rule = settings()$data_imputation$blq_imputation_rule + ) %>% + # Add bioavailability results if requested + add_f_to_pknca_results(settings()$bioavailability) %>% + # Apply standard CDISC names + mutate( + PPTESTCD = translate_terms(PPTESTCD, "PKNCA", "PPTESTCD") + ) %>% + # Apply flag rules to mark results in the `exclude` column + PKNCA_hl_rules_exclusion( + rules = isolate(settings()$flags) %>% + purrr::keep(\(x) x$is.checked) %>% + purrr::map(\(x) x$threshold) + ) %>% + # Add parameter ratio calculations + calculate_table_ratios(ratio_table = ratio_table()) %>% + # Keep only parameters requested by the user + remove_pp_not_requested() + }, + warning = function(w) { + parsed <- .parse_pknca_warning(w) + if (!is.null(parsed)) { + log_warn("Warning during NCA calculation: {conditionMessage(w)}") + pknca_warn_env$warnings <- append(pknca_warn_env$warnings, parsed) + } + invokeRestart("muffleWarning") + }) + + # Display unique warnings thrown by PKNCA run. + purrr::walk(unique(pknca_warn_env$warnings), function(w) { + w_message <- paste0("PKNCA run produced a warning: ", w) + log_warn(w_message) + showNotification(w_message, type = "warning", duration = 5) + }) updateTabsetPanel(session, "nca_navset", selected = "Results") + log_success("NCA results calculated.") res }, error = function(e) { - log_error("Error calculating NCA results:\n", conditionMessage(e)) + log_error("Error calculating NCA results:\n{conditionMessage(e)}") showNotification(.parse_pknca_error(e), type = "error", duration = NULL) NULL }, finally = { - .finalize_nca_run(auto_nca_running(), session) - auto_nca_running(FALSE) + # Delay the removal of loading modal to give it enough time to render + later::later(~shiny::removeModal(session = session), delay = 0.5) }) }) %>% bindEvent(input$run_nca) @@ -212,7 +229,6 @@ tab_nca_server <- function(id, pknca_data, extra_group_vars, settings_override, observe({ req(res_nca()) session$userData$final_units <- res_nca()$data$units - session$userData$nca_ran <- TRUE }) #' Show slopes results @@ -259,116 +275,6 @@ tab_nca_server <- function(id, pknca_data, extra_group_vars, settings_override, }) } -#' Check whether the PKNCAdata intervals contain any selected (TRUE) parameters. -#' @param pknca_data A PKNCAdata object. -#' @returns TRUE if no valid parameters are selected. -#' @noRd -.has_no_valid_parameters <- function(pknca_data) { - all(!unlist(pknca_data$intervals[sapply(pknca_data$intervals, is.logical)])) -} - -#' Show the appropriate notification when NCA has no valid parameters. -#' @param is_auto_replay Logical, whether this is an auto-replay run. -#' @param session Shiny session object. -#' @noRd -.notify_invalid_parameters <- function(is_auto_replay, session) { - if (is_auto_replay) { - session$userData$auto_replay_active <- FALSE - shiny::removeModal() - showNotification( - paste( - "Session restored but NCA could not be auto-run:", - "no suitable parameters for this dataset.", - "Please adjust settings and run NCA manually." - ), - type = "warning", duration = 10 - ) - } else { - showNotification( - paste( - "No suitable parameters selected for NCA calculation.", - "Please go back and select parameters suitable for the data." - ), - type = "error", duration = NULL - ) - } -} - -#' Run the NCA calculation pipeline with warning capture. -#' @param processed_pknca_data A processed PKNCAdata object. -#' @param settings Reactive returning the current settings list. -#' @param ratio_table Reactive returning the ratio calculations table. -#' @param custom_units Optional custom units table (or NULL). -#' @returns NCA results data frame. -#' @noRd -.run_nca_calculation <- function(processed_pknca_data, settings, ratio_table, - custom_units) { - pknca_warn_env <- new.env() - pknca_warn_env$warnings <- c() - - # Apply custom units if available - if (!is.null(custom_units)) { - by_cols <- intersect(names(processed_pknca_data$units), names(custom_units)) - by_cols <- setdiff(by_cols, c("PPSTRESU", "conversion_factor")) - processed_pknca_data$units <- rows_update( - processed_pknca_data$units, - custom_units, - by = by_cols, - unmatched = "ignore" - ) - } - - res <- withCallingHandlers({ - processed_pknca_data %>% - check_valid_pknca_data() %>% - PKNCA_calculate_nca( - blq_rule = settings()$data_imputation$blq_imputation_rule - ) %>% - add_f_to_pknca_results(settings()$bioavailability) %>% - mutate( - PPTESTCD = translate_terms(PPTESTCD, "PKNCA", "PPTESTCD") - ) %>% - PKNCA_hl_rules_exclusion( - rules = isolate(settings()$flags) %>% - purrr::keep(\(x) x$is.checked) %>% - purrr::map(\(x) x$threshold) - ) %>% - calculate_table_ratios(ratio_table = ratio_table()) %>% - remove_pp_not_requested() - }, - warning = function(w) { - parsed <- .parse_pknca_warning(w) - if (!is.null(parsed)) { - log_warn("Warning during NCA calculation: ", conditionMessage(w)) - pknca_warn_env$warnings <- append(pknca_warn_env$warnings, parsed) - } - invokeRestart("muffleWarning") - }) - - # Display unique warnings thrown by PKNCA run. - purrr::walk(unique(pknca_warn_env$warnings), function(w) { - w_message <- paste0("PKNCA run produced a warning: ", w) - log_warn(w_message) - showNotification(w_message, type = "warning", duration = 5) - }) - - res -} - -#' Clean up after NCA run (dismiss modals, reset auto-replay state). -#' @param is_auto_replay Logical, whether this is an auto-replay run. -#' @param session Shiny session object. -#' @noRd -.finalize_nca_run <- function(is_auto_replay, session) { - if (is_auto_replay) { - session$userData$auto_replay_active <- FALSE - shiny::removeModal() - log_success("Auto-replay: session restored with NCA results.") - } else { - later::later(~shiny::removeModal(session = session), delay = 0.5) - } -} - #' #' Parses error from results calculation pipeline into a html-formatted message ready #' to be displayed in the interface. @@ -420,7 +326,7 @@ tab_nca_server <- function(id, pknca_data, extra_group_vars, settings_override, # so this warning may need to be rephrased or removed. if (grepl("Units are provided for some but not all parameters; missing for: ae", msg)) { msg <- paste0( - "Urine Parameters (FREXINT, RCAMINT) calculated for non-urine samples ", + "Urine Parameters (FREXINT, RCAMINT) calculated for non-urine samples", "will return NA values and units" ) } From 5b501d3224f69c12defc788ed8f0f44c26d8223f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerardo=20J=2E=20Rodr=C3=ADguez?= <68994823+Gero1999@users.noreply.github.com> Date: Thu, 30 Apr 2026 14:42:16 +0000 Subject: [PATCH 16/20] fix: isolate settings_override read in units auto-populate observer Prevents the reactive dependency on settings_override from shifting the Shiny flush cycle timing during auto-replay, which caused the debounced NCA trigger to miss its window and the 'Restoring session' modal to hang. Co-authored-by: Ona --- inst/shiny/modules/tab_nca.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inst/shiny/modules/tab_nca.R b/inst/shiny/modules/tab_nca.R index e992f06c8..c4cb9f33f 100644 --- a/inst/shiny/modules/tab_nca.R +++ b/inst/shiny/modules/tab_nca.R @@ -107,7 +107,7 @@ tab_nca_server <- function(id, pknca_data, extra_group_vars, settings_override, return() } - imported <- settings_override() + imported <- isolate(settings_override()) has_imported_units <- !is.null(imported$units) && nrow(imported$units) > 0 if (has_imported_units) return() From 6b0dcfdbfc538b49dd58093188a61cbd8aa649f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerardo=20J=2E=20Rodr=C3=ADguez?= <68994823+Gero1999@users.noreply.github.com> Date: Fri, 8 May 2026 09:33:18 +0000 Subject: [PATCH 17/20] refactor: extract .sync_units_table to reduce tab_nca_server complexity Moves the units auto-populate logic into a helper function outside tab_nca_server to keep cyclomatic complexity under the lintr threshold. Co-authored-by: Ona --- inst/shiny/modules/tab_nca.R | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/inst/shiny/modules/tab_nca.R b/inst/shiny/modules/tab_nca.R index c4cb9f33f..5307eee2f 100644 --- a/inst/shiny/modules/tab_nca.R +++ b/inst/shiny/modules/tab_nca.R @@ -81,6 +81,20 @@ tab_nca_ui <- function(id) { ) } +# Populate units_table from pknca_data, skipping during settings import. +.sync_units_table <- function(pknca_data, settings_override, units_table) { + if (is.null(pknca_data) || is.null(pknca_data$units)) { + units_table(NULL) + return() + } + + imported <- isolate(settings_override()) + has_imported_units <- !is.null(imported$units) && nrow(imported$units) > 0 + if (has_imported_units) return() + + units_table(pknca_data$units) +} + # .apply_param_exclusions is defined in inst/shiny/functions/utils-exclusions.R tab_nca_server <- function(id, pknca_data, extra_group_vars, settings_override, @@ -93,25 +107,10 @@ tab_nca_server <- function(id, pknca_data, extra_group_vars, settings_override, #' should respect the units, regardless of location. session$userData$units_table <- reactiveVal(NULL) - # Keep units_table synchronized with the current dataset. Store the full - # units table so downstream code never receives a partial units table, and - # clear it when no units are available to avoid stale values persisting - # across uploads. - # Skip when settings are being imported with units — the import observer - # in nca_setup.R will merge imported overrides into the data-derived table. + # Keep units_table synchronized with the current dataset. + # Skips during settings import — nca_setup.R handles the merge instead. observeEvent(pknca_data(), { - current_pknca_data <- pknca_data() - - if (is.null(current_pknca_data) || is.null(current_pknca_data$units)) { - session$userData$units_table(NULL) - return() - } - - imported <- isolate(settings_override()) - has_imported_units <- !is.null(imported$units) && nrow(imported$units) > 0 - if (has_imported_units) return() - - session$userData$units_table(current_pknca_data$units) + .sync_units_table(pknca_data(), settings_override, session$userData$units_table) }, ignoreNULL = FALSE) adnca_data <- reactive(pknca_data()$conc$data) From e6ebd6dab506fd2d3850117f8bd78f97aef50bab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerardo=20J=2E=20Rodr=C3=ADguez?= <68994823+Gero1999@users.noreply.github.com> Date: Fri, 8 May 2026 09:49:32 +0000 Subject: [PATCH 18/20] fix: simplify .sync_units_table to avoid auto-replay interference Use auto_replay_active flag instead of reading settings_override. Remove ignoreNULL=FALSE to avoid firing on app startup. This eliminates the extra flush cycle that shifted auto-replay timing and caused the 'Restoring session' modal to hang. Co-authored-by: Ona --- inst/shiny/modules/tab_nca.R | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/inst/shiny/modules/tab_nca.R b/inst/shiny/modules/tab_nca.R index 5307eee2f..644d8b448 100644 --- a/inst/shiny/modules/tab_nca.R +++ b/inst/shiny/modules/tab_nca.R @@ -81,17 +81,9 @@ tab_nca_ui <- function(id) { ) } -# Populate units_table from pknca_data, skipping during settings import. -.sync_units_table <- function(pknca_data, settings_override, units_table) { - if (is.null(pknca_data) || is.null(pknca_data$units)) { - units_table(NULL) - return() - } - - imported <- isolate(settings_override()) - has_imported_units <- !is.null(imported$units) && nrow(imported$units) > 0 - if (has_imported_units) return() - +# Populate units_table from pknca_data, skipping during auto-replay. +.sync_units_table <- function(pknca_data, units_table, session) { + if (isTRUE(session$userData$auto_replay_active)) return() units_table(pknca_data$units) } @@ -108,10 +100,10 @@ tab_nca_server <- function(id, pknca_data, extra_group_vars, settings_override, session$userData$units_table <- reactiveVal(NULL) # Keep units_table synchronized with the current dataset. - # Skips during settings import — nca_setup.R handles the merge instead. + # Skips during auto-replay — nca_setup.R handles the merge instead. observeEvent(pknca_data(), { - .sync_units_table(pknca_data(), settings_override, session$userData$units_table) - }, ignoreNULL = FALSE) + .sync_units_table(pknca_data(), session$userData$units_table, session) + }) adnca_data <- reactive(pknca_data()$conc$data) From a57dcc43038d22d0bec3563102429cfccb092d7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerardo=20J=2E=20Rodr=C3=ADguez?= <68994823+Gero1999@users.noreply.github.com> Date: Fri, 8 May 2026 11:44:39 +0000 Subject: [PATCH 19/20] fix: populate units_table lazily inside res_nca instead of observer Populate units_table on first NCA run when it is still NULL, instead of using an observeEvent on pknca_data() which interfered with the auto-replay flush cycle timing. Co-authored-by: Ona --- inst/shiny/modules/tab_nca.R | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/inst/shiny/modules/tab_nca.R b/inst/shiny/modules/tab_nca.R index 644d8b448..d13e23a17 100644 --- a/inst/shiny/modules/tab_nca.R +++ b/inst/shiny/modules/tab_nca.R @@ -81,12 +81,6 @@ tab_nca_ui <- function(id) { ) } -# Populate units_table from pknca_data, skipping during auto-replay. -.sync_units_table <- function(pknca_data, units_table, session) { - if (isTRUE(session$userData$auto_replay_active)) return() - units_table(pknca_data$units) -} - # .apply_param_exclusions is defined in inst/shiny/functions/utils-exclusions.R tab_nca_server <- function(id, pknca_data, extra_group_vars, settings_override, @@ -99,12 +93,6 @@ tab_nca_server <- function(id, pknca_data, extra_group_vars, settings_override, #' should respect the units, regardless of location. session$userData$units_table <- reactiveVal(NULL) - # Keep units_table synchronized with the current dataset. - # Skips during auto-replay — nca_setup.R handles the merge instead. - observeEvent(pknca_data(), { - .sync_units_table(pknca_data(), session$userData$units_table, session) - }) - adnca_data <- reactive(pknca_data()$conc$data) # #' NCA Setup module @@ -224,6 +212,11 @@ tab_nca_server <- function(id, pknca_data, extra_group_vars, settings_override, by = by_cols, unmatched = "ignore" ) + } else { + # First NCA run: populate units_table from the data so settings + # export includes volume-simplified units even if the user never + # opens the Units modal. + session$userData$units_table(processed_pknca_data$units) } #' Calculate results From e94ce347c93e01bf13b78a29e225957a146de86d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerardo=20J=2E=20Rodr=C3=ADguez?= Date: Fri, 8 May 2026 12:11:46 +0000 Subject: [PATCH 20/20] fix: decouple units_table from settings() reactive to prevent debounce cascade Remove units from the settings() reactive in settings.R. The units field was only used for export (YAML download and ZIP), not for NCA computation. Including it in settings() created a reactive dependency that caused processed_pknca_data to re-evaluate whenever units_table changed, adding a 2500ms+ debounce cycle that pushed auto-replay beyond the 10s safety timeout. Export paths in nca_setup.R and zip-utils.R now read session$userData$units_table() directly. Co-authored-by: Ona --- inst/shiny/functions/zip-utils.R | 8 +++++--- inst/shiny/modules/tab_nca/nca_setup.R | 8 +++++--- inst/shiny/modules/tab_nca/setup/settings.R | 3 +-- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/inst/shiny/functions/zip-utils.R b/inst/shiny/functions/zip-utils.R index 0a0576983..8a55e0fcb 100644 --- a/inst/shiny/functions/zip-utils.R +++ b/inst/shiny/functions/zip-utils.R @@ -523,9 +523,11 @@ prepare_export_files <- function(target_dir, .export_settings <- function(target_dir, session) { settings_list <- session$userData$settings() - # Only export units that differ from defaults (PPSTRESU != PPORRESU) - if (!is.null(settings_list$units)) { - settings_list$units <- settings_list$units %>% + # Units are stored separately from settings() to avoid triggering + # the settings debounce cascade. Read directly for export. + units_snapshot <- session$userData$units_table() + if (!is.null(units_snapshot)) { + settings_list$units <- units_snapshot %>% dplyr::filter(!is.na(PPSTRESU), !is.na(PPORRESU), PPSTRESU != PPORRESU) } diff --git a/inst/shiny/modules/tab_nca/nca_setup.R b/inst/shiny/modules/tab_nca/nca_setup.R index fa5bee905..337d19d1f 100644 --- a/inst/shiny/modules/tab_nca/nca_setup.R +++ b/inst/shiny/modules/tab_nca/nca_setup.R @@ -247,9 +247,11 @@ nca_setup_server <- function(id, data, adnca_data, extra_group_vars, settings_ov }, content = function(con) { export_settings <- final_settings() - # Only export units that differ from defaults (PPSTRESU != PPORRESU) - if (!is.null(export_settings$units)) { - export_settings$units <- export_settings$units %>% + # Units are stored separately from settings() to avoid triggering + # the settings debounce cascade. Read directly for export. + units_snapshot <- session$userData$units_table() + if (!is.null(units_snapshot)) { + export_settings$units <- units_snapshot %>% filter(!is.na(PPSTRESU), !is.na(PPORRESU), PPSTRESU != PPORRESU) } export_settings$ratio_table <- ratio_table() diff --git a/inst/shiny/modules/tab_nca/setup/settings.R b/inst/shiny/modules/tab_nca/setup/settings.R index 93bab1922..a8512496c 100644 --- a/inst/shiny/modules/tab_nca/setup/settings.R +++ b/inst/shiny/modules/tab_nca/setup/settings.R @@ -502,8 +502,7 @@ settings_server <- function(id, data, adnca_data, settings_override) { is.checked = input$LAMZSPN_rule, threshold = input$LAMZSPN_threshold ) - ), - units = session$userData$units_table() + ) ) })