Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Package: aNCA
Title: (Pre-)Clinical NCA in a Dynamic Shiny App
Version: 0.1.0.9175
Version: 0.1.0.9176
Authors@R: c(
person("Ercan", "Suekuer", email = "ercan.suekuer@roche.com", role = "aut",
comment = c(ORCID = "0009-0001-1626-1526")),
Expand Down
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
* Dose-aware AUCint parameters now share the same PPTESTCD as their non-dose-aware counterparts in CDISC exports, with `PPANMETH` indicating the analytical method. Internal PPTESTCDs renamed from misleading `D` suffix (e.g. `AUCINTD`) to lowercase `da` suffix (e.g. `AUCINTda`). Fixed wrong PPTEST label for `AUCINTD` which said "Normalized by Dose" (#1242)
* Optional settings (`slope_rules`, `int_parameters`, `ratio_table`) are now normalized to `NULL` when empty, instead of persisting as 0-row data frames throughout the app and settings pipeline (#1262)
* Interval-specific parameters (`aucint.*`, `cav.int.*`) excluded from the Parameter Selection matrix — they require finite sub-intervals and must be configured via Partial Interval Calculations (#1309)
* Slope rules table (Slope Selector) now properly clears when all rules are removed, and row removal no longer corrupts data or leaks extraneous columns (#1302). The root cause was multi-layered: (1) `validate(need(FALSE))` retained previous output instead of clearing the reactable, (2) `reactable.extras` widgets sent Shiny input events during initialization after every re-render, overwriting valid data with widget defaults, (3) `rbind`/`bind_rows` column mismatches introduced stray columns from plot-click rules, and (4) a namespaced `updateReactable` had been a silent no-op since #1262 due to a missing module namespace suffix.

### Ratio Calculations
* Fixed `Aggregate Subject = yes/if-needed` not aggregating reference values, and ratio columns not appearing in results (#1273)
Expand Down
11 changes: 7 additions & 4 deletions inst/shiny/functions/utils-slope_selector.R
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,12 @@ check_slope_rule_overlap <- function(existing, new, .keep = FALSE) {
rows_with_same_cols <- is_matching_cols(c(slope_groups, "TYPE", "RANGE"), existing, new)
if (sum(rows_with_same_cols) > 0) {
existing <- existing[!rows_with_same_cols, ]
return(if (nrow(existing) == 0) NULL else existing)
return(if (nrow(existing) == 0) existing[0, ] else existing)
}
# Otherwise, just add the new exclusion
existing <- bind_rows(existing, new)
# Drop columns from existing that are not in new to prevent column mismatch (#1302)
common_cols <- intersect(names(existing), names(new))
existing <- bind_rows(existing[, common_cols, drop = FALSE], new)
}

if (new$TYPE == "Selection") {
Expand All @@ -174,9 +176,10 @@ check_slope_rule_overlap <- function(existing, new, .keep = FALSE) {
existing <- existing[!rows_with_same_groups, ]
}
# Otherwise, just add the new selection
existing <- bind_rows(existing, new)
common_cols <- intersect(names(existing), names(new))
existing <- bind_rows(existing[, common_cols, drop = FALSE], new)
}
if (nrow(existing) == 0) NULL else existing
if (nrow(existing) == 0) existing[0, ] else existing
}

#' Update plots with PKNCA data (for affected intervals)
Expand Down
9 changes: 9 additions & 0 deletions inst/shiny/modules/tab_nca.R
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,15 @@ tab_nca_server <- function(id, pknca_data, extra_group_vars, settings_override,
reactive(slope_rules()),
columns = NULL)

# Ensure Results table clears when slope rules become NULL/empty (#1302)
observeEvent(slope_rules(), {
sr <- slope_rules()
if (is.null(sr) || (is.data.frame(sr) && nrow(sr) == 0)) {
reactable::updateReactable(ns("manual_slopes-table"),
data = data.frame(Status = "No rules defined"))
}
}, ignoreNULL = FALSE)

# 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.
Expand Down
42 changes: 35 additions & 7 deletions inst/shiny/modules/tab_nca/setup/manual_slopes_table.R
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,23 @@ manual_slopes_table_server <- function(
# create a reactive to update the reactable UI when the table changes
refresh_reactable <- reactiveVal(0)

# Flag to suppress edit events during widget (re-)initialization after table re-render.
# When TRUE, edit observer ignores incoming events to prevent reactable.extras widgets
# from writing default values (e.g. first dropdown choice, empty text) into the data.
# Uses a generation counter so rapid successive operations don't prematurely unblock.
suppress_edit_events <- reactiveVal(0) # 0 = not suppressing, >0 = suppressing

# Set suppression on each re-render; clear after delay only if no newer re-render occurred.
observeEvent(refresh_reactable(), {
gen <- suppress_edit_events(0) + 1 # increment generation (0→1, 1→2, etc.)
suppress_edit_events(gen)
shinyjs::delay(500, {
if (identical(suppress_edit_events(), gen)) {
suppress_edit_events(0)
}
})
}, ignoreInit = TRUE)

# Add a new row to the table when the user clicks the add button
observeEvent(input$add_rule, {
log_trace("{id}: adding manual slopes row")
Expand Down Expand Up @@ -110,6 +127,11 @@ manual_slopes_table_server <- function(
} else {
new_row[0, ] # empty DF with matching columns
}
# Keep only columns present in new_row to prevent rbind column mismatch
# when old rules (e.g. from plot clicks) have extra columns like ATPTREF (#1302)
common_cols <- intersect(names(old_rows), names(new_row))
old_rows <- old_rows[, common_cols, drop = FALSE]
new_row <- new_row[, common_cols, drop = FALSE]
updated_data <- as.data.frame(
rbind(old_rows, new_row),
stringsAsFactors = FALSE
Expand All @@ -124,21 +146,26 @@ manual_slopes_table_server <- function(
log_trace("{id}: removing manual slopes row")
req(manual_slopes())
selected <- getReactableState("manual_slopes", "selected")
req(selected)
edited_slopes <- manual_slopes()[-selected, ]
if (nrow(edited_slopes) == 0) edited_slopes <- NULL
if (is.null(selected) || length(selected) == 0) {
return()
}
edited_slopes <- manual_slopes()[-selected, , drop = FALSE]
if (nrow(edited_slopes) == 0) edited_slopes <- edited_slopes[0, ]
manual_slopes(edited_slopes)
reset_reactable_memory()
refresh_reactable(refresh_reactable() + 1)
})

# Render the manual slopes table (reactable)
output$manual_slopes <- renderReactable({
req(manual_slopes())
data <- manual_slopes()
if (is.null(data) || nrow(data) == 0) {
return(NULL)
}
log_trace("{id}: rendering slope edit data table")
isolate({
data <- manual_slopes()
})
# Drop stray columns (e.g. ATPTREF from plot clicks) not in canonical column set (#1302)
canonical_cols <- c(colnames(slopes_pknca_groups()), "TYPE", "RANGE", "REASON")
data <- data[, intersect(names(data), canonical_cols), drop = FALSE]
# Define columns: group columns (dynamic), then TYPE/RANGE/REASON (fixed)
fixed_columns <- list(
TYPE = colDef(
Expand Down Expand Up @@ -195,6 +222,7 @@ manual_slopes_table_server <- function(
req(manual_slopes())
purrr::walk(colnames(manual_slopes()), function(colname) {
observeEvent(input[[paste0("edit_", colname)]], {
req(suppress_edit_events() == 0)
edit <- input[[paste0("edit_", colname)]]
edited_slopes <- manual_slopes()
edited_slopes[edit$row, edit$column] <- edit$value
Expand Down
18 changes: 7 additions & 11 deletions inst/shiny/modules/tab_nca/setup/slope_selector.R
Original file line number Diff line number Diff line change
Expand Up @@ -266,18 +266,14 @@ slope_selector_server <- function( # nolint
refresh_reactable(refresh_reactable() + 1)
})

#' Separate event handling updating displayed reactable upon every change (adding and removing
#' rows, plots selection, edits). This needs to be separate call, since simply re-rendering
#' the table would mean losing focus on text inputs when entering values.
#' Observe manual_slopes changes for inline edits.
#' NOTE: We intentionally do NOT call updateReactable here because:
#' 1. renderReactable + bindEvent(refresh_reactable()) handles all full re-renders
#' 2. reactable.extras widgets (dropdown_extra, text_extra) handle inline edits directly
#' 3. updateReactable from the parent module cannot reference the inner module's
#' outputId without hardcoding internal namespace structure (#1302).
observeEvent(manual_slopes(), {
req(manual_slopes())

# Update reactable with rules
reactable::updateReactable(
outputId = "manual_slopes",
data = manual_slopes()
)

# manual_slopes changes are handled by refresh_reactable-triggered re-renders
})
#' returns half life adjustments rules to update processed_pknca_data in nca_setup.R
manual_slopes
Expand Down
2 changes: 1 addition & 1 deletion inst/shiny/tests/testthat/test-utils-slope-selector.R
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ describe("check_slope_rule_overlap", {
RANGE = "3:6"
)

expect_null(check_slope_rule_overlap(EXISTING_FIXTURE, NEW))
expect_equal(nrow(check_slope_rule_overlap(EXISTING_FIXTURE, NEW)), 0)
})

it("should warn if more than one range for single subject, profile and rule type is detected", {
Expand Down
2 changes: 1 addition & 1 deletion tests/testthat/test-utils-slope_selector.R
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ describe("check_slope_rule_overlap", {
PARAM = "A", PCSPEC = 1, RANGE = "3:6", REASON = "outlier"
)
result <- check_slope_rule_overlap(existing, new_rule)
expect_null(result)
expect_equal(nrow(result), 0)
})

it("returns remaining rows when cancelling one of multiple exclusions", {
Expand Down
Loading