diff --git a/DESCRIPTION b/DESCRIPTION index 158ce9edd5..8854f6ebea 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -37,7 +37,7 @@ BugReports: https://github.com/insightsengineering/teal/issues Depends: R (>= 4.1), shiny (>= 1.8.1), - teal.data (>= 0.7.0), + teal.data (>= 0.7.0.9002), teal.slice (>= 0.6.0.9001) Imports: bsicons, @@ -52,8 +52,8 @@ Imports: rlang (>= 1.0.0), shinyjs, stats, - teal.code (>= 0.6.1), teal.logger (>= 0.3.2.9001), + teal.code (>= 0.6.1.9002), teal.reporter (>= 0.4.0.9004), teal.widgets (>= 0.4.3.9001), tools, @@ -76,11 +76,11 @@ Suggests: VignetteBuilder: knitr, rmarkdown -RdMacros: - lifecycle Remotes: + insightsengineering/teal.code@main, + insightsengineering/teal.data@main, insightsengineering/teal.logger@main, - insightsengineering/teal.reporter@main, + insightsengineering/teal.reporter@teal_reportable, insightsengineering/teal.slice@main, insightsengineering/teal.widgets@main Config/Needs/verdepcheck: rstudio/shiny, insightsengineering/teal.data, @@ -102,6 +102,7 @@ Roxygen: list(markdown = TRUE, packages = c("roxy.shinylive")) RoxygenNote: 7.3.2 Collate: 'TealAppDriver.R' + 'after.R' 'checkmate.R' 'dummy_functions.R' 'include_css_js.R' diff --git a/NAMESPACE b/NAMESPACE index ac53893f97..6a7d513ad6 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -15,9 +15,11 @@ S3method(ui_teal_module,teal_modules) S3method(within,teal_data_module) export(TealReportCard) export(add_landing_modal) +export(after) export(as.teal_slices) export(as_tdata) export(build_app_title) +export(disable_report) export(example_module) export(get_code_tdata) export(get_metadata) diff --git a/NEWS.md b/NEWS.md index b560e56863..aacb9ee589 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,12 @@ # teal 0.16.0.9009 +### New features + +* `init` and `srv_teal` have new `reporter` parameter, that allows to pre-define `teal.reporter::Reporter` object to be +used for storing the content of the report. You can also globally disable reporting by setting `reporter = NULL` +(and `disable = TRUE` in `ui_teal` for cases when `ui_teal` is used as shiny module). +* TODO: verify if we need to clone/deep_clone reporter in `srv_teal/init`. + # teal 0.16.0 ### New features diff --git a/R/after.R b/R/after.R new file mode 100644 index 0000000000..57ee83c72a --- /dev/null +++ b/R/after.R @@ -0,0 +1,91 @@ +#' Executes modifications to the result of a module +#' +#' Primarily used to modify the output object of module to change the containing +#' report. +#' @param x (`teal_data`) +#' @param ui (`function(id, elem, ...)`) function to receive output (`shiny.tag`) from `x$ui` +#' @param server (`function(input, output, session, data, ...)`) function to receive output data from `x$server` +#' @param ... additional argument passed to `ui` and `server` by matching their formals names. +#' @return A `teal_report` object with the result of the server function. +#' @export +after <- function(x, + ui = function(id, elem) elem, + server = function(input, output, session, data) data, + ...) { + # todo: make a method for teal_app and remove teal_extend_server? + checkmate::assert_multi_class(x, "teal_module") + if (!is.function(ui) || !all(names(formals(ui)) %in% c("id", "elem"))) { + stop("ui should be a function of id and elem") + } + if (!is.function(server) || !all(names(formals(server)) %in% c("input", "output", "session", "data"))) { + stop("server should be a function of `input` and `output`, `session`, `data`") + } + + additional_args <- list(...) + new_x <- x # because overwriting x$ui/server will cause infinite recursion + new_x$ui <- .after_ui(x$ui, ui, additional_args) + new_x$server <- .after_server(x$server, server, additional_args) + new_x +} + +.after_ui <- function(x, y, additional_args) { + # add `_`-prefix to make sure objects are not masked in the wrapper functions + `_x` <- x # nolint: object_name. + `_y` <- y # nolint: object_name. + new_x <- function(id, ...) { + original_args <- as.list(environment()) + if ("..." %in% names(formals(`_x`))) { + original_args <- c(original_args, list(...)) + } + ns <- NS(id) + original_args$id <- ns("wrapped") + original_out <- do.call(`_x`, original_args, quote = TRUE) + + wrapper_args <- c( + additional_args, + list(id = ns("wrapper"), elem = original_out) + ) + do.call(`_y`, args = wrapper_args[names(formals(`_y`))]) + } + formals(new_x) <- formals(x) + new_x +} + +.after_server <- function(x, y, additional_args) { + # add `_`-prefix to make sure objects are not masked in the wrapper functions + `_x` <- x # nolint: object_name. + `_y` <- y # nolint: object_name. + new_x <- function(id, ...) { + original_args <- as.list(environment()) + original_args$id <- "wrapped" + if ("..." %in% names(formals(`_x`))) { + original_args <- c(original_args, list(...)) + } + moduleServer(id, function(input, output, session) { + original_out <- if (all(c("input", "output", "session") %in% names(formals(`_x`)))) { + original_args$module <- `_x` + do.call(shiny::callModule, args = original_args) + } else { + do.call(`_x`, original_args) + } + original_out_r <- reactive( + if (is.reactive(original_out)) { + original_out() + } else { + original_out + } + ) + wrapper_args <- utils::modifyList( + additional_args, + list(id = "wrapper", input = input, output = output, session = session) + ) + reactive({ + req(original_out_r()) + wrapper_args$data <- original_out() + do.call(`_y`, wrapper_args[names(formals(`_y`))], quote = TRUE) + }) + }) + } + formals(new_x) <- formals(x) + new_x +} diff --git a/R/init.R b/R/init.R index c8d9265e22..8dcbc4892d 100644 --- a/R/init.R +++ b/R/init.R @@ -34,6 +34,7 @@ #' a string specifying the `shiny` module id in cases it is used as a `shiny` module #' rather than a standalone `shiny` app. #' This parameter is no longer supported. Use [ui_teal()] and [srv_teal()] instead. +#' @param reporter (`Reporter`) object used to store report contents. Set to `NULL` to globally disable reporting. #' #' @return Named list containing server and UI functions. #' @@ -99,7 +100,8 @@ init <- function(data, title = lifecycle::deprecated(), header = lifecycle::deprecated(), footer = lifecycle::deprecated(), - id = lifecycle::deprecated()) { + id = lifecycle::deprecated(), + reporter = teal.reporter::Reporter$new()) { logger::log_debug("init initializing teal app with: data ('{ class(data) }').") # argument checking (independent) @@ -183,7 +185,6 @@ init <- function(data, landing <- extract_module(modules, "teal_module_landing") modules <- drop_module(modules, "teal_module_landing") - if (lifecycle::is_present(id)) { lifecycle::deprecate_soft( when = "0.16.0", @@ -231,7 +232,13 @@ init <- function(data, ) }, server = function(input, output, session) { - srv_teal(id = "teal", data = data, modules = modules, filter = deep_copy_filter(filter)) + srv_teal( + id = "teal", + data = data, + modules = modules, + filter = deep_copy_filter(filter), + reporter = if (!is.null(reporter)) reporter$clone(deep = TRUE) + ) srv_session_info("teal-footer-session_info") } ), diff --git a/R/module_init_data.R b/R/module_init_data.R index 8ad8800ef9..0f065a523f 100644 --- a/R/module_init_data.R +++ b/R/module_init_data.R @@ -90,21 +90,31 @@ srv_init_data <- function(id, data) { #' @keywords internal .add_signature_to_data <- function(data) { hashes <- .get_hashes_code(data) + data_teal_report <- teal.reporter::as.teal_report(data) + if (!inherits(data, "teal_report")) { + teal.reporter::teal_card(data_teal_report) <- c( + teal.reporter::teal_card(), + "## Code preparation", + teal.reporter::teal_card(data_teal_report) + ) + } tdata <- do.call( - teal.data::teal_data, + teal.reporter::teal_report, c( - list(code = trimws(c(teal.code::get_code(data), hashes), which = "right")), - list(join_keys = teal.data::join_keys(data)), + list( + code = trimws(c(teal.code::get_code(data_teal_report), hashes), which = "right"), + join_keys = teal.data::join_keys(data_teal_report), + teal_card = teal.reporter::teal_card(data_teal_report) + ), sapply( - names(data), + names(data_teal_report), teal.code::get_var, - object = data, + object = data_teal_report, simplify = FALSE ) ) ) - - tdata@verified <- data@verified + tdata@verified <- data_teal_report@verified tdata } diff --git a/R/module_nested_tabs.R b/R/module_nested_tabs.R index 59e819a2a0..b791a817f1 100644 --- a/R/module_nested_tabs.R +++ b/R/module_nested_tabs.R @@ -118,6 +118,8 @@ ui_teal_module.teal_module <- function(id, modules, depth = 0L) { div( id = id, class = "teal_module", + ui_add_reporter(ns("add_reporter_wrapper")), + # uiOutput(ns("show_rcode_container")), # todo: same mechanism as for the reporter uiOutput(ns("data_reactive"), inline = TRUE), tagList( if (depth >= 2L) tags$div(), @@ -226,7 +228,7 @@ srv_teal_module <- function(id, checkmate::assert_multi_class(modules, c("teal_modules", "teal_module")) assert_reactive(datasets, null.ok = TRUE) checkmate::assert_class(slices_global, ".slicesGlobal") - checkmate::assert_class(reporter, "Reporter") + checkmate::assert_class(reporter, "Reporter", null.ok = TRUE) assert_reactive(data_load_status) UseMethod("srv_teal_module", modules) } @@ -395,21 +397,16 @@ srv_teal_module.teal_module <- function(id, }) # Call modules. - if (!inherits(modules, "teal_module_previewer")) { - obs_module <- .call_once_when( - !is.null(module_teal_data()), - ignoreNULL = TRUE, - handlerExpr = { - module_out(.call_teal_module(modules, datasets, module_teal_data, reporter)) - } - ) - } else { - # Report previewer must be initiated on app start for report cards to be included in bookmarks. - # When previewer is delayed, cards are bookmarked only if previewer has been initiated (visited). - module_out(.call_teal_module(modules, datasets, module_teal_data, reporter)) - } + obs_module <- .call_once_when( + !is.null(module_teal_data()), + ignoreNULL = TRUE, + handlerExpr = { + out <- .call_teal_module(modules, datasets, module_teal_data, reporter) + srv_add_reporter("add_reporter_wrapper", out, reporter) + module_out(out) + } + ) }) - module_out }) } @@ -420,7 +417,7 @@ srv_teal_module.teal_module <- function(id, # collect arguments to run teal_module args <- c(list(id = "module"), modules$server_args) - if (is_arg_used(modules$server, "reporter")) { + if (is_arg_used(modules$server, "reporter") && !is.null(reporter)) { args <- c(args, list(reporter = reporter)) } diff --git a/R/module_snapshot_manager.R b/R/module_snapshot_manager.R index 321d7a8af0..b521450363 100644 --- a/R/module_snapshot_manager.R +++ b/R/module_snapshot_manager.R @@ -171,6 +171,12 @@ srv_snapshot_manager <- function(id, slices_global) { showModal( modalDialog( textInput(ns("snapshot_name"), "Name the snapshot", width = "100%", placeholder = "Meaningful, unique name"), + tags$script( + shiny::HTML( + sprintf("shinyjs.autoFocusModal('%s');", ns("snapshot_name")), + sprintf("shinyjs.enterToSubmit('%s', '%s');", ns("snapshot_name"), ns("snapshot_name_accept")) + ) + ), footer = tagList( actionButton(ns("snapshot_name_accept"), "Accept", icon = icon("far fa-thumbs-up")), modalButton(label = "Cancel", icon = icon("far fa-thumbs-down")) diff --git a/R/module_teal.R b/R/module_teal.R index 79218ff9a8..f76fc80c07 100644 --- a/R/module_teal.R +++ b/R/module_teal.R @@ -50,9 +50,6 @@ ui_teal <- function(id, modules) { checkmate::assert_character(id, max.len = 1, any.missing = FALSE) checkmate::assert_class(modules, "teal_modules") ns <- NS(id) - - modules <- append_reporter_module(modules) - # show busy icon when `shiny` session is busy computing stuff # based on https://stackoverflow.com/questions/17325521/r-shiny-display-loading-message-while-function-is-running/22475216#22475216 # nolint: line_length. shiny_busy_message_panel <- conditionalPanel( @@ -103,14 +100,14 @@ ui_teal <- function(id, modules) { #' @rdname module_teal #' @export -srv_teal <- function(id, data, modules, filter = teal_slices()) { +srv_teal <- function(id, data, modules, filter = teal_slices(), reporter = teal.reporter::Reporter$new()) { checkmate::assert_character(id, max.len = 1, any.missing = FALSE) checkmate::assert_multi_class(data, c("teal_data", "teal_data_module", "reactive")) checkmate::assert_class(modules, "teal_modules") checkmate::assert_class(filter, "teal_slices") - - modules <- append_reporter_module(modules) - + if (!is.null(reporter)) { + modules <- append_reporter_module(modules) + } moduleServer(id, function(input, output, session) { logger::log_debug("srv_teal initializing.") @@ -148,7 +145,6 @@ srv_teal <- function(id, data, modules, filter = teal_slices()) { srv_check_module_datanames("datanames_warning", data_handled, modules) data_validated <- .trigger_on_success(data_handled) - data_signatured <- reactive({ req(inherits(data_validated(), "teal_data")) is_filter_ok <- check_filter_datanames(filter, names(data_validated())) @@ -181,8 +177,6 @@ srv_teal <- function(id, data, modules, filter = teal_slices()) { }) } - - if (inherits(data, "teal_data_module")) { setBookmarkExclude(c("teal_modules-active_tab")) bslib::nav_insert( @@ -215,18 +209,26 @@ srv_teal <- function(id, data, modules, filter = teal_slices()) { ) } - reporter <- teal.reporter::Reporter$new()$set_id(attr(filter, "app_id")) - module_labels <- unlist(module_labels(modules), use.names = FALSE) + module_labels <- unlist(module_labels(drop_module(modules, "teal_module_previewer")), use.names = FALSE) + slices_global <- methods::new(".slicesGlobal", filter, module_labels) modules_output <- srv_teal_module( id = "teal_modules", data = data_signatured, datasets = datasets_rv, - modules = modules, + modules = drop_module(modules, "teal_module_previewer"), slices_global = slices_global, data_load_status = data_load_status, reporter = reporter ) + + insert_reporter_previewer_tab( + session = session, + modules = modules, + modules_output = modules_output, + reporter = reporter, + app_id = attr(filter, "app_id") + ) 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) diff --git a/R/modules.R b/R/modules.R index 92cb31227c..7d6da35fae 100644 --- a/R/modules.R +++ b/R/modules.R @@ -649,7 +649,7 @@ append_module <- function(modules, module) { #' @keywords internal #' @noRd append_reporter_module <- function(modules) { - if (is_arg_used(modules, "reporter") && length(extract_module(modules, "teal_module_previewer")) == 0) { + if (length(extract_module(modules, "teal_module_previewer")) == 0) { modules <- append_module( modules, reporter_previewer_module(server_args = list(previewer_buttons = c("download", "reset"))) diff --git a/R/reporter_previewer_module.R b/R/reporter_previewer_module.R index 19b707b34f..2d557c8dff 100644 --- a/R/reporter_previewer_module.R +++ b/R/reporter_previewer_module.R @@ -46,3 +46,56 @@ reporter_previewer_module <- function(label = "Report previewer", server_args = attr(module, "teal_bookmarkable") <- TRUE module } + +#' Reporter previewer tab +#' +#' Creates navigation for reporter previewer in main tab of the teal UI +#' @noRd +insert_reporter_previewer_tab <- function(session, modules, modules_output, reporter, app_id) { + if (is.null(reporter)) { + return(FALSE) + } + reporter$set_id(app_id) + reporter_module <- extract_module(modules, "teal_module_previewer")[[1]] + modules <- drop_module(modules, "teal_module_previewer") + + previewer_out <- do.call( + reporter_module$server, + args = c(list(id = "report_previewer", reporter = reporter), reporter_module$server_args) + ) + previewer_ui <- do.call( + reporter_module$ui, + args = c(list(id = session$ns("report_previewer")), reporter_module$ui_args) + ) + + # Report Previewer tab needs to be shown only if any module has a reporter functionality + returns_teal_report <- function(x) { + if (is.reactive(x)) { + returns_teal_report(tryCatch(x(), error = function(e) e)) + } else if (inherits(x, "teal_report")) { + TRUE + } else if (is.list(x)) { + any(unlist(sapply(x, returns_teal_report))) + } else { + FALSE + } + } + any_use_reporter <- reactiveVal(FALSE) + observeEvent(returns_teal_report(modules_output), { + if ((is_arg_used(modules, "reporter") || returns_teal_report(modules_output)) && !isTRUE(any_use_reporter())) { + any_use_reporter(TRUE) + } + }) + + observeEvent(any_use_reporter(), { + if (any_use_reporter()) { + bslib::nav_insert( + id = "teal_modules-active_tab", + nav = bslib::nav_panel(title = reporter_module$label, previewer_ui), + session = session + ) + } + }) + + previewer_out # returned for testing +} diff --git a/R/teal_data_module-eval_code.R b/R/teal_data_module-eval_code.R index fa95ec9f4b..831f304302 100644 --- a/R/teal_data_module-eval_code.R +++ b/R/teal_data_module-eval_code.R @@ -18,14 +18,13 @@ setOldClass("teal_data_module") #' @include teal_data_module.R #' @name eval_code #' @rdname teal_data_module -#' @aliases eval_code,teal_data_module,character-method -#' @aliases eval_code,teal_data_module,language-method -#' @aliases eval_code,teal_data_module,expression-method +#' @aliases eval_code,teal_data_module +#' @aliases \S4method{eval_code}{teal_data_module} #' #' @importFrom methods setMethod #' @importMethodsFrom teal.code eval_code #' -setMethod("eval_code", signature = c("teal_data_module", "character"), function(object, code) { +setMethod("eval_code", signature = c(object = "teal_data_module"), function(object, code) { teal_data_module( ui = function(id) { ns <- NS(id) @@ -49,11 +48,3 @@ setMethod("eval_code", signature = c("teal_data_module", "character"), function( } ) }) - -setMethod("eval_code", signature = c("teal_data_module", "language"), function(object, code) { - eval_code(object, code = paste(lang2calls(code), collapse = "\n")) -}) - -setMethod("eval_code", signature = c("teal_data_module", "expression"), function(object, code) { - eval_code(object, code = paste(lang2calls(code), collapse = "\n")) -}) diff --git a/R/teal_data_utils.R b/R/teal_data_utils.R index 2b5b51c8b6..eaaecfb071 100644 --- a/R/teal_data_utils.R +++ b/R/teal_data_utils.R @@ -19,8 +19,15 @@ NULL #' @rdname teal_data_utilities .append_evaluated_code <- function(data, code) { checkmate::assert_class(data, "teal_data") - data@code <- c(data@code, code2list(code)) - methods::validObject(data) + if (length(code) && !identical(code, "")) { + data@code <- c(data@code, code2list(code)) + teal.reporter::teal_card(data) <- c( + teal.reporter::teal_card(data), + "## Data filtering", + teal.reporter::code_chunk(code) + ) + methods::validObject(data) + } data } @@ -33,3 +40,31 @@ NULL data@.xData <- new_env data } + +#' @rdname teal_data_utilities +.collapse_subsequent_chunks <- function(report) { + Reduce( + function(x, this) { + l <- length(x) + if ( + l && + inherits(x[[l]], "code_chunk") && + inherits(this, "code_chunk") && + identical(attr(x[[l]], "params"), attr(this, "params")) + ) { + x[[length(x)]] <- do.call( + teal.reporter::code_chunk, + args = c( + list(code = paste(x[[l]], this, sep = "\n")), + attr(x[[l]], "params") + ) + ) + x + } else { + c(x, this) + } + }, + init = teal.reporter::teal_card(), + x = report + ) +} diff --git a/R/teal_reporter.R b/R/teal_reporter.R index 48fd5dd71d..09358a1887 100644 --- a/R/teal_reporter.R +++ b/R/teal_reporter.R @@ -22,14 +22,7 @@ TealReportCard <- R6::R6Class( # nolint: object_name. #' ) #' card$get_content()[[1]]$get_content() append_src = function(src, ...) { - checkmate::assert_character(src, min.len = 0, max.len = 1) - params <- list(...) - params$eval <- FALSE - rblock <- RcodeBlock$new(src) - rblock$set_params(params) - self$append_content(rblock) - self$append_metadata("SRC", src) - invisible(self) + super$append_rcode(text = src, ...) }, #' @description Appends the filter state list to the `content` and `metadata` of this `TealReportCard`. #' If the filter state list has an attribute named `formatted`, it appends it to the card otherwise it uses @@ -40,11 +33,8 @@ TealReportCard <- R6::R6Class( # nolint: object_name. #' @return `self`, invisibly. append_fs = function(fs) { checkmate::assert_class(fs, "teal_slices") - self$append_text("Filter State", "header3") - if (length(fs)) { - self$append_content(TealSlicesBlock$new(fs)) - } else { - self$append_text("No filters specified.") + if (length(fs) > 0) { + self$append_content(code_chunk(.teal_slice_to_yaml(fs), eval = FALSE, lang = "verbatim")) } invisible(self) }, @@ -85,112 +75,109 @@ TealReportCard <- R6::R6Class( # nolint: object_name. ) ) -#' @title `TealSlicesBlock` -#' @docType class -#' @description -#' Specialized `TealSlicesBlock` block for managing filter panel content in reports. -#' @keywords internal -TealSlicesBlock <- R6::R6Class( # nolint: object_name_linter. - classname = "TealSlicesBlock", - inherit = teal.reporter:::TextBlock, - public = list( - #' @description Returns a `TealSlicesBlock` object. - #' - #' @details Returns a `TealSlicesBlock` object with no content and no parameters. - #' - #' @param content (`teal_slices`) object returned from [teal_slices()] function. - #' @param style (`character(1)`) string specifying style to apply. - #' - #' @return Object of class `TealSlicesBlock`, invisibly. - #' - initialize = function(content = teal_slices(), style = "verbatim") { - self$set_content(content) - self$set_style(style) - invisible(self) - }, +.teal_slice_to_yaml <- function(fs) { + checkmate::assert_class(fs, "teal_slices") + states_list <- lapply(fs, function(x) { + x_list <- shiny::isolate(as.list(x)) + if ( + inherits(x_list$choices, c("integer", "numeric", "Date", "POSIXct", "POSIXlt")) && + length(x_list$choices) == 2 && + length(x_list$selected) == 2 + ) { + x_list$range <- paste(x_list$selected, collapse = " - ") + x_list["selected"] <- NULL + } + if (!is.null(x_list$arg)) { + x_list$arg <- if (x_list$arg == "subset") "Genes" else "Samples" + } - #' @description Sets content of this `TealSlicesBlock`. - #' Sets content as `YAML` text which represents a list generated from `teal_slices`. - #' The list displays limited number of fields from `teal_slice` objects, but this list is - #' sufficient to conclude which filters were applied. - #' When `selected` field in `teal_slice` object is a range, then it is displayed as a "min" - #' - #' - #' @param content (`teal_slices`) object returned from [teal_slices()] function. - #' @return `self`, invisibly. - set_content = function(content) { - checkmate::assert_class(content, "teal_slices") - if (length(content) != 0) { - states_list <- lapply(content, function(x) { - x_list <- shiny::isolate(as.list(x)) - if ( - inherits(x_list$choices, c("integer", "numeric", "Date", "POSIXct", "POSIXlt")) && - length(x_list$choices) == 2 && - length(x_list$selected) == 2 - ) { - x_list$range <- paste(x_list$selected, collapse = " - ") - x_list["selected"] <- NULL - } - if (!is.null(x_list$arg)) { - x_list$arg <- if (x_list$arg == "subset") "Genes" else "Samples" - } + x_list <- x_list[ + c("dataname", "varname", "experiment", "arg", "expr", "selected", "range", "keep_na", "keep_inf") + ] + names(x_list) <- c( + "Dataset name", "Variable name", "Experiment", "Filtering by", "Applied expression", + "Selected Values", "Selected range", "Include NA values", "Include Inf values" + ) - x_list <- x_list[ - c("dataname", "varname", "experiment", "arg", "expr", "selected", "range", "keep_na", "keep_inf") - ] - names(x_list) <- c( - "Dataset name", "Variable name", "Experiment", "Filtering by", "Applied expression", - "Selected Values", "Selected range", "Include NA values", "Include Inf values" - ) + Filter(Negate(is.null), x_list) + }) - Filter(Negate(is.null), x_list) - }) + if (requireNamespace("yaml", quietly = TRUE)) { + yaml::as.yaml(states_list) + } else { + stop("yaml package is required to format the filter state list") + } +} - if (requireNamespace("yaml", quietly = TRUE)) { - super$set_content(yaml::as.yaml(states_list)) - } else { - stop("yaml package is required to format the filter state list") - } +#' @noRd +ui_add_reporter <- function(id) uiOutput(NS(id, "reporter_add_container")) + +#' @noRd +srv_add_reporter <- function(id, module_out, reporter) { + if (is.null(reporter)) { + return(FALSE) + } # early exit + moduleServer(id, function(input, output, session) { + mod_out_r <- reactive({ + req(module_out) + if (is.reactive(module_out)) { + module_out() } - private$teal_slices <- content - invisible(self) - }, - #' @description Create the `TealSlicesBlock` from a list. - #' - #' @param x (`named list`) with two fields `text` and `style`. - #' Use the `get_available_styles` method to get all possible styles. - #' - #' @return `self`, invisibly. - #' @examples - #' TealSlicesBlock <- getFromNamespace("TealSlicesBlock", "teal") - #' block <- TealSlicesBlock$new() - #' block$from_list(list(text = "sth", style = "default")) - #' - from_list = function(x) { - checkmate::assert_list(x) - checkmate::assert_names(names(x), must.include = c("text", "style")) - super$set_content(x$text) - super$set_style(x$style) - invisible(self) - }, - #' @description Convert the `TealSlicesBlock` to a list. - #' - #' @return `named list` with a text and style. - #' @examples - #' TealSlicesBlock <- getFromNamespace("TealSlicesBlock", "teal") - #' block <- TealSlicesBlock$new() - #' block$to_list() - #' - to_list = function() { - content <- self$get_content() - list( - text = if (length(content)) content else "", - style = self$get_style() - ) - } - ), - private = list( - style = "verbatim", - teal_slices = NULL # teal_slices - ) -) + }) + + doc_out <- reactive({ + req(mod_out_r()) + teal_data_handled <- tryCatch(mod_out_r(), error = function(e) e) + tcard <- if (inherits(teal_data_handled, "teal_report")) { + teal.reporter::teal_card(teal_data_handled) + } else if (inherits(teal_data_handled, "teal_data")) { + teal.reporter::teal_card(teal.reporter::as.teal_report(teal_data_handled)) + } else if (inherits(teal_data_handled, "teal_card")) { + teal_data_handled + } + + if (length(tcard)) .collapse_subsequent_chunks(tcard) + }) + + .call_once_when(!is.null(doc_out()), { + output$reporter_add_container <- renderUI({ + tags$div( + class = "teal add-reporter-container", + teal.reporter::add_card_button_ui(session$ns("reporter_add")) + ) + }) + teal.reporter::add_card_button_srv("reporter_add", reporter = reporter, card_fun = doc_out) + }) + + + + observeEvent(doc_out(), ignoreNULL = FALSE, { + shinyjs::toggleState("reporter_add_container", condition = inherits(doc_out(), "teal_card")) + }) + }) +} + +#' Disable the report for a `teal_module` +#' +#' Convenience function that disables the user's ability to add the module +#' to the report previewer. +#' @param x (`teal_module`) a `teal_module` object. +#' @return `NULL` that indicates that it should disable the reporter functionality. +#' @export +#' @examples +#' app <- init( +#' data = within(teal_data(), iris <- iris), +#' modules = modules( +#' example_module(label = "example teal module") |> disable_report() +#' ) +#' ) +#' if (interactive()) { +#' shinyApp(app$ui, app$server) +#' } +disable_report <- function(x) { + checkmate::assert_class(x, "teal_module") + after(x, server = function(data) { + teal.reporter::teal_card(data) <- teal.reporter::teal_card() + NULL + }) +} diff --git a/R/zzz.R b/R/zzz.R index 62c8029561..a4c91329fd 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -31,8 +31,6 @@ setdiff_teal_slices <- getFromNamespace("setdiff_teal_slices", "teal.slice") # This one is here because it is needed by c.teal_slices but we don't want it exported from teal.slice. coalesce_r <- getFromNamespace("coalesce_r", "teal.slice") -# all *Block objects are private in teal.reporter -RcodeBlock <- getFromNamespace("RcodeBlock", "teal.reporter") # nolint: object_name. # Use non-exported function(s) from teal.code # This one is here because lang2calls should not be exported from teal.code diff --git a/_pkgdown.yml b/_pkgdown.yml index 468399619f..f0bc2d1578 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -130,6 +130,7 @@ reference: - reporter_previewer_module - TealReportCard - report_card_template + - disable_report - title: Landing popup contents: - landing_popup_module diff --git a/inst/css/custom.css b/inst/css/custom.css index 18386de7f5..d28ff4044d 100644 --- a/inst/css/custom.css +++ b/inst/css/custom.css @@ -53,3 +53,9 @@ body > div:has(~ #shiny-modal-wrapper .blur_background) { .teal-body .bslib-mb-spacing { --bslib-mb-spacer: 0; } + +.teal.add-reporter-container { + display: flex; + justify-content: flex-end; + padding: 10px; +} diff --git a/inst/js/init.js b/inst/js/init.js index 69e9b7a27b..49eb4b9e2a 100644 --- a/inst/js/init.js +++ b/inst/js/init.js @@ -3,3 +3,23 @@ // this code alows the show R code "copy to clipbaord" button to work var clipboard = new ClipboardJS(".btn[data-clipboard-target]"); + +shinyjs.autoFocusModal = function(id) { + document.getElementById('shiny-modal').addEventListener( + 'shown.bs.modal', + () => document.getElementById(id).focus(), + { once: true } + ); +} + +shinyjs.enterToSubmit = function(id, submit_id) { + document.getElementById('shiny-modal').addEventListener( + 'shown.bs.modal', + () => document.getElementById(id).addEventListener('keyup', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); // prevent form submission + document.getElementById(submit_id).click(); + } + }) + ); +} diff --git a/man/TealReportCard.Rd b/man/TealReportCard.Rd index 061dbb73ba..85d4d2208b 100644 --- a/man/TealReportCard.Rd +++ b/man/TealReportCard.Rd @@ -57,6 +57,7 @@ card$get_content()[[1]]$get_content()
teal.reporter::ReportCard$get_name()
teal.reporter::ReportCard$initialize()
teal.reporter::ReportCard$reset()
teal.reporter::ReportCard$set_content_names()
teal.reporter::ReportCard$set_name()
teal.reporter::ReportCard$to_list()