--- title: "The link menu" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{The link menu} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include=FALSE} knitr::opts_chunk$set(eval = FALSE, comment = "#>") ``` `blockr.ui::link_menu_ui()` is a **bidirectional** card-list link picker. It lives inside the [sidebar primitive](sidebar.html) and replaces the legacy selectize-style chooser used by `blockr.dock`'s add-link flow. Each card represents a board *instance* that the new link can attach to, and the menu computes eligibility against a fixed *anchor* block (the right-clicked block in the dock flow). ## Two directions, one anchor `link_menu_ui(id, board, anchor)` renders up to two category-grouped sections relative to `anchor`. When both render, INPUT FROM appears above OUTPUT TO so the order matches the natural left-to-right data flow (sources feed the anchor; the anchor feeds targets): * **INPUT FROM** (INCOMING, card -> anchor): blocks that can link *into* the anchor. Only rendered when the anchor itself has at least one free named input port or is variadic. Eligibility = every other board block, excluding any that the anchor already reaches (would close a cycle). * **OUTPUT TO** (OUTGOING, anchor -> card): blocks the anchor can link *to*. Eligibility = every other board block with at least one free named input port or variadic arity, excluding any that already reach the anchor (would close a cycle). If both pools are empty the panel renders an in-place empty-state instead of returning `NULL`. This means right-clicking a downstream block with free inputs no longer surfaces the legacy "No inputs are currently available" warning - the menu opens with its INCOMING section populated. ## Commit contract The JS binding publishes `list(source, target, link_id, block_input, nonce)` on `input$commit`. `source` / `target` are derived from the clicked card's `data-direction` and the panel's `data-anchor`: * `data-direction = "outgoing"` -> `source = anchor`, `target = card` * `data-direction = "incoming"` -> `source = card`, `target = anchor` `link_menu_server(id)` strips the internal `nonce` and returns a `reactive` whose value is the spec. The consumer doesn't have to know the orientation up front - the spec carries both ends explicitly. ## Multi-link sessions Single click commits one link, but the menu is designed for repeated adds: the consumer pushes a typed `pool-update` payload to the open panel via `session$sendInputMessage()` after each commit, and the binding's `receiveMessage` handler hides the just-wired card client-side. Card-expansion state, scroll position, and the search filter value are preserved. If both pools drain to zero, the consumer calls `hide_sidebar()` and the panel closes itself. `link_eligible_pools(board, anchor)` is exported so the consumer recomputes the post-commit pool against the same eligibility logic the menu uses for its initial render - one source of truth shared across both call sites. ## Minimal example ```{r} library(shiny) library(blockr.ui) library(blockr.core) board <- new_board( blocks = as_blocks(list( a = new_dataset_block(), h = new_head_block(), m = new_merge_block() )) ) ui <- fluidPage( sidebar_ui("panel", side = "right", width = "420px"), actionButton("open", "Connect a"), verbatimTextOutput("spec") ) server <- function(input, output, session) { committed <- link_menu_server("menu") observeEvent(input$open, { show_sidebar( "panel", title = "Connect a", ui = link_menu_ui(session$ns("menu"), board, anchor = "a") ) }) output$spec <- renderPrint(committed()) } shinyApp(ui, server) ``` A runnable copy is shipped at: ```{r} system.file("examples", "link-menu", "app.R", package = "blockr.ui") ``` The bundled example exercises all four anchor cases (source-only, arity-1 target, arity-N target, variadic) and demonstrates the live `pool-update` flow.