diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cdab3ba28..81818efe0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,6 +20,7 @@ repos: - mirai - checkmate - cli + - htmltools - jsonlite - lifecycle - logger diff --git a/DESCRIPTION b/DESCRIPTION index 9f676084d..7c8a508ff 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -35,13 +35,14 @@ URL: https://insightsengineering.github.io/teal/, https://github.com/insightsengineering/teal/ BugReports: https://github.com/insightsengineering/teal/issues Depends: - R (>= 4.0), + R (>= 4.1), shiny (>= 1.8.1), teal.data (>= 0.6.0.9017), teal.slice (>= 0.5.1.9015) Imports: checkmate (>= 2.1.0), cli, + htmltools, jsonlite, lifecycle (>= 0.2.0), logger (>= 0.2.0), @@ -84,7 +85,7 @@ Config/Needs/verdepcheck: rstudio/shiny, insightsengineering/teal.data, rstudio/bslib, yihui/knitr, bioc::MultiAssayExperiment, r-lib/R6, rstudio/rmarkdown, tidyverse/rvest, rstudio/shinytest2, rstudio/shinyvalidate, r-lib/testthat, r-lib/withr, - yaml=vubiostat/r-yaml + yaml=vubiostat/r-yaml, rstudio/htmltools Config/Needs/website: insightsengineering/nesttemplate Encoding: UTF-8 Language: en-US @@ -106,6 +107,7 @@ Collate: 'module_filter_manager.R' 'module_init_data.R' 'module_nested_tabs.R' + 'module_session_info.R' 'module_snapshot_manager.R' 'module_teal.R' 'module_teal_data.R' @@ -120,6 +122,7 @@ Collate: 'teal_data_module-eval_code.R' 'teal_data_module-within.R' 'teal_data_utils.R' + 'teal_modifiers.R' 'teal_reporter.R' 'teal_slices-store.R' 'teal_slices.R' diff --git a/NAMESPACE b/NAMESPACE index b0124abc1..ac53893f9 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -14,6 +14,7 @@ S3method(ui_teal_module,teal_module) S3method(ui_teal_module,teal_modules) S3method(within,teal_data_module) export(TealReportCard) +export(add_landing_modal) export(as.teal_slices) export(as_tdata) export(build_app_title) @@ -23,12 +24,16 @@ export(get_metadata) export(init) export(landing_popup_module) export(make_teal_transform_server) +export(modify_footer) +export(modify_header) +export(modify_title) export(module) export(modules) export(new_tdata) export(report_card_template) export(reporter_previewer_module) export(show_rcode_modal) +export(srv_session_info) export(srv_teal) export(srv_teal_with_splash) export(srv_transform_teal_data) @@ -36,6 +41,7 @@ export(tdata2env) export(teal_data_module) export(teal_slices) export(teal_transform_module) +export(ui_session_info) export(ui_teal) export(ui_teal_with_splash) export(ui_transform_teal_data) diff --git a/NEWS.md b/NEWS.md index fec1a3a7c..9eaf0d024 100644 --- a/NEWS.md +++ b/NEWS.md @@ -4,18 +4,22 @@ * Possible to call `ui_teal` and `srv_teal` directly in any application by delivering `data` argument as a `reactive` returning `teal_data` object. #669 * Since introduction of `ui_teal` and `srv_teal` functions `id` argument in `init` is being deprecated. #1438 +* Introduce `ui_session_info` and `srv_session_info` shiny module to create the user session info and teal app lockfile lockfile download button. * Introduced `teal_transform_module` to provide a way to interactively modify data delivered to `teal_module`'s `server` and to decorate module outputs. #1228 #1384 * Introduced a new argument `once = FALSE` in `teal_data_module` to possibly reload data during a run time. * Possibility to download lockfile to restore app session for reproducibility. #479 * Datasets which name starts with `.` are ignored when `module`'s `datanames` is set as `"all"`. * Added warning when reserved `datanames`, such as `all` and `.raw_data` are being used. +* Added `add_custom_server()` to allow adding custom server logic to the main shiny server function of a teal app. ### Breaking changes * Setting `datanames()` on `data` passed to teal application no longer has effect. In order to change `teal_module`'s `datanames` one should modify `module$datanames`. -* The `landing_popup_module()` needs to be passed as the `landing_popup` argument of `init` instead of being passed as a module of the `modules` argument of `init`. +* `landing_popup_module()` is deprecated. Please use `add_landing_modal()` function to add a landing popup for your teal application. * `teal` no longer re-export `%>%`. Please load `library(magrittr)` instead or use `|>` from `base`. +* `build_app_title` will be removed in the future release. Please use the `modify_title()` function to change the title for your teal application. +* The `title`, `header`, and `footer` arguments of the `init()` function are deprecated. Please use the `modify_title`, `modify_header`, and `modify_footer` respectively. ### Enhancement diff --git a/R/TealAppDriver.R b/R/TealAppDriver.R index f170e515a..af78b8759 100644 --- a/R/TealAppDriver.R +++ b/R/TealAppDriver.R @@ -26,7 +26,8 @@ TealAppDriver <- R6::R6Class( # nolint: object_name. #' @description #' Initialize a `TealAppDriver` object for testing a `teal` application. #' - #' @param data,modules,filter,title,header,footer,landing_popup arguments passed to `init` + #' @param data,modules,filter arguments passed to `init` + #' @param title_args,header,footer,landing_popup_args to pass into the modifier functions. #' @param timeout (`numeric`) Default number of milliseconds for any timeout or #' timeout_ parameter in the `TealAppDriver` class. #' Defaults to 20s. @@ -45,25 +46,51 @@ TealAppDriver <- R6::R6Class( # nolint: object_name. initialize = function(data, modules, filter = teal_slices(), - title = build_app_title(), + title_args = list(), header = tags$p(), footer = tags$p(), - landing_popup = NULL, + landing_popup_args = NULL, timeout = rlang::missing_arg(), load_timeout = rlang::missing_arg(), ...) { private$data <- data private$modules <- modules private$filter <- filter + + new_title <- modifyList( + list( + title = "Custom Teal App Title", + favicon = "https://raw.githubusercontent.com/insightsengineering/hex-stickers/main/PNG/teal.png" + ), + title_args + ) app <- init( data = data, modules = modules, - filter = filter, - title = title, - header = header, - footer = footer, - landing_popup = landing_popup, - ) + filter = filter + ) |> + modify_title(title = new_title$title, favicon = new_title$favicon) |> + modify_header(header) |> + modify_footer(footer) + + if (!is.null(landing_popup_args)) { + default_args <- list( + title = NULL, + content = NULL, + footer = modalButton("Accept") + ) + landing_popup_args[names(default_args)] <- Map( + function(x, y) if (is.null(y)) x else y, + default_args, + landing_popup_args[names(default_args)] + ) + app <- add_landing_modal( + app, + title = landing_popup_args$title, + content = landing_popup_args$content, + footer = landing_popup_args$footer + ) + } # Default timeout is hardcoded to 4s in shinytest2:::resolve_timeout # It must be set as parameter to the AppDriver diff --git a/R/init.R b/R/init.R index ddc2f0c44..417d0e5c4 100644 --- a/R/init.R +++ b/R/init.R @@ -19,21 +19,25 @@ #' more details. #' @param filter (`teal_slices`) Optionally, #' specifies the initial filter using [teal_slices()]. -#' @param title (`shiny.tag` or `character(1)`) Optionally, +#' @param title (`shiny.tag` or `character(1)`) `r lifecycle::badge("deprecated")` Optionally, #' the browser window title. Defaults to a title "teal app" with the icon of NEST. #' Can be created using the `build_app_title()` or #' by passing a valid `shiny.tag` which is a head tag with title and link tag. -#' @param header (`shiny.tag` or `character(1)`) Optionally, +#' This parameter is deprecated. Use `modify_title()` on the teal app object instead. +#' @param header (`shiny.tag` or `character(1)`) `r lifecycle::badge("deprecated")` Optionally, #' the header of the app. -#' @param footer (`shiny.tag` or `character(1)`) Optionally, +#' This parameter is deprecated. Use `modify_header()` on the teal app object instead. +#' @param footer (`shiny.tag` or `character(1)`) `r lifecycle::badge("deprecated")` Optionally, #' the footer of the app. +#' This parameter is deprecated. Use `modify_footer()` on the teal app object instead. +#' @param id `r lifecycle::badge("deprecated")` (`character`) Optionally, +#' a string specifying the `shiny` module id in cases it is used as a `shiny` module +#' rather than a standalone `shiny` app. This is a legacy feature. Deprecated since v0.15.3 +#' please use [ui_teal()] and [srv_teal()] instead. #' @param id `r lifecycle::badge("deprecated")` (`character`) Optionally, #' a string specifying the `shiny` module id in cases it is used as a `shiny` module #' rather than a standalone `shiny` app. This is a legacy feature. Deprecated since v0.15.3 #' please use [ui_teal()] and [srv_teal()] instead. -#' -#' @param landing_popup (`teal_module_landing`) Optionally, -#' a `landing_popup_module` to show up as soon as the teal app is initialized. #' #' @return Named list containing server and UI functions. #' @@ -83,10 +87,7 @@ #' `Iris Sepal.Length histogram` = "new_iris Species", #' global_filters = "new_mtcars cyl" #' ) -#' ), -#' title = "App title", -#' header = tags$h1("Sample App"), -#' footer = tags$p("Sample footer") +#' ) #' ) #' if (interactive()) { #' shinyApp(app$ui, app$server) @@ -95,17 +96,15 @@ init <- function(data, modules, filter = teal_slices(), - title = build_app_title(), - header = tags$p(), - footer = tags$p(), - id = lifecycle::deprecated(), - landing_popup = NULL) { + title = lifecycle::deprecated(), + header = lifecycle::deprecated(), + footer = lifecycle::deprecated(), + id = lifecycle::deprecated()) { logger::log_debug("init initializing teal app with: data ('{ class(data) }').") # argument checking (independent) ## `data` checkmate::assert_multi_class(data, c("teal_data", "teal_data_module")) - checkmate::assert_class(landing_popup, "teal_module_landing", null.ok = TRUE) ## `modules` checkmate::assert( @@ -123,44 +122,9 @@ init <- function(data, ## `filter` checkmate::assert_class(filter, "teal_slices") - ## all other arguments - checkmate::assert( - .var.name = "title", - checkmate::check_string(title), - checkmate::check_multi_class(title, c("shiny.tag", "shiny.tag.list", "html")) - ) - checkmate::assert( - .var.name = "header", - checkmate::check_string(header), - checkmate::check_multi_class(header, c("shiny.tag", "shiny.tag.list", "html")) - ) - checkmate::assert( - .var.name = "footer", - checkmate::check_string(footer), - checkmate::check_multi_class(footer, c("shiny.tag", "shiny.tag.list", "html")) - ) - # log teal.logger::log_system_info() - # argument transformations - ## `modules` - landing module - landing <- extract_module(modules, "teal_module_landing") - if (length(landing) == 1L) { - landing_popup <- landing[[1L]] - modules <- drop_module(modules, "teal_module_landing") - lifecycle::deprecate_soft( - when = "0.15.3", - what = "landing_popup_module()", - details = paste( - "Pass `landing_popup_module` to the `landing_popup` argument of the `init` ", - "instead of wrapping it into `modules()` and passing to the `modules` argument" - ) - ) - } else if (length(landing) > 1L) { - stop("Only one `landing_popup_module` can be used.") - } - ## `filter` - set app_id attribute unless present (when restoring bookmark) if (is.null(attr(filter, "app_id", exact = TRUE))) attr(filter, "app_id") <- create_app_id(data, modules) @@ -222,6 +186,11 @@ init <- function(data, ) } + # argument transformations + ## `modules` - landing module + landing <- extract_module(modules, "teal_module_landing") + modules <- drop_module(modules, "teal_module_landing") + if (lifecycle::is_present(id)) { lifecycle::deprecate_soft( @@ -237,25 +206,94 @@ init <- function(data, id <- character(0) } ns <- NS(id) + # Note: UI must be a function to support bookmarking. - res <- list( - ui = function(request) { - ui_teal( - id = ns("teal"), - modules = modules, - title = title, - header = header, - footer = footer - ) - }, - server = function(input, output, session) { - if (!is.null(landing_popup)) { - do.call(landing_popup$server, c(list(id = "landing_module_shiny_id"), landing_popup$server_args)) + res <- structure( + list( + ui = function(request) { + fluidPage( + title = tags$div( + id = "teal-app-title", + tags$head( + tags$title("teal app"), + tags$link( + rel = "icon", + href = "https://raw.githubusercontent.com/insightsengineering/hex-stickers/main/PNG/nest.png", + sizes = "any" + ) + ) + ), + tags$header( + id = "teal-header", + tags$div(id = "teal-header-content") + ), + ui_teal( + id = "teal", + modules = modules + ), + tags$footer( + id = "teal-footer", + tags$div(id = "teal-footer-content"), + ui_session_info("teal-footer-session_info") + ) + ) + }, + server = function(input, output, session) { + srv_teal(id = "teal", data = data, modules = modules, filter = deep_copy_filter(filter)) + srv_session_info("teal-footer-session_info") } - srv_teal(id = ns("teal"), data = data, modules = modules, filter = deep_copy_filter(filter)) - } + ), + class = "teal_app" ) + if (lifecycle::is_present(title)) { + checkmate::assert_multi_class(title, c("shiny.tag", "shiny.tag.list", "html", "character")) + lifecycle::deprecate_warn( + when = "0.15.3", + what = "init(title)", + details = "Use `modify_title()` on the teal app object instead." + ) + res <- modify_title(res, title) + } + if (lifecycle::is_present(header)) { + checkmate::assert_multi_class(header, c("shiny.tag", "shiny.tag.list", "html", "character")) + lifecycle::deprecate_warn( + when = "0.15.3", + what = "init(header)", + details = paste( + "Use `modify_header()` on the teal app object instead." + ) + ) + res <- modify_header(res, header) + } + if (lifecycle::is_present(footer)) { + checkmate::assert_multi_class(footer, c("shiny.tag", "shiny.tag.list", "html", "character")) + lifecycle::deprecate_warn( + when = "0.15.3", + what = "init(footer)", + details = paste( + "Use `modify_footer()` on the teal app object instead." + ) + ) + res <- modify_footer(res, footer) + } + + if (length(landing) == 1L) { + res <- teal_extend_server(res, function(input, output, session) { + do.call(landing[[1L]]$server, c(list(id = "landing_module_shiny_id"))) + }) + lifecycle::deprecate_warn( + when = "0.15.3", + what = "landing_popup_module()", + details = paste( + "landing_popup_module() is deprecated.", + "Use add_landing_modal() on the teal app object instead." + ) + ) + } else if (length(landing) > 1L) { + stop("Only one `landing_popup_module` can be used.") + } + logger::log_debug("init teal app has been initialized.") res diff --git a/R/landing_popup_module.R b/R/landing_popup_module.R index 2876a9f1e..204c0e18e 100644 --- a/R/landing_popup_module.R +++ b/R/landing_popup_module.R @@ -1,9 +1,10 @@ #' Landing popup module #' -#' @description Creates a landing welcome popup for `teal` applications. +#' @description `r lifecycle::badge("deprecated")` Creates a landing welcome popup for `teal` applications. #' #' This module is used to display a popup dialog when the application starts. #' The dialog blocks access to the application and must be closed with a button before the application can be viewed. +#' This function is deprecated, please use `add_landing_modal()` on the teal app object instead. #' #' @param label (`character(1)`) Label of the module. #' @param title (`character(1)`) Text to be displayed as popup title. @@ -13,51 +14,19 @@ #' #' @return A `teal_module` (extended with `teal_landing_module` class) to be used in `teal` applications. #' -#' @examples -#' app1 <- init( -#' data = teal_data(iris = iris), -#' modules = modules( -#' example_module() -#' ), -#' landing_popup = landing_popup_module( -#' content = "A place for the welcome message or a disclaimer statement.", -#' buttons = modalButton("Proceed") -#' ) -#' ) -#' if (interactive()) { -#' shinyApp(app1$ui, app1$server) -#' } -#' -#' app2 <- init( -#' data = teal_data(iris = iris), -#' modules = modules( -#' example_module() -#' ), -#' landing_popup = landing_popup_module( -#' title = "Welcome", -#' content = tags$b( -#' "A place for the welcome message or a disclaimer statement.", -#' style = "color: red;" -#' ), -#' buttons = tagList( -#' modalButton("Proceed"), -#' actionButton("read", "Read more", -#' onclick = "window.open('http://google.com', '_blank')" -#' ), -#' actionButton("close", "Reject", onclick = "window.close()") -#' ) -#' ) -#' ) -#' -#' if (interactive()) { -#' shinyApp(app2$ui, app2$server) -#' } -#' #' @export landing_popup_module <- function(label = "Landing Popup", title = NULL, content = NULL, buttons = modalButton("Accept")) { + lifecycle::deprecate_soft( + when = "0.15.3", + what = "landing_popup_module()", + details = paste( + "landing_popup_module() is deprecated.", + "Use add_landing_modal() on the teal app object instead." + ) + ) checkmate::assert_string(label) checkmate::assert_string(title, null.ok = TRUE) checkmate::assert_multi_class( @@ -70,6 +39,7 @@ landing_popup_module <- function(label = "Landing Popup", module <- module( label = label, + datanames = NULL, server = function(id) { moduleServer(id, function(input, output, session) { showModal( diff --git a/R/module_session_info.R b/R/module_session_info.R new file mode 100644 index 000000000..682283ada --- /dev/null +++ b/R/module_session_info.R @@ -0,0 +1,54 @@ +#' `teal` user session info module +#' +#' Module to display the user session info popup and to download a lockfile. +#' +#' @rdname module_session_info +#' @name module_session_info +#' +#' @inheritParams module_teal +#' +#' @examples +#' ui <- fluidPage( +#' ui_session_info("session_info") +#' ) +#' +#' server <- function(input, output, session) { +#' srv_session_info("session_info") +#' } +#' +#' if (interactive()) { +#' shinyApp(ui, server) +#' } +#' +#' @return `NULL` invisibly +NULL + +#' @rdname module_session_info +#' @export +ui_session_info <- function(id) { + ns <- NS(id) + tags$div( + teal.widgets::verbatim_popup_ui(ns("sessionInfo"), "Session Info", type = "link"), + br(), + ui_teal_lockfile(ns("lockfile")), + textOutput(ns("identifier")) + ) +} + +#' @rdname module_session_info +#' @export +srv_session_info <- function(id) { + moduleServer(id, function(input, output, session) { + srv_teal_lockfile("lockfile") + + output$identifier <- renderText( + paste0("Pid:", Sys.getpid(), " Token:", substr(session$token, 25, 32)) + ) + + teal.widgets::verbatim_popup_srv( + "sessionInfo", + verbatim_content = utils::capture.output(utils::sessionInfo()), + title = "SessionInfo" + ) + }) +} diff --git a/R/module_teal.R b/R/module_teal.R index f4e63fadf..d78e7eb94 100644 --- a/R/module_teal.R +++ b/R/module_teal.R @@ -2,11 +2,12 @@ #' #' @description #' `r lifecycle::badge("stable")` -#' Module to create a `teal` app. This module can be called directly instead of [init()] and -#' included in your custom application. Please note that [init()] adds `reporter_previewer_module` -#' automatically, which is not a case when calling `ui/srv_teal` directly. +#' Module to create a `teal` app as a Shiny Module. #' #' @details +#' This module can be used instead of [init()] in custom Shiny applications. Unlike [init()], it doesn't +#' automatically include `reporter_previewer_module`, `module_session_info`, or UI components like +#' `header`, `footer`, and `title` which can be added separately in the Shiny app consuming this module. #' #' Module is responsible for creating the main `shiny` app layout and initializing all the necessary #' components. This module establishes reactive connection between the input `data` and every other @@ -36,48 +37,19 @@ #' @param id (`character(1)`) `shiny` module instance id. #' @param data (`teal_data`, `teal_data_module`, or `reactive` returning `teal_data`) #' The data which application will depend on. +#' @param modules (`teal_modules`) +#' `teal_modules` object. These are the specific output modules which +#' will be displayed in the `teal` application. See [modules()] and [module()] for +#' more details. #' #' @return `NULL` invisibly NULL #' @rdname module_teal #' @export -ui_teal <- function(id, - modules, - title = build_app_title(), - header = tags$p(), - footer = tags$p()) { +ui_teal <- function(id, modules) { checkmate::assert_character(id, max.len = 1, any.missing = FALSE) - checkmate::assert( - .var.name = "title", - checkmate::check_string(title), - checkmate::check_multi_class(title, c("shiny.tag", "shiny.tag.list", "html")) - ) - checkmate::assert( - .var.name = "header", - checkmate::check_string(header), - checkmate::check_multi_class(header, c("shiny.tag", "shiny.tag.list", "html")) - ) - checkmate::assert( - .var.name = "footer", - checkmate::check_string(footer), - checkmate::check_multi_class(footer, c("shiny.tag", "shiny.tag.list", "html")) - ) - - if (is.character(title)) { - title <- build_app_title(title) - } else { - validate_app_title_tag(title) - } - - if (checkmate::test_string(header)) { - header <- tags$p(header) - } - - if (checkmate::test_string(footer)) { - footer <- tags$p(footer) - } - + checkmate::assert_class(modules, "teal_modules") ns <- NS(id) # show busy icon when `shiny` session is busy computing stuff @@ -94,10 +66,8 @@ ui_teal <- function(id, fluidPage( id = id, - title = title, theme = get_teal_bs_theme(), include_teal_css_js(), - tags$header(header), tags$hr(class = "my-2"), shiny_busy_message_panel, tags$div( @@ -132,16 +102,7 @@ ui_teal <- function(id, ) ) ), - tags$hr(), - tags$footer( - tags$div( - footer, - teal.widgets::verbatim_popup_ui(ns("sessionInfo"), "Session Info", type = "link"), - br(), - ui_teal_lockfile(ns("lockfile")), - textOutput(ns("identifier")) - ) - ) + tags$hr() ) } @@ -160,18 +121,6 @@ srv_teal <- function(id, data, modules, filter = teal_slices()) { shinyjs::showLog() } - srv_teal_lockfile("lockfile") - - output$identifier <- renderText( - paste0("Pid:", Sys.getpid(), " Token:", substr(session$token, 25, 32)) - ) - - teal.widgets::verbatim_popup_srv( - "sessionInfo", - verbatim_content = utils::capture.output(utils::sessionInfo()), - title = "SessionInfo" - ) - # `JavaScript` code run_js_files(files = "init.js") diff --git a/R/module_teal_with_splash.R b/R/module_teal_with_splash.R index c4f29c0ae..7794f7ccb 100644 --- a/R/module_teal_with_splash.R +++ b/R/module_teal_with_splash.R @@ -5,6 +5,7 @@ #' #' @inheritParams ui_teal #' @inheritParams srv_teal +#' @inheritParams init #' #' @return #' Returns a `reactive` expression containing a `teal_data` object when data is loaded or `NULL` when it is not. @@ -16,24 +17,46 @@ NULL #' @rdname module_teal_with_splash ui_teal_with_splash <- function(id, data, + modules, title = build_app_title(), header = tags$p(), footer = tags$p()) { lifecycle::deprecate_soft( - when = "0.16", + when = "0.15.3", what = "ui_teal_with_splash()", details = "Deprecated, please use `ui_teal` instead" ) - ui_teal(id = id, title = title, header = header, footer = footer) + ns <- shiny::NS(id) + fluidPage( + title = tags$div( + id = ns("teal-app-title"), + tags$head( + tags$title("teal app"), + tags$link( + rel = "icon", + href = "https://raw.githubusercontent.com/insightsengineering/hex-stickers/main/PNG/nest.png", + sizes = "any" + ) + ) + ), + tags$header(id = ns("teal-header-content")), + ui_teal(id = id, modules = modules), + tags$footer( + id = "teal-footer", + tags$div(id = "teal-footer-content"), + ui_session_info(ns("teal-footer-session_info")) + ) + ) } #' @export #' @rdname module_teal_with_splash srv_teal_with_splash <- function(id, data, modules, filter = teal_slices()) { lifecycle::deprecate_soft( - when = "0.16", + when = "0.15.3", what = "srv_teal_with_splash()", details = "Deprecated, please use `srv_teal` instead" ) srv_teal(id = id, data = data, modules = modules, filter = filter) + srv_session_info("teal-footer-session_info") } diff --git a/R/show_rcode_modal.R b/R/show_rcode_modal.R index 0f7fe8822..83bd248a9 100644 --- a/R/show_rcode_modal.R +++ b/R/show_rcode_modal.R @@ -15,7 +15,7 @@ #' @export show_rcode_modal <- function(title = NULL, rcode, session = getDefaultReactiveDomain()) { lifecycle::deprecate_soft( - when = "0.16", + when = "0.15.3", what = "show_rcode_modal()", details = "This function will be removed in the next release." ) diff --git a/R/tdata.R b/R/tdata.R index bb020c99f..dc79693bf 100644 --- a/R/tdata.R +++ b/R/tdata.R @@ -52,7 +52,7 @@ as_tdata <- function(...) { .deprecate_tdata_msg <- function() { lifecycle::deprecate_stop( - when = "0.16", + when = "0.15.3", what = "tdata()", details = paste( "tdata has been removed in favour of `teal_data`.\n", diff --git a/R/teal_modifiers.R b/R/teal_modifiers.R new file mode 100644 index 000000000..3f0fb90f1 --- /dev/null +++ b/R/teal_modifiers.R @@ -0,0 +1,191 @@ +#' Replace UI Elements in `teal` UI objects +#' +#' @param x (`teal_app`) A `teal_app` object created using the `init` function. +#' @param element Replacement UI element (shiny tag or HTML) +#' @param title (`shiny.tag` or `character(1)`) The new title to be used. +#' @param favicon (`character`) The path for the icon for the title. +#' The image/icon path can be remote or the static path accessible by `shiny`, like the `www/` +#' @name teal_modifiers +#' @rdname teal_modifiers +#' +#' @keywords internal +#' +NULL + + +#' @rdname teal_modifiers +#' @keywords internal +#' @noRd +#' @param x One of: +#' - A `teal_app` object created using the `init` function. +#' - A `teal_module`, `teal_data_module`, or `teal_transform_module` object. +#' - A Shiny module UI function with `id` parameter +#' @param selector (`character(1)`) CSS selector to find elements to replace +teal_replace_ui <- function(x, selector, element) { + if (inherits(x, c("teal_app", "teal_module", "teal_data_module", "teal_transform_module"))) { + x$ui <- teal_replace_ui(x$ui, selector, element) + x + } else if (checkmate::test_function(x, args = "request")) { + # shiny ui function from teal_app + function(request) { + ui_tq <- htmltools::tagQuery(x(request = request)) + ui_tq$find(selector)$empty()$append(element)$allTags() + } + } else if (checkmate::test_function(x, args = "id")) { + # shiny module ui function + function(id, ...) { + ui_tq <- htmltools::tagQuery(x(id = id, ...)) + if (grepl("^#[a-zA-Z0-9_-]+$", selector)) { + selector <- paste0("#", NS(id, gsub("^#", "", selector))) + } + ui_tq$find(selector)$empty()$append(element)$allTags() + } + } else { + stop("Invalid UI object") + } +} + +#' @rdname teal_modifiers +#' @export +#' @examples +#' app <- init( +#' data = teal_data(IRIS = iris, MTCARS = mtcars), +#' modules = modules(example_module()) +#' ) |> +#' modify_title(title = "Custom title") +#' +#' if (interactive()) { +#' shinyApp(app$ui, app$server) +#' } +modify_title <- function( + x, + title = "teal app", + favicon = "https://raw.githubusercontent.com/insightsengineering/hex-stickers/main/PNG/nest.png") { + checkmate::assert_multi_class(x, "teal_app") + checkmate::assert_multi_class(title, c("shiny.tag", "shiny.tag.list", "html", "character")) + checkmate::assert_string(favicon) + teal_replace_ui( + x, + "#teal-app-title", + tags$head( + tags$title(title), + tags$link( + rel = "icon", + href = favicon, + sizes = "any" + ) + ) + ) +} + +#' @rdname teal_modifiers +#' @export +#' @examples +#' app <- init( +#' data = teal_data(IRIS = iris), +#' modules = modules(example_module()) +#' ) |> +#' modify_header(element = tags$div(h3("Custom header"))) +#' +#' if (interactive()) { +#' shinyApp(app$ui, app$server) +#' } +modify_header <- function(x, element = tags$p()) { + checkmate::assert_multi_class(x, "teal_app") + checkmate::assert_multi_class(element, c("shiny.tag", "shiny.tag.list", "html", "character")) + teal_replace_ui(x, "#teal-header-content", element) +} + +#' @rdname teal_modifiers +#' @export +#' @examples +#' app <- init( +#' data = teal_data(IRIS = iris), +#' modules = modules(example_module()) +#' ) |> +#' modify_footer(element = "Custom footer") +#' +#' if (interactive()) { +#' shinyApp(app$ui, app$server) +#' } +modify_footer <- function(x, element = tags$p()) { + checkmate::assert_multi_class(x, "teal_app") + checkmate::assert_multi_class(element, c("shiny.tag", "shiny.tag.list", "html", "character")) + teal_replace_ui(x, "#teal-footer-content", element) +} + +#' Add a Landing Popup to `teal` Application +#' +#' @description Adds a landing popup to the `teal` app. This popup will be shown when the app starts. +#' The dialog must be closed by the app user to proceed to the main application. +#' +#' @param x (`teal_app`) A `teal_app` object created using the `init` function. +#' @inheritParams shiny::modalDialog +#' @param content (`character(1)`, `shiny.tag` or `shiny.tag.list`) with the content of the popup. +#' @param ... Additional arguments to [shiny::modalDialog()]. +#' @export +#' @examples +#' app <- init( +#' data = teal_data(IRIS = iris, MTCARS = mtcars), +#' modules = modules(example_module()) +#' ) |> +#' add_landing_modal( +#' title = "Welcome", +#' content = "This is a landing popup.", +#' buttons = modalButton("Accept") +#' ) +#' +#' if (interactive()) { +#' shinyApp(app$ui, app$server) +#' } +add_landing_modal <- function( + x, + title = NULL, + content = NULL, + footer = modalButton("Accept"), + ...) { + checkmate::assert_class(x, "teal_app") + custom_server <- function(input, output, session) { + checkmate::assert_string(title, null.ok = TRUE) + checkmate::assert_multi_class( + content, + classes = c("character", "shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE + ) + checkmate::assert_multi_class(footer, classes = c("shiny.tag", "shiny.tag.list")) + showModal( + modalDialog( + id = "landingpopup", + title = title, + content, + footer = footer, + ... + ) + ) + } + teal_extend_server(x, custom_server) +} + +#' Add a Custom Server Logic to `teal` Application +#' +#' @description Adds a custom server function to the `teal` app. This function can define additional server logic. +#' +#' @param x (`teal_app`) A `teal_app` object created using the `init` function. +#' @param custom_server (`function(input, output, session)` or `function(id, ...)`) +#' The custom server function or server module to set. +#' @param module_id (`character(1)`) The ID of the module when a module server function is passed. +#' @keywords internal +teal_extend_server <- function(x, custom_server, module_id = character(0)) { + checkmate::assert_class(x, "teal_app") + checkmate::assert_function(custom_server) + old_server <- x$server + + x$server <- function(input, output, session) { + old_server(input, output, session) + if (all(c("input", "output", "session") %in% names(formals(custom_server)))) { + callModule(custom_server, module_id) + } else if ("id" %in% names(formals(custom_server))) { + custom_server(module_id) + } + } + x +} diff --git a/R/utils.R b/R/utils.R index 46fcddc1a..0f06e1e48 100644 --- a/R/utils.R +++ b/R/utils.R @@ -315,6 +315,11 @@ build_app_title <- function( favicon = "https://raw.githubusercontent.com/insightsengineering/hex-stickers/main/PNG/nest.png") { checkmate::assert_string(title, null.ok = TRUE) checkmate::assert_string(favicon, null.ok = TRUE) + lifecycle::deprecate_soft( + when = "0.15.3", + what = "build_app_title()", + details = "Use `modify_title()` on the object created using the `init`." + ) tags$head( tags$title(title), tags$link( diff --git a/_pkgdown.yml b/_pkgdown.yml index 94e595d09..1eea00f5f 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -105,9 +105,17 @@ reference: - make_teal_transform_server - module_teal_with_splash - module_teal + - module_session_info - module - modules - teal_slices + - title: Application modifiers for `teal` + desc: Functions to modify the `teal` app object + contents: + - modify_footer + - modify_header + - modify_title + - add_landing_modal - title: Helper Functions desc: Helper functions for `teal` contents: diff --git a/man/TealAppDriver.Rd b/man/TealAppDriver.Rd index cfdd8110d..438a12409 100644 --- a/man/TealAppDriver.Rd +++ b/man/TealAppDriver.Rd @@ -98,10 +98,10 @@ Initialize a \code{TealAppDriver} object for testing a \code{teal} application. data, modules, filter = teal_slices(), - title = build_app_title(), + title_args = list(), header = tags$p(), footer = tags$p(), - landing_popup = NULL, + landing_popup_args = NULL, timeout = rlang::missing_arg(), load_timeout = rlang::missing_arg(), ... @@ -111,7 +111,9 @@ Initialize a \code{TealAppDriver} object for testing a \code{teal} application. \subsection{Arguments}{ \if{html}{\out{
}} \describe{ -\item{\code{data, modules, filter, title, header, footer, landing_popup}}{arguments passed to \code{init}} +\item{\code{data, modules, filter}}{arguments passed to \code{init}} + +\item{\code{title_args, header, footer, landing_popup_args}}{to pass into the modifier functions.} \item{\code{timeout}}{(\code{numeric}) Default number of milliseconds for any timeout or timeout_ parameter in the \code{TealAppDriver} class. diff --git a/man/add_landing_modal.Rd b/man/add_landing_modal.Rd new file mode 100644 index 000000000..be8d6339c --- /dev/null +++ b/man/add_landing_modal.Rd @@ -0,0 +1,44 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/teal_modifiers.R +\name{add_landing_modal} +\alias{add_landing_modal} +\title{Add a Landing Popup to \code{teal} Application} +\usage{ +add_landing_modal( + x, + title = NULL, + content = NULL, + footer = modalButton("Accept"), + ... +) +} +\arguments{ +\item{x}{(\code{teal_app}) A \code{teal_app} object created using the \code{init} function.} + +\item{title}{An optional title for the dialog.} + +\item{content}{(\code{character(1)}, \code{shiny.tag} or \code{shiny.tag.list}) with the content of the popup.} + +\item{footer}{UI for footer. Use \code{NULL} for no footer.} + +\item{...}{Additional arguments to \code{\link[shiny:modalDialog]{shiny::modalDialog()}}.} +} +\description{ +Adds a landing popup to the \code{teal} app. This popup will be shown when the app starts. +The dialog must be closed by the app user to proceed to the main application. +} +\examples{ +app <- init( + data = teal_data(IRIS = iris, MTCARS = mtcars), + modules = modules(example_module()) +) |> + add_landing_modal( + title = "Welcome", + content = "This is a landing popup.", + buttons = modalButton("Accept") + ) + +if (interactive()) { + shinyApp(app$ui, app$server) +} +} diff --git a/man/init.Rd b/man/init.Rd index 81366add0..f142e43b4 100644 --- a/man/init.Rd +++ b/man/init.Rd @@ -8,11 +8,10 @@ init( data, modules, filter = teal_slices(), - title = build_app_title(), - header = tags$p(), - footer = tags$p(), - id = lifecycle::deprecated(), - landing_popup = NULL + title = lifecycle::deprecated(), + header = lifecycle::deprecated(), + footer = lifecycle::deprecated(), + id = lifecycle::deprecated() ) } \arguments{ @@ -28,24 +27,24 @@ more details.} \item{filter}{(\code{teal_slices}) Optionally, specifies the initial filter using \code{\link[=teal_slices]{teal_slices()}}.} -\item{title}{(\code{shiny.tag} or \code{character(1)}) Optionally, +\item{title}{(\code{shiny.tag} or \code{character(1)}) \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} 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 -by passing a valid \code{shiny.tag} which is a head tag with title and link tag.} +by passing a valid \code{shiny.tag} which is a head tag with title and link tag. +This parameter is deprecated. Use \code{modify_title()} on the teal app object instead.} -\item{header}{(\code{shiny.tag} or \code{character(1)}) Optionally, -the header of the app.} +\item{header}{(\code{shiny.tag} or \code{character(1)}) \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} Optionally, +the header of the app. +This parameter is deprecated. Use \code{modify_header()} on the teal app object instead.} -\item{footer}{(\code{shiny.tag} or \code{character(1)}) Optionally, -the footer of the app.} +\item{footer}{(\code{shiny.tag} or \code{character(1)}) \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} Optionally, +the footer of the app. +This parameter is deprecated. Use \code{modify_footer()} on the teal app object instead.} \item{id}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} (\code{character}) Optionally, a string specifying the \code{shiny} module id in cases it is used as a \code{shiny} module rather than a standalone \code{shiny} app. This is a legacy feature. Deprecated since v0.15.3 please use \code{\link[=ui_teal]{ui_teal()}} and \code{\link[=srv_teal]{srv_teal()}} instead.} - -\item{landing_popup}{(\code{teal_module_landing}) Optionally, -a \code{landing_popup_module} to show up as soon as the teal app is initialized.} } \value{ Named list containing server and UI functions. @@ -98,10 +97,7 @@ app <- init( `Iris Sepal.Length histogram` = "new_iris Species", global_filters = "new_mtcars cyl" ) - ), - title = "App title", - header = tags$h1("Sample App"), - footer = tags$p("Sample footer") + ) ) if (interactive()) { shinyApp(app$ui, app$server) diff --git a/man/landing_popup_module.Rd b/man/landing_popup_module.Rd index af7c79c72..304c2cedb 100644 --- a/man/landing_popup_module.Rd +++ b/man/landing_popup_module.Rd @@ -25,49 +25,9 @@ Passed to \code{...} of \code{shiny::modalDialog}. See examples.} A \code{teal_module} (extended with \code{teal_landing_module} class) to be used in \code{teal} applications. } \description{ -Creates a landing welcome popup for \code{teal} applications. +\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} Creates a landing welcome popup for \code{teal} applications. This module is used to display a popup dialog when the application starts. The dialog blocks access to the application and must be closed with a button before the application can be viewed. -} -\examples{ -app1 <- init( - data = teal_data(iris = iris), - modules = modules( - example_module() - ), - landing_popup = landing_popup_module( - content = "A place for the welcome message or a disclaimer statement.", - buttons = modalButton("Proceed") - ) -) -if (interactive()) { - shinyApp(app1$ui, app1$server) -} - -app2 <- init( - data = teal_data(iris = iris), - modules = modules( - example_module() - ), - landing_popup = landing_popup_module( - title = "Welcome", - content = tags$b( - "A place for the welcome message or a disclaimer statement.", - style = "color: red;" - ), - buttons = tagList( - modalButton("Proceed"), - actionButton("read", "Read more", - onclick = "window.open('http://google.com', '_blank')" - ), - actionButton("close", "Reject", onclick = "window.close()") - ) - ) -) - -if (interactive()) { - shinyApp(app2$ui, app2$server) -} - +This function is deprecated, please use \code{add_landing_modal()} on the teal app object instead. } diff --git a/man/module_bookmark_manager.Rd b/man/module_bookmark_manager.Rd index 1e6dc7ae8..b76af700b 100644 --- a/man/module_bookmark_manager.Rd +++ b/man/module_bookmark_manager.Rd @@ -22,9 +22,8 @@ need_bookmarking(modules) \arguments{ \item{id}{(\code{character(1)}) \code{shiny} module instance id.} -\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 +\item{modules}{(\code{teal_modules}) +\code{teal_modules} 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.} } diff --git a/man/module_filter_data.Rd b/man/module_filter_data.Rd index 39d908999..0498657c3 100644 --- a/man/module_filter_data.Rd +++ b/man/module_filter_data.Rd @@ -30,9 +30,8 @@ which implies in filter-panel to be "global". When \code{NULL} then filter-panel \item{data}{(\code{reactive} returning \code{teal_data})} -\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 +\item{modules}{(\code{teal_modules}) +\code{teal_modules} 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.} } diff --git a/man/module_session_info.Rd b/man/module_session_info.Rd new file mode 100644 index 000000000..1c15bd28b --- /dev/null +++ b/man/module_session_info.Rd @@ -0,0 +1,35 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/module_session_info.R +\name{module_session_info} +\alias{module_session_info} +\alias{ui_session_info} +\alias{srv_session_info} +\title{\code{teal} user session info module} +\usage{ +ui_session_info(id) + +srv_session_info(id) +} +\arguments{ +\item{id}{(\code{character(1)}) \code{shiny} module instance id.} +} +\value{ +\code{NULL} invisibly +} +\description{ +Module to display the user session info popup and to download a lockfile. +} +\examples{ +ui <- fluidPage( + ui_session_info("session_info") +) + +server <- function(input, output, session) { + srv_session_info("session_info") +} + +if (interactive()) { + shinyApp(ui, server) +} + +} diff --git a/man/module_teal.Rd b/man/module_teal.Rd index a3c13d295..c6ba7b206 100644 --- a/man/module_teal.Rd +++ b/man/module_teal.Rd @@ -6,36 +6,18 @@ \alias{srv_teal} \title{\code{teal} main module} \usage{ -ui_teal( - id, - modules, - title = build_app_title(), - header = tags$p(), - footer = tags$p() -) +ui_teal(id, modules) srv_teal(id, data, modules, filter = teal_slices()) } \arguments{ \item{id}{(\code{character(1)}) \code{shiny} module instance id.} -\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 +\item{modules}{(\code{teal_modules}) +\code{teal_modules} 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{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 -by passing a valid \code{shiny.tag} which is a head tag with title and link tag.} - -\item{header}{(\code{shiny.tag} or \code{character(1)}) Optionally, -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.} @@ -47,11 +29,13 @@ specifies the initial filter using \code{\link[=teal_slices]{teal_slices()}}.} } \description{ \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#stable}{\figure{lifecycle-stable.svg}{options: alt='[Stable]'}}}{\strong{[Stable]}} -Module to create a \code{teal} app. This module can be called directly instead of \code{\link[=init]{init()}} and -included in your custom application. Please note that \code{\link[=init]{init()}} adds \code{reporter_previewer_module} -automatically, which is not a case when calling \code{ui/srv_teal} directly. +Module to create a \code{teal} app as a Shiny Module. } \details{ +This module can be used instead of \code{\link[=init]{init()}} in custom Shiny applications. Unlike \code{\link[=init]{init()}}, it doesn't +automatically include \code{reporter_previewer_module}, \code{module_session_info}, or UI components like +\code{header}, \code{footer}, and \code{title} which can be added separately in the Shiny app consuming this module. + Module is responsible for creating the main \code{shiny} app layout and initializing all the necessary components. This module establishes reactive connection between the input \code{data} and every other component in the app. Reactive change of the \code{data} passed as an argument, reloads the app and diff --git a/man/module_teal_module.Rd b/man/module_teal_module.Rd index abf84d928..c12912dbd 100644 --- a/man/module_teal_module.Rd +++ b/man/module_teal_module.Rd @@ -67,9 +67,8 @@ srv_teal_module( \arguments{ \item{id}{(\code{character(1)}) \code{shiny} module instance id.} -\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 +\item{modules}{(\code{teal_modules}) +\code{teal_modules} 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.} diff --git a/man/module_teal_with_splash.Rd b/man/module_teal_with_splash.Rd index 1bad555d6..26f4a5e6f 100644 --- a/man/module_teal_with_splash.Rd +++ b/man/module_teal_with_splash.Rd @@ -9,6 +9,7 @@ ui_teal_with_splash( id, data, + modules, title = build_app_title(), header = tags$p(), footer = tags$p() @@ -22,22 +23,24 @@ srv_teal_with_splash(id, data, modules, filter = teal_slices()) \item{data}{(\code{teal_data}, \code{teal_data_module}, or \code{reactive} returning \code{teal_data}) The data which application will depend on.} -\item{title}{(\code{shiny.tag} or \code{character(1)}) Optionally, +\item{modules}{(\code{teal_modules}) +\code{teal_modules} 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{title}{(\code{shiny.tag} or \code{character(1)}) \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} 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 -by passing a valid \code{shiny.tag} which is a head tag with title and link tag.} +by passing a valid \code{shiny.tag} which is a head tag with title and link tag. +This parameter is deprecated. Use \code{modify_title()} on the teal app object instead.} -\item{header}{(\code{shiny.tag} or \code{character(1)}) Optionally, -the header of the app.} +\item{header}{(\code{shiny.tag} or \code{character(1)}) \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} Optionally, +the header of the app. +This parameter is deprecated. Use \code{modify_header()} on the teal app object instead.} -\item{footer}{(\code{shiny.tag} or \code{character(1)}) Optionally, -the footer of the app.} - -\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{footer}{(\code{shiny.tag} or \code{character(1)}) \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} Optionally, +the footer of the app. +This parameter is deprecated. Use \code{modify_footer()} on the teal app object instead.} \item{filter}{(\code{teal_slices}) Optionally, specifies the initial filter using \code{\link[=teal_slices]{teal_slices()}}.} diff --git a/man/teal_extend_server.Rd b/man/teal_extend_server.Rd new file mode 100644 index 000000000..365b7d27e --- /dev/null +++ b/man/teal_extend_server.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/teal_modifiers.R +\name{teal_extend_server} +\alias{teal_extend_server} +\title{Add a Custom Server Logic to \code{teal} Application} +\usage{ +teal_extend_server(x, custom_server, module_id = character(0)) +} +\arguments{ +\item{x}{(\code{teal_app}) A \code{teal_app} object created using the \code{init} function.} + +\item{custom_server}{(\verb{function(input, output, session)} or \verb{function(id, ...)}) +The custom server function or server module to set.} + +\item{module_id}{(\code{character(1)}) The ID of the module when a module server function is passed.} +} +\description{ +Adds a custom server function to the \code{teal} app. This function can define additional server logic. +} +\keyword{internal} diff --git a/man/teal_modifiers.Rd b/man/teal_modifiers.Rd new file mode 100644 index 000000000..7bafef0b4 --- /dev/null +++ b/man/teal_modifiers.Rd @@ -0,0 +1,63 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/teal_modifiers.R +\name{teal_modifiers} +\alias{teal_modifiers} +\alias{modify_title} +\alias{modify_header} +\alias{modify_footer} +\title{Replace UI Elements in \code{teal} UI objects} +\usage{ +modify_title( + x, + title = "teal app", + favicon = + "https://raw.githubusercontent.com/insightsengineering/hex-stickers/main/PNG/nest.png" +) + +modify_header(x, element = tags$p()) + +modify_footer(x, element = tags$p()) +} +\arguments{ +\item{x}{(\code{teal_app}) A \code{teal_app} object created using the \code{init} function.} + +\item{title}{(\code{shiny.tag} or \code{character(1)}) The new title to be used.} + +\item{favicon}{(\code{character}) The path for the icon for the title. +The image/icon path can be remote or the static path accessible by \code{shiny}, like the \verb{www/}} + +\item{element}{Replacement UI element (shiny tag or HTML)} +} +\description{ +Replace UI Elements in \code{teal} UI objects +} +\examples{ +app <- init( + data = teal_data(IRIS = iris, MTCARS = mtcars), + modules = modules(example_module()) +) |> + modify_title(title = "Custom title") + +if (interactive()) { + shinyApp(app$ui, app$server) +} +app <- init( + data = teal_data(IRIS = iris), + modules = modules(example_module()) +) |> + modify_header(element = tags$div(h3("Custom header"))) + +if (interactive()) { + shinyApp(app$ui, app$server) +} +app <- init( + data = teal_data(IRIS = iris), + modules = modules(example_module()) +) |> + modify_footer(element = "Custom footer") + +if (interactive()) { + shinyApp(app$ui, app$server) +} +} +\keyword{internal} diff --git a/tests/testthat/test-module_session_info.R b/tests/testthat/test-module_session_info.R new file mode 100644 index 000000000..bff7f092e --- /dev/null +++ b/tests/testthat/test-module_session_info.R @@ -0,0 +1,45 @@ +testthat::describe("srv_session_info lockfile", { + testthat::it(paste0( + "creation process is invoked for teal.lockfile.mode = \"enabled\" ", + "and snapshot is copied to teal_app.lock and removed after session ended" + ), { + testthat::skip_if_not_installed("mirai") + testthat::skip_if_not_installed("renv") + withr::with_options( + list(teal.lockfile.mode = "enabled"), + { + renv_filename <- "teal_app.lock" + shiny::testServer( + app = srv_session_info, + args = list(id = "test"), + expr = { + iter <- 1 + while (!file.exists(renv_filename) && iter <= 1000) { + Sys.sleep(0.5) + iter <- iter + 1 # max wait time is 500 seconds + } + testthat::expect_true(file.exists(renv_filename)) + } + ) + testthat::expect_false(file.exists(renv_filename)) + } + ) + }) + testthat::it("creation process is not invoked for teal.lockfile.mode = \"disabled\"", { + testthat::skip_if_not_installed("mirai") + testthat::skip_if_not_installed("renv") + withr::with_options( + list(teal.lockfile.mode = "disabled"), + { + renv_filename <- "teal_app.lock" + shiny::testServer( + app = srv_session_info, + args = list(id = "test"), + expr = { + testthat::expect_false(file.exists(renv_filename)) + } + ) + } + ) + }) +}) diff --git a/tests/testthat/test-module_teal.R b/tests/testthat/test-module_teal.R index 3a48c5895..a25eaec54 100644 --- a/tests/testthat/test-module_teal.R +++ b/tests/testthat/test-module_teal.R @@ -65,60 +65,6 @@ transform_list <<- list( ) ) -testthat::describe("srv_teal lockfile", { - testthat::it(paste0( - "creation process is invoked for teal.lockfile.mode = \"enabled\" ", - "and snapshot is copied to teal_app.lock and removed after session ended" - ), { - testthat::skip_if_not_installed("mirai") - testthat::skip_if_not_installed("renv") - withr::with_options( - list(teal.lockfile.mode = "enabled"), - { - renv_filename <- "teal_app.lock" - shiny::testServer( - app = srv_teal, - args = list( - id = "test", - data = teal.data::teal_data(iris = iris), - modules = modules(example_module()) - ), - expr = { - iter <- 1 - while (!file.exists(renv_filename) && iter <= 1000) { - Sys.sleep(0.5) - iter <- iter + 1 # max wait time is 500 seconds - } - testthat::expect_true(file.exists(renv_filename)) - } - ) - testthat::expect_false(file.exists(renv_filename)) - } - ) - }) - testthat::it("creation process is not invoked for teal.lockfile.mode = \"disabled\"", { - testthat::skip_if_not_installed("mirai") - testthat::skip_if_not_installed("renv") - withr::with_options( - list(teal.lockfile.mode = "disabled"), - { - renv_filename <- "teal_app.lock" - shiny::testServer( - app = srv_teal, - args = list( - id = "test", - data = teal.data::teal_data(iris = iris), - modules = modules(example_module()) - ), - expr = { - testthat::expect_false(file.exists(renv_filename)) - } - ) - } - ) - }) -}) - testthat::describe("srv_teal arguments", { testthat::it("accepts data to be teal_data", { testthat::expect_no_error( diff --git a/tests/testthat/test-shinytest2-init.R b/tests/testthat/test-shinytest2-init.R index e0672e7e5..9e0d6d436 100644 --- a/tests/testthat/test-shinytest2-init.R +++ b/tests/testthat/test-shinytest2-init.R @@ -19,7 +19,7 @@ testthat::test_that("e2e: teal app initializes with sessionInfo modal", { ) # Check if button exists. - button_selector <- "#teal-sessionInfo-button" + button_selector <- "#teal-footer-session_info-sessionInfo-button" testthat::expect_equal( app$get_text(button_selector), "Session Info" @@ -37,7 +37,7 @@ testthat::test_that("e2e: teal app initializes with sessionInfo modal", { testthat::expect_setequal( testthat::expect_length( app$get_text( - "#shiny-modal [id^='teal-sessionInfo-copy_button']" + "#shiny-modal [id^='teal-footer-session_info-sessionInfo-copy_button']" ), 2 ), @@ -53,7 +53,7 @@ testthat::test_that("e2e: teal app initializes with sessionInfo modal", { ) # Check session info output. - session_info <- app$get_text("#teal-sessionInfo-verbatim_content") + session_info <- app$get_text("#teal-footer-session_info-sessionInfo-verbatim_content") testthat::expect_match(session_info, "R version", fixed = TRUE) testthat::expect_match(session_info, "attached base packages:", fixed = TRUE) @@ -68,39 +68,36 @@ testthat::test_that("e2e: teal app initializes with sessionInfo modal", { testthat::test_that("e2e: init creates UI containing specified title, favicon, header and footer", { skip_if_too_deep(5) - app_title <- "Custom Teal App Title" - app_favicon <- "https://raw.githubusercontent.com/insightsengineering/hex-stickers/main/PNG/teal.png" - app_header <- "Custom Teal App Header" - app_footer <- "Custom Teal App Footer" app <- TealAppDriver$new( data = simple_teal_data(), modules = example_module(label = "Example Module"), - title = build_app_title( - app_title, - app_favicon + title_args = list( + title = "Custom Teal App Title", + something_else = "asdfsdf", + favicon = "https://raw.githubusercontent.com/insightsengineering/hex-stickers/main/PNG/teal.png" ), - header = app_header, - footer = app_footer + header = "Custom Teal App Header", + footer = "Custom Teal App Footer" ) testthat::expect_equal( app$get_text("head > title")[1], - app_title + "Custom Teal App Title" ) testthat::expect_equal( rvest::html_attr( rvest::html_elements(app$get_html_rvest("head > link[rel='icon']"), "link"), "href" ), - app_favicon + "https://raw.githubusercontent.com/insightsengineering/hex-stickers/main/PNG/teal.png" ) testthat::expect_match( app$get_text("header"), - app_header + "Custom Teal App Header" ) testthat::expect_match( app$get_text("footer"), - app_footer + "Custom Teal App Footer" ) app$stop() }) diff --git a/tests/testthat/test-shinytest2-landing_popup.R b/tests/testthat/test-shinytest2-landing_popup.R index d2e8ba309..96c759718 100644 --- a/tests/testthat/test-shinytest2-landing_popup.R +++ b/tests/testthat/test-shinytest2-landing_popup.R @@ -8,7 +8,7 @@ testthat::test_that("e2e: teal app with landing_popup_module initializes with no modules = modules( example_module() ), - landing_popup = landing_popup_module( + landing_popup_args = list( title = "Welcome", content = tags$b("A welcome message!", style = "color: red;") ) @@ -28,7 +28,7 @@ testthat::test_that("e2e: app with default landing_popup_module creates modal co modules = modules( example_module() ), - landing_popup = landing_popup_module() + landing_popup_args = list() ) testthat::expect_equal( @@ -46,7 +46,7 @@ testthat::test_that("e2e: when default landing_popup_module is closed, it shows modules = modules( example_module() ), - landing_popup = landing_popup_module() + landing_popup_args = list() ) # Button is clicked. @@ -65,77 +65,64 @@ testthat::test_that( "e2e: app with customized landing_popup_module creates modal containing specified title, content and buttons", { skip_if_too_deep(5) - phash <- function(text) paste0("#", text) - - modal_title <- "Custom Landing Popup Module Title" - modal_content_message <- "A welcome message!" - modal_content <- tags$b(modal_content_message, style = "color: red;") - - modal_btns <- list( - go = list(text = "Proceed"), - more = list(text = "Read more", onclick = "window.open('http://google.com', '_blank')", id = "read"), - reject = list(text = "Reject", onclick = "window.close()", id = "close") - ) - modal_buttons <- - tagList( - shiny::modalButton(modal_btns$go$text), - shiny::actionButton( - modal_btns$more$id, - label = modal_btns$more$text, - onclick = modal_btns$more$onclick - ), - shiny::actionButton( - modal_btns$reject$id, - label = modal_btns$reject$text, - onclick = modal_btns$reject$onclick - ) - ) app <- TealAppDriver$new( data = simple_teal_data(), modules = modules( example_module() ), - landing_popup = landing_popup_module( - title = modal_title, - content = modal_content, - buttons = modal_buttons + landing_popup_args = list( + title = "Custom Landing Popup Module Title", + content = tags$b("A welcome message!", style = "color: red;"), + footer = tagList( + shiny::modalButton("Proceed"), + shiny::actionButton( + "read", + label = "Read more", + onclick = "window.open('http://google.com', '_blank')" + ), + shiny::actionButton( + "close", + label = "Reject", + onclick = "window.close()" + ) + ) ) ) testthat::expect_equal( app$get_text(".modal-title"), - modal_title + "Custom Landing Popup Module Title" ) testthat::expect_equal( trimws(app$get_text(".modal-body")), - modal_content_message + "A welcome message!" ) testthat::expect_equal( app$get_text(".btn-default:nth-child(1)"), - modal_btns$go$text + "Proceed" ) testthat::expect_equal( - app$get_text(phash(modal_btns$more$id)), - modal_btns$more$text + app$get_text("#read"), + "Read more" ) testthat::expect_equal( - app$get_attr(phash(modal_btns$more$id), "onclick"), - modal_btns$more$onclick + app$get_attr("#read", "onclick"), + "window.open('http://google.com', '_blank')" ) testthat::expect_equal( - app$get_text(phash(modal_btns$reject$id)), - modal_btns$reject$text + app$get_text("#close"), + "Reject" ) testthat::expect_equal( - app$get_attr(phash(modal_btns$reject$id), "onclick"), - modal_btns$reject$onclick + app$get_attr("#close", "onclick"), + "window.close()" ) app$stop() @@ -144,20 +131,19 @@ testthat::test_that( testthat::test_that("e2e: when customized button in landing_popup_module is clicked, it redirects to a certain page", { skip_if_too_deep(5) - onclick_text <- "window.open('http://google.com', '_blank')" app <- TealAppDriver$new( data = simple_teal_data(), modules = modules( example_module() ), - landing_popup = landing_popup_module( - buttons = actionButton("read", "Read more", onclick = onclick_text) + landing_popup_args = list( + footer = actionButton("read", "Read more", onclick = "window.open('http://google.com', '_blank')") ) ) testthat::expect_equal( app$get_attr("#read", "onclick"), - onclick_text + "window.open('http://google.com', '_blank')" ) app$stop() diff --git a/vignettes/adding-support-for-reporting.Rmd b/vignettes/adding-support-for-reporting.Rmd index 9c3730610..9ba72d496 100644 --- a/vignettes/adding-support-for-reporting.Rmd +++ b/vignettes/adding-support-for-reporting.Rmd @@ -355,9 +355,9 @@ app <- init( example_reporter_module(label = "with Reporter"), my_module(label = "without Reporter") ), - filter = teal_slices(teal_slice(dataname = "AIR", varname = "Temp", selected = c(72, 85))), - header = "Example teal app with reporter" -) + filter = teal_slices(teal_slice(dataname = "AIR", varname = "Temp", selected = c(72, 85))) +) |> + modify_header("Example teal app with reporter") if (interactive()) shinyApp(app$ui, app$server) ``` diff --git a/vignettes/bootstrap-themes-in-teal.Rmd b/vignettes/bootstrap-themes-in-teal.Rmd index 8ea255e4c..3812e1a76 100644 --- a/vignettes/bootstrap-themes-in-teal.Rmd +++ b/vignettes/bootstrap-themes-in-teal.Rmd @@ -105,13 +105,12 @@ app <- init( data = teal_data(IRIS = iris), # nolint: line_length. filter = teal_slices(teal_slice("IRIS", "Sepal.Length", selected = c(5, 7))), modules = modules(example_module(), example_module()), - header = "My first teal application" ) bslib::run_with_themer(shinyApp(app$ui, app$server)) ``` -This gives us the following. +This gives us a `Theme Customizer` similar to what is shown below. diff --git a/vignettes/getting-started-with-teal.Rmd b/vignettes/getting-started-with-teal.Rmd index 342c7de05..921505cf9 100644 --- a/vignettes/getting-started-with-teal.Rmd +++ b/vignettes/getting-started-with-teal.Rmd @@ -40,11 +40,11 @@ app <- init( ), filter = teal_slices( teal_slice(dataname = "IRIS", varname = "Species", selected = "setosa") - ), - title = build_app_title(title = "My first teal app"), - header = h3("My first teal application"), - footer = tags$div(a("Powered by teal", href = "https://insightsengineering.github.io/teal/latest-tag/")) -) + ) +) |> + modify_title("My first teal application") |> + modify_header(h3("My first teal application")) |> + modify_footer(tags$div(a("Powered by teal", href = "https://insightsengineering.github.io/teal/latest-tag/"))) if (interactive()) { shinyApp(app$ui, app$server)