Skip to content

Commit

Permalink
Merge pull request rstudio#3954 from rstudio/persistent-progress
Browse files Browse the repository at this point in the history
Allow outputs to stay in progress mode after flush
  • Loading branch information
jcheng5 authored Dec 12, 2023
2 parents 59b1c46 + 33dc41c commit 300fb21
Show file tree
Hide file tree
Showing 15 changed files with 217 additions and 95 deletions.
6 changes: 5 additions & 1 deletion R/input-action.R
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
#' @param label The contents of the button or link--usually a text label, but
#' you could also use any other HTML, like an image.
#' @param icon An optional [icon()] to appear on the button.
#' @param disabled If `TRUE`, the button will not be clickable. Use
#' [updateActionButton()] to dynamically enable/disable the button.
#' @param ... Named attributes to be applied to the button or link.
#'
#' @family input elements
Expand Down Expand Up @@ -49,7 +51,8 @@
#' * Event handlers (e.g., [observeEvent()], [eventReactive()]) won't execute on initial load.
#' * Input validation (e.g., [req()], [need()]) will fail on initial load.
#' @export
actionButton <- function(inputId, label, icon = NULL, width = NULL, ...) {
actionButton <- function(inputId, label, icon = NULL, width = NULL,
disabled = FALSE, ...) {

value <- restoreInput(id = inputId, default = NULL)

Expand All @@ -58,6 +61,7 @@ actionButton <- function(inputId, label, icon = NULL, width = NULL, ...) {
type="button",
class="btn btn-default action-button",
`data-val` = value,
disabled = if (isTRUE(disabled)) NA else NULL,
list(validateIcon(icon), label),
...
)
Expand Down
49 changes: 42 additions & 7 deletions R/shiny.R
Original file line number Diff line number Diff line change
Expand Up @@ -1149,6 +1149,8 @@ ShinySession <- R6Class(
structure(list(), class = "try-error", condition = cond)
} else if (inherits(cond, "shiny.output.cancel")) {
structure(list(), class = "cancel-output")
} else if (inherits(cond, "shiny.output.progress")) {
structure(list(), class = "progress-output")
} else if (cnd_inherits(cond, "shiny.silent.error")) {
# The error condition might have been chained by
# foreign code, e.g. dplyr. Find the original error.
Expand Down Expand Up @@ -1177,6 +1179,33 @@ ShinySession <- R6Class(
# client knows that progress is over.
self$requestFlush()

if (inherits(value, "progress-output")) {
# This is the case where an output needs to compute for longer
# than this reactive flush. We put the output into progress mode
# (i.e. adding .recalculating) with a special flag that means
# the progress indication should not be cleared until this
# specific output receives a new value or error.
self$showProgress(name, persistent=TRUE)

# It's conceivable that this output already ran successfully
# within this reactive flush, in which case we could either show
# the new output while simultaneously making it .recalculating;
# or we squelch the new output and make whatever output is in
# the client .recalculating. I (jcheng) decided on the latter as
# it seems more in keeping with what we do with these kinds of
# intermediate output values/errors in general, i.e. ignore them
# and wait until we have a final answer. (Also kind of feels
# like a bug in the app code if you routinely have outputs that
# are executing successfully, only to be invalidated again
# within the same reactive flush--use priority to fix that.)
private$invalidatedOutputErrors$remove(name)
private$invalidatedOutputValues$remove(name)

# It's important that we return so that the existing output in
# the client remains untouched.
return()
}

private$sendMessage(recalculating = list(
name = name, status = 'recalculated'
))
Expand Down Expand Up @@ -1309,23 +1338,29 @@ ShinySession <- R6Class(
private$startCycle()
}
},
showProgress = function(id) {
showProgress = function(id, persistent=FALSE) {
'Send a message to the client that recalculation of the output identified
by \\code{id} is in progress. There is currently no mechanism for
explicitly turning off progress for an output component; instead, all
progress is implicitly turned off when flushOutput is next called.'
progress is implicitly turned off when flushOutput is next called.
You can use persistent=TRUE if the progress for this output component
should stay on beyond the flushOutput (or any subsequent flushOutputs); in
that case, progress is only turned off (and the persistent flag cleared)
when the output component receives a value or error, or, if
showProgress(id, persistent=FALSE) is called and a subsequent flushOutput
occurs.'

# If app is already closed, be sure not to show progress, otherwise we
# will get an error because of the closed websocket
if (self$closed)
return()

if (id %in% private$progressKeys)
return()

private$progressKeys <- c(private$progressKeys, id)
if (!id %in% private$progressKeys) {
private$progressKeys <- c(private$progressKeys, id)
}

self$sendProgress('binding', list(id = id))
self$sendProgress('binding', list(id = id, persistent = persistent))
},
sendProgress = function(type, message) {
private$sendMessage(
Expand Down
10 changes: 7 additions & 3 deletions R/update-input.R
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ updateCheckboxInput <- function(session = getDefaultReactiveDomain(), inputId, l
#' Change the label or icon of an action button on the client
#'
#' @template update-input
#' @param disabled If `TRUE`, the button will not be clickable; if `FALSE`, it
#' will be.
#' @inheritParams actionButton
#'
#' @seealso [actionButton()]
Expand Down Expand Up @@ -169,16 +171,18 @@ updateCheckboxInput <- function(session = getDefaultReactiveDomain(), inputId, l
#' }
#' @rdname updateActionButton
#' @export
updateActionButton <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, icon = NULL) {
updateActionButton <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, icon = NULL, disabled = NULL) {
validate_session_object(session)

if (!is.null(icon)) icon <- as.character(validateIcon(icon))
message <- dropNulls(list(label=label, icon=icon))
message <- dropNulls(list(label=label, icon=icon, disabled=disabled))
session$sendInputMessage(inputId, message)
}
#' @rdname updateActionButton
#' @export
updateActionLink <- updateActionButton
updateActionLink <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, icon = NULL) {
updateActionButton(session, inputId=inputId, label=label, icon=icon)
}


#' Change the value of a date input on the client
Expand Down
7 changes: 6 additions & 1 deletion R/utils.R
Original file line number Diff line number Diff line change
Expand Up @@ -1115,7 +1115,10 @@ need <- function(expr, message = paste(label, "must be provided"), label) {
#' @param ... Values to check for truthiness.
#' @param cancelOutput If `TRUE` and an output is being evaluated, stop
#' processing as usual but instead of clearing the output, leave it in
#' whatever state it happens to be in.
#' whatever state it happens to be in. If `"progress"`, do the same as `TRUE`,
#' but also keep the output in recalculating state; this is intended for cases
#' when an in-progress calculation will not be completed in this reactive
#' flush cycle, but is still expected to provide a result in the future.
#' @return The first value that was passed in.
#' @export
#' @examples
Expand Down Expand Up @@ -1147,6 +1150,8 @@ req <- function(..., cancelOutput = FALSE) {
if (!isTruthy(item)) {
if (isTRUE(cancelOutput)) {
cancelOutput()
} else if (identical(cancelOutput, "progress")) {
reactiveStop(class = "shiny.output.progress")
} else {
reactiveStop(class = "validation")
}
Expand Down
110 changes: 68 additions & 42 deletions inst/www/shared/shiny.js
Original file line number Diff line number Diff line change
Expand Up @@ -4869,6 +4869,20 @@
}
});

// node_modules/core-js/modules/es.set.constructor.js
var require_es_set_constructor = __commonJS({
"node_modules/core-js/modules/es.set.constructor.js": function() {
"use strict";
var collection = require_collection();
var collectionStrong = require_collection_strong();
collection("Set", function(init2) {
return function Set2() {
return init2(this, arguments.length ? arguments[0] : void 0);
};
}, collectionStrong);
}
});

// node_modules/core-js/internals/array-buffer-basic-detection.js
var require_array_buffer_basic_detection = __commonJS({
"node_modules/core-js/internals/array-buffer-basic-detection.js": function(exports, module) {
Expand Down Expand Up @@ -5559,20 +5573,6 @@
}
});

// node_modules/core-js/modules/es.set.constructor.js
var require_es_set_constructor = __commonJS({
"node_modules/core-js/modules/es.set.constructor.js": function() {
"use strict";
var collection = require_collection();
var collectionStrong = require_collection_strong();
collection("Set", function(init2) {
return function Set2() {
return init2(this, arguments.length ? arguments[0] : void 0);
};
}, collectionStrong);
}
});

// node_modules/core-js/internals/flatten-into-array.js
var require_flatten_into_array = __commonJS({
"node_modules/core-js/internals/flatten-into-array.js": function(exports, module) {
Expand Down Expand Up @@ -9970,22 +9970,31 @@
key: "receiveMessage",
value: function receiveMessage(el, data) {
var $el = (0, import_jquery16.default)(el);
var label = $el.text();
var icon = "";
if ($el.find("i[class]").length > 0) {
var iconHtml = $el.find("i[class]")[0];
if (iconHtml === $el.children()[0]) {
icon = (0, import_jquery16.default)(iconHtml).prop("outerHTML");
if (hasDefinedProperty(data, "label") || hasDefinedProperty(data, "icon")) {
var label = $el.text();
var icon = "";
if ($el.find("i[class]").length > 0) {
var iconHtml = $el.find("i[class]")[0];
if (iconHtml === $el.children()[0]) {
icon = (0, import_jquery16.default)(iconHtml).prop("outerHTML");
}
}
if (hasDefinedProperty(data, "label")) {
label = data.label;
}
if (hasDefinedProperty(data, "icon")) {
var _data$icon;
icon = Array.isArray(data.icon) ? "" : (_data$icon = data.icon) !== null && _data$icon !== void 0 ? _data$icon : "";
}
$el.html(icon + " " + label);
}
if (hasDefinedProperty(data, "label")) {
label = data.label;
}
if (hasDefinedProperty(data, "icon")) {
var _data$icon;
icon = Array.isArray(data.icon) ? "" : (_data$icon = data.icon) !== null && _data$icon !== void 0 ? _data$icon : "";
if (hasDefinedProperty(data, "disabled")) {
if (data.disabled) {
$el.attr("disabled", "");
} else {
$el.attr("disabled", null);
}
}
$el.html(icon + " " + label);
}
}, {
key: "unsubscribe",
Expand Down Expand Up @@ -18882,6 +18891,12 @@
return _bindAll3.apply(this, arguments);
}

// srcts/src/shiny/shinyapp.ts
var import_es_array_iterator49 = __toESM(require_es_array_iterator());

// node_modules/core-js/modules/es.set.js
require_es_set_constructor();

// srcts/src/shiny/shinyapp.ts
var import_es_regexp_exec15 = __toESM(require_es_regexp_exec());
var import_es_json_stringify4 = __toESM(require_es_json_stringify());
Expand Down Expand Up @@ -18955,7 +18970,6 @@
});

// srcts/src/shiny/shinyapp.ts
var import_es_array_iterator49 = __toESM(require_es_array_iterator());
var import_jquery38 = __toESM(require_jquery());

// srcts/src/utils/asyncQueue.ts
Expand Down Expand Up @@ -19433,9 +19447,6 @@
// node_modules/core-js/modules/es.weak-map.js
require_es_weak_map_constructor();

// node_modules/core-js/modules/es.set.js
require_es_set_constructor();

// node_modules/core-js/modules/es.array.flat.js
var $77 = require_export();
var flattenIntoArray = require_flatten_into_array();
Expand Down Expand Up @@ -22823,6 +22834,7 @@
_defineProperty20(this, "$inputValues", {});
_defineProperty20(this, "$initialInput", null);
_defineProperty20(this, "$bindings", {});
_defineProperty20(this, "$persistentProgress", /* @__PURE__ */ new Set());
_defineProperty20(this, "$values", {});
_defineProperty20(this, "$errors", {});
_defineProperty20(this, "$conditionals", {});
Expand Down Expand Up @@ -22860,6 +22872,11 @@
});
if (binding2.showProgress)
binding2.showProgress(true);
if (message.persistent) {
this.$persistentProgress.add(key);
} else {
this.$persistentProgress.delete(key);
}
}
},
open: function() {
Expand Down Expand Up @@ -23459,38 +23476,45 @@
}
return _sendMessagesToHandlers;
}()
}, {
key: "_clearProgress",
value: function _clearProgress() {
for (var name in this.$bindings) {
if (hasOwnProperty(this.$bindings, name) && !this.$persistentProgress.has(name)) {
this.$bindings[name].showProgress(false);
}
}
}
}, {
key: "_init",
value: function _init() {
var _this3 = this;
addMessageHandler("values", /* @__PURE__ */ function() {
var _ref3 = _asyncToGenerator13(/* @__PURE__ */ _regeneratorRuntime13().mark(function _callee8(message) {
var name, _key;
var _key;
return _regeneratorRuntime13().wrap(function _callee8$(_context8) {
while (1)
switch (_context8.prev = _context8.next) {
case 0:
for (name in _this3.$bindings) {
if (hasOwnProperty(_this3.$bindings, name))
_this3.$bindings[name].showProgress(false);
}
_this3._clearProgress();
_context8.t0 = _regeneratorRuntime13().keys(message);
case 2:
if ((_context8.t1 = _context8.t0()).done) {
_context8.next = 9;
_context8.next = 10;
break;
}
_key = _context8.t1.value;
if (!hasOwnProperty(message, _key)) {
_context8.next = 7;
_context8.next = 8;
break;
}
_context8.next = 7;
_this3.$persistentProgress.delete(_key);
_context8.next = 8;
return _this3.receiveOutput(_key, message[_key]);
case 7:
case 8:
_context8.next = 2;
break;
case 9:
case 10:
case "end":
return _context8.stop();
}
Expand All @@ -23502,8 +23526,10 @@
}());
addMessageHandler("errors", function(message) {
for (var _key2 in message) {
if (hasOwnProperty(message, _key2))
if (hasOwnProperty(message, _key2)) {
_this3.$persistentProgress.delete(_key2);
_this3.receiveError(_key2, message[_key2]);
}
}
});
addMessageHandler("inputMessages", /* @__PURE__ */ function() {
Expand Down
8 changes: 4 additions & 4 deletions inst/www/shared/shiny.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion inst/www/shared/shiny.min.js

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions inst/www/shared/shiny.min.js.map

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion man/actionButton.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion man/req.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 300fb21

Please sign in to comment.