diff --git a/DESCRIPTION b/DESCRIPTION index f041b19126..bb2ed75c83 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -76,6 +76,7 @@ Collate: 'include_css_js.R' 'modules.R' 'init.R' + 'landing_popup_module.R' 'module_filter_manager.R' 'module_nested_tabs.R' 'module_snapshot_manager.R' diff --git a/NAMESPACE b/NAMESPACE index fbf4258c0c..51ae8f93df 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -24,6 +24,7 @@ export(get_code_tdata) export(get_join_keys) export(get_metadata) export(init) +export(landing_popup_module) export(module) export(modules) export(new_tdata) diff --git a/NEWS.md b/NEWS.md index e7d5691e49..44336645a0 100644 --- a/NEWS.md +++ b/NEWS.md @@ -7,6 +7,7 @@ * Added internal functions for storing and restoring of `teal_slices` objects. * Filter state snapshots can now be uploaded from file. See `?snapshot`. * Added argument to `teal_slices` and made modifications to `init` to enable tagging `teal_slices` with an app id to safely upload snapshots from disk. +* Added `landing_popup_module` function which creates a module that will display a popup when the app starts. The popup will block access to the app until it is dismissed. # teal 0.14.0 diff --git a/R/init.R b/R/init.R index 449db91f10..56b6812083 100644 --- a/R/init.R +++ b/R/init.R @@ -136,6 +136,10 @@ init <- function(data, modules <- do.call(teal::modules, modules) } + landing <- extract_module(modules, "teal_module_landing") + if (length(landing) > 1L) stop("Only one `landing_popup_module` can be used.") + modules <- drop_module(modules, "teal_module_landing") + # resolve modules datanames datanames <- teal.data::get_dataname(data) join_keys <- data$get_join_keys() @@ -223,6 +227,10 @@ init <- function(data, res <- list( ui = ui_teal_with_splash(id = id, data = data, title = title, header = header, footer = footer), server = function(input, output, session) { + if (length(landing) == 1L) { + landing_module <- landing[[1L]] + do.call(landing_module$server, c(list(id = "landing_module_shiny_id"), landing_module$server_args)) + } # copy object so that load won't be shared between the session data <- data$copy(deep = TRUE) filter <- deep_copy_filter(filter) diff --git a/R/landing_popup_module.R b/R/landing_popup_module.R new file mode 100644 index 0000000000..cb94e14484 --- /dev/null +++ b/R/landing_popup_module.R @@ -0,0 +1,88 @@ +#' Landing Popup Module +#' +#' @description Creates a landing welcome popup for `teal` applications. +#' +#' This module is used to display a popup dialog when the application starts. +#' The dialog blocks the access to the application and must be closed with a button before the application is viewed. +#' +#' @param label `character(1)` the label of the module. +#' @param title `character(1)` the text to be displayed as a title of the popup. +#' @param content The content of the popup. Passed to `...` of `shiny::modalDialog`. Can be a `character` +#' or a list of `shiny.tag`s. See examples. +#' @param buttons `shiny.tag` or a list of tags (`tagList`). Typically a `modalButton` or `actionButton`. See examples. +#' +#' @return A `teal_module` (extended with `teal_landing_module` class) to be used in `teal` applications. +#' +#' @examples +#' app1 <- teal::init( +#' data = teal.data::dataset("iris", iris), +#' modules = teal::modules( +#' teal::landing_popup_module( +#' content = "A place for the welcome message or a disclaimer statement.", +#' buttons = modalButton("Proceed") +#' ), +#' example_module() +#' ) +#' ) +#' if (interactive()) { +#' shinyApp(app1$ui, app1$server) +#' } +#' +#' app2 <- teal::init( +#' data = teal.data::dataset("iris", iris), +#' modules = teal::modules( +#' teal::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()") +#' ) +#' ), +#' example_module() +#' ) +#' ) +#' +#' if (interactive()) { +#' shinyApp(app2$ui, app2$server) +#' } +#' +#' @export +landing_popup_module <- function(label = "Landing Popup", + title = NULL, + content = NULL, + buttons = modalButton("Accept")) { + checkmate::assert_string(label) + 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(buttons, classes = c("shiny.tag", "shiny.tag.list")) + + logger::log_info("Initializing landing_popup_module") + + module <- module( + label = label, + server = function(id) { + moduleServer(id, function(input, output, session) { + showModal( + modalDialog( + id = "landingpopup", + title = title, + content, + footer = buttons + ) + ) + }) + } + ) + class(module) <- c("teal_module_landing", class(module)) + module +} diff --git a/R/module_teal.R b/R/module_teal.R index fbce6ed5bd..e5d3b399e4 100644 --- a/R/module_teal.R +++ b/R/module_teal.R @@ -226,16 +226,7 @@ srv_teal <- function(id, modules, raw_data, filter = teal_slices()) { }) reporter <- teal.reporter::Reporter$new() - is_any_previewer <- function(modules) { - if (inherits(modules, "teal_modules")) { - any(unlist(lapply(modules$children, is_any_previewer), use.names = FALSE)) - } else if (inherits(modules, "teal_module_previewer")) { - TRUE - } else { - FALSE - } - } - if (is_arg_used(modules, "reporter") && !is_any_previewer(modules)) { + if (is_arg_used(modules, "reporter") && length(extract_module(modules, "teal_module_previewer")) == 0) { modules <- append_module(modules, reporter_previewer_module()) } diff --git a/R/modules.R b/R/modules.R index 2cc426b7fe..472a1076b1 100644 --- a/R/modules.R +++ b/R/modules.R @@ -89,7 +89,7 @@ modules <- function(..., label = "root") { ) } -#' Function which appends a teal_module onto the children of a teal_modules object +#' Append a `teal_module` to `children` of a `teal_modules` object #' @keywords internal #' @param modules `teal_modules` #' @param module `teal_module` object to be appended onto the children of `modules` @@ -103,6 +103,43 @@ append_module <- function(modules, module) { modules } +#' Extract/Remove module(s) of specific class +#' +#' Given a `teal_module` or a `teal_modules`, return the elements of the structure according to `class`. +#' +#' @param modules `teal_modules` +#' @param class The class name of `teal_module` to be extracted or dropped. +#' @keywords internal +#' @return +#' For `extract_module`, a `teal_module` of class `class` or `teal_modules` containing modules of class `class`. +#' For `drop_module`, the opposite, which is all `teal_modules` of class other than `class`. +#' @rdname module_management +extract_module <- function(modules, class) { + if (inherits(modules, class)) { + modules + } else if (inherits(modules, "teal_module")) { + NULL + } else if (inherits(modules, "teal_modules")) { + Filter(function(x) length(x) > 0L, lapply(modules$children, extract_module, class)) + } +} + +#' @keywords internal +#' @return `teal_modules` +#' @rdname module_management +drop_module <- function(modules, class) { + if (inherits(modules, class)) { + NULL + } else if (inherits(modules, "teal_module")) { + modules + } else if (inherits(modules, "teal_modules")) { + do.call( + "modules", + c(Filter(function(x) length(x) > 0L, lapply(modules$children, drop_module, class)), label = modules$label) + ) + } +} + #' Does the object make use of the `arg` #' #' @param modules (`teal_module` or `teal_modules`) object diff --git a/R/reporter_previewer_module.R b/R/reporter_previewer_module.R index 12dac5791c..e24174f2d7 100644 --- a/R/reporter_previewer_module.R +++ b/R/reporter_previewer_module.R @@ -7,18 +7,21 @@ #' #' If you are creating a `teal` application using [teal::init()] then this #' module will be added to your application automatically if any of your `teal modules` -#' support report generation +#' support report generation. #' #' @inheritParams module #' @param server_args (`named list`)\cr #' Arguments passed to [teal.reporter::reporter_previewer_srv()]. -#' @return `teal_module` containing the `teal.reporter` previewer functionality +#' @return `teal_module` (extended with `teal_module_previewer` class) containing the `teal.reporter` previewer +#' functionality. #' @export reporter_previewer_module <- function(label = "Report previewer", server_args = list()) { checkmate::assert_string(label) checkmate::assert_list(server_args, names = "named") checkmate::assert_true(all(names(server_args) %in% names(formals(teal.reporter::reporter_previewer_srv)))) + logger::log_info("Initializing reporter_previewer_module") + srv <- function(id, reporter, ...) { teal.reporter::reporter_previewer_srv(id, reporter, ...) } @@ -32,6 +35,8 @@ reporter_previewer_module <- function(label = "Report previewer", server_args = server = srv, ui = ui, server_args = server_args, ui_args = list(), datanames = NULL ) + # Module is created with a placeholder label and the label is changed later. + # This is to prevent another module being labeled "Report previewer". class(module) <- c("teal_module_previewer", class(module)) module$label <- label module diff --git a/_pkgdown.yml b/_pkgdown.yml index 9525fbdd7a..c75a61786d 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -107,6 +107,9 @@ reference: - reporter_previewer_module - TealReportCard - report_card_template + - title: Landing Popup + contents: + - landing_popup_module - title: Functions for Module Developers contents: - tdata diff --git a/man/append_module.Rd b/man/append_module.Rd index 3c3be16559..379e474ba9 100644 --- a/man/append_module.Rd +++ b/man/append_module.Rd @@ -2,7 +2,7 @@ % Please edit documentation in R/modules.R \name{append_module} \alias{append_module} -\title{Function which appends a teal_module onto the children of a teal_modules object} +\title{Append a \code{teal_module} to \code{children} of a \code{teal_modules} object} \usage{ append_module(modules, module) } @@ -15,6 +15,6 @@ append_module(modules, module) \code{teal_modules} object with \code{module} appended } \description{ -Function which appends a teal_module onto the children of a teal_modules object +Append a \code{teal_module} to \code{children} of a \code{teal_modules} object } \keyword{internal} diff --git a/man/landing_popup_module.Rd b/man/landing_popup_module.Rd new file mode 100644 index 0000000000..92531ca334 --- /dev/null +++ b/man/landing_popup_module.Rd @@ -0,0 +1,73 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/landing_popup_module.R +\name{landing_popup_module} +\alias{landing_popup_module} +\title{Landing Popup Module} +\usage{ +landing_popup_module( + label = "Landing Popup", + title = NULL, + content = NULL, + buttons = modalButton("Accept") +) +} +\arguments{ +\item{label}{\code{character(1)} the label of the module.} + +\item{title}{\code{character(1)} the text to be displayed as a title of the popup.} + +\item{content}{The content of the popup. Passed to \code{...} of \code{shiny::modalDialog}. Can be a \code{character} +or a list of \code{shiny.tag}s. See examples.} + +\item{buttons}{\code{shiny.tag} or a list of tags (\code{tagList}). Typically a \code{modalButton} or \code{actionButton}. See examples.} +} +\value{ +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. + +This module is used to display a popup dialog when the application starts. +The dialog blocks the access to the application and must be closed with a button before the application is viewed. +} +\examples{ +app1 <- teal::init( + data = teal.data::dataset("iris", iris), + modules = teal::modules( + teal::landing_popup_module( + content = "A place for the welcome message or a disclaimer statement.", + buttons = modalButton("Proceed") + ), + example_module() + ) +) +if (interactive()) { + shinyApp(app1$ui, app1$server) +} + +app2 <- teal::init( + data = teal.data::dataset("iris", iris), + modules = teal::modules( + teal::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()") + ) + ), + example_module() + ) +) + +if (interactive()) { + shinyApp(app2$ui, app2$server) +} + +} diff --git a/man/module_management.Rd b/man/module_management.Rd new file mode 100644 index 0000000000..bd9f88db50 --- /dev/null +++ b/man/module_management.Rd @@ -0,0 +1,26 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/modules.R +\name{extract_module} +\alias{extract_module} +\alias{drop_module} +\title{Extract/Remove module(s) of specific class} +\usage{ +extract_module(modules, class) + +drop_module(modules, class) +} +\arguments{ +\item{modules}{\code{teal_modules}} + +\item{class}{The class name of \code{teal_module} to be extracted or dropped.} +} +\value{ +For \code{extract_module}, a \code{teal_module} of class \code{class} or \code{teal_modules} containing modules of class \code{class}. +For \code{drop_module}, the opposite, which is all \code{teal_modules} of class other than \code{class}. + +\code{teal_modules} +} +\description{ +Given a \code{teal_module} or a \code{teal_modules}, return the elements of the structure according to \code{class}. +} +\keyword{internal} diff --git a/man/reporter_previewer_module.Rd b/man/reporter_previewer_module.Rd index 063bc518a5..937a3ec9dc 100644 --- a/man/reporter_previewer_module.Rd +++ b/man/reporter_previewer_module.Rd @@ -14,7 +14,8 @@ reporter_previewer_module(label = "Report previewer", server_args = list()) Arguments passed to \code{\link[teal.reporter:reporter_previewer_srv]{teal.reporter::reporter_previewer_srv()}}.} } \value{ -\code{teal_module} containing the \code{teal.reporter} previewer functionality +\code{teal_module} (extended with \code{teal_module_previewer} class) containing the \code{teal.reporter} previewer +functionality. } \description{ \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#experimental}{\figure{lifecycle-experimental.svg}{options: alt='[Experimental]'}}}{\strong{[Experimental]}} @@ -24,5 +25,5 @@ used in \code{teal} applications. If you are creating a \code{teal} application using \code{\link[=init]{init()}} then this module will be added to your application automatically if any of your \verb{teal modules} -support report generation +support report generation. }