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{