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:
- Step 1: Artwork Details - Required fields (name, collection, files)
- Step 2: Optional Details - Additional variants and artwork story
Modal Structure
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:
- New collection buttons - Disabled in demo mode
- Submit button - Disabled in demo mode
- 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 stepStatus Badges
status_badge(text, status = "primary", title = NULL)
# Returns Bootstrap badge with specified status color
status_class(status)
# Maps status names to Bootstrap classesButton Factory
button_factory(inputId, icon, color, isDisabled, disableRipple)
# Creates action buttons for the artwork tableState 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:
-
Add UI element in
new_art_modal_ui() -
Add validation rule in
validator_step_1()orvalidator_step_2() - Pass to pipeline in the submit observer
-
Update pipeline in
run_pipeline()if needed
Related Documentation
- Get Started - Module overview
- Validation Framework - Input validation
- Processing Pipeline - Async processing
