diff --git a/R/Editor.R b/R/Editor.R new file mode 100644 index 000000000..d562a52d1 --- /dev/null +++ b/R/Editor.R @@ -0,0 +1,556 @@ +#' Report previewer module +#' +#' @description `r lifecycle::badge("experimental")` +#' +#' Module offers functionalities to visualize, manipulate, +#' and interact with report cards that have been added to a report. +#' It includes a previewer interface to see the cards and options to modify the report before downloading. +#' +#' For more details see the vignette: `vignette("previewerReporter", "teal.reporter")`. +#' +#' @details `r global_knitr_details()` +#' +#' @name reporter_editor +#' +#' @param id (`character(1)`) `shiny` module instance id. +#' @param reporter (`Reporter`) instance. +#' @param global_knitr (`list`) of `knitr` parameters (passed to `knitr::opts_chunk$set`) +#' for customizing the rendering process. +#' @inheritParams reporter_download_inputs +#' +#' @return `NULL`. +NULL + +#' @rdname reporter_editor +#' @export +reporter_editor_ui <- function(id) { + ns <- shiny::NS(id) + + shiny::fluidRow( + add_editor_js(ns), + add_previewer_css(), + shiny::tagList( + shiny::tags$div(class = "col-md-3", shiny::tags$div(class = "well", shiny::uiOutput(ns("encoding")))), + shiny::tags$div(class = "col-md-9", shiny::tags$div(id = "reporter_previewer", shiny::uiOutput(ns("pcards")))) + ) + ) +} + +#' @rdname reporter_editor +#' @export +reporter_editor_srv <- function(id, + reporter, + global_knitr = getOption("teal.reporter.global_knitr"), + rmd_output = c( + "html" = "html_document", + "pdf" = "pdf_document", + "powerpoint" = "powerpoint_presentation", + "word" = "word_document" + ), + rmd_yaml_args = list( + author = "NEST", + title = "Report", + date = as.character(Sys.Date()), + output = "html_document", + toc = FALSE + )) { + checkmate::assert_class(reporter, "Reporter") + checkmate::assert_subset(names(global_knitr), names(knitr::opts_chunk$get())) + checkmate::assert_subset( + rmd_output, + c("html_document", "pdf_document", "powerpoint_presentation", "word_document"), + empty.ok = FALSE + ) + checkmate::assert_list(rmd_yaml_args, names = "named") + checkmate::assert_names( + names(rmd_yaml_args), + subset.of = c("author", "title", "date", "output", "toc"), + must.include = "output" + ) + checkmate::assert_true(rmd_yaml_args[["output"]] %in% rmd_output) + + shiny::moduleServer(id, function(input, output, session) { + ns <- session$ns + + teal.reporter::reset_report_button_srv("resetButtonPreviewer", reporter) + + output$encoding <- shiny::renderUI({ + reporter$get_reactive_add_card() + shiny::tagList( + shiny::tags$h3("Download the Report"), + shiny::tags$hr(), + reporter_download_inputs( + rmd_yaml_args = rmd_yaml_args, + rmd_output = rmd_output, + showrcode = any_rcode_block(reporter), + session = session + ), + htmltools::tagAppendAttributes( + shiny::tags$a( + id = ns("download_data_prev"), + class = "btn btn-primary shiny-download-link", + href = "", + target = "_blank", + download = NA, + shiny::tags$span("Download Report", shiny::icon("download")) + ), + class = if (length(reporter$get_cards())) "" else "disabled" + ), + teal.reporter::reset_report_button_ui(ns("resetButtonPreviewer"), label = "Reset Report") + ) + }) + + output$pcards <- shiny::renderUI({ + reporter$get_reactive_add_card() + input$card_remove_id + input$card_down_id + input$card_up_id + input$block_remove_event + input$block_add_event + input$block_modify_event + + cards <- reporter$get_cards() + + if (length(cards)) { + shiny::tags$div( + class = "panel-group accordion", + id = "reporter_previewer_panel", + lapply(seq_along(cards), function(ic) { + previewer_collapse_item(ic, cards[[ic]]$get_name(), cards[[ic]]$get_content()) + }) + ) + } else { + shiny::tags$div( + id = "reporter_previewer_panel_no_cards", + shiny::tags$p(class = "text-danger mt-4", shiny::tags$strong("No Cards added")) + ) + } + }) + + shiny::observeEvent(input$card_remove_id, { + shiny::showModal( + shiny::modalDialog( + title = "Remove the Report Card", + shiny::tags$p( + shiny::HTML( + sprintf( + "Do you really want to remove the card %s from the Report?", + input$card_remove_id + ) + ) + ), + footer = shiny::tagList( + shiny::tags$button( + type = "button", + class = "btn btn-secondary", + `data-dismiss` = "modal", + `data-bs-dismiss` = "modal", + NULL, + "Cancel" + ), + shiny::actionButton(ns("remove_card_ok"), "OK", class = "btn-danger") + ) + ) + ) + }) + + shiny::observeEvent(input$remove_card_ok, { + reporter$remove_cards(input$card_remove_id) + shiny::removeModal() + }) + + shiny::observeEvent(input$block_add_event, { + shiny::showModal( + shiny::modalDialog( + title = "Select Title", + selectInput(ns("selectTitle"), + label = "Select Title", + choices = c("Generate Custom Title & Footer", unique(titles$TABLE.ID)), + selected = "Generate Custom Title & Footer"), + footer = shiny::tagList( + shiny::tags$button( + type = "button", + class = "btn btn-secondary", + `data-dismiss` = "modal", + `data-bs-dismiss` = "modal", + NULL, + "Cancel" + ), + shiny::actionButton(ns("submit_text"), "Change title", class = "btn-danger") + ) + ) + ) + }) + + verboseTitle <- reactive({ + titles %>% + dplyr::filter(titles$TABLE.ID == input$selectTitle, titles$IDENTIFIER == "TITLE") %>% + dplyr::mutate(TEXT = paste0(TABLE.ID, ": ", TEXT)) %>% + dplyr::select(TEXT) + }) + + footer <- reactive({ + titles %>% + dplyr::filter(titles$TABLE.ID == input$selectTitle, titles$IDENTIFIER != "TITLE") %>% + dplyr::select(TEXT) %>% + dplyr::summarise(TEXT = paste(TEXT, collapse = "\n")) + }) + + shiny::observeEvent(input$submit_text, { + reporter$modify_text(as.integer(input$block_add_event[1]), 1, verboseTitle()) + + temp <- reporter$get_text(as.integer(input$block_add_event[1]), 3) + + reporter$remove_block_from_card(as.integer(input$block_add_event[1]), 3) + + reporter$add_text(as.integer(input$block_add_event[1]), footer()) + + reporter$add_text(as.integer(input$block_add_event[1]), as.character(temp)) + + reporter$trigger_reactive_add_card() + + shiny::removeModal() + }) + + shiny::observeEvent(input$block_modify_event, { + shiny::showModal( + shiny::modalDialog( + title = "Modify Text", + shiny::textAreaInput(ns("user_input"), + label = "Modify Text:", + value = reporter$get_text(card_id = as.numeric(input$block_modify_event[1]), block_id = input$block_modify_event[2]), + width = "100%", + height = "200px"), + footer = shiny::tagList( + shiny::tags$button( + type = "button", + class = "btn btn-secondary", + `data-dismiss` = "modal", + `data-bs-dismiss` = "modal", + NULL, + "Cancel" + ), + shiny::actionButton(ns("submit_text2"), "Change text", class = "btn-danger") + ) + ) + ) + }) + + shiny::observeEvent(input$submit_text2, { + reporter$modify_text(as.integer(input$block_modify_event[1]), input$block_modify_event[2], input$user_input) + + reporter$trigger_reactive_add_card() + + shiny::removeModal() + }) + + shiny::observeEvent(input$card_up_id, { + if (input$card_up_id > 1) { + reporter$swap_cards(as.integer(input$card_up_id), as.integer(input$card_up_id - 1)) + } + }) + + shiny::observeEvent(input$card_down_id, { + if (input$card_down_id < length(reporter$get_cards())) { + reporter$swap_cards(as.integer(input$card_down_id), as.integer(input$card_down_id + 1)) + } + }) + + shiny::observeEvent(input$block_remove_event, { + reporter$remove_block_from_card(as.integer(input$block_remove_event[1]), as.integer(input$block_remove_event[2])) + }) + + output$download_data_prev <- shiny::downloadHandler( + filename = function() { + paste("report_", format(Sys.time(), "%y%m%d%H%M%S"), ".zip", sep = "") + }, + content = function(file) { + shiny::showNotification("Rendering and Downloading the document.") + shinybusy::block(id = ns("download_data_prev"), text = "", type = "dots") + input_list <- lapply(names(rmd_yaml_args), function(x) input[[x]]) + names(input_list) <- names(rmd_yaml_args) + if (is.logical(input$showrcode)) global_knitr[["echo"]] <- input$showrcode + report_render_and_compress(reporter, input_list, global_knitr, file) + shinybusy::unblock(id = ns("download_data_prev")) + }, + contentType = "application/zip" + ) + }) +} + +#' @noRd +#' @keywords internal +block_to_editor_html <- function(card_id, block_id, block) { + b_content <- block$get_content() + if (inherits(block, "TextBlock")) { + shiny::tags$span( + class = paste("row", "align-middle"), + switch(block$get_style(), + header1 = shiny::tags$h1(class = "col-md-9", b_content), + header2 = shiny::tags$h2(class = "col-md-9", b_content), + header3 = shiny::tags$h3(class = "col-md-9", b_content), + header4 = shiny::tags$h4(class = "col-md-9", b_content), + verbatim = shiny::tags$pre(b_content), + shiny::tags$pre(b_content) + ), + shiny::tags$div(class="col-md-2", title = "Delete or modify text", + block_delete_button("block_delete", "trash", card_id, block_id, style = ""), + block_modify_button("block_modify", "pencil", card_id, block_id, style = "") + ) + ) + } else if (inherits(block, "RcodeBlock")) { + better_panel_item( + title = "R Code", + title_buttons = block_delete_button(class_name = "block_delete", "trash", card_id, block_id, style = "width:50px;height:25px;margin-left:20px;padding:2px"), + shiny::tags$pre(b_content) + ) + } else if (inherits(block, "PictureBlock")) { + shiny::tags$img(src = knitr::image_uri(b_content)) + } else if (inherits(block, "TableBlock")) { + b_table <- readRDS(b_content) + shiny::tags$pre(flextable::htmltools_value(b_table)) + } else if (inherits(block, "NewpageBlock")) { + shiny::tags$br() + } else { + stop("Unknown block class") + } +} + +#' @noRd +#' @keywords internal +add_previewer_css <- function() { + shiny::tagList( + shiny::singleton(shiny::tags$head(shiny::includeCSS(system.file("css/Previewer.css", package = "teal.reporter")))), + shiny::singleton(shiny::tags$head(shiny::includeCSS(system.file("css/custom.css", package = "teal.reporter")))) + ) +} + +#' @noRd +#' @keywords internal +add_editor_js <- function(ns) { + shiny::singleton( + shiny::tags$head( + shiny::tags$script( + shiny::HTML( + sprintf( + ' + $(document).ready(function(event) { + $("body").on("click", "span.card_remove_id", function() { + let val = $(this).data("cardid"); + Shiny.setInputValue("%s", val, {priority: "event"}); + }); + + $("body").on("click", "span.card_up_id", function() { + let val = $(this).data("cardid"); + Shiny.setInputValue("%s", val, {priority: "event"}); + }); + + $("body").on("click", "span.card_down_id", function() { + let val = $(this).data("cardid"); + Shiny.setInputValue("%s", val, {priority: "event"}); + }); + }); + function removeCardBlock(element) { + let cardId = element.dataset.cardid; + let blockId = element.dataset.blockid; + Shiny.setInputValue("%s", [cardId, blockId], {priority: "event"}); + } + function addText(element) { + let cardId = element.dataset.cardid; + let blockId = element.dataset.blockid; + Shiny.setInputValue("%s", [cardId, blockId], {priority: "event"}); + } + function modifyTitle(element, event) { + + if (event) { + console.log("Preventing collapse"); + event.preventDefault(); + event.stopPropagation(); + } + + let cardId = element.dataset.cardid; + let blockId = element.dataset.blockid; + Shiny.setInputValue("%s", [cardId, blockId], {priority: "event"}); + } + ', + ns("card_remove_id"), + ns("card_up_id"), + ns("card_down_id"), + ns("block_remove_event"), + ns("block_add_event"), + ns("block_modify_event") + ) + ) + ) + ) + ) +} + +#' @noRd +#' @keywords internal +block_delete_button <- function(class_name, icon_name, card_idx, block_idx, style = "") { + checkmate::assert_string(class_name) + checkmate::assert_string(icon_name) + checkmate::assert_int(card_idx) + checkmate::assert_int(block_idx) + + shiny::tags$button( + class = paste(class_name, "btn", "btn-danger", "btn-lg"), + style = style, + onclick = "removeCardBlock(this)", + `data-blockid` = block_idx, + `data-cardid` = card_idx, + shiny::icon(icon_name, sprintf("fa-%sx", 1L)) + ) +} + +#' @noRd +#' @keywords internal +block_add_button <- function(class_name, icon_name, card_idx, block_idx, style = "", addedText) { + checkmate::assert_string(class_name) + checkmate::assert_string(icon_name) + checkmate::assert_string(addedText) + checkmate::assert_int(card_idx) + checkmate::assert_int(block_idx) + + shiny::tags$button( + class = paste(class_name, "btn", "btn-success", "btn-lg"), + style = style, + onclick = "addText(this)", + `data-blockid` = block_idx, + `data-cardid` = card_idx, + shiny::tagList( + shiny::icon(icon_name, class = sprintf("fa-%sx", 1L)), + addedText + ) + ) +} + +#' @noRd +#' @keywords internal +block_modify_button <- function(class_name, icon_name, card_idx, block_idx, style = "") { + checkmate::assert_string(class_name) + checkmate::assert_string(icon_name) + checkmate::assert_int(card_idx) + checkmate::assert_int(block_idx) + + shiny::tags$button( + class = paste(class_name, "btn", "btn-warning", "btn-lg"), + style = style, + onclick = "modifyTitle(this, event)", + `data-blockid` = block_idx, + `data-cardid` = card_idx, + shiny::icon(icon_name, sprintf("fa-%sx", 1L)) + ) +} + +#' @noRd +#' @keywords internal +nav_previewer_icon <- function(name, icon_name, idx, size = 1L) { + checkmate::assert_string(name) + checkmate::assert_string(icon_name) + checkmate::assert_int(size) + + shiny::tags$span( + class = paste(name, "icon_previewer"), + # data field needed to record clicked card on the js side + `data-cardid` = idx, + shiny::icon(icon_name, sprintf("fa-%sx", size)) + ) +} + +#' @noRd +#' @keywords internal +nav_previewer_icons <- function(idx, size = 1L) { + shiny::tags$span( + class = "preview_card_control", + nav_previewer_icon(name = "card_remove_id", icon_name = "xmark", idx = idx, size = size), + nav_previewer_icon(name = "card_up_id", icon_name = "arrow-up", idx = idx, size = size), + nav_previewer_icon(name = "card_down_id", icon_name = "arrow-down", idx = idx, size = size) + ) +} + +#' @noRd +#' @keywords internal +previewer_collapse_item <- function(idx, card_name, card_blocks) { + shiny::tags$div( + .renderHook = function(x) { + # get bs version + version <- get_bs_version() + + if (version == "3") { + shiny::tags$div( + id = paste0("panel_card_", idx), + class = "panel panel-default", + shiny::tags$div( + class = "panel-heading overflow-auto", + shiny::tags$div( + class = "panel-title", + shiny::tags$span( + nav_previewer_icons(idx = idx), + shiny::tags$a( + class = "accordion-toggle block py-3 px-4 -my-3 -mx-4", + `data-toggle` = "collapse", + `data-parent` = "#reporter_previewer_panel", + href = paste0("#collapse", idx), + shiny::tags$h4(paste0("Card ", idx, ": ", card_name), shiny::icon("caret-down")) + ) + ) + ) + ), + shiny::tags$div( + id = paste0("collapse", idx), + class = "collapse out", + shiny::tags$div( + class = "panel-body", + shiny::tags$div( + block_add_button("block_add", "edit", card_id = idx, block_id = 1, style = "margin-left:20px;", "Select Title & Footer") + # block_add_button("block_add2", "plus", card_id = idx, block_id = 1, style = "", "Create New Title & Footer") + ), + shiny::tags$div(id = paste0("card", idx), lapply(seq_along(card_blocks), function(block_id) { + block_to_editor_html(card_id = idx, block_id = block_id, block = card_blocks[[block_id]]) + })) + ) + ) + ) + } else { + shiny::tags$div( + id = paste0("panel_card_", idx), + class = "card", + shiny::tags$div( + class = "overflow-auto", + shiny::tags$div( + class = "card-header", + shiny::tags$span( + nav_previewer_icons(idx = idx), + shiny::tags$a( + class = "accordion-toggle block py-3 px-4 -my-3 -mx-4", + # bs4 + `data-toggle` = "collapse", + # bs5 + `data-bs-toggle` = "collapse", + href = paste0("#collapse", idx), + shiny::tags$h4(paste0("Card ", idx, ": ", card_name), shiny::icon("caret-down")) + ) + ) + ) + ), + shiny::tags$div( + id = paste0("collapse", idx), + class = "collapse out", + # bs4 + `data-parent` = "#reporter_previewer_panel", + # bs5 + `data-bs-parent` = "#reporter_previewer_panel", + shiny::tags$div( + class = "card-body", + shiny::tags$div(id = paste0("card", idx), lapply(seq_along(card_blocks), function(block_id) { + block_to_editor_html(card_id = idx, block_id = block_id, block = card_blocks[[block_id]]) + })) + ) + ) + ) + } + } + ) +} diff --git a/R/ReportCard.R b/R/ReportCard.R index 34967041f..3986199c0 100644 --- a/R/ReportCard.R +++ b/R/ReportCard.R @@ -256,6 +256,21 @@ ReportCard <- R6::R6Class( # nolint: object_name_linter. } self$set_name(name) invisible(self) + }, + #' @description Removes a block from this `ReportCard`. + #' @param block_id (`numeric`) the removed block number. + #' @return `self`, invisibly. + #' @examples + #' + #' card <- ReportCard$new()$append_text("Some text") + #' card$append_text("Another text") + #' + #' card$remove_block(1) + #' card$get_content() + remove_block = function(block_id) { + checkmate::assert_number(block_id, lower = 1, upper = length(private$content), finite = TRUE) + private$content <- private$content[-c(block_id)] + invisible(self) } ), private = list( diff --git a/R/Reporter.R b/R/Reporter.R index 37716ad99..053294e91 100644 --- a/R/Reporter.R +++ b/R/Reporter.R @@ -18,9 +18,13 @@ Reporter <- R6::R6Class( # nolint: object_name_linter. #' initialize = function() { private$cards <- list() - private$reactive_add_card <- shiny::reactiveVal(0) + private$reactive_add_card <- shiny::reactiveVal(Sys.time()) invisible(self) }, + #' @description Trigger reactive card update, needed for additional appended user-text to update UI + trigger_reactive_add_card = function() { + private$reactive_add_card(Sys.time()) # sys.time chosen to update UI + }, #' @description Append one or more `ReportCard` objects to the `Reporter`. #' #' @param cards (`ReportCard`) or a list of such objects @@ -331,6 +335,63 @@ Reporter <- R6::R6Class( # nolint: object_name_linter. #' @return `character(1)` the `Reporter` id. get_id = function() { private$id + }, + #' @description Removes a block from a given `ReportCard` + #' + #' @param card_id (`numeric`) The id of the `ReportCard`. + #' @param block_id (`numeric`) The id of the removed block in the `ReportCard`. + #' @return self invisibly. + #' @examples + #' card1 <- ReportCard$new() + #' + #' card1$append_text("Header 2 text", "header2") + #' card1$append_text("A paragraph of default text", "header2") + #' + #' card2 <- ReportCard$new() + #' + #' card2$append_text("Header 2 text", "header2") + #' card2$append_text("A paragraph of default text", "header2") + #' + #' reporter <- Reporter$new() + #' reporter$append_cards(list(card1, card2)) + #' reporter$remove_block_from_card(2, 1) + #' reporter$get_blocks() + #' + remove_block_from_card = function(card_id, block_id) { + checkmate::assert_number(card_id, lower = 1, upper = length(private$cards)) + private$cards[[card_id]]$remove_block(block_id) + invisible(self) + }, + #' @description Appends additional user-entered text to a block in the `ReportCard` + #' + #' @param card_id (`numeric`) The id of the `ReportCard`. + #' @param text (`character`) Text to be added to block of the `ReportCard`. + #' @param block_id (`numeric`) The id of the block. + #' @return self invisibly. + add_text = function(card_id, text) { + checkmate::assert_number(card_id, lower = 1, upper = length(private$cards)) + private$cards[[card_id]]$append_text(as.character(text), "verbatim") + invisible(self) + }, + #' @description Modify user-entered text to a block in the `ReportCard` + #' + #' @param card_id (`numeric`) The id of the `ReportCard`. + #' @param text (`character`) Text to be modified in block of the `ReportCard`. + #' @param block_id (`numeric`) The id of the block. + #' @return self invisibly. + modify_text = function(card_id, block_id, text) { + checkmate::assert_number(card_id, lower = 1, upper = length(private$cards)) + as.list(private$cards[[as.numeric(card_id)]]$get_content()[[as.numeric(block_id)]])$set_content(as.character(text)) + invisible(self) + }, + #' @description Retrieve user entered text of a block in the `ReportCard` + #' + #' @param card_id (`numeric`) The id of the `ReportCard`. + #' @param block_id (`numeric`) The id of the block. + #' @return user-entered block text. + get_text = function(card_id, block_id) { + checkmate::assert_number(card_id, lower = 1, upper = length(private$cards)) + private$cards[[as.numeric(card_id)]]$get_content()[[as.numeric(block_id)]]$get_content() } ), private = list( diff --git a/R/titles.xlsx b/R/titles.xlsx new file mode 100644 index 000000000..e0494e576 Binary files /dev/null and b/R/titles.xlsx differ diff --git a/testApp.R b/testApp.R new file mode 100644 index 000000000..ddcd7a739 --- /dev/null +++ b/testApp.R @@ -0,0 +1,93 @@ +library(shiny) +library(DT) +library(ggplot2) +library(teal.reporter) +library(xlsx) +library(dplyr) + +# replace this with medra version +medraVersion = "1.0" + +titles <- xlsx::read.xlsx("R/titles.xlsx", "Sheet1") %>% + dplyr::filter(dplyr::row_number() > 1) %>% + mutate(TEXT = case_when( + grepl("\\&meddrav\\..", TEXT) ~ gsub("\\&meddrav\\..", medraVersion, TEXT), + grepl("~\\{super a\\}", TEXT) ~ gsub("~\\{super a\\}", "ᵃ", TEXT), + grepl("~\\{super b\\}", TEXT) ~ gsub("~\\{super b\\}", "ᵇ", TEXT), + grepl("~\\{super c\\}", TEXT) ~ gsub("~\\{super c\\}", "ᶜ", TEXT), + grepl("~\\{super d\\}", TEXT) ~ gsub("~\\{super d\\}", "ᵈ", TEXT), + grepl("~\\{super e\\}", TEXT) ~ gsub("~\\{super e\\}", "ᵉ", TEXT), + grepl("~\\{super f\\}", TEXT) ~ gsub("~\\{super f\\}", "ᶠ", TEXT), + grepl("~\\{super g\\}", TEXT) ~ gsub("~\\{super g\\}", "ᵍ", TEXT), + grepl("~\\{super h\\}", TEXT) ~ gsub("~\\{super h\\}", "ʰ", TEXT), + grepl("~\\{super i\\}", TEXT) ~ gsub("~\\{super i\\}", "ⁱ", TEXT), + TRUE ~ TEXT + )) + +titles[nrow(titles) + 1,] = c("Generate Custom Title & Footer", + "TITLE", "Click Yellow Button To Modify This Title") + +titles[nrow(titles) + 1,] = c("Generate Custom Title & Footer", + "FOOTNOTE1", "Click Yellow Button To Modify This Footer") + +devtools::load_all() + +ui <- fluidPage( + titlePanel(""), + tabsetPanel( + tabPanel( + "main App", + tags$br(), + sidebarLayout( + sidebarPanel( + uiOutput("encoding") + ), + mainPanel( + plotOutput("dist_plot") + ) + ) + ), + ### REPORTER + tabPanel( + "Editor", + reporter_editor_ui("editor") + ) + ### + ) +) +server <- function(input, output, session) { + + output$encoding <- renderUI({ + tagList( + ### REPORTER + simple_reporter_ui("simple_reporter"), + ### + ) + }) + plot <- reactive({ + ggplot(data = mtcars, aes(x = mpg)) + + geom_histogram(binwidth = 8) + }) + output$dist_plot <- renderPlot(plot()) + + ### REPORTER + reporter <- Reporter$new() + card_fun <- function(card = ReportCard$new(), comment) { + card$set_name("Plot Module") + card$append_text("My plot", "header2") + card$append_plot(plot()) + card$append_rcode("x <- mtcars$mpg", + echo = TRUE, + eval = FALSE + ) + + if (!comment == "") { + card$append_text("Comment", "header3") + card$append_text(comment) + } + card + } + simple_reporter_srv("simple_reporter", reporter = reporter, card_fun = card_fun) + reporter_editor_srv("editor", reporter) +} +shinyApp(ui, server)