diff --git a/NAMESPACE b/NAMESPACE index a42856456d..e4c3a538d9 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -10,7 +10,6 @@ S3method(srv_teal_module,default) S3method(srv_teal_module,teal_module) S3method(srv_teal_module,teal_modules) S3method(ui_teal_module,default) -S3method(ui_teal_module,shiny.tag) S3method(ui_teal_module,teal_module) S3method(ui_teal_module,teal_modules) S3method(within,teal_data_module) diff --git a/R/init.R b/R/init.R index e6cb66953b..a62034eaf8 100644 --- a/R/init.R +++ b/R/init.R @@ -238,7 +238,6 @@ init <- function(data, ui = function(request) { ui_teal( id = ns("teal"), - data = if (inherits(data, "teal_data_module")) data, modules = modules, title = title, header = header, diff --git a/R/module_filter_data.R b/R/module_filter_data.R index c9ea56ddae..959fd867ee 100644 --- a/R/module_filter_data.R +++ b/R/module_filter_data.R @@ -61,7 +61,7 @@ srv_filter_data <- function(id, datasets, active_datanames, data_rv, is_active) "lockEnvironment(.raw_data) #@linksto .raw_data" # this is environment and it is shared by qenvs. CAN'T MODIFY! ) ) - filtered_code <- teal.slice::get_filter_expr(datasets = datasets, datanames = datanames) + filtered_code <- .get_filter_expr(datasets = datasets, datanames = datanames) filtered_teal_data <- .append_evaluated_code(data, filtered_code) filtered_datasets <- sapply(datanames, function(x) datasets$get_data(x, filtered = TRUE), simplify = FALSE) filtered_teal_data <- .append_modified_data(filtered_teal_data, filtered_datasets) @@ -75,7 +75,7 @@ srv_filter_data <- function(id, datasets, active_datanames, data_rv, is_active) req(inherits(datasets(), "FilteredData")) new_signature <- c( teal.data::get_code(data_rv()), - teal.slice::get_filter_expr(datasets = datasets(), datanames = active_datanames()) + .get_filter_expr(datasets = datasets(), datanames = active_datanames()) ) if (!identical(previous_signature(), new_signature)) { previous_signature(new_signature) @@ -100,3 +100,12 @@ srv_filter_data <- function(id, datasets, active_datanames, data_rv, is_active) trigger_data } + +#' @rdname module_filter_data +.get_filter_expr <- function(datasets, datanames) { + if (length(datanames)) { + teal.slice::get_filter_expr(datasets = datasets, datanames = datanames) + } else { + NULL + } +} diff --git a/R/module_init_data.R b/R/module_init_data.R index e79909a1f9..8c09936a75 100644 --- a/R/module_init_data.R +++ b/R/module_init_data.R @@ -25,14 +25,11 @@ #' @inheritParams init #' #' @param data (`teal_data`, `teal_data_module`, or `reactive` returning `teal_data`) -#' The `ui` component of this module does not require `data` if `teal_data_module` is not provided. -#' The `data` argument in the `ui` is included solely for the `$ui` function of the -#' `teal_data_module`. Otherwise, it can be disregarded, ensuring that `ui_teal` does not depend on -#' the reactive data of the enclosing application. +#' The data which application will depend on. #' #' @return A `reactive` object that returns: -#' - `teal_data` when the object is validated -#' - `shiny.silent.error` when not validated. +#' Output of the `data`. If `data` fails then returned error is handled (after [tryCatch()]) so that +#' rest of the application can respond to this respectively. #' #' @rdname module_init_data #' @name module_init_data @@ -40,104 +37,55 @@ NULL #' @rdname module_init_data -ui_init_data <- function(id, data) { +ui_init_data <- function(id) { ns <- shiny::NS(id) shiny::div( id = ns("content"), - style = "display: inline-block;", - if (inherits(data, "teal_data_module")) { - ui_teal_data(ns("teal_data_module"), data_module = data) - } else { - NULL - } + style = "display: inline-block; width: 100%;", + uiOutput(ns("data")) ) } #' @rdname module_init_data -srv_init_data <- function(id, data, modules, filter = teal_slices()) { +srv_init_data <- function(id, data) { checkmate::assert_character(id, max.len = 1, any.missing = FALSE) - checkmate::assert_multi_class(data, c("teal_data", "teal_data_module", "reactive", "reactiveVal")) - checkmate::assert_class(modules, "teal_modules") - checkmate::assert_class(filter, "teal_slices") + checkmate::assert_multi_class(data, c("teal_data", "teal_data_module", "reactive")) moduleServer(id, function(input, output, session) { logger::log_debug("srv_data initializing.") - - if (getOption("teal.show_js_log", default = FALSE)) { - shinyjs::showLog() - } - # data_rv contains teal_data object # either passed to teal::init or returned from teal_data_module - data_validated <- if (inherits(data, "teal_data_module")) { - srv_teal_data( - "teal_data_module", - data = reactive(req(FALSE)), # to .fallback_on_failure to shiny.silent.error - data_module = data, - modules = modules, - validate_shiny_silent_error = FALSE - ) + data_out <- if (inherits(data, "teal_data_module")) { + output$data <- renderUI(data$ui(id = session$ns("teal_data_module"))) + data$server("teal_data_module") } else if (inherits(data, "teal_data")) { reactiveVal(data) } else if (test_reactive(data)) { - .fallback_on_failure(this = data, that = reactive(req(FALSE)), label = "Reactive data") - } - - if (inherits(data, "teal_data_module")) { - shinyjs::disable(selector = sprintf(".teal-body:has('#%s') .nav li a", session$ns("content"))) + data } - observeEvent(data_validated(), { - showNotification("Data loaded successfully.", duration = 5) - shinyjs::enable(selector = sprintf(".teal-body:has('#%s') .nav li a", session$ns("content"))) - if (isTRUE(attr(data, "once"))) { - # Hiding the data module tab. - shinyjs::hide( - selector = sprintf( - ".teal-body:has('#%s') a[data-value='teal_data_module']", - session$ns("content") - ) - ) - # Clicking the second tab, which is the first module. - shinyjs::runjs( - sprintf( - "document.querySelector('.teal-body:has(#%s) .nav li:nth-child(2) a').click();", - session$ns("content") - ) - ) - } + data_handled <- reactive({ + tryCatch(data_out(), error = function(e) e) + }) - is_filter_ok <- check_filter_datanames(filter, ls(teal.code::get_env(data_validated()))) - if (!isTRUE(is_filter_ok)) { - showNotification( - "Some filters were not applied because of incompatibility with data. Contact app developer.", - type = "warning", - duration = 10 + # We want to exclude teal_data_module elements from bookmarking as they might have some secrets + observeEvent(data_handled(), { + if (inherits(data_handled(), "teal_data")) { + app_session <- .subset2(shiny::getDefaultReactiveDomain(), "parent") + setBookmarkExclude( + session$ns( + grep( + pattern = "teal_data_module-", + x = names(reactiveValuesToList(input)), + value = TRUE + ) + ), + session = app_session ) - warning(is_filter_ok) } }) - observeEvent(data_validated(), once = TRUE, { - # Excluding the ids from teal_data_module using full namespace and global shiny app session. - app_session <- .subset2(shiny::getDefaultReactiveDomain(), "parent") - setBookmarkExclude( - session$ns( - grep( - pattern = "teal_data_module-", - x = names(reactiveValuesToList(input)), - value = TRUE - ) - ), - session = app_session - ) - }) - - # Adds signature protection to the datanames in the data - reactive({ - req(data_validated()) - .add_signature_to_data(data_validated()) - }) + data_handled }) } diff --git a/R/module_nested_tabs.R b/R/module_nested_tabs.R index 76ead65b33..cae7d9acca 100644 --- a/R/module_nested_tabs.R +++ b/R/module_nested_tabs.R @@ -20,6 +20,13 @@ #' When `datasets` is passed from the parent module (`srv_teal`) then `dataset` is a singleton #' which implies in filter-panel to be "global". When `NULL` then filter-panel is "module-specific". #' +#' @param data_load_status (`reactive` returning `character`) +#' Determines action dependent on a data loading status: +#' - `"ok"` when `teal_data` is returned from the data loading. +#' - `"teal_data_module failed"` when [teal_data_module()] didn't return `teal_data`. Disables tabs buttons. +#' - `"external failed"` when a `reactive` passed to `srv_teal(data)` didn't return `teal_data`. Hides the whole tab +#' panel. +#' #' @return #' output of currently active module. #' - `srv_teal_module.teal_module` returns `reactiveVal` containing output of the called module. @@ -45,54 +52,64 @@ ui_teal_module.default <- function(id, modules, depth = 0L) { #' @export ui_teal_module.teal_modules <- function(id, modules, depth = 0L) { ns <- NS(id) - do.call( - tabsetPanel, - c( - # by giving an id, we can reactively respond to tab changes - list( - id = ns("active_tab"), - type = if (modules$label == "root") "pills" else "tabs" - ), - lapply( - names(modules$children), - function(module_id) { - module_label <- modules$children[[module_id]]$label - if (is.null(module_label)) { - module_label <- icon("fas fa-database") - } - tabPanel( - title = module_label, - value = module_id, # when clicked this tab value changes input$ - ui_teal_module( - id = ns(module_id), - modules = modules$children[[module_id]], - depth = depth + 1L + tags$div( + id = ns("wrapper"), + do.call( + tabsetPanel, + c( + # by giving an id, we can reactively respond to tab changes + list( + id = ns("active_tab"), + type = if (modules$label == "root") "pills" else "tabs" + ), + lapply( + names(modules$children), + function(module_id) { + module_label <- modules$children[[module_id]]$label + if (is.null(module_label)) { + module_label <- icon("fas fa-database") + } + tabPanel( + title = module_label, + value = module_id, # when clicked this tab value changes input$ + ui_teal_module( + id = ns(module_id), + modules = modules$children[[module_id]], + depth = depth + 1L + ) ) - ) - } + } + ) ) ) ) } -#' @rdname module_teal_module -#' @export -ui_teal_module.shiny.tag <- function(id, modules, depth = 0L) { - modules -} - #' @rdname module_teal_module #' @export ui_teal_module.teal_module <- function(id, modules, depth = 0L) { ns <- NS(id) args <- c(list(id = ns("module")), modules$ui_args) - ui_teal <- div( + ui_teal <- tagList( div( - class = "teal_validated", + id = ns("validate_datanames"), ui_validate_reactive_teal_data(ns("validate_datanames")) ), - do.call(modules$ui, args) + shinyjs::hidden( + tags$div( + id = ns("transformer_failure_info"), + class = "teal_validated", + div( + class = "teal-output-warning", + "One of transformers failed. Please fix and continue." + ) + ) + ), + tags$div( + id = ns("teal_module_ui"), + do.call(modules$ui, args) + ) ) div( @@ -115,7 +132,13 @@ ui_teal_module.teal_module <- function(id, modules, depth = 0L) { ) ) } else { - ui_teal + div( + div( + class = "teal_validated", + uiOutput(ns("data_input_error")) + ), + ui_teal + ) } ) ) @@ -128,6 +151,7 @@ srv_teal_module <- function(id, datasets = NULL, slices_global, reporter = teal.reporter::Reporter$new(), + data_load_status = reactive("ok"), is_active = reactive(TRUE)) { checkmate::assert_string(id) assert_reactive(data_rv) @@ -135,6 +159,7 @@ srv_teal_module <- function(id, assert_reactive(datasets, null.ok = TRUE) checkmate::assert_class(slices_global, ".slicesGlobal") checkmate::assert_class(reporter, "Reporter") + assert_reactive(data_load_status) UseMethod("srv_teal_module", modules) } @@ -146,6 +171,7 @@ srv_teal_module.default <- function(id, datasets = NULL, slices_global, reporter = teal.reporter::Reporter$new(), + data_load_status = reactive("ok"), is_active = reactive(TRUE)) { stop("Modules class not supported: ", paste(class(modules), collapse = " ")) } @@ -158,10 +184,26 @@ srv_teal_module.teal_modules <- function(id, datasets = NULL, slices_global, reporter = teal.reporter::Reporter$new(), + data_load_status = reactive("ok"), is_active = reactive(TRUE)) { moduleServer(id = id, module = function(input, output, session) { logger::log_debug("srv_teal_module.teal_modules initializing the module { deparse1(modules$label) }.") + observeEvent(data_load_status(), { + tabs_selector <- sprintf("#%s li a", session$ns("active_tab")) + if (identical(data_load_status(), "ok")) { + logger::log_debug("srv_teal_module@1 enabling modules tabs.") + shinyjs::show("wrapper") + shinyjs::enable(selector = tabs_selector) + } else if (identical(data_load_status(), "teal_data_module failed")) { + logger::log_debug("srv_teal_module@1 disabling modules tabs.") + shinyjs::disable(selector = tabs_selector) + } else if (identical(data_load_status(), "external failed")) { + logger::log_debug("srv_teal_module@1 hiding modules tabs.") + shinyjs::hide("wrapper") + } + }) + modules_output <- sapply( names(modules$children), function(module_id) { @@ -190,17 +232,17 @@ srv_teal_module.teal_module <- function(id, datasets = NULL, slices_global, reporter = teal.reporter::Reporter$new(), + data_load_status = reactive("ok"), is_active = reactive(TRUE)) { logger::log_debug("srv_teal_module.teal_module initializing the module: { deparse1(modules$label) }.") moduleServer(id = id, module = function(input, output, session) { - active_datanames <- reactive(.resolve_module_datanames(data = data_rv(), modules = modules)) + active_datanames <- reactive({ + .resolve_module_datanames(data = data_rv(), modules = modules) + }) if (is.null(datasets)) { datasets <- eventReactive(data_rv(), { - if (!inherits(data_rv(), "teal_data")) { - stop("data_rv must be teal_data object.") - } + req(inherits(data_rv(), "teal_data")) logger::log_debug("srv_teal_module@1 initializing module-specific FilteredData") - teal_data_to_filtered_data(data_rv(), datanames = active_datanames()) }) } @@ -219,20 +261,37 @@ srv_teal_module.teal_module <- function(id, is_active = is_active ) + is_transformer_failed <- reactiveValues() transformed_teal_data <- srv_transform_data( "data_transform", data = filtered_teal_data, transforms = modules$transformers, - modules = modules + modules = modules, + is_transformer_failed = is_transformer_failed ) + any_transformer_failed <- reactive({ + any(unlist(reactiveValuesToList(is_transformer_failed))) + }) + observeEvent(any_transformer_failed(), { + if (isTRUE(any_transformer_failed())) { + shinyjs::hide("teal_module_ui") + shinyjs::hide("validate_datanames") + shinyjs::show("transformer_failure_info") + } else { + shinyjs::show("teal_module_ui") + shinyjs::show("validate_datanames") + shinyjs::hide("transformer_failure_info") + } + }) module_teal_data <- reactive({ + req(inherits(transformed_teal_data(), "teal_data")) all_teal_data <- transformed_teal_data() module_datanames <- .resolve_module_datanames(data = all_teal_data, modules = modules) .subset_teal_data(all_teal_data, module_datanames) }) - module_teal_data_validated <- srv_validate_reactive_teal_data( + srv_validate_reactive_teal_data( "validate_datanames", data = module_teal_data, modules = modules @@ -247,9 +306,9 @@ srv_teal_module.teal_module <- function(id, # wait for module_teal_data() to be not NULL but only once: ignoreNULL = TRUE, once = TRUE, - eventExpr = module_teal_data_validated(), + eventExpr = module_teal_data(), handlerExpr = { - module_out(.call_teal_module(modules, datasets, module_teal_data_validated, reporter)) + module_out(.call_teal_module(modules, datasets, module_teal_data, reporter)) } ) } else { diff --git a/R/module_teal.R b/R/module_teal.R index 796a306966..8624636dd2 100644 --- a/R/module_teal.R +++ b/R/module_teal.R @@ -22,6 +22,13 @@ #' - data filtering in [`module_filter_data`] #' - data transformation in [`module_transform_data`] #' +#' ## Fallback on failure +#' +#' `teal` is designed in such way that app will never crash if the error is introduced in any +#' custom `shiny` module provided by app developer (e.g. [teal_data_module()], [teal_transform_module()]). +#' If any module returns a failing object, the app will halt the evaluation and display a warning message. +#' App user should always have a chance to fix the improper input and continue without restarting the session. +#' #' @rdname module_teal #' @name module_teal #' @@ -35,12 +42,10 @@ NULL #' @export ui_teal <- function(id, modules, - data = NULL, title = build_app_title(), header = tags$p(), footer = tags$p()) { checkmate::assert_character(id, max.len = 1, any.missing = FALSE) - checkmate::assert_multi_class(data, "teal_data_module", null.ok = TRUE) checkmate::assert( .var.name = "title", checkmate::check_string(title), @@ -85,13 +90,6 @@ ui_teal <- function(id, ) ) - bookmark_panel_ui <- ui_bookmark_panel(ns("bookmark_manager"), modules) - data_elem <- ui_init_data(ns("data"), data = data) - if (!is.null(data)) { - modules$children <- c(list(teal_data_module = data_elem), modules$children) - } - tabs_elem <- ui_teal_module(id = ns("teal_modules"), modules = modules) - fluidPage( id = id, title = title, @@ -103,12 +101,12 @@ ui_teal <- function(id, tags$div( id = ns("tabpanel_wrapper"), class = "teal-body", - tabs_elem + ui_teal_module(id = ns("teal_modules"), modules = modules) ), tags$div( id = ns("options_buttons"), style = "position: absolute; right: 10px;", - bookmark_panel_ui, + ui_bookmark_panel(ns("bookmark_manager"), modules), tags$button( class = "btn action-button filter_hamburger", # see sidebar.css for style filter_hamburger href = "javascript:void(0)", @@ -149,13 +147,17 @@ ui_teal <- function(id, #' @export srv_teal <- function(id, data, modules, filter = teal_slices()) { checkmate::assert_character(id, max.len = 1, any.missing = FALSE) - checkmate::assert_multi_class(data, c("teal_data", "teal_data_module", "reactive", "reactiveVal")) + checkmate::assert_multi_class(data, c("teal_data", "teal_data_module", "reactive")) checkmate::assert_class(modules, "teal_modules") checkmate::assert_class(filter, "teal_slices") moduleServer(id, function(input, output, session) { logger::log_debug("srv_teal initializing.") + if (getOption("teal.show_js_log", default = FALSE)) { + shinyjs::showLog() + } + srv_teal_lockfile("lockfile") output$identifier <- renderText( @@ -184,17 +186,77 @@ srv_teal <- function(id, data, modules, filter = teal_slices()) { } ) - data_rv <- srv_init_data("data", data = data, modules = modules, filter = filter) + data_pulled <- srv_init_data("data", data = data) + data_validated <- srv_validate_reactive_teal_data( + "validate", + data = data_pulled, + modules = modules, + validate_shiny_silent_error = FALSE + ) + data_rv <- reactive({ + req(inherits(data_validated(), "teal_data")) + is_filter_ok <- check_filter_datanames(filter, ls(teal.code::get_env(data_validated()))) + if (!isTRUE(is_filter_ok)) { + showNotification( + "Some filters were not applied because of incompatibility with data. Contact app developer.", + type = "warning", + duration = 10 + ) + warning(is_filter_ok) + } + .add_signature_to_data(data_validated()) + }) + + data_load_status <- reactive({ + if (inherits(data_pulled(), "teal_data")) { + "ok" + } else if (inherits(data, "teal_data_module")) { + "teal_data_module failed" + } else { + "external failed" + } + }) + datasets_rv <- if (!isTRUE(attr(filter, "module_specific"))) { eventReactive(data_rv(), { - if (!inherits(data_rv(), "teal_data")) { - stop("data_rv must be teal_data object.") - } + req(inherits(data_rv(), "teal_data")) logger::log_debug("srv_teal@1 initializing FilteredData") teal_data_to_filtered_data(data_rv()) }) } + if (inherits(data, "teal_data_module")) { + setBookmarkExclude(c("teal_modules-active_tab")) + shiny::insertTab( + inputId = "teal_modules-active_tab", + position = "before", + select = TRUE, + tabPanel( + title = icon("fas fa-database"), + value = "teal_data_module", + tags$div( + ui_init_data(session$ns("data")), + ui_validate_reactive_teal_data(session$ns("validate")) + ) + ) + ) + + if (attr(data, "once")) { + observeEvent(data_rv(), once = TRUE, { + logger::log_debug("srv_teal@2 removing data tab.") + # when once = TRUE we pull data once and then remove data tab + removeTab("teal_modules-active_tab", target = "teal_data_module") + }) + } + } else { + # when no teal_data_module then we want to display messages above tabsetPanel (because there is no data-tab) + insertUI( + selector = sprintf("#%s", session$ns("tabpanel_wrapper")), + where = "beforeBegin", + ui = tags$div(ui_validate_reactive_teal_data(session$ns("validate")), tags$br()) + ) + } + module_labels <- unlist(module_labels(modules), use.names = FALSE) slices_global <- methods::new(".slicesGlobal", filter, module_labels) modules_output <- srv_teal_module( @@ -202,15 +264,12 @@ srv_teal <- function(id, data, modules, filter = teal_slices()) { data_rv = data_rv, datasets = datasets_rv, modules = modules, - slices_global = slices_global + slices_global = slices_global, + data_load_status = data_load_status ) mapping_table <- srv_filter_manager_panel("filter_manager_panel", slices_global = slices_global) snapshots <- srv_snapshot_manager_panel("snapshot_manager_panel", slices_global = slices_global) srv_bookmark_panel("bookmark_manager", modules) - - if (inherits(data, "teal_data_module")) { - setBookmarkExclude(c("teal_modules-active_tab")) - } }) invisible(NULL) diff --git a/R/module_teal_data.R b/R/module_teal_data.R index 943f635136..899ef14028 100644 --- a/R/module_teal_data.R +++ b/R/module_teal_data.R @@ -16,19 +16,18 @@ #' 5. [teal_data()] object lacks any `datanames` specified in the `modules` argument. #' #' `teal` (observers in `srv_teal`) always waits to render an app until `reactive` `teal_data` is -#' returned. If error 2-4 occurs, relevant error message is displayed to app user and after issue is -#' resolved app will continue to run. `teal` guarantees that errors in a data don't crash an app -#' (except error 1). This is possible thanks to `.fallback_on_failure` which returns input-data -#' when output-data fails -#' +#' returned. If error 2-4 occurs, relevant error message is displayed to the app user. Once the issue is +#' resolved, the app will continue to run. `teal` guarantees that errors in data don't crash the app +#' (except error 1). #' #' @param id (`character(1)`) Module id #' @param data (`reactive teal_data`) #' @param data_module (`teal_data_module`) #' @param modules (`teal_modules` or `teal_module`) For `datanames` validation purpose #' @param validate_shiny_silent_error (`logical`) If `TRUE`, then `shiny.silent.error` is validated and -#' error message is displayed. -#' Default is `FALSE` to handle empty reactive cycle on `init`. +#' @param is_transformer_failed (`reactiveValues`) contains `logical` flags named after each transformer. +#' Help to determine if any previous transformer failed, so that following transformers can be disabled +#' and display a generic failure message. #' #' @return `reactive` `teal_data` #' @@ -38,55 +37,81 @@ NULL #' @rdname module_teal_data -ui_teal_data <- function(id, data_module) { +ui_teal_data <- function(id, data_module = function(id) NULL) { checkmate::assert_string(id) - checkmate::assert_class(data_module, "teal_data_module") + checkmate::assert_function(data_module, args = "id") ns <- NS(id) + shiny::tagList( - data_module$ui(id = ns("data")), + tags$div(id = ns("wrapper"), data_module(id = ns("data"))), ui_validate_reactive_teal_data(ns("validate")) ) } #' @rdname module_teal_data srv_teal_data <- function(id, - data, - data_module, + data_module = function(id) NULL, modules = NULL, - validate_shiny_silent_error = TRUE) { + validate_shiny_silent_error = TRUE, + is_transformer_failed = reactiveValues()) { checkmate::assert_string(id) - checkmate::assert_class(data_module, "teal_data_module") + checkmate::assert_function(data_module, args = "id") checkmate::assert_multi_class(modules, c("teal_modules", "teal_module"), null.ok = TRUE) + checkmate::assert_class(is_transformer_failed, "reactivevalues") moduleServer(id, function(input, output, session) { logger::log_debug("srv_teal_data initializing.") + is_transformer_failed[[id]] <- FALSE + data_out <- data_module(id = "data") + data_handled <- reactive(tryCatch(data_out(), error = function(e) e)) + observeEvent(data_handled(), { + if (!inherits(data_handled(), "teal_data")) { + is_transformer_failed[[id]] <- TRUE + } else { + is_transformer_failed[[id]] <- FALSE + } + }) - data_out <- if (is_arg_used(data_module$server, "data")) { - data_module$server(id = "data", data = data) - } else { - data_module$server(id = "data") - } + is_previous_failed <- reactive({ + idx_this <- which(names(is_transformer_failed) == id) + is_transformer_failed_list <- reactiveValuesToList(is_transformer_failed) + idx_failures <- which(unlist(is_transformer_failed_list)) + any(idx_failures < idx_this) + }) - data_validated <- srv_validate_reactive_teal_data( - id = "validate", - data = data_out, - modules = modules, - validate_shiny_silent_error = validate_shiny_silent_error - ) + observeEvent(is_previous_failed(), { + if (is_previous_failed()) { + shinyjs::disable("wrapper") + } else { + shinyjs::enable("wrapper") + } + }) - .fallback_on_failure( - this = data_validated, - that = data, - label = sprintf("Data element '%s' for module '%s'", id, modules$label) + srv_validate_reactive_teal_data( + "validate", + data = data_handled, + modules = modules, + validate_shiny_silent_error = validate_shiny_silent_error, + hide_validation_error = is_previous_failed ) }) } #' @rdname module_teal_data ui_validate_reactive_teal_data <- function(id) { + ns <- NS(id) tagList( - uiOutput(NS(id, "shiny_errors")), - uiOutput(NS(id, "shiny_warnings")) + div( + id = ns("validate_messages"), + class = "teal_validated", + ui_validate_error(ns("silent_error")), + ui_check_class_teal_data(ns("class_teal_data")), + ui_check_shiny_warnings(ns("shiny_warnings")) + ), + div( + class = "teal_validated", + uiOutput(ns("previous_failed")) + ) ) } @@ -94,118 +119,133 @@ ui_validate_reactive_teal_data <- function(id) { srv_validate_reactive_teal_data <- function(id, # nolint: object_length data, modules = NULL, - validate_shiny_silent_error = FALSE) { + validate_shiny_silent_error = FALSE, + hide_validation_error = reactive(FALSE)) { checkmate::assert_string(id) checkmate::assert_multi_class(modules, c("teal_modules", "teal_module"), null.ok = TRUE) checkmate::assert_flag(validate_shiny_silent_error) moduleServer(id, function(input, output, session) { - data_out_r <- reactive(tryCatch(data(), error = function(e) e)) + # there is an empty reactive cycle on `init` and `data_rv` has `shiny.silent.error` class + srv_validate_error("silent_error", data, validate_shiny_silent_error) + srv_check_class_teal_data("class_teal_data", data) + srv_check_shiny_warnings("shiny_warnings", data, modules) + output$previous_failed <- renderUI({ + if (hide_validation_error()) { + shinyjs::hide("validate_messages") + tags$div("One of previous transformers failed. Please fix and continue.", class = "teal-output-warning") + } else { + shinyjs::show("validate_messages") + NULL + } + }) - data_validated <- reactive({ - # custom module can return error - data_out <- data_out_r() + .trigger_on_success(data) + }) +} - # there is an empty reactive cycle on init! - if (inherits(data_out, "shiny.silent.error") && identical(data_out$message, "")) { - if (!validate_shiny_silent_error) { - return(NULL) - } else { - validate( - need( - FALSE, - paste( - "Shiny error when executing the `data` module", - "Check your inputs or contact app developer if error persists.", - collapse = "\n" - ) - ) - ) - } - } +#' @keywords internal +ui_validate_error <- function(id) { + ns <- NS(id) + uiOutput(ns("message")) +} - # to handle errors and qenv.error(s) - if (inherits(data_out, c("qenv.error", "error"))) { +#' @keywords internal +srv_validate_error <- function(id, data, validate_shiny_silent_error) { + checkmate::assert_string(id) + checkmate::assert_flag(validate_shiny_silent_error) + moduleServer(id, function(input, output, session) { + output$message <- renderUI({ + is_shiny_silent_error <- inherits(data(), "shiny.silent.error") && identical(data()$message, "") + if (inherits(data(), "qenv.error")) { validate( need( FALSE, - paste0( + paste( "Error when executing the `data` module:", - strip_style(paste(data_out$message, collapse = "\n")), - "Check your inputs or contact app developer if error persists.", + strip_style(paste(data()$message, collapse = "\n")), + "\nCheck your inputs or contact app developer if error persists.", collapse = "\n" ) ) ) + } else if (inherits(data(), "error")) { + if (is_shiny_silent_error && !validate_shiny_silent_error) { + return(NULL) + } + validate( + need( + FALSE, + sprintf( + "Shiny error when executing the `data` module.\n%s\n%s", + data()$message, + "Check your inputs or contact app developer if error persists." + ) + ) + ) } + }) + }) +} + +#' @keywords internal +ui_check_class_teal_data <- function(id) { + ns <- NS(id) + uiOutput(ns("message")) +} + +#' @keywords internal +srv_check_class_teal_data <- function(id, data) { + checkmate::assert_string(id) + moduleServer(id, function(input, output, session) { + output$message <- renderUI({ validate( need( - checkmate::test_class(data_out, "teal_data"), - paste0( - "Assertion on return value from the 'data' module failed:", - checkmate::test_class(data_out, "teal_data"), - "Check your inputs or contact app developer if error persists.", - collapse = "\n" - ) + inherits(data(), c("teal_data", "error")), + "Did not receive `teal_data` object. Cannot proceed further." ) ) - - data_out }) + }) +} - output$shiny_errors <- renderUI({ - data_validated() - NULL - }) +#' @keywords internal +ui_check_shiny_warnings <- function(id) { + ns <- NS(id) + uiOutput(NS(id, "message")) +} - output$shiny_warnings <- renderUI({ - if (inherits(data_out_r(), "teal_data")) { - is_modules_ok <- check_modules_datanames( - modules = modules, - datanames = ls(teal.code::get_env(data_validated())) - ) +#' @keywords internal +srv_check_shiny_warnings <- function(id, data, modules) { + checkmate::assert_string(id) + moduleServer(id, function(input, output, session) { + output$message <- renderUI({ + if (inherits(data(), "teal_data")) { + is_modules_ok <- check_modules_datanames(modules = modules, datanames = ls(teal.code::get_env(data()))) if (!isTRUE(is_modules_ok)) { tags$div( + class = "teal-output-warning", is_modules_ok$html( # Show modules prefix on message only in teal_data_module tab grepl(sprintf("data-teal_data_module-%s", id), session$ns(NULL), fixed = TRUE) - ), - class = "teal-output-warning" + ) ) } } }) - - data_validated }) } -#' Fallback on failure -#' -#' Function returns the previous reactive if the current reactive is invalid (throws error or returns NULL). -#' Application: In `teal` we try to prevent the error from being thrown and instead we replace failing -#' transform module data output with data input from the previous module (or from previous `teal` reactive -#' tree elements). -#' -#' @param this (`reactive`) Current reactive. -#' @param that (`reactive`) Previous reactive. -#' @param label (`character`) Label for identifying problematic `teal_data_module` transform in logging. -#' @return `reactive` `teal_data` -#' @keywords internal -.fallback_on_failure <- function(this, that, label) { - assert_reactive(this) - assert_reactive(that) - checkmate::assert_string(label) - - reactive({ - res <- try(this(), silent = TRUE) - if (inherits(res, "teal_data")) { - logger::log_debug("{ label } evaluated successfully.") - res - } else { - logger::log_debug("{ label } failed, falling back to previous data.") - that() +.trigger_on_success <- function(data) { + out <- reactiveVal(NULL) + observeEvent(data(), { + if (inherits(data(), "teal_data")) { + if (!identical(data(), out())) { + out(data()) + } } }) + + out } diff --git a/R/module_teal_with_splash.R b/R/module_teal_with_splash.R index c7404e7920..c4f29c0ae3 100644 --- a/R/module_teal_with_splash.R +++ b/R/module_teal_with_splash.R @@ -24,7 +24,7 @@ ui_teal_with_splash <- function(id, what = "ui_teal_with_splash()", details = "Deprecated, please use `ui_teal` instead" ) - ui_teal(id = id, data = data, title = title, header = header, footer = footer) + ui_teal(id = id, title = title, header = header, footer = footer) } #' @export diff --git a/R/module_transform_data.R b/R/module_transform_data.R index 30107ef88d..418c81d06d 100644 --- a/R/module_transform_data.R +++ b/R/module_transform_data.R @@ -44,7 +44,7 @@ ui_transform_data <- function(id, transforms, class = "well") { ), div( id = wrapper_id, - ui_teal_data(id = ns(name), data_module = transforms[[name]]) + ui_teal_data(id = ns(name), data_module = transforms[[name]]$ui) ) ) } @@ -52,29 +52,26 @@ ui_transform_data <- function(id, transforms, class = "well") { } #' @rdname module_transform_data -srv_transform_data <- function(id, data, transforms, modules) { +srv_transform_data <- function(id, data, transforms, modules, is_transformer_failed = reactiveValues()) { checkmate::assert_string(id) assert_reactive(data) checkmate::assert_list(transforms, "teal_transform_module", null.ok = TRUE) checkmate::assert_class(modules, "teal_module") - if (length(transforms) == 0L) { return(data) } - labels <- lapply(transforms, function(x) attr(x, "label")) ids <- get_unique_labels(labels) names(transforms) <- ids - moduleServer(id, function(input, output, session) { logger::log_debug("srv_teal_data_modules initializing.") Reduce( function(previous_result, name) { srv_teal_data( id = name, - data = previous_result, - data_module = transforms[[name]], - modules = modules + data_module = function(id) transforms[[name]]$server(id, previous_result), + modules = modules, + is_transformer_failed = is_transformer_failed ) }, x = names(transforms), diff --git a/R/utils.R b/R/utils.R index d2bedef4e3..e5830bf0ca 100644 --- a/R/utils.R +++ b/R/utils.R @@ -139,6 +139,9 @@ check_modules_datanames <- function(modules, datanames) { if (inherits(modules, "teal_modules")) { result <- lapply(modules$children, function(module) recursive_check_datanames(module, datanames = datanames)) result <- result[vapply(result, Negate(is.null), logical(1L))] + if (length(result) == 0) { + return(NULL) + } list( string = do.call(c, as.list(unname(sapply(result, function(x) x$string)))), html = function(with_module_name = TRUE) { diff --git a/man/dot-fallback_on_failure.Rd b/man/dot-fallback_on_failure.Rd deleted file mode 100644 index 5d8f168e2d..0000000000 --- a/man/dot-fallback_on_failure.Rd +++ /dev/null @@ -1,25 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/module_teal_data.R -\name{.fallback_on_failure} -\alias{.fallback_on_failure} -\title{Fallback on failure} -\usage{ -.fallback_on_failure(this, that, label) -} -\arguments{ -\item{this}{(\code{reactive}) Current reactive.} - -\item{that}{(\code{reactive}) Previous reactive.} - -\item{label}{(\code{character}) Label for identifying problematic \code{teal_data_module} transform in logging.} -} -\value{ -\code{reactive} \code{teal_data} -} -\description{ -Function returns the previous reactive if the current reactive is invalid (throws error or returns NULL). -Application: In \code{teal} we try to prevent the error from being thrown and instead we replace failing -transform module data output with data input from the previous module (or from previous \code{teal} reactive -tree elements). -} -\keyword{internal} diff --git a/man/module_filter_data.Rd b/man/module_filter_data.Rd index e2dfe310f4..61d527b9ef 100644 --- a/man/module_filter_data.Rd +++ b/man/module_filter_data.Rd @@ -6,6 +6,7 @@ \alias{srv_filter_data} \alias{.make_filtered_teal_data} \alias{.observe_active_filter_changed} +\alias{.get_filter_expr} \title{Filter panel module in teal} \usage{ ui_filter_data(id) @@ -15,6 +16,8 @@ srv_filter_data(id, datasets, active_datanames, data_rv, is_active) .make_filtered_teal_data(modules, data, datasets = NULL, datanames) .observe_active_filter_changed(datasets, is_active, active_datanames, data_rv) + +.get_filter_expr(datasets, datanames) } \arguments{ \item{id}{(\code{character}) Optionally, diff --git a/man/module_init_data.Rd b/man/module_init_data.Rd index d61bb48e7a..7e9cd8b715 100644 --- a/man/module_init_data.Rd +++ b/man/module_init_data.Rd @@ -6,9 +6,9 @@ \alias{srv_init_data} \title{Data Module for teal} \usage{ -ui_init_data(id, data) +ui_init_data(id) -srv_init_data(id, data, modules, filter = teal_slices()) +srv_init_data(id, data) } \arguments{ \item{id}{(\code{character}) Optionally, @@ -16,26 +16,12 @@ a string specifying the \code{shiny} module id in cases it is used as a \code{sh rather than a standalone \code{shiny} app. This is a legacy feature.} \item{data}{(\code{teal_data}, \code{teal_data_module}, or \code{reactive} returning \code{teal_data}) -The \code{ui} component of this module does not require \code{data} if \code{teal_data_module} is not provided. -The \code{data} argument in the \code{ui} is included solely for the \verb{$ui} function of the -\code{teal_data_module}. Otherwise, it can be disregarded, ensuring that \code{ui_teal} does not depend on -the reactive data of the enclosing application.} - -\item{modules}{(\code{list} or \code{teal_modules} or \code{teal_module}) -Nested list of \code{teal_modules} or \code{teal_module} objects or a single -\code{teal_modules} or \code{teal_module} object. These are the specific output modules which -will be displayed in the \code{teal} application. See \code{\link[=modules]{modules()}} and \code{\link[=module]{module()}} for -more details.} - -\item{filter}{(\code{teal_slices}) Optionally, -specifies the initial filter using \code{\link[=teal_slices]{teal_slices()}}.} +The data which application will depend on.} } \value{ A \code{reactive} object that returns: -\itemize{ -\item \code{teal_data} when the object is validated -\item \code{shiny.silent.error} when not validated. -} +Output of the \code{data}. If \code{data} fails then returned error is handled (after \code{\link[=tryCatch]{tryCatch()}}) so that +rest of the application can respond to this respectively. } \description{ This module manages the \code{data} argument for \code{srv_teal}. The \code{teal} framework uses \code{\link[=teal_data]{teal_data()}}, diff --git a/man/module_teal.Rd b/man/module_teal.Rd index 29cab0645c..f2bc5d95b5 100644 --- a/man/module_teal.Rd +++ b/man/module_teal.Rd @@ -9,7 +9,6 @@ ui_teal( id, modules, - data = NULL, title = build_app_title(), header = tags$p(), footer = tags$p() @@ -28,12 +27,6 @@ Nested list of \code{teal_modules} or \code{teal_module} objects or a single will be displayed in the \code{teal} application. See \code{\link[=modules]{modules()}} and \code{\link[=module]{module()}} for more details.} -\item{data}{(\code{teal_data}, \code{teal_data_module}, or \code{reactive} returning \code{teal_data}) -The \code{ui} component of this module does not require \code{data} if \code{teal_data_module} is not provided. -The \code{data} argument in the \code{ui} is included solely for the \verb{$ui} function of the -\code{teal_data_module}. Otherwise, it can be disregarded, ensuring that \code{ui_teal} does not depend on -the reactive data of the enclosing application.} - \item{title}{(\code{shiny.tag} or \code{character(1)}) Optionally, the browser window title. Defaults to a title "teal app" with the icon of NEST. Can be created using the \code{build_app_title()} or @@ -45,6 +38,9 @@ the header of the app.} \item{footer}{(\code{shiny.tag} or \code{character(1)}) Optionally, the footer of the app.} +\item{data}{(\code{teal_data}, \code{teal_data_module}, or \code{reactive} returning \code{teal_data}) +The data which application will depend on.} + \item{filter}{(\code{teal_slices}) Optionally, specifies the initial filter using \code{\link[=teal_slices]{teal_slices()}}.} } @@ -73,4 +69,12 @@ performed: \item data transformation in \code{\link{module_transform_data}} } } + +\subsection{Fallback on failure}{ + +\code{teal} is designed in such way that app will never crash if the error is introduced in any +custom \code{shiny} module provided by app developer (e.g. \code{\link[=teal_data_module]{teal_data_module()}}, \code{\link[=teal_transform_module]{teal_transform_module()}}). +If any module returns a failing object, the app will halt the evaluation and display a warning message. +App user should always have a chance to fix the improper input and continue without restarting the session. +} } diff --git a/man/module_teal_data.Rd b/man/module_teal_data.Rd index abcbe048ff..dee0b1087e 100644 --- a/man/module_teal_data.Rd +++ b/man/module_teal_data.Rd @@ -8,14 +8,14 @@ \alias{srv_validate_reactive_teal_data} \title{Execute and validate \code{teal_data_module}} \usage{ -ui_teal_data(id, data_module) +ui_teal_data(id, data_module = function(id) NULL) srv_teal_data( id, - data, - data_module, + data_module = function(id) NULL, modules = NULL, - validate_shiny_silent_error = TRUE + validate_shiny_silent_error = TRUE, + is_transformer_failed = reactiveValues() ) ui_validate_reactive_teal_data(id) @@ -24,7 +24,8 @@ srv_validate_reactive_teal_data( id, data, modules = NULL, - validate_shiny_silent_error = FALSE + validate_shiny_silent_error = FALSE, + hide_validation_error = reactive(FALSE) ) } \arguments{ @@ -32,13 +33,15 @@ srv_validate_reactive_teal_data( \item{data_module}{(\code{teal_data_module})} -\item{data}{(\verb{reactive teal_data})} - \item{modules}{(\code{teal_modules} or \code{teal_module}) For \code{datanames} validation purpose} -\item{validate_shiny_silent_error}{(\code{logical}) If \code{TRUE}, then \code{shiny.silent.error} is validated and -error message is displayed. -Default is \code{FALSE} to handle empty reactive cycle on \code{init}.} +\item{validate_shiny_silent_error}{(\code{logical}) If \code{TRUE}, then \code{shiny.silent.error} is validated and} + +\item{is_transformer_failed}{(\code{reactiveValues}) contains \code{logical} flags named after each transformer. +Help to determine if any previous transformer failed, so that following transformers can be disabled +and display a generic failure message.} + +\item{data}{(\verb{reactive teal_data})} } \value{ \code{reactive} \code{teal_data} @@ -63,10 +66,9 @@ Output \code{data} is invalid if: } \code{teal} (observers in \code{srv_teal}) always waits to render an app until \code{reactive} \code{teal_data} is -returned. If error 2-4 occurs, relevant error message is displayed to app user and after issue is -resolved app will continue to run. \code{teal} guarantees that errors in a data don't crash an app -(except error 1). This is possible thanks to \code{.fallback_on_failure} which returns input-data -when output-data fails +returned. If error 2-4 occurs, relevant error message is displayed to the app user. Once the issue is +resolved, the app will continue to run. \code{teal} guarantees that errors in data don't crash the app +(except error 1). } \keyword{internal} diff --git a/man/module_teal_module.Rd b/man/module_teal_module.Rd index b5bb015f81..bc46f86c61 100644 --- a/man/module_teal_module.Rd +++ b/man/module_teal_module.Rd @@ -5,7 +5,6 @@ \alias{ui_teal_module} \alias{ui_teal_module.default} \alias{ui_teal_module.teal_modules} -\alias{ui_teal_module.shiny.tag} \alias{ui_teal_module.teal_module} \alias{srv_teal_module} \alias{srv_teal_module.default} @@ -19,8 +18,6 @@ ui_teal_module(id, modules, depth = 0L) \method{ui_teal_module}{teal_modules}(id, modules, depth = 0L) -\method{ui_teal_module}{shiny.tag}(id, modules, depth = 0L) - \method{ui_teal_module}{teal_module}(id, modules, depth = 0L) srv_teal_module( @@ -30,6 +27,7 @@ srv_teal_module( datasets = NULL, slices_global, reporter = teal.reporter::Reporter$new(), + data_load_status = reactive("ok"), is_active = reactive(TRUE) ) @@ -40,6 +38,7 @@ srv_teal_module( datasets = NULL, slices_global, reporter = teal.reporter::Reporter$new(), + data_load_status = reactive("ok"), is_active = reactive(TRUE) ) @@ -50,6 +49,7 @@ srv_teal_module( datasets = NULL, slices_global, reporter = teal.reporter::Reporter$new(), + data_load_status = reactive("ok"), is_active = reactive(TRUE) ) @@ -60,6 +60,7 @@ srv_teal_module( datasets = NULL, slices_global, reporter = teal.reporter::Reporter$new(), + data_load_status = reactive("ok"), is_active = reactive(TRUE) ) } @@ -85,6 +86,15 @@ which implies in filter-panel to be "global". When \code{NULL} then filter-panel \item{slices_global}{(\code{reactiveVal} returning \code{modules_teal_slices}) see \code{\link{module_filter_manager}}} + +\item{data_load_status}{(\code{reactive} returning \code{character}) +Determines action dependent on a data loading status: +\itemize{ +\item \code{"ok"} when \code{teal_data} is returned from the data loading. +\item \code{"teal_data_module failed"} when \code{\link[=teal_data_module]{teal_data_module()}} didn't return \code{teal_data}. Disables tabs buttons. +\item \code{"external failed"} when a \code{reactive} passed to \code{srv_teal(data)} didn't return \code{teal_data}. Hides the whole tab +panel. +}} } \value{ output of currently active module. diff --git a/man/module_teal_with_splash.Rd b/man/module_teal_with_splash.Rd index 4fa6ba3e3e..98b49551fb 100644 --- a/man/module_teal_with_splash.Rd +++ b/man/module_teal_with_splash.Rd @@ -22,10 +22,7 @@ a string specifying the \code{shiny} module id in cases it is used as a \code{sh rather than a standalone \code{shiny} app. This is a legacy feature.} \item{data}{(\code{teal_data}, \code{teal_data_module}, or \code{reactive} returning \code{teal_data}) -The \code{ui} component of this module does not require \code{data} if \code{teal_data_module} is not provided. -The \code{data} argument in the \code{ui} is included solely for the \verb{$ui} function of the -\code{teal_data_module}. Otherwise, it can be disregarded, ensuring that \code{ui_teal} does not depend on -the reactive data of the enclosing application.} +The data which application will depend on.} \item{title}{(\code{shiny.tag} or \code{character(1)}) Optionally, the browser window title. Defaults to a title "teal app" with the icon of NEST. diff --git a/man/module_transform_data.Rd b/man/module_transform_data.Rd index 5dbc480880..2a4a351062 100644 --- a/man/module_transform_data.Rd +++ b/man/module_transform_data.Rd @@ -8,7 +8,13 @@ \usage{ ui_transform_data(id, transforms, class = "well") -srv_transform_data(id, data, transforms, modules) +srv_transform_data( + id, + data, + transforms, + modules, + is_transformer_failed = reactiveValues() +) } \arguments{ \item{id}{(\code{character(1)}) Module id} @@ -16,6 +22,10 @@ srv_transform_data(id, data, transforms, modules) \item{data}{(\verb{reactive teal_data})} \item{modules}{(\code{teal_modules} or \code{teal_module}) For \code{datanames} validation purpose} + +\item{is_transformer_failed}{(\code{reactiveValues}) contains \code{logical} flags named after each transformer. +Help to determine if any previous transformer failed, so that following transformers can be disabled +and display a generic failure message.} } \value{ \code{reactive} \code{teal_data} diff --git a/tests/testthat/test-module_teal.R b/tests/testthat/test-module_teal.R index 9b0be9b378..e863a1325f 100644 --- a/tests/testthat/test-module_teal.R +++ b/tests/testthat/test-module_teal.R @@ -182,7 +182,7 @@ testthat::describe("srv_teal arguments", { ), expr = NULL ), - "Must inherit from class 'teal_data'/'teal_data_module'/'reactive'/'reactiveVal'" + "Assertion on 'data' failed: Must inherit from class 'teal_data'/'teal_data_module'/'reactive', but has class 'data.frame'." # nolint: line_length ) }) @@ -217,7 +217,7 @@ testthat::describe("srv_teal teal_modules", { ) ), expr = { - testthat::expect_s4_class(data_rv(), "teal_data") + testthat::expect_error(data_init(), NULL) testthat::expect_null(modules_output$module_1()) testthat::expect_null(modules_output$module_2()) } @@ -236,7 +236,7 @@ testthat::describe("srv_teal teal_modules", { ) ), expr = { - testthat::expect_s4_class(data_rv(), "teal_data") + testthat::expect_error(data_init()) session$setInputs(`teal_modules-active_tab` = "module_1") testthat::expect_identical(modules_output$module_1(), 101L) testthat::expect_null(modules_output$module_2()) @@ -259,7 +259,7 @@ testthat::describe("srv_teal teal_modules", { ) ), expr = { - testthat::expect_s4_class(data_rv(), "teal_data") + testthat::expect_error(data_init()) session$setInputs(`teal_modules-active_tab` = "module_1") testthat::expect_identical(modules_output$module_1(), 101L) testthat::expect_null(modules_output$module_2()) @@ -290,7 +290,7 @@ testthat::describe("srv_teal teal_modules", { ) ), expr = { - testthat::expect_s4_class(data_rv(), "teal_data") + testthat::expect_error(data_init()) session$setInputs(`teal_modules-active_tab` = "module_1") testthat::expect_identical(modules_output$module_1(), 101L) testthat::expect_null(modules_output$module_2()) @@ -313,7 +313,7 @@ testthat::describe("srv_teal teal_modules", { ) ), expr = { - testthat::expect_s4_class(data_rv(), "teal_data") + testthat::expect_error(data_init()) session$setInputs(`teal_modules-active_tab` = "module_1") testthat::expect_s4_class(modules_output$module_1()(), "teal_data") } @@ -340,14 +340,14 @@ testthat::describe("srv_teal teal_modules", { ), expr = { testthat::expect_null(modules_output$module_1()) - testthat::expect_error(data_rv()) + testthat::expect_error(data_init()) session$setInputs(`teal_modules-active_tab` = "module_1") testthat::expect_null(modules_output$module_1()) } ) }) - testthat::it("are not called when the teal_data_module returns validation error", { + testthat::it("are not called when teal_data_module returns validation error", { shiny::testServer( app = srv_teal, args = list( @@ -367,13 +367,14 @@ testthat::describe("srv_teal teal_modules", { ), expr = { testthat::expect_null(modules_output$module_1()) + testthat::expect_error(data_init()) session$setInputs(`teal_modules-active_tab` = "module_1") testthat::expect_null(modules_output$module_1()) } ) }) - testthat::it("are not called when the teal_data_module throw en error", { + testthat::it("are not called when teal_data_module throws an error", { shiny::testServer( app = srv_teal, args = list( @@ -393,14 +394,14 @@ testthat::describe("srv_teal teal_modules", { ), expr = { testthat::expect_null(modules_output$module_1()) - testthat::expect_error(data_rv()) + testthat::expect_error(data_init()) session$setInputs(`teal_modules-active_tab` = "module_1") testthat::expect_null(modules_output$module_1()) } ) }) - testthat::it("are not called when the teal_data_module returns qenv.error", { + testthat::it("are not called when teal_data_module returns qenv.error", { shiny::testServer( app = srv_teal, args = list( @@ -420,7 +421,7 @@ testthat::describe("srv_teal teal_modules", { ), expr = { testthat::expect_null(modules_output$module_1()) - testthat::expect_error(data_rv()) + testthat::expect_error(data_init()) session$setInputs(`teal_modules-active_tab` = "module_1") testthat::expect_null(modules_output$module_1()) } @@ -453,9 +454,9 @@ testthat::describe("srv_teal teal_modules", { ), expr = { testthat::expect_null(modules_output$module_1()) - session$setInputs(`data-teal_data_module-data-dataset` = "iris", `teal_modules-active_tab` = "module_1") + session$setInputs(`data-teal_data_module-dataset` = "iris", `teal_modules-active_tab` = "module_1") testthat::expect_setequal(ls(teal.code::get_env(modules_output$module_1()())), "iris") - session$setInputs(`data-teal_data_module-data-dataset` = "mtcars", `teal_modules-active_tab` = "module_2") + session$setInputs(`data-teal_data_module-dataset` = "mtcars", `teal_modules-active_tab` = "module_2") testthat::expect_setequal(ls(teal.code::get_env(modules_output$module_2()())), "mtcars") } ) @@ -487,12 +488,12 @@ testthat::describe("srv_teal teal_modules", { expr = { testthat::expect_null(modules_output$module_1()) session$setInputs( - `data-teal_data_module-data-dataset` = "iris", + `data-teal_data_module-dataset` = "iris", `teal_modules-active_tab` = "module_1" ) out <- modules_output$module_1() testthat::expect_true(!is.null(out)) - session$setInputs(`data-teal_data_module-data-dataset` = "mtcars") + session$setInputs(`data-teal_data_module-dataset` = "mtcars") testthat::expect_identical(out, modules_output$module_1()) } ) @@ -534,7 +535,7 @@ testthat::describe("srv_teal teal_modules", { trimws( rvest::html_text2( rvest::read_html( - output[["teal_modules-module_1-validate_datanames-shiny_warnings"]]$html + output[["teal_modules-module_1-validate_datanames-shiny_warnings-message"]]$html ) ) ), @@ -1577,7 +1578,6 @@ testthat::describe("srv_teal teal_module(s) transformer", { ), expr = { session$setInputs(`teal_modules-active_tab` = "module_1") - session$flushReact() data_from_transform <- modules_output$module_1()()[["data_from_transform"]] testthat::expect_identical(data_from_transform$mtcars, mtcars) expected_iris <- iris[iris$Species == "versicolor", ] @@ -1612,7 +1612,7 @@ testthat::describe("srv_teal teal_module(s) transformer", { ) }) - testthat::it("continues when transformer throws validation error and returns unchanged data", { + testthat::it("pauses when transformer throws validation error", { shiny::testServer( app = srv_teal, args = list( @@ -1635,12 +1635,12 @@ testthat::describe("srv_teal teal_module(s) transformer", { ), expr = { session$setInputs(`teal_modules-active_tab` = "module_1") - testthat::expect_identical(modules_output$module_1()()[["iris"]], iris) + testthat::expect_null(modules_output$module_1()) } ) }) - testthat::it("continues when transformer throws validation error and returns unchanged data", { + testthat::it("pauses when transformer throws validation error", { shiny::testServer( app = srv_teal, args = list( @@ -1663,73 +1663,66 @@ testthat::describe("srv_teal teal_module(s) transformer", { ), expr = { session$setInputs(`teal_modules-active_tab` = "module_1") - testthat::expect_identical(modules_output$module_1()()[["iris"]], iris) + testthat::expect_null(modules_output$module_1()) } ) }) - testthat::it("continues when transformer throws qenv error and returns unchanged data", { - testthat::skip("todo") - }) - testthat::it("upstream data change is updated on transformer fallback", { + testthat::it("pauses when transformer throws qenv error", { shiny::testServer( app = srv_teal, args = list( id = "test", - data = teal.data::teal_data(iris = iris, mtcars = mtcars), + data = teal.data::teal_data(iris = iris), modules = modules( module( label = "module_1", server = function(id, data) data, - transformers = transform_list[c("iris", "fail")] + transformers = list( + teal_transform_module( + ui = function(id) NULL, + server = function(id, data) { + reactive(within(data(), stop("my error"))) + } + ) + ) ) ) ), expr = { - session$setInputs("teal_modules-active_tab" = "module_1") - new_row_size <- 14 - session$setInputs("teal_modules-module_1-data_transform-transform_module-data-n" = new_row_size) - session$flushReact() - - testthat::expect_equal(nrow(modules_output$module_1()()[["iris"]]), new_row_size) + session$setInputs(`teal_modules-active_tab` = "module_1") + testthat::expect_null(modules_output$module_1()) } ) }) - testthat::it("upstream data change with double reactivity resolves with correct this/that", { + testthat::it("isn't called when `data` is not teal_data", { shiny::testServer( app = srv_teal, args = list( id = "test", - data = teal.data::teal_data(iris = iris, mtcars = mtcars), + data = teal.data::teal_data(iris = iris), modules = modules( module( label = "module_1", server = function(id, data) data, - transformers = transform_list[c("iris", "fail")] + transformers = list( + teal_transform_module( + ui = function(id) NULL, + server = function(id, data) { + reactive(data.frame()) + } + ) + ) ) ) ), expr = { - session$setInputs("teal_modules-active_tab" = "module_1") - - session$setInputs( - "teal_modules-module_1-data_transform-transform_module-data-n" = 12, - "teal_modules-module_1-data_transform-transform_module_1-data-add_error" = FALSE - ) - session$flushReact() - - testthat::expect_equal(nrow(modules_output$module_1()()[["iris"]]), 6) + session$setInputs(`teal_modules-active_tab` = "module_1") + testthat::expect_null(modules_output$module_1()) } ) }) - - testthat::it("continues when transformer throws qenv error and returns unchanged data") - - testthat::it("isn't called when `data` is not teal_data", { - testthat::skip("todo") - }) - # when reactive returned teal_data_module is not triggered (for example when button isn't clicked) }) testthat::describe("srv_teal summary table", { @@ -1746,7 +1739,6 @@ testthat::describe("srv_teal summary table", { ), expr = { session$setInputs("teal_modules-active_tab" = "module_1") - session$flushReact() testthat::expect_identical( module_summary_table(output, "module_1"), data.frame( @@ -1777,7 +1769,6 @@ testthat::describe("srv_teal summary table", { ), expr = { session$setInputs("teal_modules-active_tab" = "module_1") - session$flushReact() testthat::expect_identical( module_summary_table(output, "module_1"), data.frame( @@ -1810,7 +1801,6 @@ testthat::describe("srv_teal summary table", { ), expr = { session$setInputs("teal_modules-active_tab" = "module_1") - session$flushReact() testthat::expect_identical( module_summary_table(output, "module_1"), data.frame( @@ -1844,7 +1834,6 @@ testthat::describe("srv_teal summary table", { ), expr = { session$setInputs("teal_modules-active_tab" = "module_1") - session$flushReact() testthat::expect_identical( module_summary_table(output, "module_1"), data.frame( @@ -1879,7 +1868,6 @@ testthat::describe("srv_teal summary table", { ), expr = { session$setInputs("teal_modules-active_tab" = "module_1") - session$flushReact() testthat::expect_identical( module_summary_table(output, "module_1"), data.frame( @@ -1913,9 +1901,7 @@ testthat::describe("srv_teal summary table", { ), expr = { session$setInputs("teal_modules-active_tab" = "module_1") - slices_global$slices_set( - teal_slices(teal_slice("a", "name", selected = "a")) - ) + slices_global$slices_set(teal_slices(teal_slice("a", "name", selected = "a"))) session$flushReact() testthat::expect_identical( module_summary_table(output, "module_1"), @@ -1956,7 +1942,6 @@ testthat::describe("srv_teal summary table", { ), expr = { session$setInputs("teal_modules-active_tab" = "module_1") - session$flushReact() testthat::expect_identical( module_summary_table(output, "module_1"), data.frame( @@ -1986,7 +1971,6 @@ testthat::describe("srv_teal summary table", { ), expr = { session$setInputs("teal_modules-active_tab" = "module_1") - session$flushReact() testthat::expect_identical( module_summary_table(output, "module_1"), data.frame( @@ -2011,7 +1995,6 @@ testthat::describe("srv_teal summary table", { ), expr = { session$setInputs("teal_modules-active_tab" = "module_1") - session$flushReact() testthat::expect_identical( module_summary_table(output, "module_1"), data.frame( diff --git a/tests/testthat/test-shinytest2-filter_panel.R b/tests/testthat/test-shinytest2-filter_panel.R index aa65eb567d..22be061ab7 100644 --- a/tests/testthat/test-shinytest2-filter_panel.R +++ b/tests/testthat/test-shinytest2-filter_panel.R @@ -1,7 +1,7 @@ testthat::skip_if_not_installed("shinytest2") testthat::skip_if_not_installed("rvest") -testthat::test_that("e2e: module content is updated when a data is filtered in filter panel", { +testthat::test_that("e2e: module content is updated when data is filtered in filter panel", { skip_if_too_deep(5) app <- TealAppDriver$new( data = simple_teal_data(), diff --git a/tests/testthat/test-shinytest2-teal_data_module.R b/tests/testthat/test-shinytest2-teal_data_module.R index 9cba14c73c..7ba75f4788 100644 --- a/tests/testthat/test-shinytest2-teal_data_module.R +++ b/tests/testthat/test-shinytest2-teal_data_module.R @@ -31,7 +31,7 @@ testthat::test_that("e2e: teal_data_module will have a delayed load of datasets" modules = example_module(label = "Example Module") ) - app$click("teal-data-teal_data_module-data-submit") + app$click("teal-data-teal_data_module-submit") app$navigate_teal_tab("Example Module") testthat::expect_setequal(app$get_active_filter_vars(), c("dataset1", "dataset2")) @@ -67,7 +67,7 @@ testthat::test_that("e2e: teal_data_module shows validation errors", { modules = example_module(label = "Example Module") ) - app$click("teal-data-teal_data_module-data-submit") + app$click("teal-data-teal_data_module-submit") app$expect_validation_error() @@ -111,8 +111,8 @@ testthat::test_that("e2e: teal_data_module inputs change teal_data object that i modules = example_module(label = "Example Module") ) - app$set_input("teal-data-teal_data_module-data-new_column", "A_New_Column") - app$click("teal-data-teal_data_module-data-submit") + app$set_input("teal-data-teal_data_module-new_column", "A_New_Column") + app$click("teal-data-teal_data_module-submit") app$navigate_teal_tab("Example Module") # This may fail if teal_data_module does not perform the transformation @@ -157,14 +157,14 @@ testthat::test_that("e2e: teal_data_module gets removed after successful data lo modules = example_module(label = "Example Module") ) - submit <- "teal-data-teal_data_module-data-submit" + submit <- "teal-data-teal_data_module-submit" app$click(submit) - testthat::expect_false( - app$is_visible('#teal-teal_modules-active_tab a[data-value="teal_data_module"]') + testthat::expect_null( + app$get_html('#teal-teal_modules-active_tab a[data-value="teal_data_module"]') ) - testthat::expect_false( + testthat::expect_null( app$is_visible(sprintf("#%s", submit)) ) @@ -202,7 +202,7 @@ testthat::test_that("e2e: teal_data_module is still visible after successful dat modules = example_module(label = "Example Module") ) - app$click("teal-data-teal_data_module-data-submit") + app$click("teal-data-teal_data_module-submit") testthat::expect_true( app$is_visible('#teal-teal_modules-active_tab a[data-value="teal_data_module"]') @@ -256,7 +256,7 @@ testthat::test_that("e2e: teal_data_module will make other tabs inactive before c("disabled", "disabled") ) - app$click("teal-data-teal_data_module-data-submit") + app$click("teal-data-teal_data_module-submit") testthat::expect_true( is.na(