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"))),
+ "
This is the first paragraph
\nthis 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
\nthis 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"))), + "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), + ' ' + ) + + 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), + ' ' + ) + 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)), + "