Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

898 save app state version 3 #1011

Merged
merged 134 commits into from
Mar 28, 2024
Merged
Show file tree
Hide file tree
Changes from 90 commits
Commits
Show all changes
134 commits
Select commit Hold shift + click to select a range
9ee3c22
create functions to grab and restore app state
Aug 24, 2023
4278015
add state manager module
Aug 24, 2023
edfd6be
amend documentation
Aug 24, 2023
1b3b524
insert state manager into filter manager
Aug 24, 2023
1b149e8
rename one funciton
Aug 24, 2023
a7405d9
omit action buttons from grabs
Aug 24, 2023
6a0cc29
re-click until grab fully reset
Aug 24, 2023
130fbc8
encapsulate creating grabs
Aug 24, 2023
3b963fa
reorder file
Aug 24, 2023
634b5e8
amend documentation
Aug 24, 2023
d452326
remove dewclassing of grabbed values to keep dates and date times
Aug 25, 2023
e131b71
handle POSIXct in airDatePickerInput
Aug 25, 2023
7c21911
Merge e131b713094b85a6f73f85d82b97a5ff8e3bce87 into 54c683b92f845075a…
chlebowa Aug 25, 2023
709c4ef
[skip actions] Restyle files
github-actions[bot] Aug 28, 2023
c2e0be4
[skip actions] Roxygen Man Pages Auto Update
dependabot-preview[bot] Aug 28, 2023
735650c
remove storing initial input state as always empty
Aug 28, 2023
f854b9d
spelling
Aug 28, 2023
556c1bb
spelling
Aug 28, 2023
696375e
Merge branch 'main' into 898_save_app_state@main
chlebowa Sep 8, 2023
3e40918
use native shiny bookmarking
Sep 8, 2023
0c22dcd
open bookmarks in new window
Sep 8, 2023
b9e3e37
minor update to defauts
Sep 11, 2023
958c8d7
Merge branch 'main' into 898_save_app_state2@main
Dec 14, 2023
edb94dc
fix logic operators
Dec 14, 2023
67f8be5
fix state manager server definition and call
Dec 14, 2023
5606703
add hook to teal to set shiny option for bookmarking
Dec 15, 2023
5b47506
fix bookmarking and restoring callbacks
Dec 15, 2023
a583e23
add missing arguments in state manager and add return value in snapsh…
Dec 15, 2023
47cc2a5
properly call public methods of session object
Dec 15, 2023
81f10fb
add more missing arguments to state manager module
Dec 15, 2023
5bb0125
clean up docs
Dec 15, 2023
9e9c9de
rebuild module
Dec 15, 2023
e2fea5d
remove grab_state function
Dec 15, 2023
ca24e29
add argument checks to state manager module
Dec 15, 2023
3ec250d
remove filter panel exclusion
Dec 15, 2023
c9bbb54
don't store initial app state
Dec 15, 2023
63eb738
clean up comemnts
Dec 15, 2023
c3d40cf
init returns ui as function
Dec 18, 2023
ac363d2
use state manager module always
Dec 18, 2023
f24dd8c
Merge branch 'main' into 898_save_app_state3@main
Dec 18, 2023
b02fee4
Merge branch 'main' into 898_save_app_state3@main
Mar 13, 2024
c0f854c
all managers return
Mar 13, 2024
159e337
isolate manager_manager_module
Mar 13, 2024
0eb331e
rename manager_manager where it is used
Mar 13, 2024
ef6c049
move snapshot_manager and state_manager to manager manager
Mar 13, 2024
d25397c
rename module
Mar 13, 2024
b8b7b5c
shorten line
Mar 13, 2024
712a5c3
remove superfluous utility function
Mar 13, 2024
1084353
rename module to bookmark_manager
Mar 13, 2024
b950e31
rename grab to bookmark
Mar 13, 2024
a3d2147
fix typo
Mar 14, 2024
2149306
separate utility function in filter manager
Mar 14, 2024
af3ebfc
move renaming of global filtered data list
Mar 14, 2024
8ba143a
rename flat filtered data list and tweak utility function
Mar 14, 2024
6a3689a
rename filtered_data_list to datasets
Mar 14, 2024
fe0afb0
rename filtered_data_flat to datasets_flat
Mar 14, 2024
e59ab91
improve documentation for flatten_datasets
Mar 14, 2024
2b08206
add logging to all manager modules
Mar 14, 2024
3b82671
update name of programmatically clicked button
Mar 14, 2024
4371359
fix erroneous log
Mar 14, 2024
5d62ff4
remove delayed module initiation when starting from bookmark
Mar 14, 2024
23d408d
bug fix: missing argument value
Mar 14, 2024
c4b669c
fix update in example module
Mar 14, 2024
6d07bd2
improve logs in bookmark manager
Mar 14, 2024
43752bf
add bookmark exclusions
Mar 14, 2024
cd6c6bc
add code comment
Mar 14, 2024
fc3872a
modify button titles
Mar 15, 2024
8181ca6
remove superfluous CSS
Mar 15, 2024
07fa52f
change CSS class names
Mar 15, 2024
d896983
adjust style of bnon-first children in manager_table_row
Mar 15, 2024
6fbf146
Merge branch 'main' into 898_save_app_state3@main
chlebowa Mar 15, 2024
ed34a94
update code comment
Mar 15, 2024
2495fb3
simplify 'when from bookmark' condition
Mar 15, 2024
81f9346
Merge branch '898_save_app_state3@main' of github.com:insightsenginee…
Mar 15, 2024
5b5a1e2
replace bookmarkButton with action Button
Mar 15, 2024
debf0f2
amend documentation
Mar 15, 2024
24966a6
amend NEWS
Mar 15, 2024
ed94c39
rearrange code
Mar 15, 2024
38c9598
[skip style] [skip vbump] Restyle files
github-actions[bot] Mar 15, 2024
fa33460
fix spelling
Mar 18, 2024
9ac1fbd
Merge branch 'main' into 898_save_app_state3@main
chlebowa Mar 18, 2024
4dcc66c
improve flow control condition
Mar 18, 2024
882c1cf
amend unit tests
Mar 18, 2024
9db31ac
Merge branch '898_save_app_state3@main' of github.com:insightsenginee…
Mar 18, 2024
40d7cb9
assign return value in wunder_bar server
Mar 18, 2024
6b99ded
simplify unit test for snapshot manager
Mar 18, 2024
2b81ce2
add unit tests for wunder_bar module
Mar 18, 2024
47f93dd
Merge branch 'main' into 898_save_app_state3@main
chlebowa Mar 19, 2024
7888356
Merge branch 'main' into 898_save_app_state3@main
chlebowa Mar 19, 2024
68a7a0d
Merge branch 'main' into 898_save_app_state3@main
chlebowa Mar 19, 2024
a766110
remove delay on starting report previewer
Mar 20, 2024
40bed62
exclude all buttons (app-wide) from bookmark
Mar 20, 2024
7e3aed9
Merge branch 'main' into 898_save_app_state3@main
chlebowa Mar 20, 2024
92391a5
simplify code
Mar 21, 2024
a83b42c
change condition for initiating reporte previewer module
Mar 21, 2024
e2836b0
Merge branch 'main' into 898_save_app_state3@main
Mar 21, 2024
9d7d7b9
restoreValue
gogonzo Mar 22, 2024
557364c
remove browser
gogonzo Mar 22, 2024
be642f5
trigger
Mar 22, 2024
d36aac6
Merge branch 'main' into 898_save_app_state3@main
Mar 22, 2024
55bf1e0
add documentation for restoreValue
Mar 22, 2024
75ffb21
add function for comparing bookmarked states
Mar 22, 2024
4b94872
reorganize code
Mar 22, 2024
72bce64
amend documentation for restoreValue
Mar 22, 2024
c4e7556
move storing values to respective modules
Mar 25, 2024
c0d1e32
correct for namespace when restoring filter in module_teal
Mar 25, 2024
1b716d2
[skip roxygen] [skip vbump] Roxygen Man Pages Auto Update
dependabot-preview[bot] Mar 25, 2024
02a7db4
modify restoreValue
Mar 25, 2024
320014e
update documentation for restoreValue
Mar 25, 2024
5dbf498
clean up bookmark exclusions in bookmark manager
Mar 25, 2024
02b6710
modify storing filter state on bookmark
Mar 25, 2024
9e0f213
add comment headers
Mar 25, 2024
5c1896e
add enforcement of server-side bookmarks
Mar 25, 2024
ae46faa
register bookmark exclusions in snapshot manager
Mar 25, 2024
3423345
[skip style] [skip vbump] Restyle files
github-actions[bot] Mar 25, 2024
0e25d4c
[skip roxygen] [skip vbump] Roxygen Man Pages Auto Update
dependabot-preview[bot] Mar 25, 2024
e1df2fd
remove agruments in bookmark manager
Mar 25, 2024
b01a395
fix return value
Mar 25, 2024
5294f7c
exclude buttons from bookmark in wunder bar
Mar 25, 2024
56c55a0
amend documentation for snapshot and bookmark managers
Mar 25, 2024
8aac98c
extend message in bookmarks_identical
Mar 25, 2024
bf8b39b
teal_bookmarkable flags
Mar 26, 2024
d4a2b4d
only set app id on filter when missing
Mar 26, 2024
9edf512
Bookmarking info (#1184)
gogonzo Mar 28, 2024
92890d5
remove test
gogonzo Mar 28, 2024
30e8b28
trigger
Mar 28, 2024
33a5ba7
Merge branch 'main' into 898_save_app_state3@main
chlebowa Mar 28, 2024
a3659db
fix docs collate
gogonzo Mar 28, 2024
d3a0e09
fix docs
gogonzo Mar 28, 2024
7b40974
[skip roxygen] [skip vbump] Roxygen Man Pages Auto Update
dependabot-preview[bot] Mar 28, 2024
0aca94c
trigger
Mar 28, 2024
97c2efb
adding testing
gogonzo Mar 28, 2024
b40bd15
Merge branch '898_save_app_state3@main' of github.com:insightsenginee…
gogonzo Mar 28, 2024
6682d75
revert NEWS update
Mar 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,14 @@ Collate:
'modules.R'
'init.R'
'landing_popup_module.R'
'module_bookmark_manager.R'
'module_filter_manager.R'
'module_nested_tabs.R'
'module_snapshot_manager.R'
'module_tabs_with_filters.R'
'module_teal.R'
'module_teal_with_splash.R'
'module_wunder_bar.R'
'reporter_previewer_module.R'
'show_rcode_modal.R'
'tdata.R'
Expand Down
6 changes: 6 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# teal 0.15.2.9008

### New features
* Introduced bookmarking feature. Click the bookmark icon in the top-right corner to access the bookmark manager.

### Miscellaneous
* Filter mapping display is no longer coupled to the snapshot manager.

# teal 0.15.2

### Bug fixes
Expand Down
6 changes: 5 additions & 1 deletion R/dummy_functions.R
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ example_module <- function(label = "example teal module", datanames = "all") {
server = function(id, data) {
checkmate::assert_class(data(), "teal_data")
moduleServer(id, function(input, output, session) {
updateSelectInput(session, "dataname", choices = isolate(teal.data::datanames(data())))
updateSelectInput(
inputId = "dataname",
choices = isolate(teal.data::datanames(data())),
selected = restoreInput(session$ns("dataname"), NULL)
)
output$text <- renderPrint({
req(input$dataname)
data()[[input$dataname]]
Expand Down
5 changes: 3 additions & 2 deletions R/init.R
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
#' string specifying the `shiny` module id in cases it is used as a `shiny` module
#' rather than a standalone `shiny` app. This is a legacy feature.
#'
#' @return Named list with server and UI functions.
#' @return Named list containing server and UI functions.
#'
#' @export
#'
Expand Down Expand Up @@ -221,8 +221,9 @@ init <- function(data,
# Note regarding case `id = character(0)`:
# rather than creating a submodule of this module, we directly modify
# the UI and server with `id = character(0)` and calling the server function directly
# Note: UI must be a function to support bookmarking.
res <- list(
ui = ui_teal_with_splash(id = id, data = data, title = title, header = header, footer = footer),
ui = function(request) ui_teal_with_splash(id = id, data = data, title = title, header = header, footer = footer),
server = function(input, output, session) {
if (!is.null(landing_module)) {
do.call(landing_module$server, c(list(id = "landing_module_shiny_id"), landing_module$server_args))
Expand Down
222 changes: 222 additions & 0 deletions R/module_bookmark_manager.R
chlebowa marked this conversation as resolved.
Show resolved Hide resolved
chlebowa marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
#' App state management.
#'
#' Capture and restore the global (app) input state.
#'
#' This module introduces bookmarks into `teal` apps: the `shiny` bookmarking mechanism becomes enabled
#' and server-side bookmarks can be created.
#'
#' The bookmark manager is accessed with the bookmark icon in the [`wunder_bar`].
#' The manager's header contains a title and a bookmark icon. Clicking the icon creates a bookmark.
#' As bookmarks are added, they will show up as rows in a table, each being a link that, when clicked,
#' will open the bookmarked application in a new window.
#'
#' @section Server logic:
#' A bookmark is a URL that contains the app address with a `/?_state_id_=<bookmark_dir>` suffix.
#' `<bookmark_dir>` is a directory created on the server, where the state of the application is saved.
#' Accessing the bookmark URL opens a new session of the app that starts in the previously saved state.
#'
#' Bookmarks are stored in a `reactiveVal` as a named list.
#' For every bookmark created a piece of HTML is created that contains a link,
#' whose text is the name of the bookmark and whose href is the bookmark URL.
#'
#' @section Bookmark mechanics:
#' When a bookmark is added, the user is prompted to name it.
#' New bookmark names are validated so that thy are unique. Leading and trailing white space is trimmed.
#'
#' Once a bookmark name has been accepted, the app state is saved: values of all inputs,
#' which are kept in the `input` slot of the `session` object, are dumped into the `input.rds` file
#' in the `<bookmark_dir>` directory on the server.
#' This is out of the box behavior that permeates the entire app, no adjustments to modules are necessary.
#' An additional `onBookmark` callback creates a snapshot of the current filter state
#' (the module has access to the filter state of the application through `slices_global` and `mapping_matrix`).
#' Then that snapshot, the previous snapshot history (which is passed to this module as argument),
#' and the previous bookmark history are dumped into the `values.rds` file in `<bookmark_dir>`.
#'
#' Finally, an `onBookmarked` callback adds the newly created bookmark to the bookmark history.
#' Notably, this occurs _after_ creating the bookmark is concluded so the bookmark history that was stored
#' does not include the newly added bookmark.
#'
#' When starting the app from a bookmark, `shiny` recognizes that the app is being restored,
#' locates the bookmark directory and loads both `.rds` file.
#' Values stored in `input.rds` are automatically set to their corresponding inputs.
#' The filter state that the app had upon bookmarking, which was saved as a separate snapshot, is restored.
#' This is done in the same manner as in the `snapshot_manager` module and thus requires access to `datasets_flat`,
#' which is passed to this module as argument.
#' Finally, snapshot history and bookmark history are loaded from `values.rds` and set to appropriate `reactiveVal`s.
#'
#' @section Note:
#' All `teal` apps are inherently bookmarkable. Normal `shiny` apps require that `enableBookmarking` be set to "server",
#' either by setting an argument in a `shinyApp` call or by calling a special function. In `teal` bookmarks are enabled
#' by automatically setting an option when the package is loaded.
#'
#' @param id (`character(1)`) `shiny` module instance id.
#' @inheritParams module_snapshot_manager
#' @param snapshot_history (named `list`) of unlisted `teal_slices` objects, as returned by the `snapshot_manager`.
#'
#' @return `reactiveVal` containing a named list of bookmark URLs.
#'
#' @name module_bookmark_manager
#' @aliases bookmark bookmark_manager bookmark_manager_module
#'

#' @rdname module_bookmark_manager
#' @keywords internal
#'
bookmark_manager_ui <- function(id) {
ns <- NS(id)
div(
class = "manager_content",
div(
class = "manager_table_row",
span(tags$b("Bookmark manager")),
actionLink(ns("bookmark_add"), NULL, icon = suppressMessages(icon("solid fa-bookmark")), title = "add bookmark"),
NULL
),
uiOutput(ns("bookmark_list"))
)
}

#' @rdname module_bookmark_manager
#' @keywords internal
#'
bookmark_manager_srv <- function(id, slices_global, mapping_matrix, datasets, snapshot_history) {
checkmate::assert_character(id)
checkmate::assert_true(is.reactive(slices_global))
checkmate::assert_class(isolate(slices_global()), "teal_slices")
checkmate::assert_true(is.reactive(mapping_matrix))
checkmate::assert_data_frame(isolate(mapping_matrix()), null.ok = TRUE)
checkmate::assert_list(datasets, types = "FilteredData", any.missing = FALSE, names = "named")
checkmate::assert_true(is.reactive(snapshot_history))
checkmate::assert_list(isolate(snapshot_history()), names = "unique")

moduleServer(id, function(input, output, session) {
logger::log_trace("bookmark_manager_srv initializing")

# Set up bookmarking callbacks.
app_session <- .subset2(shiny::getDefaultReactiveDomain(), "parent")
# These exclusions are to ensure the right modals open in bookmarked app (first 2) and for extra security (3rd).
setBookmarkExclude(c("bookmark_add", "bookmark_name", "bookmark_accept"))
app_session$onBookmark(function(state) {
# Add current filter state to bookmark.
logger::log_trace("bookmark_manager_srv@onBookmark: storing filter state")
snapshot <- as.list(slices_global(), recursive = TRUE)
attr(snapshot, "mapping") <- matrix_to_mapping(mapping_matrix())
state$values$filter_state_on_bookmark <- snapshot
# Add snapshot history and bookmark history to bookmark.
logger::log_trace("bookmark_manager_srv@onBookmark: storing snapshot and bookmark history")
state$values$snapshot_history <- snapshot_history() # isolate this?
state$values$bookmark_history <- bookmark_history() # isolate this?
})
app_session$onBookmarked(function(url) {
logger::log_trace("bookmark_manager_srv@onBookmarked: bookmark button clicked, registering bookmark")
bookmark_name <- trimws(input$bookmark_name)
if (identical(bookmark_name, "")) {
logger::log_trace("bookmark_manager_srv@onBookmarked: bookmark name rejected")
showNotification(
"Please name the bookmark.",
type = "message"
)
updateTextInput(inputId = "bookmark_name", value = "", placeholder = "Meaningful, unique name")
unlink(strsplit(url, "_state_id_=")[[1L]][[2L]], recursive = TRUE, force = TRUE, expand = FALSE)
} else if (is.element(make.names(bookmark_name), make.names(names(bookmark_history())))) {
logger::log_trace("bookmark_manager_srv@onBookmarked: bookmark name rejected")
showNotification(
"This name is in conflict with other bookmark names. Please choose a different one.",
type = "message"
)
updateTextInput(inputId = "bookmark_name", value = "", placeholder = "Meaningful, unique name")
unlink(strsplit(url, "_state_id_=")[[1L]][[2L]], recursive = TRUE, force = TRUE, expand = FALSE)
} else {
# Add bookmark URL to bookmark history (with name).
logger::log_trace("bookmark_manager_srv@onBookmarked: bookmark name accepted, adding to history")
bookmark_update <- c(bookmark_history(), list(url))
names(bookmark_update)[length(bookmark_update)] <- bookmark_name
bookmark_history(bookmark_update)

removeModal()
}
})
app_session$onRestored(function(state) {
# Restore filter state.
logger::log_trace("bookmark_manager_srv@onRestored: restoring filter state")
snapshot <- state$values$filter_state_on_bookmark
snapshot_state <- as.teal_slices(snapshot)
mapping_unfolded <- unfold_mapping(attr(snapshot_state, "mapping"), names(datasets))
mapply(
function(filtered_data, filter_ids) {
filtered_data$clear_filter_states(force = TRUE)
slices <- Filter(function(x) x$id %in% filter_ids, snapshot_state)
filtered_data$set_filter_state(slices)
},
filtered_data = datasets,
filter_ids = mapping_unfolded
)
slices_global(snapshot_state)
# Restore snapshot history and bookmark history.
logger::log_trace("bookmark_manager_srv@onRestored: restoring snapshot and bookmark history")
snapshot_history(state$values$snapshot_history)
bookmark_history(state$values$bookmark_history)
})

ns <- session$ns

# Store input states.
bookmark_history <- reactiveVal({
list()
})

# Bookmark current input state - name bookmark.
observeEvent(input$bookmark_add, {
logger::log_trace("bookmark_manager_srv: bookmark_add button clicked")
showModal(
modalDialog(
textInput(ns("bookmark_name"), "Name the bookmark", width = "100%", placeholder = "Meaningful, unique name"),
footer = tagList(
actionButton(ns("bookmark_accept"), label = "Accept", icon = icon("thumbs-up")),
modalButton(label = "Cancel", icon = icon("thumbs-down"))
),
size = "s"
)
)
})

# Initiate bookmarking with normal action button b/c `bookmarkButton` may not work on Windows.
observeEvent(input$bookmark_accept, {
app_session$doBookmark()
})

# Create UI elements and server logic for the bookmark table.
# Divs are tracked for a slight speed margin.
divs <- reactiveValues()

observeEvent(bookmark_history(), {
logger::log_trace("bookmark_manager_srv: bookmark history changed, updating bookmark list")
lapply(names(bookmark_history()), function(s) {
id_rowme <- sprintf("rowme_%s", make.names(s))

# Create a row for the bookmark table.
if (!is.element(id_rowme, names(divs))) {
divs[[id_rowme]] <- div(
class = "manager_table_row",
a(h5(s), title = "go to bookmark", href = bookmark_history()[[s]], target = "blank")
)
}
})
})

# Create table to display list of bookmarks and their actions.
output$bookmark_list <- renderUI({
rows <- lapply(rev(reactiveValuesToList(divs)), function(d) d)
if (length(rows) == 0L) {
div(
class = "manager_placeholder",
"Bookmarks will appear here."
)
} else {
rows
}
})
chlebowa marked this conversation as resolved.
Show resolved Hide resolved

bookmark_history
})
}
Loading
Loading