---
title: "4. Extend blockr"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{4. Extend blockr}
  %\VignetteEngine{quarto::html}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)
```

```{r packages, message=FALSE, warning=FALSE}
library(shiny)
library(bslib)
library(scoutbaR)
library(blockr.core)
```

```{r custom-plugin-setup, eval = FALSE, echo=FALSE}
chr_ply <- function(x, fun, ..., length = 1L, use_names = FALSE) {
  vapply(x, fun, character(length), ..., USE.NAMES = use_names)
}

#' @keywords internal
lgl_ply <- function(x, fun, ..., length = 1L, use_names = FALSE) {
  vapply(x, fun, logical(length), ..., USE.NAMES = use_names)
}

dropNulls <- function(x) {
  x[!lgl_ply(x, is.null)]
}

blk_icon <- function(category) {
  switch(
    category,
    "data" = "table",
    "file" = "file-import",
    "parse" = "cogs",
    "plot" = "chart-line",
    "transform" = "wand-magic-sparkles",
    "table" = "table"
  )
}

blk_choices <- function() {
  blk_cats <- sort(
    unique(chr_ply(available_blocks(), \(b) attr(b, "category")))
  )

  lapply(blk_cats, \(cat) {
    scout_section(
      label = cat,
      .list = dropNulls(
        unname(
          lapply(available_blocks(), \(choice) {
            if (attr(choice, "category") == cat) {
              scout_action(
                id = attr(choice, "classes")[1],
                label = attr(choice, "name"),
                description = attr(choice, "description"),
                icon = blk_icon(cat)
              )
            }
          })
        )
      )
    )
  })
}
```

## Introduction

In the `Create block` [vignette](https://bristolmyerssquibb.github.io/blockr.core/articles/create-block.html),
you were taught how to design new blocks for blockr. Did you know that we could go much further?

Each major `blockr.core` feature belongs to its own __plugin__, materialized as a shiny module:

- Manage blocks (create/remove, append, ...)
- Manage __links__, that is how blocks are connected. Linking block A to block B means that block A passes its output
data to block B.
- Manage __stacks__ (group blocks together).
- Preserve the __board__ state: save and restore the application state.
- ...

All of the above is fully customizable by yourself, `blockr.core` only provides reasonable defaults to get you started.
`blockr.ui` is an example of full customization.

```{mermaid}
flowchart TD
  subgraph board[board]
    subgraph plugins[plugins]
      subgraph manage_blocks[Manage blocks]
      end
      subgraph manage_links[Manage links]
      end
      subgraph manage_stacks[Manage stacks]
      end
      subgraph preserve_board[Preserve board]
      end
      subgraph generate_code[Generate code]
      end
      subgraph notify_user[Notify user]
      end
      subgraph edit_block[Edit block]
      end
      subgraph edit_stack[Edit stack]
      end
    end
  end
```

## blockr plugins

### Background

__plugins__ are used to customize/enhance UX aspects of the __board__ module, that is the top level module exposed by `blockr.core`.
As stated above, there are a couple of plugins already available in the core, such that when you want to create a custom blockr app,
you can do this on the UI side:

```{r, eval=FALSE}
main_ui <- function(id, board) {
  ns <- NS(id)
  board_ui(
    ns("board"),
    board,
    plugins = board_plugins(
      board,
      c(
        "preserve_board",
        "manage_blocks",
        "manage_links",
        "manage_stacks",
        "generate_code",
        "notify_user"
      )
    )
  )
}
```

`board_ui()` expects the namespace of the module, a __board__ object which you can create with `new_board`. The board is, in general, passed
when you call `serve` on the board object such that you can start an app with predefined blocks, links and stacks.
where `board_plugins()` expect a vector of plugin names. It is important to state that at the moment, you can only overwrite existing plugins
but not create new ones. On the server side, you call `board_server()`, the server counter part of `board_ui()` which expects a namespace, the board object
and a subset (or all) of plugins. `callbacks` are to inject code directly into the board server function, as opposed to plugins which are nested submodules.
`parent` is used to __communicate__ application state between all parts of the application in a standardized way.

```{r, eval=FALSE}
main_server <- function(id, board) {
  moduleServer(
    id,
    function(input, output, session) {
      ns <- session$n

      app_state <- reactiveValues(
        # App state for module communication
      )

      # Board module
      board_server(
        "board",
        board,
        plugins = board_plugins(
          board,
          c(
            "preserve_board",
            "manage_blocks",
            "manage_links",
            "manage_stacks",
            "generate_code",
            "notify_user"
          )
        ),
        callbacks = list(),
        parent = app_state
      )
    }
  )
}
```

Looking at the `board_plugins.board()` function:

```{r, eval=FALSE}
board_plugins.board <- function(x, which = NULL) {

  plugins <- plugins(
    preserve_board(),
    manage_blocks(),
    manage_links(),
    manage_stacks(),
    notify_user(),
    generate_code(),
    edit_block(),
    edit_stack()
  )

  if (is.null(which)) {
    return(plugins)
  }

  plugins[which]
}
```

Each plugin is composed of a __server__ and __ui__ part, since they are modules. For instance, the `manage_blocks` plugin is defined as:

```{r, eval=FALSE}
manage_blocks <- function(server, ui) {
  new_plugin(server, ui, validator = expect_null, class = "manage_blocks")
}
```

In the following, we want to create a custom `manage_blocks` plugin that uses the `scoutbaR` package, described in [vignette](https://bristolmyerssquibb.github.io/blockr.core/articles/blocks-registry.html)

### A custom manage_blocks

To create our custom manage blocks, we'll first need to overwrite the `add_rm_block_server` and `add_rm_block_ui` functions. 
For sake of simplicity, on the UI side, we provide a `Add` block button as well as as scoutbar widget (`blk_choices()` is described in the following [vignette](https://bristolmyerssquibb.github.io/blockr.core/articles/blocks-registry.html)):

```{r custom-plugin-ui, eval=FALSE}
add_rm_block_ui <- function(id, board) {
  tagList(
    scoutbar(
      NS(id, "scoutbar"),
      placeholder = "Search for a block",
      actions = blk_choices(),
      theme = "dark",
      showRecentSearch = TRUE
    ),
    actionButton(
      NS(id, "add_block"),
      "New block",
      icon = icon("circle-plus"),
    )
  )
}
```

On the server part, a plugin is always defined as follows (documentation has been left for reference):

```{r, eval=FALSE}
#' Add/remove block module
#'
#' Customizable logic for adding/removing blocks to the board.
#'
#' @param id Namespace ID
#' @param board Reactive values object
#' @param update Reactive value object to initiate board updates
#' @param ... Extra arguments passed from parent scope
#'
#' @return A [shiny::reactiveValues()] object with components `add` and `rm`,
#' where `add` may be `NULL` or a `block` object and `rm` be `NULL` or a string
#' (block ID).
#'
#' @rdname add_rm_block
#' @export
add_rm_block_server <- function(id, board, update, ...) {
  moduleServer(
    id,
    function(input, output, session) {
      # SERVER LOGIC

      NULL
    }
  )
}
```

The server function __signature__ must start with the module id, `board` refers to internal reactive values (read-only), `update` is a reactive value
to send updates to the board module and `...` is used to recover parameters passed from the top level like `parent`. The plugin always returns `NULL`.

We now want to open the `scoutbaR` widget whenever the users clicks on the `Add block` button. We can achieve that by calling `update_scoutbar` passing `revealScoutbar = TRUE`.

```{r, eval=FALSE}
add_rm_block_server <- function(id, board, update, ...) {
  moduleServer(
    id,
    function(input, output, session) {
      # Trigger add block
      observeEvent(
        input$add_block,
        {
          update_scoutbar(
            session,
            "scoutbar",
            revealScoutbar = TRUE
          )
        }
      )

      NULL
    }
  )
}
```

Next step is to manage the user choice, that is when a scoutbar action is selected. We listen to `input$scoutbar` which holds the name of the selected block. Since it is a string, we call `create_block()`, which instantiates a block from its name, and wrap it by `as_blocks()`. Finally, we signal this change to the board by refreshing the `update` reactive value, saying we want to add a new block `list(blocks = list(add = new_blk))`:

```{r custom-plugin-server, eval=FALSE}
add_rm_block_server <- function(id, board, update, ...) {
  moduleServer(
    id,
    function(input, output, session) {
      # Trigger add block
      observeEvent(
        input$add_block,
        {
          update_scoutbar(
            session,
            "scoutbar",
            revealScoutbar = TRUE
          )
        }
      )

      observeEvent(input$scoutbar, {
        new_blk <- as_blocks(create_block(input$scoutbar))
        update(
          list(blocks = list(add = new_blk))
        )
      })

      NULL
    }
  )
}
```

### Register plugins

To register our new plugin, we can defined a custom `board_plugins()` function that calls our own plugin for `manage_blocks()`. For sake of simplicity, all other plugins are omitted:

```{r custom-plugin-helpers, eval=FALSE}
custom_board_plugins <- function(which = NULL) {
  plugins <- plugins(
    manage_blocks(server = add_rm_block_server, ui = add_rm_block_ui)
  )

  if (is.null(which)) {
    return(plugins)
  }

  plugins[which]
}
```

### Testing the new plugin

In the below example, you may click on the `Add block` button and see the scoutbar opening and then select a block.

```{r custom-plugin-app, eval=FALSE}
#| code-fold: true
main_ui <- function(id, board) {
  ns <- NS(id)
  board_ui(
    ns("board"),
    board,
    plugins = custom_board_plugins(
      c(
        "manage_blocks"
      )
    )
  )
}

main_server <- function(id, board) {
  moduleServer(
    id,
    function(input, output, session) {
      ns <- session$n

      # Board module
      board_server(
        "board",
        board,
        plugins = custom_board_plugins(
          c(
            "manage_blocks"
          )
        ),
        callbacks = list()
      )
    }
  )
}

board <- new_board()

ui <- page_fluid(
  main_ui("app", board)
)

server <- function(input, output, session) {
  main_server("app", board)
}

shinyApp(ui, server)
```

::: {.callout-note}
The demo below runs with shinylive. Not all feature may work as expected due to compatibility issues with webR.
:::

```{r shinylive_url, echo = FALSE, results = 'asis'}
# extract the code from knitr code chunks by ID
code <- paste0(
  c(
    "webr::install(\"blockr.core\", repos = \"https://cynkra.github.io/blockr.webR/\")",
    knitr::knit_code$get("packages"),
    knitr::knit_code$get("custom-plugin-setup"),
    knitr::knit_code$get("custom-plugin-ui"),
    knitr::knit_code$get("custom-plugin-server"),
    knitr::knit_code$get("custom-plugin-helpers"),
    knitr::knit_code$get("custom-plugin-app")
  ),
  collapse = "\n"
)

url <- roxy.shinylive::create_shinylive_url(code, header = FALSE)
```

```{r shinylive_iframe, echo = FALSE, eval = TRUE}
shiny::tags$iframe(
  class = "border border-5 rounded shadow-lg",
  src = url,
  style = "zoom: 0.75;",
  width = "100%",
  height = "1100px"
)
```

## Custom UI components

If you'd like to use the board with another UI kit than `bslib` you can create a new method for `board_ui()`. For that,
you'll need a little bit of S3 [knowledge](https://adv-r.hadley.nz/s3.html).

The function signature should contain `id` (module namespace), `x` (board object), and `plugins` to use `blockr.core` plugins.
In the following, we leverage the brand new `shinyNextUI` to power the custom board UI:

```{r custom-board-ui, eval=FALSE}
board_ui.custom_board <- function(id, x, plugins = list(), ...) {
  plugins <- as_plugins(plugins)
  div(
    id = paste0(id, "_board"),
    board_ui(id, plugins[["manage_blocks"]], x),
    div(
      id =  paste0(id, "_blocks"),
      block_ui(id, x)
    )
  )
}
```

We have to customize the `block_ui` too. Overall, we leverage the `shinyNextUI::card` component to create the block layout:

```{r custom-block-ui, eval=FALSE}
get_block_registry <- function(x) {
  stopifnot(is_block(x))
  available_blocks()[[strsplit(attr(x, "ctor"), "new_")[[1]][2]]]
}

block_ui.custom_board <- function(id, x, blocks = NULL, ...) {
  block_card <- function(x, id, ns) {
    id <- paste0("block_", id)

    blk_info <- get_block_registry(x)

    div(
      class = "m-2",
      id = ns(id),
      shinyNextUI::card(
        variant = "bordered",
        shinyNextUI::card_header(
          className = "d-flex justify-content-between",
          icon(blk_icon(attr(blk_info, "category"))),
          sprintf(
            "Block: %s (id: %s)",
            attr(blk_info, "name"),
            gsub("block_", "", id)
          ),
          shinyNextUI::tooltip(
            icon("info-circle"),
            content = tagList(
              p(
              icon("lightbulb"),
              "How to use this block?",
              ),
              p(attr(blk_info, "description"), ".")
            )
          )
        ),
        shinyNextUI::divider(),
        shinyNextUI::card_body(
          expr_ui(ns(id), x),
          block_ui(ns(id), x)
        ),
        shinyNextUI::divider(),
        shinyNextUI::card_footer(
          sprintf(
            "Type: %s; Package: %s",
            attr(blk_info, "category"),
            attr(blk_info, "package")
          )
        )
      )
    )
  }

  stopifnot(is.character(id) && length(id) == 1L)

  if (is.null(blocks)) {
    blocks <- board_blocks(x)
  } else if (is.character(blocks)) {
    blocks <- board_blocks(x)[blocks]
  }

  stopifnot(is_blocks(blocks))

  tagList(
    Map(
      block_card,
      blocks,
      names(blocks),
      MoreArgs = list(ns = NS(id)),
      USE.NAMES = FALSE
    )
  )
}
```

Notice the use of few `blockr.core` helpers along the way:

- `board_blocks()` to extract and validate the blocks of a board.
- `is_blocks()` check whether an object correspond to a list of blocks.
- `get_block_registry()` to get the current block metadata from the [registry](https://bristolmyerssquibb.github.io/blockr.core/articles/blocks-registry.html).

`add_rm_block_ui()` now leverages `shinyNextUI::actionButton`:

```{r custom-plugin-ui-nextui, eval=FALSE}
add_rm_block_ui <- function(id, board) {
  tagList(
    scoutbar(
      NS(id, "scoutbar"),
      placeholder = "Search for a block",
      actions = blk_choices(),
      theme = "dark",
      showRecentSearch = TRUE
    ),
    shinyNextUI::actionButton(
      NS(id, "add_block"),
      "New block",
      icon = icon("circle-plus"),
    )
  )
}
```

Since `blockr.core` blocks utilizes `shiny`/`bslib` UI, you'd also have to rewrite the UI and/or server part whenever necessary. This [vignette](https://bristolmyerssquibb.github.io/blockr.core/articles/create-block.html) provides a starting point to authoring blocks.

As a final step, when you call `new_board()` don't forget to add it the `custom_board` class so that the custom S3 methods are invoked.

```{r custom-ui-app, eval=FALSE}
#| code-fold: true
board <- new_board(class = "custom_board")

ui <- nextui_page(
  board_ui(
    "board",
    board,
    plugins = custom_board_plugins(
      c(
        "manage_blocks"
      )
    )
  ) 
)

server <- function(input, output, session) {
  board_server(
    "board",
    board,
    plugins = custom_board_plugins(
      c(
        "manage_blocks"
      )
    ),
    callbacks = list()
  )
}

shinyApp(ui, server)
```

::: {.callout-note}
The demo below runs with shinylive. Not all feature may work as expected due to compatibility issues with webR.
:::

```{r shinylive2_url, echo = FALSE, results = 'asis'}
# extract the code from knitr code chunks by ID
code <- paste0(
  c(
    "webr::install(\"blockr.core\", repos = \"https://cynkra.github.io/blockr.webR/\")",
    "library(shiny)",
    "library(scoutbaR)",
    "library(blockr.core)",
    "library(shinyNextUI)",
    knitr::knit_code$get("custom-plugin-setup"),
    knitr::knit_code$get("custom-plugin-ui-nextui"),
    knitr::knit_code$get("custom-plugin-server"),
    knitr::knit_code$get("custom-plugin-helpers"),
    knitr::knit_code$get("custom-block-ui"),
    knitr::knit_code$get("custom-board-ui"),
    knitr::knit_code$get("custom-ui-app")
  ),
  collapse = "\n"
)

url <- roxy.shinylive::create_shinylive_url(code, header = FALSE)
```

```{r shinylive2_iframe, echo = FALSE, eval = TRUE}
shiny::tags$iframe(
  class = "border border-5 rounded shadow-lg",
  src = url,
  style = "zoom: 0.75;",
  width = "100%",
  height = "1100px"
)
```

## Customize board options

TBD
