Skip to contents

Overview

This developer guide explains the multi-step wizard UI pattern used in modUpload for guided artwork submission. Understanding this workflow is essential for maintaining, debugging, or extending the upload experience.

Wizard Architecture

The wizard is implemented as a tabbed modal dialog with two steps:

  1. Step 1: Artwork Details - Required fields (name, collection, files)
  2. Step 2: Optional Details - Additional variants and artwork story
new_art_modal_ui <- function(id) {
  ns <- shiny::NS(id)
  
  shiny::modalDialog(
    title = "Add To Collection",
    size = "xl",
    easyClose = FALSE,
    
    shiny::tabsetPanel(
      id = ns("wizard_tabs"),
      type = "tabs",
      
      # Step 1: Required
      shiny::tabPanel("Step 1: Artwork Details", ...),
      
      # Step 2: Optional  
      shiny::tabPanel("Step 2: Optional Details", ...)
    ),
    
    footer = shiny::tagList(
      shiny::actionButton(ns("btn_cancel"), "Cancel"),
      shiny::actionButton(ns("btn_submit"), "Submit")
    )
  )
}

User Experience Flow

Click "Add" button
        |
Modal opens on Step 1
        |
User fills required fields:
  - Artwork Name
  - Collection (select or create new)
  - Artwork Image (PNG)
  - Canvas File (.procreate)
  - Timelapse Video (MP4)
  - Canvas Stats (image)
        |
User navigates to Step 2 (optional)
  - Additional Variants (multiple PNGs)
  - Artwork Story (text)
        |
Click Submit
        |
Validate Step 1 fields
        |
Pass? -> Close modal, start pipeline
Fail? -> Show validation errors

Form Layout

Step 1 uses a two-column layout:

Left Column Right Column
Artwork Name Artwork Image
Collection picker Canvas File
Create new collection Timelapse Video
Canvas Stats

This layout ensures file inputs don’t crowd the text inputs.

Collection Management

Users can select an existing collection or create a new one:

# In server
handle_new_collect <- function(new_collection) {
  # Security: Block in demo mode
  if (artcore::is_demo()) {
    shinyalert::shinyalert(
      title = "Demo Mode",
      text = "Creating collections is disabled in demo mode.",
      type = "warning"
    )
    return(invisible(NULL))
  }
  
  # Check if collection already exists
  if (!new_collection %in% names(r_collections())) {
    # Create new collection
    cid <- artutils::add_collection(r_artist(), new_collection)
    
    # Update reactive collections
    r$appdata$artist$collections <- c(collects, setNames(list(cid), new_collection))
    
    # Update picker input
    shinyWidgets::updatePickerInput(
      session = session,
      inputId = "art_collection",
      choices = r$appdata$artist$collections,
      selected = cid
    )
  }
}

Demo Mode Handling

The wizard respects demo mode throughout:

  1. New collection buttons - Disabled in demo mode
  2. Submit button - Disabled in demo mode
  3. Server validation - Blocks submission in demo mode
# In UI - conditional rendering
if (artcore::is_demo()) {
  shiny::tags$button(
    class = "btn btn-primary",
    disabled = "disabled",
    title = "Submission disabled in demo mode",
    "Submit (Demo Mode)"
  )
} else {
  shiny::actionButton(
    inputId = ns("btn_submit"),
    label = "Submit",
    class = "btn btn-primary"
  )
}

Wizard Helper Functions

modUpload exports several UI helpers for the wizard:

Step Labels

bslab_step_required(step)
# Returns badge: "Required" or "Optional"

bxlab_step_number(step)
# Returns badge: "Step 1" or "Step 2"

Progress Bar

bxpbar_step_progress(step)
# Returns animated Bootstrap progress bar showing current step

Status Badges

status_badge(text, status = "primary", title = NULL)
# Returns Bootstrap badge with specified status color

status_class(status)
# Maps status names to Bootstrap classes

Button Factory

button_factory(inputId, icon, color, isDisabled, disableRipple)
# Creates action buttons for the artwork table

State Management

The wizard tracks several reactive values:

# Upload completion flag
r_UPLOAD_COMPLETE <- shiny::reactiveVal(FALSE)

# Current artwork being uploaded
r_art_name <- shiny::reactiveVal()

# Artwork data table
r_artDT <- shiny::reactive(r$appdata$artist$artDT)

# Collections
r_collections <- shiny::reactive(r$appdata$artist$collections)

Button State Management

The “Add” button state changes during workflow:

# Set to busy state
add_busy <- function(session) {
  shinyjs::disable("btn_add")
  shiny::updateActionButton(
    session = session,
    inputId = "btn_add",
    label = "Adding...",
    icon = shiny::icon("spinner", class = "fa-spin")
  )
}

# Reset to ready state
add_ready <- function(session) {
  shiny::updateActionButton(
    session = session,
    inputId = "btn_add",
    label = "Add",
    icon = shiny::icon("plus")
  )
  shinyjs::enable("btn_add")
}

Observer Cleanup

The module cleans up observers on session end to prevent memory leaks:

session$onSessionEnded(function() {
  # Clean up edit observers (may already be destroyed, errors silenced)
  for (name in ls(edit_observers)) {
    try(edit_observers[[name]]$destroy(), silent = TRUE)
  }
  rm(list = ls(edit_observers), envir = edit_observers)

  # Clean up pipeline observers (may already be destroyed, errors silenced)
  for (uuid in ls(pipeline_observers)) {
    try(pipeline_observers[[uuid]]$view$destroy(), silent = TRUE)
    try(pipeline_observers[[uuid]]$done$destroy(), silent = TRUE)
  }
  rm(list = ls(pipeline_observers), envir = pipeline_observers)
})

Extending the Wizard

When adding new fields to the wizard:

  1. Add UI element in new_art_modal_ui()
  2. Add validation rule in validator_step_1() or validator_step_2()
  3. Pass to pipeline in the submit observer
  4. Update pipeline in run_pipeline() if needed