Skip to content

Allow outputs to stay in progress mode after flush #3954

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

Merged
merged 3 commits into from
Dec 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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