mirror of
https://github.com/rstudio/shiny.git
synced 2026-04-29 03:00:45 -04:00
document similar things together; add prependTab and appendTab
This commit is contained in:
267
R/insert-tab.R
Normal file
267
R/insert-tab.R
Normal file
@@ -0,0 +1,267 @@
|
||||
|
||||
getIcon <- function(tab) {
|
||||
iconClass <- tab$attribs$`data-icon-class`
|
||||
if (!is.null(iconClass)) {
|
||||
# for font-awesome we specify fixed-width
|
||||
if (grepl("fa-", iconClass, fixed = TRUE))
|
||||
iconClass <- paste(iconClass, "fa-fw")
|
||||
icon(name = NULL, class = iconClass)
|
||||
} else NULL
|
||||
}
|
||||
|
||||
#' Dynamically insert/remove a tabPanel
|
||||
#'
|
||||
#' Dynamically insert or remove a \code{\link{tabPanel}} from an existing
|
||||
#' \code{\link{tabsetPanel}}, \code{\link{navlistPanel}} or
|
||||
#' \code{\link{navbarPage}}.
|
||||
#'
|
||||
#' When you want to insert a new tab before of after an existing tab, you
|
||||
#' should use \code{insertTab}. When you want to prepend a tab (i.e. add a
|
||||
#' tab to the beginning of the \code{tabsetPanel}), use \code{prependTab}.
|
||||
#' When you want to append a tab (i.e. add a tab to the end of the
|
||||
#' \code{tabsetPanel}), use \code{appendTab}.
|
||||
#'
|
||||
#' For \code{navbarPage}, you can insert/remove conventional
|
||||
#' \code{tabPanel}s (whether at the top level or nested inside a
|
||||
#' \code{navbarMenu}), as well as an entire \code{\link{navbarMenu}}.
|
||||
#' For the latter case, \code{target} should be the \code{menuName} that
|
||||
#' you gave your \code{navbarMenu} when you first created it (by default,
|
||||
#' this is equal to the value of the \code{title} argument).
|
||||
#'
|
||||
#' @param inputId The \code{id} of the \code{tabsetPanel} (or
|
||||
#' \code{navlistPanel} or \code{navbarPage})into which \code{tab} will
|
||||
#' be inserted/removed.
|
||||
#'
|
||||
#' @param tab The tab element to be added (must be created with
|
||||
#' \code{tabPanel}).
|
||||
#'
|
||||
#' @param target The \code{value} of an existing \code{tabPanel}, next to
|
||||
#' which \code{tab} will be added. If \code{NULL},the \code{tab} will be
|
||||
#' placed either as the first tab or the last tab, depending on the
|
||||
#' \code{position} argument.
|
||||
#'
|
||||
#' @param position Should \code{tab} be added before or after the
|
||||
#' \code{target} tab?
|
||||
#'
|
||||
#' @param session The shiny session within which to call \code{insertTab}.
|
||||
#'
|
||||
#' @seealso \code{\link{showTab}}
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run this example in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#' ui <- fluidPage(
|
||||
#' sidebarLayout(
|
||||
#' sidebarPanel(
|
||||
#' actionButton("add", "Add 'Dynamic' tab"),
|
||||
#' actionButton("remove", "Remove 'Foo' tab")
|
||||
#' ),
|
||||
#' mainPanel(
|
||||
#' tabsetPanel(id = "tabs",
|
||||
#' tabPanel("Hello", "This is the hello tab"),
|
||||
#' tabPanel("Foo", "This is the foo tab"),
|
||||
#' tabPanel("Bar", "This is the bar tab")
|
||||
#' )
|
||||
#' )
|
||||
#' )
|
||||
#' )
|
||||
#' server <- function(input, output, session) {
|
||||
#' observeEvent(input$add, {
|
||||
#' insertTab(inputId = "tabs",
|
||||
#' tabPanel("Dynamic", "This a dynamically-added tab"),
|
||||
#' target = "Bar"
|
||||
#' )
|
||||
#' })
|
||||
#' observeEvent(input$remove, {
|
||||
#' removeTab(inputId = "tabs", target = "Foo")
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#' @export
|
||||
insertTab <- function(inputId, tab, target,
|
||||
position = c("before", "after"),
|
||||
session = getDefaultReactiveDomain()) {
|
||||
|
||||
force(inputId)
|
||||
force(tab)
|
||||
force(target)
|
||||
position <- match.arg(position)
|
||||
force(session)
|
||||
|
||||
callback <- function() {
|
||||
session$sendInsertTab(
|
||||
inputId = inputId,
|
||||
tab = processDeps(tab, session),
|
||||
icon = processDeps(getIcon(tag), session),
|
||||
target = target,
|
||||
prepend = FALSE,
|
||||
append = FALSE,
|
||||
position = position)
|
||||
}
|
||||
|
||||
session$onFlushed(callback, once = TRUE)
|
||||
}
|
||||
|
||||
#' @rdname insertTab
|
||||
#' @export
|
||||
prependTab <- function(inputId, tab, menuName = NULL,
|
||||
session = getDefaultReactiveDomain()) {
|
||||
|
||||
force(inputId)
|
||||
force(tab)
|
||||
force(menuName)
|
||||
force(session)
|
||||
|
||||
callback <- function() {
|
||||
session$sendInsertTab(
|
||||
inputId = inputId,
|
||||
tab = processDeps(tab, session),
|
||||
icon = processDeps(getIcon(tag), session),
|
||||
target = NULL,
|
||||
prepend = TRUE,
|
||||
append = FALSE,
|
||||
position = NULL)
|
||||
}
|
||||
|
||||
session$onFlushed(callback, once = TRUE)
|
||||
}
|
||||
|
||||
#' @rdname insertTab
|
||||
#' @export
|
||||
appendTab <- function(inputId, tab, menuName = NULL,
|
||||
session = getDefaultReactiveDomain()) {
|
||||
|
||||
force(inputId)
|
||||
force(tab)
|
||||
force(menuName)
|
||||
force(session)
|
||||
|
||||
callback <- function() {
|
||||
session$sendInsertTab(
|
||||
inputId = inputId,
|
||||
tab = processDeps(tab, session),
|
||||
icon = processDeps(getIcon(tag), session),
|
||||
target = NULL,
|
||||
prepend = FALSE,
|
||||
append = TRUE,
|
||||
position = NULL)
|
||||
}
|
||||
|
||||
session$onFlushed(callback, once = TRUE)
|
||||
}
|
||||
|
||||
#' @rdname insertTab
|
||||
#' @export
|
||||
removeTab <- function(inputId, target, immediate = FALSE,
|
||||
session = getDefaultReactiveDomain()) {
|
||||
|
||||
force(inputId)
|
||||
force(target)
|
||||
force(session)
|
||||
|
||||
callback <- function() {
|
||||
session$sendRemoveTab(
|
||||
inputId = inputId,
|
||||
target = target)
|
||||
}
|
||||
|
||||
if (!immediate) session$onFlushed(callback, once = TRUE)
|
||||
else callback()
|
||||
}
|
||||
|
||||
|
||||
#' Dynamically hide/show a tabPanel
|
||||
#'
|
||||
#' Dynamically hide or show a \code{\link{tabPanel}} from an existing
|
||||
#' \code{\link{tabsetPanel}}, \code{\link{navlistPanel}} or
|
||||
#' \code{\link{navbarPage}}.
|
||||
#'
|
||||
#' For \code{navbarPage}, you can hide/show conventional
|
||||
#' \code{tabPanel}s (whether at the top level or nested inside a
|
||||
#' \code{navbarMenu}), as well as an entire \code{\link{navbarMenu}}.
|
||||
#' For the latter case, \code{target} should be the \code{menuName} that
|
||||
#' you gave your \code{navbarMenu} when you first created it (by default,
|
||||
#' this is equal to the value of the \code{title} argument).
|
||||
#'
|
||||
#' @param inputId The \code{id} of the \code{tabsetPanel} (or
|
||||
#' \code{navlistPanel} or \code{navbarPage})into which \code{tab} will
|
||||
#' be inserted/removed.
|
||||
#'
|
||||
#' @param target The \code{value} of the \code{tabPanel} to be
|
||||
#' hidden/shown. See Details if you want to hide/show an entire
|
||||
#' \code{navbarMenu} instead.
|
||||
#'
|
||||
#' @param session The shiny session within which to call this
|
||||
#' function.
|
||||
#'
|
||||
#' @seealso \code{\link{insertTab}}
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run this example in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#' ui <- fluidPage(
|
||||
#' sidebarLayout(
|
||||
#' sidebarPanel(actionButton("show", "Show tab")),
|
||||
#' mainPanel(
|
||||
#' tabsetPanel(id = "tabs",
|
||||
#' tabPanel("Hello", "This is the hello tab"),
|
||||
#' tabPanel("Foo", "This is the foo tab"),
|
||||
#' tabPanel("Bar", "This is the bar tab")
|
||||
#' )
|
||||
#' )
|
||||
#' )
|
||||
#' )
|
||||
#' server <- function(input, output, session) {
|
||||
#' # Hide tab as soon as app starts up
|
||||
#' hideTab(inputId = "tabs", target = "Foo")
|
||||
#'
|
||||
#' observeEvent(input$show, {
|
||||
#' showTab(inputId = "tabs", target = "Foo")
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#'
|
||||
#' # TODO: add example usage for `navbarMenu`
|
||||
#'
|
||||
#' @export
|
||||
showTab <- function(inputId, target,
|
||||
session = getDefaultReactiveDomain()) {
|
||||
|
||||
force(inputId)
|
||||
force(target)
|
||||
force(session)
|
||||
|
||||
callback <- function() {
|
||||
session$sendChangeTabVisibility(
|
||||
inputId = inputId,
|
||||
target = target,
|
||||
type = "show"
|
||||
)
|
||||
}
|
||||
|
||||
session$onFlushed(callback, once = TRUE)
|
||||
}
|
||||
|
||||
#' @rdname showTab
|
||||
#' @export
|
||||
hideTab <- function(inputId, target,
|
||||
session = getDefaultReactiveDomain()) {
|
||||
|
||||
force(inputId)
|
||||
force(target)
|
||||
force(session)
|
||||
|
||||
callback <- function() {
|
||||
session$sendChangeTabVisibility(
|
||||
inputId = inputId,
|
||||
target = target,
|
||||
type = "hide"
|
||||
)
|
||||
}
|
||||
|
||||
session$onFlushed(callback, once = TRUE)
|
||||
}
|
||||
271
R/insert-ui.R
271
R/insert-ui.R
@@ -173,274 +173,3 @@ removeUI <- function(selector,
|
||||
else callback()
|
||||
}
|
||||
|
||||
|
||||
#' Dynamically add a tabPanel
|
||||
#'
|
||||
#' Dynamically add a \code{tabPanel()} into an existing \code{tabsetPanel}.
|
||||
#'
|
||||
#' @param tabsetPanelId The \code{id} of the \code{tabsetPanel()} into which
|
||||
#' \code{tab} will be inserted.
|
||||
#'
|
||||
#' @param tab The tab element to be added (must be created with
|
||||
#' \code{tabPanel}).
|
||||
#'
|
||||
#' @param target The \code{value} of an existing \code{tabPanel()}, next to
|
||||
#' which \code{tab} will be added. If \code{NULL},the \code{tab} will be
|
||||
#' placed either as the first tab or the last tab, depending on the
|
||||
#' \code{position} argument.
|
||||
#'
|
||||
#' @param position Should \code{tab} be added to the right ot to the left
|
||||
#' of \code{target}?
|
||||
#'
|
||||
#' @param immediate Whether \code{tab} should be immediately inserted into
|
||||
#' the app when you call \code{insertTab}, or whether Shiny should wait until
|
||||
#' all outputs have been updated and all observers have been run (default).
|
||||
#'
|
||||
#' @param session The shiny session within which to call \code{insertTab}.
|
||||
#'
|
||||
#' @seealso \code{\link{removeTab}}, \code{\link{showTab}},
|
||||
#' \code{\link{hideTab}}
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run this example in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#' ui <- fluidPage(
|
||||
#' sidebarLayout(
|
||||
#' sidebarPanel(actionButton("add", "Add tab")),
|
||||
#' mainPanel(
|
||||
#' tabsetPanel(id = "tabs",
|
||||
#' tabPanel("Hello", "This is the hello tab"),
|
||||
#' tabPanel("Foo", "This is the foo tab"),
|
||||
#' tabPanel("Bar", "This is the bar tab")
|
||||
#' )
|
||||
#' )
|
||||
#' )
|
||||
#' )
|
||||
#' server <- function(input, output, session) {
|
||||
#' observeEvent(input$add, {
|
||||
#' insertTab(tabsetPanelId = "tabs",
|
||||
#' tabPanel("Dynamic", "This a dynamically-added tab"),
|
||||
#' target = "Bar"
|
||||
#' )
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#' @export
|
||||
insertTab <- function(tabsetPanelId, tab, target = NULL,
|
||||
position = c("right", "left"), immediate = FALSE,
|
||||
session = getDefaultReactiveDomain()) {
|
||||
|
||||
force(tabsetPanelId)
|
||||
force(tab)
|
||||
force(target)
|
||||
position <- match.arg(position)
|
||||
force(session)
|
||||
|
||||
iconClass <- tab$attribs$`data-icon-class`
|
||||
icon <- if (!is.null(iconClass)) {
|
||||
# for font-awesome we specify fixed-width
|
||||
if (grepl("fa-", iconClass, fixed = TRUE))
|
||||
iconClass <- paste(iconClass, "fa-fw")
|
||||
icon(name = NULL, class = iconClass)
|
||||
} else NULL
|
||||
|
||||
callback <- function() {
|
||||
session$sendInsertTab(
|
||||
tabsetPanelId = tabsetPanelId,
|
||||
tab = processDeps(tab, session),
|
||||
icon = processDeps(icon, session),
|
||||
target = target,
|
||||
position = position)
|
||||
}
|
||||
|
||||
if (!immediate) session$onFlushed(callback, once = TRUE)
|
||||
else callback()
|
||||
}
|
||||
|
||||
|
||||
#' Dynamically remove a tabPanel
|
||||
#'
|
||||
#' Dynamically remove a \code{tabPanel()} from an existing \code{tabsetPanel}.
|
||||
#'
|
||||
#' @param tabsetPanelId The \code{id} of the \code{tabsetPanel()} from which
|
||||
#' \code{target} will be removed.
|
||||
#'
|
||||
#' @param target The \code{value} of the \code{tabPanel()} to be removed.
|
||||
#'
|
||||
#' @param immediate Whether \code{tab} should be immediately removed from
|
||||
#' the app when you call \code{removeTab}, or whether Shiny should wait until
|
||||
#' all outputs have been updated and all observers have been run (default).
|
||||
#'
|
||||
#' @param session The shiny session within which to call \code{removeTab}.
|
||||
#'
|
||||
#' @seealso \code{\link{insertTab}}, \code{\link{showTab}},
|
||||
#' \code{\link{hideTab}}
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run this example in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#' ui <- fluidPage(
|
||||
#' sidebarLayout(
|
||||
#' sidebarPanel(actionButton("remove", "Remove tab")),
|
||||
#' mainPanel(
|
||||
#' tabsetPanel(id = "tabs",
|
||||
#' tabPanel("Hello", "This is the hello tab"),
|
||||
#' tabPanel("Foo", "This is the foo tab"),
|
||||
#' tabPanel("Bar", "This is the bar tab")
|
||||
#' )
|
||||
#' )
|
||||
#' )
|
||||
#' )
|
||||
#' server <- function(input, output, session) {
|
||||
#' observeEvent(input$remove, {
|
||||
#' removeTab(tabsetPanelId = "tabs", target = "Foo")
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#' @export
|
||||
removeTab <- function(tabsetPanelId, target, immediate = FALSE,
|
||||
session = getDefaultReactiveDomain()) {
|
||||
|
||||
force(tabsetPanelId)
|
||||
force(target)
|
||||
force(session)
|
||||
|
||||
callback <- function() {
|
||||
session$sendRemoveTab(
|
||||
tabsetPanelId = tabsetPanelId,
|
||||
target = target)
|
||||
}
|
||||
|
||||
if (!immediate) session$onFlushed(callback, once = TRUE)
|
||||
else callback()
|
||||
}
|
||||
|
||||
|
||||
#' Dynamically show a tabPanel
|
||||
#'
|
||||
#' Dynamically show (expose) a hidden \code{tabPanel()} from an existing
|
||||
#' \code{tabsetPanel}.
|
||||
#'
|
||||
#' @param tabsetPanelId The \code{id} of the \code{tabsetPanel()} that
|
||||
#' \code{target} belongs to.
|
||||
#'
|
||||
#' @param target The \code{value} of the \code{tabPanel()} to be shown.
|
||||
#'
|
||||
#' @param immediate Whether \code{tab} should be immediately shown from when
|
||||
#' you call \code{removeTab}, or whether Shiny should wait until all
|
||||
#' outputs have been updated and all observers have been run (default).
|
||||
#'
|
||||
#' @param session The shiny session within which to call \code{showTab}.
|
||||
#'
|
||||
#' @seealso \code{\link{insertTab}}, \code{\link{removeTab}},
|
||||
#' \code{\link{hideTab}}
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run this example in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#' ui <- fluidPage(
|
||||
#' sidebarLayout(
|
||||
#' sidebarPanel(actionButton("show", "Show tab")),
|
||||
#' mainPanel(
|
||||
#' tabsetPanel(id = "tabs",
|
||||
#' tabPanel("Hello", "This is the hello tab"),
|
||||
#' tabPanel("Foo", "This is the foo tab"),
|
||||
#' tabPanel("Bar", "This is the bar tab")
|
||||
#' )
|
||||
#' )
|
||||
#' )
|
||||
#' )
|
||||
#' server <- function(input, output, session) {
|
||||
#' # Hide tab as soon as app starts up
|
||||
#' hideTab(tabsetPanelId = "tabs", target = "Foo")
|
||||
#'
|
||||
#' observeEvent(input$show, {
|
||||
#' showTab(tabsetPanelId = "tabs", target = "Foo")
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#' @export
|
||||
showTab <- function(tabsetPanelId, target, immediate = FALSE,
|
||||
session = getDefaultReactiveDomain()) {
|
||||
|
||||
force(tabsetPanelId)
|
||||
force(target)
|
||||
force(session)
|
||||
|
||||
callback <- function() {
|
||||
session$sendShowTab(
|
||||
tabsetPanelId = tabsetPanelId,
|
||||
target = target)
|
||||
}
|
||||
|
||||
if (!immediate) session$onFlushed(callback, once = TRUE)
|
||||
else callback()
|
||||
}
|
||||
|
||||
|
||||
#' Dynamically hide a tabPanel
|
||||
#'
|
||||
#' Dynamically hide \code{tabPanel()} from an existing
|
||||
#' \code{tabsetPanel}.
|
||||
#'
|
||||
#' @param tabsetPanelId The \code{id} of the \code{tabsetPanel()} that
|
||||
#' \code{target} belongs to.
|
||||
#'
|
||||
#' @param target The \code{value} of the \code{tabPanel()} to be hidden.
|
||||
#'
|
||||
#' @param immediate Whether \code{tab} should be immediately shown from when
|
||||
#' you call \code{removeTab}, or whether Shiny should wait until all
|
||||
#' outputs have been updated and all observers have been run (default).
|
||||
#'
|
||||
#' @param session The shiny session within which to call \code{hideTab}.
|
||||
#'
|
||||
#' @seealso \code{\link{insertTab}}, \code{\link{removeTab}},
|
||||
#' \code{\link{showTab}}
|
||||
#'
|
||||
#' @examples
|
||||
#' ## Only run this example in interactive R sessions
|
||||
#' if (interactive()) {
|
||||
#' ui <- fluidPage(
|
||||
#' sidebarLayout(
|
||||
#' sidebarPanel(actionButton("hide", "Hide tab")),
|
||||
#' mainPanel(
|
||||
#' tabsetPanel(id = "tabs",
|
||||
#' tabPanel("Hello", "This is the hello tab"),
|
||||
#' tabPanel("Foo", "This is the foo tab"),
|
||||
#' tabPanel("Bar", "This is the bar tab")
|
||||
#' )
|
||||
#' )
|
||||
#' )
|
||||
#' )
|
||||
#' server <- function(input, output, session) {
|
||||
#' observeEvent(input$hide, {
|
||||
#' hideTab(tabsetPanelId = "tabs", target = "Foo")
|
||||
#' })
|
||||
#' }
|
||||
#'
|
||||
#' shinyApp(ui, server)
|
||||
#' }
|
||||
#' @export
|
||||
hideTab <- function(tabsetPanelId, target, immediate = FALSE,
|
||||
session = getDefaultReactiveDomain()) {
|
||||
|
||||
force(tabsetPanelId)
|
||||
force(target)
|
||||
force(session)
|
||||
|
||||
callback <- function() {
|
||||
session$sendHideTab(
|
||||
tabsetPanelId = tabsetPanelId,
|
||||
target = target)
|
||||
}
|
||||
|
||||
if (!immediate) session$onFlushed(callback, once = TRUE)
|
||||
else callback()
|
||||
}
|
||||
|
||||
|
||||
50
R/shiny.R
50
R/shiny.R
@@ -1492,38 +1492,42 @@ ShinySession <- R6Class(
|
||||
)
|
||||
)
|
||||
},
|
||||
sendInsertTab = function(tabsetPanelId, tab, icon, target, position) {
|
||||
private$sendMessage(
|
||||
`shiny-insert-tab` = list(
|
||||
tabsetPanelId = tabsetPanelId,
|
||||
tab = tab,
|
||||
icon = icon,
|
||||
target = target,
|
||||
position = position
|
||||
sendInsertTab = function(inputId, tab, icon, target,
|
||||
prepend, append, position) {
|
||||
|
||||
if (is.null(target)) {
|
||||
if (prepend == append) {
|
||||
stop("If target is NULL, either `prepend` or `append` must be TRUE.",
|
||||
"Both cannot be TRUE, however.")
|
||||
}
|
||||
}
|
||||
|
||||
private$sendMessage(
|
||||
`shiny-insert-tab` = list(
|
||||
inputId = inputId,
|
||||
tab = tab,
|
||||
icon = icon,
|
||||
target = target,
|
||||
prepend = prepend,
|
||||
append = append,
|
||||
position = position
|
||||
)
|
||||
)
|
||||
)
|
||||
},
|
||||
sendRemoveTab = function(tabsetPanelId, target) {
|
||||
sendRemoveTab = function(inputId, target) {
|
||||
private$sendMessage(
|
||||
`shiny-remove-tab` = list(
|
||||
tabsetPanelId = tabsetPanelId,
|
||||
inputId = inputId,
|
||||
target = target
|
||||
)
|
||||
)
|
||||
},
|
||||
sendShowTab = function(tabsetPanelId, target) {
|
||||
sendChangeTabVisibility = function(inputId, target, type) {
|
||||
private$sendMessage(
|
||||
`shiny-show-tab` = list(
|
||||
tabsetPanelId = tabsetPanelId,
|
||||
target = target
|
||||
)
|
||||
)
|
||||
},
|
||||
sendHideTab = function(tabsetPanelId, target) {
|
||||
private$sendMessage(
|
||||
`shiny-hide-tab` = list(
|
||||
tabsetPanelId = tabsetPanelId,
|
||||
target = target
|
||||
`shiny-change-tab-visibility` = list(
|
||||
inputId = inputId,
|
||||
target = target,
|
||||
type = type
|
||||
)
|
||||
)
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user