diff --git a/DESCRIPTION b/DESCRIPTION index 7ad4c6b7..e4da849c 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: blockr Title: A block-based framework for data manipulation and visualization -Version: 0.0.2.9031 +Version: 0.0.3.9000 Authors@R: c(person(given = "Nicolas", family = "Bennett", @@ -42,7 +42,8 @@ Imports: shinyWidgets Remotes: DivadNojnarg/DiagrammeR, - BristolMyersSquibb/blockr.data + BristolMyersSquibb/blockr.data, + cynkra/scoutbaR Suggests: knitr, rmarkdown, @@ -55,7 +56,8 @@ Suggests: ggplot2, vdiffr, palmerpenguins, - blockr.data + blockr.data, + scoutbaR Config/testthat/edition: 3 VignetteBuilder: knitr Depends: diff --git a/NAMESPACE b/NAMESPACE index 4ed32af5..a8f34c68 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -16,6 +16,15 @@ S3method(block_icon,data_block) S3method(block_icon,default) S3method(block_icon,plot_block) S3method(block_icon,transform_block) +S3method(block_input_check,data_block) +S3method(block_input_check,parser_block) +S3method(block_input_check,result_block) +S3method(block_input_check,transform_block) +S3method(block_output_ptype,dataset_block) +S3method(block_output_ptype,filesbrowser_block) +S3method(block_output_ptype,result_block) +S3method(block_output_ptype,transform_block) +S3method(block_output_ptype,upload_block) S3method(blockr_serialize,block) S3method(blockr_serialize,field) S3method(blockr_serialize,stack) @@ -148,7 +157,9 @@ export(block_combiner) export(block_descr) export(block_header) export(block_icon) +export(block_input_check) export(block_name) +export(block_output_ptype) export(blockr_deserialize) export(blockr_serialize) export(cat_logger) @@ -166,7 +177,6 @@ export(generate_server) export(generate_ui) export(get_compatible_blocks) export(get_field_name) -export(get_registry) export(get_stack_name) export(get_stack_title) export(get_workspace) @@ -180,6 +190,7 @@ export(init_lock) export(initialize_block) export(initialize_field) export(inject_remove_button) +export(input_failure) export(input_ids) export(is_block) export(is_field) diff --git a/NEWS.md b/NEWS.md index f6f2e77c..4bc95d5c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,47 @@ +# blockr 0.0.3.9000 + +## Breaking changes +- `register_blocks()` and `register_block()` __input__, __output__ have been changed. If your package is composed of __data__ or __transform__ blocks, nothing has to be done, expect removing the old registration API. If you developed custom blocks, whose classes are neither __data__ or __transform__, you'll have to develop a type checker like so, for a plot block: + +```r +block_input_check.plot_block <- function(x, data, ...) { + + if (inherits(data, "data.frame")) { + return(invisible(NULL)) + } + + input_failure("Expecting data.frame input.") +} + +block_output_ptype.plot_block <- function(x, ...) ggplot() +``` + +Besides, there is no need to specify the __classes__ as this is automatically retrieved from the constructed block. +As a consequence, the new registration looks like: + +```r +# NEW +register_block( + constructor = new_ggplot_block, + name = "ggplot", + description = "Initialise a ggplot2 plot", + category = "Plot", + package = pkg +) + +# OLD +register_block( + constructor = new_ggplot_block, + name = "ggplot", + description = "Initialise a ggplot2 plot", + classes = c("ggplot_block", "plot_block"), + input = "plot", + output = "plot", + category = "Plot", + package = pkg +) +``` + # blockr 0.0.2.9031 ## Feature diff --git a/R/block-core.R b/R/block-core.R index 452b6366..87858d5c 100644 --- a/R/block-core.R +++ b/R/block-core.R @@ -312,3 +312,89 @@ update_fields.transform_block <- function(x, session, data, ...) { #' @rdname update_fields #' @export update_fields.plot_block <- update_fields.transform_block + +#' Block input/output +#' +#' Used for checking whether blocks are compatible +#' +#' @param x Block +#' @param data Input data. +#' @param ... For generic consistency. +#' @rdname block_io +#' @export +block_input_check <- function(x, data, ...) UseMethod("block_input_check") + +#' @rdname block_io +#' @export +block_input_check.data_block <- function(x, data, ...) { + + if (missing(data) || is.null(data)) { + return(invisible(NULL)) + } + + input_failure("No (or empty) input expected.") +} + +#' @rdname block_io +#' @export +block_input_check.transform_block <- function(x, data, ...) { + + if (inherits(data, "data.frame")) { + return(invisible(NULL)) + } + + input_failure("Expecting data.frame input.") +} + +#' @rdname block_io +#' @export +block_input_check.parser_block <- function(x, data, ...) { + + if (is_string(data)) { + return(invisible(NULL)) + } + + input_failure("Expecting string-valued input.") +} + +#' @rdname block_io +#' @export +block_input_check.result_block <- function(x, data, ...) { + + if (length(get_workspace_stacks()) <= 1L) { + input_failure("Expecting at least two stacks.") + } + + NextMethod() +} + +#' @rdname block_io +#' @param class Custom class +#' @export +input_failure <- function(..., class = character()) { + rlang::abort(paste0(...), class = c(class, "input_failure")) +} + +#' @rdname block_io +#' @export +block_output_ptype <- function(x, ...) UseMethod("block_output_ptype") + +#' @rdname block_io +#' @export +block_output_ptype.dataset_block <- function(x, ...) data.frame() + +#' @rdname block_io +#' @export +block_output_ptype.result_block <- function(x, ...) value(x[["stack"]]) + +#' @rdname block_io +#' @export +block_output_ptype.upload_block <- function(x, ...) character(1L) + +#' @rdname block_io +#' @export +block_output_ptype.filesbrowser_block <- function(x, ...) character(1L) + +#' @rdname block_io +#' @export +block_output_ptype.transform_block <- function(x, ...) data.frame() diff --git a/R/registry.R b/R/registry.R index 938190bb..eef1ab7c 100644 --- a/R/registry.R +++ b/R/registry.R @@ -38,18 +38,16 @@ block_name <- block_descrs_getter(block_descr_getter("name")) #' @export block_descr <- block_descrs_getter(block_descr_getter("description")) -new_block_descr <- function(constructor, name, description, classes, input, +new_block_descr <- function(constructor, name, description, id, classes, input, output, pkg, category) { stopifnot( is.function(constructor), is_string(name), is_string(description), - is_string(category), - is.character(classes), length(classes) >= 1L, - is_string(input), is_string(output), is_string(pkg) + is_string(category), is.character(classes), length(classes) >= 1L, + is.function(input), is_string(pkg), is_string(id) ) structure( - constructor, - name = name, description = description, + constructor, name = name, description = description, id = id, classes = classes, input = input, output = output, package = pkg, category = category, class = "block_descr" ) @@ -62,6 +60,8 @@ block_registry <- new.env() #' @param classes Block classes #' @param input,output Object types the block consumes and produces #' @param package Package where block is defined +#' @param ptype Block ptype +#' @param id Block registry ID #' @param category Useful to sort blocks by topics. If not specified, #' blocks are uncategorized. #' @@ -71,18 +71,25 @@ register_block <- function( constructor, name, description, - classes, - input, - output, + ptype = constructor(), + classes = class(ptype), + input = get_s3_method("block_input_check", ptype), + output = block_output_ptype(ptype), + id = classes[1L], package = NA_character_, category = "uncategorized") { + descr <- new_block_descr( - constructor, name, description, classes, input, - output, package, category + constructor, + name, + description, + id, classes, + input, + output, + package, + category ) - id <- classes[1L] - if (id %in% list_blocks()) { warning("block ", id, " already exists and will be overwritten.") } @@ -90,35 +97,40 @@ register_block <- function( assign(id, descr, envir = block_registry) } +get_s3_method <- function(generic, obj) { + + for (cls in class(obj)) { + res <- try(utils::getS3method("block_input_check", cls), silent = TRUE) + if (!inherits(res, "try-error")) { + return(res) + } + } + + stop("no method found for generic ", generic, " and classes ", + paste0(class(obj), collapse = ", ")) +} + #' @param ... Forwarded to `register_block()` #' @rdname available_blocks #' @export -register_blocks <- function( - constructor, - name, - description, - classes, - input, - output, - package = NA_character_, - category = "uncategorized") { - if (length(constructor) == 1L && !is.list(classes)) { - classes <- list(classes) +register_blocks <- function(...) { + + arg_processor <- function(constructor, ...) { + + wrap_list <- function(x) { + if (length(x) > 1L) list(x) else x + } + + if (length(constructor) > 1L) { + return(c(list(constructor), list(...))) + } + + c(list(constructor), lapply(list(...), wrap_list)) } - res <- Map( - register_block, - constructor = constructor, - name = name, - description = description, - classes = classes, - input = input, - output = output, - package = package, - category = category + invisible( + do.call(Map, c(register_block, arg_processor(...))) ) - - invisible(res) } list_blocks <- function() { @@ -212,60 +224,6 @@ register_blockr_blocks <- function(pkg) { "Select n first rows of dataset", "Mutate block" ), - classes = list( - c("dataset_block", "data_block"), - c("result_block", "data_block"), - c("upload_block", "data_block"), - c("filesbrowser_block", "data_block"), - c("csv_block", "parser_block", "transform_block"), - c("rds_block", "parser_block", "transform_block"), - c("json_block", "parser_block", "transform_block"), - c("xpt_block", "parser_block", "transform_block"), - c("filter_block", "transform_block"), - c("select_block", "transform_block"), - c("summarize_block", "transform_block"), - c("arrange_block", "transform_block"), - c("group_by_block", "transform_block"), - c("join_block", "transform_block"), - c("head_block", "transform_block"), - c("mutate_block", "transform_block") - ), - input = c( - NA_character_, - NA_character_, - NA_character_, - NA_character_, - "string", - "string", - "string", - "string", - "data.frame", - "data.frame", - "data.frame", - "data.frame", - "data.frame", - "data.frame", - "data.frame", - "data.frame" - ), - output = c( - "data.frame", - "data.frame", - "string", - "string", - "data.frame", - "data.frame", - "data.frame", - "data.frame", - "data.frame", - "data.frame", - "data.frame", - "data.frame", - "data.frame", - "data.frame", - "data.frame", - "data.frame" - ), package = pkg, category = c( "data", @@ -292,6 +250,7 @@ register_blockr_blocks <- function(pkg) { #' @rdname available_blocks #' @export construct_block <- function(block, ...) { + if (is_string(block)) { block <- get_block_descr(block) } @@ -301,29 +260,40 @@ construct_block <- function(block, ...) { block(...) } -#' List available blocks as a data.frame +are_blocks_compatible <- function(x, y) { + + stopifnot(inherits(x, "block_descr")) + + tryCatch( + { + attr(x, "input")(x(), y) + TRUE + }, + input_failure = function(e) { + structure(FALSE, msg = conditionMessage(e)) + } + ) +} + +#' Find stack compatible blocks #' -#' Provides an alternate way of displaying -#' the registry information. -#' This can be useful to create dynamic UI elements -#' like in \link{add_block_ui}. +#' Given a stack, we use the registry to find +#' what are the blocks compatible with the last stack block. +#' If the stack is empy, we return data blocks. #' -#' @return A dataframe. +#' @param stack Stack object. +#' +#' @return a dataframe. #' #' @export -get_registry <- function() { - res <- lapply(seq_along(available_blocks()), \(i) { - blk <- available_blocks()[[i]] - attrs <- attributes(blk) - data.frame( - ctor = names(available_blocks())[[i]], - description = attrs[["description"]], - category = attrs[["category"]], - classes = paste(c(attrs[["classes"]], "block"), collapse = ", "), - input = attrs[["input"]], - output = attrs[["output"]], - package = attrs[["package"]] - ) - }) - do.call(rbind, res) +get_compatible_blocks <- function(stack) { + + if (length(stack)) { + dat <- block_output_ptype(stack[[length(stack)]]) + } else { + dat <- NULL + } + + blocks <- available_blocks() + blocks[lgl_ply(blocks, are_blocks_compatible, dat)] } diff --git a/R/server.R b/R/server.R index 70653199..3f7f4d75 100644 --- a/R/server.R +++ b/R/server.R @@ -210,8 +210,9 @@ generate_server_block <- function( out_dat <- if (attr(x, "submit") > -1) { eventReactive(input$submit, { - req(is_valid$block) - if (is.null(in_dat())) { + if (!isTruthy(is_valid$block)) { + block_output_ptype(blk()) + } else if (is.null(in_dat())) { evaluate_block(blk()) } else { evaluate_block(blk(), data = in_dat()) @@ -223,8 +224,9 @@ generate_server_block <- function( ) } else { reactive({ - req(is_valid$block) - if (is.null(in_dat()) && !inherits(x, "transform_block")) { + if (!isTruthy(is_valid$block)) { + block_output_ptype(blk()) + } else if (is.null(in_dat()) && !inherits(x, "transform_block")) { evaluate_block(blk()) } else { evaluate_block(blk(), data = in_dat()) @@ -522,14 +524,20 @@ add_block_server.default <- function(x, id, vals, ...) { blk_choices(get_compatible_blocks(vals$stack)) choices <- blk_choices() - choices$name <- paste(choices$package, sep = "::", choices$ctor) + + choices <- lapply( + set_names(nm = c("name", "package", "id", "category", "description")), + function(x) chr_ply(choices, attr, x) + ) + + choices$name <- paste0(choices$name, " (", choices$package, ")") shinyWidgets::updateVirtualSelect( "search", choices = shinyWidgets::prepare_choices( - choices, + as.data.frame(choices), .data$name, - .data$ctor, + .data$id, group_by = .data$category, description = .data$description ) diff --git a/R/stack.R b/R/stack.R index 8a7ae563..2024bbe6 100644 --- a/R/stack.R +++ b/R/stack.R @@ -248,13 +248,13 @@ add_block <- function(stack, block, position = NULL) { if (!length(stack)) { tmp <- initialize_block( - do.call(block, list()) + construct_block(block) ) } else { tmp <- initialize_block( - do.call(block, list(position = position)), + construct_block(block, position = position), data ) } @@ -262,45 +262,6 @@ add_block <- function(stack, block, position = NULL) { set_stack_blocks(stack, append(stack, list(tmp), position), data) } -#' Find stack compatible blocks -#' -#' Given a stack, we use the registry to find -#' what are the blocks compatible with the last stack block. -#' If the stack is empy, we return data blocks. -#' -#' @param stack Stack object. -#' -#' @return a dataframe. -#' -#' @export -get_compatible_blocks <- function(stack) { - registry <- get_registry() - # Only data blocks can be used for a 0 length stack - if (length(stack) == 0) { - tmp <- registry[is.na(registry$input), ] - # Drop result block if there are no other stacks - if (length(get_workspace_stacks()) <= 1) { - tmp <- tmp[tmp$ctor != "result_block", ] - } - return(tmp) - } - # Otherwise we compare the output of the last block - # and propose any of the block that have compatible input - cls <- paste(class(stack[[length(stack)]]), collapse = ", ") - last_blk_output <- registry[grep(cls, registry$classes), "output"] - - tmp <- registry[!is.na(registry$input) & registry$input == last_blk_output, ] - - # Insert result block if there are other stacks - if (length(get_workspace_stacks()) > 1) { - tmp <- rbind( - registry[registry$ctor == "result_block", ], - tmp - ) - } - tmp -} - #' @param stack An object inheriting form `"stack"` #' @param id Stack ID #' diff --git a/R/sysdata.rda b/R/sysdata.rda index 42616776..53a7d360 100644 Binary files a/R/sysdata.rda and b/R/sysdata.rda differ diff --git a/_pkgdown.yml b/_pkgdown.yml index 978229fb..cac08513 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -144,6 +144,8 @@ reference: - "`get_registry`" - "`add_block_ui`" - "`add_block_server`" + - "`block_input_check`" + - "`block_output_ptype`" - title: Server utilities desc: Shiny server elements @@ -206,6 +208,7 @@ reference: news: releases: + - text: "blockr 0.0.3.9000" - text: "blockr 0.0.2.9031" - text: "blockr 0.0.2" - text: "blockr 0.0.1.9000" diff --git a/inst/shinylive/apps/ae-forest-plot/app.R b/inst/shinylive/apps/ae-forest-plot/app.R index c778c670..d74ce8b4 100644 --- a/inst/shinylive/apps/ae-forest-plot/app.R +++ b/inst/shinylive/apps/ae-forest-plot/app.R @@ -139,14 +139,22 @@ new_forest_plot_block <- function(...) { ) } +block_input_check.plot_block <- function(x, data, ...) { + + if (inherits(data, "data.frame")) { + return(invisible(NULL)) + } + + input_failure("Expecting data.frame input.") +} + +block_output_ptype.plot_block <- function(x, ...) ggplot() + # Register the custom block register_block( new_forest_plot_block, name = "Adverse Event Forest Plot", - description = "Create a forest plot of adverse events comparing two treatment arms", - classes = c("adverse_event_plot_block", "plot_block"), - input = "data.frame", - output = "plot" + description = "Create a forest plot of adverse events comparing two treatment arms" ) # Create the stack diff --git a/inst/shinylive/apps/ggplot-block/app.R b/inst/shinylive/apps/ggplot-block/app.R index 372d6fd6..4ecbd068 100644 --- a/inst/shinylive/apps/ggplot-block/app.R +++ b/inst/shinylive/apps/ggplot-block/app.R @@ -38,6 +38,38 @@ new_geompoint_block <- function(color = character(), shape = character(), ...) { ) } +block_input_check.plot_block <- function(x, data, ...) { + + if (inherits(data, "data.frame")) { + return(invisible(NULL)) + } + + input_failure("Expecting data.frame input.") +} + +block_output_ptype.plot_block <- function(x, ...) ggplot() + +block_input_check.plot_layer_block <- function(x, data, ...) { + if (inherits(data, "ggplot")) { + return(invisible(NULL)) + } + + input_failure("Expecting ggplot input.") +} + +block_output_ptype.plot_layer_block <- function(x, ...) ggplot() + +register_blocks( + constructor = c(new_ggplot_block, new_geompoint_block), + name = c("ggplot block", "geompoint block"), + description = c( + "Builds a ggplot object", + "Add points geom to ggplot object" + ), + package = "blockr.demo", + category = c("Visualization", "Visualization") +) + stack <- new_stack( data_block = new_dataset_block("penguins", "palmerpenguins"), plot_block = new_ggplot_block("flipper_length_mm", "body_mass_g"), diff --git a/inst/shinylive/apps/palmer-penguins/app.R b/inst/shinylive/apps/palmer-penguins/app.R index 3fdffe4b..42595c42 100644 --- a/inst/shinylive/apps/palmer-penguins/app.R +++ b/inst/shinylive/apps/palmer-penguins/app.R @@ -38,9 +38,41 @@ new_geompoint_block <- function(color = character(), shape = character(), ...) { ) } +block_input_check.plot_block <- function(x, data, ...) { + + if (inherits(data, "data.frame")) { + return(invisible(NULL)) + } + + input_failure("Expecting data.frame input.") +} + +block_output_ptype.plot_block <- function(x, ...) ggplot() + +block_input_check.plot_layer_block <- function(x, data, ...) { + if (inherits(data, "ggplot")) { + return(invisible(NULL)) + } + + input_failure("Expecting ggplot input.") +} + +block_output_ptype.plot_layer_block <- function(x, ...) ggplot() + +register_blocks( + constructor = c(new_ggplot_block, new_geompoint_block), + name = c("ggplot block", "geompoint block"), + description = c( + "Builds a ggplot object", + "Add points geom to ggplot object" + ), + package = "blockr.demo", + category = c("Visualization", "Visualization") +) + stack <- new_stack( data_block = new_dataset_block("penguins", "palmerpenguins"), - filter_block = new_filter_block("sex", "female"), + filter_block = new_filter_block("sex", "female", submit = TRUE), plot_block = new_ggplot_block("flipper_length_mm", "body_mass_g"), layer_block = new_geompoint_block("species", "species") ) diff --git a/inst/shinylive/apps/registry-demo/app.R b/inst/shinylive/apps/registry-demo/app.R index 2601680c..b02fa557 100644 --- a/inst/shinylive/apps/registry-demo/app.R +++ b/inst/shinylive/apps/registry-demo/app.R @@ -19,10 +19,7 @@ new_tail_block <- function(data, n_rows = numeric(), ...) { register_block( constructor = new_tail_block, name = "tail block", - description = "return last n rows", - classes = c("tail_block", "transform_block"), - input = "data.frame", - output = "data.frame" + description = "return last n rows" ) stack <- new_stack(new_dataset_block) diff --git a/inst/testdata/test.registry/R/zzz.R b/inst/testdata/test.registry/R/zzz.R index ab799814..b7abf67e 100644 --- a/inst/testdata/test.registry/R/zzz.R +++ b/inst/testdata/test.registry/R/zzz.R @@ -5,9 +5,6 @@ name = "dummy block", description = "Returns input dataset", category = "transform", - classes = c("dummy_block", "transform_block"), - input = "data.frame", - output = "data.frame", package = pkgname ) diff --git a/man/available_blocks.Rd b/man/available_blocks.Rd index d2777f30..4bd97120 100644 --- a/man/available_blocks.Rd +++ b/man/available_blocks.Rd @@ -20,23 +20,16 @@ register_block( constructor, name, description, - classes, - input, - output, + ptype = constructor(), + classes = class(ptype), + input = get_s3_method("block_input_check", ptype), + output = block_output_ptype(ptype), + id = classes[1L], package = NA_character_, category = "uncategorized" ) -register_blocks( - constructor, - name, - description, - classes, - input, - output, - package = NA_character_, - category = "uncategorized" -) +register_blocks(...) unregister_blocks(ids = NULL, package = NULL) @@ -49,20 +42,24 @@ construct_block(block, ...) \item{name, description}{Metadata describing the block} +\item{ptype}{Block ptype} + \item{classes}{Block classes} \item{input, output}{Object types the block consumes and produces} +\item{id}{Block registry ID} + \item{package}{Package where block is defined} \item{category}{Useful to sort blocks by topics. If not specified, blocks are uncategorized.} +\item{...}{Forwarded to \code{register_block()}} + \item{ids}{Character vector of block IDs (first entry in class attribute)} \item{block}{Block name or description object} - -\item{...}{Forwarded to \code{register_block()}} } \description{ List available blocks. diff --git a/man/block_io.Rd b/man/block_io.Rd new file mode 100644 index 00000000..0b93f9b2 --- /dev/null +++ b/man/block_io.Rd @@ -0,0 +1,53 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/block-core.R +\name{block_input_check} +\alias{block_input_check} +\alias{block_input_check.data_block} +\alias{block_input_check.transform_block} +\alias{block_input_check.parser_block} +\alias{block_input_check.result_block} +\alias{input_failure} +\alias{block_output_ptype} +\alias{block_output_ptype.dataset_block} +\alias{block_output_ptype.result_block} +\alias{block_output_ptype.upload_block} +\alias{block_output_ptype.filesbrowser_block} +\alias{block_output_ptype.transform_block} +\title{Block input/output} +\usage{ +block_input_check(x, data, ...) + +\method{block_input_check}{data_block}(x, data, ...) + +\method{block_input_check}{transform_block}(x, data, ...) + +\method{block_input_check}{parser_block}(x, data, ...) + +\method{block_input_check}{result_block}(x, data, ...) + +input_failure(..., class = character()) + +block_output_ptype(x, ...) + +\method{block_output_ptype}{dataset_block}(x, ...) + +\method{block_output_ptype}{result_block}(x, ...) + +\method{block_output_ptype}{upload_block}(x, ...) + +\method{block_output_ptype}{filesbrowser_block}(x, ...) + +\method{block_output_ptype}{transform_block}(x, ...) +} +\arguments{ +\item{x}{Block} + +\item{data}{Input data.} + +\item{...}{For generic consistency.} + +\item{class}{Custom class} +} +\description{ +Used for checking whether blocks are compatible +} diff --git a/man/get_compatible_blocks.Rd b/man/get_compatible_blocks.Rd index b0f3ce2a..0313a424 100644 --- a/man/get_compatible_blocks.Rd +++ b/man/get_compatible_blocks.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/stack.R +% Please edit documentation in R/registry.R \name{get_compatible_blocks} \alias{get_compatible_blocks} \title{Find stack compatible blocks} diff --git a/man/get_registry.Rd b/man/get_registry.Rd deleted file mode 100644 index 812d0421..00000000 --- a/man/get_registry.Rd +++ /dev/null @@ -1,17 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/registry.R -\name{get_registry} -\alias{get_registry} -\title{List available blocks as a data.frame} -\usage{ -get_registry() -} -\value{ -A dataframe. -} -\description{ -Provides an alternate way of displaying -the registry information. -This can be useful to create dynamic UI elements -like in \link{add_block_ui}. -} diff --git a/tests/testthat/_snaps/block/block-app-001_.new.png b/tests/testthat/_snaps/block/block-app-001_.new.png index 63396905..74be8a30 100644 Binary files a/tests/testthat/_snaps/block/block-app-001_.new.png and b/tests/testthat/_snaps/block/block-app-001_.new.png differ diff --git a/tests/testthat/_snaps/block/block-app-002_.new.png b/tests/testthat/_snaps/block/block-app-002_.new.png index 88e2cad6..d196cc23 100644 Binary files a/tests/testthat/_snaps/block/block-app-002_.new.png and b/tests/testthat/_snaps/block/block-app-002_.new.png differ diff --git a/tests/testthat/_snaps/block/block-app-003_.new.png b/tests/testthat/_snaps/block/block-app-003_.new.png index b4d056ed..c5d76266 100644 Binary files a/tests/testthat/_snaps/block/block-app-003_.new.png and b/tests/testthat/_snaps/block/block-app-003_.new.png differ diff --git a/tests/testthat/_snaps/block/block-app-004_.new.png b/tests/testthat/_snaps/block/block-app-004_.new.png index 7cf37837..aa52392f 100644 Binary files a/tests/testthat/_snaps/block/block-app-004_.new.png and b/tests/testthat/_snaps/block/block-app-004_.new.png differ diff --git a/tests/testthat/_snaps/block/block-app-005_.new.png b/tests/testthat/_snaps/block/block-app-005_.new.png index f5ae933a..30bec99f 100644 Binary files a/tests/testthat/_snaps/block/block-app-005_.new.png and b/tests/testthat/_snaps/block/block-app-005_.new.png differ diff --git a/tests/testthat/_snaps/result/result-app-001_.new.png b/tests/testthat/_snaps/result/result-app-001_.new.png index e9b0d0c5..45c5c9b2 100644 Binary files a/tests/testthat/_snaps/result/result-app-001_.new.png and b/tests/testthat/_snaps/result/result-app-001_.new.png differ diff --git a/tests/testthat/_snaps/stack/stack-app-001_.png b/tests/testthat/_snaps/stack/stack-app-001_.png index f5ae933a..30bec99f 100644 Binary files a/tests/testthat/_snaps/stack/stack-app-001_.png and b/tests/testthat/_snaps/stack/stack-app-001_.png differ diff --git a/tests/testthat/_snaps/stack/stack-app-002_.new.png b/tests/testthat/_snaps/stack/stack-app-002_.new.png deleted file mode 100644 index 6b451e7f..00000000 Binary files a/tests/testthat/_snaps/stack/stack-app-002_.new.png and /dev/null differ diff --git a/tests/testthat/_snaps/stack/stack-app-002_.png b/tests/testthat/_snaps/stack/stack-app-002_.png index d28a2e6a..8e0e0892 100644 Binary files a/tests/testthat/_snaps/stack/stack-app-002_.png and b/tests/testthat/_snaps/stack/stack-app-002_.png differ diff --git a/tests/testthat/_snaps/stack/stack-app-003.json b/tests/testthat/_snaps/stack/stack-app-003.json index 8819cc6c..af565436 100644 --- a/tests/testthat/_snaps/stack/stack-app-003.json +++ b/tests/testthat/_snaps/stack/stack-app-003.json @@ -39,8 +39,8 @@ ], "choices": [ - "function (data) ", - "colnames(data)" + "structure(function (data) ", + "colnames(data), result = c(\"Time\", \"demand\"))" ], "multiple": true } diff --git a/tests/testthat/_snaps/stack/stack-app-003_.new.png b/tests/testthat/_snaps/stack/stack-app-003_.new.png deleted file mode 100644 index b6a99d73..00000000 Binary files a/tests/testthat/_snaps/stack/stack-app-003_.new.png and /dev/null differ diff --git a/tests/testthat/_snaps/stack/stack-app-003_.png b/tests/testthat/_snaps/stack/stack-app-003_.png index 173fce55..ce74019b 100644 Binary files a/tests/testthat/_snaps/stack/stack-app-003_.png and b/tests/testthat/_snaps/stack/stack-app-003_.png differ diff --git a/tests/testthat/_snaps/stack/stack-app-004.json b/tests/testthat/_snaps/stack/stack-app-004.json index 8819cc6c..af565436 100644 --- a/tests/testthat/_snaps/stack/stack-app-004.json +++ b/tests/testthat/_snaps/stack/stack-app-004.json @@ -39,8 +39,8 @@ ], "choices": [ - "function (data) ", - "colnames(data)" + "structure(function (data) ", + "colnames(data), result = c(\"Time\", \"demand\"))" ], "multiple": true } diff --git a/tests/testthat/_snaps/stack/stack-app-004_.new.png b/tests/testthat/_snaps/stack/stack-app-004_.new.png deleted file mode 100644 index 3cd83561..00000000 Binary files a/tests/testthat/_snaps/stack/stack-app-004_.new.png and /dev/null differ diff --git a/tests/testthat/_snaps/stack/stack-app-004_.png b/tests/testthat/_snaps/stack/stack-app-004_.png index df7d30a3..443e9662 100644 Binary files a/tests/testthat/_snaps/stack/stack-app-004_.png and b/tests/testthat/_snaps/stack/stack-app-004_.png differ diff --git a/tests/testthat/_snaps/stack/stack-app-005.json b/tests/testthat/_snaps/stack/stack-app-005.json index 7e62e68f..a70b29a8 100644 --- a/tests/testthat/_snaps/stack/stack-app-005.json +++ b/tests/testthat/_snaps/stack/stack-app-005.json @@ -39,8 +39,8 @@ ], "choices": [ - "function (data) ", - "colnames(data)" + "structure(function (data) ", + "colnames(data), result = c(\"Time\", \"demand\"))" ], "multiple": true } diff --git a/tests/testthat/_snaps/stack/stack-app-005_.new.png b/tests/testthat/_snaps/stack/stack-app-005_.new.png deleted file mode 100644 index e4785146..00000000 Binary files a/tests/testthat/_snaps/stack/stack-app-005_.new.png and /dev/null differ diff --git a/tests/testthat/_snaps/stack/stack-app-005_.png b/tests/testthat/_snaps/stack/stack-app-005_.png index 4f1e88e2..c8ed93db 100644 Binary files a/tests/testthat/_snaps/stack/stack-app-005_.png and b/tests/testthat/_snaps/stack/stack-app-005_.png differ diff --git a/tests/testthat/_snaps/stack/stack-app-006.json b/tests/testthat/_snaps/stack/stack-app-006.json index 3952aeb2..2cf11793 100644 --- a/tests/testthat/_snaps/stack/stack-app-006.json +++ b/tests/testthat/_snaps/stack/stack-app-006.json @@ -39,8 +39,8 @@ ], "choices": [ - "function (data) ", - "colnames(data)" + "structure(function (data) ", + "colnames(data), result = c(\"Time\", \"demand\"))" ], "multiple": true } diff --git a/tests/testthat/_snaps/stack/stack-app-006_.new.png b/tests/testthat/_snaps/stack/stack-app-006_.new.png deleted file mode 100644 index f4b7011e..00000000 Binary files a/tests/testthat/_snaps/stack/stack-app-006_.new.png and /dev/null differ diff --git a/tests/testthat/_snaps/stack/stack-app-006_.png b/tests/testthat/_snaps/stack/stack-app-006_.png index 0c362ef4..cd714d6d 100644 Binary files a/tests/testthat/_snaps/stack/stack-app-006_.png and b/tests/testthat/_snaps/stack/stack-app-006_.png differ diff --git a/tests/testthat/_snaps/stack/stack-app-007.json b/tests/testthat/_snaps/stack/stack-app-007.json index 5d5eae82..2e06ed50 100644 --- a/tests/testthat/_snaps/stack/stack-app-007.json +++ b/tests/testthat/_snaps/stack/stack-app-007.json @@ -39,8 +39,8 @@ ], "choices": [ - "function (data) ", - "colnames(data)" + "structure(function (data) ", + "colnames(data), result = c(\"Time\", \"demand\"))" ], "multiple": true } diff --git a/tests/testthat/_snaps/stack/stack-app-007_.png b/tests/testthat/_snaps/stack/stack-app-007_.png index 9e958ce7..475b2e6e 100644 Binary files a/tests/testthat/_snaps/stack/stack-app-007_.png and b/tests/testthat/_snaps/stack/stack-app-007_.png differ diff --git a/tests/testthat/_snaps/workspace/restore-workspace-app-001_.new.png b/tests/testthat/_snaps/workspace/restore-workspace-app-001_.new.png index 83cfc7a3..50ee23bb 100644 Binary files a/tests/testthat/_snaps/workspace/restore-workspace-app-001_.new.png and b/tests/testthat/_snaps/workspace/restore-workspace-app-001_.new.png differ diff --git a/tests/testthat/_snaps/workspace/workspace-app-001_.new.png b/tests/testthat/_snaps/workspace/workspace-app-001_.new.png index ba1f0729..30df0c07 100644 Binary files a/tests/testthat/_snaps/workspace/workspace-app-001_.new.png and b/tests/testthat/_snaps/workspace/workspace-app-001_.new.png differ diff --git a/tests/testthat/_snaps/workspace/workspace-app-002_.new.png b/tests/testthat/_snaps/workspace/workspace-app-002_.new.png index 491126e9..df0a8915 100644 Binary files a/tests/testthat/_snaps/workspace/workspace-app-002_.new.png and b/tests/testthat/_snaps/workspace/workspace-app-002_.new.png differ diff --git a/tests/testthat/_snaps/workspace/workspace-app-003_.new.png b/tests/testthat/_snaps/workspace/workspace-app-003_.new.png index 6b69d050..95f66b3c 100644 Binary files a/tests/testthat/_snaps/workspace/workspace-app-003_.new.png and b/tests/testthat/_snaps/workspace/workspace-app-003_.new.png differ diff --git a/tests/testthat/_snaps/workspace/workspace-app-004_.new.png b/tests/testthat/_snaps/workspace/workspace-app-004_.new.png index ba1f0729..30df0c07 100644 Binary files a/tests/testthat/_snaps/workspace/workspace-app-004_.new.png and b/tests/testthat/_snaps/workspace/workspace-app-004_.new.png differ diff --git a/tests/testthat/test-add-block.R b/tests/testthat/test-add-block.R index 6e790822..f9399abd 100644 --- a/tests/testthat/test-add-block.R +++ b/tests/testthat/test-add-block.R @@ -4,16 +4,16 @@ test_that("add_block works", { stack <- new_stack() expect_error( - stack |> add_block(new_select_block) + stack |> add_block(available_blocks()[["select_block"]]) ) stack <- new_stack(new_dataset_block) |> - add_block(new_select_block) + add_block(available_blocks()[["select_block"]]) expect_length(stack, 2) expect_s3_class(stack[[2]], "select_block") - stack <- add_block(stack, new_filter_block, 1) + stack <- add_block(stack, available_blocks()[["filter_block"]], 1) expect_length(stack, 3) expect_s3_class(stack[[2]], "filter_block") diff --git a/tests/testthat/test-registry.R b/tests/testthat/test-registry.R index 32ded562..2fcf5667 100644 --- a/tests/testthat/test-registry.R +++ b/tests/testthat/test-registry.R @@ -68,9 +68,10 @@ test_that("3rd party blocks can be registrerd (script)", { } register_block( - new_dummy_block, "Dummy block", "return first n rows", - c("dummy_block", "transform_block"), "data.frame", "data.frame", - "transform" + new_dummy_block, + "Dummy block", + "return first n rows", + category = "transform" ) expect_true("dummy_block" %in% list_blocks()) @@ -78,8 +79,7 @@ test_that("3rd party blocks can be registrerd (script)", { expect_warning( register_block( new_dummy_block, "dummy block", "return first n rows", - c("dummy_block", "transform_block"), "data.frame", "data.frame", - "transform" + category = "transform" ) ) diff --git a/tests/testthat/test-stack.R b/tests/testthat/test-stack.R index e730f5db..319efaa9 100644 --- a/tests/testthat/test-stack.R +++ b/tests/testthat/test-stack.R @@ -60,19 +60,37 @@ test_that("Get compatible blocks", { stack <- new_stack() res <- get_compatible_blocks(stack) # Might change if we add new data blocks - expect_equal(nrow(res), 3) - expect_identical(unique(res$category), "data") + expect_equal(length(res), 3) + expect_identical(unique(chr_ply(res, attr, "category")), "data") # Uncategorized block - attr(block_registry$dataset_block, "category") <- "uncategorized" + new_dummy_data_block <- function(...) { + new_block( + fields = list(), + expr = quote(data), + class = c("dummy_block", "dataset_block", "data_block"), + ... + ) + } + + register_block( + new_dummy_data_block, + "Dummy block", + "return first n rows" + ) + stack <- new_stack() res <- get_compatible_blocks(stack) - expect_equal(nrow(res), 3) - expect_contains(unique(res$category), c("data", "uncategorized")) + expect_equal(length(res), 4) + expect_contains( + unique(chr_ply(res, attr, "category")), + c("data", "uncategorized") + ) + unregister_blocks("dummy_block") stack <- new_stack(new_dataset_block()) res <- get_compatible_blocks(stack) - expect_identical(unique(res$category), "transform") + expect_identical(unique(chr_ply(res, attr, "category")), "transform") # Check for workspace and result block stack1 <- new_stack( @@ -83,8 +101,8 @@ test_that("Get compatible blocks", { stack2 <- new_stack() set_workspace(stack1 = stack1, stack2 = stack2) - expect_true("result_block" %in% get_compatible_blocks(stack2)$ctor) - expect_true("result_block" %in% get_compatible_blocks(stack1)$ctor) + expect_true("result_block" %in% names(get_compatible_blocks(stack2))) + expect_true("result_block" %in% names(get_compatible_blocks(stack1))) }) withr::local_package("shinytest2") diff --git a/tests/testthat/test-validate-block.R b/tests/testthat/test-validate-block.R index 8513dc8b..1a8ab128 100644 --- a/tests/testthat/test-validate-block.R +++ b/tests/testthat/test-validate-block.R @@ -14,7 +14,6 @@ testServer(module_server_test, { # Invalidate session$setInputs("columns" = "") session$flushReact() - expect_error(out_dat()) expect_false(is_valid$block) expect_identical(is_valid$message, "selected value(s) not among provided choices") expect_identical(is_valid$fields, "columns") diff --git a/vignettes/blockr-examples.Rmd b/vignettes/blockr-examples.Rmd index 2162b02c..740f1a92 100644 --- a/vignettes/blockr-examples.Rmd +++ b/vignettes/blockr-examples.Rmd @@ -96,7 +96,8 @@ filter_in_block <- function(data, ...) { dplyr::filter(!!dplyr::sym(.(column)) %in% vals[[1]]) }), ..., - class = c("filter_in_block", "transform_block", "submit_block") + class = c("filter_in_block", "transform_block"), + submit = NA ) } @@ -104,10 +105,7 @@ blockr::register_block( constructor = filter_in_block, name = "Filter in", description = "Filter on a vector", - category = "transform", - classes = c("filter_in_block", "transform_block"), - input = "data.frame", - output = "data.frame" + category = "transform" ) ``` @@ -168,24 +166,29 @@ new_prophet_forecast_block <- function(columns = character(), ...) { ) } +block_input_check.plot_block <- function(x, data, ...) { + + if (inherits(data, "data.frame")) { + return(invisible(NULL)) + } + + input_failure("Expecting data.frame input.") +} + +block_output_ptype.plot_block <- function(x, ...) ggplot() + # Register custom blocks register_block( new_stock_data_block, name = "Stock Data", description = "Fetch stock data", - category = "data", - classes = c("stock_data_block", "data_block"), - input = NA_character_, - output = "data.frame" + category = "data" ) register_block( new_prophet_forecast_block, name = "Prophet Forecast", description = "Forecast using Prophet", - category = "plot", - classes = c("prophet_forecast_block", "plot_block"), - input = "data.frame", - output = "plot" + category = "plot" ) # Create the stack @@ -282,18 +285,27 @@ new_cardinal02_block <- function(...) { expr = expr, fields = fields, ..., - class = c("cardinal02_block", "rtables_block", "submit_block") + class = c("cardinal02_block", "rtables_block"), + submit = NA ) } +block_input_check.rtables_block <- function(x, data, ...) { + + if (inherits(data, "data.frame")) { + return(invisible(NULL)) + } + + input_failure("Expecting data.frame input.") +} + +block_output_ptype.rtables_block <- function(x, ...) list() + register_block( new_cardinal02_block, "Cardinal 02", "A Cardinal 02 table", - category = "table", - input = "data.frame", - output = "list", - classes = c("cardinal02_block", "rtables_block", "submit_block") + category = "table" ) @@ -380,25 +392,30 @@ new_pollution_forecast_block <- function(columns = character(), ...) { ) } +block_input_check.plot_block <- function(x, data, ...) { + + if (inherits(data, "data.frame")) { + return(invisible(NULL)) + } + + input_failure("Expecting data.frame input.") +} + +block_output_ptype.plot_block <- function(x, ...) ggplot() + # Register custom blocks register_block( new_air_quality_block, name = "Air Quality Data", description = "Import air quality data", - category = "data", - classes = c("air_quality_block", "data_block"), - input = NA_character_, - output = "data.frame" + category = "data" ) register_block( new_pollution_forecast_block, name = "Pollution Forecast", description = "Forecast pollution levels", - category = "plot", - classes = c("pollution_forecast_block", "plot_block"), - input = "data.frame", - output = "plot" + category = "plot" ) # Create the stack @@ -560,24 +577,29 @@ new_causal_impact_block <- function(columns = character(), ...) { ) } +block_input_check.plot_block <- function(x, data, ...) { + + if (inherits(data, "data.frame")) { + return(invisible(NULL)) + } + + input_failure("Expecting data.frame input.") +} + +block_output_ptype.plot_block <- function(x, ...) ggplot() + # Register custom blocks register_block( new_marketing_data_block, name = "Marketing Data", description = "Load and prepare marketing data", - category = "data", - classes = c("marketing_data_block", "data_block"), - input = NA_character_, - output = "data.frame" + category = "data" ) register_block( new_causal_impact_block, name = "Causal Impact Analysis", description = "Perform Causal Impact analysis on marketing data", - category = "plot", - classes = c("causal_impact_block", "plot_block"), - input = "data.frame", - output = "plot" + category = "plot" ) # Create the stack diff --git a/vignettes/figures/cmdk-data.png b/vignettes/figures/cmdk-data.png new file mode 100644 index 00000000..761f325a Binary files /dev/null and b/vignettes/figures/cmdk-data.png differ diff --git a/vignettes/figures/cmdk-transform.png b/vignettes/figures/cmdk-transform.png new file mode 100644 index 00000000..138f5fe2 Binary files /dev/null and b/vignettes/figures/cmdk-transform.png differ diff --git a/vignettes/plot-block.Rmd b/vignettes/plot-block.Rmd index 26812058..3d2a515f 100644 --- a/vignettes/plot-block.Rmd +++ b/vignettes/plot-block.Rmd @@ -363,6 +363,33 @@ new_geompoint_block <- function(color = character(), shape = character(), ...) { Note the class `plot_layer_block`. This is necessary to invoke the corresponding `evaluate_block()` method (to use `+` instead of `%>%`). +### Notes about the registry + +As mentioned in the __registry__ [vignette](https://bristolmyerssquibb.github.io/blockr/articles/registry.html), to register the newly defined blocks, we have to define 2 methods that check the block __input__ and __output__ (it is no mandatory to register blocks but we highly advise you to do so if you want to benefit from the clever "add block" feature): + +```r +block_input_check.plot_block <- function(x, data, ...) { + + if (inherits(data, "data.frame")) { + return(invisible(NULL)) + } + + input_failure("Expecting data.frame input.") +} + +block_output_ptype.plot_block <- function(x, ...) ggplot() + +block_input_check.plot_layer_block <- function(x, data, ...) { + + if (inherits(data, "ggplot")) { + return(invisible(NULL)) + } + + input_failure("Expecting ggplot input.") +} + +block_output_ptype.plot_layer_block <- function(x, ...) ggplot() +``` ### Try it diff --git a/vignettes/registry.Rmd b/vignettes/registry.Rmd index 6ed103f0..a79fd7cb 100644 --- a/vignettes/registry.Rmd +++ b/vignettes/registry.Rmd @@ -58,11 +58,13 @@ mermaid(" blockr_echarts4r --> |register| registry subgraph registry[Registry] subgraph select_reg[Select block] + reg_id[ID: select_block] reg_name[Name: select block] reg_descr[Description: select columns in a table] reg_classes[Classes: select_block, tranform_block] reg_input[Input: data.frame] reg_output[Output: data.frame] + reg_category[Category: Transform blocks] reg_package[Package: blockr] end subgraph filter_reg[Filter block] @@ -88,42 +90,66 @@ ready to be queried by `available_blocks()`. A truncated output example below: ```r $dataset_block -function(...) { - ... +function(selected = character(), package = "datasets", ...) { + # ... TRUNCATED ... # } attr(,"name") [1] "data block" attr(,"description") [1] "Choose a dataset from a package" +attr(,"id") +[1] "dataset_block" attr(,"classes") -[1] "dataset_block" "data_block" +[1] "dataset_block" "data_block" "block" attr(,"input") -[1] NA +function(x, data, ...) { + + if (missing(data) || is.null(data)) { + return(invisible(NULL)) + } + + input_failure("No (or empty) input expected.") +} + attr(,"output") -[1] "data.frame" +data frame with 0 columns and 0 rows attr(,"package") [1] "blockr" +attr(,"category") +[1] "data" attr(,"class") [1] "block_descr" $select_block -function(data, ...) { - ... +function(columns = character(), ...) { + # ... TRUNCATED ... # } attr(,"name") [1] "select block" attr(,"description") [1] "select columns in a table" +attr(,"id") +[1] "select_block" attr(,"classes") -[1] "select_block" "transform_block" +[1] "select_block" "transform_block" "block" attr(,"input") -[1] "data.frame" +function(x, data, ...) { + + if (inherits(data, "data.frame")) { + return(invisible(NULL)) + } + + input_failure("Expecting data.frame input.") +} + attr(,"output") -[1] "data.frame" +data frame with 0 columns and 0 rows attr(,"package") [1] "blockr" +attr(,"category") +[1] "transform" attr(,"class") [1] "block_descr" ``` @@ -155,15 +181,39 @@ new_tail_block <- function(data, n_rows = numeric(), ...) { ... ) } +``` + +In addition to that, you also might have to specify what kind of __input__ and __output__ are accepted by that block. We designed 2 helpers, namely `block_input_check()` and `block_output_ptype()`. `{blockr}` already exposes methods like for `dataset_block` and `transform_block` so you don't need to create a new method for those classes. However, we see a practical example on how to create such methods for plots in this [vignette](https://bristolmyerssquibb.github.io/blockr/articles/plot-block.html). Below is what these methods look like, don't forget to export them if you are running inside a R package: + +```r +#' @export +block_input_check.transform_block <- function(x, data, ...) { + if (inherits(data, "data.frame")) { + return(invisible(NULL)) + } + + input_failure("Expecting data.frame input.") +} +#' @export +block_output_ptype.transform_block <- function(x, ...) data.frame() +``` + +If you forget to create a custom method while it is required, you get an error message as a reminder: + +```r +Error in `UseMethod()`: +! no applicable method for 'block_output_ptype' applied to an object of class "c('dummy_block', 'data_block', 'block')" +``` + +We can now register the new block, as follows: + +```{r, eval=TRUE} register_block( constructor = new_tail_block, name = "tail block", description = "return last n rows", - category = "transform", - classes = c("tail_block", "transform_block"), - input = "data.frame", - output = "data.frame" + category = "transform" ) ``` @@ -204,14 +254,6 @@ register_blocks( "summarize data groups" ), category = c("data", rep("transform", 3)), - classes = list( - c("dataset_block", "data_block"), - c("filter_block", "transform_block", "submit_block"), - c("select_block", "transform_block"), - c("summarize_block", "transform_block", "submit_block") - ), - input = c(NA_character_, "data.frame", "data.frame", "data.frame"), - output = c("data.frame", "data.frame", "data.frame", "data.frame"), package = "" ) ``` @@ -231,3 +273,109 @@ names(available_blocks()) where __ids__ is the first entry we applied in the class attributes when the block was created, that is `tail_block`. If you had to remove multiple blocks at once, you can pass a vector in __ids__. + + +## Leverage the registry API + +Having a registry is useful if you want to perform block __compatibility checks__. For instance, we use the registry in the `add_block_server.default` method, responsible for adding a block within a stack. Under the hood, given a stack, `get_compatible_blocks()` is able to return which __registered__ block is compatible with the last stack block, which substantially enhance the user experience: + +```{r} +stack <- new_stack() +res <- get_compatible_blocks(stack) +names(res) +``` + +If we now add a new `dataset_block` to the stack, the list of suggestion is updated: + +```{r} +stack <- stack |> add_block(available_blocks()[["dataset_block"]]) +res <- get_compatible_blocks(stack) +names(res) +``` + +Within `add_block_server.default`, we update the `shinyWidgets::virtualSelect` choices, being created with `get_compatible_blocks()`. If you wish to develop your own UI, this is totally fine. As an example, we could leverage a contextual menu React widget powered by the [scoutbar](https://www.scoutbar.co/) JS API, those R API is available at [`{scoutbaR}`](https://cynkra.github.io/scoutbaR/). On the UI side, we define the menu placeholder: + +```r +pak::pak("cynkra/scoutbaR") + +add_block_ui.custom <- function(x, id, ...) { + ns <- shiny::NS(id) + + scoutbaR::scoutbar( + ns("scoutbar"), + placeholder = c("Click on any block to add it to the current stack") + ) +} +``` + +On the server side, we update the menu for each new block added to the stack, through `vals$blocks`: + +```r +add_block_server.custom <- function(x, id, vals, ...) { + moduleServer(id, function(input, output, session) { + ns <- session$ns + + # Triggers on init + observeEvent(vals$blocks, + { + # Pills are dynamically updated from the server + # depending on the block compatibility + choices <- get_compatible_blocks(vals$stack) + cats <- unique(chr_ply(choices, \(b) attr(b, "category"))) + + # Create one page per block category + choices <- lapply(cats, \(cat) { + scoutbaR::scout_section( + label = cat, + .list = dropNulls( + unname(lapply(choices, \(choice) { + if (attr(choice, "category") == cat) { + scoutbaR::scout_action( + id = attr(choice, "id"), + label = paste0(attr(choice, "name"), " (", attr(choice, "package"), ")"), + description = attr(choice, "description") + ) + } + }) + )) + ) + }) + + # In this setup, the scoutbar will close after adding a block. + # It may be reopened by setting revealScoutbar = TRUE. + scoutbaR::update_scoutbar( + session, + "scoutbar", + configuration = list( + actions = choices + ) + ) + } + ) + + return( + list( + selected = reactive(input$scoutbar) + ) + ) + }) +} +``` + +Then, we run the stack, passing it a `custom` class so we can benefir from the new defined modules: + +```r +my_stack <- new_stack() +attr(my_stack, "class") <- c(attr(my_stack, "class"), "custom") +serve_stack(my_stack) +``` + +The contextual menu may be triggered with `cmd + K` on Mac or similar on Windows. + +```{r cmdk-empty, echo=FALSE, fig.cap='Scoutbar with empty stack', fig.align = 'center', out.width='50%'} +knitr::include_graphics("figures/cmdk-data.png") +``` + +```{r cmdk-tranform, echo=FALSE, fig.cap='Scoutbar with data block stack', fig.align = 'center', out.width='50%'} +knitr::include_graphics("figures/cmdk-transform.png") +```