diff --git a/DESCRIPTION b/DESCRIPTION index ea53f49..765aa62 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -14,4 +14,9 @@ Imports: shiny (>= 1.7.4) Encoding: UTF-8 LazyData: true -RoxygenNote: 7.1.1 +RoxygenNote: 7.2.3 +Suggests: + spelling, + testthat (>= 3.0.0) +Config/testthat/edition: 3 +Language: en-US diff --git a/NAMESPACE b/NAMESPACE index 2b8d54c..8218a94 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -7,4 +7,9 @@ importFrom(golem,add_resource_path) importFrom(golem,bundle_resources) importFrom(golem,favicon) importFrom(golem,with_golem_options) +importFrom(shiny,HTML) +importFrom(shiny,column) importFrom(shiny,shinyApp) +importFrom(shiny,tagAppendAttributes) +importFrom(shiny,tagList) +importFrom(shiny,tags) diff --git a/R/golem_utils_server.R b/R/golem_utils_server.R new file mode 100644 index 0000000..0099c19 --- /dev/null +++ b/R/golem_utils_server.R @@ -0,0 +1,63 @@ +#' Inverted versions of in, is.null and is.na +#' +#' @noRd +#' +#' @examples +#' 1 %not_in% 1:10 +#' not_null(NULL) +`%not_in%` <- Negate(`%in%`) + +not_null <- Negate(is.null) + +not_na <- Negate(is.na) + +#' Removes the null from a vector +#' +#' @noRd +#' +#' @example +#' drop_nulls(list(1, NULL, 2)) +drop_nulls <- function(x) { + x[!sapply(x, is.null)] +} + +#' If x is `NULL`, return y, otherwise return x +#' +#' @param x,y Two elements to test, one potentially `NULL` +#' +#' @noRd +#' +#' @examples +#' NULL %||% 1 +"%||%" <- function(x, y) { + if (is.null(x)) { + y + } else { + x + } +} + +#' If x is `NA`, return y, otherwise return x +#' +#' @param x,y Two elements to test, one potentially `NA` +#' +#' @noRd +#' +#' @examples +#' NA %|NA|% 1 +"%|NA|%" <- function(x, y) { + if (is.na(x)) { + y + } else { + x + } +} + +#' Typing reactiveValues is too long +#' +#' @inheritParams reactiveValues +#' @inheritParams reactiveValuesToList +#' +#' @noRd +rv <- function(...) shiny::reactiveValues(...) +rvtl <- function(...) shiny::reactiveValuesToList(...) diff --git a/R/golem_utils_ui.R b/R/golem_utils_ui.R new file mode 100644 index 0000000..5a9ee8d --- /dev/null +++ b/R/golem_utils_ui.R @@ -0,0 +1,405 @@ +#' Turn an R list into an HTML list +#' +#' @param list An R list +#' @param class a class for the list +#' +#' @return an HTML list +#' @noRd +#' +#' @examples +#' list_to_li(c("a", "b")) +#' @importFrom shiny tags tagAppendAttributes tagList +list_to_li <- function(list, class = NULL) { + if (is.null(class)) { + tagList( + lapply( + list, + tags$li + ) + ) + } else { + res <- lapply( + list, + tags$li + ) + res <- lapply( + res, + function(x) { + tagAppendAttributes( + x, + class = class + ) + } + ) + tagList(res) + } +} +#' Turn an R list into corresponding HTML paragraph tags +#' +#' @param list an R list +#' @param class a class for the paragraph tags +#' +#' @return An HTML tag +#' @noRd +#' +#' @examples +#' list_to_p(c("This is the first paragraph", "this is the second paragraph")) +#' @importFrom shiny tags tagAppendAttributes tagList +#' +list_to_p <- function(list, class = NULL) { + if (is.null(class)) { + tagList( + lapply( + list, + tags$p + ) + ) + } else { + res <- lapply( + list, + tags$p + ) + res <- lapply( + res, + function(x) { + tagAppendAttributes( + x, + class = class + ) + } + ) + tagList(res) + } +} + +#' @importFrom shiny tags tagAppendAttributes tagList +named_to_li <- function(list, class = NULL) { + if (is.null(class)) { + res <- mapply( + function(x, y) { + tags$li( + HTML( + sprintf("%s: %s", y, x) + ) + ) + }, + list, + names(list), + SIMPLIFY = FALSE + ) + tagList(res) + } else { + res <- mapply( + function(x, y) { + tags$li( + HTML( + sprintf("%s: %s", y, x) + ) + ) + }, + list, + names(list), + SIMPLIFY = FALSE + ) + res <- lapply( + res, + function(x) { + tagAppendAttributes( + x, + class = class + ) + } + ) + tagList(res) + } +} + +#' Remove a tag attribute +#' +#' @param tag the tag +#' @param ... the attributes to remove +#' +#' @return a new tag +#' @noRd +#' +#' @examples +#' a <- shiny::tags$p(src = "plop", "pouet") +#' tagRemoveAttributes(a, "src") +tagRemoveAttributes <- function(tag, ...) { + attrs <- as.character(list(...)) + for (i in seq_along(attrs)) { + tag$attribs[[attrs[i]]] <- NULL + } + tag +} + +#' Hide or display a tag +#' +#' @param tag the tag +#' +#' @return a tag +#' @noRd +#' +#' @examples +#' ## Hide +#' a <- shiny::tags$p(src = "plop", "pouet") +#' undisplay(a) +#' b <- shiny::actionButton("go_filter", "go") +#' undisplay(b) +#' @importFrom shiny tagList +undisplay <- function(tag) { + # if not already hidden + if ( + !is.null(tag$attribs$style) && + !grepl("display:\\s+none", tag$attribs$style) + ) { + tag$attribs$style <- paste( + "display: none;", + tag$attribs$style + ) + } else { + tag$attribs$style <- "display: none;" + } + tag +} + +#' @importFrom shiny tagList +display <- function(tag) { + if ( + !is.null(tag$attribs$style) && + grepl("display:\\s+none", tag$attribs$style) + ) { + tag$attribs$style <- gsub( + "(\\s)*display:(\\s)*none(\\s)*(;)*(\\s)*", + "", + tag$attribs$style + ) + } + tag +} + +#' Hide an elements by calling jquery hide on it +#' +#' @param id the id of the element to hide +#' +#' @noRd +#' +#' @importFrom shiny tags +jq_hide <- function(id) { + tags$script(sprintf("$('#%s').hide()", id)) +} + +#' Add a red star at the end of the text +#' +#' Adds a red star at the end of the text +#' (for example for indicating mandatory fields). +#' +#' @param text the HTLM text to put before the red star +#' +#' @return an html element +#' @noRd +#' +#' @examples +#' with_red_star("Enter your name here") +#' @importFrom shiny tags HTML +with_red_star <- function(text) { + shiny::tags$span( + HTML( + paste0( + text, + shiny::tags$span( + style = "color:red", + "*" + ) + ) + ) + ) +} + + + +#' Repeat tags$br +#' +#' @param times the number of br to return +#' +#' @return the number of br specified in times +#' @noRd +#' +#' @examples +#' rep_br(5) +#' @importFrom shiny HTML +rep_br <- function(times = 1) { + HTML(rep("
", times = times)) +} + +#' Create an url +#' +#' @param url the URL +#' @param text the text to display +#' +#' @return an a tag +#' @noRd +#' +#' @examples +#' enurl("https://www.thinkr.fr", "ThinkR") +#' @importFrom shiny tags +enurl <- function(url, text) { + tags$a(href = url, text) +} + +#' Columns wrappers +#' +#' These are convenient wrappers around +#' `column(12, ...)`, `column(6, ...)`, `column(4, ...)`... +#' +#' @noRd +#' +#' @importFrom shiny column +col_12 <- function(...) { + column(12, ...) +} + +#' @importFrom shiny column +col_10 <- function(...) { + column(10, ...) +} + +#' @importFrom shiny column +col_8 <- function(...) { + column(8, ...) +} + +#' @importFrom shiny column +col_6 <- function(...) { + column(6, ...) +} + + +#' @importFrom shiny column +col_4 <- function(...) { + column(4, ...) +} + + +#' @importFrom shiny column +col_3 <- function(...) { + column(3, ...) +} + + +#' @importFrom shiny column +col_2 <- function(...) { + column(2, ...) +} + + +#' @importFrom shiny column +col_1 <- function(...) { + column(1, ...) +} + + + +#' Make the current tag behave like an action button +#' +#' Only works with compatible tags like button or links +#' +#' @param tag Any compatible tag. +#' @param inputId Unique id. This will host the input value to be used +#' on the server side. +#' +#' @return The modified tag with an extra id and the action button class. +#' @noRd +#' +#' @examples +#' if (interactive()) { +#' library(shiny) +#' +#' link <- a(href = "#", "My super link", style = "color: lightblue;") +#' +#' ui <- fluidPage( +#' make_action_button(link, inputId = "mylink") +#' ) +#' +#' server <- function(input, output, session) { +#' observeEvent(input$mylink, { +#' showNotification("Pouic!") +#' }) +#' } +#' +#' shinyApp(ui, server) +#' } +make_action_button <- function(tag, inputId = NULL) { + # some obvious checks + if (!inherits(tag, "shiny.tag")) stop("Must provide a shiny tag.") + if (!is.null(tag$attribs$class)) { + if (grep("action-button", tag$attribs$class)) { + stop("tag is already an action button") + } + } + if (is.null(inputId) && is.null(tag$attribs$id)) { + stop("tag does not have any id. Please use inputId to be able to + access it on the server side.") + } + + # handle id + if (!is.null(inputId)) { + if (!is.null(tag$attribs$id)) { + warning( + paste( + "tag already has an id. Please use input$", + tag$attribs$id, + "to access it from the server side. inputId will be ignored." + ) + ) + } else { + tag$attribs$id <- inputId + } + } + + # handle class + if (is.null(tag$attribs$class)) { + tag$attribs$class <- "action-button" + } else { + tag$attribs$class <- paste(tag$attribs$class, "action-button") + } + # return tag + tag +} + + +# UNCOMMENT AND USE +# +# attachment::att_amend_desc() +# +# To use this part of the UI +# +#' #' Include Content From a File +#' #' +#' #' Load rendered RMarkdown from a file and turn into HTML. +#' #' +#' #' @rdname includeRMarkdown +#' #' @export +#' #' +#' #' @importFrom rmarkdown render +#' #' @importFrom markdown markdownToHTML +#' #' @importFrom shiny HTML +#' includeRMarkdown <- function(path){ +#' +#' md <- tempfile(fileext = '.md') +#' +#' on.exit(unlink(md),add = TRUE) +#' +#' rmarkdown::render( +#' path, +#' output_format = 'md_document', +#' output_dir = tempdir(), +#' output_file = md,quiet = TRUE +#' ) +#' +#' html <- markdown::markdownToHTML(md, fragment.only = TRUE) +#' +#' Encoding(html) <- "UTF-8" +#' +#' return(HTML(html)) +#' } diff --git a/dev/01_start.R b/dev/01_start.R index 3249c84..cb8444a 100644 --- a/dev/01_start.R +++ b/dev/01_start.R @@ -22,10 +22,10 @@ golem::fill_desc( pkg_name = "chatgptimages", # The Name of the package containing the App pkg_title = "A 'Shiny' App for creating images with ChatGPT", # The Title of the package containing the App pkg_description = "A system for accessing the ChatGPT Image Generation tool, and easily creating pictures based on user description.", # The Description of the package containing the App - author_first_name = "AUTHOR_FIRST", # Your First Name - author_last_name = "AUTHOR_LAST", # Your Last Name + author_first_name = "Ross", # Your First Name + author_last_name = "Armstrong", # Your Last Name author_email = "ross.armstrong@analyticsinmotion.com", # Your Email - repo_url = NULL # The URL of the GitHub Repo (optional) + repo_url = "https://github.com/analyticsinmotion/chatgpt-images-r-shiny" # The URL of the GitHub Repo (optional) ) ## Set {golem} options ---- @@ -50,7 +50,7 @@ golem::use_recommended_tests() ## Favicon ---- # If you want to change the favicon (default is golem's one) -golem::use_favicon() # path = "path/to/ico". Can be an online file. +golem::use_favicon("https://www.analyticsinmotion.com/favicon.ico") # path = "path/to/ico". Can be an online file. # golem::remove_favicon() # Uncomment to remove the default favicon ## Add helper functions ---- diff --git a/inst/WORDLIST b/inst/WORDLIST new file mode 100644 index 0000000..8db7151 --- /dev/null +++ b/inst/WORDLIST @@ -0,0 +1,3 @@ +ChatGPT +chatgpt +golem diff --git a/inst/app/www/favicon.ico b/inst/app/www/favicon.ico index 4c0982c..433f78a 100644 Binary files a/inst/app/www/favicon.ico and b/inst/app/www/favicon.ico differ diff --git a/man/run_app.Rd b/man/run_app.Rd index 6be75ea..c6c36f7 100644 --- a/man/run_app.Rd +++ b/man/run_app.Rd @@ -33,7 +33,7 @@ request to determine whether the \code{ui} should be used to handle the request. Note that the entire request path must match the regular expression in order for the match to be considered successful.} -\item{...}{arguments to pass to golem_opts. +\item{...}{arguments to pass to golem_opts. See `?golem::get_golem_options` for more details.} } \description{ diff --git a/tests/spelling.R b/tests/spelling.R new file mode 100644 index 0000000..6713838 --- /dev/null +++ b/tests/spelling.R @@ -0,0 +1,3 @@ +if(requireNamespace('spelling', quietly = TRUE)) + spelling::spell_check_test(vignettes = TRUE, error = FALSE, + skip_on_cran = TRUE) diff --git a/tests/testthat.R b/tests/testthat.R new file mode 100644 index 0000000..531684a --- /dev/null +++ b/tests/testthat.R @@ -0,0 +1,12 @@ +# This file is part of the standard setup for testthat. +# It is recommended that you do not modify it. +# +# Where should you do additional test configuration? +# Learn more about the roles of various files in: +# * https://r-pkgs.org/tests.html +# * https://testthat.r-lib.org/reference/test_package.html#special-files + +library(testthat) +library(chatgptimages) + +test_check("chatgptimages") diff --git a/tests/testthat/test-golem-recommended.R b/tests/testthat/test-golem-recommended.R new file mode 100644 index 0000000..b314130 --- /dev/null +++ b/tests/testthat/test-golem-recommended.R @@ -0,0 +1,74 @@ +test_that("app ui", { + ui <- app_ui() + golem::expect_shinytaglist(ui) + # Check that formals have not been removed + fmls <- formals(app_ui) + for (i in c("request")) { + expect_true(i %in% names(fmls)) + } +}) + +test_that("app server", { + server <- app_server + expect_type(server, "closure") + # Check that formals have not been removed + fmls <- formals(app_server) + for (i in c("input", "output", "session")) { + expect_true(i %in% names(fmls)) + } +}) + +test_that( + "app_sys works", + { + expect_true( + app_sys("golem-config.yml") != "" + ) + } +) + +test_that( + "golem-config works", + { + config_file <- app_sys("golem-config.yml") + skip_if(config_file == "") + + expect_true( + get_golem_config( + "app_prod", + config = "production", + file = config_file + ) + ) + expect_false( + get_golem_config( + "app_prod", + config = "dev", + file = config_file + ) + ) + } +) + +# Configure this test to fit your need. +# testServer() function makes it possible to test code in server functions and modules, without needing to run the full Shiny application +testServer(app_server, { + + # Set and test an input + session$setInputs(x = 2) + expect_equal(input$x, 2) + + # Example of tests you can do on the server: + # - Checking reactiveValues + # expect_equal(r$lg, 'EN') + # - Checking output + # expect_equal(output$txt, "Text") +}) + +# Configure this test to fit your need +test_that( + "app launches", + { + golem::expect_running(sleep = 5) + } +) diff --git a/tests/testthat/test-golem_utils_server.R b/tests/testthat/test-golem_utils_server.R new file mode 100644 index 0000000..770d387 --- /dev/null +++ b/tests/testthat/test-golem_utils_server.R @@ -0,0 +1,54 @@ +test_that("not_in works", { + expect_true(1 %not_in% 2:10) + expect_false(1 %not_in% 1:10) +}) + +test_that("not_null works", { + expect_true(not_null(1)) + expect_false(not_null(NULL)) +}) + +test_that("not_na works", { + expect_true(not_na(1)) + expect_false(not_na(NA)) +}) + +test_that("drop_nulls works", { + expect_equal( + drop_nulls( + list(x = NULL, y = 2) + ), + list(y = 2) + ) +}) + +test_that("%||% works", { + expect_equal( + NULL %||% 1, + 1 + ) + expect_equal( + 2 %||% 1, + 2 + ) +}) + +test_that("%|NA|% works", { + expect_equal( + NA %|NA|% 1, + 1 + ) + expect_equal( + 2 %|NA|% 1, + 2 + ) +}) + +test_that("rv and rvtl work", { + expect_true( + inherits(rv, "function") + ) + expect_true( + inherits(rvtl, "function") + ) +}) diff --git a/tests/testthat/test-golem_utils_ui.R b/tests/testthat/test-golem_utils_ui.R new file mode 100644 index 0000000..a33935c --- /dev/null +++ b/tests/testthat/test-golem_utils_ui.R @@ -0,0 +1,177 @@ +test_that("Test with_red_star works", { + expect_s3_class(with_red_star("golem"), "shiny.tag") + expect_equal( + as.character(with_red_star("Enter your name here")), + 'Enter your name here*' + ) +}) + +test_that("Test list_to_li works", { + expect_s3_class(list_to_li(c("a", "b")), "shiny.tag.list") + expect_equal( + as.character(list_to_li(c("a", "b"))), + "
  • a
  • \n
  • b
  • " + ) + expect_equal( + as.character(list_to_li(c("a", "b"), class = "my_li")), + '
  • a
  • \n
  • b
  • ' + ) +}) + +test_that("Test list_to_p works", { + expect_s3_class( + list_to_p(c( + "This is the first paragraph", + "this is the second paragraph" + )), + "shiny.tag.list" + ) + expect_equal( + as.character( + list_to_p(c( + "This is the first paragraph", + "this is the second paragraph" + )) + ), + "

    This is the first paragraph

    \n

    this is the second paragraph

    " + ) + expect_equal( + as.character( + list_to_p( + c( + "This is the first paragraph", + "this is the second paragraph" + ), + class = "my_li" + ) + ), + '

    This is the first paragraph

    \n

    this is the second paragraph

    ' + ) +}) + +test_that("Test named_to_li works", { + expect_s3_class(named_to_li(list(a = "a", b = "b")), "shiny.tag.list") + expect_equal( + as.character(named_to_li(list(a = "a", b = "b"))), + "
  • a: a
  • \n
  • b: b
  • " + ) + expect_equal( + as.character(named_to_li(list(a = "a", b = "b"), class = "mylist")), + '
  • a: a
  • \n
  • b: b
  • ' + ) +}) + +test_that("Test tagRemoveAttributes works", { + a_with_tag <- shiny::tags$p(src = "plop", "pouet") + expect_s3_class(a_with_tag, "shiny.tag") + expect_equal( + as.character(a_with_tag), + '

    pouet

    ' + ) + + a_without_tag <- tagRemoveAttributes(a_with_tag, "src") + expect_s3_class(a_without_tag, "shiny.tag") + expect_equal( + as.character(a_without_tag), + "

    pouet

    " + ) +}) + +test_that("Test undisplay works", { + a <- shiny::tags$p(src = "plop", "pouet") + expect_s3_class(a, "shiny.tag") + expect_equal( + as.character(a), + '

    pouet

    ' + ) + a_undisplay <- undisplay(a) + expect_s3_class(a_undisplay, "shiny.tag") + expect_equal( + as.character(a_undisplay), + '

    pouet

    ' + ) + + b <- shiny::actionButton("go_filter", "go") + expect_s3_class(b, "shiny.tag") + expect_equal( + as.character(b), + '' + ) + b_undisplay <- undisplay(b) + expect_s3_class(b, "shiny.tag") + expect_equal( + as.character(b_undisplay), + '' + ) +}) + +test_that("Test display works", { + a_undisplay <- shiny::tags$p(src = "plop", "pouet", style = "display: none;") + expect_s3_class(a_undisplay, "shiny.tag") + expect_equal( + as.character(a_undisplay), + '

    pouet

    ' + ) + a_display <- display(a_undisplay) + expect_s3_class(a_display, "shiny.tag") + expect_equal( + as.character(a_display), + '

    pouet

    ' + ) +}) + +test_that("Test jq_hide works", { + expect_s3_class(jq_hide("golem"), "shiny.tag") + expect_equal( + as.character(jq_hide("golem")), + "" + ) +}) + +test_that("Test rep_br works", { + expect_s3_class(rep_br(5), "html") + expect_equal( + as.character(rep_br(5)), + "




    " + ) +}) + +test_that("Test enurl works", { + expect_s3_class(enurl("https://www.thinkr.fr", "ThinkR"), "shiny.tag") + expect_equal( + as.character(enurl("https://www.thinkr.fr", "ThinkR")), + 'ThinkR' + ) +}) + +test_that("Test columns wrappers works", { + expect_s3_class(col_12(), "shiny.tag") + expect_s3_class(col_10(), "shiny.tag") + expect_s3_class(col_8(), "shiny.tag") + expect_s3_class(col_6(), "shiny.tag") + expect_s3_class(col_4(), "shiny.tag") + expect_s3_class(col_3(), "shiny.tag") + expect_s3_class(col_2(), "shiny.tag") + expect_s3_class(col_1(), "shiny.tag") + + expect_equal(as.character(col_12()), '
    ') + expect_equal(as.character(col_10()), '
    ') + expect_equal(as.character(col_8()), '
    ') + expect_equal(as.character(col_6()), '
    ') + expect_equal(as.character(col_4()), '
    ') + expect_equal(as.character(col_3()), '
    ') + expect_equal(as.character(col_2()), '
    ') + expect_equal(as.character(col_1()), '
    ') +}) + +test_that("Test make_action_button works", { + button <- make_action_button( + a(href = "#", "My super link", style = "color: lightblue;"), + inputId = "mylink" + ) + expect_s3_class(button, "shiny.tag") + expect_equal( + as.character(button), + 'My super link' + ) +})