11# YT#VH04ddb3b9e9d0bc00e6606ce5e074418b#VH20fe8acb2e57832933eb60226847381a#
22CM <- local({ # _C_hecked _M_odule
3+ # 2026-05-07: [cleanup] Moved `CM$module` to `dv.manager` and `CM$message_well` to DR.R
34 # 2026-03-26: [cleanup] Drop hyperspecific `CM$check_unique_sub_cat_par_vis()`
45 # [cleanup] Drop unused `warning_messages` parameter and related codepaths
56 # [cleanup] Drop type mapping
67 # [feature] New `CM$format_inline_asis` helper for custom diagnostic messages
78 # [feature] Clarify that it is the app creator who is responsible for addressing errors
89 # [cleanup] Split multi-line strings to silence YT.R warning
910 # 2025-07-11: [feature] New `manual_check` flag to tell `CM$generate_check_functions()` to ignore particular elements
10- # 2025-04-09: [fix] Make `generate_map_afmm_function` map multi-variable parameters (e.g. `visit_vars`)
11- # 2025-03-21: [feature] report errors for all loaded datasets
12- # [fix] dehardcode "PARAM" string and use `par` argument
13-
14- message_well <- function (title , contents , color = " f5f5f5" ) {
15- style <- sprintf(
16- paste0(
17- " padding: 0.5rem;" ,
18- " padding-left: 1rem;" ,
19- " margin-bottom: 20px;" ,
20- " background-color: %s;" ,
21- " border: 1px solid #e3e3e3;" ,
22- " border-radius: 4px;" ,
23- " -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.05);" ,
24- " box-shadow: inset 0 1px 1px rgba(0,0,0,.05);"
25- ),
26- color
27- )
28-
29- res <- list (shiny :: h3(title ))
30- if (length(contents )) res <- append(res , list (shiny :: tags [[" div" ]](contents , style = style )))
31- return (res )
32- }
33-
34- app_creator_feedback_ui <- function (id , ui ) {
35- id <- paste(c(id , " validator" ), collapse = " -" )
36- ns <- shiny :: NS(id )
37-
38- hide <- function (e ) shiny :: tags [[" div" ]](e , style = " display: none" )
39-
40- res <- list (
41- shiny :: uiOutput(ns(" ui" )),
42- hide(shiny :: checkboxInput(inputId = ns(" show_ui" ), label = NULL )),
43- shiny :: conditionalPanel(condition = " input.show_ui == true" , ui , ns = ns )
44- )
45- return (res )
46- }
47-
48- app_creator_feedback_server <- function (id , error_messages , preface ) {
49- id <- paste(c(id , " validator" ), collapse = " -" )
50- module <- shiny :: moduleServer(
51- id ,
52- function (input , output , session ) {
53- output [[" ui" ]] <- shiny :: renderUI({
54- res <- list ()
55-
56- if (length(error_messages )) {
57- app_creator_disclaimer <- htmltools :: p(htmltools :: HTML(
58- paste(" <i>Configuration errors prevent this module from running." ,
59- " <b>The following diagnostic information is meant for the app creator</b>.</i>" )
60- ), style = " font-size: small;" )
61-
62- error_messages <- append(list (app_creator_disclaimer ), error_messages )
63- res [[length(res ) + 1 ]] <- message_well(" Module configuration errors" , error_messages , color = " #f4d7d7" )
64- }
65-
66- return (res )
67- })
68- shiny :: outputOptions(output , " ui" , suspendWhenHidden = FALSE )
69-
70- if (length(error_messages ) == 0 ) {
71- shiny :: updateCheckboxInput(inputId = " show_ui" , value = TRUE )
72- }
73- }
74- )
75-
76- return (module )
77- }
78-
79- # Wrap the UI and server of a module so that, once parameterized, they:
80- # - go through a check function prior to running
81- # - provide `dataset_info` to module manager
82- module <- function (module , check_mod_fn , dataset_info_fn ) {
83- local({
84- # Make sure that the signature of `check_mod_fn` matches that of `module` except for the expected differences
85- check_formals <- names(formals(check_mod_fn ))
86- if (! identical(utils :: head(check_formals , 2 ), c(" afmm" , " datasets" ))) {
87- stop(" The first two arguments of check functions passed onto `module` should be `afmm` and `datasets`" )
88- }
89- check_formals <- check_formals [c(- 1 , - 2 )]
90-
91- mod_formals <- names(formals(module ))
92- if (! identical(check_formals , mod_formals )) {
93- stop(paste(
94- " Check function arguments do not exactly match those of the module function" ,
95- " (after discarding `afmm` and `datasets`)"
96- ))
97- }
98- })
99-
100- mandatory_module_args <- local({
101- args <- formals(module )
102- names(args )[sapply(args , function (x ) is.name(x ) && nchar(x ) == 0 )]
103- })
104-
105- wrapper <- function (... ) {
106- # Match arguments explicitly to provide graphical error feedback
107- # https://cran.r-project.org/doc/manuals/r-release/R-lang.html#Argument-matching
108-
109- module_ui <- function (... ) list ()
110- module_server <- function (... ) NULL
111- module_id <- " error_id"
112-
113- matched_args <- try(as.list(match.call(module )), silent = TRUE )
114- error_message <- attr(matched_args , " condition" )$ message
115- if (is.null(error_message )) {
116- missing_args <- setdiff(mandatory_module_args , names(matched_args ))
117- if (length(missing_args )) {
118- error_message <- sprintf(" Missing mandatory arguments: `%s`." , paste(missing_args , collapse = " , " ))
119- }
120- }
121-
122- if (is.null(error_message )) {
123- args <- list (... )
124- evaluated_module <- do.call(module , args )
125- module_ui <- evaluated_module [[" ui" ]]
126- module_server <- evaluated_module [[" server" ]]
127- module_id <- evaluated_module [[" module_id" ]]
128- }
129-
130- res <- list (
131- ui = function (module_id ) app_creator_feedback_ui(module_id , module_ui(module_id )), # `module` UI gated by app_creator_feedback_server
132- server = function (afmm ) {
133- fb <- local({
134- res <- NULL
135- if (! is.null(error_message )) {
136- res <- list (errors = error_message )
137- } else {
138- # NOTE: We check the call here and not inside the module server function because:
139- # - app creators interact with the davinci module and not with the ui-server combo, so
140- # errors reported with respect to the module signature will make sense to them.
141- # The module server function might use a different function signature.
142- # - Here we also have access to the original datasets, which allows us to ensure call
143- # correctness independent of filter state or operation in a single pass.
144- # - "catch errors early"
145- dataset_count <- length(afmm [[" data" ]])
146-
147- res_by_dataset <- list ()
148- error_count_by_dataset <- integer(0 )
149- error_count <- 0
150- for (i_dataset in seq_len(dataset_count )) {
151- dataset <- afmm [[" data" ]][[i_dataset ]]
152- if (is.function(dataset )) dataset <- dataset()
153- check_args <- append(
154- list (
155- afmm = afmm , # To check receiver_ids, among others
156- datasets = dataset # Allows data checks prior to reactive time
157- ),
158- args
159- )
160- info <- do.call(check_mod_fn , check_args )
161- res_by_dataset [[i_dataset ]] <- info
162- error_count_by_dataset [[i_dataset ]] <- length(info [[" errors" ]])
163- error_count <- error_count + length(info [[" errors" ]])
164- }
165-
166- as_items <- function (x ) htmltools :: p(htmltools :: HTML(paste(" \u 2022" , x )))
167-
168- if (error_count == 0 ) NULL
169- else if (dataset_count == 1 ) {
170- # single dataset
171- res <- res_by_dataset [[1 ]]
172- res [[" errors" ]] <- Map(as_items , res [[" errors" ]])
173- } else {
174- # multiple datasets
175- errors <- list ()
176-
177- dataset_names <- names(afmm [[" datasets" ]])
178-
179- errors <- c(
180- list (htmltools :: p(htmltools :: HTML(
181- " Issues have been grouped by input dataset. Expand/collapse the elements below to inspect them:"
182- )))
183- )
184-
185- details_extra <- " open"
186- for (i_dataset in seq_len(dataset_count )){
187- if (error_count_by_dataset [[i_dataset ]] == 0 ) next
188-
189- details_pre <- htmltools :: HTML(
190- sprintf(' <details %s><summary style="display:list-item"><b>%s</b></summary>' ,
191- details_extra , names(afmm [[" data" ]]))[[i_dataset ]])
192- details_extra <- " "
193-
194- details_post <- htmltools :: HTML(" </details>" )
195-
196- errors <- c(errors , list (details_pre ))
197-
198- errors <- c(
199- errors ,
200- list (htmltools :: HTML(paste0(
201- " <div style='padding: 0.5rem; margin-bottom: 1rem; background-color: #FFFFFF55;" ,
202- " border: 1px solid #AAAAAA; border-radius: 4px;'>"
203- ))),
204- Map(as_items , res_by_dataset [[i_dataset ]][[" errors" ]]),
205- list (htmltools :: HTML(" </div>" ))
206- )
207-
208- errors <- c(errors , list (details_post ))
209-
210- res [[" errors" ]] <- errors
211- }
212- }
213- }
214-
215- return (res )
216- })
217-
218- app_creator_feedback_server(id = module_id , error_messages = fb [[" errors" ]], preface = fb [[" preface" ]])
219-
220- if (length(fb [[" errors" ]]) == 0 ) {
221- res <- try(module_server(afmm ), silent = TRUE )
222- }
223-
224- return (res )
225- },
226- module_id = module_id ,
227- meta = list (
228- dataset_info = {
229- # extract defaults from the formals for consistency
230- missing_args <- setdiff(names(formals(module )), names(matched_args ))
231- args <- c(args , formals(module )[missing_args ])
232- do.call(dataset_info_fn , args )
233- }
234- )
235- )
236-
237- return (res )
238- }
239-
240- roxygen_wrapper <- function () { # to keep parameters in the reference docs
241- args <- (match.call() | > as.list())[c(- 1 )]
242- do.call(wrapper , args , envir = parent.frame())
243- }
244- formals(roxygen_wrapper ) <- formals(module )
245- return (roxygen_wrapper )
246- }
11+ # 2025-04-09: [fix] Make `generate_map_afmm_function` map multi-variable parameters (e.g. `visit_vars`)
12+ # 2025-03-21: [feature] Report errors for all loaded datasets
13+ # [fix] Dehardcode "PARAM" string and use `par` argument
24714
24815 container <- function () list2env(x = list (messages = character (0 )), parent = emptyenv())
24916 assert <- function (container , cond , msg ) {
@@ -645,7 +412,6 @@ CM <- local({ # _C_hecked _M_odule
645412 }
646413
647414 list (
648- module = module ,
649415 container = container ,
650416 assert = assert ,
651417 format_inline_asis = format_inline_asis ,
@@ -657,7 +423,6 @@ CM <- local({ # _C_hecked _M_odule
657423 check_choice_from_col_contents = check_choice_from_col_contents ,
658424 check_choice = check_choice ,
659425 check_function = check_function ,
660- check_subjid_col = check_subjid_col ,
661- message_well = message_well
426+ check_subjid_col = check_subjid_col
662427 )
663428})
0 commit comments