From b0b680f32e2185d561a4d0e1f40549a8988eb630 Mon Sep 17 00:00:00 2001 From: agenius-mohammed-ali <76930969+agenius-mohammed-ali@users.noreply.github.com> Date: Fri, 5 Sep 2025 19:20:49 +0300 Subject: [PATCH] Cran (#115) --- .circleci/config.yml | 5 +- DESCRIPTION | 4 +- NAMESPACE | 2 + NEWS.md | 13 + R/downloadFile.R | 102 ++++- R/downloadablePlot.R | 26 +- R/downloadableReactTable.R | 364 +++++++++++++++ R/downloadableTable.R | 25 +- R/logViewer.R | 33 +- R/logger.R | 29 +- R/ui_helpers.R | 2 +- README.md | 3 +- cran-comments.md | 2 +- docs/404.html | 5 +- docs/articles/announcement-module.html | 9 +- docs/articles/announcement_addin.html | 9 +- docs/articles/applicationReset-module.html | 9 +- docs/articles/downloadFile-module.html | 14 +- docs/articles/downloadablePlot-module.html | 9 +- .../downloadableReactTable-module.html | 389 ++++++++++++++++ docs/articles/downloadableTable-module.html | 9 +- .../figures/downloadableReactTable-1.png | Bin 0 -> 25216 bytes docs/articles/index.html | 7 +- docs/articles/logViewer-module.html | 19 +- docs/articles/migrate_to_v0_2_0.html | 5 +- docs/articles/new-application.html | 9 +- docs/articles/themeBuilder_addin.html | 9 +- docs/authors.html | 9 +- docs/index.html | 13 +- docs/news/index.html | 21 +- docs/pkgdown.yml | 3 +- docs/reference/add_ui_body.html | 5 +- docs/reference/add_ui_footer.html | 5 +- docs/reference/add_ui_header.html | 5 +- docs/reference/add_ui_left_sidebar.html | 5 +- docs/reference/add_ui_right_sidebar.html | 5 +- .../announcementConfigurationsAddin.html | 5 +- docs/reference/appReset.html | 5 +- docs/reference/appResetButton.html | 5 +- docs/reference/createPSAlert.html | 5 +- docs/reference/create_application.html | 13 +- docs/reference/create_left_sidebar.html | 5 +- docs/reference/create_right_sidebar.html | 5 +- docs/reference/downloadFile.html | 15 +- docs/reference/downloadFileButton.html | 7 +- .../downloadFile_AvailableTypes.html | 5 +- .../reference/downloadFile_ValidateTypes.html | 5 +- docs/reference/downloadablePlot.html | 11 +- docs/reference/downloadablePlotUI.html | 11 +- docs/reference/downloadableReactTable.html | 276 +++++++++++ docs/reference/downloadableReactTableUI.html | 238 ++++++++++ docs/reference/downloadableTable.html | 5 +- docs/reference/downloadableTableUI.html | 5 +- docs/reference/get_url_parameters.html | 5 +- docs/reference/index.html | 13 +- docs/reference/load_announcements.html | 5 +- docs/reference/logViewerOutput.html | 40 +- docs/reference/logging-entrypoints.html | 5 +- docs/reference/periscope2.html | 5 +- docs/reference/set_app_parameters.html | 7 +- docs/reference/themeConfigurationsAddin.html | 5 +- docs/reference/ui_tooltip.html | 5 +- docs/sitemap.xml | 3 + inst/WORDLIST | 12 +- inst/fw_templ/p_example/custom.css | 33 ++ inst/fw_templ/p_example/custom.js | 25 + inst/fw_templ/p_example/global.R | 1 + inst/fw_templ/p_example/plots.R | 6 +- inst/fw_templ/p_example/program_helpers.R | 1 + inst/fw_templ/p_example/server_local.R | 37 ++ inst/fw_templ/p_example/ui_body.R | 50 +- .../p_example/ui_body_no_left_sidebar.R | 51 ++- inst/fw_templ/www/custom.css | 27 ++ inst/fw_templ/www/custom.js | 25 + man/downloadFile.Rd | 8 +- man/downloadFileButton.Rd | 2 +- man/downloadablePlot.Rd | 6 +- man/downloadablePlotUI.Rd | 6 +- man/downloadableReactTable.Rd | 136 ++++++ man/downloadableReactTableUI.Rd | 114 +++++ man/logViewerOutput.Rd | 7 +- man/set_app_parameters.Rd | 2 +- periscope2.Rproj | 1 + .../_snaps/download_file/download.csv | 2 + .../_snaps/download_file/download.tsv | 2 + .../_snaps/download_file/mydownload1.txt | 12 +- .../download_file/show_row_names_download.tsv | 7 + .../download_file/show_row_names_download.txt | 7 + tests/testthat/_snaps/log_viewer.md | 25 +- .../program/fxn/plots.R | 6 +- .../program/fxn/program_helpers.R | 5 +- .../sample_app_both_sidebars/program/global.R | 1 + .../program/server_local.R | 37 ++ .../program/ui_body.R | 50 +- .../www/css/custom.css | 27 ++ .../sample_app_both_sidebars/www/js/custom.js | 25 + .../program/fxn/plots.R | 6 +- .../program/fxn/program_helpers.R | 5 +- .../sample_app_left_sidebar/program/global.R | 1 + .../program/server_local.R | 37 ++ .../sample_app_left_sidebar/program/ui_body.R | 50 +- .../www/css/custom.css | 27 ++ .../sample_app_left_sidebar/www/js/custom.js | 25 + .../program/fxn/plots.R | 6 +- .../program/fxn/program_helpers.R | 5 +- .../program/global.R | 1 + .../program/server_local.R | 37 ++ .../program/ui_body.R | 51 ++- .../www/css/custom.css | 27 ++ .../www/js/custom.js | 25 + .../program/fxn/plots.R | 6 +- .../program/fxn/program_helpers.R | 5 +- .../sample_app_right_sidebar/program/global.R | 1 + .../program/server_local.R | 37 ++ .../program/ui_body.R | 51 ++- .../www/css/custom.css | 26 ++ .../sample_app_right_sidebar/www/js/custom.js | 25 + tests/testthat/setup.R | 22 + tests/testthat/test_download_file.R | 184 ++++++-- tests/testthat/test_downloadable_plot.R | 8 +- .../testthat/test_downloadable_react_table.R | 433 ++++++++++++++++++ tests/testthat/test_downloadable_table.R | 80 ++-- tests/testthat/test_log_viewer.R | 21 +- tests/testthat/test_logger.R | 125 ++++- tests/testthat/test_ui_functions.R | 94 ++-- vignettes/announcement-module.Rmd | 1 + vignettes/announcement_addin.Rmd | 1 + vignettes/applicationReset-module.Rmd | 1 + vignettes/downloadFile-module.Rmd | 4 + vignettes/downloadablePlot-module.Rmd | 1 + vignettes/downloadableReactTable-module.Rmd | 165 +++++++ vignettes/downloadableTable-module.Rmd | 1 + .../figures/downloadableReactTable-1.png | Bin 0 -> 25216 bytes vignettes/logViewer-module.Rmd | 6 + vignettes/new-application.Rmd | 1 + vignettes/themeBuilder_addin.Rmd | 1 + 136 files changed, 3897 insertions(+), 359 deletions(-) create mode 100644 R/downloadableReactTable.R create mode 100644 docs/articles/downloadableReactTable-module.html create mode 100644 docs/articles/figures/downloadableReactTable-1.png create mode 100644 docs/reference/downloadableReactTable.html create mode 100644 docs/reference/downloadableReactTableUI.html create mode 100644 man/downloadableReactTable.Rd create mode 100644 man/downloadableReactTableUI.Rd create mode 100644 tests/testthat/_snaps/download_file/download.csv create mode 100644 tests/testthat/_snaps/download_file/download.tsv create mode 100644 tests/testthat/_snaps/download_file/show_row_names_download.tsv create mode 100644 tests/testthat/_snaps/download_file/show_row_names_download.txt create mode 100644 tests/testthat/test_downloadable_react_table.R create mode 100644 vignettes/downloadableReactTable-module.Rmd create mode 100644 vignettes/figures/downloadableReactTable-1.png diff --git a/.circleci/config.yml b/.circleci/config.yml index 3ecba496..741d3bfb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -38,6 +38,8 @@ jobs: R -e 'install.packages("bs4Dash", repos = "https://cran.rstudio.com/")' R -e 'install.packages(c("shinyWidgets", "yaml", "DT", "writexl", "fresh", "miniUI", "shinyFeedback"))' R -e 'install.packages(c("canvasXpress", "waiter", "shinyjs", "openxlsx", "spelling", "colourpicker", "lifecycle"))' + R -e 'install.packages("reactable")' + R -e 'install.packages("openxlsx2", repos = "https://cloud.r-project.org")' - run: name: Session information and installed package versions @@ -80,8 +82,9 @@ jobs: name: Install R dependencies command: | R -e 'install.packages(c("bs4Dash", "shinyWidgets", "yaml", "DT", "writexl", "fresh", "miniUI", "shinyFeedback"))' - R -e 'install.packages(c("canvasXpress", "waiter", "shinyjs", "openxlsx", "spelling", "colourpicker", "lifecycle"))' + R -e 'install.packages(c("canvasXpress", "waiter", "shinyjs", "openxlsx", "openxlsx2", "spelling", "colourpicker", "lifecycle"))' R -e 'install.packages("covr")' + R -e 'install.packages("reactable")' - run: name: Session information and installed package versions diff --git a/DESCRIPTION b/DESCRIPTION index 2a2b1ff6..9a11a5d9 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: periscope2 Type: Package Title: Enterprise Streamlined 'shiny' Application Framework Using 'bs4Dash' -Version: 0.2.4 +Version: 0.3.0 Authors@R: c( person("Mohammed", "Ali", role = c("aut", "cre"), email = "mohammed@aggregate-genius.com"), person("Constance", "Brett", role = c("ctb")), @@ -27,6 +27,7 @@ Imports: grDevices, lubridate, methods, + reactable, shinyFeedback, shinyWidgets, utils, @@ -43,6 +44,7 @@ Suggests: lattice, miniUI, openxlsx, + openxlsx2, rmarkdown, shinyjs, spelling, diff --git a/NAMESPACE b/NAMESPACE index 2ecfe94e..a789ce6b 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -18,6 +18,8 @@ export(downloadFile_AvailableTypes) export(downloadFile_ValidateTypes) export(downloadablePlot) export(downloadablePlotUI) +export(downloadableReactTable) +export(downloadableReactTableUI) export(downloadableTable) export(downloadableTableUI) export(get_url_parameters) diff --git a/NEWS.md b/NEWS.md index 70f30e08..b5196680 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,16 @@ +# periscope2 0.3.0 + +## New Features +- Added new module *?downloadableReactTable* based on reactable package and downloadFile module + +## Enhancements +- Added openxlsx2 support while keeping openxlsx to support legacy apps and backwards compatibility. writexl package is used as last resort. +- Updated *?logViewerOutput* module to display log data using *?downloadableReactTable*. +- Added loglevel filtering according to level chosen- Added loglevel filtering according to level chosen +- Added new feature to *?downloadFile* module to control whether row names are to be written for tabular data. + +----- + # periscope2 0.2.4 ## Enhancements diff --git a/R/downloadFile.R b/R/downloadFile.R index c72c3da9..bf4681b9 100644 --- a/R/downloadFile.R +++ b/R/downloadFile.R @@ -61,7 +61,7 @@ #' logger = "", #' filenameroot = "mydownload1", #' datafxns = list(csv = reactiveVal(iris)), -#' aspectratio = 1) +#' row_names = FALSE) #' # multiple download types #' downloadFile(id = "object_id2", #' logger = "", @@ -119,6 +119,14 @@ downloadFileButton <- function(id, file_button_ui } +check_openxlsx2_availability <- function() { + length(find.package("openxlsx2", quiet = TRUE)) > 0 +} + +check_openxlsx_availability <- function() { + length(find.package("openxlsx", quiet = TRUE)) > 0 +} + #' downloadFile module server function #' @@ -137,6 +145,8 @@ downloadFileButton <- function(id, #' @param aspectratio the downloaded chart image width:height ratio (ex: #' 1 = square, 1.3 = 4:3, 0.5 = 1:2). Where not applicable for a download type #' it is ignored (e.g. data downloads). +#' @param row_names logical value indicating whether row names are to be written +#' for tabular data. Where not applicable for a download type it is ignored. #' #' @return no return value, called for downloading selected file type #' @@ -174,7 +184,7 @@ downloadFileButton <- function(id, #' logger = "", #' filenameroot = "mydownload1", #' datafxns = list(csv = reactiveVal(iris)), -#' aspectratio = 1) +#' row_names = FALSE) #' # multiple download types #' downloadFile(id = "object_id2", #' logger = "", @@ -189,7 +199,8 @@ downloadFile <- function(id, logger = NULL, filenameroot = "download", datafxns = NULL, - aspectratio = 1) { + aspectratio = 1, + row_names = TRUE) { shiny::moduleServer( id, function(input, output, session) { @@ -204,8 +215,12 @@ downloadFile <- function(id, filename = shiny::reactive({paste(rootname(), "csv", sep = ".")}), content = function(file) { if (!is.null(datafxns)) { - writeFile("csv", datafxns$csv(), file, logger, - shiny::reactive({paste(rootname(), "csv", sep = ".")})) + writeFile(type = "csv", + data = datafxns$csv(), + file = file, + show_rownames = row_names, + logger = logger, + filename = shiny::reactive({paste(rootname(), "csv", sep = ".")})) } }) @@ -213,8 +228,12 @@ downloadFile <- function(id, filename = shiny::reactive({paste(rootname(), "xlsx", sep = ".")}), content = function(file) { if (!is.null(datafxns)) { - writeFile("xlsx", datafxns$xlsx(), file, logger, - shiny::reactive({paste(rootname(), "xlsx", sep = ".")})) + writeFile(type = "xlsx", + data = datafxns$xlsx(), + file = file, + show_rownames = row_names, + logger = logger, + filename = shiny::reactive({paste(rootname(), "xlsx", sep = ".")})) } }) @@ -222,8 +241,12 @@ downloadFile <- function(id, filename = shiny::reactive({paste(rootname(), "tsv", sep = ".")}), content = function(file) { if (!is.null(datafxns)) { - writeFile("tsv", datafxns$tsv(), file, logger, - shiny::reactive({paste(rootname(), "tsv", sep = ".")})) + writeFile(type = "tsv", + data = datafxns$tsv(), + file = file, + show_rownames = row_names, + logger = logger, + filename = shiny::reactive({paste(rootname(), "tsv", sep = ".")})) } }) @@ -231,17 +254,31 @@ downloadFile <- function(id, filename = shiny::reactive({paste(rootname(), "txt", sep = ".")}), content = function(file) { if (!is.null(datafxns)) { - writeFile("txt", datafxns$txt(), file, logger, - shiny::reactive({paste(rootname(), "txt", sep = ".")})) + writeFile(type = "txt", + data = datafxns$txt(), + file = file, + show_rownames = row_names, + logger = logger, + filename = shiny::reactive({paste(rootname(), "txt", sep = ".")})) } }) - # filename is expected to be a reactive expression - writeFile <- function(type, data, file, logger, filename) { + + ## writeFile + ## + ## @param type type of file to write + ## @param data data to write + ## @param file file path to write to + ## @param show_rownames if TRUE, row names are written for tabular data + ## @param logger logger to use + ## @param filename name of downloaded file, expected to be a reactive expression + ## + ## @returns no return value + writeFile <- function(type, data, file, show_rownames, logger, filename) { + show_rownames <- isTRUE(show_rownames) + # tabular values if ((type == "csv") || (type == "tsv")) { - show_rownames <- attr(data, "show_rownames") - show_rownames <- !is.null(show_rownames) && show_rownames show_colnames <- TRUE if (show_rownames) { show_colnames <- NA @@ -256,17 +293,32 @@ downloadFile <- function(id, } # excel file else if (type == "xlsx") { - if (length(find.package("openxlsx", quiet = TRUE) > 0)) { - if ((inherits(data, "Workbook")) && ("openxlsx" %in% attributes(class(data)))) { - openxlsx::saveWorkbook(data, file) + + if (inherits(data, "wbWorkbook") && check_openxlsx2_availability()) { + openxlsx2::wb_save(data, file) + } else if (inherits(data, "Workbook") && + ("openxlsx" %in% attributes(class(data))) && + check_openxlsx_availability()) { + openxlsx::saveWorkbook(data, file) + } else { + # tabular data + if (check_openxlsx2_availability()) { + openxlsx2::write_xlsx(data, + file, + as_table = TRUE, + row_names = show_rownames) + } else if (check_openxlsx_availability()) { + openxlsx::write.xlsx(data, + file, + asTable = TRUE, + rowNames = show_rownames) } else { - show_rownames <- attr(data, "show_rownames") - openxlsx::write.xlsx(data, file, - asTable = TRUE, - rowNames = !is.null(show_rownames) && show_rownames) + if (show_rownames) { + logwarn("Downloading with 'writexl', row names are not included", + logger = logger) + } + writexl::write_xlsx(data, file) } - } else { - writexl::write_xlsx(data, file) } } # text file processing @@ -274,7 +326,7 @@ downloadFile <- function(id, if (inherits(data, "character")) { writeLines(data, file) } else if (is.data.frame(data) || is.matrix(data)) { - utils::write.table(data, file) + utils::write.table(data, file, row.names = show_rownames) } else { msg <- paste(type, "could not be processed") logwarn(msg) diff --git a/R/downloadablePlot.R b/R/downloadablePlot.R index 4c8cc6f1..32c50932 100644 --- a/R/downloadablePlot.R +++ b/R/downloadablePlot.R @@ -74,9 +74,9 @@ #' download_plot <- function() { #' ggplot(data = mtcars, aes(x = wt, y = mpg)) + #' geom_point(aes(color = cyl)) + -#' theme(legend.justification = c(1, 1), -#' legend.position = c(1, 1), -#' legend.title = element_blank()) + +#' theme(legend.justification = c(1, 1), +#' legend.position.inside = c(1, 1), +#' legend.title = element_blank()) + #' ggtitle("GGPlot Example ") + #' xlab("wt") + #' ylab("mpg") @@ -217,9 +217,9 @@ downloadablePlotUI <- function(id, #' download_plot <- function() { #' ggplot(data = mtcars, aes(x = wt, y = mpg)) + #' geom_point(aes(color = cyl)) + -#' theme(legend.justification = c(1, 1), -#' legend.position = c(1, 1), -#' legend.title = element_blank()) + +#' theme(legend.justification = c(1, 1), +#' legend.position.inside = c(1, 1), +#' legend.title = element_blank()) + #' ggtitle("GGPlot Example ") + #' xlab("wt") + #' ylab("mpg") @@ -244,13 +244,10 @@ downloadablePlot <- function(id, id, function(input, output, session) { downloadFile("dplotButtonID", logger, filenameroot, downloadfxns, aspectratio) - dpInfo <- shiny::reactiveValues(visibleplot = NULL, - downloadfxns = NULL) shiny::observe({ - dpInfo$visibleplot <- visibleplot() output$dplotOutputID <- shiny::renderPlot({ - plot <- dpInfo$visibleplot + plot <- visibleplot() if (inherits(plot, "grob")) { plot <- grid::grid.draw(plot) } @@ -259,15 +256,6 @@ downloadablePlot <- function(id, }) shiny::observe({ - if (length(downloadfxns) > 0) { - dpInfo$downloadfxns <- lapply(downloadfxns, do.call, list()) - rowct <- lapply(dpInfo$downloadfxns, is.null) - session$sendCustomMessage( - "downloadbutton_toggle", - message = list(btn = session$ns("dplotButtonDiv"), - rows = sum(unlist(rowct) == FALSE)) ) - } - output$displayButton <- shiny::reactive(length(downloadfxns) > 0) shiny::outputOptions(output, "displayButton", suspendWhenHidden = FALSE) }) diff --git a/R/downloadableReactTable.R b/R/downloadableReactTable.R new file mode 100644 index 00000000..6f88b9f8 --- /dev/null +++ b/R/downloadableReactTable.R @@ -0,0 +1,364 @@ +# ------------------------------------------------- +# -- Application Downloadable React Table Module -- +# ------------------------------------------------- + + +#' downloadableReactTable module UI function +#' +#' downloadableReactTable module is extending \code{?reactable} package table functions by creating +#' a custom high-functionality table paired with \link[periscope2]{downloadFile} button. +#' The table has the following default functionality:search, highlight functionality, infinite scrolling, sorting by columns and +#' returns a reactive dataset of selected items and table current state. +#' +#' \link[periscope2]{downloadFile} button will be hidden if \code{downloadableReactTableUI} parameter +#' \code{downloadtypes} is empty +#' +#' @param id character id for the object +#' @param downloadtypes vector of values for data download types +#' @param hovertext download button tooltip hover text +#' +#' @return list of downloadFileButton UI and reactable table and hidden inputs for contentHeight option +#' +#' @section Table Features: +#' \itemize{ +#' \item Consistent styling of the table +#' \item downloadFile module button functionality built-in to the table (it will be shown only if downloadtypes is defined) +#' \item Ability to show different data from the download data +#' \item Table is automatically fit to the window size with infinite +#' y-scrolling +#' \item Table search functionality including highlighting built-in +#' \item Multi-select built in, including reactive feedback on which table +#' items are selected +#' } +#' +#' @section Example: +#' \code{downloadableReactTableUI("mytableID", c("csv", "tsv"), +#' "Click Here")} +#' +#' @section Notes: +#' When there are no rows to download in any of the linked downloaddatafxns the +#' button will be hidden as there is nothing to download. +#' +#' @section Shiny Usage: +#' Call this function at the place in ui.R where the table should be placed. +#' +#' Paired with a call to \code{downloadableReactTable(id, ...)} +#' in server.R +#' +#' @seealso \link[periscope2]{downloadableReactTable} +#' @seealso \link[periscope2]{downloadFile} +#' @seealso \link[periscope2]{logViewerOutput} +#' @seealso \link[periscope2]{downloadFile} +#' @seealso \link[periscope2]{downloadFile_ValidateTypes} +#' @seealso \link[periscope2]{downloadFile_AvailableTypes} +#' @seealso \link[periscope2]{downloadablePlot} +#' +#' @examples +#' if (interactive()) { +#' library(shiny) +#' library(periscope2) +#' library(reactable) +#' +#' shinyApp( +#' ui = fluidPage(fluidRow(column( +#' width = 12, +#' downloadableReactTableUI( +#' id = "object_id1", +#' downloadtypes = c("csv", "tsv"), +#' hovertext = "Download the data here!")))), +#' server = function(input, output) { +#' table_state <- downloadableReactTable( +#' id = "object_id1", +#' table_data = reactiveVal(iris), +#' download_data_fxns = list(csv = reactiveVal(iris), tsv = reactiveVal(iris)), +#' selection_mode = "multiple", +#' pre_selected_rows = function() {c(1, 3, 5)}, +#' table_options = list(columns = list( +#' Sepal.Length = colDef(name = "Sepal Length"), +#' Sepal.Width = colDef(filterable = TRUE), +#' Petal.Length = colDef(show = FALSE), +#' Petal.Width = colDef(defaultSortOrder = "desc")), +#' showSortable = TRUE, +#' theme = reactableTheme( +#' borderColor = "#dfe2e5", +#' stripedColor = "#f6f8fa", +#' highlightColor = "#f0f5f9", +#' cellPadding = "8px 12px"))) +#' +#' observeEvent(table_state(), { print(table_state()) }) +#' }) +#' } +#' +#' @export +downloadableReactTableUI <- function(id, + downloadtypes = NULL, + hovertext = NULL) { + ns <- shiny::NS(id) + list( + shiny::conditionalPanel( + condition = "output.displayButton", + ns = ns, + shiny::span( + id = ns("reactTableButtonDiv"), + class = "periscope-downloadable-react-table-button", + style = ifelse(length(downloadtypes) > 0, "", "display:none"), + downloadFileButton(ns("reactTableButtonID"), + downloadtypes, + hovertext))), + reactable::reactableOutput(ns("reactTableOutputID")) + ) +} + + +#' downloadableReactTable module server function +#' +#' Server-side function for the downloadableReactTableUI. +#' +#' +#' @param id the ID of the Module's UI element +#' @param table_data reactive expression (or parameter-less function) that acts as table data source +#' @param selection_mode to enable row selection, set \code{selection_mode} value to either "single" for single row +#' selection or "multiple" for multiple rows selection, case insensitive. Any other value will +#' disable row selection. Row selection will be enabled by radio buttons in "single" selection +#' and checkboxes in "multiple" selection (default = NULL) +#' @param pre_selected_rows reactive expression (or parameter-less function) provides the rows indices of the rows to +#' be selected when the table is rendered. If selection_mode is disabled, this parameter will +#' have no effect. If selection_mode is "single" only the first row index will be used (default = NULL) +#' @param file_name_root the base name used for user-downloaded file. It can be either a character string +#' a reactive expression or a function returning a character string (default = 'data_file') +#' @param download_data_fxns a \strong{named} list of functions providing the data as return values. +#' The names for the list should be the same names as the ones used in the +#' downloadableReactTableUI (default = NULL) +#' @param pagination to enable table pagination (default = FALSE) +#' @param table_height max table height in pixels. Vertical scroll will be shown after that height value +#' @param show_rownames enable displaying rownames as a separate column (default = FALSE) +#' @param columns_filter enable the filtering input on each column in the table (default = FALSE) +#' @param global_search enable table global searching input to search and filter in all columns at once +#' (default = TRUE) +#' @param row_highlight enable highlighting rows upon mouse hover (default = TRUE) +#' @param row_striping add zebra-striped style to table rows (default = TRUE) +#' @param table_options optional table formatting parameters check \code{?reactable::reactable} for options full list. +#' Also see example below to see how to pass options (default = list()) +#' @param logger logger to use (default = NULL) +#' +#' @return A named list of two elements: +#' \itemize{ +#' \item selected_rows: data.frame of current selected rows +#' \item table_state: a list of the current table state. The list keys are +#' ("page", "pageSize", "pages", "sorted" and "selected"). +#' Review \code{?reactable::getReactableState} for more info. +#' } +#' +#' +#' @section Shiny Usage: +#' This function is not called directly by consumers - it is accessed in +#' server.R using the same id provided in \code{downloadableReactTableUI}: +#' +#' \strong{\code{downloadableReactTable(id)}} +#' +#' @seealso \link[periscope2]{downloadableReactTableUI} +#' @seealso \link[periscope2]{downloadFileButton} +#' @seealso \link[periscope2]{logViewerOutput} +#' @seealso \link[periscope2]{downloadFile} +#' @seealso \link[periscope2]{downloadFile_ValidateTypes} +#' @seealso \link[periscope2]{downloadFile_AvailableTypes} +#' @seealso \link[periscope2]{downloadablePlot} +#' +#' @examples +#' if (interactive()) { +#' library(shiny) +#' library(periscope2) +#' library(reactable) +#' +#' shinyApp( +#' ui = fluidPage(fluidRow(column( +#' width = 12, +#' downloadableReactTableUI( +#' id = "object_id1", +#' downloadtypes = c("csv", "tsv"), +#' hovertext = "Download the data here!")))), +#' server = function(input, output) { +#' table_state <- downloadableReactTable( +#' id = "object_id1", +#' table_data = reactiveVal(iris), +#' download_data_fxns = list(csv = reactiveVal(iris), tsv = reactiveVal(iris)), +#' selection_mode = "multiple", +#' pre_selected_rows = function() {c(1, 3, 5)}, +#' table_options = list(columns = list( +#' Sepal.Length = colDef(name = "Sepal Length"), +#' Sepal.Width = colDef(filterable = TRUE), +#' Petal.Length = colDef(show = FALSE), +#' Petal.Width = colDef(defaultSortOrder = "desc")), +#' theme = reactableTheme( +#' borderColor = "#dfe2e5", +#' stripedColor = "#f6f8fa", +#' highlightColor = "#f0f5f9", +#' cellPadding = "8px 12px"))) +#' +#' observeEvent(table_state(), { print(table_state()) }) +#' }) +#' } +#' +#' @export +downloadableReactTable <- function(id, + table_data, + selection_mode = NULL, + pre_selected_rows = NULL, + file_name_root = "data_file", + download_data_fxns = NULL, + pagination = FALSE, + table_height = 600, + show_rownames = FALSE, + columns_filter = FALSE, + global_search = TRUE, + row_highlight = TRUE, + row_striping = TRUE, + table_options = list(), + logger = NULL) { + shiny::moduleServer(id, + function(input, output, session) { + if (is.null(table_data) || !is.function(table_data)) { + logerror("'table_data' parameter must be a function or reactive expression.", logger = logger) + output$reactTableOutputID <- reactable::renderReactable({ NULL }) + } else { + table_react_params <- shiny::reactiveValues(table_data = NULL, + pre_selected_rows = NULL) + if (is.null(file_name_root)) { + logwarn("'file_name_root' parameter should not be NULL. Setting default value 'data_file'.", logger = logger) + file_name_root <- "data_file" + } + + downloadFile(id = "reactTableButtonID", + logger = logger, + filenameroot = file_name_root, + datafxns = download_data_fxns, + row_names = show_rownames) + shiny::observe({ + output$displayButton <- shiny::reactive(length(download_data_fxns) > 0) + shiny::outputOptions(output, "displayButton", suspendWhenHidden = FALSE) + }) + + if (!is.null(pre_selected_rows) && !is.function(pre_selected_rows)) { + logwarn("'pre_selected_rows' parameter must be a function or reactive expression. Setting default value NULL.", logger = logger) + pre_selected_rows <- NULL + } + + shiny::observe({ + if (!is.data.frame(table_data())) { + table_data <- shiny::reactiveVal(data.frame(table_data())) + } + + table_react_params$table_data <- table_data() + output$displayButton <- shiny::reactive((length(download_data_fxns) > 0)) + + if (NROW(table_react_params$table_data) == 0) { + output$displayButton <- shiny::reactive(FALSE) + + } + shiny::outputOptions(output, "displayButton", suspendWhenHidden = FALSE) + }) + + shiny::observe({ + table_react_params$pre_selected_rows <- NULL + if ((is.null(selection_mode) || !(tolower(selection_mode) %in% c("single", "multiple"))) && + !is.null(pre_selected_rows)) { + logwarn("'selection_mode' parameter must be either 'single' or 'multiple' to use 'pre_selected_rows' param. Setting default value NULL.", + logger = logger) + } else if (!is.null(pre_selected_rows) && !is.numeric(pre_selected_rows())) { + logwarn("'pre_selected_rows' parameter must be a function or reactive expression that returns numeric vector. Setting default value NULL.", + logger = logger) + } else if (!is.null(pre_selected_rows)) { + table_react_params$pre_selected_rows <- pre_selected_rows() + } + + }) + output$reactTableOutputID <- reactable::renderReactable({ + table_output <- NULL + + if (!all(is.na(table_react_params$table_data)) && + !is.null(table_react_params$table_data) && + (NCOL(table_react_params$table_data) > 0)) { + row_selection_mode <- NULL + if (!is.null(selection_mode) && (tolower(selection_mode) %in% c("single", "multiple"))) { + row_selection_mode <- tolower(selection_mode) + + if ((row_selection_mode == "single") && (length(table_react_params$pre_selected_rows) > 1)) { + logwarn("when 'selection_mode' is 'single', only first value of 'pre_selected_rows' will be used", + logger = logger) + table_react_params$pre_selected_rows <- table_react_params$pre_selected_rows[1] + } + + if (length(table_react_params$pre_selected_rows) > 0) { + excluded_indices <- table_react_params$pre_selected_rows[ + sapply(table_react_params$pre_selected_rows, + function(x) { + x < 1 || x > NROW(table_react_params$table_data) + })] + if (length(excluded_indices) > 0) { + if (length(excluded_indices) == length(table_react_params$pre_selected_rows)) { + logwarn("All 'pre_selected_rows' values are out of range. Setting default value NULL.", + logger = logger) + table_react_params$pre_selected_rows <- NULL + } else { + value_msg <- paste("value:", excluded_indices[1] ,"as it is") + if (length(excluded_indices) > 1) { + value_msg <- paste("values:", paste0(excluded_indices, collapse = ", "), "as they are") + } + + logwarn(paste("Excluding 'pre_selected_rows'" , value_msg, "out of range."), + logger = logger) + table_react_params$pre_selected_rows <- table_react_params$pre_selected_rows[ + !(table_react_params$pre_selected_rows %in% excluded_indices)] + } + } + } + } + + table_arguments <- list(data = table_react_params$table_data, + selection = row_selection_mode, + defaultSelected = table_react_params$pre_selected_rows, + pagination = pagination, + height = table_height, + rownames = show_rownames, + filterable = columns_filter, + searchable = global_search, + highlight = row_highlight, + striped = row_striping) + if (length(table_options) > 0) { + all_options <- methods::formalArgs(reactable::reactable) + unnamed_options <- append(table_options[names(table_options) == ""], + table_options[is.null(names(table_options))]) + invalid_options <- table_options[!(names(table_options) %in% all_options)] + invalid_options <- invalid_options[!(invalid_options %in% unnamed_options)] + valid_options <- table_options[names(table_options) %in% all_options] + + if (length(unnamed_options) > 0) { + logwarn(paste("Excluding the following unnamed option(s):", + paste(unnamed_options, collapse = ", ")), logger = logger) + } + + if (length(invalid_options) > 0) { + logwarn(paste("Excluding the following invalid option(s):", + paste(names(invalid_options), collapse = ", ")), logger = logger) + } + + table_arguments <- append(table_arguments, valid_options) + } + + table_output <- do.call(reactable::reactable, table_arguments) + + } + table_output + }) + } + shiny::reactive({ + table_state <- reactable::getReactableState("reactTableOutputID") + selected_rows <- NULL + if (!is.null(table_state) && !is.null(table_state$selected) && is.data.frame(table_data())) { + selected_rows <- table_data()[table_state$selected, ] + } + list(selected_rows = selected_rows, table_state = table_state) + }) + } + ) +} diff --git a/R/downloadableTable.R b/R/downloadableTable.R index 923291ca..8329c3f3 100644 --- a/R/downloadableTable.R +++ b/R/downloadableTable.R @@ -235,16 +235,15 @@ downloadableTable <- function(id, filenameroot <- shiny::isolate(filenameroot()) } - downloadFile("dtableButtonID", logger, filenameroot, downloaddatafxns) + downloadFile(id = "dtableButtonID", + logger = logger, + filenameroot = filenameroot, + datafxns = downloaddatafxns, + row_names = table_options$rownames) - session$sendCustomMessage("downloadbutton_toggle", - message = list(btn = session$ns("dtableButtonDiv"), - rows = -1)) - - dtInfo <- shiny::reactiveValues(selection = NULL, - selected = NULL, - tabledata = NULL, - downloaddatafxns = NULL) + dtInfo <- shiny::reactiveValues(selection = NULL, + selected = NULL, + tabledata = NULL) shiny::observe({ result <- list(mode = ifelse(input$dtableSingleSelect == "TRUE", "single", "multiple")) @@ -269,14 +268,6 @@ downloadableTable <- function(id, }) shiny::observe({ - if (length(downloaddatafxns) > 0) { - dtInfo$downloaddatafxns <- lapply(downloaddatafxns, do.call, list()) - - rowct <- lapply(dtInfo$downloaddatafxns, NROW) - session$sendCustomMessage("downloadbutton_toggle", - message = list(btn = session$ns("dtableButtonDiv"), - rows = sum(unlist(rowct)))) - } output$displayButton <- shiny::reactive(length(downloaddatafxns) > 0) shiny::outputOptions(output, "displayButton", suspendWhenHidden = FALSE) }) diff --git a/R/logViewer.R b/R/logViewer.R index 96e4910d..7324d79e 100644 --- a/R/logViewer.R +++ b/R/logViewer.R @@ -5,13 +5,14 @@ #' Display app logs #' -#' Creates a shiny table with table containing logged user actions. Table contents are auto updated whenever a user action is -#' logged. The id must match the same id configured in \bold{server.R} file upon calling \code{fw_server_setup} method +#' Display app log data in downloadableReactTable table containing logged user actions. Table contents are auto updated +#' whenever a user action is logged. User can search for logs, sort them by time and download them in CSV or TSV format. +#' The id must match the same id configured in \bold{server.R} file upon calling \code{fw_server_setup} method #' #' #' @param id character id for the object(default = "logViewer") #' -#' @return shiny tableOutput instance +#' @return downloadableReactTableUI instance #' #' @section Table columns: #' \itemize{ @@ -44,7 +45,9 @@ #' @seealso \link[periscope2]{downloadableTable} logViewerOutput <- function(id = "logViewer") { ns <- shiny::NS(id) - shiny::tableOutput(ns(id)) + downloadableReactTableUI(id = ns(id), + downloadtypes = c("csv", "tsv"), + hovertext = "Download application logs") } @@ -57,7 +60,7 @@ logViewerOutput <- function(id = "logViewer") { #' @param id - the ID of the Module's UI element #' @param logger - action logs to be displayed #' -#' @return Shiny table render expression containing the currently logged lines +#' @return downloadableReactTable instance with logged lines #' #' #' @section Shiny Usage: @@ -68,8 +71,10 @@ logViewer <- function(id = "logViewer", logger) { shiny::moduleServer( id, function(input, output, session) { - output[[id]] <- shiny::renderTable({ - lines <- logger() + get_log_data <- function() { + log_data <- data.frame() + lines <- logger() + if (length(lines) > 0) { out1 <- data.frame(orig = lines, stringsAsFactors = F) loc1 <- regexpr("\\[", out1$orig) @@ -83,10 +88,16 @@ logViewer <- function(id = "logViewer", logger) { out1$action <- substring(out1$orig, loc2 + 1) out1$action <- trimws(out1$action, "both") - data.frame(action = out1$action, - time = format(out1$timestamp, - format = .g_opts$datetime.fmt)) + log_data <- data.frame(action = out1$action, + time = format(out1$timestamp, + format = .g_opts$datetime.fmt)) } - }) + log_data + } + + downloadableReactTable(id = id, + table_data = get_log_data, + download_data_fxns = list(csv = get_log_data, tsv = get_log_data), + file_name_root = "log_data") }) } diff --git a/R/logger.R b/R/logger.R index 73dd8630..301aeda5 100644 --- a/R/logger.R +++ b/R/logger.R @@ -622,6 +622,17 @@ updateOptions.Logger <- function(container, ...) { updateOptions.environment(container, ...) } +## Filtering log messages according to chosen level +## +logging_level <- function(level_name, current_log_level) { + switch(current_log_level, + "INFO" = level_name %in% c("INFO", "WARNING", "ERROR"), + "WARNING" = level_name %in% c("WARNING", "ERROR"), + "ERROR" = level_name == "ERROR", + TRUE + ) +} + ## ## Predefined(sample) handler actions ## @@ -679,9 +690,13 @@ writeToConsole <- function(msg, handler, ...) { stopifnot(length(list(...)) > 0) - level_name <- list(...)[[1]]$levelname - msg <- handler$color_msg(msg, level_name) - cat(paste0(msg, "\n")) + current_log_level <- fw_get_loglevel() + level_name <- list(...)[[1]]$levelname + + if (logging_level(level_name, current_log_level)) { + msg <- handler$color_msg(msg, level_name) + cat(paste0(msg, "\n")) + } } .build_msg_coloring <- function() { @@ -727,7 +742,13 @@ writeToConsole <- function(msg, handler, ...) { writeToFile <- function(msg, handler, ...) { if (length(list(...)) && "dry" %in% names(list(...))) return(exists("file", envir = handler)) - cat(paste0(msg, "\n"), file = with(handler, file), append = TRUE) + + current_log_level <- fw_get_loglevel() + level_name <- list(...)[[1]]$levelname + + if (logging_level(level_name, current_log_level)) { + cat(paste0(msg, "\n"), file = with(handler, file), append = TRUE) + } } ## the single predefined formatter diff --git a/R/ui_helpers.R b/R/ui_helpers.R index 858989f7..f2edfe13 100644 --- a/R/ui_helpers.R +++ b/R/ui_helpers.R @@ -567,7 +567,7 @@ ui_tooltip <- function(id, #' application title.} #' \item{Supplying \strong{NULL} will disable the title link functionality.} #' } -#' @param log_level Designating the log level to use for the user log as 'DEBUG','INFO', 'WARN' or 'ERROR' (default = 'DEBUG') +#' @param log_level Designating the log level to use for the user log as 'DEBUG','INFO', 'WARNING' or 'ERROR' (default = 'DEBUG') #' @param app_version Character string designating the application version (default = '1.0.0') #' @param loading_indicator It uses waiter (see https://waiter.john-coene.com/#/).\cr #' Pass a list like list(html = spin_1(), color = "#333e48") to \cr configure diff --git a/README.md b/README.md index 554b6395..e334e3d3 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ output: * Predefined but flexible templates for new Shiny applications with a default [bs4Dash](https://bs4dash.rinterface.com/) layout * Separation by file of functionality that exists in one of the three shiny scopes: global, server-global, and server-local * Generated applications are organized in an easy to follow and maintain folder structure based on files functionality -* Off-the-shelf and ready to be used modules ('Table Downloader', 'Plot Downloader', 'File Downloader' and 'Reset Application' +* Off-the-shelf and ready to be used modules ('Table/React Table Downloader', 'Plot Downloader', 'File Downloader' and 'Reset Application' * Different methods and tools to alert users and add useful information about application UI and server operations * Application logger with different levels and a UI tool to display and review recorded application logs * Application look and feel can be customized easily via 'www/periscope_style.yaml' or more advanced via 'www/css/custom.css' @@ -120,6 +120,7 @@ User can adapt layout for both packages generated apps easily via related functi - Both packages share the following modules: - downloadable file + - Use openxlsx2 to download .xlsx files, openxlsx for legacy apps and backwards compatibility, and writexl as a fallback in case openxlsx2 or openxlsx not installed. - downloadable plot - downloadable table - However, periscope2 has more modules (more to come with each new version) as Announcements module diff --git a/cran-comments.md b/cran-comments.md index 88cceef0..b0e49e7c 100644 --- a/cran-comments.md +++ b/cran-comments.md @@ -12,7 +12,7 @@ RStudio 2023.06.1+524 (Windows 11 x64 (build 22621)) CircleCI * R 4.0.5 -* R 4.5.0 +* R 4.5.1 devtools diff --git a/docs/404.html b/docs/404.html index 9c31e350..da33cbf0 100644 --- a/docs/404.html +++ b/docs/404.html @@ -32,7 +32,7 @@ @@ -60,6 +60,9 @@
vignettes/announcement-module.Rmd
announcement-module.Rmdvignettes/announcement_addin.Rmd
announcement_addin.Rmdvignettes/applicationReset-module.Rmd
applicationReset-module.Rmdvignettes/downloadFile-module.Rmd
downloadFile-module.Rmdvignettes/downloadablePlot-module.Rmd
downloadablePlot-module.Rmdvignettes/downloadableReactTable-module.Rmd
+ downloadableReactTable-module.RmdThe document explains how to use +downloadableReactTable shiny module in periscope2 +applications.
+downloadableReactTable parameter
+download_data_fxns or downloadableReactTableUI
+parameter downloadtypes is emptyShiny modules consist of a pair of functions that modularize, or +package, a small piece of reusable functionality. The UI function is +called directly by the user to place the UI in the correct location (as +with other shiny UI objects). The module server function that is called +only once to set it up using the module name as a function inside the +server function (i.e. user-local session scope. The first function +argument is a string that represents the module id (the same id used in +module UI function). Additional arguments can be supplied by the user +based on the specific shiny module that is called. There can be +additional helper functions that are a part of a shiny module.
+The downloadableReactTable Shiny Module is a part of +the periscope2 package and consists of the following +functions:
+The downloadableReactTableUI function is called from +the ui.R (or equivalent) file in the location where the table should be +placed. This is similar to other UI element placement in shiny.
+The downloadableReactTableUI looks like:
+
+The downloadableReactTableUI function takes the unique object ID for +the UI object.
+The next two arguments (downloadtypes and hovertext) are passed to +the downloadFileButton and set the file types the button will allow the +user to request and the downloadFileButton’s tooltip text.
+
+# Inside ui_body.R or ui_sidebar.R
+
+ downloadableReactTableUI(
+ id = "object_id1",
+ downloadtypes = c("csv", "tsv"),
+ hovertext = "Download the data here!")The downloadableReactTable function is called +directly. The call consists of the following:
+Data Function or Reactive Expression +Requirements
+Reactive Return Value
+* Note that this is the data, not references, rownumbers, etc from the table -- it is the actual, visible, table row data. This allows the developer to use this more easily to update another table, chart, etc. as desired.
+Customization Options
+downloadableReactTable module can be customized using
+reactable function arguments(see ?reactable::reactable).
+These options can be sent as a named options via the server function,
+see example below.
+# Inside server_local.R
+ library(shiny)
+ library(periscope2)
+ library(reactable)
+
+table_state <- downloadableReactTable(
+ id = "object_id1",
+ table_data = reactiveVal(iris),
+ download_data_fxns = list(csv = reactiveVal(iris), tsv = reactiveVal(iris)),
+ selection_mode = "multiple",
+ pre_selected_rows = function() {c(1, 3, 5)},
+ table_options = list(columns = list(
+ Sepal.Length = colDef(name = "Sepal Length"),
+ Sepal.Width = colDef(filterable = TRUE),
+ Petal.Length = colDef(show = FALSE),
+ Petal.Width = colDef(defaultSortOrder = "desc")),
+ showSortable = TRUE,
+ theme = reactableTheme(
+ borderColor = "#dfe2e5",
+ stripedColor = "#f6f8fa",
+ highlightColor = "#f0f5f9",
+ cellPadding = "8px 12px")))
+
+ observeEvent(table_state(), { print(table_state()) })
+
+# NOTE: table_state is the reactive return value, captured for later useFor a complete running shiny example application using the +downloadableReactTable module you can create and run a +periscope2 sample application using:
+
+library(periscope2)
+
+app_dir = tempdir()
+create_new_application(name = 'mysampleapp', location = app_dir, sample_app = TRUE)
+runApp(paste(app_dir, 'mysampleapp', sep = .Platform$file.sep))vignettes/downloadableTable-module.Rmd
downloadableTable-module.RmdIM`0+p$Doh_wO zPRe+3kA2W}NNyDv>3x~$7rvw@>QZ+!&+&=0fdX&;4{Eu^Kld&bS$_u<&s}$iir1AG zMEaC<@mRxt9uoYh7Hn5x%gF>3HJvkv>@}IBve)9c^Bqf}XwY)HFQ=(I)|#F-wuCnQ z3+w-*Zn1Q&C?9f@%_XL!J$7B(%?X}q3gV&>JZ<;XRlcMMGWcNkZ4z>1_axA=UO{Y^ ztvExBc=y1d1j8#me?3}kB~^Ruc41c^BSG#1DT{*4m7l#$_goG`nkC2YDoaK{^xLC~ z<@W8gYGO|kq7SQ$^>#RB)D&jk0V5=iZI(jfew?_qD>e=o`4P132C{Bnsuua3a+fCn zWeyWfwE0rwMH3$55{_;~v7hc}@f=y~ie=sze&NX+N|W)HY2`2f@%uCrM{q;=%DW-* z8xmn8CaAg-X+(GWM#`reT6SbLFA4o#9$cdc}D(DeRbOM#GL&+ z3NPCv;C`uZgk(3C^b;ofM5JdT_mGazqaX=Ft_L!ll!v?Jq2b*hL0!{UIuuA~-clat zUvArFwQCh04QUABtxfARieyWTu)>b9k_ ?^&_`mf#_2#;;v#^ z;B6EdDyMkBcA+~VD==>!&!nu~eq?3wY#Pb>{XT`CnbMJtZtk71eOfMYbQ#?`YCg=v z;>l}%wA0pfN`+xydr 1 Bi9+2 zq0ebf?ZDULdqRcdM1zyno1-i6Mp1)BG}kCe4bib0HzKTkjVELua+I~yYQj?bt{Z&q z=%l4Hpg^%kF0nX!WZm-ky1n8!Z($)`L$6cxsTwwg5sC*G+coly>|NF!BFF)mMc4w# zVaV}MQC9ai;O(~JypS7@QL`ah`rb>!4rd!WH@AWk{!;fraLY$u)u{kHjbyC0e=&Wr zd<8gifnJz_>%(Oy4ntBHxo<0`srYf}S4?|6OCt6%el^;JeAsHfcep1)2Q!=OMbGlp z+y@rSp7V2#^^%ZtP`wYfGEwtDho5%8*})9v{p3I{y3d!fmPdS#uwIFy|1BvIDtOw0 zBSPEmO)<0YPTfP%Jn}kjgCVyNilG+oO;e@RIyi`%DjIi3&HG&jt1w05U7nmd1W?*v zI<;KT{Zx}k1Nvrw7~I|LJE}5J@+2=;k94A*?a5|JfX }?I0&DK@G~Sy{H@@{Tn_u-~jR`+I6Ca^531EMK z-wTqZ;b3QfTT3OnH83@h()RSlSB^j}b7{qR0!Sa#$yWp7h86`@y%gx_7V;eW!AdVQ zMaQ-goro3xUHf*#k;ze)mkii_lwlMoKg+wmJ{MUAZ~uP8ovEhxmk-aw+rqCLd>LC` zW{#?rMOrjqbnLBOS2}{5*tElu(x#K9DJ0U8>_g)Hfbu}w8{u8Wx3}ILyKjlR)82aA ze2yY_g9+ %bO%OiVdCv%pF1DwfCv&X20kWisq z)$N0OZEn-??^sNu$5Z0iXrHdDChF&Q8h#fWvAY-@N=knIi^`#J8DU49taOt8PyB5> zw*#+GiQT`7Es1wlVkCJz(sWBW(;ns>e-o}vXtgNiIG84i!mNp;t#50HKtE9EMZ4-@ zdL(=ti8RJ16bP4&lnHHzO1uR18&fgn1}1-HxHs?0s340dXG<1S(DQ_umR9CnS{0vq z357KsO_%MO-lrw5s?U;aw@qFPT3m(E8hWKvtd C*=-f}29>^6WFy|{9fbBSBL6ML4E zcY2f3JNleYnpphxz^08a3w_9%y}j`iqCwtrPvrP0;Fj{dux#lKRk zD9QJTnm;v@dNUC3T~WFUw|@8MCyVm)w! d zD?FLspAU_vi@T}CH&Z}ETjIc*ySw$zV^Xyhb!oh>pu`8S+#Zu^lGm3-P;_{8_z!7R zx~2^)58Wn2YmW@&OgtS-{zgx(R(yt@nskO0`xq1E`4!P;p(_S+W>IKK)B Q4WYIQ5J<=GC$V3l^-?+iC9rpAEVV*|1>du6^9B*J4Sca>@!+RHV#DXZK9 zrAqwou3U|lhC6EZs23zY)_T+u$a1f2#eEj_hv{8$&0807{CmCQ?E6pQI){pN@N>s$ z#8_-!G$wF5BhWZqYb7~p*4;>N4 KDGl`bZL8|azqb!1pwDr=5NQN+Pq~CYPkWv)02j`LtI2_pbyM;edlS(o-2bJt zvkq!=U(hg64zx&tQi@ygLh&NSin|nd*P_Kew0NOVN^vWGS~NJt-3jh)A-I!Z*^i#x zyL 40hiAX*FfY1W08x06d z1n*s~D^ma~*#hx%@+`m{SX}|$`G2o8{om_If(^wt@G^>uyCl`OWW^4aw0%L}Ok|@(BZT3BA9JX1iSlwTmWop8(SKn_+fVljzqn)@vzb*qkWRq0l}h z;SS>5n7GQKixyc;jR<***(w;@-5eY*d1wnUu9s-dtb;u-YFPJjUUX3Pm-`!d$$p?v zYNYcaSK b|1AT#*&?LTD{Sw8&P)%5 zQUJ;6T%pvY^8i3c6Sm%5FJ1^kVv+q^i9r1ci%ZWR%=ST0gSet$fmw_8dWz|&>wS;D z2YA0DbiQK0V~#rgMvK{<84{g3c%)_#EVmesKs-!^><`1g@u#$SJfw9?;|Y{S5Fe#e zuG@*$3N{fkaoI_VrMAo@7G5MaKDIo8^;pyelrjY(cZO~(HlLLMu!F+tmSb7>=lj2K z2No9!(9);sNe{92>+G;YgzyAj2>2pM00khq5>qc=Ce{a!*u`}19iOmXYo?mm(!$A; z{0HD!XKr(4v|Eg $O>MQl_w>lG!>c%eW9 z( 0c2{yvSkxsp1@5)nYd+r((G#r&gsI5}h6n%hy4o~!ZIE7UMzZ0Y z60%+82u&D7*1 zp@+LZc$a5;vT0*fo(Z~t&t|N8p4ajfOwT2ry$rbTMb$7ai@l; z*vfjb>JbhJRae1LJv}4C(Vv2GKOo)lTEEozWT#ba20scP1USCxOG`Ym%{^D5AUE*+ z F2<+ZyFmipW}_ftPMNuPo=METnLX>q_SA@^jjBX)g#r@ zg$N2GX(ORjdJ(YWOZg?<3^_syBkoku$_f(lETuWGNVbQnTOa;ppm;eSEmxwEoFZW@ zB6fq)yAfr9`RMVEtRz++LNI=Cyyd{%6>%P=sZ!gyyZvf18g!3v8j@bQ12hNo-)YXp z(GjYXqeLvUk;^S{kGhq0EU6Tn>0-hok!2e+TN2@4#JUvM><)rVzt`}PsRk~%8}v-C zDmJUjzvu0zC*kWUwQLb7$jAa69zw>ln&{ML%21-B8AEi`RRZ^6IrFrDDl;zytT<3$ z5F>f*Ot3Op8lQed c<9cEZV*3w1ZlA-^59l*I;{oB?#Q+WMW6IgK8vJ!NuM4Ug?>Ay%S;E=2nx*4P zrjc70;Q@8j3PImiiGdaTIlm8mg)eh+WJi`(cX;ik`uJ|`1ijHfd!}a4QulCNl6-=* zHxv-5Du(YVq#AFZoSKi_^Pt?<$y_Nnu=5S~Zfp7@d1)l|^J?+;WT(w{-}sCeXH{{D zzPLSW|HRL2>E!lt8HWx|q?+1t%xFiXywa48J;k9e7;Y@kH$07v=$bK2LiWAY_7d#f zVV9K)B;Q^M6H=?&r|-*k?Hj`MLS7w-Gyc%1fD7L+r?soPk+~dhNZYSt?FIoQ(KnR@ z!aPgujiisPa-VwsjRC3J?C@UGc@n|)pti3^elo>_v_U1E zpG_al3KYR*^>l%@i%`JZj=`BWA3ry>`qHH2gEdHa+)?PDh2r{T*OWqaf`-n{$XUyu z3$Nh$secfJga%w!qJl6)xXH%9j)pApgm%fpNXj-})GF^-{My^RY+IO_G`!$_8T#$H z$wi4P{4%2^B!gn5M>1w0a)QdvzMFG@p^_el6&X$iTA>+d%v^(Stf%`v{}9O3bGpsg zbJ~#L20o86R5=J6w@fcK%~ZhFSwJll(&x6+vBzLLt<9kFWzAa9Sn;sI5177M+x3iz zr-q`roX}y{qtH2uhp*fGh~n6tq&5#4CM@K7XM%%Vuw~JK3_yaf#2363(E}8zbA1>+ zvEoos+RpZEtQQk41sXGkLl3}dg8O_bSy^nuo%Ur XYA)KFebmK@_-5 zIm%nx^n2=iV$*(iNYG)oCpt}R&3%vU!=3X|wB-f9k&N|gShC&na7(c@)^k^X9M*Wk zWJ?88 }>>xH7_RV3sRvMVTd ABW zpp-Pin(GMe0-=JpZ=dy1lj`X6YXZ%(@(`|o9`IQ?I6E3L(kCp*b~+{S)H!->lwE!> zEmO@?E9O;NKscq0gRcxHCtV7^cK+Bd >@TmOSc zC8*C TA{5wqxWvb&oJz 6a!KSJBgP z@)J{N|IoYog)3=xNW5od>d5VsZyhSUZal!4t~8fwf62b v{r_gUeRRaw;~# zx`m@(9nmdGj#3yw+M;I>*fixC*p~Wz9;XG@!l1cD{hI2(N@&-bHCFVeU76U{9s&=| zZJUbYoWRLR6{|rCBTqb__+dNm!F(D>)n?!v zlgsdiAX%kk?Zc8;$OYva+ihEI_w=qog2%bMEvb>cDag%9&mvMnDhXRD^lT&F6Vp9U zqYV1$-a_GO#!L#=2$Si=E#y1ntL>(?KmzxXc!xdm!4`l}A#$kMqcU^8_YmJSL %mP>&Y-z1RC+fO z*KTPz)TDWU&STAv+ZyV^P3eD#{avS+)VbCz-Jd5ddY*h`xxcGs&YB81wNYMKT;he9 z&R9~aS(%{N4ON<-)VD{L#+ +Q^dO&<30LsBB3?Pa9j$M_Wns?j{w G{0`?2a0MN`LbU2y0f1G%K}_I8hj>k)I@G zw2ppB3ApmC5~>ZMVzgQ^(!4>H*%8peBjZyKiray1nNhR+sB*Ie=>zc$&Yl%r^pI1h z8E?3-|2L+YL0`~b%u?bw{N2d6Dva-JeMifR=Z`jMdn759=hO#ux}x6p1Xdn$xB@k` z%Zg{bd#(sCnhce%*eQF3J<%S32yss4$u;Vyhn;%vtd?8ou^?<{JJlkd{~)?@{N9X_ zqb&F>HpP+s3*wK~jwsi&s4gI&0K5nK+wEuo`vY?jqFlf%AYk (1XI7?@^y^}k}K=D6gX@w^iMG`MphT5HRElQ3s7-PTWBJEC5i zgUb$GK}&{5Z$AJ#Lir3g!EmD>9dch!?9PdD+}gz%jPZoM-MIW{k}H-D?IzU;k8T-2 z!34nQ)|UERLjMC3F%#kbNuXYD)wQu3B`Zz6TFeBuKJ OP~lP^ZM1yalg$$q>o$zzXs?mv;X$M1Qo#U0U5 zCKS(4zN^EyySqv6pMZ+MoNZo}G|wUt*M%^fw}?3I>6eTpEvfeMWC4?-u7W915&liD zN(Mx7Sp4h++UB(0;sg%GT9?UIFjp>&p==Z= s(3DLt3O4D7P)um zT8mtISvS>1B*@EmE<0O|W@XZ^hJY dy5$5$-(V5<^P;0&i_8H|#c2VrgG+fD%UungD)8ybAJ8 z456lkg8iijzngVBxG_^+e|Z#4MR%v$2YJgjgPC<3K=J?s*P=a~)G8n<@cG}7B=K+} zqp&po>`{5>dLSw3xN(lsj;~k^gZRkxS=}^eDW;=efxVGMbVg+ zqaiSEz@C^~Zn^s902Mab9NDj{k$_nboVm#`ag(awy2+cB^k O3^;NMY t(uu+?JYndAJjk9A z*b%CoqW{X-mIkpNroU5=(OPhNZjO>ym=}lpr6sP;it`L{DL }v#JL^E#f2qT_BQwd|B%IIx0H6XJ0U5OqHVh__zex+R_Af8@tdr|NDzMv zT+oZ4(@xY6DoG+79?~iGHSso~RW-}}~P{n0U=?_JdbKuL@D5(UTsQ0k`nYlljLxob3bwtMr^eEP$%ol(M@Bq~v ziCu>|b3IGFM7ePM*fsPzmEcP1!Jw1$hJn#>%vt`mpA~8&`;Scg^{Ke+%k^YW4UG4( zICOi!XXIwJR#!m=|AhYu`LzfEq)Gg2=mY#3^NA>IdGO#=gzJ=Fo5YuW#kzgZxf?xs z@bmGdbM$Ocaedzd4lw5q e?H=LgrbqUAEi9q9ENA3&A;-;)6D((RdVi$|=g?jkrvyJu?0dtM% ziVIT;RnS7}l;WCz<(_W#KM`o697O^bfs>mJqVn1S@!u(GbryxfmnPm3B;0>>Cp+6x zN#fGh#6WR%N?_2!jyc ?Msg-;;(`5$(U%>l6Tm!P2Qn_BmwU|{ ztl0^BBIJ>2*H3dXaV<_>tn>sEXO $)M)?zy`9-5FYjii)MoCor@t zz v=}XB&`bAhim|GBGQp%~v%(^{#Qm=fwdjH=5LlxUwzGrH#n3o+)X*0t zcXder@v>23fv?qf?jqKNU;x(txl^+tYv$Al!*Dr_CWvkzh0~T p}$41{vsep zh%5<_Im5J^y}#YImn9NB$q;W=Zq9N$%S=2V4O+!{LPe^8gqfI8ouEf)7t@=K{XtGd zfKz6z9OhQb;UBMsNILIbi45T{7qjC`zt(TvvUtq0sZ(mQ^Gia--)i%$hKFL=Kk>Pk zSE|_Pw=Z9`1Nlx(3v9!{c`*H5(py-fPkXg`#|oZZ#+Q)8od;%xjj0i|N*CBD4j) zpC$zmRm!q+X!XxntM=p`lID@KyAAgC2Cc-@jHtDXrW>r^|L|}$k#esvqNuN%Fv7M# z6(_R|51m#&_suM`3tV|@sRX9B;K9Fw2A6EJCdlPnIP+{MSeFG)Nnq%N3l;wLn*9x( z{((GhNw=Q~Xg?%#)Vw9&>fM~sv$a0q< H42Wq}CzHtzRgH!`chW#N#v1dc1=%Vt9 zSn_*1rA|md$^?Z0@qe-@_yc?;NE)qOhR8!*yeG&pp>J8ZPvow5SJRyLbntKAX|v6e z=Dh@DNRV%Qd$t?gFHh37d}9`Rt}AbHN4+!$EoVldB%Lb5T^Two5K)F-K_d|hSiIJ@ z6Gq9R{wc{%uUTFU)OI%C&=wF_kiNSPedI8W(R>?L<%&8YQ5x{H$JbI2e5f^f_(EMV zU2mW>^pYS+k9)G_OOrkImb5+nrKjDyZA0);CeQXa(O}}(nd!LdFQGmDkyww*zMe<9 z_@M-}G{+b~U*(LaKjU@8)rFWLzW>*3mEl`Izm&XaxQCs}`3kKVI=~oTdy*|{i1K0I zkY-$=G>v}|iWonb8Y`J3)p}xHEEXtv0xp|rp2kXk2znk 8O-r(jbMM_8V ow;!WjKoRjynAQitJz zecU5wD?Gs=;fIa$X{`-Sc4OIjl8bMxOoU9Ub-73(UQhMF>90RmXV%`nOTz+0A|dL* zz$K#Pt!3X-hXIDU_qyT8F4a@bN7skJ^~aN0b1Zt{I*6hl?6zJrVXvS}Y)(sXlXDsA zosSZ(s+;QDJBk|1MK|0i*VEV|-&vZ84e`|e*iCv($JNOh@LfuW1I-{4I#Kw?H{{PI zU- g+Z@xFh!F=)*LnEu@- zT17zf4wz*JUQ}-XUtG8NdpD_`Lcmptzkv`4afP`;7x$aB8dsn=DN~)q;*v7pUn5ZD MrB$R#B}_m58|m{hd;kCd literal 0 HcmV?d00001 diff --git a/docs/articles/index.html b/docs/articles/index.html index 3d79b0f4..472b0975 100644 --- a/docs/articles/index.html +++ b/docs/articles/index.html @@ -17,7 +17,7 @@ @@ -43,6 +43,9 @@ Using the downloadablePlot Shiny Module ++ Using downloadableReactTable Shiny Module + Using the downloadableTable Shiny Module @@ -95,6 +98,8 @@All vignettes
Using the downloadablePlot Shiny Module + Using downloadableReactTable Shiny Module +Using the downloadableTable Shiny Module Using the downloadFile Shiny Module diff --git a/docs/articles/logViewer-module.html b/docs/articles/logViewer-module.html index 0d1e4b00..ed2098e5 100644 --- a/docs/articles/logViewer-module.html +++ b/docs/articles/logViewer-module.html @@ -32,7 +32,7 @@ @@ -60,6 +60,9 @@Using the downloadablePlot Shiny Module ++ Using downloadableReactTable Shiny Module + Using the downloadableTable Shiny Module @@ -108,7 +111,7 @@Using the logViewer Shiny Module
Mohammed Ali
-2025-04-14
+2025-09-02
Source:vignettes/logViewer-module.Rmd@@ -140,6 +143,16 @@logViewer-module.RmdFeatures
Many actions are automatically logged by the framework and it is easy for developers to add additional items as they see fit. +Filtering logs by setting log_level argument as desired in +set_app_parameters + +
+- “DEBUG” will log logdebug, loginfo, logwarn, and logerror +messages
+- “INFO” will log loginfo, logwarn, and logerror messages
+- “WARNING” will log logwarn and logerror messages
+- “ERROR” will log logerror messages
+It is important to note that the log rolls over for each session and is reset if using the appReset module. @@ -217,6 +230,8 @@Additional ResourcesTheme Configuration Builder +
downloadableReactTable +Module diff --git a/docs/articles/migrate_to_v0_2_0.html b/docs/articles/migrate_to_v0_2_0.html index 0d3072a6..7b0a124a 100644 --- a/docs/articles/migrate_to_v0_2_0.html +++ b/docs/articles/migrate_to_v0_2_0.html @@ -32,7 +32,7 @@ @@ -60,6 +60,9 @@Using the downloadablePlot Shiny Module ++ Using downloadableReactTable Shiny Module + Using the downloadableTable Shiny Module diff --git a/docs/articles/new-application.html b/docs/articles/new-application.html index fb6fb404..e27464ab 100644 --- a/docs/articles/new-application.html +++ b/docs/articles/new-application.html @@ -32,7 +32,7 @@ @@ -60,6 +60,9 @@Using the downloadablePlot Shiny Module ++ Using downloadableReactTable Shiny Module + Using the downloadableTable Shiny Module @@ -108,7 +111,7 @@Creating a new framework-based application
Dr. Connie Brett
-2025-04-14
+2025-09-02
Source:vignettes/new-application.Rmd@@ -789,6 +792,8 @@new-application.Rmdwww/img Builder
Theme Configuration Builder +downloadableReactTable +Module diff --git a/docs/articles/themeBuilder_addin.html b/docs/articles/themeBuilder_addin.html index 439fab93..f0294835 100644 --- a/docs/articles/themeBuilder_addin.html +++ b/docs/articles/themeBuilder_addin.html @@ -32,7 +32,7 @@ @@ -60,6 +60,9 @@Using the downloadablePlot Shiny Module ++ Using downloadableReactTable Shiny Module + Using the downloadableTable Shiny Module @@ -108,7 +111,7 @@Theme Configuration Builder
Mohammed Ali
-2025-04-14
+2025-09-02
Source:vignettes/themeBuilder_addin.Rmd@@ -280,6 +283,8 @@themeBuilder_addin.RmdDownloaded Configuration File Usage
logViewer Module applicationReset Module +downloadableReactTable +Module diff --git a/docs/authors.html b/docs/authors.html index 7a9911b8..47b4b538 100644 --- a/docs/authors.html +++ b/docs/authors.html @@ -17,7 +17,7 @@ @@ -43,6 +43,9 @@Using the downloadablePlot Shiny Module ++ Using downloadableReactTable Shiny Module + Using the downloadableTable Shiny Module @@ -108,13 +111,13 @@Citation
Ali M (2025). periscope2: Enterprise Streamlined 'shiny' Application Framework Using 'bs4Dash'. -R package version 0.2.4, http://periscopeapps.org:3838, https://github.com/Aggregate-Genius/periscope2. +R package version 0.3.0, http://periscopeapps.org:3838, https://github.com/Aggregate-Genius/periscope2.
@Manual{, title = {periscope2: Enterprise Streamlined 'shiny' Application Framework Using 'bs4Dash'}, author = {Mohammed Ali}, year = {2025}, - note = {R package version 0.2.4, http://periscopeapps.org:3838}, + note = {R package version 0.3.0, http://periscopeapps.org:3838}, url = {https://github.com/Aggregate-Genius/periscope2}, }diff --git a/docs/index.html b/docs/index.html index c2113cc5..7f4d5b0a 100644 --- a/docs/index.html +++ b/docs/index.html @@ -33,7 +33,7 @@ @@ -61,6 +61,9 @@Using the downloadablePlot Shiny Module ++ Using downloadableReactTable Shiny Module + Using the downloadableTable Shiny Module @@ -115,7 +118,7 @@OverviewPredefined but flexible templates for new Shiny applications with a default bs4Dash layout
Separation by file of functionality that exists in one of the three shiny scopes: global, server-global, and server-local Generated applications are organized in an easy to follow and maintain folder structure based on files functionality -Off-the-shelf and ready to be used modules (‘Table Downloader’, ‘Plot Downloader’, ‘File Downloader’ and ‘Reset Application’ +Off-the-shelf and ready to be used modules (‘Table/React Table Downloader’, ‘Plot Downloader’, ‘File Downloader’ and ‘Reset Application’ Different methods and tools to alert users and add useful information about application UI and server operations Application logger with different levels and a UI tool to display and review recorded application logs Application look and feel can be customized easily via ‘www/periscope_style.yaml’ or more advanced via ‘www/css/custom.css’ @@ -231,7 +234,11 @@Periscope2 Modules periscope2 - 0.2.4 + 0.3.0 @@ -43,6 +43,9 @@
Using the downloadablePlot Shiny Module ++ Using downloadableReactTable Shiny Module + Using the downloadableTable Shiny Module @@ -85,7 +88,21 @@Changelog
-+periscope2 0.2.4
+periscope2 0.3.0
+++New Features
+
- Added new module ?downloadableReactTable based on reactable package and downloadFile module
+++Enhancements
+
- Added openxlsx2 support while keeping openxlsx to support legacy apps and backwards compatibility. writexl package is used as last resort.
+- Updated ?logViewerOutput module to display log data using ?downloadableReactTable.
+- Added loglevel filtering according to level chosen- Added loglevel filtering according to level chosen
+- Added new feature to ?downloadFile module to control whether row names are to be written for tabular data.
++@@ -44,6 +44,9 @@periscope2 0.2.42025-04-14
@@ -44,6 +44,9 @@Enhancements
- Updated
diff --git a/docs/pkgdown.yml b/docs/pkgdown.yml index ae405c81..a7c6f493 100644 --- a/docs/pkgdown.yml +++ b/docs/pkgdown.yml @@ -6,10 +6,11 @@ articles: announcement-module: announcement-module.html applicationReset-module: applicationReset-module.html downloadablePlot-module: downloadablePlot-module.html + downloadableReactTable-module: downloadableReactTable-module.html downloadableTable-module: downloadableTable-module.html downloadFile-module: downloadFile-module.html logViewer-module: logViewer-module.html migrate_to_v0_2_0: migrate_to_v0_2_0.html new-application: new-application.html themeBuilder_addin: themeBuilder_addin.html -last_built: 2025-04-14T11:09Z +last_built: 2025-09-03T05:05Z diff --git a/docs/reference/add_ui_body.html b/docs/reference/add_ui_body.html index c221244a..519d77f1 100644 --- a/docs/reference/add_ui_body.html +++ b/docs/reference/add_ui_body.html @@ -18,7 +18,7 @@set_app_parametersmethod documentation to displayed html tags correctlyUsing the downloadablePlot Shiny Module ++ Using downloadableReactTable Shiny Module + Using the downloadableTable Shiny Module diff --git a/docs/reference/add_ui_footer.html b/docs/reference/add_ui_footer.html index eaf2d230..8a6e188d 100644 --- a/docs/reference/add_ui_footer.html +++ b/docs/reference/add_ui_footer.html @@ -18,7 +18,7 @@Using the downloadablePlot Shiny Module ++ Using downloadableReactTable Shiny Module + Using the downloadableTable Shiny Module diff --git a/docs/reference/add_ui_header.html b/docs/reference/add_ui_header.html index 499e31eb..4b622156 100644 --- a/docs/reference/add_ui_header.html +++ b/docs/reference/add_ui_header.html @@ -18,7 +18,7 @@ @@ -44,6 +44,9 @@Using the downloadablePlot Shiny Module ++ Using downloadableReactTable Shiny Module + Using the downloadableTable Shiny Module diff --git a/docs/reference/add_ui_left_sidebar.html b/docs/reference/add_ui_left_sidebar.html index d99e64fd..15acdb31 100644 --- a/docs/reference/add_ui_left_sidebar.html +++ b/docs/reference/add_ui_left_sidebar.html @@ -18,7 +18,7 @@ @@ -44,6 +44,9 @@Using the downloadablePlot Shiny Module ++ Using downloadableReactTable Shiny Module + Using the downloadableTable Shiny Module diff --git a/docs/reference/add_ui_right_sidebar.html b/docs/reference/add_ui_right_sidebar.html index b5da2b94..6b3fa7d3 100644 --- a/docs/reference/add_ui_right_sidebar.html +++ b/docs/reference/add_ui_right_sidebar.html @@ -18,7 +18,7 @@ @@ -44,6 +44,9 @@Using the downloadablePlot Shiny Module ++ Using downloadableReactTable Shiny Module + Using the downloadableTable Shiny Module diff --git a/docs/reference/announcementConfigurationsAddin.html b/docs/reference/announcementConfigurationsAddin.html index 693f0578..22b26d60 100644 --- a/docs/reference/announcementConfigurationsAddin.html +++ b/docs/reference/announcementConfigurationsAddin.html @@ -18,7 +18,7 @@ @@ -44,6 +44,9 @@Using the downloadablePlot Shiny Module ++ Using downloadableReactTable Shiny Module + Using the downloadableTable Shiny Module diff --git a/docs/reference/appReset.html b/docs/reference/appReset.html index 738d23ef..51fabd09 100644 --- a/docs/reference/appReset.html +++ b/docs/reference/appReset.html @@ -19,7 +19,7 @@ @@ -45,6 +45,9 @@Using the downloadablePlot Shiny Module ++ Using downloadableReactTable Shiny Module + Using the downloadableTable Shiny Module diff --git a/docs/reference/appResetButton.html b/docs/reference/appResetButton.html index 64e9a7c9..f865f676 100644 --- a/docs/reference/appResetButton.html +++ b/docs/reference/appResetButton.html @@ -20,7 +20,7 @@ @@ -46,6 +46,9 @@Using the downloadablePlot Shiny Module ++ Using downloadableReactTable Shiny Module + Using the downloadableTable Shiny Module diff --git a/docs/reference/createPSAlert.html b/docs/reference/createPSAlert.html index ad4c560a..49afe05e 100644 --- a/docs/reference/createPSAlert.html +++ b/docs/reference/createPSAlert.html @@ -17,7 +17,7 @@ @@ -43,6 +43,9 @@Using the downloadablePlot Shiny Module ++ Using downloadableReactTable Shiny Module + Using the downloadableTable Shiny Module diff --git a/docs/reference/create_application.html b/docs/reference/create_application.html index b072fd4d..24b8e69e 100644 --- a/docs/reference/create_application.html +++ b/docs/reference/create_application.html @@ -19,7 +19,7 @@ @@ -45,6 +45,9 @@Using the downloadablePlot Shiny Module ++ Using downloadableReactTable Shiny Module + Using the downloadableTable Shiny Module @@ -218,25 +221,25 @@Examples
diff --git a/docs/reference/create_left_sidebar.html b/docs/reference/create_left_sidebar.html index 590b78fc..dd2aaa19 100644 --- a/docs/reference/create_left_sidebar.html +++ b/docs/reference/create_left_sidebar.html @@ -17,7 +17,7 @@ @@ -43,6 +43,9 @@# sample app named 'mytestapp' created in a temp dir location <- tempdir() create_application(name = 'mytestapp', location = location, sample_app = TRUE) -#> periscope2 application mytestapp was created successfully at /tmp/RtmpBjjCUk +#> periscope2 application mytestapp was created successfully at /tmp/Rtmpk1yVfS unlink(paste0(location,'/mytestapp'), TRUE) # sample app named 'mytestapp' with a right sidebar using a custom icon created in a temp dir location <- tempdir() create_application(name = 'mytestapp', location = location, sample_app = TRUE, right_sidebar = TRUE) -#> periscope2 application mytestapp was created successfully at /tmp/RtmpBjjCUk +#> periscope2 application mytestapp was created successfully at /tmp/Rtmpk1yVfS unlink(paste0(location,'/mytestapp'), TRUE) # blank app named 'myblankapp' created in a temp dir location <- tempdir() create_application(name = 'myblankapp', location = location) -#> periscope2 application myblankapp was created successfully at /tmp/RtmpBjjCUk +#> periscope2 application myblankapp was created successfully at /tmp/Rtmpk1yVfS unlink(paste0(location,'/myblankapp'), TRUE) # blank app named 'myblankapp' without a left sidebar created in a temp dir location <- tempdir() create_application(name = 'myblankapp', location = location, left_sidebar = FALSE) -#> periscope2 application myblankapp was created successfully at /tmp/RtmpBjjCUk +#> periscope2 application myblankapp was created successfully at /tmp/Rtmpk1yVfS unlink(paste0(location,'/myblankapp'), TRUE)Using the downloadablePlot Shiny Module ++ Using downloadableReactTable Shiny Module + Using the downloadableTable Shiny Module diff --git a/docs/reference/create_right_sidebar.html b/docs/reference/create_right_sidebar.html index c5cbf3dc..be481434 100644 --- a/docs/reference/create_right_sidebar.html +++ b/docs/reference/create_right_sidebar.html @@ -17,7 +17,7 @@ @@ -43,6 +43,9 @@Using the downloadablePlot Shiny Module ++ Using downloadableReactTable Shiny Module + Using the downloadableTable Shiny Module diff --git a/docs/reference/downloadFile.html b/docs/reference/downloadFile.html index a949eef1..9331c5ae 100644 --- a/docs/reference/downloadFile.html +++ b/docs/reference/downloadFile.html @@ -19,7 +19,7 @@ @@ -45,6 +45,9 @@Using the downloadablePlot Shiny Module ++ Using downloadableReactTable Shiny Module + Using the downloadableTable Shiny Module @@ -99,7 +102,8 @@downloadFile module server function
logger = NULL, filenameroot = "download", datafxns = NULL, - aspectratio = 1 + aspectratio = 1, + row_names = TRUE ) @@ -132,6 +136,11 @@Arguments
1 = square, 1.3 = 4:3, 0.5 = 1:2). Where not applicable for a download type it is ignored (e.g. data downloads). + +row_names ++ logical value indicating whether row names are to be written +for tabular data. Where not applicable for a download type it is ignored.
@@ -46,6 +46,9 @@Value
@@ -177,7 +186,7 @@Examples
logger = "", filenameroot = "mydownload1", datafxns = list(csv = reactiveVal(iris)), - aspectratio = 1) + row_names = FALSE) # multiple download types downloadFile(id = "object_id2", logger = "", diff --git a/docs/reference/downloadFileButton.html b/docs/reference/downloadFileButton.html index 61b1d085..5d992a64 100644 --- a/docs/reference/downloadFileButton.html +++ b/docs/reference/downloadFileButton.html @@ -20,7 +20,7 @@Using the downloadablePlot Shiny Module ++ Using downloadableReactTable Shiny Module + Using the downloadableTable Shiny Module @@ -174,7 +177,7 @@Examples
logger = "", filenameroot = "mydownload1", datafxns = list(csv = reactiveVal(iris)), - aspectratio = 1) + row_names = FALSE) # multiple download types downloadFile(id = "object_id2", logger = "", diff --git a/docs/reference/downloadFile_AvailableTypes.html b/docs/reference/downloadFile_AvailableTypes.html index 7a6476e6..573a1a83 100644 --- a/docs/reference/downloadFile_AvailableTypes.html +++ b/docs/reference/downloadFile_AvailableTypes.html @@ -17,7 +17,7 @@ @@ -43,6 +43,9 @@Using the downloadablePlot Shiny Module ++ Using downloadableReactTable Shiny Module + Using the downloadableTable Shiny Module diff --git a/docs/reference/downloadFile_ValidateTypes.html b/docs/reference/downloadFile_ValidateTypes.html index f37569ab..76c1733c 100644 --- a/docs/reference/downloadFile_ValidateTypes.html +++ b/docs/reference/downloadFile_ValidateTypes.html @@ -17,7 +17,7 @@ @@ -43,6 +43,9 @@Using the downloadablePlot Shiny Module ++ Using downloadableReactTable Shiny Module + Using the downloadableTable Shiny Module diff --git a/docs/reference/downloadablePlot.html b/docs/reference/downloadablePlot.html index 6eb200d7..891ef861 100644 --- a/docs/reference/downloadablePlot.html +++ b/docs/reference/downloadablePlot.html @@ -18,7 +18,7 @@ @@ -44,6 +44,9 @@Using the downloadablePlot Shiny Module ++ Using downloadableReactTable Shiny Module + Using the downloadableTable Shiny Module @@ -190,9 +193,9 @@Examples
download_plot <- function() { ggplot(data = mtcars, aes(x = wt, y = mpg)) + geom_point(aes(color = cyl)) + - theme(legend.justification = c(1, 1), - legend.position = c(1, 1), - legend.title = element_blank()) + + theme(legend.justification = c(1, 1), + legend.position.inside = c(1, 1), + legend.title = element_blank()) + ggtitle("GGPlot Example ") + xlab("wt") + ylab("mpg") diff --git a/docs/reference/downloadablePlotUI.html b/docs/reference/downloadablePlotUI.html index 21ae0b5c..854ebf12 100644 --- a/docs/reference/downloadablePlotUI.html +++ b/docs/reference/downloadablePlotUI.html @@ -19,7 +19,7 @@ @@ -45,6 +45,9 @@Using the downloadablePlot Shiny Module ++ Using downloadableReactTable Shiny Module + Using the downloadableTable Shiny Module @@ -226,9 +229,9 @@Examples
download_plot <- function() { ggplot(data = mtcars, aes(x = wt, y = mpg)) + geom_point(aes(color = cyl)) + - theme(legend.justification = c(1, 1), - legend.position = c(1, 1), - legend.title = element_blank()) + + theme(legend.justification = c(1, 1), + legend.position.inside = c(1, 1), + legend.title = element_blank()) + ggtitle("GGPlot Example ") + xlab("wt") + ylab("mpg") diff --git a/docs/reference/downloadableReactTable.html b/docs/reference/downloadableReactTable.html new file mode 100644 index 00000000..0d207525 --- /dev/null +++ b/docs/reference/downloadableReactTable.html @@ -0,0 +1,276 @@ + +downloadableReactTable module server function — downloadableReactTable • periscope2 + + +++ + + + + + + + diff --git a/docs/reference/downloadableReactTableUI.html b/docs/reference/downloadableReactTableUI.html new file mode 100644 index 00000000..8edd37f2 --- /dev/null +++ b/docs/reference/downloadableReactTableUI.html @@ -0,0 +1,238 @@ + ++ + + + ++ + +++ +++ +downloadableReactTable module server function
+ Source:R/downloadableReactTable.R++downloadableReactTable.Rd++ +Server-side function for the downloadableReactTableUI.
+++ ++downloadableReactTable( + id, + table_data, + selection_mode = NULL, + pre_selected_rows = NULL, + file_name_root = "data_file", + download_data_fxns = NULL, + pagination = FALSE, + table_height = 600, + show_rownames = FALSE, + columns_filter = FALSE, + global_search = TRUE, + row_highlight = TRUE, + row_striping = TRUE, + table_options = list(), + logger = NULL +)++Arguments
+ + +
- id
+- + + +
the ID of the Module's UI element
- table_data
+- + + +
reactive expression (or parameter-less function) that acts as table data source
- selection_mode
+- + + +
to enable row selection, set
selection_modevalue to either "single" for single row +selection or "multiple" for multiple rows selection, case insensitive. Any other value will +disable row selection. Row selection will be enabled by radio buttons in "single" selection +and checkboxes in "multiple" selection (default = NULL)- pre_selected_rows
+- + + +
reactive expression (or parameter-less function) provides the rows indices of the rows to +be selected when the table is rendered. If selection_mode is disabled, this parameter will +have no effect. If selection_mode is "single" only the first row index will be used (default = NULL)
- file_name_root
+- + + +
the base name used for user-downloaded file. It can be either a character string +a reactive expression or a function returning a character string (default = 'data_file')
- download_data_fxns
+- + + +
a named list of functions providing the data as return values. +The names for the list should be the same names as the ones used in the +downloadableReactTableUI (default = NULL)
- pagination
+- + + +
to enable table pagination (default = FALSE)
- table_height
+- + + +
max table height in pixels. Vertical scroll will be shown after that height value
- show_rownames
+- + + +
enable displaying rownames as a separate column (default = FALSE)
- columns_filter
+- + + +
enable the filtering input on each column in the table (default = FALSE)
- global_search
+- + + +
enable table global searching input to search and filter in all columns at once +(default = TRUE)
- row_highlight
+- + + +
enable highlighting rows upon mouse hover (default = TRUE)
- row_striping
+- + + +
add zebra-striped style to table rows (default = TRUE)
- table_options
+- + + +
optional table formatting parameters check
?reactable::reactablefor options full list. +Also see example below to see how to pass options (default = list())- logger
+- + +
logger to use (default = NULL)
++Value
+A named list of two elements:
- +
selected_rows: data.frame of current selected rows
- +
table_state: a list of the current table state. The list keys are +("page", "pageSize", "pages", "sorted" and "selected"). +Review
?reactable::getReactableStatefor more info.++Shiny Usage
+ + +This function is not called directly by consumers - it is accessed in +server.R using the same id provided in
+downloadableReactTableUI:+
downloadableReactTable(id)++ +See also
+ +++Examples
++if (interactive()) { + library(shiny) + library(periscope2) + library(reactable) + + shinyApp( + ui = fluidPage(fluidRow(column( + width = 12, + downloadableReactTableUI( + id = "object_id1", + downloadtypes = c("csv", "tsv"), + hovertext = "Download the data here!")))), + server = function(input, output) { + table_state <- downloadableReactTable( + id = "object_id1", + table_data = reactiveVal(iris), + download_data_fxns = list(csv = reactiveVal(iris), tsv = reactiveVal(iris)), + selection_mode = "multiple", + pre_selected_rows = function() {c(1, 3, 5)}, + table_options = list(columns = list( + Sepal.Length = colDef(name = "Sepal Length"), + Sepal.Width = colDef(filterable = TRUE), + Petal.Length = colDef(show = FALSE), + Petal.Width = colDef(defaultSortOrder = "desc")), + theme = reactableTheme( + borderColor = "#dfe2e5", + stripedColor = "#f6f8fa", + highlightColor = "#f0f5f9", + cellPadding = "8px 12px"))) + + observeEvent(table_state(), { print(table_state()) }) + }) +} + +downloadableReactTable module UI function — downloadableReactTableUI • periscope2 + + +++ + + + + + + + diff --git a/docs/reference/downloadableTable.html b/docs/reference/downloadableTable.html index daa4272c..50fee61b 100644 --- a/docs/reference/downloadableTable.html +++ b/docs/reference/downloadableTable.html @@ -19,7 +19,7 @@ @@ -45,6 +45,9 @@+ + + + ++ + +++ +++ +downloadableReactTable module UI function
+ Source:R/downloadableReactTable.R++downloadableReactTableUI.Rd++ +downloadableReactTable module is extending
+?reactablepackage table functions by creating +a custom high-functionality table paired with downloadFile button. +The table has the following default functionality:search, highlight functionality, infinite scrolling, sorting by columns and +returns a reactive dataset of selected items and table current state.++ ++downloadableReactTableUI(id, downloadtypes = NULL, hovertext = NULL)++Arguments
+ + +++Value
+list of downloadFileButton UI and reactable table and hidden inputs for contentHeight option
+++Details
+downloadFile button will be hidden if
+downloadableReactTableUIparameter +downloadtypesis empty++Table Features
+ + +
- +
Consistent styling of the table
- +
downloadFile module button functionality built-in to the table (it will be shown only if downloadtypes is defined)
- +
Ability to show different data from the download data
- +
Table is automatically fit to the window size with infinite +y-scrolling
- +
Table search functionality including highlighting built-in
- +
Multi-select built in, including reactive feedback on which table +items are selected
++Example
+ + ++
downloadableReactTableUI("mytableID", c("csv", "tsv"), +"Click Here")++Notes
+ + +When there are no rows to download in any of the linked downloaddatafxns the +button will be hidden as there is nothing to download.
+++Shiny Usage
+ + +Call this function at the place in ui.R where the table should be placed.
+Paired with a call to
+downloadableReactTable(id, ...)+in server.R++ +See also
+ +++Examples
++if (interactive()) { + library(shiny) + library(periscope2) + library(reactable) + + shinyApp( + ui = fluidPage(fluidRow(column( + width = 12, + downloadableReactTableUI( + id = "object_id1", + downloadtypes = c("csv", "tsv"), + hovertext = "Download the data here!")))), + server = function(input, output) { + table_state <- downloadableReactTable( + id = "object_id1", + table_data = reactiveVal(iris), + download_data_fxns = list(csv = reactiveVal(iris), tsv = reactiveVal(iris)), + selection_mode = "multiple", + pre_selected_rows = function() {c(1, 3, 5)}, + table_options = list(columns = list( + Sepal.Length = colDef(name = "Sepal Length"), + Sepal.Width = colDef(filterable = TRUE), + Petal.Length = colDef(show = FALSE), + Petal.Width = colDef(defaultSortOrder = "desc")), + showSortable = TRUE, + theme = reactableTheme( + borderColor = "#dfe2e5", + stripedColor = "#f6f8fa", + highlightColor = "#f0f5f9", + cellPadding = "8px 12px"))) + + observeEvent(table_state(), { print(table_state()) }) + }) +} + +Using the downloadablePlot Shiny Module ++ Using downloadableReactTable Shiny Module + Using the downloadableTable Shiny Module diff --git a/docs/reference/downloadableTableUI.html b/docs/reference/downloadableTableUI.html index 8867e2e4..969af537 100644 --- a/docs/reference/downloadableTableUI.html +++ b/docs/reference/downloadableTableUI.html @@ -19,7 +19,7 @@ @@ -45,6 +45,9 @@Using the downloadablePlot Shiny Module ++ Using downloadableReactTable Shiny Module + Using the downloadableTable Shiny Module diff --git a/docs/reference/get_url_parameters.html b/docs/reference/get_url_parameters.html index d6b77367..9bde4aa2 100644 --- a/docs/reference/get_url_parameters.html +++ b/docs/reference/get_url_parameters.html @@ -18,7 +18,7 @@ @@ -44,6 +44,9 @@Using the downloadablePlot Shiny Module ++ Using downloadableReactTable Shiny Module + Using the downloadableTable Shiny Module diff --git a/docs/reference/index.html b/docs/reference/index.html index f358f82f..2a7aacab 100644 --- a/docs/reference/index.html +++ b/docs/reference/index.html @@ -17,7 +17,7 @@ @@ -43,6 +43,9 @@Using the downloadablePlot Shiny Module ++ Using downloadableReactTable Shiny Module + Using the downloadableTable Shiny Module @@ -159,6 +162,14 @@All functions
downloadablePlotUI()+ downloadablePlot module UI function
+ + ++ downloadableReactTable module server function
+ + +downloadableReactTable module UI function
diff --git a/docs/reference/load_announcements.html b/docs/reference/load_announcements.html index b16b3ae7..54bd7551 100644 --- a/docs/reference/load_announcements.html +++ b/docs/reference/load_announcements.html @@ -18,7 +18,7 @@ @@ -44,6 +44,9 @@ Using the downloadablePlot Shiny Module ++ Using downloadableReactTable Shiny Module + Using the downloadableTable Shiny Module diff --git a/docs/reference/logViewerOutput.html b/docs/reference/logViewerOutput.html index 5c61fff1..b006d291 100644 --- a/docs/reference/logViewerOutput.html +++ b/docs/reference/logViewerOutput.html @@ -1,6 +1,7 @@ -Display app logs — logViewerOutput • periscope2 Display app logs — logViewerOutput • periscope2 @@ -18,7 +19,7 @@ @@ -44,6 +45,9 @@Using the downloadablePlot Shiny Module ++ Using downloadableReactTable Shiny Module + Using the downloadableTable Shiny Module @@ -87,8 +91,9 @@Display app logs
-Creates a shiny table with table containing logged user actions. Table contents are auto updated whenever a user action is -logged. The id must match the same id configured in server.R file upon calling
+fw_server_setupmethodDisplay app log data in downloadableReactTable table containing logged user actions. Table contents are auto updated +whenever a user action is logged. User can search for logs, sort them by time and download them in CSV or TSV format. +The id must match the same id configured in server.R file upon calling
fw_server_setupmethod@@ -105,7 +110,7 @@Arguments
Value
-shiny tableOutput instance
+downloadableReactTableUI instance
@@ -43,6 +43,9 @@Table columns
@@ -143,7 +148,28 @@Examples
diff --git a/docs/reference/logging-entrypoints.html b/docs/reference/logging-entrypoints.html index a696fe57..111425c9 100644 --- a/docs/reference/logging-entrypoints.html +++ b/docs/reference/logging-entrypoints.html @@ -17,7 +17,7 @@# Inside ui_body add the log viewer box to your box list logViewerOutput('logViewerId') -#> <div id="logViewerId-logViewerId" class="shiny-html-output"></div> +#> [[1]] +#> <div class="shiny-panel-conditional" data-display-if="output.displayButton" data-ns-prefix="logViewerId-logViewerId-"> +#> <span id="logViewerId-logViewerId-reactTableButtonDiv" class="periscope-downloadable-react-table-button" style=""> +#> <span class="btn-group" data-toggle="tooltip" data-placement="top" title="Download application logs"> +#> <button aria-expanded="false" aria-haspopup="true" class="btn btn-default action-button dropdown-toggle periscope-download-btn" data-toggle="dropdown" id="logViewerId-logViewerId-reactTableButtonID-downloadFileList" type="button action"> +#> <i class="far fa-copy" role="presentation" aria-label="copy icon"></i> +#> </button> +#> <ul class="dropdown-menu" id="logViewerId-logViewerId-reactTableButtonID-testList"> +#> <li> +#> <a aria-disabled="true" class="shiny-download-link disabled periscope-download-choice" download href="" id="logViewerId-logViewerId-reactTableButtonID-csv" tabindex="-1" target="_blank">csv</a> +#> </li> +#> <li> +#> <a aria-disabled="true" class="shiny-download-link disabled periscope-download-choice" download href="" id="logViewerId-logViewerId-reactTableButtonID-tsv" tabindex="-1" target="_blank">tsv</a> +#> </li> +#> </ul> +#> </span> +#> </span> +#> </div> +#> +#> [[2]] +#> <div class="reactable html-widget html-widget-output shiny-report-size html-fill-item" data-reactable-output="logViewerId-logViewerId-reactTableOutputID" id="logViewerId-logViewerId-reactTableOutputID" style="width:auto;height:auto;"></div> +#>Using the downloadablePlot Shiny Module ++ Using downloadableReactTable Shiny Module + Using the downloadableTable Shiny Module diff --git a/docs/reference/periscope2.html b/docs/reference/periscope2.html index f7d82eee..ba853cc6 100644 --- a/docs/reference/periscope2.html +++ b/docs/reference/periscope2.html @@ -21,7 +21,7 @@ @@ -47,6 +47,9 @@Using the downloadablePlot Shiny Module ++ Using downloadableReactTable Shiny Module + Using the downloadableTable Shiny Module diff --git a/docs/reference/set_app_parameters.html b/docs/reference/set_app_parameters.html index c935d94e..17cac068 100644 --- a/docs/reference/set_app_parameters.html +++ b/docs/reference/set_app_parameters.html @@ -17,7 +17,7 @@ @@ -43,6 +43,9 @@Using the downloadablePlot Shiny Module ++ Using downloadableReactTable Shiny Module + Using the downloadableTable Shiny Module @@ -122,7 +125,7 @@Arguments
log_level -+ Designating the log level to use for the user log as 'DEBUG','INFO', 'WARN' or 'ERROR' (default = 'DEBUG')
Designating the log level to use for the user log as 'DEBUG','INFO', 'WARNING' or 'ERROR' (default = 'DEBUG')
app_version diff --git a/docs/reference/themeConfigurationsAddin.html b/docs/reference/themeConfigurationsAddin.html index cc93464c..431f5dfb 100644 --- a/docs/reference/themeConfigurationsAddin.html +++ b/docs/reference/themeConfigurationsAddin.html @@ -18,7 +18,7 @@ @@ -44,6 +44,9 @@Using the downloadablePlot Shiny Module ++ Using downloadableReactTable Shiny Module + Using the downloadableTable Shiny Module diff --git a/docs/reference/ui_tooltip.html b/docs/reference/ui_tooltip.html index 0a3d3c91..f5f933c1 100644 --- a/docs/reference/ui_tooltip.html +++ b/docs/reference/ui_tooltip.html @@ -18,7 +18,7 @@ @@ -44,6 +44,9 @@Using the downloadablePlot Shiny Module ++ Using downloadableReactTable Shiny Module + Using the downloadableTable Shiny Module diff --git a/docs/sitemap.xml b/docs/sitemap.xml index d7dd9a40..19a48ff5 100644 --- a/docs/sitemap.xml +++ b/docs/sitemap.xml @@ -5,6 +5,7 @@/articles/applicationReset-module.html /articles/downloadFile-module.html + /articles/downloadablePlot-module.html /articles/downloadableReactTable-module.html /articles/downloadableTable-module.html /articles/index.html @@ -33,6 +34,8 @@ /articles/logViewer-module.html /reference/downloadFile_ValidateTypes.html /reference/downloadablePlot.html + /reference/downloadablePlotUI.html + /reference/downloadableReactTable.html /reference/downloadableReactTableUI.html /reference/downloadableTable.html /reference/downloadableTableUI.html diff --git a/inst/WORDLIST b/inst/WORDLIST index 74479df0..7b99f585 100644 --- a/inst/WORDLIST +++ b/inst/WORDLIST @@ -45,6 +45,8 @@ downloadFileButton downloadFileButton's downloadablePlot downloadablePlotUI +downloadableReactTable +downloadableReactTableUI downloadableTable downloadableTableUI downloadableTables @@ -59,7 +61,6 @@ ggplot gitignore hoverOpts hovertext -href htmlOutput http https @@ -71,6 +72,11 @@ javascript js logViewer logViewerOutput +logdebug +logerror +loginfo +loglevel +logwarn mailto mis modularize @@ -78,10 +84,13 @@ msg mytestapp navbar navbarMenu +openxlsx +pageSize param params periscopeapps prettyToggle +reactable rownumbers runnable scalable @@ -104,6 +113,7 @@ unitless userAction valueBox waiterShowOnLoad +writexl www xlsx yaml diff --git a/inst/fw_templ/p_example/custom.css b/inst/fw_templ/p_example/custom.css index 4166a1dd..c781656d 100644 --- a/inst/fw_templ/p_example/custom.css +++ b/inst/fw_templ/p_example/custom.css @@ -53,6 +53,11 @@ ul.dropdown-menu { /* End of Application Header CSS rules */ +.hidden { + display: none !important; +} + + /* Other */ .help-block { @@ -95,6 +100,12 @@ table.shiny-table { float: left; } + +/* Downloadable React Table */ +.periscope-downloadable-react-table-button { + display: inline-flex; +} + /* Downloadable Plot */ .periscope-downloadable-plot { } .periscope-downloadable-plot-button { @@ -188,6 +199,28 @@ div#swal2-html-container { color: black; padding: 15px; } + +/* downloadableReactTable Tooltip */ +.tooltip[data-tooltip-for="react-download-button"] .tooltip-inner { + background-color: #ffffff !important; + color: #212529 !important; + border: 1px solid #d1d5db !important; +} + +.tooltip[data-tooltip-for="react-download-button"].bs-tooltip-top .arrow::before { + border-top-color: #d1d5db !important; +} + +.tooltip[data-tooltip-for="react-download-button"].bs-tooltip-top .arrow::after { + content: ''; + position: absolute; + bottom: 1px; + left: 50%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: #ffffff transparent transparent transparent; +} /* END of periscope2 basic CSS*/ /* ========================================================================== */ diff --git a/inst/fw_templ/p_example/custom.js b/inst/fw_templ/p_example/custom.js index 9fc54789..191358df 100644 --- a/inst/fw_templ/p_example/custom.js +++ b/inst/fw_templ/p_example/custom.js @@ -20,6 +20,31 @@ setInterval(function() { }, 100); +function initializeReactTableTooltips() { + // Initialize all downloadable react table tooltips + $('.periscope-downloadable-react-table-button span.btn-group') + .tooltip() + .attr('data-tooltip-for', 'react-download-button'); + + // Add event handler only once + $(document).off('show.bs.tooltip.custom').on('show.bs.tooltip.custom', function(e) { + var $trigger = $(e.target); + var tooltipFor = $trigger.attr('data-tooltip-for'); + + if (tooltipFor) { + var tooltipInstance = $trigger.data('bs.tooltip'); + var $tip = $(tooltipInstance.tip); + $tip.attr('data-tooltip-for', tooltipFor); + } + }); +} + + +$(document).ready(function() { + initializeReactTableTooltips(); +}); + + // Resolve bs4Dash alert title and close button issues Shiny.addCustomMessageHandler('pcreate-alert', function (message) { // setup target diff --git a/inst/fw_templ/p_example/global.R b/inst/fw_templ/p_example/global.R index 20fc290b..d7d8c593 100644 --- a/inst/fw_templ/p_example/global.R +++ b/inst/fw_templ/p_example/global.R @@ -9,6 +9,7 @@ # to server, UI and session scopes # ---------------------------------------- library(DT) +library(reactable) library(shiny) library(periscope2) library(shinyWidgets) diff --git a/inst/fw_templ/p_example/plots.R b/inst/fw_templ/p_example/plots.R index 14049ce9..dc0ce9b1 100644 --- a/inst/fw_templ/p_example/plots.R +++ b/inst/fw_templ/p_example/plots.R @@ -15,9 +15,9 @@ attr(mtcars, "show_rownames") <- TRUE plot2ggplot <- function() { ggplot(data = mtcars, aes(x = wt, y = mpg)) + geom_point(aes(color = cyl)) + - theme(legend.justification = c(1, 1), - legend.position = c(1, 1), - legend.title = element_blank()) + + theme(legend.justification = c(1, 1), + legend.position.inside = c(1, 1), + legend.title = element_blank()) + ggtitle("GGPlot Example w/Hover") + xlab("wt") + ylab("mpg") diff --git a/inst/fw_templ/p_example/program_helpers.R b/inst/fw_templ/p_example/program_helpers.R index 9dbdf6f0..fcdb03d3 100644 --- a/inst/fw_templ/p_example/program_helpers.R +++ b/inst/fw_templ/p_example/program_helpers.R @@ -9,6 +9,7 @@ files_idx <- read.csv("program/data/struc_indx.csv") rownames(app_files) <- app_files$X app_files$X <- NULL +app_files <- app_files %>% mutate(across(where(is.character), ~na_if(., ""))) rownames(files_idx) <- files_idx$X files_idx$X <- NULL diff --git a/inst/fw_templ/p_example/server_local.R b/inst/fw_templ/p_example/server_local.R index d6167639..fa1b0129 100644 --- a/inst/fw_templ/p_example/server_local.R +++ b/inst/fw_templ/p_example/server_local.R @@ -78,6 +78,40 @@ downloadableTable("exampleDT1", formatStyle = list(columns = c("Natural.Increase"), backgroundColor = DT::styleInterval(c(7614, 15914, 34152), c("lightgray", "gray", "cadetblue", "#808000"))))) +downloadableReactTable(id = "exampleReactTable", + logger = ss_userAction.Log, + table_data = load_data3, + file_name_root = "exampleReacttable", + download_data_fxns = list(csv = load_data3, tsv = load_data3), + table_options = list( + defaultSorted = "Total.Population.Change", + columnGroups = list(colGroup(name = "Statistics", columns = c("Total.Population.Change", "Natural.Increase"))), + columns = list( + Total.Population.Change = colDef( + name = "Change", + filterable = TRUE, + cell = function(value) { + if (value <= 0) { + tags$span(style = "color:red", value) + } else { + tags$span(style = "color:green", value) + } + }), + Natural.Increase = colDef( + name = "Increase", + filterable = TRUE, + cell = function(value) { + if (value <= 7614) { + tags$span(class = "badge bg-primary", value) + } else if (value <= 15914) { + tags$span(class = "badge bg-secondary", value) + } else if (value <= 34152) { + tags$span(class = "badge bg-info", value) + } else { + tags$span(class = "badge bg-success", value) + } + }), + Geographic.Area = colDef(name = "Location", filterable = TRUE)))) downloadablePlot("examplePlot2", ss_userAction.Log, filenameroot = "plot2_ggplot", @@ -204,6 +238,9 @@ output$file_structure_plot <- renderCanvasXpress({ hierarchy = list("App_Root", "L1"), title = "Empty Application Files", graphOrientation = "horizontal", + xAxis = list("order"), + colorBy = list("App_Root"), + showLegend = FALSE, events = node_event ) }) diff --git a/inst/fw_templ/p_example/ui_body.R b/inst/fw_templ/p_example/ui_body.R index 07289ae5..f55f051f 100644 --- a/inst/fw_templ/p_example/ui_body.R +++ b/inst/fw_templ/p_example/ui_body.R @@ -141,6 +141,47 @@ application_setup <- tabItem(tabName = "application_setup", plot2_hover <- hoverOpts(id = "examplePlot2_hover") +react_table_box <- box( + id = "react_downloader", + title = "React Table Downloader", + status = "info", + solidHeader = TRUE, + collapsible = TRUE, + width = 12, + fluidRow(column(width = 6, + tags$dl(tags$dt("Features"), + tags$ul(tags$li("React table downloader module displays tabular data in rich formatted tables using the", tags$b("`reactable`"), "package"), + tags$li("downloadableReactTable returns a named reactive list that can be used in the application:", + tags$ul(tags$li(tags$b("selected_rows"), ": data.frame of current selected rows"), + tags$li(tags$b("table_state"), ": a list of the current table state"))), + tags$li("Table data can be downloaded in different formats such as: ", tags$b("'csv'"), + ", ", tags$b("'tsv'"), ", ", tags$b("'txt'"), "and/or ", tags$b("'xlsx'")), + tags$li("User can customize downloadableReactTable modules using reactable package options."), + tags$li("For more information about table options please visit the", + tags$a("Reactable documentation", target = "_blank", href = "https://glin.github.io/reactable/"), + "site") + ))), + column(width = 6, + tags$dl(tags$dt("Setup"), + tags$li("Module should be configured in both UI and Server code"), + tags$li("In the 'body_ui.R' add the following lines:", + blockQuote("downloadableReactTableUI('exampleReactTable', + 'Download react table data'))", color = "info")), + tags$li("In the 'server_local.R' add the following lines (make sure the two use the same ID):", + blockQuote("downloadableReactTable('exampleReactTable', + ss_userAction.Log, + 'exampletable', + list(csv = load_data3, tsv = load_data3))", color = "info")), + tags$li("Review ", tags$b("?downloadableReactTableUI"), " and ", tags$b("?downloadableReactTable"), " for more information"), + tags$li("Review below table for detailed example code")))), + fluidRow(column(width = 12, + downloadableReactTableUI(id = "exampleReactTable", + downloadtypes = list("csv", "tsv"), + hovertext = "Download react table data"))) + +) + + table_downloader_box <- box( id = "table_downloader", title = "Table Downloader", @@ -168,11 +209,11 @@ table_downloader_box <- box( column(width = 6, tags$dl(tags$dt("Setup"), tags$li("Module should be configured in both UI and Server code"), - tags$li("In your 'body_ui.R', place module UI part as follow: ", + tags$li("In the 'body_ui.R' add the following lines:", blockQuote("downloadableTableUI('exampleDT1', list('csv', 'tsv'), 'Download table data'))", color = "info")), - tags$li("In your 'server_local.R', place module server part, ", tags$em("with the same id used with UI part"), ", as follow: ", + tags$li("In the 'server_local.R' add the following lines (make sure the two use the same ID):", blockQuote("downloadableTable('exampleDT1', ss_userAction.Log, 'exampletable', @@ -237,9 +278,9 @@ plot_downloader_box <- box( column(width = 6, tags$dl(tags$dt("Setup"), tags$ul(tags$li("Module should be configured in both the UI and Server code"), - tags$li("In your 'body_ui.R', place module UI part as follow: ", + tags$li("In the 'body_ui.R' add the following lines:", blockQuote("downloadablePlotUI('myplotID', c('png', 'csv'), 'Download Plot or Data', '300px'))", color = "info")), - tags$li("In your 'server_local.R', place module server part, ", tags$em("with the same id used with UI part"), ", as follow: ", + tags$li("In the 'server_local.R' add the following lines (make sure the two use the same ID):", blockQuote("downloadablePlot('myplotID', ss_userAction.Log, filenameroot = 'mydownload1', @@ -299,6 +340,7 @@ reset_application_box <- box( periscope_modules <- tabItem(tabName = "periscope_modules", file_downloader_box, reset_application_box, + react_table_box, table_downloader_box, plot_downloader_box) diff --git a/inst/fw_templ/p_example/ui_body_no_left_sidebar.R b/inst/fw_templ/p_example/ui_body_no_left_sidebar.R index 7ede0919..16bf4579 100644 --- a/inst/fw_templ/p_example/ui_body_no_left_sidebar.R +++ b/inst/fw_templ/p_example/ui_body_no_left_sidebar.R @@ -135,6 +135,48 @@ files_organization_box <- box( plot2_hover <- hoverOpts(id = "examplePlot2_hover") +react_table_box <- box( + id = "react_downloader", + title = "React Table Downloader", + status = "info", + solidHeader = TRUE, + collapsible = TRUE, + width = 12, + fluidRow(column(width = 6, + tags$dl(tags$dt("Features"), + tags$ul(tags$li("React table downloader module displays tabular data in rich formatted tables using the", tags$b("`reactable`"), "package"), + tags$li("downloadableReactTable returns a named reactive list that can be used in the application:", + tags$ul(tags$li(tags$b("selected_rows"), ": data.frame of current selected rows"), + tags$li(tags$b("table_state"), ": a list of the current table state"))), + tags$li("Table data can be downloaded in different formats such as: ", tags$b("'csv'"), + ", ", tags$b("'tsv'"), ", ", tags$b("'txt'"), "and/or ", tags$b("'xlsx'")), + tags$li("User can customize downloadableReactTable modules using reactable package options."), + tags$li("For more information about table options please visit the", + tags$a("Reactable documentation", target = "_blank", href = "https://glin.github.io/reactable/"), + "site") + ))), + column(width = 6, + tags$dl(tags$dt("Setup"), + tags$li("Module should be configured in both UI and Server code"), + tags$li("In the 'body_ui.R' add the following lines:", + blockQuote("downloadableReactTableUI('exampleReactTable', + 'Download react table data'))", color = "info")), + tags$li("In the 'server_local.R' add the following lines (make sure the two use the same ID):", + blockQuote("downloadableReactTable('exampleReactTable', + ss_userAction.Log, + 'exampletable', + list(csv = load_data3, tsv = load_data3))", color = "info")), + tags$li("Review ", tags$b("?downloadableReactTableUI"), " and ", tags$b("?downloadableReactTable"), " for more information"), + tags$li("Review below table for detailed example code")))), + fluidRow(column(width = 12, + downloadableReactTableUI(id = "exampleReactTable", + downloadtypes = list("csv", "tsv"), + hovertext = "Download react table data"))) + +) + + + table_downloader_box <- box( id = "table_downloader", title = "Table Downloader", @@ -162,11 +204,11 @@ table_downloader_box <- box( column(width = 6, tags$dl(tags$dt("Setup"), tags$li("Module should be configured in both UI and Server code"), - tags$li("In your 'body_ui.R', place module UI part as follow: ", + tags$li("In the 'body_ui.R' add the following lines:", blockQuote("downloadableTableUI('exampleDT1', list('csv', 'tsv'), 'Download table data'))", color = "info")), - tags$li("In your 'server_local.R', place module server part, ", tags$em("with the same id used with UI part"), ", as follow: ", + tags$li("In the 'server_local.R' add the following lines (make sure the two use the same ID):", blockQuote("downloadableTable('exampleDT1', ss_userAction.Log, 'exampletable', @@ -231,9 +273,9 @@ plot_downloader_box <- box( column(width = 6, tags$dl(tags$dt("Setup"), tags$ul(tags$li("Module should be configured in both the UI and Server code"), - tags$li("In your 'body_ui.R', place module UI part as follow: ", + tags$li("In the 'body_ui.R' add the following lines:", blockQuote("downloadablePlotUI('myplotID', c('png', 'csv'), 'Download Plot or Data', '300px'))", color = "info")), - tags$li("In your 'server_local.R', place module server part, ", tags$em("with the same id used with UI part"), ", as follow: ", + tags$li("In the 'server_local.R' add the following lines (make sure the two use the same ID):", blockQuote("downloadablePlot('myplotID', ss_userAction.Log, filenameroot = 'mydownload1', @@ -536,6 +578,7 @@ add_ui_body(list(uiOutput("app_theme"), announcements_box, fluidRow(file_downloader_box, reset_application_box), + react_table_box, table_downloader_box, plot_downloader_box, logger_box, diff --git a/inst/fw_templ/www/custom.css b/inst/fw_templ/www/custom.css index b2f717b2..7a198e39 100644 --- a/inst/fw_templ/www/custom.css +++ b/inst/fw_templ/www/custom.css @@ -115,6 +115,11 @@ table.shiny-table { float: left; } +/* Downloadable React Table */ +.periscope-downloadable-react-table-button { + display: inline-flex; +} + /* Downloadable Plot */ .periscope-downloadable-plot { } .periscope-downloadable-plot-button { @@ -169,4 +174,26 @@ div#swal2-html-container { } /* End of modal related CSS rules */ + +/* downloadableReactTable Tooltip */ +.tooltip[data-tooltip-for="react-download-button"] .tooltip-inner { + background-color: #ffffff !important; + color: #212529 !important; + border: 1px solid #d1d5db !important; +} + +.tooltip[data-tooltip-for="react-download-button"].bs-tooltip-top .arrow::before { + border-top-color: #d1d5db !important; +} + +.tooltip[data-tooltip-for="react-download-button"].bs-tooltip-top .arrow::after { + content: ''; + position: absolute; + bottom: 1px; + left: 50%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: #ffffff transparent transparent transparent; +} /* END of periscope2 basic CSS*/ diff --git a/inst/fw_templ/www/custom.js b/inst/fw_templ/www/custom.js index f16620c1..b1afffbc 100644 --- a/inst/fw_templ/www/custom.js +++ b/inst/fw_templ/www/custom.js @@ -21,6 +21,31 @@ setInterval(function() { }, 100); +function initializeReactTableTooltips() { + // Initialize all downloadable react table tooltips + $('.periscope-downloadable-react-table-button span.btn-group') + .tooltip() + .attr('data-tooltip-for', 'react-download-button'); + + // Add event handler only once + $(document).off('show.bs.tooltip.custom').on('show.bs.tooltip.custom', function(e) { + var $trigger = $(e.target); + var tooltipFor = $trigger.attr('data-tooltip-for'); + + if (tooltipFor) { + var tooltipInstance = $trigger.data('bs.tooltip'); + var $tip = $(tooltipInstance.tip); + $tip.attr('data-tooltip-for', tooltipFor); + } + }); +} + + +$(document).ready(function() { + initializeReactTableTooltips(); +}); + + // Resolve bs4Dash alert title and close button issues Shiny.addCustomMessageHandler('pcreate-alert', function (message) { // setup target diff --git a/man/downloadFile.Rd b/man/downloadFile.Rd index 3aa1a449..4c936387 100644 --- a/man/downloadFile.Rd +++ b/man/downloadFile.Rd @@ -9,7 +9,8 @@ downloadFile( logger = NULL, filenameroot = "download", datafxns = NULL, - aspectratio = 1 + aspectratio = 1, + row_names = TRUE ) } \arguments{ @@ -28,6 +29,9 @@ when the button UI was created.} \item{aspectratio}{the downloaded chart image width:height ratio (ex: 1 = square, 1.3 = 4:3, 0.5 = 1:2). Where not applicable for a download type it is ignored (e.g. data downloads).} + +\item{row_names}{logical value indicating whether row names are to be written +for tabular data. Where not applicable for a download type it is ignored.} } \value{ no return value, called for downloading selected file type @@ -65,7 +69,7 @@ if (interactive()) { logger = "", filenameroot = "mydownload1", datafxns = list(csv = reactiveVal(iris)), - aspectratio = 1) + row_names = FALSE) # multiple download types downloadFile(id = "object_id2", logger = "", diff --git a/man/downloadFileButton.Rd b/man/downloadFileButton.Rd index 0a0eb7b0..555b3b8b 100644 --- a/man/downloadFileButton.Rd +++ b/man/downloadFileButton.Rd @@ -65,7 +65,7 @@ if (interactive()) { logger = "", filenameroot = "mydownload1", datafxns = list(csv = reactiveVal(iris)), - aspectratio = 1) + row_names = FALSE) # multiple download types downloadFile(id = "object_id2", logger = "", diff --git a/man/downloadablePlot.Rd b/man/downloadablePlot.Rd index cbff6f58..0b565c5b 100644 --- a/man/downloadablePlot.Rd +++ b/man/downloadablePlot.Rd @@ -74,9 +74,9 @@ if (interactive()) { download_plot <- function() { ggplot(data = mtcars, aes(x = wt, y = mpg)) + geom_point(aes(color = cyl)) + - theme(legend.justification = c(1, 1), - legend.position = c(1, 1), - legend.title = element_blank()) + + theme(legend.justification = c(1, 1), + legend.position.inside = c(1, 1), + legend.title = element_blank()) + ggtitle("GGPlot Example ") + xlab("wt") + ylab("mpg") diff --git a/man/downloadablePlotUI.Rd b/man/downloadablePlotUI.Rd index e09250e9..38e68591 100644 --- a/man/downloadablePlotUI.Rd +++ b/man/downloadablePlotUI.Rd @@ -95,9 +95,9 @@ if (interactive()) { download_plot <- function() { ggplot(data = mtcars, aes(x = wt, y = mpg)) + geom_point(aes(color = cyl)) + - theme(legend.justification = c(1, 1), - legend.position = c(1, 1), - legend.title = element_blank()) + + theme(legend.justification = c(1, 1), + legend.position.inside = c(1, 1), + legend.title = element_blank()) + ggtitle("GGPlot Example ") + xlab("wt") + ylab("mpg") diff --git a/man/downloadableReactTable.Rd b/man/downloadableReactTable.Rd new file mode 100644 index 00000000..e5314f54 --- /dev/null +++ b/man/downloadableReactTable.Rd @@ -0,0 +1,136 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/downloadableReactTable.R +\name{downloadableReactTable} +\alias{downloadableReactTable} +\title{downloadableReactTable module server function} +\usage{ +downloadableReactTable( + id, + table_data, + selection_mode = NULL, + pre_selected_rows = NULL, + file_name_root = "data_file", + download_data_fxns = NULL, + pagination = FALSE, + table_height = 600, + show_rownames = FALSE, + columns_filter = FALSE, + global_search = TRUE, + row_highlight = TRUE, + row_striping = TRUE, + table_options = list(), + logger = NULL +) +} +\arguments{ +\item{id}{the ID of the Module's UI element} + +\item{table_data}{reactive expression (or parameter-less function) that acts as table data source} + +\item{selection_mode}{to enable row selection, set \code{selection_mode} value to either "single" for single row +selection or "multiple" for multiple rows selection, case insensitive. Any other value will +disable row selection. Row selection will be enabled by radio buttons in "single" selection +and checkboxes in "multiple" selection (default = NULL)} + +\item{pre_selected_rows}{reactive expression (or parameter-less function) provides the rows indices of the rows to +be selected when the table is rendered. If selection_mode is disabled, this parameter will +have no effect. If selection_mode is "single" only the first row index will be used (default = NULL)} + +\item{file_name_root}{the base name used for user-downloaded file. It can be either a character string +a reactive expression or a function returning a character string (default = 'data_file')} + +\item{download_data_fxns}{a \strong{named} list of functions providing the data as return values. +The names for the list should be the same names as the ones used in the +downloadableReactTableUI (default = NULL)} + +\item{pagination}{to enable table pagination (default = FALSE)} + +\item{table_height}{max table height in pixels. Vertical scroll will be shown after that height value} + +\item{show_rownames}{enable displaying rownames as a separate column (default = FALSE)} + +\item{columns_filter}{enable the filtering input on each column in the table (default = FALSE)} + +\item{global_search}{enable table global searching input to search and filter in all columns at once +(default = TRUE)} + +\item{row_highlight}{enable highlighting rows upon mouse hover (default = TRUE)} + +\item{row_striping}{add zebra-striped style to table rows (default = TRUE)} + +\item{table_options}{optional table formatting parameters check \code{?reactable::reactable} for options full list. +Also see example below to see how to pass options (default = list())} + +\item{logger}{logger to use (default = NULL)} +} +\value{ +A named list of two elements: +\itemize{ +\item selected_rows: data.frame of current selected rows +\item table_state: a list of the current table state. The list keys are +("page", "pageSize", "pages", "sorted" and "selected"). +Review \code{?reactable::getReactableState} for more info. +} +} +\description{ +Server-side function for the downloadableReactTableUI. +} +\section{Shiny Usage}{ + +This function is not called directly by consumers - it is accessed in +server.R using the same id provided in \code{downloadableReactTableUI}: + +\strong{\code{downloadableReactTable(id)}} +} + +\examples{ +if (interactive()) { + library(shiny) + library(periscope2) + library(reactable) + + shinyApp( + ui = fluidPage(fluidRow(column( + width = 12, + downloadableReactTableUI( + id = "object_id1", + downloadtypes = c("csv", "tsv"), + hovertext = "Download the data here!")))), + server = function(input, output) { + table_state <- downloadableReactTable( + id = "object_id1", + table_data = reactiveVal(iris), + download_data_fxns = list(csv = reactiveVal(iris), tsv = reactiveVal(iris)), + selection_mode = "multiple", + pre_selected_rows = function() {c(1, 3, 5)}, + table_options = list(columns = list( + Sepal.Length = colDef(name = "Sepal Length"), + Sepal.Width = colDef(filterable = TRUE), + Petal.Length = colDef(show = FALSE), + Petal.Width = colDef(defaultSortOrder = "desc")), + theme = reactableTheme( + borderColor = "#dfe2e5", + stripedColor = "#f6f8fa", + highlightColor = "#f0f5f9", + cellPadding = "8px 12px"))) + + observeEvent(table_state(), { print(table_state()) }) + }) +} + +} +\seealso{ +\link[periscope2]{downloadableReactTableUI} + +\link[periscope2]{downloadFileButton} + +\link[periscope2]{logViewerOutput} + +\link[periscope2]{downloadFile} + +\link[periscope2]{downloadFile_ValidateTypes} + +\link[periscope2]{downloadFile_AvailableTypes} + +\link[periscope2]{downloadablePlot} +} diff --git a/man/downloadableReactTableUI.Rd b/man/downloadableReactTableUI.Rd new file mode 100644 index 00000000..0cfe4fa5 --- /dev/null +++ b/man/downloadableReactTableUI.Rd @@ -0,0 +1,114 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/downloadableReactTable.R +\name{downloadableReactTableUI} +\alias{downloadableReactTableUI} +\title{downloadableReactTable module UI function} +\usage{ +downloadableReactTableUI(id, downloadtypes = NULL, hovertext = NULL) +} +\arguments{ +\item{id}{character id for the object} + +\item{downloadtypes}{vector of values for data download types} + +\item{hovertext}{download button tooltip hover text} +} +\value{ +list of downloadFileButton UI and reactable table and hidden inputs for contentHeight option +} +\description{ +downloadableReactTable module is extending \code{?reactable} package table functions by creating +a custom high-functionality table paired with \link[periscope2]{downloadFile} button. +The table has the following default functionality:search, highlight functionality, infinite scrolling, sorting by columns and +returns a reactive dataset of selected items and table current state. +} +\details{ +\link[periscope2]{downloadFile} button will be hidden if \code{downloadableReactTableUI} parameter +\code{downloadtypes} is empty +} +\section{Table Features}{ + +\itemize{ +\item Consistent styling of the table +\item downloadFile module button functionality built-in to the table (it will be shown only if downloadtypes is defined) +\item Ability to show different data from the download data +\item Table is automatically fit to the window size with infinite +y-scrolling +\item Table search functionality including highlighting built-in +\item Multi-select built in, including reactive feedback on which table +items are selected +} +} + +\section{Example}{ + +\code{downloadableReactTableUI("mytableID", c("csv", "tsv"), +"Click Here")} +} + +\section{Notes}{ + +When there are no rows to download in any of the linked downloaddatafxns the +button will be hidden as there is nothing to download. +} + +\section{Shiny Usage}{ + +Call this function at the place in ui.R where the table should be placed. + +Paired with a call to \code{downloadableReactTable(id, ...)} +in server.R +} + +\examples{ +if (interactive()) { + library(shiny) + library(periscope2) + library(reactable) + + shinyApp( + ui = fluidPage(fluidRow(column( + width = 12, + downloadableReactTableUI( + id = "object_id1", + downloadtypes = c("csv", "tsv"), + hovertext = "Download the data here!")))), + server = function(input, output) { + table_state <- downloadableReactTable( + id = "object_id1", + table_data = reactiveVal(iris), + download_data_fxns = list(csv = reactiveVal(iris), tsv = reactiveVal(iris)), + selection_mode = "multiple", + pre_selected_rows = function() {c(1, 3, 5)}, + table_options = list(columns = list( + Sepal.Length = colDef(name = "Sepal Length"), + Sepal.Width = colDef(filterable = TRUE), + Petal.Length = colDef(show = FALSE), + Petal.Width = colDef(defaultSortOrder = "desc")), + showSortable = TRUE, + theme = reactableTheme( + borderColor = "#dfe2e5", + stripedColor = "#f6f8fa", + highlightColor = "#f0f5f9", + cellPadding = "8px 12px"))) + + observeEvent(table_state(), { print(table_state()) }) + }) +} + +} +\seealso{ +\link[periscope2]{downloadableReactTable} + +\link[periscope2]{downloadFile} + +\link[periscope2]{logViewerOutput} + +\link[periscope2]{downloadFile} + +\link[periscope2]{downloadFile_ValidateTypes} + +\link[periscope2]{downloadFile_AvailableTypes} + +\link[periscope2]{downloadablePlot} +} diff --git a/man/logViewerOutput.Rd b/man/logViewerOutput.Rd index 1998e6d0..95aa5c8a 100644 --- a/man/logViewerOutput.Rd +++ b/man/logViewerOutput.Rd @@ -10,11 +10,12 @@ logViewerOutput(id = "logViewer") \item{id}{character id for the object(default = "logViewer")} } \value{ -shiny tableOutput instance +downloadableReactTableUI instance } \description{ -Creates a shiny table with table containing logged user actions. Table contents are auto updated whenever a user action is -logged. The id must match the same id configured in \bold{server.R} file upon calling \code{fw_server_setup} method +Display app log data in downloadableReactTable table containing logged user actions. Table contents are auto updated +whenever a user action is logged. User can search for logs, sort them by time and download them in CSV or TSV format. +The id must match the same id configured in \bold{server.R} file upon calling \code{fw_server_setup} method } \section{Table columns}{ diff --git a/man/set_app_parameters.Rd b/man/set_app_parameters.Rd index 57f77dd7..80f99ca6 100644 --- a/man/set_app_parameters.Rd +++ b/man/set_app_parameters.Rd @@ -29,7 +29,7 @@ application title.} \item{Supplying \strong{NULL} will disable the title link functionality.} }} -\item{log_level}{Designating the log level to use for the user log as 'DEBUG','INFO', 'WARN' or 'ERROR' (default = 'DEBUG')} +\item{log_level}{Designating the log level to use for the user log as 'DEBUG','INFO', 'WARNING' or 'ERROR' (default = 'DEBUG')} \item{app_version}{Character string designating the application version (default = '1.0.0')} diff --git a/periscope2.Rproj b/periscope2.Rproj index fcef6073..f49ce596 100644 --- a/periscope2.Rproj +++ b/periscope2.Rproj @@ -1,4 +1,5 @@ Version: 1.0 +ProjectId: 3a9c291b-cb49-4c16-935f-c669031d1d14 RestoreWorkspace: Default SaveWorkspace: Default diff --git a/tests/testthat/_snaps/download_file/download.csv b/tests/testthat/_snaps/download_file/download.csv new file mode 100644 index 00000000..e88f823e --- /dev/null +++ b/tests/testthat/_snaps/download_file/download.csv @@ -0,0 +1,2 @@ +"","x" +"1","123" diff --git a/tests/testthat/_snaps/download_file/download.tsv b/tests/testthat/_snaps/download_file/download.tsv new file mode 100644 index 00000000..9899d5e3 --- /dev/null +++ b/tests/testthat/_snaps/download_file/download.tsv @@ -0,0 +1,2 @@ +"" "x" +"1" "123" diff --git a/tests/testthat/_snaps/download_file/mydownload1.txt b/tests/testthat/_snaps/download_file/mydownload1.txt index 20d1ddfc..d90dffad 100644 --- a/tests/testthat/_snaps/download_file/mydownload1.txt +++ b/tests/testthat/_snaps/download_file/mydownload1.txt @@ -1,7 +1,7 @@ "mpg" "cyl" "disp" "hp" "drat" "wt" "qsec" "vs" "am" "gear" "carb" -"Mazda RX4" 21 6 160 110 3.9 2.62 16.46 0 1 4 4 -"Mazda RX4 Wag" 21 6 160 110 3.9 2.875 17.02 0 1 4 4 -"Datsun 710" 22.8 4 108 93 3.85 2.32 18.61 1 1 4 1 -"Hornet 4 Drive" 21.4 6 258 110 3.08 3.215 19.44 1 0 3 1 -"Hornet Sportabout" 18.7 8 360 175 3.15 3.44 17.02 0 0 3 2 -"Valiant" 18.1 6 225 105 2.76 3.46 20.22 1 0 3 1 +21 6 160 110 3.9 2.62 16.46 0 1 4 4 +21 6 160 110 3.9 2.875 17.02 0 1 4 4 +22.8 4 108 93 3.85 2.32 18.61 1 1 4 1 +21.4 6 258 110 3.08 3.215 19.44 1 0 3 1 +18.7 8 360 175 3.15 3.44 17.02 0 0 3 2 +18.1 6 225 105 2.76 3.46 20.22 1 0 3 1 diff --git a/tests/testthat/_snaps/download_file/show_row_names_download.tsv b/tests/testthat/_snaps/download_file/show_row_names_download.tsv new file mode 100644 index 00000000..0ce0ccfa --- /dev/null +++ b/tests/testthat/_snaps/download_file/show_row_names_download.tsv @@ -0,0 +1,7 @@ +"" "mpg" "cyl" "disp" "hp" "drat" "wt" "qsec" "vs" "am" "gear" "carb" +"Mazda RX4" 21 6 160 110 3.9 2.62 16.46 0 1 4 4 +"Mazda RX4 Wag" 21 6 160 110 3.9 2.875 17.02 0 1 4 4 +"Datsun 710" 22.8 4 108 93 3.85 2.32 18.61 1 1 4 1 +"Hornet 4 Drive" 21.4 6 258 110 3.08 3.215 19.44 1 0 3 1 +"Hornet Sportabout" 18.7 8 360 175 3.15 3.44 17.02 0 0 3 2 +"Valiant" 18.1 6 225 105 2.76 3.46 20.22 1 0 3 1 diff --git a/tests/testthat/_snaps/download_file/show_row_names_download.txt b/tests/testthat/_snaps/download_file/show_row_names_download.txt new file mode 100644 index 00000000..20d1ddfc --- /dev/null +++ b/tests/testthat/_snaps/download_file/show_row_names_download.txt @@ -0,0 +1,7 @@ +"mpg" "cyl" "disp" "hp" "drat" "wt" "qsec" "vs" "am" "gear" "carb" +"Mazda RX4" 21 6 160 110 3.9 2.62 16.46 0 1 4 4 +"Mazda RX4 Wag" 21 6 160 110 3.9 2.875 17.02 0 1 4 4 +"Datsun 710" 22.8 4 108 93 3.85 2.32 18.61 1 1 4 1 +"Hornet 4 Drive" 21.4 6 258 110 3.08 3.215 19.44 1 0 3 1 +"Hornet Sportabout" 18.7 8 360 175 3.15 3.44 17.02 0 0 3 2 +"Valiant" 18.1 6 225 105 2.76 3.46 20.22 1 0 3 1 diff --git a/tests/testthat/_snaps/log_viewer.md b/tests/testthat/_snaps/log_viewer.md index 688cb4bb..99c835d9 100644 --- a/tests/testthat/_snaps/log_viewer.md +++ b/tests/testthat/_snaps/log_viewer.md @@ -1,8 +1,29 @@ # logViewerOutput - + [[1]] + /reference/get_url_parameters.html + + + + + + ++ + [[2]] + + # logViewer - valid sample log - [1] "\n
" + {"x":{"tag":{"name":"Reactable","attribs":{"data":{"action":["Be Sure to Remember to Log ALL user actions","Sample Title (click for an info pop-up) started with log level\n action time \n Be Sure to Remember to Log ALL user actions 02-19-2022 14:03 \n Sample Title (click for an info pop-up) started with log level <DEBUG> 02-19-2022 14:03 \n Application Reset requested by user. Resetting in 5 seconds 02-19-2022 14:04 ","Application Reset requested by user. Resetting in 5 seconds"],"time":["02-19-2022 14:03","02-19-2022 14:03","02-19-2022 14:04"]},"columns":[{"id":"action","name":"action","type":"character"},{"id":"time","name":"time","type":"character"}],"searchable":true,"pagination":false,"highlight":true,"striped":true,"height":"600px","dataKey":"b624cab69fef11e8d7efb72689e14787","static":false},"children":[]},"class":"reactR_markup"},"evals":[],"jsHooks":[],"deps":[]} diff --git a/tests/testthat/sample_app_both_sidebars/program/fxn/plots.R b/tests/testthat/sample_app_both_sidebars/program/fxn/plots.R index 01e99446..96de8aa5 100644 --- a/tests/testthat/sample_app_both_sidebars/program/fxn/plots.R +++ b/tests/testthat/sample_app_both_sidebars/program/fxn/plots.R @@ -14,9 +14,9 @@ attr(mtcars, "show_rownames") <- TRUE plot2ggplot <- function() { ggplot(data = mtcars, aes(x = wt, y = mpg)) + geom_point(aes(color = cyl)) + - theme(legend.justification = c(1, 1), - legend.position = c(1, 1), - legend.title = element_blank()) + + theme(legend.justification = c(1, 1), + legend.position.inside = c(1, 1), + legend.title = element_blank()) + ggtitle("GGPlot Example w/Hover") + xlab("wt") + ylab("mpg") diff --git a/tests/testthat/sample_app_both_sidebars/program/fxn/program_helpers.R b/tests/testthat/sample_app_both_sidebars/program/fxn/program_helpers.R index 5e0b6d64..d6d7d968 100644 --- a/tests/testthat/sample_app_both_sidebars/program/fxn/program_helpers.R +++ b/tests/testthat/sample_app_both_sidebars/program/fxn/program_helpers.R @@ -9,6 +9,7 @@ files_idx <- read.csv("program/data/struc_indx.csv") rownames(app_files) <- app_files$X app_files$X <- NULL +app_files <- app_files %>% mutate(across(where(is.character), ~na_if(., ""))) rownames(files_idx) <- files_idx$X files_idx$X <- NULL @@ -32,10 +33,10 @@ load_data2 <- function() { load_data3 <- function() { ldf <- df %>% - select(1:3) %>% + select(1:3) %>% mutate(Total.Population.Change = as.numeric(gsub(",", "", Total.Population.Change)), Natural.Increase = as.numeric(gsub(",", "", Natural.Increase))) - + as.data.frame(ldf) } diff --git a/tests/testthat/sample_app_both_sidebars/program/global.R b/tests/testthat/sample_app_both_sidebars/program/global.R index f0ec7ffb..5952b8dc 100644 --- a/tests/testthat/sample_app_both_sidebars/program/global.R +++ b/tests/testthat/sample_app_both_sidebars/program/global.R @@ -9,6 +9,7 @@ # to server, UI and session scopes # ---------------------------------------- library(DT) +library(reactable) library(shiny) library(periscope2) library(waiter) diff --git a/tests/testthat/sample_app_both_sidebars/program/server_local.R b/tests/testthat/sample_app_both_sidebars/program/server_local.R index 2b7647bb..da48d989 100644 --- a/tests/testthat/sample_app_both_sidebars/program/server_local.R +++ b/tests/testthat/sample_app_both_sidebars/program/server_local.R @@ -70,6 +70,43 @@ downloadableTable("exampleDT1", formatStyle = list(columns = c("Natural.Increase"), backgroundColor = DT::styleInterval(c(7614, 15914, 34152), c("lightgray", "gray", "cadetblue", "#808000"))))) + +downloadableReactTable(id = "exampleReactTable", + logger = ss_userAction.Log, + table_data = load_data3, + file_name_root = "exampleReacttable", + download_data_fxns = list(csv = load_data3, tsv = load_data3), + table_options = list( + defaultSorted = "Total.Population.Change", + columnGroups = list(colGroup(name = "Statistics", columns = c("Total.Population.Change", "Natural.Increase"))), + columns = list( + Total.Population.Change = colDef( + name = "Change", + filterable = TRUE, + cell = function(value) { + if (value <= 0) { + tags$span(style = "color:red", value) + } else { + tags$span(style = "color:green", value) + } + }), + Natural.Increase = colDef( + name = "Increase", + filterable = TRUE, + cell = function(value) { + if (value <= 7614) { + tags$span(class = "badge bg-primary", value) + } else if (value <= 15914) { + tags$span(class = "badge bg-secondary", value) + } else if (value <= 34152) { + tags$span(class = "badge bg-info", value) + } else { + tags$span(class = "badge bg-success", value) + } + }), + Geographic.Area = colDef(name = "Location", filterable = TRUE)))) + + downloadablePlot("examplePlot2", ss_userAction.Log, filenameroot = "plot2_ggplot", diff --git a/tests/testthat/sample_app_both_sidebars/program/ui_body.R b/tests/testthat/sample_app_both_sidebars/program/ui_body.R index 84fb2d53..07f7a348 100644 --- a/tests/testthat/sample_app_both_sidebars/program/ui_body.R +++ b/tests/testthat/sample_app_both_sidebars/program/ui_body.R @@ -141,6 +141,47 @@ application_setup <- tabItem(tabName = "application_setup", plot2_hover <- hoverOpts(id = "examplePlot2_hover") +react_table_box <- box( + id = "react_downloader", + title = "React Table Downloader", + status = "info", + solidHeader = TRUE, + collapsible = TRUE, + width = 12, + fluidRow(column(width = 6, + tags$dl(tags$dt("Features"), + tags$ul(tags$li("React table downloader module displays tabular data in rich formatted tables using the", tags$b("`reactable`"), "package"), + tags$li("downloadableReactTable returns a named reactive list that can be used in the application:", + tags$ul(tags$li(tags$b("selected_rows"), ": data.frame of current selected rows"), + tags$li(tags$b("table_state"), ": a list of the current table state"))), + tags$li("Table data can be downloaded in different formats such as: ", tags$b("'csv'"), + ", ", tags$b("'tsv'"), ", ", tags$b("'txt'"), "and/or ", tags$b("'xlsx'")), + tags$li("User can customize downloadableReactTable modules using reactable package options."), + tags$li("For more information about table options please visit the", + tags$a("Reactable documentation", target = "_blank", href = "https://glin.github.io/reactable/"), + "site") + ))), + column(width = 6, + tags$dl(tags$dt("Setup"), + tags$li("Module should be configured in both UI and Server code"), + tags$li("In the 'body_ui.R' add the following lines:", + blockQuote("downloadableReactTableUI('exampleReactTable', + 'Download react table data'))", color = "info")), + tags$li("In the 'server_local.R' add the following lines (make sure the two use the same ID):", + blockQuote("downloadableReactTable('exampleReactTable', + ss_userAction.Log, + 'exampletable', + list(csv = load_data3, tsv = load_data3))", color = "info")), + tags$li("Review ", tags$b("?downloadableReactTableUI"), " and ", tags$b("?downloadableReactTable"), " for more information"), + tags$li("Review below table for detailed example code")))), + fluidRow(column(width = 12, + downloadableReactTableUI(id = "exampleReactTable", + downloadtypes = list("csv", "tsv"), + hovertext = "Download react table data"))) + +) + + table_downloader_box <- box( id = "table_downloader", title = "Table Downloader", @@ -168,11 +209,11 @@ table_downloader_box <- box( column(width = 6, tags$dl(tags$dt("Setup"), tags$li("Module should be configured in both UI and Server code"), - tags$li("In your 'body_ui.R', place module UI part as follow: ", + tags$li("In the 'body_ui.R' add the following lines:", blockQuote("downloadableTableUI('exampleDT1', list('csv', 'tsv'), 'Download table data'))", color = "info")), - tags$li("In your 'server_local.R', place module server part, ", tags$em("with the same id used with UI part"), ", as follow: ", + tags$li("In the 'server_local.R' add the following lines (make sure the two use the same ID):", blockQuote("downloadableTable('exampleDT1', ss_userAction.Log, 'exampletable', @@ -237,9 +278,9 @@ plot_downloader_box <- box( column(width = 6, tags$dl(tags$dt("Setup"), tags$ul(tags$li("Module should be configured in both the UI and Server code"), - tags$li("In your 'body_ui.R', place module UI part as follow: ", + tags$li("In the 'body_ui.R' add the following lines:", blockQuote("downloadablePlotUI('myplotID', c('png', 'csv'), 'Download Plot or Data', '300px'))", color = "info")), - tags$li("In your 'server_local.R', place module server part, ", tags$em("with the same id used with UI part"), ", as follow: ", + tags$li("In the 'server_local.R' add the following lines (make sure the two use the same ID):", blockQuote("downloadablePlot('myplotID', ss_userAction.Log, filenameroot = 'mydownload1', @@ -297,6 +338,7 @@ reset_application_box <- box( periscope_modules <- tabItem(tabName = "periscope_modules", + react_table_box, table_downloader_box, plot_downloader_box, file_downloader_box, diff --git a/tests/testthat/sample_app_both_sidebars/www/css/custom.css b/tests/testthat/sample_app_both_sidebars/www/css/custom.css index f59e4811..b2bc613c 100644 --- a/tests/testthat/sample_app_both_sidebars/www/css/custom.css +++ b/tests/testthat/sample_app_both_sidebars/www/css/custom.css @@ -60,6 +60,11 @@ table.shiny-table { float: left; } +/* Downloadable React Table */ +.periscope-downloadable-react-table-button { + display: inline-flex; +} + /* Downloadable Plot */ .periscope-downloadable-plot { } .periscope-downloadable-plot-button { @@ -119,6 +124,28 @@ a:focus:not(.nav-link) { /* END of periscope2 basic CSS*/ +/* downloadableReactTable Tooltip */ +.tooltip[data-tooltip-for="react-download-button"] .tooltip-inner { + background-color: #ffffff !important; + color: #212529 !important; + border: 1px solid #d1d5db !important; +} + +.tooltip[data-tooltip-for="react-download-button"].bs-tooltip-top .arrow::before { + border-top-color: #d1d5db !important; +} + +.tooltip[data-tooltip-for="react-download-button"].bs-tooltip-top .arrow::after { + content: ''; + position: absolute; + bottom: 1px; + left: 50%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: #ffffff transparent transparent transparent; +} + /* ========================================================================== */ /* --- APP-SPECIFIC CUSTOM CSS --- diff --git a/tests/testthat/sample_app_both_sidebars/www/js/custom.js b/tests/testthat/sample_app_both_sidebars/www/js/custom.js index 728ebc18..a613b78a 100644 --- a/tests/testthat/sample_app_both_sidebars/www/js/custom.js +++ b/tests/testthat/sample_app_both_sidebars/www/js/custom.js @@ -20,6 +20,31 @@ setInterval(function() { }, 100); +function initializeReactTableTooltips() { + // Initialize all downloadable react table tooltips + $('.periscope-downloadable-react-table-button span.btn-group') + .tooltip() + .attr('data-tooltip-for', 'react-download-button'); + + // Add event handler only once + $(document).off('show.bs.tooltip.custom').on('show.bs.tooltip.custom', function(e) { + var $trigger = $(e.target); + var tooltipFor = $trigger.attr('data-tooltip-for'); + + if (tooltipFor) { + var tooltipInstance = $trigger.data('bs.tooltip'); + var $tip = $(tooltipInstance.tip); + $tip.attr('data-tooltip-for', tooltipFor); + } + }); +} + + +$(document).ready(function() { + initializeReactTableTooltips(); +}); + + // Resolve bs4Dash alert title and close button issues Shiny.addCustomMessageHandler('pcreate-alert', function (message) { // setup target diff --git a/tests/testthat/sample_app_left_sidebar/program/fxn/plots.R b/tests/testthat/sample_app_left_sidebar/program/fxn/plots.R index b6157c8f..c7abcc03 100644 --- a/tests/testthat/sample_app_left_sidebar/program/fxn/plots.R +++ b/tests/testthat/sample_app_left_sidebar/program/fxn/plots.R @@ -14,9 +14,9 @@ attr(mtcars, "show_rownames") <- TRUE plot2ggplot <- function() { ggplot(data = mtcars, aes(x = wt, y = mpg)) + geom_point(aes(color = cyl)) + - theme(legend.justification = c(1, 1), - legend.position = c(1, 1), - legend.title = element_blank()) + + theme(legend.justification = c(1, 1), + legend.position.inside = c(1, 1), + legend.title = element_blank()) + ggtitle("GGPlot Example w/Hover") + xlab("wt") + ylab("mpg") diff --git a/tests/testthat/sample_app_left_sidebar/program/fxn/program_helpers.R b/tests/testthat/sample_app_left_sidebar/program/fxn/program_helpers.R index 5e0b6d64..d6d7d968 100644 --- a/tests/testthat/sample_app_left_sidebar/program/fxn/program_helpers.R +++ b/tests/testthat/sample_app_left_sidebar/program/fxn/program_helpers.R @@ -9,6 +9,7 @@ files_idx <- read.csv("program/data/struc_indx.csv") rownames(app_files) <- app_files$X app_files$X <- NULL +app_files <- app_files %>% mutate(across(where(is.character), ~na_if(., ""))) rownames(files_idx) <- files_idx$X files_idx$X <- NULL @@ -32,10 +33,10 @@ load_data2 <- function() { load_data3 <- function() { ldf <- df %>% - select(1:3) %>% + select(1:3) %>% mutate(Total.Population.Change = as.numeric(gsub(",", "", Total.Population.Change)), Natural.Increase = as.numeric(gsub(",", "", Natural.Increase))) - + as.data.frame(ldf) } diff --git a/tests/testthat/sample_app_left_sidebar/program/global.R b/tests/testthat/sample_app_left_sidebar/program/global.R index f0ec7ffb..5952b8dc 100644 --- a/tests/testthat/sample_app_left_sidebar/program/global.R +++ b/tests/testthat/sample_app_left_sidebar/program/global.R @@ -9,6 +9,7 @@ # to server, UI and session scopes # ---------------------------------------- library(DT) +library(reactable) library(shiny) library(periscope2) library(waiter) diff --git a/tests/testthat/sample_app_left_sidebar/program/server_local.R b/tests/testthat/sample_app_left_sidebar/program/server_local.R index beb90bda..cbfc91bf 100644 --- a/tests/testthat/sample_app_left_sidebar/program/server_local.R +++ b/tests/testthat/sample_app_left_sidebar/program/server_local.R @@ -70,6 +70,43 @@ downloadableTable("exampleDT1", formatStyle = list(columns = c("Natural.Increase"), backgroundColor = DT::styleInterval(c(7614, 15914, 34152), c("lightgray", "gray", "cadetblue", "#808000"))))) + +downloadableReactTable(id = "exampleReactTable", + logger = ss_userAction.Log, + table_data = load_data3, + file_name_root = "exampleReacttable", + download_data_fxns = list(csv = load_data3, tsv = load_data3), + table_options = list( + defaultSorted = "Total.Population.Change", + columnGroups = list(colGroup(name = "Statistics", columns = c("Total.Population.Change", "Natural.Increase"))), + columns = list( + Total.Population.Change = colDef( + name = "Change", + filterable = TRUE, + cell = function(value) { + if (value <= 0) { + tags$span(style = "color:red", value) + } else { + tags$span(style = "color:green", value) + } + }), + Natural.Increase = colDef( + name = "Increase", + filterable = TRUE, + cell = function(value) { + if (value <= 7614) { + tags$span(class = "badge bg-primary", value) + } else if (value <= 15914) { + tags$span(class = "badge bg-secondary", value) + } else if (value <= 34152) { + tags$span(class = "badge bg-info", value) + } else { + tags$span(class = "badge bg-success", value) + } + }), + Geographic.Area = colDef(name = "Location", filterable = TRUE)))) + + downloadablePlot("examplePlot2", ss_userAction.Log, filenameroot = "plot2_ggplot", diff --git a/tests/testthat/sample_app_left_sidebar/program/ui_body.R b/tests/testthat/sample_app_left_sidebar/program/ui_body.R index d1c33791..62619516 100644 --- a/tests/testthat/sample_app_left_sidebar/program/ui_body.R +++ b/tests/testthat/sample_app_left_sidebar/program/ui_body.R @@ -168,11 +168,11 @@ table_downloader_box <- box( column(width = 6, tags$dl(tags$dt("Setup"), tags$li("Module should be configured in both UI and Server code"), - tags$li("In your 'body_ui.R', place module UI part as follow: ", + tags$li("In the 'body_ui.R' add the following lines:", blockQuote("downloadableTableUI('exampleDT1', list('csv', 'tsv'), 'Download table data'))", color = "info")), - tags$li("In your 'server_local.R', place module server part, ", tags$em("with the same id used with UI part"), ", as follow: ", + tags$li("In the 'server_local.R' add the following lines (make sure the two use the same ID):", blockQuote("downloadableTable('exampleDT1', ss_userAction.Log, 'exampletable', @@ -186,6 +186,47 @@ table_downloader_box <- box( ) + +react_table_box <- box( + id = "react_downloader", + title = "React Table Downloader", + status = "info", + solidHeader = TRUE, + collapsible = TRUE, + width = 12, + fluidRow(column(width = 6, + tags$dl(tags$dt("Features"), + tags$ul(tags$li("React table downloader module displays tabular data in rich formatted tables using the", tags$b("`reactable`"), "package"), + tags$li("downloadableReactTable returns a named reactive list that can be used in the application:", + tags$ul(tags$li(tags$b("selected_rows"), ": data.frame of current selected rows"), + tags$li(tags$b("table_state"), ": a list of the current table state"))), + tags$li("Table data can be downloaded in different formats such as: ", tags$b("'csv'"), + ", ", tags$b("'tsv'"), ", ", tags$b("'txt'"), "and/or ", tags$b("'xlsx'")), + tags$li("User can customize downloadableReactTable modules using reactable package options."), + tags$li("For more information about table options please visit the", + tags$a("Reactable documentation", target = "_blank", href = "https://glin.github.io/reactable/"), + "site") + ))), + column(width = 6, + tags$dl(tags$dt("Setup"), + tags$li("Module should be configured in both UI and Server code"), + tags$li("In the 'body_ui.R' add the following lines:", + blockQuote("downloadableReactTableUI('exampleReactTable', + 'Download react table data'))", color = "info")), + tags$li("In the 'server_local.R' add the following lines (make sure the two use the same ID):", + blockQuote("downloadableReactTable('exampleReactTable', + ss_userAction.Log, + 'exampletable', + list(csv = load_data3, tsv = load_data3))", color = "info")), + tags$li("Review ", tags$b("?downloadableReactTableUI"), " and ", tags$b("?downloadableReactTable"), " for more information"), + tags$li("Review below table for detailed example code")))), + fluidRow(column(width = 12, + downloadableReactTableUI(id = "exampleReactTable", + downloadtypes = list("csv", "tsv"), + hovertext = "Download react table data"))) + +) + file_downloader_box <- box( id = "file_downloader", title = "File Downloader", @@ -237,9 +278,9 @@ plot_downloader_box <- box( column(width = 6, tags$dl(tags$dt("Setup"), tags$ul(tags$li("Module should be configured in both the UI and Server code"), - tags$li("In your 'body_ui.R', place module UI part as follow: ", + tags$li("In the 'body_ui.R' add the following lines:", blockQuote("downloadablePlotUI('myplotID', c('png', 'csv'), 'Download Plot or Data', '300px'))", color = "info")), - tags$li("In your 'server_local.R', place module server part, ", tags$em("with the same id used with UI part"), ", as follow: ", + tags$li("In the 'server_local.R' add the following lines (make sure the two use the same ID):", blockQuote("downloadablePlot('myplotID', ss_userAction.Log, filenameroot = 'mydownload1', @@ -297,6 +338,7 @@ reset_application_box <- box( periscope_modules <- tabItem(tabName = "periscope_modules", + react_table_box, table_downloader_box, plot_downloader_box, file_downloader_box, diff --git a/tests/testthat/sample_app_left_sidebar/www/css/custom.css b/tests/testthat/sample_app_left_sidebar/www/css/custom.css index f59e4811..586bf0c6 100644 --- a/tests/testthat/sample_app_left_sidebar/www/css/custom.css +++ b/tests/testthat/sample_app_left_sidebar/www/css/custom.css @@ -60,6 +60,11 @@ table.shiny-table { float: left; } +/* Downloadable React Table */ +.periscope-downloadable-react-table-button { + display: inline-flex; +} + /* Downloadable Plot */ .periscope-downloadable-plot { } .periscope-downloadable-plot-button { @@ -117,6 +122,28 @@ a:focus:not(.nav-link) { color: #1F2D3D!important; } +/* downloadableReactTable Tooltip */ +.tooltip[data-tooltip-for="react-download-button"] .tooltip-inner { + background-color: #ffffff !important; + color: #212529 !important; + border: 1px solid #d1d5db !important; +} + +.tooltip[data-tooltip-for="react-download-button"].bs-tooltip-top .arrow::before { + border-top-color: #d1d5db !important; +} + +.tooltip[data-tooltip-for="react-download-button"].bs-tooltip-top .arrow::after { + content: ''; + position: absolute; + bottom: 1px; + left: 50%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: #ffffff transparent transparent transparent; +} + /* END of periscope2 basic CSS*/ /* ========================================================================== */ diff --git a/tests/testthat/sample_app_left_sidebar/www/js/custom.js b/tests/testthat/sample_app_left_sidebar/www/js/custom.js index 728ebc18..a613b78a 100644 --- a/tests/testthat/sample_app_left_sidebar/www/js/custom.js +++ b/tests/testthat/sample_app_left_sidebar/www/js/custom.js @@ -20,6 +20,31 @@ setInterval(function() { }, 100); +function initializeReactTableTooltips() { + // Initialize all downloadable react table tooltips + $('.periscope-downloadable-react-table-button span.btn-group') + .tooltip() + .attr('data-tooltip-for', 'react-download-button'); + + // Add event handler only once + $(document).off('show.bs.tooltip.custom').on('show.bs.tooltip.custom', function(e) { + var $trigger = $(e.target); + var tooltipFor = $trigger.attr('data-tooltip-for'); + + if (tooltipFor) { + var tooltipInstance = $trigger.data('bs.tooltip'); + var $tip = $(tooltipInstance.tip); + $tip.attr('data-tooltip-for', tooltipFor); + } + }); +} + + +$(document).ready(function() { + initializeReactTableTooltips(); +}); + + // Resolve bs4Dash alert title and close button issues Shiny.addCustomMessageHandler('pcreate-alert', function (message) { // setup target diff --git a/tests/testthat/sample_app_no_both_sidebars/program/fxn/plots.R b/tests/testthat/sample_app_no_both_sidebars/program/fxn/plots.R index 4b3d5aea..5942a824 100644 --- a/tests/testthat/sample_app_no_both_sidebars/program/fxn/plots.R +++ b/tests/testthat/sample_app_no_both_sidebars/program/fxn/plots.R @@ -14,9 +14,9 @@ attr(mtcars, "show_rownames") <- TRUE plot2ggplot <- function() { ggplot(data = mtcars, aes(x = wt, y = mpg)) + geom_point(aes(color = cyl)) + - theme(legend.justification = c(1, 1), - legend.position = c(1, 1), - legend.title = element_blank()) + + theme(legend.justification = c(1, 1), + legend.position.inside = c(1, 1), + legend.title = element_blank()) + ggtitle("GGPlot Example w/Hover") + xlab("wt") + ylab("mpg") diff --git a/tests/testthat/sample_app_no_both_sidebars/program/fxn/program_helpers.R b/tests/testthat/sample_app_no_both_sidebars/program/fxn/program_helpers.R index 5e0b6d64..d6d7d968 100644 --- a/tests/testthat/sample_app_no_both_sidebars/program/fxn/program_helpers.R +++ b/tests/testthat/sample_app_no_both_sidebars/program/fxn/program_helpers.R @@ -9,6 +9,7 @@ files_idx <- read.csv("program/data/struc_indx.csv") rownames(app_files) <- app_files$X app_files$X <- NULL +app_files <- app_files %>% mutate(across(where(is.character), ~na_if(., ""))) rownames(files_idx) <- files_idx$X files_idx$X <- NULL @@ -32,10 +33,10 @@ load_data2 <- function() { load_data3 <- function() { ldf <- df %>% - select(1:3) %>% + select(1:3) %>% mutate(Total.Population.Change = as.numeric(gsub(",", "", Total.Population.Change)), Natural.Increase = as.numeric(gsub(",", "", Natural.Increase))) - + as.data.frame(ldf) } diff --git a/tests/testthat/sample_app_no_both_sidebars/program/global.R b/tests/testthat/sample_app_no_both_sidebars/program/global.R index f0ec7ffb..5952b8dc 100644 --- a/tests/testthat/sample_app_no_both_sidebars/program/global.R +++ b/tests/testthat/sample_app_no_both_sidebars/program/global.R @@ -9,6 +9,7 @@ # to server, UI and session scopes # ---------------------------------------- library(DT) +library(reactable) library(shiny) library(periscope2) library(waiter) diff --git a/tests/testthat/sample_app_no_both_sidebars/program/server_local.R b/tests/testthat/sample_app_no_both_sidebars/program/server_local.R index 7f5c46ab..5066c62a 100644 --- a/tests/testthat/sample_app_no_both_sidebars/program/server_local.R +++ b/tests/testthat/sample_app_no_both_sidebars/program/server_local.R @@ -70,6 +70,43 @@ downloadableTable("exampleDT1", formatStyle = list(columns = c("Natural.Increase"), backgroundColor = DT::styleInterval(c(7614, 15914, 34152), c("lightgray", "gray", "cadetblue", "#808000"))))) + +downloadableReactTable(id = "exampleReactTable", + logger = ss_userAction.Log, + table_data = load_data3, + file_name_root = "exampleReacttable", + download_data_fxns = list(csv = load_data3, tsv = load_data3), + table_options = list( + defaultSorted = "Total.Population.Change", + columnGroups = list(colGroup(name = "Statistics", columns = c("Total.Population.Change", "Natural.Increase"))), + columns = list( + Total.Population.Change = colDef( + name = "Change", + filterable = TRUE, + cell = function(value) { + if (value <= 0) { + tags$span(style = "color:red", value) + } else { + tags$span(style = "color:green", value) + } + }), + Natural.Increase = colDef( + name = "Increase", + filterable = TRUE, + cell = function(value) { + if (value <= 7614) { + tags$span(class = "badge bg-primary", value) + } else if (value <= 15914) { + tags$span(class = "badge bg-secondary", value) + } else if (value <= 34152) { + tags$span(class = "badge bg-info", value) + } else { + tags$span(class = "badge bg-success", value) + } + }), + Geographic.Area = colDef(name = "Location", filterable = TRUE)))) + + downloadablePlot("examplePlot2", ss_userAction.Log, filenameroot = "plot2_ggplot", diff --git a/tests/testthat/sample_app_no_both_sidebars/program/ui_body.R b/tests/testthat/sample_app_no_both_sidebars/program/ui_body.R index 488885a4..2779ba24 100644 --- a/tests/testthat/sample_app_no_both_sidebars/program/ui_body.R +++ b/tests/testthat/sample_app_no_both_sidebars/program/ui_body.R @@ -162,11 +162,11 @@ table_downloader_box <- box( column(width = 6, tags$dl(tags$dt("Setup"), tags$li("Module should be configured in both UI and Server code"), - tags$li("In your 'body_ui.R', place module UI part as follow: ", + tags$li("In the 'body_ui.R' add the following lines:", blockQuote("downloadableTableUI('exampleDT1', list('csv', 'tsv'), 'Download table data'))", color = "info")), - tags$li("In your 'server_local.R', place module server part, ", tags$em("with the same id used with UI part"), ", as follow: ", + tags$li("In the 'server_local.R' add the following lines (make sure the two use the same ID):", blockQuote("downloadableTable('exampleDT1', ss_userAction.Log, 'exampletable', @@ -180,6 +180,48 @@ table_downloader_box <- box( ) + +react_table_box <- box( + id = "react_downloader", + title = "React Table Downloader", + status = "info", + solidHeader = TRUE, + collapsible = TRUE, + width = 12, + fluidRow(column(width = 6, + tags$dl(tags$dt("Features"), + tags$ul(tags$li("React table downloader module displays tabular data in rich formatted tables using the", tags$b("`reactable`"), "package"), + tags$li("downloadableReactTable returns a named reactive list that can be used in the application:", + tags$ul(tags$li(tags$b("selected_rows"), ": data.frame of current selected rows"), + tags$li(tags$b("table_state"), ": a list of the current table state"))), + tags$li("Table data can be downloaded in different formats such as: ", tags$b("'csv'"), + ", ", tags$b("'tsv'"), ", ", tags$b("'txt'"), "and/or ", tags$b("'xlsx'")), + tags$li("User can customize downloadableReactTable modules using reactable package options."), + tags$li("For more information about table options please visit the", + tags$a("Reactable documentation", target = "_blank", href = "https://glin.github.io/reactable/"), + "site") + ))), + column(width = 6, + tags$dl(tags$dt("Setup"), + tags$li("Module should be configured in both UI and Server code"), + tags$li("In the 'body_ui.R' add the following lines:", + blockQuote("downloadableReactTableUI('exampleReactTable', + 'Download react table data'))", color = "info")), + tags$li("In the 'server_local.R' add the following lines (make sure the two use the same ID):", + blockQuote("downloadableReactTable('exampleReactTable', + ss_userAction.Log, + 'exampletable', + list(csv = load_data3, tsv = load_data3))", color = "info")), + tags$li("Review ", tags$b("?downloadableReactTableUI"), " and ", tags$b("?downloadableReactTable"), " for more information"), + tags$li("Review below table for detailed example code")))), + fluidRow(column(width = 12, + downloadableReactTableUI(id = "exampleReactTable", + downloadtypes = list("csv", "tsv"), + hovertext = "Download react table data"))) + +) + + file_downloader_box <- box( id = "file_downloader", title = "File Downloader", @@ -231,9 +273,9 @@ plot_downloader_box <- box( column(width = 6, tags$dl(tags$dt("Setup"), tags$ul(tags$li("Module should be configured in both the UI and Server code"), - tags$li("In your 'body_ui.R', place module UI part as follow: ", + tags$li("In the 'body_ui.R' add the following lines:", blockQuote("downloadablePlotUI('myplotID', c('png', 'csv'), 'Download Plot or Data', '300px'))", color = "info")), - tags$li("In your 'server_local.R', place module server part, ", tags$em("with the same id used with UI part"), ", as follow: ", + tags$li("In the 'server_local.R' add the following lines (make sure the two use the same ID):", blockQuote("downloadablePlot('myplotID', ss_userAction.Log, filenameroot = 'mydownload1', @@ -291,6 +333,7 @@ reset_application_box <- box( periscope_modules <- tabItem(tabName = "periscope_modules", + react_table_box, table_downloader_box, plot_downloader_box, file_downloader_box, diff --git a/tests/testthat/sample_app_no_both_sidebars/www/css/custom.css b/tests/testthat/sample_app_no_both_sidebars/www/css/custom.css index 6e10e612..e19ef002 100644 --- a/tests/testthat/sample_app_no_both_sidebars/www/css/custom.css +++ b/tests/testthat/sample_app_no_both_sidebars/www/css/custom.css @@ -60,6 +60,11 @@ table.shiny-table { float: left; } +/* Downloadable React Table */ +.periscope-downloadable-react-table-button { + display: inline-flex; +} + /* Downloadable Plot */ .periscope-downloadable-plot { } .periscope-downloadable-plot-button { @@ -117,6 +122,28 @@ a:focus:not(.nav-link) { color: #1F2D3D!important; } + +/* downloadableReactTable Tooltip */ +.tooltip[data-tooltip-for="react-download-button"] .tooltip-inner { + background-color: #ffffff !important; + color: #212529 !important; + border: 1px solid #d1d5db !important; +} + +.tooltip[data-tooltip-for="react-download-button"].bs-tooltip-top .arrow::before { + border-top-color: #d1d5db !important; +} + +.tooltip[data-tooltip-for="react-download-button"].bs-tooltip-top .arrow::after { + content: ''; + position: absolute; + bottom: 1px; + left: 50%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: #ffffff transparent transparent transparent; +} /* END of periscope2 basic CSS*/ /* ========================================================================== */ diff --git a/tests/testthat/sample_app_no_both_sidebars/www/js/custom.js b/tests/testthat/sample_app_no_both_sidebars/www/js/custom.js index 728ebc18..a613b78a 100644 --- a/tests/testthat/sample_app_no_both_sidebars/www/js/custom.js +++ b/tests/testthat/sample_app_no_both_sidebars/www/js/custom.js @@ -20,6 +20,31 @@ setInterval(function() { }, 100); +function initializeReactTableTooltips() { + // Initialize all downloadable react table tooltips + $('.periscope-downloadable-react-table-button span.btn-group') + .tooltip() + .attr('data-tooltip-for', 'react-download-button'); + + // Add event handler only once + $(document).off('show.bs.tooltip.custom').on('show.bs.tooltip.custom', function(e) { + var $trigger = $(e.target); + var tooltipFor = $trigger.attr('data-tooltip-for'); + + if (tooltipFor) { + var tooltipInstance = $trigger.data('bs.tooltip'); + var $tip = $(tooltipInstance.tip); + $tip.attr('data-tooltip-for', tooltipFor); + } + }); +} + + +$(document).ready(function() { + initializeReactTableTooltips(); +}); + + // Resolve bs4Dash alert title and close button issues Shiny.addCustomMessageHandler('pcreate-alert', function (message) { // setup target diff --git a/tests/testthat/sample_app_right_sidebar/program/fxn/plots.R b/tests/testthat/sample_app_right_sidebar/program/fxn/plots.R index b6157c8f..c7abcc03 100644 --- a/tests/testthat/sample_app_right_sidebar/program/fxn/plots.R +++ b/tests/testthat/sample_app_right_sidebar/program/fxn/plots.R @@ -14,9 +14,9 @@ attr(mtcars, "show_rownames") <- TRUE plot2ggplot <- function() { ggplot(data = mtcars, aes(x = wt, y = mpg)) + geom_point(aes(color = cyl)) + - theme(legend.justification = c(1, 1), - legend.position = c(1, 1), - legend.title = element_blank()) + + theme(legend.justification = c(1, 1), + legend.position.inside = c(1, 1), + legend.title = element_blank()) + ggtitle("GGPlot Example w/Hover") + xlab("wt") + ylab("mpg") diff --git a/tests/testthat/sample_app_right_sidebar/program/fxn/program_helpers.R b/tests/testthat/sample_app_right_sidebar/program/fxn/program_helpers.R index 5e0b6d64..d6d7d968 100644 --- a/tests/testthat/sample_app_right_sidebar/program/fxn/program_helpers.R +++ b/tests/testthat/sample_app_right_sidebar/program/fxn/program_helpers.R @@ -9,6 +9,7 @@ files_idx <- read.csv("program/data/struc_indx.csv") rownames(app_files) <- app_files$X app_files$X <- NULL +app_files <- app_files %>% mutate(across(where(is.character), ~na_if(., ""))) rownames(files_idx) <- files_idx$X files_idx$X <- NULL @@ -32,10 +33,10 @@ load_data2 <- function() { load_data3 <- function() { ldf <- df %>% - select(1:3) %>% + select(1:3) %>% mutate(Total.Population.Change = as.numeric(gsub(",", "", Total.Population.Change)), Natural.Increase = as.numeric(gsub(",", "", Natural.Increase))) - + as.data.frame(ldf) } diff --git a/tests/testthat/sample_app_right_sidebar/program/global.R b/tests/testthat/sample_app_right_sidebar/program/global.R index f0ec7ffb..5952b8dc 100644 --- a/tests/testthat/sample_app_right_sidebar/program/global.R +++ b/tests/testthat/sample_app_right_sidebar/program/global.R @@ -9,6 +9,7 @@ # to server, UI and session scopes # ---------------------------------------- library(DT) +library(reactable) library(shiny) library(periscope2) library(waiter) diff --git a/tests/testthat/sample_app_right_sidebar/program/server_local.R b/tests/testthat/sample_app_right_sidebar/program/server_local.R index df843931..3f6a703e 100644 --- a/tests/testthat/sample_app_right_sidebar/program/server_local.R +++ b/tests/testthat/sample_app_right_sidebar/program/server_local.R @@ -70,6 +70,43 @@ downloadableTable("exampleDT1", formatStyle = list(columns = c("Natural.Increase"), backgroundColor = DT::styleInterval(c(7614, 15914, 34152), c("lightgray", "gray", "cadetblue", "#808000"))))) + +downloadableReactTable(id = "exampleReactTable", + logger = ss_userAction.Log, + table_data = load_data3, + file_name_root = "exampleReacttable", + download_data_fxns = list(csv = load_data3, tsv = load_data3), + table_options = list( + defaultSorted = "Total.Population.Change", + columnGroups = list(colGroup(name = "Statistics", columns = c("Total.Population.Change", "Natural.Increase"))), + columns = list( + Total.Population.Change = colDef( + name = "Change", + filterable = TRUE, + cell = function(value) { + if (value <= 0) { + tags$span(style = "color:red", value) + } else { + tags$span(style = "color:green", value) + } + }), + Natural.Increase = colDef( + name = "Increase", + filterable = TRUE, + cell = function(value) { + if (value <= 7614) { + tags$span(class = "badge bg-primary", value) + } else if (value <= 15914) { + tags$span(class = "badge bg-secondary", value) + } else if (value <= 34152) { + tags$span(class = "badge bg-info", value) + } else { + tags$span(class = "badge bg-success", value) + } + }), + Geographic.Area = colDef(name = "Location", filterable = TRUE)))) + + downloadablePlot("examplePlot2", ss_userAction.Log, filenameroot = "plot2_ggplot", diff --git a/tests/testthat/sample_app_right_sidebar/program/ui_body.R b/tests/testthat/sample_app_right_sidebar/program/ui_body.R index e8a2df84..ede2f8f7 100644 --- a/tests/testthat/sample_app_right_sidebar/program/ui_body.R +++ b/tests/testthat/sample_app_right_sidebar/program/ui_body.R @@ -168,11 +168,11 @@ table_downloader_box <- box( column(width = 6, tags$dl(tags$dt("Setup"), tags$li("Module should be configured in both UI and Server code"), - tags$li("In your 'body_ui.R', place module UI part as follow: ", + tags$li("In the 'body_ui.R' add the following lines:", blockQuote("downloadableTableUI('exampleDT1', list('csv', 'tsv'), 'Download table data'))", color = "info")), - tags$li("In your 'server_local.R', place module server part, ", tags$em("with the same id used with UI part"), ", as follow: ", + tags$li("In the 'server_local.R' add the following lines (make sure the two use the same ID):", blockQuote("downloadableTable('exampleDT1', ss_userAction.Log, 'exampletable', @@ -186,6 +186,48 @@ table_downloader_box <- box( ) + +react_table_box <- box( + id = "react_downloader", + title = "React Table Downloader", + status = "info", + solidHeader = TRUE, + collapsible = TRUE, + width = 12, + fluidRow(column(width = 6, + tags$dl(tags$dt("Features"), + tags$ul(tags$li("React table downloader module displays tabular data in rich formatted tables using the", tags$b("`reactable`"), "package"), + tags$li("downloadableReactTable returns a named reactive list that can be used in the application:", + tags$ul(tags$li(tags$b("selected_rows"), ": data.frame of current selected rows"), + tags$li(tags$b("table_state"), ": a list of the current table state"))), + tags$li("Table data can be downloaded in different formats such as: ", tags$b("'csv'"), + ", ", tags$b("'tsv'"), ", ", tags$b("'txt'"), "and/or ", tags$b("'xlsx'")), + tags$li("User can customize downloadableReactTable modules using reactable package options."), + tags$li("For more information about table options please visit the", + tags$a("Reactable documentation", target = "_blank", href = "https://glin.github.io/reactable/"), + "site") + ))), + column(width = 6, + tags$dl(tags$dt("Setup"), + tags$li("Module should be configured in both UI and Server code"), + tags$li("In the 'body_ui.R' add the following lines:", + blockQuote("downloadableReactTableUI('exampleReactTable', + 'Download react table data'))", color = "info")), + tags$li("In the 'server_local.R' add the following lines (make sure the two use the same ID):", + blockQuote("downloadableReactTable('exampleReactTable', + ss_userAction.Log, + 'exampletable', + list(csv = load_data3, tsv = load_data3))", color = "info")), + tags$li("Review ", tags$b("?downloadableReactTableUI"), " and ", tags$b("?downloadableReactTable"), " for more information"), + tags$li("Review below table for detailed example code")))), + fluidRow(column(width = 12, + downloadableReactTableUI(id = "exampleReactTable", + downloadtypes = list("csv", "tsv"), + hovertext = "Download react table data"))) + +) + + file_downloader_box <- box( id = "file_downloader", title = "File Downloader", @@ -237,9 +279,9 @@ plot_downloader_box <- box( column(width = 6, tags$dl(tags$dt("Setup"), tags$ul(tags$li("Module should be configured in both the UI and Server code"), - tags$li("In your 'body_ui.R', place module UI part as follow: ", + tags$li("In the 'body_ui.R' add the following lines:", blockQuote("downloadablePlotUI('myplotID', c('png', 'csv'), 'Download Plot or Data', '300px'))", color = "info")), - tags$li("In your 'server_local.R', place module server part, ", tags$em("with the same id used with UI part"), ", as follow: ", + tags$li("In the 'server_local.R' add the following lines (make sure the two use the same ID):", blockQuote("downloadablePlot('myplotID', ss_userAction.Log, filenameroot = 'mydownload1', @@ -297,6 +339,7 @@ reset_application_box <- box( periscope_modules <- tabItem(tabName = "periscope_modules", + react_table_box table_downloader_box, plot_downloader_box, file_downloader_box, diff --git a/tests/testthat/sample_app_right_sidebar/www/css/custom.css b/tests/testthat/sample_app_right_sidebar/www/css/custom.css index 728be5f0..ec2f2a37 100644 --- a/tests/testthat/sample_app_right_sidebar/www/css/custom.css +++ b/tests/testthat/sample_app_right_sidebar/www/css/custom.css @@ -59,6 +59,11 @@ table.shiny-table { float: left; } +/* Downloadable React Table */ +.periscope-downloadable-react-table-button { + display: inline-flex; +} + /* Downloadable Plot */ .periscope-downloadable-plot { } .periscope-downloadable-plot-button { @@ -116,6 +121,27 @@ a:focus:not(.nav-link) { color: #1F2D3D!important; } +/* downloadableReactTable Tooltip */ +.tooltip[data-tooltip-for="react-download-button"] .tooltip-inner { + background-color: #ffffff !important; + color: #212529 !important; + border: 1px solid #d1d5db !important; +} + +.tooltip[data-tooltip-for="react-download-button"].bs-tooltip-top .arrow::before { + border-top-color: #d1d5db !important; +} + +.tooltip[data-tooltip-for="react-download-button"].bs-tooltip-top .arrow::after { + content: ''; + position: absolute; + bottom: 1px; + left: 50%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: #ffffff transparent transparent transparent; +} /* END of periscope2 basic CSS*/ /* ========================================================================== */ diff --git a/tests/testthat/sample_app_right_sidebar/www/js/custom.js b/tests/testthat/sample_app_right_sidebar/www/js/custom.js index 728ebc18..a613b78a 100644 --- a/tests/testthat/sample_app_right_sidebar/www/js/custom.js +++ b/tests/testthat/sample_app_right_sidebar/www/js/custom.js @@ -20,6 +20,31 @@ setInterval(function() { }, 100); +function initializeReactTableTooltips() { + // Initialize all downloadable react table tooltips + $('.periscope-downloadable-react-table-button span.btn-group') + .tooltip() + .attr('data-tooltip-for', 'react-download-button'); + + // Add event handler only once + $(document).off('show.bs.tooltip.custom').on('show.bs.tooltip.custom', function(e) { + var $trigger = $(e.target); + var tooltipFor = $trigger.attr('data-tooltip-for'); + + if (tooltipFor) { + var tooltipInstance = $trigger.data('bs.tooltip'); + var $tip = $(tooltipInstance.tip); + $tip.attr('data-tooltip-for', tooltipFor); + } + }); +} + + +$(document).ready(function() { + initializeReactTableTooltips(); +}); + + // Resolve bs4Dash alert title and close button issues Shiny.addCustomMessageHandler('pcreate-alert', function (message) { // setup target diff --git a/tests/testthat/setup.R b/tests/testthat/setup.R index f4fdf5df..5c769262 100644 --- a/tests/testthat/setup.R +++ b/tests/testthat/setup.R @@ -11,3 +11,25 @@ if (interactive()) { FUN = function(x) source(file.path(test_source_path, x)))) rm(test_source_path) } + + +reset_g_opts <- function() { + .g_opts$tt_image <- "img/tooltip.png" + .g_opts$tt_height <- "16px" + .g_opts$tt_width <- "16px" + .g_opts$datetime.fmt <- "%m-%d-%Y %H:%M" + .g_opts$log.formatter <- function(record) { paste0(record$logger, " [", record$timestamp, "] ", record$msg) } + .g_opts$loglevel <- "DEBUG" + .g_opts$app_title <- "Set using add_ui_header() in program/ui_header.R" + .g_opts$app_info <- NULL + .g_opts$app_version <- "1.0.0" + .g_opts$loading_indicator <- NULL + .g_opts$announcements_file <- NULL + .g_opts$data_download_types <- c("csv", "xlsx", "tsv", "txt") + .g_opts$plot_download_types <- c("png", "jpeg", "tiff", "bmp") + .g_opts$left_sidebar <- list(disable = TRUE) + .g_opts$body_elements <- c() + .g_opts$header <- NULL + .g_opts$right_sidebar <- NULL + .g_opts$footer <- NULL +} diff --git a/tests/testthat/test_download_file.R b/tests/testthat/test_download_file.R index 3786dbdb..c7020b15 100644 --- a/tests/testthat/test_download_file.R +++ b/tests/testthat/test_download_file.R @@ -5,9 +5,9 @@ local_edition(3) download_plot <- function() { ggplot2::ggplot(data = mtcars, aes(x = wt, y = mpg)) + geom_point(aes(color = cyl)) + - theme(legend.justification = c(1, 1), - legend.position = c(1, 1), - legend.title = element_blank()) + + theme(legend.justification = c(1, 1), + legend.position.inside = c(1, 1), + legend.title = element_blank()) + ggtitle("GGPlot Example w/Hover") + xlab("wt") + ylab("mpg") @@ -21,11 +21,6 @@ download_data <- function() { head(mtcars) } -download_data_show_row_names <- function() { - attr(mtcars, "show_rownames") <- TRUE - head(mtcars) -} - download_string_list <- function() { c("test1", "test2", "tests") } @@ -34,6 +29,18 @@ download_char_data <- function() { "A123B" } +create_openxlsx2_wb <- function() { + openxlsx2::wb_workbook()$add_worksheet("openxlsx2_workbook")$add_data(x = download_data()) +} + +create_openxlsx_wb <- function() { + wb <- openxlsx::createWorkbook() + openxlsx::addWorksheet(wb, "openxlsx_workbook") + data <- as.data.frame(download_data()) + openxlsx::writeData(wb, "openxlsx_workbook", data) + wb +} + # UI Testing test_that("downloadFileButton", { file_btn <- downloadFileButton(id = "myid", @@ -87,9 +94,11 @@ test_that("downloadFile_AvailableTypes", { }) test_that("downloadFile - all download types", { + skip_if(getRversion() < "4.1.0", "Skipping due to lifecycle warnings in R < 4.1.0") testServer(downloadFile, args = list(logger = periscope2:::fw_get_user_log(), filenameroot = "mydownload1", + row_names = FALSE, datafxns = list(csv = download_data, xlsx = download_data, tsv = download_data, @@ -99,19 +108,28 @@ test_that("downloadFile - all download types", { tiff = download_plot, bmp = download_plot)), expr = { - expect_snapshot_file(output$csv) - expect_snapshot_file(output$tsv) - expect_snapshot_file(output$txt) - expect_true(file.exists(output$xlsx)) - expect_true(file.exists(output$png)) - expect_true(file.exists(output$jpeg)) - expect_true(file.exists(output$tiff)) - expect_true(file.exists(output$bmp)) + expect_true(grepl(pattern = "INFO:actions:File downloaded in browser: < mydownload1.csv >", + x = capture_output(expect_snapshot_file(output$csv)))) + expect_true(grepl(pattern = "INFO:actions:File downloaded in browser: < mydownload1.tsv >", + x = capture_output(expect_snapshot_file(output$tsv)))) + expect_true(grepl(pattern = "INFO:actions:File downloaded in browser: < mydownload1.txt >", + x = capture_output(expect_snapshot_file(output$txt)))) + expect_true(grepl(pattern = "INFO:actions:File downloaded in browser: < mydownload1.xlsx >", + x = capture_output(file.exists(output$xlsx)))) + expect_true(grepl(pattern = "INFO:actions:File downloaded in browser: < mydownload1.png >", + x = capture_output(file.exists(output$png)))) + expect_true(grepl(pattern = "INFO:actions:File downloaded in browser: < mydownload1.jpeg >", + x = capture_output(file.exists(output$jpeg)))) + expect_true(grepl(pattern = "INFO:actions:File downloaded in browser: < mydownload1.tiff >", + x = capture_output(file.exists(output$tiff)))) + expect_true(grepl(pattern = "INFO:actions:File downloaded in browser: < mydownload1.bmp >", + x = capture_output(file.exists(output$bmp)))) }) }) test_that("downloadFile - lattice plot", { + skip_if(getRversion() < "4.1.0", "Skipping due to lifecycle warnings in R < 4.1.0") testServer(downloadFile, args = list(logger = periscope2:::fw_get_user_log(), filenameroot = "mydownload1", @@ -120,10 +138,14 @@ test_that("downloadFile - lattice plot", { tiff = download_plot, bmp = download_lattice_plot)), expr = { - expect_true(file.exists(output$png)) - expect_true(file.exists(output$jpeg)) - expect_true(file.exists(output$tiff)) - expect_true(file.exists(output$bmp)) + expect_true(grepl(pattern = "INFO:actions:File downloaded in browser: < mydownload1.png >", + x = capture_output(expect_true(file.exists(output$png))))) + expect_true(grepl(pattern = "INFO:actions:File downloaded in browser: < mydownload1.jpeg >", + x = capture_output(expect_true(file.exists(output$jpeg))))) + expect_true(grepl(pattern = "INFO:actions:File downloaded in browser: < mydownload1.tiff >", + x = capture_output(expect_true(file.exists(output$tiff))))) + expect_true(grepl(pattern = "INFO:actions:File downloaded in browser: < mydownload1.bmp >", + x = capture_output(expect_true(file.exists(output$bmp))))) }) }) @@ -133,9 +155,20 @@ test_that("downloadFile - show rownames", { testServer(downloadFile, args = list(logger = periscope2:::fw_get_user_log(), filenameroot = "show_row_names_download", - datafxns = list(csv = download_data_show_row_names)), + row_names = TRUE, + datafxns = list(csv = download_data, + tsv = download_data, + txt = download_data, + xlsx = download_data)), expr = { - expect_snapshot_file(output$csv) + expect_true(grepl(pattern = "INFO:actions:File downloaded in browser: < show_row_names_download.csv >", + x = capture_output(expect_snapshot_file(output$csv)))) + expect_true(grepl(pattern = "INFO:actions:File downloaded in browser: < show_row_names_download.tsv >", + x = capture_output(expect_snapshot_file(output$tsv)))) + expect_true(grepl(pattern = "INFO:actions:File downloaded in browser: < show_row_names_download.txt >", + x = capture_output(expect_snapshot_file(output$txt)))) + expect_true(grepl(pattern = "INFO:actions:File downloaded in browser: < show_row_names_download.xlsx >", + x = capture_output(file.exists(output$xlsx)))) }) }) @@ -143,35 +176,58 @@ test_that("downloadFile - download char data", { testServer(downloadFile, args = list(logger = periscope2:::fw_get_user_log(), filenameroot = "my_char_download", + row_names = FALSE, datafxns = list(txt = download_char_data, tsv = download_char_data, csv = download_char_data)), expr = { - expect_snapshot_file(output$txt) - expect_snapshot_file(output$tsv) - expect_snapshot_file(output$csv) + expect_true(grepl(pattern = "INFO:actions:File downloaded in browser: < my_char_download.csv >", + x = capture_output(expect_snapshot_file(output$csv)))) + expect_true(grepl(pattern = "INFO:actions:File downloaded in browser: < my_char_download.tsv >", + x = capture_output(expect_snapshot_file(output$tsv)))) + expect_true(grepl(pattern = "INFO:actions:File downloaded in browser: < my_char_download.txt >", + x = capture_output(expect_snapshot_file(output$txt)))) }) }) -test_that("downloadFile - download txt numeric data", { +test_that("downloadFile - download numeric data", { testServer(downloadFile, args = list(logger = periscope2:::fw_get_user_log(), filenameroot = "my_numeric_data", - datafxns = list(txt = function() {123})), + datafxns = list(txt = function() {123}, + csv = function() {123}, + tsv = function() {123})), expr = { - expect_warning(output$txt, "txt could not be processed") + expect_warning(expect_true(grepl( + pattern = "INFO:actions:File downloaded in browser: < my_numeric_data.txt >", + x = capture_output(output$txt))), "txt could not be processed") + expect_true(grepl( + pattern = "INFO:actions:File downloaded in browser: < my_numeric_data.csv >", + x = capture_output(output$csv))) + expect_true(grepl( + pattern = "INFO:actions:File downloaded in browser: < my_numeric_data.tsv >", + x = capture_output(output$tsv))) + }) }) test_that("downloadFile - default values", { testServer(downloadFile, - args = list(datafxns = list(txt = function() {"123"})), + args = list(datafxns = list(txt = function() {"123"}, + csv = function() {"123"}, + tsv = function() {"123"})), expr = { - expect_snapshot_file(output$txt) + expect_true(grepl(pattern = "INFO::File downloaded in browser: < download.txt >", + x = capture_output(expect_snapshot_file(output$txt)))) + expect_true(grepl(pattern = "INFO::File downloaded in browser: < download.csv >", + x = capture_output(expect_snapshot_file(output$csv)))) + expect_true(grepl(pattern = "INFO::File downloaded in browser: < download.tsv >", + x = capture_output(expect_snapshot_file(output$tsv)))) }) }) test_that("downloadFile - invalid type", { + skip_if(getRversion() < "4.1.0", "Skipping due to lifecycle warnings in R < 4.1.0") testServer(downloadFile, args = list(datafxns = list(ttt = function() {"123"}, jeg = download_lattice_plot, @@ -182,3 +238,71 @@ test_that("downloadFile - invalid type", { expect_error(output$tff) }) }) + +# Testing for xlsx downloads +test_that("Testing workbook openxlsx2", { + skip_if(getRversion() < "4.1.0", "Skipping due to lifecycle warnings in R < 4.1.0") + skip_if_not_installed("openxlsx2") + testServer(downloadFile, + args = list(logger = periscope2:::fw_get_user_log(), + filenameroot = "excel_test_openxlsx2_wb", + datafxns = list(xlsx = create_openxlsx2_wb)), + expr = { + expect_true(grepl(pattern = "INFO:actions:File downloaded in browser: < excel_test_openxlsx2_wb.xlsx >", + x = capture_output(file.exists(output$xlsx)))) + }) +}) + +test_that("Testing workbook openxlsx", { + skip_if(getRversion() < "4.1.0", "Skipping due to lifecycle warnings in R < 4.1.0") + skip_if_not_installed("openxlsx") + testServer(downloadFile, + args = list(logger = periscope2:::fw_get_user_log(), + filenameroot = "excel_test_openxlsx_wb", + datafxns = list(xlsx = create_openxlsx_wb)), + expr = { + expect_true(grepl(pattern = "INFO:actions:File downloaded in browser: < excel_test_openxlsx_wb.xlsx >", + x = capture_output(file.exists(output$xlsx)))) + }) +}) + +test_that("Dataframe xlsx download works with openxlsx2", { + skip_if(getRversion() < "4.1.0", "Skipping due to lifecycle warnings in R < 4.1.0") + skip_if_not_installed("openxlsx2") + testServer(downloadFile, + args = list(logger = periscope2:::fw_get_user_log(), + filenameroot = "excel_test_dataframe", + datafxns = list(xlsx = download_data)), + expr = { + expect_true(grepl(pattern = "INFO:actions:File downloaded in browser: < excel_test_dataframe.xlsx >", + x = capture_output(file.exists(output$xlsx)))) + }) +}) + +test_that("Dataframe xlsx download works with openxlsx", { + skip_if(getRversion() < "4.1.0", "Skipping due to lifecycle warnings in R < 4.1.0") + skip_if_not_installed("openxlsx") + local_mocked_bindings(check_openxlsx2_availability = function() FALSE) + testServer(downloadFile, + args = list(logger = periscope2:::fw_get_user_log(), + filenameroot = "excel_test_dataframe", + datafxns = list(xlsx = download_data)), + expr = { + expect_true(grepl(pattern = "INFO:actions:File downloaded in browser: < excel_test_dataframe.xlsx >", + x = capture_output(file.exists(output$xlsx)))) + }) +}) + +test_that("Dataframe xlsx download works with writexl", { + skip_if(getRversion() < "4.1.0", "Skipping due to lifecycle warnings in R < 4.1.0") + local_mocked_bindings(check_openxlsx2_availability = function() FALSE) + local_mocked_bindings(check_openxlsx_availability = function() FALSE) + testServer(downloadFile, + args = list(logger = periscope2:::fw_get_user_log(), + filenameroot = "excel_test_dataframe", + datafxns = list(xlsx = download_data)), + expr = { + expect_true(grepl(pattern = "INFO:actions:File downloaded in browser: < excel_test_dataframe.xlsx >", + x = capture_output(file.exists(output$xlsx)))) + }) +}) diff --git a/tests/testthat/test_downloadable_plot.R b/tests/testthat/test_downloadable_plot.R index 80fdda4d..82202f72 100644 --- a/tests/testthat/test_downloadable_plot.R +++ b/tests/testthat/test_downloadable_plot.R @@ -105,9 +105,9 @@ test_that("downloadablePlotUI invalid btn_valign", { download_plot <- function() { ggplot2::ggplot(data = download_data(), aes(x = wt, y = mpg)) + geom_point(aes(color = cyl)) + - theme(legend.justification = c(1, 1), - legend.position = c(1, 1), - legend.title = element_blank()) + + theme(legend.justification = c(1, 1), + legend.position.inside = c(1, 1), + legend.title = element_blank()) + ggtitle("GGPlot Example w/Hover") + xlab("wt") + ylab("mpg") @@ -118,6 +118,7 @@ download_data <- function() { } test_that("downloadablePlot", { + skip_if(getRversion() < "4.1.0", "Skipping due to lifecycle warnings in R < 4.1.0") testServer(downloadablePlot, args = list(logger = periscope2:::fw_get_user_log(), filenameroot = "mydownload1", @@ -141,6 +142,7 @@ test_that("downloadablePlot", { test_that("downloadablePlot- default values", { + skip_if(getRversion() < "4.1.0", "Skipping due to lifecycle warnings in R < 4.1.0") testServer(downloadablePlot, args = list(visibleplot = download_plot), expr = { diff --git a/tests/testthat/test_downloadable_react_table.R b/tests/testthat/test_downloadable_react_table.R new file mode 100644 index 00000000..bfe2f217 --- /dev/null +++ b/tests/testthat/test_downloadable_react_table.R @@ -0,0 +1,433 @@ +context("periscope2 - downloadable react table") +local_edition(3) + +test_that("downloadableReactTableUI", { + table_ui <- downloadableReactTableUI(id = "myid", + downloadtypes = c("csv"), + hovertext = "myHoverText") + expect_equal(length(table_ui), 2) + expect_true(grepl('id="myid-reactTableButtonDiv"', table_ui[[1]], fixed = TRUE)) + expect_true(grepl('title="myHoverText"', table_ui[[1]], fixed = TRUE)) + expect_true(grepl('id="myid-reactTableButtonID-csv"', table_ui[[1]], fixed = TRUE)) + expect_false(grepl('style="display:none">', table_ui[[1]], fixed = TRUE)) + expect_true(grepl('id="myid-reactTableOutputID"', table_ui[[2]], fixed = TRUE)) +}) + + +test_that("downloadableReactTableUI - no download types", { + table_ui <- downloadableReactTableUI(id = "myid2") + expect_equal(length(table_ui), 2) + expect_true(grepl('id="myid2-reactTableButtonDiv"', table_ui[[1]], fixed = TRUE)) + expect_true(grepl('style="display:none">', table_ui[[1]], fixed = TRUE)) + expect_true(grepl('id="myid2-reactTableOutputID"', table_ui[[2]], fixed = TRUE)) +}) + + +# helper functions +get_mtcars_data <- reactive({ + head(mtcars) +}) + + +test_that("downloadableReactTable - valid data", { + testServer(downloadableReactTable, + args = list(table_data = get_mtcars_data), + expr = { + expect_true(grepl(paste0(names(get_mtcars_data()), collapse = "|"), output$reactTableOutputID)) + expect_false(grepl(paste0(rownames(get_mtcars_data()), collapse = "|"), output$reactTableOutputID)) + expect_true(grepl(paste0(get_mtcars_data()$mpg, collapse = "|"), output$reactTableOutputID)) + expect_true(grepl(paste0(get_mtcars_data()$cyl, collapse = "|"), output$reactTableOutputID)) + expect_true(grepl(paste0(get_mtcars_data()$disp, collapse = "|"), output$reactTableOutputID)) + expect_true(grepl(paste0(get_mtcars_data()$hp, collapse = "|"), output$reactTableOutputID)) + expect_true(grepl(paste0(get_mtcars_data()$drat, collapse = "|"), output$reactTableOutputID)) + expect_true(grepl(paste0(get_mtcars_data()$wt, collapse = "|"), output$reactTableOutputID)) + expect_true(grepl(paste0(get_mtcars_data()$qsec, collapse = "|"), output$reactTableOutputID)) + expect_true(grepl(paste0(get_mtcars_data()$vs, collapse = "|"), output$reactTableOutputID)) + expect_true(grepl(paste0(get_mtcars_data()$am, collapse = "|"), output$reactTableOutputID)) + expect_true(grepl(paste0(get_mtcars_data()$gear, collapse = "|"), output$reactTableOutputID)) + expect_true(grepl(paste0(get_mtcars_data()$carb, collapse = "|"), output$reactTableOutputID)) + }) +}) + + +test_that("downloadableReactTable - null or empty data.frame", { + no_table_data_error <- "'table_data' parameter must be a function or reactive expression." + server_error <- capture_output( + testServer(downloadableReactTable, + args = list(table_data = NULL), + expr = { + expect_true(grepl('"x":null', output$reactTableOutputID, fixed = TRUE)) + })) + expect_true(grepl(no_table_data_error, server_error)) + + server_error <- capture_output( + testServer(downloadableReactTable, + args = list(table_data = data.frame()), + expr = { + expect_true(grepl('"x":null', output$reactTableOutputID, fixed = TRUE)) + })) + expect_true(grepl(no_table_data_error, server_error)) + + server_error <- capture_output( + testServer(downloadableReactTable, + args = list(table_data = NA), + expr = { + expect_true(grepl('"x":null', output$reactTableOutputID, fixed = TRUE)) + })) + expect_true(grepl(no_table_data_error, server_error)) + + + + testServer(downloadableReactTable, + args = list(table_data = function() { NULL }), + expr = { + expect_true(grepl('"x":null', output$reactTableOutputID, fixed = TRUE)) + }) + + testServer(downloadableReactTable, + args = list(table_data = shiny::reactiveVal(NULL)), + expr = { + expect_true(grepl('"x":null', output$reactTableOutputID, fixed = TRUE)) + }) + + testServer(downloadableReactTable, + args = list(table_data = shiny::reactiveVal(NA)), + expr = { + expect_true(grepl('"x":null', output$reactTableOutputID, fixed = TRUE)) + }) + + testServer(downloadableReactTable, + args = list(table_data = function() { data.frame() }), + expr = { + expect_true(grepl('"x":null', output$reactTableOutputID, fixed = TRUE)) + }) + + testServer(downloadableReactTable, + args = list(table_data = shiny::reactiveVal(data.frame())), + expr = { + expect_true(grepl('"x":null', output$reactTableOutputID, fixed = TRUE)) + }) +}) + + +test_that("downloadableReactTable - single values", { + no_table_data_error <- "'table_data' parameter must be a function or reactive expression." + server_error <- capture_output( + testServer(downloadableReactTable, + args = list(table_data = 5), + expr = { + expect_true(grepl('"x":null', output$reactTableOutputID, fixed = TRUE)) + })) + expect_true(grepl(no_table_data_error, server_error)) + + testServer(downloadableReactTable, + args = list(table_data = function() {5}), + expr = { + expect_true(grepl('"table_data..":[5]', output$reactTableOutputID, fixed = TRUE)) + }) + + testServer(downloadableReactTable, + args = list(table_data = shiny::reactiveVal(5)), + expr = { + expect_true(grepl('"table_data..":[5]', output$reactTableOutputID, fixed = TRUE)) + }) +}) + + +test_that("downloadableReactTable - empty data.frame", { + no_table_data_error <- "'table_data' parameter must be a function or reactive expression." + server_error <- capture_output( + testServer(downloadableReactTable, + args = list(table_data = data.frame(5)), + expr = { + expect_true(grepl('"x":null', output$reactTableOutputID, fixed = TRUE)) + })) + expect_true(grepl(no_table_data_error, server_error)) + + + testServer(downloadableReactTable, + args = list(table_data = function() {data.frame(5)}), + expr = { + expect_true(grepl('data":{"X5":[5]}', output$reactTableOutputID, fixed = TRUE)) + }) + + testServer(downloadableReactTable, + args = list(table_data = shiny::reactiveVal(data.frame(5))), + expr = { + expect_true(grepl('data":{"X5":[5]}', output$reactTableOutputID, fixed = TRUE)) + }) +}) + + +test_that("downloadableReactTable - selection_mode", { + skip_if(getRversion() < "4.1.0", "Skipping due to lifecycle warnings in R < 4.1.0") + testServer(downloadableReactTable, + args = list(table_data = get_mtcars_data, + selection_mode = "Single"), + expr = { + expect_true(grepl('"id":".selection"', output$reactTableOutputID, fixed = TRUE)) + expect_true(grepl('"selection":"single"', output$reactTableOutputID, fixed = TRUE)) + }) + + testServer(downloadableReactTable, + args = list(table_data = get_mtcars_data, + selection_mode = "multiple"), + expr = { + expect_true(grepl('"id":".selection"', output$reactTableOutputID, fixed = TRUE)) + expect_true(grepl('"selection":"multiple"', output$reactTableOutputID, fixed = TRUE)) + }) + + testServer(downloadableReactTable, + args = list(table_data = get_mtcars_data, + selection_mode = "not_valid_mode"), + expr = { + expect_false(grepl('"id":".selection"', output$reactTableOutputID, fixed = TRUE)) + expect_false(grepl('"selection""', output$reactTableOutputID, fixed = TRUE)) + }) +}) + + +test_that("downloadableReactTable - pre_selected_rows", { + skip_if(getRversion() < "4.1.0", "Skipping due to lifecycle warnings in R < 4.1.0") + + testServer(downloadableReactTable, + args = list(table_data = get_mtcars_data, + pre_selected_rows = function() {c(1, 3)}), + expr = { + server_error <- capture_output(output$reactTableOutputID, print = TRUE) + error_msg <- "'selection_mode' parameter must be either 'single' or 'multiple' to use 'pre_selected_rows' param. Setting default value NULL" + expect_true(grepl(error_msg, server_error)) + }) + + server_error <- capture_output( + testServer(downloadableReactTable, + args = list(table_data = get_mtcars_data, + selection_mode = "multiple", + pre_selected_rows = c(1, 3)), + expr = { + output$reactTableOutputID + })) + expect_true(grepl("'pre_selected_rows' parameter must be a function or reactive expression. Setting default value NULL.", server_error)) + + server_error <- testServer(downloadableReactTable, + args = list(table_data = get_mtcars_data, + selection_mode = "multiple", + pre_selected_rows = function() {c(1, 3, "a")}), + expr = { + server_error <- capture_output(output$reactTableOutputID, print = TRUE) + error_msg <- "'pre_selected_rows' parameter must be a function or reactive expression that returns numeric vector. Setting default value NULL." + expect_true(grepl(error_msg, server_error)) + }) + + testServer(downloadableReactTable, + args = list(table_data = get_mtcars_data, + selection_mode = "single", + pre_selected_rows = function() {c(1, 3)}), + expr = { + server_error <- capture_output(output$reactTableOutputID, print = TRUE) + error_msg <- "when 'selection_mode' is 'single', only first value of 'pre_selected_rows' will be used" + expect_true(grepl(error_msg, server_error)) + expect_true(grepl('"defaultSelected":[0]', output$reactTableOutputID, fixed = TRUE)) + }) + + testServer(downloadableReactTable, + args = list(table_data = get_mtcars_data, + selection_mode = "single", + pre_selected_rows = function() {c(1)}), + expr = { + expect_true(grepl('"defaultSelected":[0]', output$reactTableOutputID, fixed = TRUE)) + }) + + testServer(downloadableReactTable, + args = list(table_data = get_mtcars_data, + selection_mode = "multiple", + pre_selected_rows = function() {c(1, 3)}), + expr = { + expect_true(grepl('"defaultSelected":[0,2]', output$reactTableOutputID, fixed = TRUE)) + }) + + testServer(downloadableReactTable, + args = list(table_data = get_mtcars_data, + selection_mode = "multiple", + pre_selected_rows = function() {c(1, 300)}), + expr = { + server_error <- capture_output(output$reactTableOutputID, print = TRUE) + error_msg <- "Excluding 'pre_selected_rows' value: 300 as it is out of range." + expect_true(grepl(error_msg, server_error)) + expect_true(grepl('"defaultSelected":[0]', output$reactTableOutputID, fixed = TRUE)) + }) + + testServer(downloadableReactTable, + args = list(table_data = get_mtcars_data, + selection_mode = "multiple", + pre_selected_rows = function() {c(-1, 2)}), + expr = { + server_error <- capture_output(output$reactTableOutputID, print = TRUE) + error_msg <- "Excluding 'pre_selected_rows' value: -1 as it is out of range." + expect_true(grepl(error_msg, server_error)) + expect_true(grepl('"defaultSelected":[1]', output$reactTableOutputID, fixed = TRUE)) + }) + + testServer(downloadableReactTable, + args = list(table_data = get_mtcars_data, + selection_mode = "multiple", + pre_selected_rows = function() {c(-1, 2, 4, 300)}), + expr = { + server_error <- capture_output(output$reactTableOutputID, print = TRUE) + error_msg <- "Excluding 'pre_selected_rows' values: -1, 300 as they are out of range." + expect_true(grepl(error_msg, server_error)) + expect_true(grepl('"defaultSelected":[1,3]', output$reactTableOutputID, fixed = TRUE)) + }) + + server_error <- capture_output( + testServer(downloadableReactTable, + args = list(table_data = get_mtcars_data, + selection_mode = "multiple", + pre_selected_rows = function() {c(-1, 300)}), + expr = { + expect_false(grepl('"defaultSelected"', output$reactTableOutputID, fixed = TRUE)) + }), print = TRUE) + error_msg <- "All 'pre_selected_rows' values are out of range. Setting default value NULL." + expect_true(grepl(error_msg, server_error)) + + testServer(downloadableReactTable, + args = list(table_data = get_mtcars_data, + selection_mode = "single", + pre_selected_rows = function() {c(-1, 300)}), + expr = { + server_error <- capture_output(output$reactTableOutputID, print = TRUE) + error_msg <- "when 'selection_mode' is 'single', only first value of 'pre_selected_rows' will be used" + expect_true(grepl(error_msg, server_error)) + expect_false(grepl('"defaultSelected"', output$reactTableOutputID, fixed = TRUE)) + }) +}) + + +test_that("downloadableReactTable - file_name_root and download_data_fxns", { + testServer(downloadableReactTable, + args = list(table_data = get_mtcars_data, + download_data_fxns = list(csv = get_mtcars_data), + file_name_root = "test"), + expr = { + expect_true(grepl(paste0(names(get_mtcars_data()), collapse = "|"), output$reactTableOutputID)) + }) + + testServer(downloadableReactTable, + args = list(table_data = get_mtcars_data, + download_data_fxns = list(csv = get_mtcars_data), + file_name_root = reactiveVal("test")), + expr = { + expect_true(grepl(paste0(names(get_mtcars_data()), collapse = "|"), output$reactTableOutputID)) + }) + + testServer(downloadableReactTable, + args = list(table_data = get_mtcars_data, + download_data_fxns = list(csv = get_mtcars_data), + file_name_root = function() { "test" }), + expr = { + expect_true(grepl(paste0(names(get_mtcars_data()), collapse = "|"), output$reactTableOutputID)) + }) + + server_warning <- capture_output(testServer(downloadableReactTable, + args = list(table_data = function() { "test" }, + download_data_fxns = list(csv = get_mtcars_data), + file_name_root = NULL), + expr = { + expect_true(grepl(paste0(names(get_mtcars_data()), collapse = "|"), output$reactTableOutputID)) + }), print = TRUE) + + warn_msg <- "file_name_root' parameter should not be NULL. Setting default value 'data_file'" + expect_true(grepl(warn_msg, server_warning)) +}) + + +test_that("downloadableReactTable - table_options", { + testServer(downloadableReactTable, + args = list(table_data = function() { "test" }, + table_options = list(showSortable = TRUE)), + expr = { + expect_true(grepl('"showSortable":true', output$reactTableOutputID)) + }) + + testServer(downloadableReactTable, + args = list(table_data = function() { "test" }, + table_options = list("unnamed_option")), + expr = { + server_warning <- capture_output(output$reactTableOutputID, print = TRUE) + warn_msg <- "Excluding the following unnamed option(s): unnamed_option" + expect_true(grepl(warn_msg, server_warning, fixed = TRUE)) + }) + + testServer(downloadableReactTable, + args = list(table_data = function() { "test" }, + table_options = list(not_valid = "option")), + expr = { + server_warning <- capture_output(output$reactTableOutputID, print = TRUE) + warn_msg <- "Excluding the following invalid option(s): not_valid" + expect_true(grepl(warn_msg, server_warning, fixed = TRUE)) + }) + + server_warning <- capture_output(testServer( + downloadableReactTable, + args = list(table_data = function() { "test" }, + table_options = list(invalid = "option", + showSortable = TRUE, + invalid_2 = "option2", + "unnamed_option")), + expr = { + expect_true(grepl('"showSortable":true', output$reactTableOutputID)) + }), print = TRUE) + + warn_msg1 <- "Excluding the following unnamed option(s): unnamed_option" + warn_msg2 <- "Excluding the following invalid option(s): invalid, invalid_2" + + expect_true(grepl(warn_msg1, server_warning, fixed = TRUE)) + expect_true(grepl(warn_msg2, server_warning, fixed = TRUE)) + +}) + + +test_that("downloadableReactTable - module return", { + skip_if(getRversion() < "4.1.0", "Skipping due to lifecycle warnings in R < 4.1.0") + local_mocked_bindings( + getReactableState = function(...) { + list(showSortable = TRUE, + defaultSelected = c(2, 3), + selected = c(2, 3)) + }, + .package = "reactable") + + testServer( + downloadableReactTable, + args = list(table_data = function() { "test" }), + expr = { + result <- session$returned() + expect_equal(length(result), 2) + expect_true(all(c("selected_rows", "table_state") %in% names(result))) + expect_true(is.null(result$selected_rows)) + }) + + local_mocked_bindings( + getReactableState = function(...) { + list(data = get_mtcars_data(), + showSortable = TRUE, + defaultSelected = c(2, 3), + selected = c(2, 3)) + }, + .package = "reactable") + + + testServer( + downloadableReactTable, + args = list(table_data = get_mtcars_data), + expr = { + result <- session$returned() + expect_equal(length(result), 2) + expect_true(all(c("selected_rows", "table_state") %in% names(result))) + expect_true(NROW(result$selected_rows) == 2) + expect_true(all(c("Mazda RX4 Wag", "Datsun 710") %in% rownames(result$selected_rows))) + }) + +}) + + diff --git a/tests/testthat/test_downloadable_table.R b/tests/testthat/test_downloadable_table.R index 9ea3cafe..d4174086 100644 --- a/tests/testthat/test_downloadable_table.R +++ b/tests/testthat/test_downloadable_table.R @@ -125,41 +125,57 @@ test_that("downloadableTable - invalid_selection", { test_that("downloadableTable - filenameroot", { - testServer(downloadableTable, - args = list(filenameroot = "test", - downloaddatafxns = list(csv = data), - tabledata = data), - expr = { - selected <- '"selection":{"mode":"multiple","selected":null,"target":"row","selectable":null}' - expect_true(grepl(selected, output$dtableOutputID, fixed = TRUE)) - }) + expect_message( + testServer(downloadableTable, + args = list(filenameroot = "test", + downloaddatafxns = list(csv = data), + tabledata = data), + expr = { + selected <- '"selection":{"mode":"multiple","selected":null,"target":"row","selectable":null}' + expect_true(grepl(selected, output$dtableOutputID, fixed = TRUE)) + }), + regexp = "Could not apply DT options due to.*selection.*argument is incorrect", + class = "message" + ) - testServer(downloadableTable, - args = list(filenameroot = reactiveVal("test"), - downloaddatafxns = list(csv = data), - tabledata = data), - expr = { - selected <- '"selection":{"mode":"multiple","selected":null,"target":"row","selectable":null}' - expect_true(grepl(selected, output$dtableOutputID, fixed = TRUE)) - }) + expect_message( + testServer(downloadableTable, + args = list(filenameroot = reactiveVal("test"), + downloaddatafxns = list(csv = data), + tabledata = data), + expr = { + selected <- '"selection":{"mode":"multiple","selected":null,"target":"row","selectable":null}' + expect_true(grepl(selected, output$dtableOutputID, fixed = TRUE)) + }), + regexp = "Could not apply DT options due to.*selection.*argument is incorrect", + class = "message" + ) - testServer(downloadableTable, - args = list(filenameroot = function(){"test"}, - downloaddatafxns = list(csv = data), - tabledata = data), - expr = { - selected <- '"selection":{"mode":"multiple","selected":null,"target":"row","selectable":null}' - expect_true(grepl(selected, output$dtableOutputID, fixed = TRUE)) - }) + expect_message( + testServer(downloadableTable, + args = list(filenameroot = function(){"test"}, + downloaddatafxns = list(csv = data), + tabledata = data), + expr = { + selected <- '"selection":{"mode":"multiple","selected":null,"target":"row","selectable":null}' + expect_true(grepl(selected, output$dtableOutputID, fixed = TRUE)) + }), + regexp = "Could not apply DT options due to.*selection.*argument is incorrect", + class = "message" + ) - testServer(downloadableTable, - args = list(filenameroot = NULL, - downloaddatafxns = list(csv = data), - tabledata = data), - expr = { - selected <- '"selection":{"mode":"multiple","selected":null,"target":"row","selectable":null}' - expect_true(grepl(selected, output$dtableOutputID, fixed = TRUE)) - }) + expect_message( + testServer(downloadableTable, + args = list(filenameroot = NULL, + downloaddatafxns = list(csv = data), + tabledata = data), + expr = { + selected <- '"selection":{"mode":"multiple","selected":null,"target":"row","selectable":null}' + expect_true(grepl(selected, output$dtableOutputID, fixed = TRUE)) + }), + regexp = "Could not apply DT options due to.*selection.*argument is incorrect", + class = "message" + ) }) test_that("downloadableTable - no downloads", { diff --git a/tests/testthat/test_log_viewer.R b/tests/testthat/test_log_viewer.R index 86036c61..66e8c4cd 100644 --- a/tests/testthat/test_log_viewer.R +++ b/tests/testthat/test_log_viewer.R @@ -7,11 +7,7 @@ sample_log <- function(){ "actions [2022-02-19 14:04:32] Application Reset requested by user. Resetting in 5 seconds") } -null_log <- function(){ - NULL -} - -empty_log <- function(){ +null_log <- function() { NULL } @@ -26,24 +22,15 @@ test_that("logViewer - valid sample log", { testServer(logViewer, args = list(id = "myid", logger = sample_log), expr = { - expect_snapshot_output(output$myid) + expect_snapshot_output(output$"myid-reactTableOutputID") }) }) -test_that("logViewer - null sample log", { +test_that("logViewer - null log", { testServer(logViewer, args = list(id = "nullLogger", logger = null_log), expr = { - expect_null(output$nullLogger) - }) -}) - - -test_that("logViewer - empty sample log", { - testServer(logViewer, - args = list(id = "emptyLogger", logger = empty_log), - expr = { - expect_null(output$emptyLogger) + expect_true(grepl('"x":null', output$"nullLogger-reactTableOutputID", fixed = TRUE)) }) }) diff --git a/tests/testthat/test_logger.R b/tests/testthat/test_logger.R index c4821953..c974e778 100644 --- a/tests/testthat/test_logger.R +++ b/tests/testthat/test_logger.R @@ -1,10 +1,12 @@ context("periscope2 - logging functionality") -writeToConsole <- periscope2:::writeToConsole -writeToFile <- periscope2:::writeToFile -loglevels <- periscope2:::loglevels -test_file_name <- file.path(tempdir(), c("1", "2", "3")) +writeToConsole <- periscope2:::writeToConsole +writeToFile <- periscope2:::writeToFile +loglevels <- periscope2:::loglevels +set_app_parameters <- periscope2:::set_app_parameters +test_file_name <- file.path(tempdir(), c("1", "2", "3")) +test_file_log <- file.path(tempdir(), "test_log.txt") env_setup <- function() { test_env <- new.env(parent = emptyenv()) @@ -110,8 +112,8 @@ test_that("UpdateOptions - updateOptions()", { periscope2:::logReset() periscope2:::basicConfig() - periscope2:::updateOptions.character("", level = "WARN") - expect_equal(periscope2:::getLogger()$getLevel(), loglevels["WARN"]) + periscope2:::updateOptions.character("", level = "WARNING") + expect_equal(periscope2:::getLogger()$getLevel(), loglevels["WARNING"]) }) test_that("LoggingToConsole", { @@ -122,8 +124,10 @@ test_that("LoggingToConsole", { periscope2:::addHandler(writeToConsole, level = "DEBUG") expect_equal(with(periscope2:::getLogger(), names(handlers)), c("basic.stdout", "writeToConsole")) - logdebug("log generated for testing") - loginfo("log generated for testing") + expect_true(grepl(pattern = "DEBUG::log generated for testing", + x = capture_output(logdebug("log generated for testing"), print = TRUE))) + expect_true(grepl(pattern = "INFO::log generated for testing", + x = capture_output(loginfo("log generated for testing"), print = TRUE))) succeed() }) @@ -207,3 +211,108 @@ test_that("MsgComposer function - defaultMsgCompose()",{ expect_equal(periscope2:::defaultMsgCompose(msg = paste(rep(LETTERS, 316), collapse = "")), paste(rep(LETTERS, 316), collapse = "")) }) + +# Testing logging levels +test_that("DEBUG level shows all messages", { + set_app_parameters(log_level = "DEBUG") + periscope2:::addHandler(writeToConsole) + expect_output(logdebug("debug message"), "DEBUG::debug message") + expect_output(loginfo("info message"), "INFO::info message") + expect_output(logwarn("warn message"), "WARNING::warn message") + expect_output(logerror("error message"), "ERROR::error message") +}) +I +test_that("INFO level filters DEBUG", { + set_app_parameters(log_level = "INFO") + periscope2:::addHandler(writeToConsole) + expect_silent(logdebug("debug message")) + expect_output(loginfo("info message"), "INFO::info message") + expect_output(logwarn("warn message"), "WARNING::warn message") + expect_output(logerror("error message"), "ERROR::error message") +}) + +test_that("WARNING level filters DEBUG and INFO", { + set_app_parameters(log_level = "WARNING") + periscope2:::addHandler(writeToConsole) + expect_silent(logdebug("debug message")) + expect_silent(loginfo("info message")) + expect_output(logwarn("warn message"), "WARNING::warn message") + expect_output(logerror("error message"), "ERROR::error message") +}) + +test_that("ERROR level filters all but ERROR", { + set_app_parameters(log_level = "ERROR") + periscope2:::addHandler(writeToConsole) + expect_silent(logdebug("debug message")) + expect_silent(loginfo("info message")) + expect_silent(logwarn("warn message")) + expect_output(logerror("error message"), "ERROR::error message") +}) + +test_that("File logging level DEBUG", { + unlink(test_file_log, force = TRUE) + set_app_parameters(log_level = "DEBUG") + periscope2:::addHandler(writeToFile, file = test_file_log) + + expect_output(logdebug("debug message"), "debug message") + expect_output(loginfo("info message"), "info message") + expect_output(logwarn("warn message"), "warn message") + expect_output(logerror("error message"), "error message") + + log_content <- readLines(test_file_log) + expect_true(any(grepl("debug message", log_content))) + expect_true(any(grepl("info message", log_content))) + expect_true(any(grepl("warn message", log_content))) + expect_true(any(grepl("error message", log_content))) +}) + +test_that("File logging level INFO", { + unlink(test_file_log, force = TRUE) + set_app_parameters(log_level = "INFO") + periscope2:::addHandler(writeToFile, file = test_file_log) + + logdebug("debug message") + expect_output(loginfo("info message"), "info message") + expect_output(logwarn("warn message"), "warn message") + expect_output(logerror("error message"), "error message") + + log_content <- readLines(test_file_log) + expect_false(any(grepl("debug message", log_content))) + expect_true(any(grepl("info message", log_content))) + expect_true(any(grepl("warn message", log_content))) + expect_true(any(grepl("error message", log_content))) +}) + +test_that("File logging level WARNING", { + unlink(test_file_log, force = TRUE) + set_app_parameters(log_level = "WARNING") + periscope2:::addHandler(writeToFile, file = test_file_log) + + logdebug("debug message") + loginfo("info message") + expect_output(logwarn("warn message"), "warn message") + expect_output(logerror("error message"), "error message") + + log_content <- readLines(test_file_log) + expect_false(any(grepl("debug message", log_content))) + expect_false(any(grepl("info message", log_content))) + expect_true(any(grepl("warn message", log_content))) + expect_true(any(grepl("error message", log_content))) +}) + +test_that("File logging level ERROR", { + unlink(test_file_log, force = TRUE) + set_app_parameters(log_level = "ERROR") + periscope2:::addHandler(writeToFile, file = test_file_log) + + logdebug("debug message") + loginfo("info message") + logwarn("warn message") + expect_output(logerror("error message"), "error message") + + log_content <- readLines(test_file_log) + expect_false(any(grepl("debug message", log_content))) + expect_false(any(grepl("info message", log_content))) + expect_false(any(grepl("warn message", log_content))) + expect_true(any(grepl("error message", log_content))) +}) diff --git a/tests/testthat/test_ui_functions.R b/tests/testthat/test_ui_functions.R index c06bbe67..34d53151 100644 --- a/tests/testthat/test_ui_functions.R +++ b/tests/testthat/test_ui_functions.R @@ -1,9 +1,11 @@ context("periscope2 - UI functionality") local_edition(3) +loglevels <- periscope2:::loglevels +set_app_parameters <- periscope2:::set_app_parameters # helper functions create_announcements <- function(start_date = NULL, - end_data = NULL, + end_date = NULL, start_date_format = NULL, end_date_format = NULL, style = NULL, @@ -13,7 +15,7 @@ create_announcements <- function(start_date = NULL, appTemp <- tempfile(pattern = "TestThatApp", tmpdir = appTemp_dir) announcements_file <- paste0(gsub('\\\\|/', '', (gsub(appTemp_dir, "", appTemp, fixed = TRUE))), ".yaml") yaml::write_yaml(list("start_date" = start_date, - "end_date" = end_data, + "end_date" = end_date, "start_date_format" = start_date_format, "end_date_format" = end_date_format, "style" = style, @@ -34,8 +36,10 @@ test_that("add_ui_header - no header", { }) test_that("set_app_parameters default values", { + reset_g_opts() expect_equal(shiny::isolate(periscope2:::.g_opts$app_title), "Set using add_ui_header() in program/ui_header.R") expect_null(shiny::isolate(periscope2:::.g_opts$app_info), NULL) + expect_equal(shiny::isolate(periscope2:::.g_opts$loglevel), "DEBUG") expect_equal(shiny::isolate(periscope2:::.g_opts$app_version), "1.0.0") expect_null(shiny::isolate(periscope2:::.g_opts$loading_indicator)) @@ -100,7 +104,7 @@ test_that("add_ui_header - ui element", { # busy indicator - title - UI elements (center as well) expect_warning(periscope2::add_ui_header(ui_elements = menu, - ui_position = "center"), + ui_position = "center"), regexp = "title_position cannot be equal to ui_position") header <- shiny::isolate(periscope2:::.g_opts$header) @@ -140,8 +144,8 @@ test_that("add_ui_header - ui element", { # busy indicator - title positions is NULL- UI elements position is center warn_msgs <- capture_warnings(periscope2::add_ui_header(ui_elements = menu, - ui_position = "center", - title_position = NULL)) + ui_position = "center", + title_position = NULL)) expect_equal("title_position must be on of 'left', 'center'or 'right' values. Setting default value 'center'", warn_msgs[1]) expect_equal("title_position cannot be equal to ui_position. Setting default values", @@ -406,7 +410,8 @@ test_that("load_announcements empty file", { announcements_file <- paste0(gsub('\\\\|/', '', (gsub(appTemp_dir, "", appTemp, fixed = TRUE))), ".yaml") yaml::write_yaml("", announcements_file) - expect_null(load_announcements(announcements_file_path = announcements_file)) + output_msg <- capture_output(expect_null(load_announcements(announcements_file_path = announcements_file))) + expect_true(grepl('Announcements will be ignored', output_msg, fixed = TRUE)) unlink(announcements_file, TRUE) }) @@ -418,33 +423,56 @@ test_that("load_announcements - parsing error", { cat(":", file = (con <- file(announcements_file, "w", encoding = "UTF-8"))) close(con) - expect_warning(load_announcements(announcements_file_path = announcements_file), - regexp = "[(Could not parse TestThatApp)]") + output_msg <- capture_output(expect_warning(load_announcements(announcements_file_path = announcements_file), + regexp = "[(Could not parse TestThatApp)]")) + expect_true(grepl('Parser error', output_msg, fixed = TRUE)) unlink(announcements_file, TRUE) }) test_that("load_announcements function parameters", { expect_null(create_announcements(start_date = "2222-11-26", - end_data = "2222-12-26")) - expect_null(create_announcements(start_date = "2022-11-26", - end_data = "2222-12-26", - style = "not-style")) - expect_null(create_announcements(start_date = "11-26-2222", - end_data = "12-26-2222", - start_date_format = "%m-%d-%Y", - end_date_format = "%m-%d-%Y")) - expect_null(create_announcements(start_date = "11-26-2222", - end_data = "12-26-2222", - end_date_format = "%m-%d-%Y")) - expect_null(create_announcements(start_date = "11-26-2222", - end_data = "12-26-2222", - start_date_format = "%m-%d-%Y")) + end_date = "2222-12-26")) + output_msg <- capture_output(expect_null( + create_announcements(start_date = "2022-11-26", + end_date = "2222-12-26", + style = "not-style"))) + expect_true(grepl("Announcement 'style' must be one of info, danger, success, warning, primary", + output_msg, fixed = TRUE)) + expect_null( + create_announcements(start_date = "11-26-2222", + end_date = "12-26-2222", + start_date_format = "%m-%d-%Y", + end_date_format = "%m-%d-%Y")) + + output_msg <- capture_output(expect_null( + create_announcements(start_date = "11-26-2222", + end_date = "12-26-2222", + end_date_format = "%m-%d-%Y"))) + expect_true(grepl("All formats failed to parse. No formats found", + output_msg, fixed = TRUE)) + + output_msg <- capture_output(expect_null( + create_announcements(start_date = "11-26-2222", + end_date = "12-26-2222", + start_date_format = "%m-%d-%Y"))) + expect_true(grepl("All formats failed to parse. No formats found", + output_msg, fixed = TRUE)) + expect_null(create_announcements(start_date = "11-26-2222", start_date_format = "%m-%d-%Y")) - expect_null(create_announcements(style = "info")) - expect_null(create_announcements(style = "info", - text = "text", - auto_close = "abc")) + expect_true(grepl("All formats failed to parse. No formats found", + output_msg, fixed = TRUE)) + + output_msg <- capture_output(expect_null(create_announcements(style = "info"))) + expect_true(grepl("Announcement 'text' value is empty. It must contain non empty text value", + output_msg, fixed = TRUE)) + + output_msg <- capture_output(expect_null( + create_announcements(style = "info", + text = "text", + auto_close = "abc"))) + expect_true(grepl("Announcement 'auto_close' value ' abc ' is invalid", + output_msg, fixed = TRUE)) }) test_that("load_theme_settings - null settings", { @@ -504,20 +532,6 @@ test_that("theme - invalid color", { }) -# test_that("theme - invalid width", { -# theme_settings <- yaml::read_yaml(system.file("fw_templ", "p_example", "periscope_style.yaml", package = "periscope2")) -# dir.create("www") -# theme_settings[["sidebar_width"]] <- "300" -# theme_settings[["control_sidebar_width"]] <- "-300" -# -# yaml::write_yaml(theme_settings, "www/periscope_style.yaml") -# expect_warning(nchar(create_theme()), -# regexp = "invalid theme settings -300 must be positive value. Setting default value") -# unlink("www/periscope_style.yaml") -# unlink("www", recursive = TRUE) -# }) - - test_that("dashboard - create default dashboard", { expect_snapshot(periscope2:::create_application_dashboard()) }) diff --git a/vignettes/announcement-module.Rmd b/vignettes/announcement-module.Rmd index fdc59c16..5462f25a 100644 --- a/vignettes/announcement-module.Rmd +++ b/vignettes/announcement-module.Rmd @@ -96,3 +96,4 @@ runApp(paste(app_dir, 'mysampleapp', sep = .Platform$file.sep)) * [logViewer Module](logViewer-module.html) * [applicationReset Module](applicationReset-module.html) * [Theme Configuration Builder](themeBuilder_addin.html) +* [downloadableReactTable Module](downloadableReactTable-module.html) diff --git a/vignettes/announcement_addin.Rmd b/vignettes/announcement_addin.Rmd index b483bb9d..00fd9067 100644 --- a/vignettes/announcement_addin.Rmd +++ b/vignettes/announcement_addin.Rmd @@ -144,3 +144,4 @@ For detailed usage for that file refer to [announcement Module](announcement-mod * [logViewer Module](logViewer-module.html) * [applicationReset Module](applicationReset-module.html) * [Theme Configuration Builder](themeBuilder_addin.html) +* [downloadableReactTable Module](downloadableReactTable-module.html) diff --git a/vignettes/applicationReset-module.Rmd b/vignettes/applicationReset-module.Rmd index 41973313..4d26d750 100644 --- a/vignettes/applicationReset-module.Rmd +++ b/vignettes/applicationReset-module.Rmd @@ -117,3 +117,4 @@ runApp(paste(app_dir, 'mysampleapp', sep = .Platform$file.sep)) * [announcement Module](announcement-module.html) * [Announcement Configuration Builder](announcement_addin.html) * [Theme Configuration Builder](themeBuilder_addin.html) +* [downloadableReactTable Module](downloadableReactTable-module.html) diff --git a/vignettes/downloadFile-module.Rmd b/vignettes/downloadFile-module.Rmd index bf6f5e79..81d32475 100644 --- a/vignettes/downloadFile-module.Rmd +++ b/vignettes/downloadFile-module.Rmd @@ -90,6 +90,9 @@ downloadFileButton(id = "object_id2", The **downloadFile** function is called directly. The call consists of the following: +**Note on Excel file support:** For xlsx files, the module will first attempt to use `openxlsx2` if available, then fall back to `openxlsx`, and finally use `writexl` if neither is available. We recommend `openxlsx2` for best performance with Excel files. + + * the unique object ID that was provided to downloadFileButton when creating the UI object * the logging logger to be used @@ -154,3 +157,4 @@ runApp(paste(app_dir, 'mysampleapp', sep = .Platform$file.sep)) * [announcement Module](announcement-module.html) * [Announcement Configuration Builder](announcement_addin.html) * [Theme Configuration Builder](themeBuilder_addin.html) +* [downloadableReactTable Module](downloadableReactTable-module.html) diff --git a/vignettes/downloadablePlot-module.Rmd b/vignettes/downloadablePlot-module.Rmd index af4799b1..5652b334 100644 --- a/vignettes/downloadablePlot-module.Rmd +++ b/vignettes/downloadablePlot-module.Rmd @@ -151,3 +151,4 @@ downloadablePlot(id = "object_id1", * [announcement Module](announcement-module.html) * [Announcement Configuration Builder](announcement_addin.html) * [Theme Configuration Builder](themeBuilder_addin.html) +* [downloadableReactTable Module](downloadableReactTable-module.html) diff --git a/vignettes/downloadableReactTable-module.Rmd b/vignettes/downloadableReactTable-module.Rmd new file mode 100644 index 00000000..e68a38cd --- /dev/null +++ b/vignettes/downloadableReactTable-module.Rmd @@ -0,0 +1,165 @@ +--- +title: "Using downloadableReactTable Shiny Module" +author: "Mohammed Ali" +date: "`r Sys.Date()`" +output: + rmarkdown::html_vignette: + toc: TRUE + toc_depth: 3 +vignette: > + %\VignetteIndexEntry{Using downloadableReactTable Shiny Module} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +# Overview + +## Purpose +The document explains how to use **downloadableReactTable** shiny module in periscope2 applications. + +## Features +* Ability to display and download datasets. +* Table rows selection can be multiple, single or none (the default) + * When selection mode is enabled an additional column will be added to the table + * The selection controls will be radio buttons for single row selection and checkboxes for "multiple" rows selection mode +* Returns a reactive expression containing named list with two elements: **selected_rows** and **table_state** +* Supports full-table searching including regular expressions +* Columns are sort-able in both directions +* Configurable table "window" (viewing area) height with infinite vertical +scrolling (no paging by default) +* Supports rownames +* Requires minimal code (see the Usage section for details) +* Uses **downloadFile** Shiny Module functionality to ensure consistent +download functionality for table data. +* **downloadFile** button will be hidden if `downloadableReactTable` parameter `download_data_fxns` or +`downloadableReactTableUI` parameter `downloadtypes` is empty + +# Usage + +## Shiny Module Overview +Shiny modules consist of a pair of functions that modularize, or package, a small piece of reusable functionality. The UI function is called directly by the user to place the UI in the correct location (as with other shiny UI objects). The module server function that is called only once to set it up using the module name as a function inside the server function (i.e. user-local session scope. The first function argument is a string that represents the module id (the same id used in module UI function). Additional arguments can be supplied by the user based on the specific shiny module that is called. There can be additional helper functions that are a part of a shiny module. + +The **downloadableReactTable** Shiny Module is a part of the *periscope2* package and +consists of the following functions: + +* **downloadableReactTableUI** - the UI function to place the table in the application +* **downloadableReactTable** - the server function to be called inside server_local.R. + +## downloadableReactTableUI + +The **downloadableReactTableUI** function is called from the ui.R (or equivalent) +file in the location where the table should be placed. This is similar to other +UI element placement in shiny. + +The downloadableReactTableUI looks like: + + + +The downloadableReactTableUI function takes the unique object ID for the UI object. + +The next two arguments (downloadtypes and hovertext) are passed to the +downloadFileButton and set the file types the button will allow the user to +request and the downloadFileButton's tooltip text. + + +```{r, eval=F} +# Inside ui_body.R or ui_sidebar.R + + downloadableReactTableUI( + id = "object_id1", + downloadtypes = c("csv", "tsv"), + hovertext = "Download the data here!") +``` + +## downloadableReactTable +The **downloadableReactTable** function is called directly. The call consists of the following: + +* the unique object ID that was provided to downloadableTableUI when creating the UI object +* the logging logger to be used +* **file_name_root** is an optional character, function or reactive expression providing downloadable file name +* the root (prefix) of the downloaded file name to be used in the browser as a character string or a reactive expression that returns a character string +* **download_data_fxns** named list of functions or reactive expressions that provide the data to the downloadFileButton (see below). + * It is important that the types of files to be downloaded are matched to the correct data function in the list. + * The function/reactive expression names are unquoted - they will be called at the time the user initiates a download *(see requirements below)*. + * The download functions or reactive expressions can all be the same or different from each other and/or the one provided to **table_data**. This allows finer control over what the user can view vs. download if desired. For example you can allow a user to view a smaller subset of data but download an expanded dataset, or perhaps download a redacted version of data, etc. +* This module also supports most of reactable table options for further customization. See the example below. + +**Data Function or Reactive Expression Requirements** + +* If a function is provided it must be parameter-less (require NO parameters). No parameters will be provided when a function is called to retrieve the plot or data. Reactive expressions cannot take parameters by definition. +* The function or reactive expression must return an appropriate data format for the file type. +* For instance: csv/tsv/xlsx types require data that is convertible to a tabular type, +to various download types see the downloadFile module help or vignette. +* For the visible table data the return value must be able to be converted to tabular format +* Since the function or reactive expression is called at the time the user requests the data it is +recommended that reactive expressions are used to provide dynamic values from the application to create the table. + +**Reactive Return Value** + +* The server function returns a reactive expression containing named list with two elements: + * **selected_rows**: data.frame of current selected rows. + ``` + * Note that this is the data, not references, rownumbers, etc from the table -- it is the actual, visible, table row data. This allows the developer to use this more easily to update another table, chart, etc. as desired. + ``` + * **table_state**: a list of the current table state. The list keys are ("page", "pageSize", "pages", "sorted" and "selected") + + +* It is acceptable to ignore the return value as well if this functionality is not needed. Simply do not assign the result to a variable. + +**Customization Options** + +*downloadableReactTable* module can be customized using reactable function arguments(see `?reactable::reactable`). These options can be sent as a named options via the server function, see example below. + +```{r, eval = F} +# Inside server_local.R + library(shiny) + library(periscope2) + library(reactable) + +table_state <- downloadableReactTable( + id = "object_id1", + table_data = reactiveVal(iris), + download_data_fxns = list(csv = reactiveVal(iris), tsv = reactiveVal(iris)), + selection_mode = "multiple", + pre_selected_rows = function() {c(1, 3, 5)}, + table_options = list(columns = list( + Sepal.Length = colDef(name = "Sepal Length"), + Sepal.Width = colDef(filterable = TRUE), + Petal.Length = colDef(show = FALSE), + Petal.Width = colDef(defaultSortOrder = "desc")), + showSortable = TRUE, + theme = reactableTheme( + borderColor = "#dfe2e5", + stripedColor = "#f6f8fa", + highlightColor = "#f0f5f9", + cellPadding = "8px 12px"))) + + observeEvent(table_state(), { print(table_state()) }) + +# NOTE: table_state is the reactive return value, captured for later use +``` + +## Sample Application +For a complete running shiny example application using the downloadableReactTable module you can create and run a *periscope2* sample application using: + +```{r, eval=F} +library(periscope2) + +app_dir = tempdir() +create_new_application(name = 'mysampleapp', location = app_dir, sample_app = TRUE) +runApp(paste(app_dir, 'mysampleapp', sep = .Platform$file.sep)) +``` + +# Additional Resources + + +**Vignettes** + +* [New Application](new-application.html) +* [downloadFile Module](downloadFile-module.html) +* [downloadablePlot Module](downloadablePlot-module.html) +* [logViewer Module](logViewer-module.html) +* [applicationReset Module](applicationReset-module.html) +* [announcement Module](announcement-module.html) +* [Announcement Configuration Builder](announcement_addin.html) +* [Theme Configuration Builder](themeBuilder_addin.html) diff --git a/vignettes/downloadableTable-module.Rmd b/vignettes/downloadableTable-module.Rmd index 094a0233..67e791e7 100644 --- a/vignettes/downloadableTable-module.Rmd +++ b/vignettes/downloadableTable-module.Rmd @@ -201,3 +201,4 @@ runApp(paste(app_dir, 'mysampleapp', sep = .Platform$file.sep)) * [announcement Module](announcement-module.html) * [Announcement Configuration Builder](announcement_addin.html) * [Theme Configuration Builder](themeBuilder_addin.html) +* [downloadableReactTable Module](downloadableReactTable-module.html) diff --git a/vignettes/figures/downloadableReactTable-1.png b/vignettes/figures/downloadableReactTable-1.png new file mode 100644 index 0000000000000000000000000000000000000000..3d2266dc3bcc2216b2e5724f0ce5e13b37c2b882 GIT binary patch literal 25216 zcmb?@1yo#3w`D^J?gY000fM_j1HpqOxJ%>i?n!V95Zr f4Kl%4s=qpQm>80M;5 z#Yh+4uWy}_f`8U+`Z4)afrbbEn?P)W*WGY_euIDxEL^nSfa~+qq@J(0-Yq*{$V`{d zg;t!pw;pcKcd($Zx^5Ew*r>TfvB$eh2NxGEZYM(tadGiZr1za6(3i4O$jh(tb1|tX zDJd0XC?uf=(_%kFK#%cK2NFP!IebKBgC1M|@dosAA|2g7I5F{jXC#vcj{#Jr)z{ZY zAuOEvk=aq+(ZQi@U?4mQjTpq995Dp#)G?CR`5kG+T=3oH!N$JlTCMD_4-r$ATeD!J zn>+5#iGjj)T)y!2brAB|dEd}bmP9xnGCTCVnco9}Nhl1sy?uRcGc^=wjZF+Xp&Zch z{R*hzX>LY?UX+bH96E-$-zt=SBO^T%0|OeamKl0_n>#zi124mQM~@2{&J>1RP_asu zl$0n4)kukp=av-e_CZG}zK{A3#;#Hza7D&z@89|}q1$l_m(=9aNwMPXmYEUCjl|ht zJKe< 9u`)NRqNLAtTq+J_GCXTVZ+ILo(RwHFvX;{=lW&A(fGxn z7PEIFje6&zk}vbHIqy=3>16neC=KXCwA(f+jP|Zv(Rj{il&Z6tKg0EPYV!FfRQ)t= zW;*D2l)ywtY>AWmWFY;f=!Ck|Tl*y>qe7?!qyzuZcD{k$Tct3ob1$i!_w#S?<%i<= z33Ohst0M4=Gvt1l!Uie$+|i>b4Lf%HHYYn=nw({Xc6wH*DjVXE_G%(W)yL>uH9Xoc zI!o;M_X!sHa|XBp+W901dgh;j-7zBqA=3!(;4T~V!(Ds*%OO1lT+r{gz3*FJzQ`-! zgX|I6ky)X=x~Nwz9v<)BV{n` M|5p5V#WUHA^MF^4SUIuit7%Ms(^Fa$-Y$ z+3~2JMiRt)@tQ^TO41#ARZ867378v4M`(uzQws;PjV9->qc81BjS)s$8v-0YLmfTl zV?n}%%!{f1HI1@#EoD+k98T| Vv-9(UP0E>f z`1ttHPRwV^C}`sP{?6x2$iNb~X8gsun3;HZ62E+b%_j%r)nP*yfV-#X`oV#f_1)CV zI=2+DwPoHJPR}vy2&%1n3|g^fQ0fQ^3xh7WEWIXARWjp$jk_0@;r;toYi@>C>GX~5 zZOM)xG`jciS-DjOpw@_?|D>ZM2i bS;b2Zg%ElMj(< z5`Vry_bF+4x@vp(1IJ~LYtol@f}K#ruWdU=EgCoc!Y4c22FFM=%3)FHm~$l`A_09u*55$%UX2!+<6d-zs^ z!nkiI30Z9c*^Yv*%tJ%rT&iYk-JR+DileX*LQM~c6Ddn$+J{7h<945cXRt}j>%k+! zi&Zgkopp7rf?ktn_>%T4s=cHk;AE*jx%v!BkM4_mN>3e(2QN1h7J1nK2b2{ZhiB2# zbe`#1e6*w?NF2!=J9Ni(P9bhj6;W}3Hz!+W3#;At9uIqaa(>R3Krdm?-m@>WZgcAy zhnISzgi_N&^2LRU@5(PS !Y|Y;uc=cz^N1;RRy(czPoyx8D@5!9 z{VZ3nNz%FA>%C#Zn`S}yRT+q|B*$6873V3J5qj89H-uhZ_u55MU)$MKnd?BT-37!7 z+T5b0);`l$$rnATNEgtb)hEYv*mdjN?L)*M4siqo<8T#rUO`uTa6RbCzQvR?G%rq? zGX4{~`OBLO7nw^guMV$`EQ6*84tn3iN0HFv%*JK9&6ntu3&r-Xnkmc&f&85w8lPpS znY_{yO^4Q>uV85v=A|sHiF9YkM#iC@<^6`p_$VJ^L(nnd)wWEX+Ho`Y32DZYI#`$?72g8YxOZR%!kAr7La{)K^I5xfN)whO9VExb%fR#t7cN+C6sd+K*}{Ae2xXYJqCflYHmn^)$I&Qt=^{d zdwhY&FD`G9==(ONHR0WV=-TP!-RAA8_HXB?&KypE_@b(>XM0zioJ!fST4%;*=VPL8 z0S;{H^Wt()w|reg$YDCjiYFny*{{KfH)wiaHHv!znIt@qVbN$l! 9zG{nQVsaPFjQ$6BK%KzA+@u^CK_M#R bMw&!EzMPwiJsNM3m!otqBsabT-bt%;~N9<9^`e74i+Iq1w?=p2xc{F5L zcYb2DK69kTWV~^vxbnK1<$Lmnb+^$fKY}6I8A?Zw#CzgeBECA0BsrSJ#YKDQw#t0~ zgc?XSYy7(RJutXS2~d)eG=$Gb>s$gDNOqdEGH70{9ODZQgjH7@L}M;L?D}Cn(-ARe zZ2ji;lKzp${<0d{|If0hfc@3>d7u*6J{lmRSEFPW-R= zm^7}vi_6yGTt#=ebh`c3;T*D^r23MjZ#X4o)%W~-TD))xQOW(Z>}*O#Jd})#jP5+k z{8Ca(tOClqy1F-S-h9%reyF#MnID*%!c|mM^!D+Ily8~eYIYTQ2?ydOGKnZ`_-fEk z;%WZ>$@EVt7#9~;buK6GpQMI=2VeL5;=-BRc2U~XvmU(1v-9uUYRDv7Vm+tlOo{G0 z8kz(kWTiz|x89j u+9c2SY3P{ z&Pt7lVSQsR`8C`(cGrtf{0K=AOMgW8!y$yNX}Thbeb%=SUM->qmG*553c)f|d*U{2 zi;56VSIh#+?$9RL1V##4+I*d2N@%DzUaWQ4vP_v}pj7HQ<5r+pPr#B(&ulF>L1Ii5 zPB*N%Fm7#Bu{NTX5nQJDR!y4s$(LwjOQli=^HtA9zK;QBQ_y9BxWA0q=?Uh%D?!_O zhsl(lo3Pw<>)ZL2h0NoqC5b;{QKSO&xCQ`6M@ML`9SqjVf!ZtAbwF{`E>11xHJrHM zU?CVhNP@56pPKx=%ryy6Uf#>jyz{&F<8lGA0-COIT>=$Iv&BLP5@}^+-R{}{rD_pj z#X8EA7g)o#L@*?kI$hx_2?y-%MxHTp1N-0>1UmD}h+Ecl$P!HZF*hKA#{Fi_jr*9G zi_}4XjYJaMpVnuA7B_)c7VzG;o3CBIOMhVD1~=C}X+-$wvys9$COTsB-9Q0l2Lv zfh0-9Fs;odV}iU4F*#}g9Hx8WZiYCy!TSyC>Mgz!sw6XP9D*d?FF-a6Ou=L(hQs{a zFvde2_DEXL!1RQ#H6yuvbZh=DV1O*O)hTqZdW>2b^Hq+KyMxYqpsGMSOI5_F^ElK- zy#owxpVWGLdV Jbx7KImd=bt0E&(_M*K_zrDsV09j+-pMhT!0IaGhLVobd_W$9n zCglxRG{9tflrxauiE9CNj}OW+sRv9Fbj!(Z?(+G0qLH?>GGFqg#7ZuEFvPo;mQ|E_ z>|;uH&%#DP%Li{cHMya$Corzv;%r=Oq*fL7$0Spk!+FlGzL^NB_ZgHfBGF_*yECOL zCgk0KbL~$dSTXZMg~BYu 2J&W04@jtpXgMRf)Q5ww?z-#^);@XdWHnxPkfA?d zioG*EPHPT}@1x7L)f!1)0TFJ!{@y>Idnn7O9*|jKlb@b~wly|ap>eWeJkH&PIp;yb zfI~Y$cu2+S#d{(KyP-{}4j_37@nEo4|BC7jOrFfW%srM~%Q>x9R`U>EnAbwjXbtBu z(o9bKFph$o8HQcJ^x|35=X*Tin=IJwhB~}V@&L6cnqd>;N$e;h0fDnZ#XEtq2vm$5 zn-jQ%IXjh &MEQSyyn>7qqd%l>Gaz8YEz|KsIg-&@rh5O zKybzlfU`qeF7Q)$q*HEVSi~s*<5}qH)YR!930XvRYJ86NMiQ^Nb6sJKFH@#3+q-7r zNP&i=wv^#&>amNuXLbtt&OYd7C(UGLcwXrsr|Z<}$`#$0c`PK>uTU}RbZA9gx@l!t zO;g56Z1BFcB*gGRSE#Nzz(`*TeL%JM!*rcN_ptn1E*D%y_*W7ZMbgd>Y6d8EiS?zr z=OXu8^V%8};M^Qst3H1PJ??4d9FNPEzTD!l>h$b-6=|G;*Dn@T8wm4@KuDmjVDx<7 zrrVAbjY#Y64eJ3AlHw4=+$B oKLy^TG291gpP+mD}omvEWQ zc45EQSQ4$(T;z__(#fUcc7%}w2Av_;aK-Jr$6syMPpLNCy_Fab=#j;ByIBip{R0(g zBrxaQ0Bb_+h?xoBa8WV^Nk!eGVIpo1oHN2TEfz2`S@{tN)!!+3L2D!FVmy0@l{cr+ z`*a`c#P?< tESh^z{yM=bzxKzvO7EgRM2O?^!3T#wjxnkudw?*sel*lKZ@42}bZOx^V$F9Q!M$ zf)m1sIBI9?<9+=cYpI#T&EAN}2a2-ebJg?#CNc=Ev}rQoIeWhf<8tvPe4qv#F58}C z4(GnT*-#$iLEqHw *Ox$ 6 zH%K@i2lPuDdVmH&ZM>eeU94|c>FxE(-NTfsOyNUp&~Bnkhr1b#a(H9@AhD@at~lPV z85RZ>g}c;CYpc0RAb>D)wo5?+dDIbhR@Q}^%?1mmdd=YXl+ +GZKV}r zo0wSzbA>ng!J2qR18rKhZU+(sOL8FuiBZ1dLrh@$sWJf}ncDQU5y`m{6TsRFm= Msb` oTtpUEOW8to EaOBg-0+fGhe?nR`DtU$+cq*6l@)uP zzDkDm%@vKpyB*tgdv3-f`I^_i@rVue086RfdsnDJ)2-IJr5xLAVIJBmI=WG7L}$NW znLUUL2ZYXOIQZzK={6hI#}Wb{FMGlq>1^K6P{SsJ9@+e9o^V|5-zPU ;LJyS zzw(tV0iR&PlEn8a%9JPNxc45q%}F-eI5xs?(MAuE=xDYya=2*<`uGB=L${_2I?37A z+`{BN3V`9*a{4G6gyD4@0&!Vk9F2+F&C+P_ abAJZVbmzyY8@JPrl9RX6vi45T*e*DImRm9xX!cyfTt8`k z#big=--=FC&OL0hc_Du9HS=p_qxa+Lse{AeFvnQ)GvaY0HKLUkvQ)UW_Rs=vU{>7v zoKLsh&Xd^Q)L?(}jk)BLz>s07Wj+xio7!~Ou!$R%{RU@j>`Yu*T5GPF$Y=KRm5N6y zJ3Rx)1btYv>$?hSy5vfEHt{=vZJS{I`v9hbzIS$7if3Vc&IrBQee)_IX$`?OK?^)Q z-V_MFvmF6-SWyCo>UBPJxFlUteSNt!EQUoG{( Q}2H@${VhebR zV<9zGAOk;-W|1 C6r%uxZrM-K|Rz{yQ<)A_Fx)umT}vU`Sw_qIC-d8eDZ=F2I-$!Wt;>uNz#J#NRz zeM6vD!#DMv()FfO(N8~m&m!ZWZr&+$47TpXNy{qGSV;aZt`wScO|#Q9Czn74J(M5i z+J2kSN<;OHA4a`5L2U~AMYE>IhmAi zhK~w$8ki&RbU*eoH?MmkUncjo`@H%)m;JF>Q5t!NSD`5u5{Yhe^P=yRBo`)#eYR%h zN_l62%37Vd?zL0S<1BBzi+~hgtF4!>xv=BArK6bXO-vhkUp4L#q8@)TipnE_nJ1aS z54N_NnFED`&Ahv#amPS^?YB&?NQuSwYX?>39y)YfGf=jZXR`NDS)K79kv3K+>=Vpn z#)2R9AmQ}*fcZ wjEq+H`(9~1J;PhyfUFBk_*F&O4iZ0d z@&$BKA!h(X+5Liw_L4F27a`Bir|cNQ5vmqqk<8{-NwtGlhBQJI*>gi(hKN!NLHT zp?-44d*8~}`uq_FD(0{4RH*M1*Seml5j^Mahx<&%F7^ccfh}Hxz_ZL@nR^Q7zU$hb zKvW3q_fDspZ&;oqZxW99t+dPDr{@IQ2-}7e8frhE5LCk$8_yD1f}wt^`j#Vrtx4*9 zvE0~EEIWyi*hIE~@@(M*`KL_ICz>Ymo*VSbqb`7_ykBNb5W-lw3m`8%EgHkp)Xzcj zV>Md5esQ)+#({u;=sC_D>1d`ID9>R>)q8fAOej&{k-}~c?u)=TR_o9FNgq7UqOTNr za8Fnf#@Jay04zq~h^t%5#~0&)f+se3;sdP!xl8&}vYqLKQ%I> }ll P9lHiJ zj>n!rB66Lz2R#2{DyfC1;j{JRb4$y;kjTqm_iaXjPtE7QtmO?XxLZHz>0)) 0(y2;!}C^h5+ z=3-AMAk`FFoNWQ0wlMK{jJ~^pwCNOO6dyDlJS3RaeDR=yq=+p*drdI z-QtnkNo}_gey3nIn@cCyT$zm||6;5HwKkk2zWysDhwJPyXq3uVbSHQT^zK@3Mr>Mc zdcHdn-7W|_zhj4h_vA?Uzjz1}2 o)T*4 zQ$oD5llg6sjqZXuXxg~l4$2`84In~SS>CK`agNJ6%|`H&>y*-U8Rpw0;itK)ry8$i z#Jh;p_DPE|RFvq|I(~bKkGR1};k(#F{av4(uV4-eoTD |Gkd)Q?K~0k1V`tF*p;cnXKj7$*Y3!*vu7)ATWIzqfG=X0}4sJJD z*(ln^q-;o%O {>oY6 {`_!{ZC|Rw^j(2f>5qj zM<6zdNHD-eqAkLYHbKe`(O uZ52YSpTfLBQDQX2VFSK((`d=RJpA|Yv z@w%Y&)KpnfQJAi-u5OwbhVA|RFeob8+}zxqSD3w_{3kX_u`WS(?{sf4QA1rO1j;6F zE!H`4TF;jC`@s~X{x@zb{z hNQ pP7e; zhltx+T2wRuycf22^W#t6hH{CCs)x7U!Jl`XZ}9PzTYP N;m%7%lKka^8S-dSg%C c>y~F-94hv|Ri_d?6H{bkqo7e8N#Rs~v(J6R7E%7+69I2< zaZfj*g^31m$E9CX13DZ~ES=s5cf3G3KjPQFe)a!LSn!J;i4DqCrU`jL`Se@W;S8bh z!h=#_aTo88E~BS(Gv$Byfu%00XT+YKo*=NsMeuvoyx+eO!@IuZ=jZ<~bEO02|NWLV z8j1cZLat+Nja>)&@5s=4dVb(9dgu&W&Bc>tN=Qg-Wwf02Gi+t(_v0uEem<^CP1Ua3 zX_@|a#;v72<`+H6U%iBP8egZ526U#e9AN(%@jq5j6euNXXfxs&$-E%&X>-=D`?5t0 zxG$26Tvx|!s1D#ikZlww;dr^YKv$CV(Z=y{R6~PTT?&W%VX;aG9U~)W!b=}`_Wy@I z{x75-5Cv@LI)(S}@?Bf~GVxGZ^*y_Npm|CSi3bCpXk}eAHFWvr+mC03-0(3sTHQ#T zmGikfNFVof>4&R~Nbl+$S|d%U%7G`QW(VK(fg&}I7}=7s73xqKJDWkUFD;EiEd;4n z>6Ykhghd(m7 7ky$o$cVHoV>Q)I ztqjy-dX-TC#qhr4+)tN-dXTxv3x6*elCF4ngjRG*#;_ycr 4Yh>08Y8DsCfW!GqlJ z`HJeSqRE6b>>B&Np1gR51$5NLo7W7HIyo~Y$&s_|7uRZ;PU)2&6SpIjKA1JgdvcE{ zqjFhR)b_hK+R&N}-d8K8pkZ=gdRTMF9xT+n`~_AO&)w&=bLGMDl)|yK2Bh4h8h6hs z8v`%c$c2B%Y} `o8=6m8tsQAlJL3xW^$lWsM?H?CGF;AcU5s<#26DfKuwRT} zM<(S{Nb%RpRY?$8H48CbTZo_SyN2G{A0fP@dCmuHX=_517B<$f-W@cUQ$>vSH{1u3 z6YDs}+w&Rt8XRpTay_x?o$x|?7Q @$q4t1GBIwNQG#Ha75) zJtQ $$Hs0OA3-4YgS%=Wc1q*rbSVk z(O2OXI55WOSh4ky`-EJ+;NT7yVAa8q^T>%_p3r+#I`uSs6*zVub@4UQS~=`y*^?Mu z{Mt%5wCBno0`TGCqTK{4dJEJV*=knTA1ov09xRc$e-@`W=B!(gJ6A(rD5Uzs)R|kN zjplYi^=3857uAR>RG-+34>7s39lr fv>fVe!7_J9#dQTsgM{UEDt$E!bTJ%^%nDI;~xB2YT>eJ#EXs_-M>p1eEW_@V! zQOl~H2`zuY@4@`hmBfo7MTAi)riNo~bn9(l1BvnFK;WA?(WiXP!<*Xg5E_%yA^nLt z)?GwlUrkioW6t_lZ=;tKGmQPhPDQNb4y1W{1;x%HrDp9nu(%hU(jJ4+miGNEk=a(t zEw?P%1*)h8?rF;vtEg}CCK82z{qXy~iLt|RyM~2&GeMktD%5}h;52O&y9Q|yN!<91 zU|)N{<7de9(^%Yzb*9}2P4rZ=RciJvSH#rTDYAKvveJ~o`U* qP*0ybBbSD+ z7WCf=9@y6N1%1EJfVT~hz#sv6ne*7s4KRdmiS-8uqqAMM$)O9mh73vp-?DpADn3BO zH4$Pyp61?bC@!8O^IrUZ^xj&VAkp+d{~erI%J512Ngbf3;Ok9P=&%Y=ygYUMs$Z_N zcOqdG Msrxxp>@%&teao AXa)g8gOdOexorr#KD|b5!qP=-}*U#6y$rGs25~@}3ia=xTq2 zO(ma`n@m2Vpk{ZcTtholBtqS3#w)wk_-N4}{VU?4s|8oXEyJ`Wt3XE1sRSipT`2$d z*hnmg>RuD^8WZPu&y3F5DZfwb$+=}}7JOBhfEibB%ayQT_Py4X`aqV9A!FMuAv|5_ zE>!h=B@n5!!hXEC(#o9OwT$gGHrAygcI@(w;e;b7(d H{pBzVQ^LQaGX1hEN06Gyr|e1U8u0Ige|ofBW#mqdg^Y; r zMG a$fVQuUD z&@hEKem}O%`09tw{aaa?A-bc`OOhLRKAq)RD^@JipF>{ ux$hOV2lvT=YS~Sei(OzH4G`w*H^`JC@EQWqw6$h|VrXFs^KNr8Pd3U&3vijAC zwNdvSl)^C^rSpMjAp=Jrm@yfzvFT@jVW|J9y}9n4+5oddcUM<5ZET6+DYOYOpw?-wE-wWi`<-PeUwgKC`0}Y%57%dZ{19>3 zV}?m{oIpifdr?O-9hIU zjK>qND#8p0MP lOxOsWjMQM8i(Jois{4s>grt zrgjnzX~#BWuvUC8$VraViCb?etRfbV&BYfxUeoh>VNA~3D;j#es}o6QPCJ!rQ*q0{ z501XWS!J^t2*?~VbGQ0W4L(#B`6!Xa_S4JF9D#!s-QC!U8Z9AT$)DqubJn_k|2`2E zeq!j+C~^5i*J*s(RSs8e0??SPp^9 MH0^#_kBkO^4Y>hSCp*51mU|v_%Uy^)K zeUte4HdT`ILfi#AH( oBX@wwrbLf+e(7BaXd2-I|ljbB$*dv z5P8Ajf@WXA%jFzvCjy7M$vcS%-+|9b*fZ@wl^RPlwyk9I8Gb~^^ZrOjjS6K=K?^_| z=?BDP@}crq=#l=H!Ej{cxVBapHVr`>j*^s8EagR949UOZq;TfBpi-aqT@DK*f%E?P z8X;l-(18iQdQ k5|{X#w7J3pScfAXJ^J-066$&W4T|A5U}b#akP6bcL)jHk9E(& zaUQaTB51q+5VFIPtXP+e+5sFI8<}!Qx}h;xK_1`5g{^EhjZf4 +3}nO@GLf$ zS$T~X8R;%G`6Hx?X|JK0Zsygh>^1Jo$HTD=7V0LJrjq(>c(I5;A+Pbanc(uoSm$MT zSBeOq@Z4e~K}!YW{fbi)DlUWsR>vFXtKgRIyJDH5Xf#@%?oX1;%(DFVcv5VRdi*gg zrIdVh0r>SaU~3->4g-^OCozIs3vE4VIf|o$YeIcH3aWf+!uC&H)lCnqPG?f*V>4dT z3wOYrhqJP2j;r!H{FZN}0t%}+kup%WsJHc^PA)>8$)@HhZMZsVOt6+KO?~ nj5j-9sJM754D!4AguRe_ A zVPvBB;X0zze4Z4gQPDqr{Dolx)*_l1l8Q*_NL+T~W}NsB%&zmH?Mw)YtI?#4hl}yH zR)P;$=V}^+_l{6B6%Gq?G@*BQjHnr}06F=bkbMRtacAL7u@o*!_~GpkV7KS{&kxIs zS2PGCuw8uC+H64<9_Fy6Dg|OJX~sJTfe#>SK#5ll#S>6sb72LY eh z@B!d^bLOK&8)|*mN|uzzG(>6c$Y?9vFY)!C0&t Vsl{OP%|KYMYTuH5N< Z#Y^D|raRJ_OJS}r;m^|&lk4kTzSbj%9GQmW)*ow9 zDB9GFsKTz>)C_oNJUf@6|DKqMaQU_NGy(B00VsJccld^KAm|%&fBp{VbbiUslz>5( zJ_05QTlx^L?PUM(PB1o6)wei99E~z-p*}XT$aLmD%O)Lh?3lvsebEj{8^nX#cMzcm zvPXp|vw7EhzIPDc#y7gHw>!g5zIP7TcRV|j7Kl(6XH{Af+~b}@Db3dBeeyGNC`MTQ za!;D!el)nV1M#KsS|U_96pr?4fOqyR20OfV(W(kcQsR+3w|?f_?kuXe-hGcFzJtk| zlJU9OTkvbnx6xbvR^gDh<9wct-uO~5NAqiOHeEXjPpBk^H;DO7N4XnY$zMW9$UFKs z;b@YvlM+9*VJb}1Z!-l*ZQz|5i)(w+H`pTN(ZJk%A0A^s_M&vu?4J0bXXtz#^tBG( z ylA!D zPCNV?^!sH{(Fk;t%7gGGka2VWV0GI0(dVp?*0sW;=33d7xwau?5EgAXJaa{n+^DkM z)oDb;6}1K Dt{TCc>6>a~hY2-r8XzdFLWdDbb^f83IUMeuNdc7( *#o8=Oc-!66#j&q~>Qf(t zHP4f_I!Yl3_I@_msrdw#r(Ed%(~Tg@2LY3aKLwZ6jLqrWk+BDqTQgMUROW$&dFAeG z?Sr?n5;q!R|3Qi=t1s#OJn@VN9hBDbcEd}L%dtsxcH8Zv6291p#_EouR2q(As~vQ1 zGug&mm)R)b(X~(BYfmEepCKJ={)J{obF;=ifHR_*!Ko-`*tyD*WRscWCg?_0k?6U8 zrl_a`@;cXlh8y>Vu>N{dI>0aZamPuxEpL$9a1_bY`n9!zcr`dajy#j5 9ejy2v=KCr8)RFkHI=p=iFp8wb%e4|kYOa=Z;1kI+4?x7~F&L(BJ#g<--}W~ 3?$`#7{T`2VT;}tySE&Ap$^QNfsH{4k_cSf#fL8`*r_b>)@T=?Cc1tTF_=&gxa3*(=$Rk(MSd1TKV23cx;&sYTIBbY2#v#*jG6Ob#s8(D zY@jrK-u|etJhTa#j<0*a=&7NIb rm+H>l+nx|GgPR0905H2M-S=J1nP* zlEEr}pX?CNRq2T$wOFhLgLR^!qcLn2s<{%Z(*E}IqyD)501*Wx$M^BBo2CpQDFd1( z<;fEpDM!AD5TOqmvTpc*K9cY`e)GA%a@g#}Qb^~Q5D@_krSjy b_Xg0GzJ>E}aNTsl8D?Db zlSv-SU7B^z`t?9E(236?s_Jk=lk;3Yw%?^lCX=JdjHMm=1dcT(R=%qAae)qUc%+N} z2ZclJfiO~rS?{9kv8~vvD(`dU*GJPh-seNeYKOCf%N)3s;k?jWb&d?e&&h5-C(*H= zf2kb=JK^4tQmv&I=L$@YPdCeaXb*lz?(m*$sb715qje=qE$0Ivkq^Eyd@vEQc;{Z4 z!O@@h@GaX?6*E+tKP=3dmt_wRd@G@aK^4MA(p@uo+dv#$IAw5%_IS!PgDGZ>7krFe z6%9RgO{EJYq2Z;@ho|YmsyPR?KOISJnt91CKok7=y9I=$7kAc<4<=B~H<;H{k`Yo< zx(yXtnWC-TxVFCC&mZ^QYfu6&y1%0M6`!?>@N_T0gfi`<7DSw?{=o@~A+Hz0oWC zbw5Sc%fppf;F=NM!;?|9qJ~I5BuCSEd|K~jONC||b$tVA;r2#xfp5|dlL6?Jgn?px zG~@%%Zgfb;PJWjZP2T_Ryjt{Keq-xAvXr;DH5Elm)qlOSdFOYNM_yOtWx^xRu}u1I zpo`vA>8G>Qaa)A(X4>CYtWQ!v 2X;gBzAcW1 zp9l-ewnw8^Xh!+;e#1JK8M;kNx~D{}%Mw_-Zjvj1&A7t61 IM`0+p$Doh_wO zPRe+3kA2W}NNyDv>3x~$7rvw@>QZ+!&+&=0fdX&;4{Eu^Kld&bS$_u<&s}$iir1AG zMEaC<@mRxt9uoYh7Hn5x%gF>3HJvkv>@}IBve)9c^Bqf}XwY)HFQ=(I)|#F-wuCnQ z3+w-*Zn1Q&C?9f@%_XL!J$7B(%?X}q3gV&>JZ<;XRlcMMGWcNkZ4z>1_axA=UO{Y^ ztvExBc=y1d1j8#me?3}kB~^Ruc41c^BSG#1DT{*4m7l#$_goG`nkC2YDoaK{^xLC~ z<@W8gYGO|kq7SQ$^>#RB)D&jk0V5=iZI(jfew?_qD>e=o`4P132C{Bnsuua3a+fCn zWeyWfwE0rwMH3$55{_;~v7hc}@f=y~ie=sze&NX+N|W)HY2`2f@%uCrM{q;=%DW-* z8xmn8CaAg-X+(GWM#`reT6SbLFA4o#9$cdc}D(DeRbOM#GL&+ z3NPCv;C`uZgk(3C^b;ofM5JdT_mGazqaX=Ft_L!ll!v?Jq2b*hL0!{UIuuA~-clat zUvArFwQCh04QUABtxfARieyWTu)>b9k_ ?^&_`mf#_2#;;v#^ z;B6EdDyMkBcA+~VD==>!&!nu~eq?3wY#Pb>{XT`CnbMJtZtk71eOfMYbQ#?`YCg=v z;>l}%wA0pfN`+xydr 1 Bi9+2 zq0ebf?ZDULdqRcdM1zyno1-i6Mp1)BG}kCe4bib0HzKTkjVELua+I~yYQj?bt{Z&q z=%l4Hpg^%kF0nX!WZm-ky1n8!Z($)`L$6cxsTwwg5sC*G+coly>|NF!BFF)mMc4w# zVaV}MQC9ai;O(~JypS7@QL`ah`rb>!4rd!WH@AWk{!;fraLY$u)u{kHjbyC0e=&Wr zd<8gifnJz_>%(Oy4ntBHxo<0`srYf}S4?|6OCt6%el^;JeAsHfcep1)2Q!=OMbGlp z+y@rSp7V2#^^%ZtP`wYfGEwtDho5%8*})9v{p3I{y3d!fmPdS#uwIFy|1BvIDtOw0 zBSPEmO)<0YPTfP%Jn}kjgCVyNilG+oO;e@RIyi`%DjIi3&HG&jt1w05U7nmd1W?*v zI<;KT{Zx}k1Nvrw7~I|LJE}5J@+2=;k94A*?a5|JfX }?I0&DK@G~Sy{H@@{Tn_u-~jR`+I6Ca^531EMK z-wTqZ;b3QfTT3OnH83@h()RSlSB^j}b7{qR0!Sa#$yWp7h86`@y%gx_7V;eW!AdVQ zMaQ-goro3xUHf*#k;ze)mkii_lwlMoKg+wmJ{MUAZ~uP8ovEhxmk-aw+rqCLd>LC` zW{#?rMOrjqbnLBOS2}{5*tElu(x#K9DJ0U8>_g)Hfbu}w8{u8Wx3}ILyKjlR)82aA ze2yY_g9+ %bO%OiVdCv%pF1DwfCv&X20kWisq z)$N0OZEn-??^sNu$5Z0iXrHdDChF&Q8h#fWvAY-@N=knIi^`#J8DU49taOt8PyB5> zw*#+GiQT`7Es1wlVkCJz(sWBW(;ns>e-o}vXtgNiIG84i!mNp;t#50HKtE9EMZ4-@ zdL(=ti8RJ16bP4&lnHHzO1uR18&fgn1}1-HxHs?0s340dXG<1S(DQ_umR9CnS{0vq z357KsO_%MO-lrw5s?U;aw@qFPT3m(E8hWKvtd C*=-f}29>^6WFy|{9fbBSBL6ML4E zcY2f3JNleYnpphxz^08a3w_9%y}j`iqCwtrPvrP0;Fj{dux#lKRk zD9QJTnm;v@dNUC3T~WFUw|@8MCyVm)w! d zD?FLspAU_vi@T}CH&Z}ETjIc*ySw$zV^Xyhb!oh>pu`8S+#Zu^lGm3-P;_{8_z!7R zx~2^)58Wn2YmW@&OgtS-{zgx(R(yt@nskO0`xq1E`4!P;p(_S+W>IKK)B Q4WYIQ5J<=GC$V3l^-?+iC9rpAEVV*|1>du6^9B*J4Sca>@!+RHV#DXZK9 zrAqwou3U|lhC6EZs23zY)_T+u$a1f2#eEj_hv{8$&0807{CmCQ?E6pQI){pN@N>s$ z#8_-!G$wF5BhWZqYb7~p*4;>N4 KDGl`bZL8|azqb!1pwDr=5NQN+Pq~CYPkWv)02j`LtI2_pbyM;edlS(o-2bJt zvkq!=U(hg64zx&tQi@ygLh&NSin|nd*P_Kew0NOVN^vWGS~NJt-3jh)A-I!Z*^i#x zyL 40hiAX*FfY1W08x06d z1n*s~D^ma~*#hx%@+`m{SX}|$`G2o8{om_If(^wt@G^>uyCl`OWW^4aw0%L}Ok|@(BZT3BA9JX1iSlwTmWop8(SKn_+fVljzqn)@vzb*qkWRq0l}h z;SS>5n7GQKixyc;jR<***(w;@-5eY*d1wnUu9s-dtb;u-YFPJjUUX3Pm-`!d$$p?v zYNYcaSK b|1AT#*&?LTD{Sw8&P)%5 zQUJ;6T%pvY^8i3c6Sm%5FJ1^kVv+q^i9r1ci%ZWR%=ST0gSet$fmw_8dWz|&>wS;D z2YA0DbiQK0V~#rgMvK{<84{g3c%)_#EVmesKs-!^><`1g@u#$SJfw9?;|Y{S5Fe#e zuG@*$3N{fkaoI_VrMAo@7G5MaKDIo8^;pyelrjY(cZO~(HlLLMu!F+tmSb7>=lj2K z2No9!(9);sNe{92>+G;YgzyAj2>2pM00khq5>qc=Ce{a!*u`}19iOmXYo?mm(!$A; z{0HD!XKr(4v|Eg $O>MQl_w>lG!>c%eW9 z( 0c2{yvSkxsp1@5)nYd+r((G#r&gsI5}h6n%hy4o~!ZIE7UMzZ0Y z60%+82u&D7*1 zp@+LZc$a5;vT0*fo(Z~t&t|N8p4ajfOwT2ry$rbTMb$7ai@l; z*vfjb>JbhJRae1LJv}4C(Vv2GKOo)lTEEozWT#ba20scP1USCxOG`Ym%{^D5AUE*+ z F2<+ZyFmipW}_ftPMNuPo=METnLX>q_SA@^jjBX)g#r@ zg$N2GX(ORjdJ(YWOZg?<3^_syBkoku$_f(lETuWGNVbQnTOa;ppm;eSEmxwEoFZW@ zB6fq)yAfr9`RMVEtRz++LNI=Cyyd{%6>%P=sZ!gyyZvf18g!3v8j@bQ12hNo-)YXp z(GjYXqeLvUk;^S{kGhq0EU6Tn>0-hok!2e+TN2@4#JUvM><)rVzt`}PsRk~%8}v-C zDmJUjzvu0zC*kWUwQLb7$jAa69zw>ln&{ML%21-B8AEi`RRZ^6IrFrDDl;zytT<3$ z5F>f*Ot3Op8lQed c<9cEZV*3w1ZlA-^59l*I;{oB?#Q+WMW6IgK8vJ!NuM4Ug?>Ay%S;E=2nx*4P zrjc70;Q@8j3PImiiGdaTIlm8mg)eh+WJi`(cX;ik`uJ|`1ijHfd!}a4QulCNl6-=* zHxv-5Du(YVq#AFZoSKi_^Pt?<$y_Nnu=5S~Zfp7@d1)l|^J?+;WT(w{-}sCeXH{{D zzPLSW|HRL2>E!lt8HWx|q?+1t%xFiXywa48J;k9e7;Y@kH$07v=$bK2LiWAY_7d#f zVV9K)B;Q^M6H=?&r|-*k?Hj`MLS7w-Gyc%1fD7L+r?soPk+~dhNZYSt?FIoQ(KnR@ z!aPgujiisPa-VwsjRC3J?C@UGc@n|)pti3^elo>_v_U1E zpG_al3KYR*^>l%@i%`JZj=`BWA3ry>`qHH2gEdHa+)?PDh2r{T*OWqaf`-n{$XUyu z3$Nh$secfJga%w!qJl6)xXH%9j)pApgm%fpNXj-})GF^-{My^RY+IO_G`!$_8T#$H z$wi4P{4%2^B!gn5M>1w0a)QdvzMFG@p^_el6&X$iTA>+d%v^(Stf%`v{}9O3bGpsg zbJ~#L20o86R5=J6w@fcK%~ZhFSwJll(&x6+vBzLLt<9kFWzAa9Sn;sI5177M+x3iz zr-q`roX}y{qtH2uhp*fGh~n6tq&5#4CM@K7XM%%Vuw~JK3_yaf#2363(E}8zbA1>+ zvEoos+RpZEtQQk41sXGkLl3}dg8O_bSy^nuo%Ur XYA)KFebmK@_-5 zIm%nx^n2=iV$*(iNYG)oCpt}R&3%vU!=3X|wB-f9k&N|gShC&na7(c@)^k^X9M*Wk zWJ?88 }>>xH7_RV3sRvMVTd ABW zpp-Pin(GMe0-=JpZ=dy1lj`X6YXZ%(@(`|o9`IQ?I6E3L(kCp*b~+{S)H!->lwE!> zEmO@?E9O;NKscq0gRcxHCtV7^cK+Bd >@TmOSc zC8*C TA{5wqxWvb&oJz 6a!KSJBgP z@)J{N|IoYog)3=xNW5od>d5VsZyhSUZal!4t~8fwf62b v{r_gUeRRaw;~# zx`m@(9nmdGj#3yw+M;I>*fixC*p~Wz9;XG@!l1cD{hI2(N@&-bHCFVeU76U{9s&=| zZJUbYoWRLR6{|rCBTqb__+dNm!F(D>)n?!v zlgsdiAX%kk?Zc8;$OYva+ihEI_w=qog2%bMEvb>cDag%9&mvMnDhXRD^lT&F6Vp9U zqYV1$-a_GO#!L#=2$Si=E#y1ntL>(?KmzxXc!xdm!4`l}A#$kMqcU^8_YmJSL %mP>&Y-z1RC+fO z*KTPz)TDWU&STAv+ZyV^P3eD#{avS+)VbCz-Jd5ddY*h`xxcGs&YB81wNYMKT;he9 z&R9~aS(%{N4ON<-)VD{L#+ +Q^dO&<30LsBB3?Pa9j$M_Wns?j{w G{0`?2a0MN`LbU2y0f1G%K}_I8hj>k)I@G zw2ppB3ApmC5~>ZMVzgQ^(!4>H*%8peBjZyKiray1nNhR+sB*Ie=>zc$&Yl%r^pI1h z8E?3-|2L+YL0`~b%u?bw{N2d6Dva-JeMifR=Z`jMdn759=hO#ux}x6p1Xdn$xB@k` z%Zg{bd#(sCnhce%*eQF3J<%S32yss4$u;Vyhn;%vtd?8ou^?<{JJlkd{~)?@{N9X_ zqb&F>HpP+s3*wK~jwsi&s4gI&0K5nK+wEuo`vY?jqFlf%AYk (1XI7?@^y^}k}K=D6gX@w^iMG`MphT5HRElQ3s7-PTWBJEC5i zgUb$GK}&{5Z$AJ#Lir3g!EmD>9dch!?9PdD+}gz%jPZoM-MIW{k}H-D?IzU;k8T-2 z!34nQ)|UERLjMC3F%#kbNuXYD)wQu3B`Zz6TFeBuKJ OP~lP^ZM1yalg$$q>o$zzXs?mv;X$M1Qo#U0U5 zCKS(4zN^EyySqv6pMZ+MoNZo}G|wUt*M%^fw}?3I>6eTpEvfeMWC4?-u7W915&liD zN(Mx7Sp4h++UB(0;sg%GT9?UIFjp>&p==Z= s(3DLt3O4D7P)um zT8mtISvS>1B*@EmE<0O|W@XZ^hJY dy5$5$-(V5<^P;0&i_8H|#c2VrgG+fD%UungD)8ybAJ8 z456lkg8iijzngVBxG_^+e|Z#4MR%v$2YJgjgPC<3K=J?s*P=a~)G8n<@cG}7B=K+} zqp&po>`{5>dLSw3xN(lsj;~k^gZRkxS=}^eDW;=efxVGMbVg+ zqaiSEz@C^~Zn^s902Mab9NDj{k$_nboVm#`ag(awy2+cB^k O3^;NMY t(uu+?JYndAJjk9A z*b%CoqW{X-mIkpNroU5=(OPhNZjO>ym=}lpr6sP;it`L{DL }v#JL^E#f2qT_BQwd|B%IIx0H6XJ0U5OqHVh__zex+R_Af8@tdr|NDzMv zT+oZ4(@xY6DoG+79?~iGHSso~RW-}}~P{n0U=?_JdbKuL@D5(UTsQ0k`nYlljLxob3bwtMr^eEP$%ol(M@Bq~v ziCu>|b3IGFM7ePM*fsPzmEcP1!Jw1$hJn#>%vt`mpA~8&`;Scg^{Ke+%k^YW4UG4( zICOi!XXIwJR#!m=|AhYu`LzfEq)Gg2=mY#3^NA>IdGO#=gzJ=Fo5YuW#kzgZxf?xs z@bmGdbM$Ocaedzd4lw5q e?H=LgrbqUAEi9q9ENA3&A;-;)6D((RdVi$|=g?jkrvyJu?0dtM% ziVIT;RnS7}l;WCz<(_W#KM`o697O^bfs>mJqVn1S@!u(GbryxfmnPm3B;0>>Cp+6x zN#fGh#6WR%N?_2!jyc ?Msg-;;(`5$(U%>l6Tm!P2Qn_BmwU|{ ztl0^BBIJ>2*H3dXaV<_>tn>sEXO $)M)?zy`9-5FYjii)MoCor@t zz v=}XB&`bAhim|GBGQp%~v%(^{#Qm=fwdjH=5LlxUwzGrH#n3o+)X*0t zcXder@v>23fv?qf?jqKNU;x(txl^+tYv$Al!*Dr_CWvkzh0~T