ID des input
Pensez à nommer les ID de vos inputs de cette façon : ns(le_nom_de_mon_input_%widget_id%)
.
L'ID est contenu dans la fonction ns()
et qu'il contient la balise %widget_id%
en suffixe, qui sera remplacée par l'ID du widget, ce qui permet d'assurer le caractère unique de chaque éléménet de l'UI, sans quoi la page ne s'afficherait pas.
Création d'un plugin avancé
Template de développement
La création de plugins peuvent rapidement s’avérer complexe, en étant composé de nombreux scripts.
C’est pourquoi InterHop a créé un plugin servant de template de développement, que vous pouvez télécharger sur le catalogue de contenus d’InterHop, dans la rubrique “Plugins”.
Une fois le template téléchargé, créez un nouveau plugin à partir du template.
Ceci va créer un nouveau plugin en copiant l’ensemble des fichiers du template.
Sélectionnez ce nouveau plugin puis rendez-vous sur l’onglet “Code”. Vous devriez avoir ces scripts.
Nous retrouvons donc les trois fichiers de base composant n’importe quel plugin (qu’il n’est d’ailleurs pas possible de renommer ou supprimer) : ui.R
, server.R
et translations.csv
.
Nous avons en plus de ceux-ci des fichiers préfixés par ui_*
et server_*
. Ces scripts permettent de fragmenter le code exécuté dans ui.R
et server.R
. Ils sont chacun importés avec la balise %import_script()%
.
Lorsque le plugin sera exécuté, ne seront exécutés que les trois fichiers de base. Ces fichiers doivent donc appeler les autres fichiers composant le plugin.
Voici une partie du fichier ui.R
.
%import_script('ui_load_general_settings.R')%
tagList(
# Code retiré pour l'exemple...
shinyjs::hidden(
div(
id = ns("general_settings_div_%widget_id%"),
div(
id = ns("general_settings_sidenav_%widget_id%"),
save_general_settings_button,
class = "widget_icon",
style = "border-right: solid grey 0.5px; width: 25px;"
),
%import_script('ui_general_settings.R')%,
style = "display: flex; height: calc(100% - 40px);"
)
),
shinyjs::hidden(
div(
id = ns("settings_files_div_%widget_id%"),
%import_script('ui_settings_files.R')%,
style = "display: flex; height: calc(100% - 40px);"
)
)
)
Nous voyons ici que nous utilisons plusieurs fois la balise %import_script()%
pour intégrer les autres scripts de l’UI. La syntaxe est simple, il suffit de mettre le nom du script avec son extension à l’intérieur d’apostrophes, tel que %import_script('ui_load_general_settings.R')%
pour importer le script ui_load_general_settings.R
.
Ces balises seront remplacées à l’exécution du plugin par un copier-coller du code contenu dans le script.
Pour les fichiers ui_*.R
, les scripts doivent contenir du code R uniquement. Cela peut être du code R standard comme nous le verrons dans ui_load_general_settings.R
, ou du code R - Shiny. Nous verrons que nous pouvons également importer des scripts Python dans server.R
.
Si vous exécutez le code du plugin, vous devriez obtenir le résultat suivant.
Cette structure doit vous rappeler celle détaillée dans Créer un projet > Créer des onglets et des widgets.
Nous retrouvons les trois ou quatre icônes en haut de l’écran (selon les paramètres généraux du widget) :
- Figure : permet d’afficher la figure ou plus globalement le résultat que le plugin est censé afficher
- Paramètres de la figure : permet de configurer la figure à l’aide d’une interface graphique
- Code de la figure : permet d’éditer le code R ou Python qui permet d’afficher la figure
- Paramètres généraux : ce sont les paramètres généraux du widget, permettant par exemple d’afficher ou de masquer certains éléments
Par défaut, la page affichée est celle des paramètres de la figure côte à côte avec la figure.
UI - interfance utilisateur / frontend
Voici le contenu du fichier ui.R
en entier (susceptible de changer en fonction des versions du template de développement).
# UI - main file
## Hide save buttons if the user does not have access
if ("projects_console_access" %in% user_accesses){
code_button <- shiny.fluent::IconButton.shinyInput(
ns("code_button_%widget_id%"), iconProps = list(iconName = "Code"), title = i18np$t("show_code_editor"),
onClick = htmlwidgets::JS(paste0("item => {",
"Shiny.setInputValue('", id, "-current_tab_trigger_%widget_id%', Math.random());",
"Shiny.setInputValue('", id, "-current_tab_%widget_id%', 'code');",
"}"))
)
} else code_button <- ""
if ("projects_widgets_settings" %in% user_accesses){
save_figure_settings_buttons <- shiny.fluent::IconButton.shinyInput(ns("save_params_and_code_%widget_id%"), iconProps = list(iconName = "Save"), title = i18np$t("save_figure_settings_and_code"), style = "margin: 0")
save_general_settings_button <- shiny.fluent::IconButton.shinyInput(ns("save_general_settings_button_%widget_id%"), iconProps = list(iconName = "Save"), title = i18np$t("save_general_settings"), style = "margin: 0")
} else {
save_figure_settings_buttons <- ""
save_general_settings_button <- ""
}
%import_script('ui_load_general_settings.R')%
tagList(
div(
shinyjs::hidden(
div(
id = ns("figure_button_div_%widget_id%"),
shiny.fluent::IconButton.shinyInput(
ns("figure_button_%widget_id%"), iconProps = list(iconName = "BarChart4"), title = i18np$t("show_figure"),
onClick = htmlwidgets::JS(paste0("item => {",
"Shiny.setInputValue('", id, "-current_tab_trigger_%widget_id%', Math.random());",
"Shiny.setInputValue('", id, "-current_tab_%widget_id%', 'figure');",
"}"))
)
)
),
shiny.fluent::IconButton.shinyInput(
ns("figure_settings_button_%widget_id%"), iconProps = list(iconName = "AllApps"), title = i18np$t("show_figure_settings"),
onClick = htmlwidgets::JS(paste0("item => {",
"Shiny.setInputValue('", id, "-current_tab_trigger_%widget_id%', Math.random());",
"Shiny.setInputValue('", id, "-current_tab_%widget_id%', 'figure_settings');",
"}"))
),
code_button,
shiny.fluent::IconButton.shinyInput(
ns("general_settings_button_%widget_id%"), iconProps = list(iconName = "Settings"), title = i18np$t("show_general_settings"),
onClick = htmlwidgets::JS(paste0("item => {",
"Shiny.setInputValue('", id, "-current_tab_trigger_%widget_id%', Math.random());",
"Shiny.setInputValue('", id, "-current_tab_%widget_id%', 'general_settings');",
"}"))
),
uiOutput(
ns("settings_files_ui_%widget_id%"),
onclick = paste0("Shiny.setInputValue('", id, "-show_settings_files_tab_%widget_id%', Math.random())")
),
class = "widget_icon",
style = "display: flex; color: #808080; border-bottom: solid grey 0.5px; height: 28px; padding: 5px 0 0 5px; font-size: 12px;"
),
div(
id = ns("figure_settings_code_div_%widget_id%"),
div(
id = ns("figure_settings_code_sidenav_%widget_id%"),
shiny.fluent::IconButton.shinyInput(ns("display_figure_%widget_id%"), iconProps = list(iconName = "Play"), title = i18np$t("display_figure"), style = "margin: 0"),
save_figure_settings_buttons,
class = "widget_icon",
style = "border-right: solid grey 0.5px; width: 25px; padding-left: 5px;"
),
div(
id = ns("figure_div_%widget_id%"),
%import_script('ui_figure.R')%,
style = paste0("height: 100%; flex-basis: ", div_width, "; flex: 1; box-sizing: border-box; min-width: 50px;"),
class = "left-panel"
),
div(
id = ns("resizer_%widget_id%"),
style = "width: 5px; cursor: col-resize; background-color: #ccc;",
class = "resizer"
),
div(
id = ns("figure_settings_div_%widget_id%"),
%import_script('ui_figure_settings.R')%,
style = paste0("height: 100%; flex-basis: ", div_width, "%; padding: 5px; overflow: auto; flex: 1; box-sizing: border-box;")
),
shinyjs::hidden(
div(
id = ns("code_div_%widget_id%"),
%import_script('ui_code.R')%,
style = paste0("height: 100%; flex-basis: ", div_width, "%; overflow: auto; flex: 1; box-sizing: border-box;"),
class = "right-panel"
)
),
style = "display: flex; height: calc(100% - 34px);"
),
shinyjs::hidden(
div(
id = ns("general_settings_div_%widget_id%"),
%import_script('ui_general_settings.R')%,
style = "height: calc(100% - 40px);"
)
),
shinyjs::hidden(
div(
id = ns("settings_files_div_%widget_id%"),
%import_script('ui_settings_files.R')%,
style = "display: flex; height: calc(100% - 40px);"
)
)
)
Prenons ces parties une par une.
## Hide save buttons if the user does not have access
if ("projects_console_access" %in% user_accesses){
code_button <- shiny.fluent::IconButton.shinyInput(
ns("code_button_%widget_id%"), iconProps = list(iconName = "Code"), title = i18np$t("show_code_editor"),
onClick = htmlwidgets::JS(paste0("item => {",
"Shiny.setInputValue('", id, "-current_tab_trigger_%widget_id%', Math.random());",
"Shiny.setInputValue('", id, "-current_tab_%widget_id%', 'code');",
"}"))
)
} else code_button <- ""
if ("projects_widgets_settings" %in% user_accesses){
save_figure_settings_buttons <- shiny.fluent::IconButton.shinyInput(ns("save_params_and_code_%widget_id%"), iconProps = list(iconName = "Save"), title = i18np$t("save_figure_settings_and_code"), style = "margin: 0")
save_general_settings_button <- shiny.fluent::IconButton.shinyInput(ns("save_general_settings_button_%widget_id%"), iconProps = list(iconName = "Save"), title = i18np$t("save_general_settings"), style = "margin: 0")
} else {
save_figure_settings_buttons <- ""
save_general_settings_button <- ""
}
Les droits de l’utilisateur sont disponibles dans la variable user_accesses
. Ceci nous permet de :
- masquer le bouton d’édition du code si l’utilisateur n’a pas accès à la console dans les projets
- masquer les boutons d’édition des widgets si l’utilisateur n’a pas ce droit
Ensuite nous importons le fichier ui_load_general_settings.R
avec
%import_script('ui_load_general_settings.R')%
Nous y reviendrons plus tard.
Détaillons les différents éléments du tagList
:
UI du header
div(
shinyjs::hidden(
div(
id = ns("figure_button_div_%widget_id%"),
shiny.fluent::IconButton.shinyInput(
ns("figure_button_%widget_id%"), iconProps = list(iconName = "BarChart4"), title = i18np$t("show_figure"),
onClick = htmlwidgets::JS(paste0("item => {",
"Shiny.setInputValue('", id, "-current_tab_trigger_%widget_id%', Math.random());",
"Shiny.setInputValue('", id, "-current_tab_%widget_id%', 'figure');",
"}"))
)
)
),
shiny.fluent::IconButton.shinyInput(
ns("figure_settings_button_%widget_id%"), iconProps = list(iconName = "AllApps"), title = i18np$t("show_figure_settings"),
onClick = htmlwidgets::JS(paste0("item => {",
"Shiny.setInputValue('", id, "-current_tab_trigger_%widget_id%', Math.random());",
"Shiny.setInputValue('", id, "-current_tab_%widget_id%', 'figure_settings');",
"}"))
),
code_button,
shiny.fluent::IconButton.shinyInput(
ns("general_settings_button_%widget_id%"), iconProps = list(iconName = "Settings"), title = i18np$t("show_general_settings"),
onClick = htmlwidgets::JS(paste0("item => {",
"Shiny.setInputValue('", id, "-current_tab_trigger_%widget_id%', Math.random());",
"Shiny.setInputValue('", id, "-current_tab_%widget_id%', 'general_settings');",
"}"))
),
uiOutput(
ns("settings_files_ui_%widget_id%"),
onclick = paste0("Shiny.setInputValue('", id, "-show_settings_files_tab_%widget_id%', Math.random())")
),
class = "widget_icon",
style = "display: flex; color: #808080; border-bottom: solid grey 0.5px; height: 28px; padding: 5px 0 0 5px; font-size: 12px;"
)
Il s’agit du code permettant d’afficher les icônes du header du plugin. Notez que l’on utilise la variable code_button
définie plue haut, afin de masquer ce bouton si l’utilisateur n’a pas accès.
Nous utilisons la classe widget_icon
pour le CSS des icônes.
Nous utilisons des triggers avec onclick
(pour les fonctions Shiny) ou OnClick
(pour les fonctions shiny.fluent
) qui vont permettre de modifier la valeur de input$current_tab_%widget_id%
afin de changer l’onglet du widget affiché.
input$show_settings_files_tab_%widget_id%
permettra d’afficher la page des fichiers de sauvegarde lorsque l’on cliquera sur le nom du fichier de sauvegarde dans le header (en cliquant sur “Pas de fichier de sauvegarde sélectionné” sur la capture d’écran ci-dessus).
UI de la figure, des paramètres et du code de la figure
div(
id = ns("figure_settings_code_div_%widget_id%"),
div(
id = ns("figure_settings_code_sidenav_%widget_id%"),
shiny.fluent::IconButton.shinyInput(ns("display_figure_%widget_id%"), iconProps = list(iconName = "Play"), title = i18np$t("display_figure"), style = "margin: 0"),
save_figure_settings_buttons,
class = "widget_icon",
style = "border-right: solid grey 0.5px; width: 25px; padding-left: 5px;"
),
div(
id = ns("figure_div_%widget_id%"),
%import_script('ui_figure.R')%,
style = paste0("height: 100%; flex-basis: ", div_width, "; flex: 1; box-sizing: border-box; min-width: 50px;"),
class = "left-panel"
),
div(
id = ns("resizer_%widget_id%"),
style = "width: 5px; cursor: col-resize; background-color: #ccc;",
class = "resizer"
),
div(
id = ns("figure_settings_div_%widget_id%"),
%import_script('ui_figure_settings.R')%,
style = paste0("height: 100%; flex-basis: ", div_width, "%; padding: 5px; overflow: auto; flex: 1; box-sizing: border-box;")
),
shinyjs::hidden(
div(
id = ns("code_div_%widget_id%"),
%import_script('ui_code.R')%,
style = paste0("height: 100%; flex-basis: ", div_width, "%; overflow: auto; flex: 1; box-sizing: border-box;"),
class = "right-panel"
)
),
style = "display: flex; height: calc(100% - 34px);"
)
Le premier div permet d’afficher les boutons de la barre latérale gauche, avec le bouton pour afficher la figure et le bouton pour sauvegarder les paramètres et le code de la figure.
div(
id = ns("figure_settings_code_sidenav_%widget_id%"),
shiny.fluent::IconButton.shinyInput(ns("display_figure_%widget_id%"), iconProps = list(iconName = "Play"), title = i18np$t("display_figure"), style = "margin: 0"),
save_figure_settings_buttons,
class = "widget_icon",
style = "border-right: solid grey 0.5px; width: 25px; padding-left: 5px;"
)
Notez que l’on utilise save_figure_settings_buttons
afin de masquer le bouton de sauvegarde si l’utilisateur n’a pas l’accès.
- UI de la figure
Le script de l’UI de la figure est importé dans ce code.
div(
id = ns("figure_div_%widget_id%"),
%import_script('ui_figure.R')%,
style = paste0("height: 100%; flex-basis: ", div_width, "; flex: 1; box-sizing: border-box; min-width: 50px;"),
class = "left-panel"
)
Regardons le fichier ui_figure.R
.
# UI - Figure page
#
# Insert the UI components for the figure in this section.
div(
shinyjs::hidden(
div(
id = ns("error_message_div_%widget_id%"),
uiOutput(ns("error_message_%widget_id%")),
style = "padding-top: 10px;"
)
),
# Div containing the figure output
style = "width: 100%; height: 100%;"
)
On a un uiOutput
pour afficher un message d’erreur, que nous verrons dans le script backend correspondant.
C’est à vous d’ajouter l’UI le code pour l’output comme vous l’entendez, avec une fonction d’output Shiny.
Ce tableau rassemble les fonctions d’output proposées par Shiny, mais il en existe d’autres, proposées par des librairies de visualisation, telle que plotlyOutput.
- UI des paramètres de la figure
Le script de l’UI des paramètres de la figure est importé dans ce code.
div(
id = ns("figure_settings_div_%widget_id%"),
%import_script('ui_figure_settings.R')%,
style = paste0("height: 100%; flex-basis: ", div_width, "%; margin: 5px 10px; overflow: auto; flex: 1; box-sizing: border-box;")
)
Notez que nous avons entre le code de la figure et le code des paramètres de la figure un resizer, qui permettra d’ajuster la taille des deux blocs lorsqu’ils seront affichés côte à côte.
div(
id = ns("resizer_%widget_id%"),
style = "width: 5px; cursor: col-resize; background-color: #ccc;",
class = "resizer"
)
Voici le contenu du fichier ui_figure_settings.R
.
# UI - Figure settings
#
# Insert the UI components for configuring the figure settings in this section.
div(
)
Cette UI est donc vide, à vous d’y ajouter les éléments permettant de configurer la figure, avec les fonctions d’input Shiny ou celles de shiny.fluent
.
- UI du code de la figure
Et finalement, voici le code permettant d’importer l’UI du code de la figure.
shinyjs::hidden(
div(
id = ns("code_div_%widget_id%"),
%import_script('ui_code.R')%,
style = paste0("height: 100%; flex-basis: ", div_width, "%; overflow: auto; flex: 1; box-sizing: border-box;"),
class = "right-panel"
)
)
On voit que le code est encapsulé dans une fonction shiny.hidden
, ce qui lui attribura un style avec display: none
par défaut. Ceci permet au chargement du widget d’afficher la figure et les paramètres de la figure côte à côte en masquant le bloc avec l’éditeur de code.
Voici le contenu de ui_code.R
.
# UI - Code page
shinyAce::aceEditor(
ns("code_%widget_id%"), value = "", mode = "r",
hotkeys = list(
save = list(win = "CTRL-S", mac = "CTRL-S|CMD-S"),
run_all = list(win = "CTRL-SHIFT-ENTER", mac = "CTRL-SHIFT-ENTER|CMD-SHIFT-ENTER"),
comment = list(win = "CTRL-SHIFT-C", mac = "CTRL-SHIFT-C|CMD-SHIFT-C")
),
autoScrollEditorIntoView = TRUE, height = "100%", debounce = 100, fontSize = 11, showPrintMargin = FALSE
)
Comme partout dans LinkR, nous utilisons les éditeurs de texte Ace avec la fonction shinyAce
.
On utilise ici les mêmes raccourcis qu’ailleurs sur LinkR, pour sauvegarder le code, le commenter et l’exécuter. Le debounce
à 100 permet que l’input
correspondant à l’éditeur soit mis à jour fréquemment.
UI des paramètres généraux du widget
L’UI des paramètres généraux est généré avec ce code.
shinyjs::hidden(
div(
id = ns("general_settings_div_%widget_id%"),
%import_script('ui_general_settings.R')%,
style = "height: calc(100% - 40px);"
)
)
Voici le contenu de ui_general_settings.R
.
# UI - General settings
#
# Include the UI components for the general settings in this section.
# If you add any fields here, ensure that you also update server_general_settings.R and ui_general_settings.R accordingly.
div(
div(
tags$strong(i18np$t("display")), br(),
div(
shiny.fluent::Toggle.shinyInput(
ns("show_settings_file_%widget_id%"), value = toggle_values$show_settings_file,
onClick = htmlwidgets::JS(paste0("item => {Shiny.setInputValue('", id, "-save_general_settings_%widget_id%', Math.random());}"))
),
tags$label(i18np$t("show_settings_file"), `for` = ns("show_settings_file_%widget_id%"), style = "margin-left: 5px;"),
style = "display: flex; margin-top: 8px;"
),
div(
shiny.fluent::Toggle.shinyInput(
ns("figure_and_settings_side_by_side_%widget_id%"), value = toggle_values$figure_and_settings_side_by_side,
onClick = htmlwidgets::JS(paste0("item => {Shiny.setInputValue('", id, "-save_general_settings_%widget_id%', Math.random());}"))
),
tags$label(i18np$t("figure_and_settings_side_by_side"), `for` = ns("figure_and_settings_side_by_side_%widget_id%"), style = "margin-left: 5px;"),
style = "display: flex; margin-top: 5px;"
)
),
# div(
# tags$strong(i18np$t("code_execution")), br(),
# div(
# shiny.fluent::Toggle.shinyInput(
# ns("run_code_at_settings_file_load_%widget_id%"), value = toggle_values$run_code_at_settings_file_load,
# onClick = htmlwidgets::JS(paste0("item => {Shiny.setInputValue('", id, "-save_general_settings_%widget_id%', Math.random());}"))
# ),
# tags$label(i18np$t("run_code_at_settings_file_load"), `for` = ns("run_code_at_settings_file_load_%widget_id%"), style = "margin-left: 5px;"),
# style = "display: flex; margin-top: 8px;"
# ),
# div(
# shiny.fluent::Toggle.shinyInput(
# ns("run_code_on_data_update_%widget_id%"), value = toggle_values$run_code_on_data_update,
# onClick = htmlwidgets::JS(paste0("item => {Shiny.setInputValue('", id, "-save_general_settings_%widget_id%', Math.random());}"))
# ),
# tags$label(i18np$t("run_code_on_data_update"), `for` = ns("run_code_on_data_update_%widget_id%"), style = "margin-left: 5px;"),
# style = "display: flex; margin-top: 5px;"
# ),
# style = "margin-top: 10px;"
# ),
style = "padding: 10px;"
)
Nous affichons par défaut les toggles permettant de gérer l’affichage. Les toggles permettant de gérer l’exécution du code sont par défaut mis en commentaires.
Nous voyons que les valeurs sont définies par la variable toggle_values
, qui est déclarée dans ui_load_general_settings.R
, que voici :
# UI - Load general settings
#
# Insert the UI components for configuring the figure settings here.
sql <- glue::glue_sql("SELECT link_id, name, value, value_num FROM widgets_options WHERE widget_id = %widget_id% AND category = 'general_settings'", .con = m$db)
general_settings <- DBI::dbGetQuery(m$db, sql)
toggle_values <- list()
if (nrow(general_settings) == 0){
toggle_values$show_settings_file <- TRUE
toggle_values$figure_and_settings_side_by_side <- TRUE
toggle_values$run_code_on_data_update <- FALSE
toggle_values$run_code_at_settings_file_load <- FALSE
dropdown_options <- list()
selected_file <- NULL
} else if (nrow(general_settings) > 0){
# Toggles values
# general_settings_vec <- c("show_saved_file", "figure_and_settings_side_by_side", "run_code_on_data_update", "run_code_at_settings_file_load")
general_settings_vec <- c("show_settings_file", "figure_and_settings_side_by_side")
sapply(general_settings_vec, function(name){
toggle_value <- FALSE
row <- general_settings %>% dplyr::filter(name == !!name)
if (nrow(row) > 0){
if (is.na(row %>% dplyr::pull(value_num))) toggle_value <- FALSE
else (toggle_value <- as.logical(row %>% dplyr::pull(value_num)))
}
toggle_values[[name]] <<- toggle_value
})
# Selected settings file
sql <- glue::glue_sql("SELECT id, value AS name FROM widgets_options WHERE widget_id = %widget_id% AND category = 'settings_files' AND name = 'file_name'", .con = m$db)
m$settings_filenames_%widget_id% <- DBI::dbGetQuery(m$db, sql)
dropdown_options <- convert_tibble_to_list(m$settings_filenames_%widget_id%, key_col = "id", text_col = "name")
selected_file <- general_settings %>% dplyr::filter(name == "selected_file_id") %>% dplyr::pull(value_num)
}
if (toggle_values$figure_and_settings_side_by_side) div_width <- "50%" else div_width <- "100%"
On requête la base de données de l’application pour aller chercher les valeurs enregistrées des paramètres généraux.
UI des fichiers de sauvegarde
L’UI des fichiers de sauvegarde est généré avec ce code.
shinyjs::hidden(
div(
id = ns("settings_files_div_%widget_id%"),
%import_script('ui_settings_files.R')%,
style = "display: flex; height: calc(100% - 40px);"
)
)
Voici le contenu de ui_settings_files.R
.
# UI - Settings files
#
# This code corresponds to the page that appears when you click on the file icon at the top of the widget.
# The UI allows you to select, add, and delete settings files.
tagList(
# Add settings file modal
shinyjs::hidden(
div(
id = ns("add_settings_file_modal_%widget_id%"),
div(
div(
tags$h1(i18np$t("create_settings_file"), style = "font-size: 14px;"),
shiny.fluent::IconButton.shinyInput(ns("close_add_settings_file_modal_%widget_id%"), iconProps = list(iconName = "ChromeClose")),
style = "display: flex; justify-content: space-between;",
class = "small_close_button"
),
div(shiny.fluent::TextField.shinyInput(ns("settings_file_name_%widget_id%"), label = i18np$t("file_name")), style = "width: 200px;"),
div(
shiny.fluent::PrimaryButton.shinyInput(ns("add_settings_file_%widget_id%"), i18np$t("add")),
style = "position: absolute; right: 10px; bottom: 8px;"
),
style = "background: #fff; padding: 5px 10px 10px 15px; position: relative; width: 400px; height: 120px;"
),
style = "display: flex; align-items: center; justify-content: center; position: absolute; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.2); z-index: 1000;"
)
),
# Delete settings file modal
shinyjs::hidden(
div(
id = ns("delete_settings_file_modal_%widget_id%"),
div(
tags$h1(i18np$t("delete_settings_file_title"), style = "font-size: 14px;"),
tags$p(i18np$t("delete_settings_file_text")),
div(
shiny.fluent::DefaultButton.shinyInput(ns("close_file_deletion_modal_%widget_id%"), i18np$t("dont_delete")),
div(shiny.fluent::PrimaryButton.shinyInput(ns("confirm_file_deletion_%widget_id%"), i18np$t("delete")), class = "delete_button"),
style = "position: absolute; right: 10px; bottom: 8px; display: flex; gap: 5px;"
),
style = "background: #fff; padding: 5px 10px 10px 15px; position: relative; width: 400px; height: 120px;"
),
style = "display: flex; align-items: center; justify-content: center; position: absolute; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.2); z-index: 1000;"
)
),
# Sidenav
div(
id = ns("settings_files_sidenav_%widget_id%"),
shiny.fluent::IconButton.shinyInput(ns("create_settings_file_%widget_id%"), iconProps = list(iconName = "Add"), title = i18np$t("create_settings_file"), style = "margin: 0"),
class = "widget_icon",
style = "border-right: solid grey 0.5px; padding-left: 5px;"
),
# Dropdown and delete button
div(
div(shiny.fluent::Dropdown.shinyInput(ns("settings_file_%widget_id%"), label = i18np$t("file"), options = dropdown_options, value = selected_file), style = "width: 200px"),
shinyjs::hidden(
div(
id = ns("delete_settings_file_div_%widget_id%"),
shiny.fluent::IconButton.shinyInput(ns("delete_settings_file_%widget_id%"), iconProps = list(iconName = "Delete")),
style = "margin-top: 26px;", class = "widget_icon"
)
),
style = "display: flex; gap: 5px; margin: 5px 10px;"
)
)
Prenons d’abord les deux derniers blocs, affichés par défaut.
# Sidenav
div(
id = ns("settings_files_sidenav_%widget_id%"),
shiny.fluent::IconButton.shinyInput(ns("create_settings_file_%widget_id%"), iconProps = list(iconName = "Add"), title = i18np$t("create_settings_file"), style = "margin: 0"),
class = "widget_icon",
style = "border-right: solid grey 0.5px; padding-left: 5px;"
),
# Dropdown and delete button
div(
div(shiny.fluent::Dropdown.shinyInput(ns("settings_file_%widget_id%"), label = i18np$t("file"), options = dropdown_options, value = selected_file), style = "width: 200px"),
shinyjs::hidden(
div(
id = ns("delete_settings_file_div_%widget_id%"),
shiny.fluent::IconButton.shinyInput(ns("delete_settings_file_%widget_id%"), iconProps = list(iconName = "Delete")),
style = "margin-top: 26px;", class = "widget_icon"
)
),
style = "display: flex; gap: 5px; margin: 5px 10px;"
)
Ce code nous permet d’afficher le bouton “+” latéralement pour afficher un fichier et le menu déroulant permettant de choisir le fichier.
Nous avons ensuite le modal permettant de créer un fichier, avec le bouton permettant de fermer le modal, l’input de texte et le bouton permettant de valider l’ajout du fichier.
# Add settings file modal
shinyjs::hidden(
div(
id = ns("add_settings_file_modal_%widget_id%"),
div(
div(
tags$h1(i18np$t("create_settings_file"), style = "font-size: 14px;"),
shiny.fluent::IconButton.shinyInput(ns("close_add_settings_file_modal_%widget_id%"), iconProps = list(iconName = "ChromeClose")),
style = "display: flex; justify-content: space-between;",
class = "small_close_button"
),
div(shiny.fluent::TextField.shinyInput(ns("settings_file_name_%widget_id%"), label = i18np$t("file_name")), style = "width: 200px;"),
div(
shiny.fluent::PrimaryButton.shinyInput(ns("add_settings_file_%widget_id%"), i18np$t("add")),
style = "position: absolute; right: 10px; bottom: 8px;"
),
style = "background: #fff; padding: 5px 10px 10px 15px; position: relative; width: 400px; height: 120px;"
),
style = "display: flex; align-items: center; justify-content: center; position: absolute; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.2); z-index: 1000;"
)
)
Et finalement, le code du modal permettant de confirmer la suppression du fichier.
# Delete settings file modal
shinyjs::hidden(
div(
id = ns("delete_settings_file_modal_%widget_id%"),
div(
tags$h1(i18np$t("delete_settings_file_title"), style = "font-size: 14px;"),
tags$p(i18np$t("delete_settings_file_text")),
div(
shiny.fluent::DefaultButton.shinyInput(ns("close_file_deletion_modal_%widget_id%"), i18np$t("dont_delete")),
div(shiny.fluent::PrimaryButton.shinyInput(ns("confirm_file_deletion_%widget_id%"), i18np$t("delete")), class = "delete_button"),
style = "position: absolute; right: 10px; bottom: 8px; display: flex; gap: 5px;"
),
style = "background: #fff; padding: 5px 10px 10px 15px; position: relative; width: 400px; height: 120px;"
),
style = "display: flex; align-items: center; justify-content: center; position: absolute; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.2); z-index: 1000;"
)
)
Passons maintenant à la partie backend de notre plugin.
Serveur / backend
Invalidation des observers
Pensez à systématiquement ajouter une balise %req%
au début des observers, ce qui permet d'invalider les anciens observers.
Pensez également à ajouter un message avec if (debug) ... cat()
. Ceci permet de localiser les bugs à partir du fichier de log.
Voici le contenu du fichier server.R
en entier.
# Server - Main file
# Tabs
## All tabs
tabs <- c("figure", "figure_settings", "code", "general_settings")
observeEvent(input$current_tab_trigger_%widget_id%, {
%req%
if (debug) cat(paste0("\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$current_tab_trigger"))
tab <- input$current_tab_%widget_id%
tryCatch({
if (tab == "general_settings") shinyjs::hide("figure_settings_code_div_%widget_id%")
else shinyjs::show("figure_settings_code_div_%widget_id%")
sapply(paste0(setdiff(c("figure_settings", "code", "general_settings"), tab), "_div_%widget_id%"), shinyjs::hide)
shinyjs::hide("settings_files_div_%widget_id%")
if (tab != "code" | (tab == "code" & "projects_console_access" %in% user_accesses)) shinyjs::show(paste0(tab, "_div_%widget_id%"))
if (tab %in% c("figure_settings", "code")){
if (length(input$figure_and_settings_side_by_side_%widget_id%) > 0) if (input$figure_and_settings_side_by_side_%widget_id%) shinyjs::show("figure_div_%widget_id%")
else shinyjs::hide("figure_div_%widget_id%")
shinyjs::show("figure_settings_code_sidenav_%widget_id%")
if (tab == "figure_settings") anti_tab <- "code"
else anti_tab <- "figure_settings"
shinyjs::runjs(paste0("
var figureSettingsDiv = document.getElementById('", id, "-figure_settings_div_%widget_id%');
var codeDiv = document.getElementById('", id, "-code_div_%widget_id%');
if ('", tab, "' === 'figure_settings') {
figureSettingsDiv.style.flexBasis = codeDiv.style.flexBasis;
} else {
codeDiv.style.flexBasis = figureSettingsDiv.style.flexBasis;
}
"))
}
else {
shinyjs::hide("figure_settings_code_sidenav_%widget_id%")
if (tab != "figure") shinyjs::hide("figure_div_%widget_id%")
}
# Prevent a bug with scroll into ace editor
shinyjs::runjs("var event = new Event('resize'); window.dispatchEvent(event);")
}, error = function(e) cat(paste0("\n", now(), " - widget %widget_id% - error = ", toString(e))))
})
# Figure settings
%import_script('server_figure_settings.R')%
# Code
%import_script('server_code.R')%
# Settings files
%import_script('server_settings_files.R')%
# General settings
%import_script('server_general_settings.R')%
Backend des onglets
Le code permettant d’afficher ou de masquer chaque onglet est le suivant :
## All tabs
tabs <- c("figure", "figure_settings", "code", "general_settings")
observeEvent(input$current_tab_trigger_%widget_id%, {
%req%
if (debug) cat(paste0("\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$current_tab_trigger"))
tab <- input$current_tab_%widget_id%
tryCatch({
if (tab == "general_settings") shinyjs::hide("figure_settings_code_div_%widget_id%")
else shinyjs::show("figure_settings_code_div_%widget_id%")
sapply(paste0(setdiff(c("figure_settings", "code", "general_settings"), tab), "_div_%widget_id%"), shinyjs::hide)
shinyjs::hide("settings_files_div_%widget_id%")
if (tab != "code" | (tab == "code" & "projects_console_access" %in% user_accesses)) shinyjs::show(paste0(tab, "_div_%widget_id%"))
if (tab %in% c("figure_settings", "code")){
if (length(input$figure_and_settings_side_by_side_%widget_id%) > 0) if (input$figure_and_settings_side_by_side_%widget_id%) shinyjs::show("figure_div_%widget_id%")
else shinyjs::hide("figure_div_%widget_id%")
shinyjs::show("figure_settings_code_sidenav_%widget_id%")
if (tab == "figure_settings") anti_tab <- "code"
else anti_tab <- "figure_settings"
shinyjs::runjs(paste0("
var figureSettingsDiv = document.getElementById('", id, "-figure_settings_div_%widget_id%');
var codeDiv = document.getElementById('", id, "-code_div_%widget_id%');
if ('", tab, "' === 'figure_settings') {
figureSettingsDiv.style.flexBasis = codeDiv.style.flexBasis;
} else {
codeDiv.style.flexBasis = figureSettingsDiv.style.flexBasis;
}
"))
}
else {
shinyjs::hide("figure_settings_code_sidenav_%widget_id%")
if (tab != "figure") shinyjs::hide("figure_div_%widget_id%")
}
# Prevent a bug with scroll into ace editor
shinyjs::runjs("var event = new Event('resize'); window.dispatchEvent(event);")
}, error = function(e) cat(paste0("\n", now(), " - widget %widget_id% - error = ", toString(e))))
})
Ce code correspond à l’UI de l’header, où il existe un bouton par onglet : pour afficher l’onglet de la figure, celui des paramètres de la figure, du code et des paramètres généraux du plugin.
Les noms des onglets sont contenus dans la variable tabs
. Un observer sur input$current_tab_trigger_%widget_id%
permet de déclencher le code lorsque l’on clique sur une icône du header. L’affichage des onglets du widget sera réalisé avec shinyjs::show()
ou shinyjs::hide()
pour afficher ou masquer les éléments de l’UI.
Il est prévu également d’afficher l’onglet figure
si l’option pour afficher la figure et les paramètres / le code de la figure côte à côte est cochée, et que l’onglet paramètres de la figure
ou code de la figure
est sélectionné.
Le code Javascript exécuté dans shinyjs::runjs
permet, lorsque la barre, qui sépare le div de la figure et celui soit du code soit des paramètres de la figure, est changée de position, d’appliquer la nouvelle largeur à l’autre élément (au div figure_settings
si on a changé la largeur du div code
et inversement).
Backend des paramètres de la figure
Le code de server_figure_settings.R
est le suivant.
# Server - Figure settings
# Load figure settings
observeEvent(input$load_figure_settings_%widget_id%, {
%req%
if (debug) cat(paste0("\\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$load_figure_settings"))
# Update figure settings UI
link_id <- input$settings_file_%widget_id%
sql <- glue::glue_sql("SELECT name, value, value_num FROM widgets_options WHERE widget_id = %widget_id% AND category = 'figure_settings' AND link_id = {link_id}", .con = m$db)
figure_settings <- DBI::dbGetQuery(m$db, sql)
if (nrow(figure_settings) > 0){
sapply(figure_settings$name, function(name){
value <- figure_settings %>% dplyr::filter(name == !!name) %>% dplyr::pull(value)
value_num <- figure_settings %>% dplyr::filter(name == !!name) %>% dplyr::pull(value_num)
# Update figure settings UI here with loaded figure settings
})
}
# Run code if toggle is activated
# if (length(input$run_code_at_settings_file_load_%widget_id%) > 0){
# if (input$run_code_at_settings_file_load_%widget_id%){
# shinyjs::delay(500, shinyjs::runjs(paste0("Shiny.setInputValue('", id, "-display_figure_%widget_id%', Math.random());")))
# }
# }
})
# Save current settings
observeEvent(input$save_params_and_code_%widget_id%, {
%req%
if (debug) cat(paste0("\\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$save_params_and_code"))
tryCatch({
# If no settings file is selected, go to settings files management page
if (length(input$settings_file_%widget_id%) == 0) shinyjs::runjs(paste0("Shiny.setInputValue('", id, "-show_settings_files_tab_%widget_id%', Math.random());"))
if (length(input$settings_file_%widget_id%) > 0){
link_id <- input$settings_file_%widget_id%
# Delete old settings
sql_send_statement(m$db, glue::glue_sql("DELETE FROM widgets_options WHERE widget_id = %widget_id% AND category = 'figure_settings' AND link_id = {link_id}", .con = m$db))
# Add new settings in db
# new_data <- tibble::tribble(
# ~name, ~value, ~value_num,
# ...
# )
# new_data <-
# new_data %>%
# dplyr::transmute(
# id = get_last_row(m$db, "widgets_options") + 1:nrow(new_data), widget_id = %widget_id%, person_id = NA_integer_, link_id = link_id,
# category = "figure_settings", name, value, value_num, creator_id = m$user_id, datetime = now(), deleted = FALSE
# )
# DBI::dbAppendTable(m$db, "widgets_options", new_data)
# Notify user
show_message_bar(output, "modif_saved", "success", i18n = i18n, ns = ns)
}
}, error = function(e) cat(paste0("\\n", now(), " - widget %widget_id% - error = ", toString(e))))
})
Commençons par la première partie du code qui permet de charger les paramètres de la figure sauvegardés.
observeEvent(input$load_figure_settings_%widget_id%, {
%req%
if (debug) cat(paste0("\\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$load_figure_settings"))
# Update figure settings UI
link_id <- input$settings_file_%widget_id%
sql <- glue::glue_sql("SELECT name, value, value_num FROM widgets_options WHERE widget_id = %widget_id% AND category = 'figure_settings' AND link_id = {link_id}", .con = m$db)
figure_settings <- DBI::dbGetQuery(m$db, sql)
if (nrow(figure_settings) > 0){
sapply(figure_settings$name, function(name){
value <- figure_settings %>% dplyr::filter(name == !!name) %>% dplyr::pull(value)
value_num <- figure_settings %>% dplyr::filter(name == !!name) %>% dplyr::pull(value_num)
# Update figure settings UI here with loaded figure settings
})
}
# Run code if toggle is activated
# if (length(input$run_code_at_settings_file_load_%widget_id%) > 0){
# if (input$run_code_at_settings_file_load_%widget_id%){
# shinyjs::delay(500, shinyjs::runjs(paste0("Shiny.setInputValue('", id, "-display_figure_%widget_id%', Math.random());")))
# }
# }
})
Le code se déclenche lorsqu’un nouvelle valeur est sélectionnée dans le dropdown load_figure_settings_%widget_id%
. Nous pouvons récupérer l’ID du fichier de paramètres avec la variable input$load_figure_settings_%widget_id%
.
Pour sauvegarder n’importe quelle information sur un widget (que ce soit dans les paramètres de la figure ou dans les paramètres généraux du plugin), nous enregistrons les données dans la table widgets_options
, accessible avec la connexion m$db
.
Regardons la structure de cette table :
Nom de la colonne | Type | Description |
---|---|---|
id |
Entier | Identifiant unique pour chaque enregistrement |
widget_id |
Entier | Identifiant associé au widget |
person_id |
Entier | Identifiant du patient |
link_id |
Entier | Identifiant pour faire le lien entre deux enregistrements |
category |
Texte | Catégorie associée à l’enregistrement |
name |
Texte | Nom ou libellé de l’enregistrement |
value |
Texte | Valeur enregistrée sous forme de texte |
value_num |
Numérique | Valeur enregistrée sous forme numérique |
creator_id |
Entier | Identifiant du créateur de l’enregistrement |
datetime |
Texte/Date | Date et heure associées à l’enregistrement |
deleted |
Entier | Indicateur si l’enregistrement est supprimé : n’est plus utilisé |
Vous pouvez utiliser cette table comme vous l’entendez, dans le sens où c’est vous qui choisissez les valeurs de category
, de name
, de value
et de value_num
.
La colonne link_id
permet de rattacher un enregistrement à d’autres enregistrements.
Nous utilisons donc le code suivant pour charger les fichiers de sauvegarde :
SELECT name, value, value_num
FROM widgets_options
WHERE widget_id = %widget_id% AND category = 'figure_settings' AND link_id = {link_id}
%widget_id%
est remplacé par l’ID du widget, link_id
contient l’ID du fichier de sauvegardé sélectionné. Nous allons donc récupérer toutes les entrées de la table de la catégorie figure_settings
rattachés au fichier de sauvegade sélectionné.
Ensuite, chaque valeur des paramètres sera chargée avec la boucle sapply
, les valeurs textuelles et numériques seront récupérées avec value
et value_num
, et nous allons pouvoir modifier les input avec ces nouvelles valeurs.
Le code commenté permet d’exécuter le code après le chargement d’un fichier de sauvegardé, si l’option est sélectionnée dans les paramètres généraux du plugin.
Voyons maintenant le code pour sauvegarder les paramètres.
observeEvent(input$save_params_and_code_%widget_id%, {
%req%
if (debug) cat(paste0("\\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$save_params_and_code"))
tryCatch({
# If no settings file is selected, go to settings files management page
if (length(input$settings_file_%widget_id%) == 0) shinyjs::runjs(paste0("Shiny.setInputValue('", id, "-show_settings_files_tab_%widget_id%', Math.random());"))
if (length(input$settings_file_%widget_id%) > 0){
link_id <- input$settings_file_%widget_id%
# Delete old settings
sql_send_statement(m$db, glue::glue_sql("DELETE FROM widgets_options WHERE widget_id = %widget_id% AND category = 'figure_settings' AND link_id = {link_id}", .con = m$db))
# Add new settings in db
# new_data <- tibble::tribble(
# ~name, ~value, ~value_num,
# ...
# )
# new_data <-
# new_data %>%
# dplyr::transmute(
# id = get_last_row(m$db, "widgets_options") + 1:nrow(new_data), widget_id = %widget_id%, person_id = NA_integer_, link_id = link_id,
# category = "figure_settings", name, value, value_num, creator_id = m$user_id, datetime = now(), deleted = FALSE
# )
# DBI::dbAppendTable(m$db, "widgets_options", new_data)
# Notify user
show_message_bar(output, "modif_saved", "success", i18n = i18n, ns = ns)
}
}, error = function(e) cat(paste0("\\n", now(), " - widget %widget_id% - error = ", toString(e))))
})
Si aucun fichier de sauvegarde n’est sélectionné, l’utilisateur sera renvoyé à la page des fichiers de sauvegarde afin d’en créer un.
Les anciennes entrées correspond au fichier de sauvegarde sont supprimées et remplacées avec les nouveaux paramètres.
Backend du code et de la figure
Voici le code de server_code.R
.
Il n’y a pas de fichier server_figure.R
: l’onglet des paramètres de la figure permet de générer le code, qui est ensuite exécuté. Ces deux aspects (génération du code à partir des paramètres + exécution du code) sont regroupés au sein du même fichier.
# Server - Code
# Init code var
m$code_%widget_id% <- ""
# Prevent a bug with scroll into ace editor
shinyjs::delay(300, shinyjs::runjs("var event = new Event('resize'); window.dispatchEvent(event);"))
# Comment code
observeEvent(input$code_%widget_id%_comment, {
%req%
if (debug) cat(paste0("\\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$code_comment"))
tryCatch({
lines <- strsplit(input$code_%widget_id%, "\\n")[[1]]
req(length(lines) > 0)
start_row <- input$code_%widget_id%_comment$range$start$row + 1
end_row <- input$code_%widget_id%_comment$range$end$row + 1
for (i in start_row:end_row) if (startsWith(lines[i], "# ")) lines[i] <- substr(lines[i], 3, nchar(lines[i])) else lines[i] <- paste0("# ", lines[i])
shinyAce::updateAceEditor(session, "code_%widget_id%", value = paste0(lines, collapse = "\\n"))
shinyjs::runjs(sprintf("
var editor = ace.edit('%s-rode');
editor.moveCursorTo(%d, %d);
editor.focus();
", id, input$code_%widget_id%_comment$range$end$row, input$code_%widget_id%_comment$range$end$column))
}, error = function(e) cat(paste0("\\n", now(), " - widget %widget_id% - error = ", toString(e))))
})
# Run all code with shortcut
observeEvent(input$code_%widget_id%_run_all, {
%req%
if (debug) cat(paste0("\\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$code_run_all"))
m$code_%widget_id% <- input$code_%widget_id%
shinyjs::runjs(paste0("Shiny.setInputValue('", id, "-run_code_%widget_id%', Math.random());"))
})
# Run code when button is clicked
observeEvent(input$display_figure_%widget_id%, {
%req%
if (debug) cat(paste0("\\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$display_figure"))
tryCatch({
# If current selected tab is figure settings when run code button is clicked, generate code from these settings
if (length(input$current_tab_%widget_id%) == 0) current_tab <- "figure_settings"
else current_tab <- input$current_tab_%widget_id%
if (current_tab == "figure_settings"){
# Code to generate code from figure settings
# ...
code <- ""
# Update ace editor with generated code
shinyAce::updateAceEditor(session, "code_%widget_id%", value = code)
m$code_%widget_id% <- code
}
# Check if user has access
else if ("projects_console_access" %in% user_accesses) m$code_%widget_id% <- input$code_%widget_id%
shinyjs::runjs(paste0("Shiny.setInputValue('", id, "-run_code_%widget_id%', Math.random());"))
}, error = function(e){
show_message_bar(output, "error_displaying_figure", "severeWarning", i18n = i18np, ns = ns)
cat(paste0("\\n", now(), " - widget %widget_id% - input$display_figure - error = ", toString(e)))
})
})
# Run code
observeEvent(input$run_code_%widget_id%, {
%req%
if (debug) cat(paste0("\\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$run_code"))
tryCatch({
# Put here the code to execute when the "Run code" button is clicked
# ...
# Go to figure tab
if (!input$figure_and_settings_side_by_side_%widget_id%) shinyjs::click("figure_button_%widget_id%")
}, error = function(e){
show_message_bar(output, "error_displaying_figure", "severeWarning", i18n = i18np, ns = ns)
cat(paste0("\\n", now(), " - widget %widget_id% - input$display_figure - error = ", toString(e)))
})
})
# Save code with shortcut
observeEvent(input$code_%widget_id%_save, {
%req%
if (debug) cat(paste0("\\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$code_save"))
shinyjs::runjs(paste0("Shiny.setInputValue('", id, "-save_params_and_code_%widget_id%', Math.random());"))
})
Le code dans l’observeur observeEvent(input$code_%widget_id%_comment, {})
permet de commenter le code dans l’éditeur avec le raccourci CTRL/CMD + SHIFT + C.
Le code peut être exécuté de deux façons :
- avec le raccourci CTRL/CMD + SHIFT + C via l’observeur
observeEvent(input$code_%widget_id%_run_all, {})
- en cliquant sur le bouton “Afficher la figure” à gauche du widget, via l’observeur
observeEvent(input$display_figure_%widget_id%, {})
Dans les deux cas, le code est copié dans la variable m$code_%widget_id%
puis l’observeur observeEvent(input$run_code_%widget_id%, {})
est exécuté,
permettant d’exécuter le code à l’origine de la figure et de basculer sur l’onglet de la figure.
Dans le cas où l’on se situe sur l’onglet des paramètres de la figure et que l’on clique sur le bouton “Afficher la figure”, le code est généré à partir des paramètres de la figure avant d’être exécuté. L’éditeur de code est mis à jour avec le code ainsi généré.
Enfin, nous avons un observeur pour sauvegarder le code depuis le raccourci (observeur observeEvent(input$code_%widget_id%_save, {})
).
Backend des fichiers de sauvegarde
Le code de server_settings_files.R
est le suivant.
# Server - Settings files management
settings_files_ui_style <- paste0(
"display: inline-block; color: white; max-width: 250px; border-radius: 8px; padding: 1px 5px; align-items: center;",
"height: 18px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: pointer; margin: 2px 5px;"
)
output$settings_files_ui_%widget_id% <- renderUI({
%req%
if (debug) cat(paste0("\\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer output$settings_files"))
div(i18np$t("no_settings_file_selected"), style = paste0(settings_files_ui_style, "background-color: #606060ab;"))
})
## Show / hide settings file
observeEvent(input$show_settings_file_%widget_id%, {
%req%
if (debug) cat(paste0("\\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$show_settings_file"))
if (input$show_settings_file_%widget_id%) shinyjs::show("settings_files_ui_%widget_id%")
else shinyjs::hide("settings_files_ui_%widget_id%")
})
## Show / hide settings files div
observeEvent(input$show_settings_files_tab_%widget_id%, {
%req%
if (debug) cat(paste0("\\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$show_settings_files_tab"))
sapply(c(paste0(tabs, "_div_%widget_id%"), "figure_settings_code_div_%widget_id%"), shinyjs::hide)
shinyjs::show("settings_files_div_%widget_id%")
})
## Show add settings file modal
observeEvent(input$create_settings_file_%widget_id%, {
%req%
if (debug) cat(paste0("\\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$create_settings_file"))
shinyjs::show("add_settings_file_modal_%widget_id%")
})
## Close add settings file modal
observeEvent(input$close_add_settings_file_modal_%widget_id%, {
%req%
if (debug) cat(paste0("\\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$close_add_settings_file_modal"))
shinyjs::hide("add_settings_file_modal_%widget_id%")
})
## Confirm creation of settings file
observeEvent(input$add_settings_file_%widget_id%, {
%req%
if (debug) cat(paste0("\\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$add_settings_file"))
tryCatch({
file_name <- input$settings_file_name_%widget_id%
# Check if name if empty
empty_name <- TRUE
if (length(file_name) > 0) if (!is.na(file_name) & file_name != "") empty_name <- FALSE
if (empty_name) shiny.fluent::updateTextField.shinyInput(session, "settings_file_name_%widget_id%", errorMessage = i18np$t("provide_valid_name"))
else {
shiny.fluent::updateTextField.shinyInput(session, "settings_file_name_%widget_id%", errorMessage = NULL)
# Check if name is already used
sql <- glue::glue_sql("SELECT name FROM widgets_options WHERE widget_id = %widget_id% AND category = 'settings_files' AND name = 'file_name' AND LOWER(value) = {tolower(file_name)}", .con = m$db)
name_already_used <- nrow(DBI::dbGetQuery(m$db, sql) > 0)
if (name_already_used) shiny.fluent::updateTextField.shinyInput(session, "settings_file_name_%widget_id%", errorMessage = i18np$t("name_already_used"))
else {
new_id <- get_last_row(m$db, "widgets_options") + 1
# Add settings file in database
new_data <- tibble::tibble(
id = new_id, widget_id = %widget_id%, person_id = NA_integer_, link_id = NA_integer_,
category = "settings_files", name = "file_name", value = file_name, value_num = NA_real_, creator_id = m$user_id, datetime = now(), deleted = FALSE
)
DBI::dbAppendTable(m$db, "widgets_options", new_data)
# Reset fields
shiny.fluent::updateTextField.shinyInput(session, "settings_file_name_%widget_id%", value = "")
# Update dropdown
shiny.fluent::updateDropdown.shinyInput(session, "settings_file_%widget_id%", value = new_id)
shinyjs::runjs(paste0("Shiny.setInputValue('", id, "-reload_dropdown_%widget_id%', Math.random());"))
# Close modal
shinyjs::hide("add_settings_file_modal_%widget_id%")
# Notify user
show_message_bar(output, "new_settings_file_added", "success", i18n = i18np, ns = ns)
}
}
}, error = function(e) cat(paste0("\\n", now(), " - widget %widget_id% - error = ", toString(e))))
})
## Update dropdown
observeEvent(input$reload_dropdown_%widget_id%, {
%req%
if (debug) cat(paste0("\\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$settings_file"))
tryCatch({
sql <- glue::glue_sql("SELECT id, value AS name FROM widgets_options WHERE widget_id = %widget_id% AND category = 'settings_files' AND name = 'file_name'", .con = m$db)
m$settings_filenames_%widget_id% <- DBI::dbGetQuery(m$db, sql)
dropdown_options <- convert_tibble_to_list(m$settings_filenames_%widget_id%, key_col = "id", text_col = "name")
shiny.fluent::updateDropdown.shinyInput(session, "settings_file_%widget_id%", options = dropdown_options)
}, error = function(e) cat(paste0("\\n", now(), " - widget %widget_id% - error = ", toString(e))))
})
## A settings file is selected
observeEvent(input$settings_file_%widget_id%, {
%req%
if (debug) cat(paste0("\\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$settings_file"))
tryCatch({
# Show delete button
shinyjs::show("delete_settings_file_div_%widget_id%")
# Get file name
file_id <- input$settings_file_%widget_id%
filename <- m$settings_filenames_%widget_id% %>% dplyr::filter(id == file_id) %>% dplyr::pull(name)
output$settings_files_ui_%widget_id% <- renderUI(div(filename, style = paste0(settings_files_ui_style, "background-color: #1d94ce;")))
# Save that this file is selected
sql_send_statement(m$db, glue::glue_sql("DELETE FROM widgets_options WHERE widget_id = %widget_id% AND category = 'general_settings' AND name = 'selected_file_id'", .con = m$db))
new_data <- tibble::tibble(
id = get_last_row(m$db, "widgets_options") + 1, widget_id = %widget_id%, person_id = NA_integer_, link_id = NA_integer_,
category = "general_settings", name = "selected_file_id", value = NA_character_, value_num = file_id, creator_id = m$user_id, datetime = now(), deleted = FALSE
)
print(new_data)
DBI::dbAppendTable(m$db, "widgets_options", new_data)
# Load saved settings
shinyjs::runjs(paste0("Shiny.setInputValue('", id, "-load_figure_settings_%widget_id%', Math.random());"))
}, error = function(e) cat(paste0("\\n", now(), " - widget %widget_id% - error = ", toString(e))))
})
## Open delete a settings file modal
observeEvent(input$delete_settings_file_%widget_id%, {
%req%
req(length(input$settings_file_%widget_id%) > 0)
if (debug) cat(paste0("\\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$delete_settings_file"))
shinyjs::show("delete_settings_file_modal_%widget_id%")
})
## Close delete a settings file modal
observeEvent(input$close_file_deletion_modal_%widget_id%, {
%req%
if (debug) cat(paste0("\\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$close_file_deletion_modal"))
shinyjs::hide("delete_settings_file_modal_%widget_id%")
})
## Confirm settings file deletion
observeEvent(input$confirm_file_deletion_%widget_id%, {
%req%
if (debug) cat(paste0("\\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$confirm_file_deletion"))
tryCatch({
file_id <- input$settings_file_%widget_id%
# Delete row in db
sql_send_statement(m$db, glue::glue_sql("DELETE FROM widgets_options WHERE id = {file_id}", .con = m$db))
# Update dropdown
shinyjs::runjs(paste0("Shiny.setInputValue('", id, "-reload_dropdown_%widget_id%', Math.random());"))
# Close modal
shinyjs::hide("delete_settings_file_modal_%widget_id%")
# Update selected settings file UI
output$settings_files_ui_%widget_id% <- renderUI(div(i18np$t("no_settings_file_selected"), style = paste0(settings_files_ui_style, "background-color: #606060ab;")))
# Hide delete button
shinyjs::hide("delete_settings_file_div_%widget_id%")
# Notify user
show_message_bar(output, "settings_file_delete", "warning", i18n = i18np, ns = ns)
}, error = function(e) cat(paste0("\\n", now(), " - widget %widget_id% - error = ", toString(e))))
})
Vous devriez normalement avoir avec les explications précédentes les clefs pour comprendre le code ci-dessus, avec les commentaires fournis dans le code.
Le bloc avec output$settings_files_ui_%widget_id%
permet d’afficher par défaut qu’il n’y a pas de fichier chargé, dans le header.
Voici le détail des observeurs :
observeEvent(input$show_settings_file_%widget_id%, {})
permet d’afficher ou de masquer le div avec le nom du fichier sélectionné, selon la valeur du paramètre dans les paramètres généraux du widget.observeEvent(input$show_settings_files_tab_%widget_id%, {})
permet d’afficher ou de masquer l’UI de l’onglet permettant de gérer les fichiers de sauvegarde.observeEvent(input$create_settings_file_%widget_id%, {})
permet d’afficher le modal de création d’un fichier de sauvegarde lorsque l’on clique sur le bouton “+”.observeEvent(input$close_add_settings_file_modal_%widget_id%, {})
permet de masquer le modal lorsque l’on clique sur la croix pour quitter le modal.observeEvent(input$add_settings_file_%widget_id%, {})
permet d’ajouter les informations du fichier créé dans la base de données et de réinitialiser les champsobserveEvent(input$settings_file_%widget_id, {})
permet de charger le code et les paramètres de la figure associés au fichier de sauvegarde sélectionnéobserveEvent(input$delete_settings_file_%widget_id%, {})
permet d’ouvrir le modal de suppression du fichier de sauvegarde sélectionnéobserveEvent(input$confirm_file_deletion_%widget_id%, {})
permet de supprimer le fichier de savuegarde de la base de données
Backend des paramètres généraux
Voici le code de server_general_settings.R
:
# Server - General settings
# Settings / editor side-by-side with figure
observeEvent(input$figure_and_settings_side_by_side_%widget_id%, {
%req%
if (debug) cat(paste0("\\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$figure_and_settings_side_by_side"))
tryCatch({
if (input$figure_and_settings_side_by_side_%widget_id%){
shinyjs::runjs(paste0("
$('#", id, "-figure_div_%widget_id%').css('flex-basis', '50%');
$('#", id, "-figure_settings_div_%widget_id%').css('flex-basis', '50%');
$('#", id, "-code_div_%widget_id%').css('flex-basis', '50%');
if (!window.resizingInitialized_%widget_id%) {
var container = document.getElementById('", id, "-figure_settings_code_div_%widget_id%');
var isResizing = false;
var lastDownX = 0;
var leftPanel = container.querySelector('.left-panel');
var figureSettingsPanel = document.getElementById('", id, "-figure_settings_div_%widget_id%');
var codePanel = document.getElementById('", id, "-code_div_%widget_id%');
var resizer = container.querySelector('.resizer');
function triggerResizeEvent() {
var event = new Event('resize');
window.dispatchEvent(event);
}
resizer.addEventListener('mousedown', function(e) {
isResizing = true;
lastDownX = e.clientX;
document.addEventListener('mousemove', resizePanels);
document.addEventListener('mouseup', stopResizing);
});
function resizePanels(e) {
if (!isResizing) return;
var offsetLeftPanel = leftPanel.offsetWidth;
var offsetFigureSettingsPanel = figureSettingsPanel.offsetWidth;
var offsetCodePanel = codePanel.offsetWidth;
var deltaX = e.clientX - lastDownX;
leftPanel.style.flexBasis = (offsetLeftPanel + deltaX) + 'px';
figureSettingsPanel.style.flexBasis = (offsetFigureSettingsPanel - deltaX) + 'px';
codePanel.style.flexBasis = (offsetCodePanel - deltaX) + 'px';
lastDownX = e.clientX;
triggerResizeEvent();
}
function stopResizing(e) {
isResizing = false;
document.removeEventListener('mousemove', resizePanels);
document.removeEventListener('mouseup', stopResizing);
triggerResizeEvent();
}
}
window.resizingInitialized_%widget_id% = true;
"))
shinyjs::hide("figure_button_div_%widget_id%")
shinyjs::show("resizer_%widget_id%")
}
else {
shinyjs::runjs(paste0("
$('#", id, "-figure_div_%widget_id%').css('flex-basis', '100%');
$('#", id, "-figure_settings_div_%widget_id%').css('flex-basis', '100%');
$('#", id, "-code_div_%widget_id%').css('flex-basis', '100%');
"))
shinyjs::show("figure_button_div_%widget_id%");
shinyjs::hide("resizer_%widget_id%");
}
}, error = function(e) cat(paste0("\\n", now(), " - widget %widget_id% - error = ", toString(e))))
})
# Save general settings in db
observeEvent(input$save_general_settings_%widget_id%, {
%req%
if (debug) cat(paste0("\\n", now(), " - mod_", id, " - widget_id = %widget_id% - observer input$save_general_settings"))
tryCatch({
# Delete old rows
sql_send_statement(m$db, glue::glue_sql("DELETE FROM widgets_options WHERE widget_id = %widget_id% AND category = 'general_settings' AND name != 'selected_file_id'", .con = m$db))
file_id <- input$settings_file_%widget_id%
# new_data <- tibble::tibble(name = "selected_file_id", value = NA_character_, value_num = NA_integer_, link_id = file_id)
new_data <- tibble::tibble(name = character(), value = character(), value_num = integer())
# general_settings_vec <- c("show_saved_file", "figure_and_settings_side_by_side", "run_code_on_data_update", "run_code_at_settings_file_load")
general_settings_vec <- c("show_settings_file", "figure_and_settings_side_by_side")
sapply(general_settings_vec, function(name){
toggle_value <- 0L
input_name <- paste0(name, "_%widget_id%")
if (length(input[[input_name]]) > 0) if (input[[input_name]]) toggle_value <- 1L
new_data <<- new_data %>% dplyr::bind_rows(tibble::tibble(name = name, value = NA_character_, value_num = toggle_value))
})
new_data <-
new_data %>%
dplyr::transmute(
id = get_last_row(m$db, "widgets_options") + 1:(length(general_settings_vec)), widget_id = %widget_id%, person_id = NA_integer_, link_id = NA_integer_,
category = "general_settings", name, value, value_num, creator_id = m$user_id, datetime = now(), deleted = FALSE
)
DBI::dbAppendTable(m$db, "widgets_options", new_data)
}, error = function(e) cat(paste0("\\n", now(), " - widget %widget_id% - error = ", toString(e))))
})
Le premier observeur, observeEvent(input$figure_and_settings_side_by_side_%widget_id%, {})
contient le code Javascript qui permettra de gérer
la taille des div côte à côte lorsque l’utilisateur cliquera sur la barre de séparation entre ces deux div.
L’observeur observeEvent(input$save_general_settings_%widget_id%, {})
permet de sauvegarder les paramètres généraux du widget automatiquement à chaque modification d’un toggle.
Conclusion
Vous êtes maintenant prêts à créer vos propres plugins !
La structure de base du template est somme toute assez complète.
Voici les étapes à suivre lors de la création d'un plugin à partir du template :
- Créez un nouveau plugin en dupliquant le template
- Codez dans un premier temps l'UI des paramètres du plugin et de la figure
- Développez progressivement le backend de la création de la figure à partir des paramètres
- Configurez le chargement des paramètres de la figure à partir des fichiers de sauvegarde
- Seulement une fois que tout fonctionne, passez à la génération du code à partir des paramètres de la figure
- Configurez enfin les paramètres généraux du widget
- Une fois le plugin terminé, pensez à le documenter avec la description et à la partager !