diff --git a/DESCRIPTION b/DESCRIPTION index e60b385..8a0b37f 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: clinsight Title: ClinSight Version: 0.1.1.9011 -DevexVersion: 9000 +DevexVersion: 9001 Authors@R: c( person("Leonard Daniël", "Samson", , "lsamson@gcp-service.com", role = c("cre", "aut"), comment = c(ORCID = "0000-0002-6252-7639")), diff --git a/NEWS.md b/NEWS.md index 6110346..4f1fc0f 100644 --- a/NEWS.md +++ b/NEWS.md @@ -9,12 +9,16 @@ - Added form type as a class to be used in `create_table()` to display tables. - Add a logging table to the DB for reviews. - Simplify pulling data from DB for reviews. -- Review data by records IDs instead of subject & form ## Bug fixes - The test-coverage GHA workflow is updated so that codecov uploads work again. +## `devex` changes +- Added `Excel` download button to Queries table & patient listings that need review. +- Added helper function to automatically determine when adding said excel button is appropriate. +- Review data by records IDs instead of subject & form + # clinsight 0.1.1 ## Changed @@ -26,10 +30,6 @@ - Fixed inconsistencies in app messages when saving a review for a form with items with different review states (with some items reviewed previously by a different reviewer, and some items being completely new). - Fixed a bug where clinsight deployed with `shinyproxy` would crash when a user with non-ASCII letters in their name would attempt to login. In this new version, when using the `shinyproxy` deployment configuration, the user name is now expected to be base64 encoded, and will now be base64 encoded by `clinsight` by default, so that the app can also handle non-ASCII signs in user names that are stored in HTTP headers. To display the user name correctly, use base64 encoding in the `application.yml` in ShinyProxy settings (for example: `http-headers.X_SP_USERNAME: "#{T(java.util.Base64).getEncoder().encodeToString(oidcUser.getFullName().getBytes())}"`). -## `devex` changes -- Added `Excel` download button to Queries table & patient listings that need review. -- Added helper function to automatically determine when adding said excel button is appropriate. - # clinsight 0.1.0 ## Changed diff --git a/R/app_server.R b/R/app_server.R index 0135aac..3d0ff4f 100644 --- a/R/app_server.R +++ b/R/app_server.R @@ -30,6 +30,9 @@ app_server <- function( }) check_appdata(app_data, meta) + session$userData$review_records <- reactiveValues() + session$userData$update_checkboxes <- reactiveValues() + res_auth <- authenticate_server( all_sites = app_vars$Sites$site_code, credentials_db = credentials_db, diff --git a/R/fct_tables.R b/R/fct_tables.R index 6d9337a..e54a425 100644 --- a/R/fct_tables.R +++ b/R/fct_tables.R @@ -51,6 +51,17 @@ create_table.default <- function( stopifnot(is.character(keep_vars)) stopifnot(is.character(name_column)) stopifnot(is.character(value_column)) + if ("reviewed" %in% names(data)) { + data <- dplyr::mutate( + data, + o_reviewed = dplyr::case_when( + any(reviewed == "No") & any(reviewed == "Yes") ~ list(list(reviewed = NA, ids = id)), + any(reviewed == "Yes") ~ list(list(reviewed = TRUE, ids = id)), + .default = list(list(reviewed = FALSE, ids = id)) + ), + .by = dplyr::all_of(keep_vars)) + keep_vars <- c("o_reviewed", keep_vars) + } df <- data[c(keep_vars, name_column, value_column)] |> tidyr::pivot_wider( names_from = {{name_column}}, @@ -298,6 +309,7 @@ create_table.medication <- function( ) |> dplyr::arrange(dplyr::desc(in_use), dplyr::desc(`Start Date`)) |> dplyr::select( + dplyr::any_of("o_reviewed"), dplyr::all_of(c(keep_vars, "Name")), dplyr::everything(), -dplyr::all_of(c("in_use", "Active Ingredient", "Trade Name", diff --git a/R/mod_common_forms.R b/R/mod_common_forms.R index 5551c9a..0861d00 100644 --- a/R/mod_common_forms.R +++ b/R/mod_common_forms.R @@ -94,6 +94,7 @@ mod_common_forms_server <- function( ns <- session$ns data_active <- reactive({ + req(!is.null(input$show_all_data)) shiny::validate(need( !is.null(r$filtered_data[[form]]), paste0("Warning: no data found in the database for the form '", form, "'.") @@ -125,23 +126,90 @@ mod_common_forms_server <- function( SAE_data <- data_active() |> dplyr::filter(grepl("Yes", `Serious Adverse Event`)) |> dplyr::select(dplyr::any_of( - c("subject_id","form_repeat", "Name", "AESI", "SAE Start date", + c("o_reviewed", "subject_id","form_repeat", "Name", "AESI", "SAE Start date", "SAE End date", "CTCAE severity", "Treatment related", "Treatment action", "Other action", "SAE Category", "SAE Awareness date", "SAE Date of death", "SAE Death reason") )) |> adjust_colnames("^SAE ") if(!input$show_all_data) SAE_data$subject_id <- NULL - + # determine DT dom / exts / opts DT <- dt_config(SAE_data, table_name = paste("SAE", ifelse(input$show_all_data, "all_patients", r$subject_id), sep = ".")) datatable_custom( - SAE_data, rename_vars = table_names, rownames= FALSE, - title = "Serious Adverse Events", escape = FALSE, - dom = DT$dom, extensions = DT$exts, options = DT$opts - ) + SAE_data, + rename_vars = c("Review Status" = "o_reviewed", table_names), + rownames = FALSE, + title = "Serious Adverse Events", + escape = FALSE, + dom = DT$dom, + extensions = DT$exts, + selection = "none", + callback = checkbox_callback, + options = append(list( + columnDefs = list(list( + targets = 0, + render = checkbox_render + )), + createdRow = checkbox_create_callback + ), + DT$opts)) + }, server = FALSE) + + observeEvent(data_active(), { + session$userData$update_checkboxes[[form]] <- NULL + session$userData$review_records[[form]] <- data.frame(id = integer(), reviewed = character()) + }) + + observeEvent(input$common_form_table_review_selection, { + session$userData$update_checkboxes[[form]] <- NULL + + session$userData$review_records[[form]] <- + dplyr::rows_upsert( + session$userData$review_records[[form]], + input$common_form_table_review_selection, + by = "id" + ) |> + dplyr::filter(!is.na(reviewed)) |> + dplyr::semi_join( + subset(r$review_data, subject_id == r$subject_id & item_group == form), + by = "id" + ) |> + dplyr::anti_join( + subset(r$review_data, subject_id == r$subject_id & item_group == form), + by = c("id", "reviewed") + ) |> + dplyr::arrange(id) + }) + + observeEvent(session$userData$update_checkboxes[[form]], { + checked <- session$userData$update_checkboxes[[form]] + + update_cbs(ns("common_form_table"), checked) + update_cbs(ns("SAE_table"), checked) + }) + + observeEvent(input$SAE_table_review_selection, { + session$userData$update_checkboxes[[form]] <- NULL + + session$userData$review_records[[form]] <- + dplyr::rows_upsert( + session$userData$review_records[[form]], + input$SAE_table_review_selection, + by = "id" + ) |> + dplyr::filter(!is.na(reviewed)) |> + dplyr::semi_join( + subset(r$review_data, subject_id == r$subject_id & item_group == form), + by = "id" + ) |> + dplyr::anti_join( + subset(r$review_data, subject_id == r$subject_id & item_group == form), + by = c("id", "reviewed") + ) |> + dplyr::arrange(id) }) output[["common_form_table"]] <- DT::renderDT({ @@ -153,16 +221,31 @@ mod_common_forms_server <- function( dplyr::select(-dplyr::starts_with("SAE")) } if(!input$show_all_data) df$subject_id <- NULL - + # determine DT dom / exts / opts DT <- dt_config(df, table_name = paste(form, ifelse(input$show_all_data, "all_patients", r$subject_id), sep = ".")) datatable_custom( - df, rename_vars = table_names, rownames= FALSE,title = form, - escape = FALSE, dom = DT$dom, extensions = DT$exts, options = DT$opts) - }) - + df, + rename_vars = c("Review Status" = "o_reviewed", table_names), + rownames= FALSE, + title = form, + escape = FALSE, + dom = DT$dom, + extensions = DT$exts, + selection = "none", + callback = checkbox_callback, + options = append(list( + columnDefs = list(list( + targets = 0, + render = checkbox_render + )), + createdRow = checkbox_create_callback + ), + DT$opts)) + }, server = FALSE) + }) } diff --git a/R/mod_review_forms.R b/R/mod_review_forms.R index 2cea435..b9c5c14 100644 --- a/R/mod_review_forms.R +++ b/R/mod_review_forms.R @@ -20,7 +20,8 @@ mod_review_forms_ui <- function(id){ inputId = ns("form_reviewed"), label = "Reviewed", value = FALSE - ), + ) |> + shiny::tagAppendAttributes(class = "cs_checkbox", .cssSelector = "input"), "Mark as reviewed", placement = "bottom" ), @@ -44,6 +45,7 @@ mod_review_forms_ui <- function(id){ label = NULL ) ), + progress_bar(ns("progress_bar")), bslib::layout_columns( col_widths = c(11, 12), shiny::actionButton( @@ -113,13 +115,52 @@ mod_review_forms_server <- function( with(r$review_data, r$review_data[ subject_id == r$subject_id & item_group == active_form(), ]) - }) + }) + + review_indeterminate <- reactiveVal() + + observeEvent(review_indeterminate(), { + shinyjs::runjs(sprintf("$('#%s').prop('indeterminate', %s)", ns("form_reviewed"), tolower(review_indeterminate()))) + }) + + observe({ + req(session$userData$review_records[[active_form()]]) + review_status <- + review_data_active()[,c("id", "reviewed")] |> + dplyr::rows_update(session$userData$review_records[[active_form()]][,c("id", "reviewed")], by = "id") |> + dplyr::distinct(reviewed) |> + dplyr::pull() + + shinyjs::runjs(sprintf("$('#%s').prop('checked', %s)", ns("form_reviewed"), tolower(identical(review_status, "Yes")))) + review_indeterminate(length(review_status) > 1) + }) |> + bindEvent(active_form(), session$userData$review_records[[active_form()]]) + + observeEvent(r$subject_id, { + session$userData$update_checkboxes[[active_form()]] <- NULL + session$userData$review_records[[active_form()]] <- data.frame(id = integer(), reviewed = character()) + }) + + observeEvent(input$form_reviewed, { + session$userData$update_checkboxes[[active_form()]] <- input$form_reviewed + + session$userData$review_records[[active_form()]] <- + review_data_active() |> + dplyr::mutate(reviewed = ifelse(input$form_reviewed, "Yes", "No")) |> + dplyr::select(id, reviewed) |> + dplyr::anti_join( + subset(r$review_data, item_group == active_form()), + by = c("id", "reviewed") + ) |> + dplyr::arrange(id) + }, ignoreInit = TRUE) observeEvent(c(active_form(), r$subject_id), { cat("Update confirm review button\n\n\n") req(r$review_data) golem::cat_dev("review_data_active:\n") golem::print_dev(review_data_active()) + review_indeterminate(FALSE) if(nrow(review_data_active()) == 0){ cat("No review data found for Subject id: ", r$subject_id, " and group: ", active_form(), "\n") @@ -131,16 +172,14 @@ mod_review_forms_server <- function( # it will give a warning. This would be rare since it would mean a datapoint with the same edit date-time was reviewed but another one was not. # probably better to use defensive coding here to ensure the app does not crash in that case. However we need to define which review status we need to select # in this case get the reviewed = "No" - review_status <- with(review_data_active(), reviewed[edit_date_time == max(as.POSIXct(edit_date_time))]) |> unique() - review_comment <- with(review_data_active(), comment[edit_date_time == max(as.POSIXct(edit_date_time))]) |> unique() - if(length(review_status) != 1) warning("multiple variables in review_status, namely: ", - review_status, "Verify data.") + review_status <- unique(review_data_active()[["reviewed"]]) + review_comment <- with(review_data_active(), comment[edit_date_time == max(as.POSIXct(edit_date_time))]) |> unique() |> paste(collapse = "; ") + if(length(review_status) != 1) + review_indeterminate(TRUE) } - updateCheckboxInput( - inputId = "form_reviewed", - value = identical(review_status, "Yes") - ) + shinyjs::runjs(sprintf("$('#%s').prop('checked', %s)", ns("form_reviewed"), tolower(identical(review_status, "Yes")))) + shinyjs::runjs(sprintf("$('#%s').prop('indeterminate', %s)", ns("form_reviewed"), tolower(review_indeterminate()))) shinyWidgets::updatePrettySwitch( session = session, @@ -180,15 +219,12 @@ mod_review_forms_server <- function( enable_save_review <- reactive({ req( - review_data_active(), - is.logical(input$form_reviewed), + active_form(), + session$userData$review_records[[active_form()]], is.logical(enable_any_review()) ) if(!enable_any_review()) return(FALSE) - any(c( - unique(with(review_data_active(), reviewed[edit_date_time == max(as.POSIXct(edit_date_time))])) == "No" & input$form_reviewed, - unique(with(review_data_active(), reviewed[edit_date_time == max(as.POSIXct(edit_date_time))])) == "Yes" & !input$form_reviewed - )) + nrow(session$userData$review_records[[active_form()]]) != 0 }) observeEvent(c(enable_any_review(), enable_save_review()), { @@ -198,7 +234,7 @@ mod_review_forms_server <- function( shinyjs::enable("save_review") shinyjs::enable("add_comment") shinyjs::enable("review_comment") - } else{ + } else { shinyjs::disable("save_review") shinyjs::disable("add_comment") shinyjs::disable("review_comment") @@ -221,23 +257,17 @@ mod_review_forms_server <- function( review_save_error <- reactiveVal(FALSE) observeEvent(input$save_review, { - req(is.logical(input$form_reviewed), review_data_active()) + req(review_data_active()) req(enable_save_review()) review_save_error(FALSE) - golem::cat_dev("Save review status reviewed:", input$form_reviewed, "\n") + # golem::cat_dev("Save review status reviewed:", input$form_reviewed, "\n") - old_review_status <- if (!input$form_reviewed) "Yes" else "No" - review_records <- review_data_active()[ - review_data_active()$reviewed == old_review_status, - "id", - drop = FALSE - ] |> + review_records <- session$userData$review_records[[active_form()]][c("id", "reviewed")] |> dplyr::mutate( - reviewed = if(input$form_reviewed) "Yes" else "No", comment = ifelse(is.null(input$review_comment), "", input$review_comment), reviewer = paste0(r$user_name, " (", r$user_role, ")"), timestamp = time_stamp(), - status = if(input$form_reviewed) "old" else "new" + status = ifelse(reviewed == "Yes", "old", "new") ) golem::cat_dev("review records to add:\n") @@ -265,6 +295,9 @@ mod_review_forms_server <- function( names(review_records_db) ] + session$userData$update_checkboxes[[active_form()]] <- NULL + session$userData$review_records[[active_form()]] <- data.frame(id = integer(), reviewed = character()) + review_save_error(any( !isTRUE(all.equal(review_records_db, review_records, check.attributes = FALSE)), !isTRUE(all.equal(updated_records_memory, review_records_db, check.attributes = FALSE)) @@ -287,6 +320,21 @@ mod_review_forms_server <- function( showNotification("Input saved successfully", duration = 1, type = "message") }) + output[["progress_bar"]] <- render_progress_bar({ + req( + review_data_active(), + active_form(), + session$userData$review_records[[active_form()]] + ) + + list( + completed = sum(review_data_active()$reviewed == "Yes"), + unmarking = sum(session$userData$review_records[[active_form()]]$reviewed == "No"), + marking = sum(session$userData$review_records[[active_form()]]$reviewed == "Yes"), + total = nrow(review_data_active()) + ) + }) + output[["review_header"]] <- renderText({active_form()}) output[["save_review_error"]] <- renderPrint({ @@ -307,7 +355,7 @@ mod_review_forms_server <- function( "No user name found. Cannot save review" )) validate(need( - !unique(with(review_data_active(), reviewed[edit_date_time == max(as.POSIXct(edit_date_time))])) == "Yes", + any(review_data_active()[["reviewed"]] != "Yes"), "Form already reviewed" )) validate(need(input$form_reviewed, "Requires review")) diff --git a/R/mod_study_forms.R b/R/mod_study_forms.R index 8db7185..ad34d43 100644 --- a/R/mod_study_forms.R +++ b/R/mod_study_forms.R @@ -163,6 +163,7 @@ mod_study_forms_server <- function( }) table_data_active <- reactive({ + req(!is.null(input$show_all)) validate(need( r$filtered_data[[form]], paste0("Warning: no data found in database for the form '", form, "'") @@ -193,6 +194,38 @@ mod_study_forms_server <- function( lapply(add_missing_columns(item_info, cols)[1, cols], isTRUE) }) + observeEvent(table_data_active(), { + session$userData$update_checkboxes[[form]] <- NULL + session$userData$review_records[[form]] <- data.frame(id = integer(), reviewed = character(), row_index = character()) + }) + + observeEvent(input$table_review_selection, { + session$userData$update_checkboxes[[form]] <- NULL + + session$userData$review_records[[form]] <- + dplyr::rows_upsert( + session$userData$review_records[[form]], + input$table_review_selection, + by = "id" + ) |> + dplyr::filter(!is.na(reviewed)) |> + dplyr::semi_join( + subset(r$review_data, subject_id == r$subject_id & item_group == form), + by = "id" + ) |> + dplyr::anti_join( + subset(r$review_data, subject_id == r$subject_id & item_group == form), + by = c("id", "reviewed") + ) |> + dplyr::arrange(id) + }) + + observeEvent(session$userData$update_checkboxes[[form]], { + checked <- session$userData$update_checkboxes[[form]] + + update_cbs(ns("table"), checked) + }) + ############################### Outputs: ################################### dynamic_figure <- reactive({ req(nrow(fig_data()) > 0, scaling_data()) @@ -223,14 +256,28 @@ mod_study_forms_server <- function( output[["table"]] <- DT::renderDT({ req(table_data_active()) - # determine DT dom / exts / opts - DT <- dt_config(table_data_active(), - table_name = paste(form, ifelse(input$show_all, - "all_patients", r$subject_id), sep = ".")) - datatable_custom(table_data_active(), table_names, escape = FALSE, - dom = DT$dom, extensions = DT$exts, options = DT$opts) - }) - + DT <- dt_config(table_data_active(), + table_name = paste(form, ifelse(input$show_all, + "all_patients", r$subject_id), sep = ".")) + datatable_custom( + table_data_active(), + rename_vars = c("Review Status" = "o_reviewed", table_names), + rownames= FALSE, + escape = FALSE, + dom = DT$dom, + extensions = DT$exts, + selection = "none", + callback = checkbox_callback, + options = append(list( + columnDefs = list(list( + targets = 0, + render = checkbox_render + )), + createdRow = checkbox_create_callback + ), + DT$opts)) + }, server = FALSE) + if(form %in% c("Vital signs", "Vitals adjusted")){ shiny::exportTestValues( table_data = table_data_active(), diff --git a/R/shiny.R b/R/shiny.R new file mode 100644 index 0000000..4a697a9 --- /dev/null +++ b/R/shiny.R @@ -0,0 +1,72 @@ +shiny::registerInputHandler('CS.reviewInfo', function(val, ...) { + with(val, data.frame( + id = unlist(ids), + reviewed = ifelse(isTRUE(review), "Yes", ifelse(isFALSE(review), "No", NA_character_)) + )) +}, TRUE) + +checkbox_callback <- DT::JS( + "table.on('click', 'input[type=\"checkbox\"]', function(){", + "var tblId = $(this).closest('.datatables').attr('id');", + "var cell = table.cell($(this).closest('td'));", + "var rowIdx = table.row($(this).closest('tr')).index();", + "var ids = cell.data().ids;", + "var review = $(this).is(':indeterminate') ? null : $(this).is(':checked');", + "cell.data().updated = review;", + "var info = {review: review, ids: ids, row: tblId + '_row_' + rowIdx};", + "Shiny.setInputValue(tblId + '_review_selection:CS.reviewInfo', info);", + "})" +) + +checkbox_render <- DT::JS( + "function(data, type, row, meta) {", + "var reviewed = data.reviewed;", + "var updated = data.updated;", + "return ``;", + "}" +) + +checkbox_create_callback <- DT::JS( + "function(row, data, dataIndex) {", + "if (data[0].reviewed == null) {", + "let cb = row.cells[0].getElementsByTagName('input')[0]", + "cb.indeterminate = true;", + "}", + "}" +) + +update_cbs <- function(tblId, checked) { + "$(':checkbox:not(.indeterminate)', $('#%s .table').DataTable().rows().nodes()).prop('checked', %s)" |> + sprintf(tblId, tolower(checked)) |> + shinyjs::runjs() + "$(':checkbox.indeterminate', $('#%s .table').DataTable().rows().nodes()).prop('checked', %s).prop('indeterminate', false).prop('readOnly', %s)" |> + sprintf(tblId, tolower(checked), tolower(!checked)) |> + shinyjs::runjs() +} + +progress_bar <- function(outputId) { + div( + id = outputId, + class = "cs-progress-container", + div( + class = "cs-progress-bar", + div(class = c("cs-progress", "completed")), + div(class = c("cs-progress", "unmarking")), + div(class = c("cs-progress", "marking")) + ), + div( + class = "cs-completed" + ) + ) +} + +render_progress_bar <- function(expr, env = parent.frame(), quoted = FALSE) { + func <- exprToFunction(expr, env, quoted) + + function(){ + func() + } +} diff --git a/inst/app/www/custom.css b/inst/app/www/custom.css index b69e7f6..7ee0afc 100644 --- a/inst/app/www/custom.css +++ b/inst/app/www/custom.css @@ -1,3 +1,8 @@ +:root { + --cs-completed: #91C483; + --cs-unmarking: #FF6464; + --cs-marking: #97b0f8; +} .dataTables_wrapper .dt-buttons { padding-left: 0.75em; @@ -66,3 +71,57 @@ div.datatables div.header { font-weight: bold; } +tr:has(td input[type="checkbox"].checked:not(:checked))>td, + tr:has(td input[type="checkbox"].indeterminate:not(:indeterminate):not(:checked))>td { + background-color: var(--cs-unmarking, #FF6464); + border-color: var(--cs-unmarking, #FF6464); + color: #000000; +} + +tr:has(td input[type="checkbox"].unchecked:checked)>td, + tr:has(td input[type="checkbox"].indeterminate:not(:indeterminate):checked)>td { + background-color: var(--cs-marking, #97b0f8); + border-color: var(--cs-marking, #97b0f8); + color: #000000; +} + +div.cs-progress-container { + display: flex; + margin-bottom: 1rem; + align-items: center; + gap: 5px; +} + +div.cs-progress-container>.cs-progress-bar { + width: 100%; + background-color: #eeeeee; + border-color: #b3b3b3; + border-style: solid; + border-width: thin; + color: #ffffff; + border-radius: 10px; + height: 10px; + overflow: hidden; + display: flex; +} + +div.cs-progress-container>.cs-completed { + cursor: default; +} + +div.cs-progress-container>.cs-progress-bar>.cs-progress { + height: 100%; + transition: width 1s; +} + +div.cs-progress-container>.cs-progress-bar>.cs-progress.completed { + background-color: var(--cs-completed, #91C483); +} + +div.cs-progress-container>.cs-progress-bar>.cs-progress.unmarking { + background-color: var(--cs-unmarking, #FF6464); +} + +div.cs-progress-container>.cs-progress-bar>.cs-progress.marking { + background-color: var(--cs-marking, #97b0f8); +} diff --git a/inst/app/www/custom.js b/inst/app/www/custom.js new file mode 100644 index 0000000..c2a61be --- /dev/null +++ b/inst/app/www/custom.js @@ -0,0 +1,61 @@ +function ts(cb) { + if (cb.readOnly) { + cb.indeterminate=true; + cb.readOnly=cb.checked=false; + } else if (!cb.checked) { + cb.readOnly=true; + cb.indeterminate=false; + } +} + +$(document).ready(function() { + + /* Define custom Shiny input binding for overall review checkbox. + This is needed to assign an event priority to the checkbox.*/ + var customCheckbox = new Shiny.InputBinding(); + + $.extend(customCheckbox, { + find: function(scope) { + return $(scope).find("input[type='checkbox'].cs_checkbox"); + }, + getValue: function(el) { + return el.checked; + }, + setValue: function(el, value) { + el.checked = value; + }, + subscribe: function(el, callback) { + $(el).on("change.checkboxInputBinding", function() { + Shiny.onInputChange($(this).attr('id'), this.checked, {priority: 'event'}); + }); + }, + unsubscribe: function(el) { + $(el).off(".checkboxInputBinding"); + } + }); + + Shiny.inputBindings.register(customCheckbox); + + /* Define custom Shiny output binding for review progress bar. + It expects 4 values: completed, unmarking, marking, and total.*/ + var customProgressBar = new Shiny.OutputBinding(); + + $.extend(customProgressBar, { + find: function(scope) { + return $(scope).find("div.cs-progress-container"); + }, + renderValue: function(el, data) { + let cmp_pct = (data.completed-data.unmarking)/data.total*100; + let um_pct = data.unmarking/data.total*100; + let m_pct = data.marking/data.total*100; + let true_cmp_pct = data.completed/data.total*100; + $('#' + el.id + " .cs-progress.completed").width(cmp_pct.toFixed(2) + "%") + $('#' + el.id + " .cs-progress.unmarking").width(um_pct.toFixed(2) + "%") + $('#' + el.id + " .cs-progress.marking").width(m_pct.toFixed(2) + "%") + $('#' + el.id + " .cs-completed").html(true_cmp_pct.toFixed(1) + "%") + } + }); + + Shiny.outputBindings.register(customProgressBar) + +}); diff --git a/tests/testthat/_snaps/app_feature_01/app-feature-1-002.json b/tests/testthat/_snaps/app_feature_01/app-feature-1-002.json index e24a32e..5861e60 100644 --- a/tests/testthat/_snaps/app_feature_01/app-feature-1-002.json +++ b/tests/testthat/_snaps/app_feature_01/app-feature-1-002.json @@ -9,7 +9,51 @@ "Scroller", "ColReorder" ], - "container": "\n \n \n
N<\/th>\n Name<\/th>\n AESI<\/th>\n Start date<\/th>\n End date<\/th>\n CTCAE severity<\/th>\n Treatment related<\/th>\n Treatment action<\/th>\n Other action<\/th>\n Category<\/th>\n Awareness date<\/th>\n Date of death<\/th>\n Death reason<\/th>\n <\/tr>\n <\/thead>\n<\/table>", + "data": [ + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ] + ], + "container": "\n \n \n
Review Status<\/th>\n N<\/th>\n Name<\/th>\n AESI<\/th>\n Start date<\/th>\n End date<\/th>\n CTCAE severity<\/th>\n Treatment related<\/th>\n Treatment action<\/th>\n Other action<\/th>\n Category<\/th>\n Awareness date<\/th>\n Date of death<\/th>\n Death reason<\/th>\n <\/tr>\n <\/thead>\n<\/table>", "options": { "scrollY": 400, "scrollX": true, @@ -18,88 +62,94 @@ "scrollResize": true, "scrollCollapse": true, "colReorder": true, - "initComplete": "function() {\n$(this.api().table().container()).find('.header').html(\"Serious Adverse Events\")\n}", - "dom": "f<\"header h5\">ti", "columnDefs": [ + { + "targets": 0, + "render": "function(data, type, row, meta) {\nvar reviewed = data.reviewed;\nvar updated = data.updated;\nreturn ``;\n}" + }, { "className": "dt-right", + "targets": 1 + }, + { + "name": "Review Status", "targets": 0 }, { "name": "N", - "targets": 0 + "targets": 1 }, { "name": "Name", - "targets": 1 + "targets": 2 }, { "name": "AESI", - "targets": 2 + "targets": 3 }, { "name": "Start date", - "targets": 3 + "targets": 4 }, { "name": "End date", - "targets": 4 + "targets": 5 }, { "name": "CTCAE severity", - "targets": 5 + "targets": 6 }, { "name": "Treatment related", - "targets": 6 + "targets": 7 }, { "name": "Treatment action", - "targets": 7 + "targets": 8 }, { "name": "Other action", - "targets": 8 + "targets": 9 }, { "name": "Category", - "targets": 9 + "targets": 10 }, { "name": "Awareness date", - "targets": 10 + "targets": 11 }, { "name": "Date of death", - "targets": 11 + "targets": 12 }, { "name": "Death reason", - "targets": 12 + "targets": 13 } ], + "createdRow": "function(row, data, dataIndex) {\nif (data[0].reviewed == null) {\nlet cb = row.cells[0].getElementsByTagName('input')[0]\ncb.indeterminate = true;\n}\n}", + "initComplete": "function() {\n$(this.api().table().container()).find('.header').html(\"Serious Adverse Events\")\n}", + "dom": "f<\"header h5\">ti", "order": [ ], "autoWidth": false, - "orderClasses": false, - "ajax": { - "type": "POST", - "data": "function(d) {\nd.search.caseInsensitive = true;\nd.search.smart = true;\nd.escape = false;\nvar encodeAmp = function(x) { x.value = x.value.replace(/&/g, \"%26\"); }\nencodeAmp(d.search);\n$.each(d.columns, function(i, v) {encodeAmp(v.search);});\n}" - }, - "serverSide": true, - "processing": true + "orderClasses": false }, + "callback": "function(table) {\ntable.on('click', 'input[type=\"checkbox\"]', function(){\nvar tblId = $(this).closest('.datatables').attr('id');\nvar cell = table.cell($(this).closest('td'));\nvar rowIdx = table.row($(this).closest('tr')).index();\nvar ids = cell.data().ids;\nvar review = $(this).is(':indeterminate') ? null : $(this).is(':checked');\ncell.data().updated = review;\nvar info = {review: review, ids: ids, row: tblId + '_row_' + rowIdx};\nShiny.setInputValue(tblId + '_review_selection:CS.reviewInfo', info);\n})\n}", "selection": { - "mode": "single", + "mode": "none", "selected": null, "target": "row", "selectable": null } }, "evals": [ + "options.columnDefs.0.render", + "options.createdRow", "options.initComplete", - "options.ajax.data" + "callback" ], "jsHooks": [ @@ -243,7 +293,151 @@ "Scroller", "ColReorder" ], - "container": "\n \n \n
N<\/th>\n Name<\/th>\n AESI<\/th>\n start date<\/th>\n end date<\/th>\n CTCAE severity<\/th>\n Treatment related<\/th>\n Treatment action<\/th>\n Other action<\/th>\n Serious Adverse Event<\/th>\n <\/tr>\n <\/thead>\n<\/table>", + "data": [ + [ + { + "reviewed": false, + "ids": [ + 2195, + 2196, + 2197, + 2198, + 2199, + 2200, + 2201, + 2202, + 2235 + ] + }, + { + "reviewed": false, + "ids": [ + 2203, + 2204, + 2205, + 2206, + 2207, + 2208, + 2209, + 2210, + 2236 + ] + }, + { + "reviewed": false, + "ids": [ + 2211, + 2212, + 2213, + 2214, + 2215, + 2216, + 2217, + 2218, + 2237 + ] + }, + { + "reviewed": false, + "ids": [ + 2219, + 2220, + 2221, + 2222, + 2223, + 2224, + 2225, + 2226, + 2238 + ] + }, + { + "reviewed": false, + "ids": [ + 2227, + 2228, + 2229, + 2230, + 2231, + 2232, + 2233, + 2234, + 2239 + ] + } + ], + [ + 1, + 2, + 3, + 4, + 5 + ], + [ + "Hypotension*<\/b>", + "Atrial Fibrillation*<\/b>", + "Tachycardia*<\/b>", + "Urinary Tract Infection*<\/b>", + "Atrial Fibrillation*<\/b>" + ], + [ + "None*<\/b>", + "None*<\/b>", + "None*<\/b>", + "None*<\/b>", + "None*<\/b>" + ], + [ + "2023-07-07*<\/b>", + "2023-07-05*<\/b>", + "2023-07-05*<\/b>", + "2023-07-05*<\/b>", + "2023-08-16*<\/b>" + ], + [ + null, + null, + null, + null, + null + ], + [ + "Grade 1*<\/b>", + "Grade 2*<\/b>", + "Grade 2*<\/b>", + "Grade 1*<\/b>", + "Grade 2*<\/b>" + ], + [ + "Not related*<\/b>", + "Not related*<\/b>", + "Not related*<\/b>", + "Not related*<\/b>", + "Not related*<\/b>" + ], + [ + "Not Applicable*<\/b>", + "Not Applicable*<\/b>", + "Not Applicable*<\/b>", + "Not Applicable*<\/b>", + "Not Applicable*<\/b>" + ], + [ + "None*<\/b>", + "Concomitant therapy/medication*<\/b>", + "Concomitant therapy/medication*<\/b>", + "Concomitant therapy/medication*<\/b>", + "Concomitant therapy/medication*<\/b>" + ], + [ + "No*<\/b>", + "No*<\/b>", + "No*<\/b>", + "No*<\/b>", + "No*<\/b>" + ] + ], + "container": "\n \n \n
Review Status<\/th>\n N<\/th>\n Name<\/th>\n AESI<\/th>\n start date<\/th>\n end date<\/th>\n CTCAE severity<\/th>\n Treatment related<\/th>\n Treatment action<\/th>\n Other action<\/th>\n Serious Adverse Event<\/th>\n <\/tr>\n <\/thead>\n<\/table>", "options": { "scrollY": 400, "scrollX": true, @@ -252,83 +446,89 @@ "scrollResize": true, "scrollCollapse": true, "colReorder": true, - "buttons": [ - { - "extend": "excel", - "text": "<\/i>", - "filename": "clinsight.Adverse-events.BEL_04_772" - } - ], - "initComplete": "function() {\n$(this.api().table().container()).find('.header').html(\"Adverse events\")\n}", - "dom": "Bf<\"header h5\">ti", "columnDefs": [ + { + "targets": 0, + "render": "function(data, type, row, meta) {\nvar reviewed = data.reviewed;\nvar updated = data.updated;\nreturn ``;\n}" + }, { "className": "dt-right", + "targets": 1 + }, + { + "name": "Review Status", "targets": 0 }, { "name": "N", - "targets": 0 + "targets": 1 }, { "name": "Name", - "targets": 1 + "targets": 2 }, { "name": "AESI", - "targets": 2 + "targets": 3 }, { "name": "start date", - "targets": 3 + "targets": 4 }, { "name": "end date", - "targets": 4 + "targets": 5 }, { "name": "CTCAE severity", - "targets": 5 + "targets": 6 }, { "name": "Treatment related", - "targets": 6 + "targets": 7 }, { "name": "Treatment action", - "targets": 7 + "targets": 8 }, { "name": "Other action", - "targets": 8 + "targets": 9 }, { "name": "Serious Adverse Event", - "targets": 9 + "targets": 10 } ], + "createdRow": "function(row, data, dataIndex) {\nif (data[0].reviewed == null) {\nlet cb = row.cells[0].getElementsByTagName('input')[0]\ncb.indeterminate = true;\n}\n}", + "buttons": [ + { + "extend": "excel", + "text": "<\/i>", + "filename": "clinsight.Adverse-events.BEL_04_772" + } + ], + "initComplete": "function() {\n$(this.api().table().container()).find('.header').html(\"Adverse events\")\n}", + "dom": "Bf<\"header h5\">ti", "order": [ ], "autoWidth": false, - "orderClasses": false, - "ajax": { - "type": "POST", - "data": "function(d) {\nd.search.caseInsensitive = true;\nd.search.smart = true;\nd.escape = false;\nvar encodeAmp = function(x) { x.value = x.value.replace(/&/g, \"%26\"); }\nencodeAmp(d.search);\n$.each(d.columns, function(i, v) {encodeAmp(v.search);});\n}" - }, - "serverSide": true, - "processing": true + "orderClasses": false }, + "callback": "function(table) {\ntable.on('click', 'input[type=\"checkbox\"]', function(){\nvar tblId = $(this).closest('.datatables').attr('id');\nvar cell = table.cell($(this).closest('td'));\nvar rowIdx = table.row($(this).closest('tr')).index();\nvar ids = cell.data().ids;\nvar review = $(this).is(':indeterminate') ? null : $(this).is(':checked');\ncell.data().updated = review;\nvar info = {review: review, ids: ids, row: tblId + '_row_' + rowIdx};\nShiny.setInputValue(tblId + '_review_selection:CS.reviewInfo', info);\n})\n}", "selection": { - "mode": "single", + "mode": "none", "selected": null, "target": "row", "selectable": null } }, "evals": [ + "options.columnDefs.0.render", + "options.createdRow", "options.initComplete", - "options.ajax.data" + "callback" ], "jsHooks": [ @@ -815,6 +1015,12 @@ ] }, "main_sidebar_1-navigate_forms_1-form_name": "
Adverse events<\/b><\/center>", + "main_sidebar_1-review_forms_1-progress_bar": { + "completed": 0, + "unmarking": 0, + "marking": 0, + "total": 45 + }, "main_sidebar_1-review_forms_1-save_review_error": { "message": "Requires review", "call": "NULL", diff --git a/tests/testthat/_snaps/app_feature_01/app-feature-1-003.json b/tests/testthat/_snaps/app_feature_01/app-feature-1-003.json index e0fd19b..3f12732 100644 --- a/tests/testthat/_snaps/app_feature_01/app-feature-1-003.json +++ b/tests/testthat/_snaps/app_feature_01/app-feature-1-003.json @@ -9,7 +9,51 @@ "Scroller", "ColReorder" ], - "container": "\n \n \n
N<\/th>\n Name<\/th>\n AESI<\/th>\n Start date<\/th>\n End date<\/th>\n CTCAE severity<\/th>\n Treatment related<\/th>\n Treatment action<\/th>\n Other action<\/th>\n Category<\/th>\n Awareness date<\/th>\n Date of death<\/th>\n Death reason<\/th>\n <\/tr>\n <\/thead>\n<\/table>", + "data": [ + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ] + ], + "container": "\n \n \n
Review Status<\/th>\n N<\/th>\n Name<\/th>\n AESI<\/th>\n Start date<\/th>\n End date<\/th>\n CTCAE severity<\/th>\n Treatment related<\/th>\n Treatment action<\/th>\n Other action<\/th>\n Category<\/th>\n Awareness date<\/th>\n Date of death<\/th>\n Death reason<\/th>\n <\/tr>\n <\/thead>\n<\/table>", "options": { "scrollY": 400, "scrollX": true, @@ -18,88 +62,94 @@ "scrollResize": true, "scrollCollapse": true, "colReorder": true, - "initComplete": "function() {\n$(this.api().table().container()).find('.header').html(\"Serious Adverse Events\")\n}", - "dom": "f<\"header h5\">ti", "columnDefs": [ + { + "targets": 0, + "render": "function(data, type, row, meta) {\nvar reviewed = data.reviewed;\nvar updated = data.updated;\nreturn ``;\n}" + }, { "className": "dt-right", + "targets": 1 + }, + { + "name": "Review Status", "targets": 0 }, { "name": "N", - "targets": 0 + "targets": 1 }, { "name": "Name", - "targets": 1 + "targets": 2 }, { "name": "AESI", - "targets": 2 + "targets": 3 }, { "name": "Start date", - "targets": 3 + "targets": 4 }, { "name": "End date", - "targets": 4 + "targets": 5 }, { "name": "CTCAE severity", - "targets": 5 + "targets": 6 }, { "name": "Treatment related", - "targets": 6 + "targets": 7 }, { "name": "Treatment action", - "targets": 7 + "targets": 8 }, { "name": "Other action", - "targets": 8 + "targets": 9 }, { "name": "Category", - "targets": 9 + "targets": 10 }, { "name": "Awareness date", - "targets": 10 + "targets": 11 }, { "name": "Date of death", - "targets": 11 + "targets": 12 }, { "name": "Death reason", - "targets": 12 + "targets": 13 } ], + "createdRow": "function(row, data, dataIndex) {\nif (data[0].reviewed == null) {\nlet cb = row.cells[0].getElementsByTagName('input')[0]\ncb.indeterminate = true;\n}\n}", + "initComplete": "function() {\n$(this.api().table().container()).find('.header').html(\"Serious Adverse Events\")\n}", + "dom": "f<\"header h5\">ti", "order": [ ], "autoWidth": false, - "orderClasses": false, - "ajax": { - "type": "POST", - "data": "function(d) {\nd.search.caseInsensitive = true;\nd.search.smart = true;\nd.escape = false;\nvar encodeAmp = function(x) { x.value = x.value.replace(/&/g, \"%26\"); }\nencodeAmp(d.search);\n$.each(d.columns, function(i, v) {encodeAmp(v.search);});\n}" - }, - "serverSide": true, - "processing": true + "orderClasses": false }, + "callback": "function(table) {\ntable.on('click', 'input[type=\"checkbox\"]', function(){\nvar tblId = $(this).closest('.datatables').attr('id');\nvar cell = table.cell($(this).closest('td'));\nvar rowIdx = table.row($(this).closest('tr')).index();\nvar ids = cell.data().ids;\nvar review = $(this).is(':indeterminate') ? null : $(this).is(':checked');\ncell.data().updated = review;\nvar info = {review: review, ids: ids, row: tblId + '_row_' + rowIdx};\nShiny.setInputValue(tblId + '_review_selection:CS.reviewInfo', info);\n})\n}", "selection": { - "mode": "single", + "mode": "none", "selected": null, "target": "row", "selectable": null } }, "evals": [ + "options.columnDefs.0.render", + "options.createdRow", "options.initComplete", - "options.ajax.data" + "callback" ], "jsHooks": [ @@ -243,7 +293,151 @@ "Scroller", "ColReorder" ], - "container": "\n \n \n
N<\/th>\n Name<\/th>\n AESI<\/th>\n start date<\/th>\n end date<\/th>\n CTCAE severity<\/th>\n Treatment related<\/th>\n Treatment action<\/th>\n Other action<\/th>\n Serious Adverse Event<\/th>\n <\/tr>\n <\/thead>\n<\/table>", + "data": [ + [ + { + "reviewed": false, + "ids": [ + 2195, + 2196, + 2197, + 2198, + 2199, + 2200, + 2201, + 2202, + 2235 + ] + }, + { + "reviewed": false, + "ids": [ + 2203, + 2204, + 2205, + 2206, + 2207, + 2208, + 2209, + 2210, + 2236 + ] + }, + { + "reviewed": false, + "ids": [ + 2211, + 2212, + 2213, + 2214, + 2215, + 2216, + 2217, + 2218, + 2237 + ] + }, + { + "reviewed": false, + "ids": [ + 2219, + 2220, + 2221, + 2222, + 2223, + 2224, + 2225, + 2226, + 2238 + ] + }, + { + "reviewed": false, + "ids": [ + 2227, + 2228, + 2229, + 2230, + 2231, + 2232, + 2233, + 2234, + 2239 + ] + } + ], + [ + 1, + 2, + 3, + 4, + 5 + ], + [ + "Hypotension*<\/b>", + "Atrial Fibrillation*<\/b>", + "Tachycardia*<\/b>", + "Urinary Tract Infection*<\/b>", + "Atrial Fibrillation*<\/b>" + ], + [ + "None*<\/b>", + "None*<\/b>", + "None*<\/b>", + "None*<\/b>", + "None*<\/b>" + ], + [ + "2023-07-07*<\/b>", + "2023-07-05*<\/b>", + "2023-07-05*<\/b>", + "2023-07-05*<\/b>", + "2023-08-16*<\/b>" + ], + [ + null, + null, + null, + null, + null + ], + [ + "Grade 1*<\/b>", + "Grade 2*<\/b>", + "Grade 2*<\/b>", + "Grade 1*<\/b>", + "Grade 2*<\/b>" + ], + [ + "Not related*<\/b>", + "Not related*<\/b>", + "Not related*<\/b>", + "Not related*<\/b>", + "Not related*<\/b>" + ], + [ + "Not Applicable*<\/b>", + "Not Applicable*<\/b>", + "Not Applicable*<\/b>", + "Not Applicable*<\/b>", + "Not Applicable*<\/b>" + ], + [ + "None*<\/b>", + "Concomitant therapy/medication*<\/b>", + "Concomitant therapy/medication*<\/b>", + "Concomitant therapy/medication*<\/b>", + "Concomitant therapy/medication*<\/b>" + ], + [ + "No*<\/b>", + "No*<\/b>", + "No*<\/b>", + "No*<\/b>", + "No*<\/b>" + ] + ], + "container": "\n \n \n
Review Status<\/th>\n N<\/th>\n Name<\/th>\n AESI<\/th>\n start date<\/th>\n end date<\/th>\n CTCAE severity<\/th>\n Treatment related<\/th>\n Treatment action<\/th>\n Other action<\/th>\n Serious Adverse Event<\/th>\n <\/tr>\n <\/thead>\n<\/table>", "options": { "scrollY": 400, "scrollX": true, @@ -252,83 +446,89 @@ "scrollResize": true, "scrollCollapse": true, "colReorder": true, - "buttons": [ - { - "extend": "excel", - "text": "<\/i>", - "filename": "clinsight.Adverse-events.BEL_04_772" - } - ], - "initComplete": "function() {\n$(this.api().table().container()).find('.header').html(\"Adverse events\")\n}", - "dom": "Bf<\"header h5\">ti", "columnDefs": [ + { + "targets": 0, + "render": "function(data, type, row, meta) {\nvar reviewed = data.reviewed;\nvar updated = data.updated;\nreturn ``;\n}" + }, { "className": "dt-right", + "targets": 1 + }, + { + "name": "Review Status", "targets": 0 }, { "name": "N", - "targets": 0 + "targets": 1 }, { "name": "Name", - "targets": 1 + "targets": 2 }, { "name": "AESI", - "targets": 2 + "targets": 3 }, { "name": "start date", - "targets": 3 + "targets": 4 }, { "name": "end date", - "targets": 4 + "targets": 5 }, { "name": "CTCAE severity", - "targets": 5 + "targets": 6 }, { "name": "Treatment related", - "targets": 6 + "targets": 7 }, { "name": "Treatment action", - "targets": 7 + "targets": 8 }, { "name": "Other action", - "targets": 8 + "targets": 9 }, { "name": "Serious Adverse Event", - "targets": 9 + "targets": 10 } ], + "createdRow": "function(row, data, dataIndex) {\nif (data[0].reviewed == null) {\nlet cb = row.cells[0].getElementsByTagName('input')[0]\ncb.indeterminate = true;\n}\n}", + "buttons": [ + { + "extend": "excel", + "text": "<\/i>", + "filename": "clinsight.Adverse-events.BEL_04_772" + } + ], + "initComplete": "function() {\n$(this.api().table().container()).find('.header').html(\"Adverse events\")\n}", + "dom": "Bf<\"header h5\">ti", "order": [ ], "autoWidth": false, - "orderClasses": false, - "ajax": { - "type": "POST", - "data": "function(d) {\nd.search.caseInsensitive = true;\nd.search.smart = true;\nd.escape = false;\nvar encodeAmp = function(x) { x.value = x.value.replace(/&/g, \"%26\"); }\nencodeAmp(d.search);\n$.each(d.columns, function(i, v) {encodeAmp(v.search);});\n}" - }, - "serverSide": true, - "processing": true + "orderClasses": false }, + "callback": "function(table) {\ntable.on('click', 'input[type=\"checkbox\"]', function(){\nvar tblId = $(this).closest('.datatables').attr('id');\nvar cell = table.cell($(this).closest('td'));\nvar rowIdx = table.row($(this).closest('tr')).index();\nvar ids = cell.data().ids;\nvar review = $(this).is(':indeterminate') ? null : $(this).is(':checked');\ncell.data().updated = review;\nvar info = {review: review, ids: ids, row: tblId + '_row_' + rowIdx};\nShiny.setInputValue(tblId + '_review_selection:CS.reviewInfo', info);\n})\n}", "selection": { - "mode": "single", + "mode": "none", "selected": null, "target": "row", "selectable": null } }, "evals": [ + "options.columnDefs.0.render", + "options.createdRow", "options.initComplete", - "options.ajax.data" + "callback" ], "jsHooks": [ @@ -815,6 +1015,12 @@ ] }, "main_sidebar_1-navigate_forms_1-form_name": "
Vital signs<\/b><\/center>", + "main_sidebar_1-review_forms_1-progress_bar": { + "completed": 0, + "unmarking": 0, + "marking": 0, + "total": 47 + }, "main_sidebar_1-review_forms_1-save_review_error": { "message": "Requires review", "call": "NULL", diff --git a/tests/testthat/_snaps/app_feature_01/app-feature-1-004.json b/tests/testthat/_snaps/app_feature_01/app-feature-1-004.json index f1c108f..c6551b7 100644 --- a/tests/testthat/_snaps/app_feature_01/app-feature-1-004.json +++ b/tests/testthat/_snaps/app_feature_01/app-feature-1-004.json @@ -9,7 +9,51 @@ "Scroller", "ColReorder" ], - "container": "\n \n \n
N<\/th>\n Name<\/th>\n AESI<\/th>\n Start date<\/th>\n End date<\/th>\n CTCAE severity<\/th>\n Treatment related<\/th>\n Treatment action<\/th>\n Other action<\/th>\n Category<\/th>\n Awareness date<\/th>\n Date of death<\/th>\n Death reason<\/th>\n <\/tr>\n <\/thead>\n<\/table>", + "data": [ + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ] + ], + "container": "\n \n \n
Review Status<\/th>\n N<\/th>\n Name<\/th>\n AESI<\/th>\n Start date<\/th>\n End date<\/th>\n CTCAE severity<\/th>\n Treatment related<\/th>\n Treatment action<\/th>\n Other action<\/th>\n Category<\/th>\n Awareness date<\/th>\n Date of death<\/th>\n Death reason<\/th>\n <\/tr>\n <\/thead>\n<\/table>", "options": { "scrollY": 400, "scrollX": true, @@ -18,88 +62,94 @@ "scrollResize": true, "scrollCollapse": true, "colReorder": true, - "initComplete": "function() {\n$(this.api().table().container()).find('.header').html(\"Serious Adverse Events\")\n}", - "dom": "f<\"header h5\">ti", "columnDefs": [ + { + "targets": 0, + "render": "function(data, type, row, meta) {\nvar reviewed = data.reviewed;\nvar updated = data.updated;\nreturn ``;\n}" + }, { "className": "dt-right", + "targets": 1 + }, + { + "name": "Review Status", "targets": 0 }, { "name": "N", - "targets": 0 + "targets": 1 }, { "name": "Name", - "targets": 1 + "targets": 2 }, { "name": "AESI", - "targets": 2 + "targets": 3 }, { "name": "Start date", - "targets": 3 + "targets": 4 }, { "name": "End date", - "targets": 4 + "targets": 5 }, { "name": "CTCAE severity", - "targets": 5 + "targets": 6 }, { "name": "Treatment related", - "targets": 6 + "targets": 7 }, { "name": "Treatment action", - "targets": 7 + "targets": 8 }, { "name": "Other action", - "targets": 8 + "targets": 9 }, { "name": "Category", - "targets": 9 + "targets": 10 }, { "name": "Awareness date", - "targets": 10 + "targets": 11 }, { "name": "Date of death", - "targets": 11 + "targets": 12 }, { "name": "Death reason", - "targets": 12 + "targets": 13 } ], + "createdRow": "function(row, data, dataIndex) {\nif (data[0].reviewed == null) {\nlet cb = row.cells[0].getElementsByTagName('input')[0]\ncb.indeterminate = true;\n}\n}", + "initComplete": "function() {\n$(this.api().table().container()).find('.header').html(\"Serious Adverse Events\")\n}", + "dom": "f<\"header h5\">ti", "order": [ ], "autoWidth": false, - "orderClasses": false, - "ajax": { - "type": "POST", - "data": "function(d) {\nd.search.caseInsensitive = true;\nd.search.smart = true;\nd.escape = false;\nvar encodeAmp = function(x) { x.value = x.value.replace(/&/g, \"%26\"); }\nencodeAmp(d.search);\n$.each(d.columns, function(i, v) {encodeAmp(v.search);});\n}" - }, - "serverSide": true, - "processing": true + "orderClasses": false }, + "callback": "function(table) {\ntable.on('click', 'input[type=\"checkbox\"]', function(){\nvar tblId = $(this).closest('.datatables').attr('id');\nvar cell = table.cell($(this).closest('td'));\nvar rowIdx = table.row($(this).closest('tr')).index();\nvar ids = cell.data().ids;\nvar review = $(this).is(':indeterminate') ? null : $(this).is(':checked');\ncell.data().updated = review;\nvar info = {review: review, ids: ids, row: tblId + '_row_' + rowIdx};\nShiny.setInputValue(tblId + '_review_selection:CS.reviewInfo', info);\n})\n}", "selection": { - "mode": "single", + "mode": "none", "selected": null, "target": "row", "selectable": null } }, "evals": [ + "options.columnDefs.0.render", + "options.createdRow", "options.initComplete", - "options.ajax.data" + "callback" ], "jsHooks": [ @@ -243,7 +293,151 @@ "Scroller", "ColReorder" ], - "container": "\n \n \n
N<\/th>\n Name<\/th>\n AESI<\/th>\n start date<\/th>\n end date<\/th>\n CTCAE severity<\/th>\n Treatment related<\/th>\n Treatment action<\/th>\n Other action<\/th>\n Serious Adverse Event<\/th>\n <\/tr>\n <\/thead>\n<\/table>", + "data": [ + [ + { + "reviewed": false, + "ids": [ + 2195, + 2196, + 2197, + 2198, + 2199, + 2200, + 2201, + 2202, + 2235 + ] + }, + { + "reviewed": false, + "ids": [ + 2203, + 2204, + 2205, + 2206, + 2207, + 2208, + 2209, + 2210, + 2236 + ] + }, + { + "reviewed": false, + "ids": [ + 2211, + 2212, + 2213, + 2214, + 2215, + 2216, + 2217, + 2218, + 2237 + ] + }, + { + "reviewed": false, + "ids": [ + 2219, + 2220, + 2221, + 2222, + 2223, + 2224, + 2225, + 2226, + 2238 + ] + }, + { + "reviewed": false, + "ids": [ + 2227, + 2228, + 2229, + 2230, + 2231, + 2232, + 2233, + 2234, + 2239 + ] + } + ], + [ + 1, + 2, + 3, + 4, + 5 + ], + [ + "Hypotension*<\/b>", + "Atrial Fibrillation*<\/b>", + "Tachycardia*<\/b>", + "Urinary Tract Infection*<\/b>", + "Atrial Fibrillation*<\/b>" + ], + [ + "None*<\/b>", + "None*<\/b>", + "None*<\/b>", + "None*<\/b>", + "None*<\/b>" + ], + [ + "2023-07-07*<\/b>", + "2023-07-05*<\/b>", + "2023-07-05*<\/b>", + "2023-07-05*<\/b>", + "2023-08-16*<\/b>" + ], + [ + null, + null, + null, + null, + null + ], + [ + "Grade 1*<\/b>", + "Grade 2*<\/b>", + "Grade 2*<\/b>", + "Grade 1*<\/b>", + "Grade 2*<\/b>" + ], + [ + "Not related*<\/b>", + "Not related*<\/b>", + "Not related*<\/b>", + "Not related*<\/b>", + "Not related*<\/b>" + ], + [ + "Not Applicable*<\/b>", + "Not Applicable*<\/b>", + "Not Applicable*<\/b>", + "Not Applicable*<\/b>", + "Not Applicable*<\/b>" + ], + [ + "None*<\/b>", + "Concomitant therapy/medication*<\/b>", + "Concomitant therapy/medication*<\/b>", + "Concomitant therapy/medication*<\/b>", + "Concomitant therapy/medication*<\/b>" + ], + [ + "No*<\/b>", + "No*<\/b>", + "No*<\/b>", + "No*<\/b>", + "No*<\/b>" + ] + ], + "container": "\n \n \n
Review Status<\/th>\n N<\/th>\n Name<\/th>\n AESI<\/th>\n start date<\/th>\n end date<\/th>\n CTCAE severity<\/th>\n Treatment related<\/th>\n Treatment action<\/th>\n Other action<\/th>\n Serious Adverse Event<\/th>\n <\/tr>\n <\/thead>\n<\/table>", "options": { "scrollY": 400, "scrollX": true, @@ -252,83 +446,89 @@ "scrollResize": true, "scrollCollapse": true, "colReorder": true, - "buttons": [ - { - "extend": "excel", - "text": "<\/i>", - "filename": "clinsight.Adverse-events.BEL_04_772" - } - ], - "initComplete": "function() {\n$(this.api().table().container()).find('.header').html(\"Adverse events\")\n}", - "dom": "Bf<\"header h5\">ti", "columnDefs": [ + { + "targets": 0, + "render": "function(data, type, row, meta) {\nvar reviewed = data.reviewed;\nvar updated = data.updated;\nreturn ``;\n}" + }, { "className": "dt-right", + "targets": 1 + }, + { + "name": "Review Status", "targets": 0 }, { "name": "N", - "targets": 0 + "targets": 1 }, { "name": "Name", - "targets": 1 + "targets": 2 }, { "name": "AESI", - "targets": 2 + "targets": 3 }, { "name": "start date", - "targets": 3 + "targets": 4 }, { "name": "end date", - "targets": 4 + "targets": 5 }, { "name": "CTCAE severity", - "targets": 5 + "targets": 6 }, { "name": "Treatment related", - "targets": 6 + "targets": 7 }, { "name": "Treatment action", - "targets": 7 + "targets": 8 }, { "name": "Other action", - "targets": 8 + "targets": 9 }, { "name": "Serious Adverse Event", - "targets": 9 + "targets": 10 } ], + "createdRow": "function(row, data, dataIndex) {\nif (data[0].reviewed == null) {\nlet cb = row.cells[0].getElementsByTagName('input')[0]\ncb.indeterminate = true;\n}\n}", + "buttons": [ + { + "extend": "excel", + "text": "<\/i>", + "filename": "clinsight.Adverse-events.BEL_04_772" + } + ], + "initComplete": "function() {\n$(this.api().table().container()).find('.header').html(\"Adverse events\")\n}", + "dom": "Bf<\"header h5\">ti", "order": [ ], "autoWidth": false, - "orderClasses": false, - "ajax": { - "type": "POST", - "data": "function(d) {\nd.search.caseInsensitive = true;\nd.search.smart = true;\nd.escape = false;\nvar encodeAmp = function(x) { x.value = x.value.replace(/&/g, \"%26\"); }\nencodeAmp(d.search);\n$.each(d.columns, function(i, v) {encodeAmp(v.search);});\n}" - }, - "serverSide": true, - "processing": true + "orderClasses": false }, + "callback": "function(table) {\ntable.on('click', 'input[type=\"checkbox\"]', function(){\nvar tblId = $(this).closest('.datatables').attr('id');\nvar cell = table.cell($(this).closest('td'));\nvar rowIdx = table.row($(this).closest('tr')).index();\nvar ids = cell.data().ids;\nvar review = $(this).is(':indeterminate') ? null : $(this).is(':checked');\ncell.data().updated = review;\nvar info = {review: review, ids: ids, row: tblId + '_row_' + rowIdx};\nShiny.setInputValue(tblId + '_review_selection:CS.reviewInfo', info);\n})\n}", "selection": { - "mode": "single", + "mode": "none", "selected": null, "target": "row", "selectable": null } }, "evals": [ + "options.columnDefs.0.render", + "options.createdRow", "options.initComplete", - "options.ajax.data" + "callback" ], "jsHooks": [ @@ -815,6 +1015,12 @@ ] }, "main_sidebar_1-navigate_forms_1-form_name": "
Vital signs<\/b><\/center>", + "main_sidebar_1-review_forms_1-progress_bar": { + "completed": 0, + "unmarking": 0, + "marking": 0, + "total": 47 + }, "main_sidebar_1-review_forms_1-save_review_error": { "message": "Requires review", "call": "NULL", @@ -8313,7 +8519,174 @@ "Scroller", "ColReorder" ], - "container": "\n \n \n
<\/th>\n Event<\/th>\n Systolic blood pressure<\/th>\n Diastolic blood pressure<\/th>\n Pulse<\/th>\n Resp<\/th>\n Temperature<\/th>\n Weight change since screening<\/th>\n BMI<\/th>\n Weight<\/th>\n <\/tr>\n <\/thead>\n<\/table>", + "data": [ + [ + { + "reviewed": false, + "ids": [ + 1217, + 1218, + 1219, + 1220, + 1221, + 1222, + 1223, + 1441 + ] + }, + { + "reviewed": false, + "ids": [ + 1224, + 1225, + 1226, + 1227, + 1228, + 1229, + 1230, + 1442 + ] + }, + { + "reviewed": false, + "ids": [ + 1231, + 1232, + 1233, + 1234, + 1235 + ] + }, + { + "reviewed": false, + "ids": [ + 1236, + 1237, + 1238, + 1239, + 1240, + 1241, + 1242, + 1443 + ] + }, + { + "reviewed": false, + "ids": [ + 1243, + 1244, + 1245, + 1246, + 1247 + ] + }, + { + "reviewed": false, + "ids": [ + 1248, + 1249, + 1250, + 1251, + 1252, + 1253, + 1254, + 1444 + ] + }, + { + "reviewed": false, + "ids": [ + 1255, + 1256, + 1257, + 1258, + 1259 + ] + } + ], + [ + "Screening", + "Visit 1", + "Visit 2", + "Visit 3", + "Visit 4", + "Visit 5", + "Visit 6" + ], + [ + "131*<\/b> mmHg", + "132*<\/b> mmHg", + "129*<\/b> mmHg", + "117*<\/b> mmHg", + "128*<\/b> mmHg", + "137*<\/b> mmHg", + "131*<\/b> mmHg" + ], + [ + "68*<\/b> mmHg", + "79*<\/b> mmHg", + "73*<\/b> mmHg", + "77*<\/b> mmHg", + "62*<\/b> mmHg", + "76*<\/b> mmHg", + "61*<\/b> mmHg" + ], + [ + "76*<\/b> beats/min", + "63*<\/b> beats/min", + "73*<\/b> beats/min", + "70*<\/b> beats/min", + "70*<\/b> beats/min", + "75*<\/b> beats/min", + "72*<\/b> beats/min" + ], + [ + "17*<\/b> breaths/min", + "16*<\/b> breaths/min", + "16*<\/b> breaths/min", + "18*<\/b> breaths/min", + "18*<\/b> breaths/min", + "18*<\/b> breaths/min", + "19*<\/b> breaths/min" + ], + [ + "37*<\/b> °C", + "36.7*<\/b> °C", + "36.9*<\/b> °C", + "36*<\/b> °C", + "36.6*<\/b> °C", + "36.1*<\/b> °C", + "36.9*<\/b> °C" + ], + [ + "0.01*<\/b> %", + "2*<\/b> %", + null, + "0.01*<\/b> %", + null, + "0.01*<\/b> %", + null + ], + [ + "17.44*<\/b> kg/m2", + "17.1*<\/b> kg/m2", + null, + "17.1*<\/b> kg/m2", + null, + "24.13*<\/b> kg/m2", + null + ], + [ + "61*<\/b> kg", + "62*<\/b> kg", + null, + "50*<\/b> kg", + null, + "50*<\/b> kg", + null + ] + ], + "container": "\n \n \n
Review Status<\/th>\n Event<\/th>\n Systolic blood pressure<\/th>\n Diastolic blood pressure<\/th>\n Pulse<\/th>\n Resp<\/th>\n Temperature<\/th>\n Weight change since screening<\/th>\n BMI<\/th>\n Weight<\/th>\n <\/tr>\n <\/thead>\n<\/table>", "options": { "scrollY": 400, "scrollX": true, @@ -8322,22 +8695,13 @@ "scrollResize": true, "scrollCollapse": true, "colReorder": true, - "buttons": [ - { - "extend": "excel", - "text": "<\/i>", - "filename": "clinsight.Vital-signs.BEL_04_772" - } - ], - "initComplete": "function() {\n$(this.api().table().container()).find('.header').html(\"\")\n}", - "dom": "Bf<\"header h5\">ti", "columnDefs": [ { - "orderable": false, - "targets": 0 + "targets": 0, + "render": "function(data, type, row, meta) {\nvar reviewed = data.reviewed;\nvar updated = data.updated;\nreturn ``;\n}" }, { - "name": " ", + "name": "Review Status", "targets": 0 }, { @@ -8377,28 +8741,35 @@ "targets": 9 } ], + "createdRow": "function(row, data, dataIndex) {\nif (data[0].reviewed == null) {\nlet cb = row.cells[0].getElementsByTagName('input')[0]\ncb.indeterminate = true;\n}\n}", + "buttons": [ + { + "extend": "excel", + "text": "<\/i>", + "filename": "clinsight.Vital-signs.BEL_04_772" + } + ], + "initComplete": "function() {\n$(this.api().table().container()).find('.header').html(\"\")\n}", + "dom": "Bf<\"header h5\">ti", "order": [ ], "autoWidth": false, - "orderClasses": false, - "ajax": { - "type": "POST", - "data": "function(d) {\nd.search.caseInsensitive = true;\nd.search.smart = true;\nd.escape = false;\nvar encodeAmp = function(x) { x.value = x.value.replace(/&/g, \"%26\"); }\nencodeAmp(d.search);\n$.each(d.columns, function(i, v) {encodeAmp(v.search);});\n}" - }, - "serverSide": true, - "processing": true + "orderClasses": false }, + "callback": "function(table) {\ntable.on('click', 'input[type=\"checkbox\"]', function(){\nvar tblId = $(this).closest('.datatables').attr('id');\nvar cell = table.cell($(this).closest('td'));\nvar rowIdx = table.row($(this).closest('tr')).index();\nvar ids = cell.data().ids;\nvar review = $(this).is(':indeterminate') ? null : $(this).is(':checked');\ncell.data().updated = review;\nvar info = {review: review, ids: ids, row: tblId + '_row_' + rowIdx};\nShiny.setInputValue(tblId + '_review_selection:CS.reviewInfo', info);\n})\n}", "selection": { - "mode": "single", + "mode": "none", "selected": null, "target": "row", "selectable": null } }, "evals": [ + "options.columnDefs.0.render", + "options.createdRow", "options.initComplete", - "options.ajax.data" + "callback" ], "jsHooks": [ diff --git a/tests/testthat/_snaps/app_feature_01/app-feature-1-005.json b/tests/testthat/_snaps/app_feature_01/app-feature-1-005.json index a97090f..4ad1f05 100644 --- a/tests/testthat/_snaps/app_feature_01/app-feature-1-005.json +++ b/tests/testthat/_snaps/app_feature_01/app-feature-1-005.json @@ -9,7 +9,51 @@ "Scroller", "ColReorder" ], - "container": "\n \n \n
N<\/th>\n Name<\/th>\n AESI<\/th>\n Start date<\/th>\n End date<\/th>\n CTCAE severity<\/th>\n Treatment related<\/th>\n Treatment action<\/th>\n Other action<\/th>\n Category<\/th>\n Awareness date<\/th>\n Date of death<\/th>\n Death reason<\/th>\n <\/tr>\n <\/thead>\n<\/table>", + "data": [ + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ] + ], + "container": "\n \n \n
Review Status<\/th>\n N<\/th>\n Name<\/th>\n AESI<\/th>\n Start date<\/th>\n End date<\/th>\n CTCAE severity<\/th>\n Treatment related<\/th>\n Treatment action<\/th>\n Other action<\/th>\n Category<\/th>\n Awareness date<\/th>\n Date of death<\/th>\n Death reason<\/th>\n <\/tr>\n <\/thead>\n<\/table>", "options": { "scrollY": 400, "scrollX": true, @@ -18,88 +62,94 @@ "scrollResize": true, "scrollCollapse": true, "colReorder": true, - "initComplete": "function() {\n$(this.api().table().container()).find('.header').html(\"Serious Adverse Events\")\n}", - "dom": "f<\"header h5\">ti", "columnDefs": [ + { + "targets": 0, + "render": "function(data, type, row, meta) {\nvar reviewed = data.reviewed;\nvar updated = data.updated;\nreturn ``;\n}" + }, { "className": "dt-right", + "targets": 1 + }, + { + "name": "Review Status", "targets": 0 }, { "name": "N", - "targets": 0 + "targets": 1 }, { "name": "Name", - "targets": 1 + "targets": 2 }, { "name": "AESI", - "targets": 2 + "targets": 3 }, { "name": "Start date", - "targets": 3 + "targets": 4 }, { "name": "End date", - "targets": 4 + "targets": 5 }, { "name": "CTCAE severity", - "targets": 5 + "targets": 6 }, { "name": "Treatment related", - "targets": 6 + "targets": 7 }, { "name": "Treatment action", - "targets": 7 + "targets": 8 }, { "name": "Other action", - "targets": 8 + "targets": 9 }, { "name": "Category", - "targets": 9 + "targets": 10 }, { "name": "Awareness date", - "targets": 10 + "targets": 11 }, { "name": "Date of death", - "targets": 11 + "targets": 12 }, { "name": "Death reason", - "targets": 12 + "targets": 13 } ], + "createdRow": "function(row, data, dataIndex) {\nif (data[0].reviewed == null) {\nlet cb = row.cells[0].getElementsByTagName('input')[0]\ncb.indeterminate = true;\n}\n}", + "initComplete": "function() {\n$(this.api().table().container()).find('.header').html(\"Serious Adverse Events\")\n}", + "dom": "f<\"header h5\">ti", "order": [ ], "autoWidth": false, - "orderClasses": false, - "ajax": { - "type": "POST", - "data": "function(d) {\nd.search.caseInsensitive = true;\nd.search.smart = true;\nd.escape = false;\nvar encodeAmp = function(x) { x.value = x.value.replace(/&/g, \"%26\"); }\nencodeAmp(d.search);\n$.each(d.columns, function(i, v) {encodeAmp(v.search);});\n}" - }, - "serverSide": true, - "processing": true + "orderClasses": false }, + "callback": "function(table) {\ntable.on('click', 'input[type=\"checkbox\"]', function(){\nvar tblId = $(this).closest('.datatables').attr('id');\nvar cell = table.cell($(this).closest('td'));\nvar rowIdx = table.row($(this).closest('tr')).index();\nvar ids = cell.data().ids;\nvar review = $(this).is(':indeterminate') ? null : $(this).is(':checked');\ncell.data().updated = review;\nvar info = {review: review, ids: ids, row: tblId + '_row_' + rowIdx};\nShiny.setInputValue(tblId + '_review_selection:CS.reviewInfo', info);\n})\n}", "selection": { - "mode": "single", + "mode": "none", "selected": null, "target": "row", "selectable": null } }, "evals": [ + "options.columnDefs.0.render", + "options.createdRow", "options.initComplete", - "options.ajax.data" + "callback" ], "jsHooks": [ @@ -243,7 +293,151 @@ "Scroller", "ColReorder" ], - "container": "\n \n \n
N<\/th>\n Name<\/th>\n AESI<\/th>\n start date<\/th>\n end date<\/th>\n CTCAE severity<\/th>\n Treatment related<\/th>\n Treatment action<\/th>\n Other action<\/th>\n Serious Adverse Event<\/th>\n <\/tr>\n <\/thead>\n<\/table>", + "data": [ + [ + { + "reviewed": false, + "ids": [ + 2195, + 2196, + 2197, + 2198, + 2199, + 2200, + 2201, + 2202, + 2235 + ] + }, + { + "reviewed": false, + "ids": [ + 2203, + 2204, + 2205, + 2206, + 2207, + 2208, + 2209, + 2210, + 2236 + ] + }, + { + "reviewed": false, + "ids": [ + 2211, + 2212, + 2213, + 2214, + 2215, + 2216, + 2217, + 2218, + 2237 + ] + }, + { + "reviewed": false, + "ids": [ + 2219, + 2220, + 2221, + 2222, + 2223, + 2224, + 2225, + 2226, + 2238 + ] + }, + { + "reviewed": false, + "ids": [ + 2227, + 2228, + 2229, + 2230, + 2231, + 2232, + 2233, + 2234, + 2239 + ] + } + ], + [ + 1, + 2, + 3, + 4, + 5 + ], + [ + "Hypotension*<\/b>", + "Atrial Fibrillation*<\/b>", + "Tachycardia*<\/b>", + "Urinary Tract Infection*<\/b>", + "Atrial Fibrillation*<\/b>" + ], + [ + "None*<\/b>", + "None*<\/b>", + "None*<\/b>", + "None*<\/b>", + "None*<\/b>" + ], + [ + "2023-07-07*<\/b>", + "2023-07-05*<\/b>", + "2023-07-05*<\/b>", + "2023-07-05*<\/b>", + "2023-08-16*<\/b>" + ], + [ + null, + null, + null, + null, + null + ], + [ + "Grade 1*<\/b>", + "Grade 2*<\/b>", + "Grade 2*<\/b>", + "Grade 1*<\/b>", + "Grade 2*<\/b>" + ], + [ + "Not related*<\/b>", + "Not related*<\/b>", + "Not related*<\/b>", + "Not related*<\/b>", + "Not related*<\/b>" + ], + [ + "Not Applicable*<\/b>", + "Not Applicable*<\/b>", + "Not Applicable*<\/b>", + "Not Applicable*<\/b>", + "Not Applicable*<\/b>" + ], + [ + "None*<\/b>", + "Concomitant therapy/medication*<\/b>", + "Concomitant therapy/medication*<\/b>", + "Concomitant therapy/medication*<\/b>", + "Concomitant therapy/medication*<\/b>" + ], + [ + "No*<\/b>", + "No*<\/b>", + "No*<\/b>", + "No*<\/b>", + "No*<\/b>" + ] + ], + "container": "\n \n \n
Review Status<\/th>\n N<\/th>\n Name<\/th>\n AESI<\/th>\n start date<\/th>\n end date<\/th>\n CTCAE severity<\/th>\n Treatment related<\/th>\n Treatment action<\/th>\n Other action<\/th>\n Serious Adverse Event<\/th>\n <\/tr>\n <\/thead>\n<\/table>", "options": { "scrollY": 400, "scrollX": true, @@ -252,83 +446,89 @@ "scrollResize": true, "scrollCollapse": true, "colReorder": true, - "buttons": [ - { - "extend": "excel", - "text": "<\/i>", - "filename": "clinsight.Adverse-events.BEL_04_772" - } - ], - "initComplete": "function() {\n$(this.api().table().container()).find('.header').html(\"Adverse events\")\n}", - "dom": "Bf<\"header h5\">ti", "columnDefs": [ + { + "targets": 0, + "render": "function(data, type, row, meta) {\nvar reviewed = data.reviewed;\nvar updated = data.updated;\nreturn ``;\n}" + }, { "className": "dt-right", + "targets": 1 + }, + { + "name": "Review Status", "targets": 0 }, { "name": "N", - "targets": 0 + "targets": 1 }, { "name": "Name", - "targets": 1 + "targets": 2 }, { "name": "AESI", - "targets": 2 + "targets": 3 }, { "name": "start date", - "targets": 3 + "targets": 4 }, { "name": "end date", - "targets": 4 + "targets": 5 }, { "name": "CTCAE severity", - "targets": 5 + "targets": 6 }, { "name": "Treatment related", - "targets": 6 + "targets": 7 }, { "name": "Treatment action", - "targets": 7 + "targets": 8 }, { "name": "Other action", - "targets": 8 + "targets": 9 }, { "name": "Serious Adverse Event", - "targets": 9 + "targets": 10 } ], + "createdRow": "function(row, data, dataIndex) {\nif (data[0].reviewed == null) {\nlet cb = row.cells[0].getElementsByTagName('input')[0]\ncb.indeterminate = true;\n}\n}", + "buttons": [ + { + "extend": "excel", + "text": "<\/i>", + "filename": "clinsight.Adverse-events.BEL_04_772" + } + ], + "initComplete": "function() {\n$(this.api().table().container()).find('.header').html(\"Adverse events\")\n}", + "dom": "Bf<\"header h5\">ti", "order": [ ], "autoWidth": false, - "orderClasses": false, - "ajax": { - "type": "POST", - "data": "function(d) {\nd.search.caseInsensitive = true;\nd.search.smart = true;\nd.escape = false;\nvar encodeAmp = function(x) { x.value = x.value.replace(/&/g, \"%26\"); }\nencodeAmp(d.search);\n$.each(d.columns, function(i, v) {encodeAmp(v.search);});\n}" - }, - "serverSide": true, - "processing": true + "orderClasses": false }, + "callback": "function(table) {\ntable.on('click', 'input[type=\"checkbox\"]', function(){\nvar tblId = $(this).closest('.datatables').attr('id');\nvar cell = table.cell($(this).closest('td'));\nvar rowIdx = table.row($(this).closest('tr')).index();\nvar ids = cell.data().ids;\nvar review = $(this).is(':indeterminate') ? null : $(this).is(':checked');\ncell.data().updated = review;\nvar info = {review: review, ids: ids, row: tblId + '_row_' + rowIdx};\nShiny.setInputValue(tblId + '_review_selection:CS.reviewInfo', info);\n})\n}", "selection": { - "mode": "single", + "mode": "none", "selected": null, "target": "row", "selectable": null } }, "evals": [ + "options.columnDefs.0.render", + "options.createdRow", "options.initComplete", - "options.ajax.data" + "callback" ], "jsHooks": [ @@ -815,6 +1015,12 @@ ] }, "main_sidebar_1-navigate_forms_1-form_name": "
Vital signs<\/b><\/center>", + "main_sidebar_1-review_forms_1-progress_bar": { + "completed": 0, + "unmarking": 0, + "marking": 0, + "total": 15 + }, "main_sidebar_1-review_forms_1-save_review_error": { "message": "Requires review", "call": "NULL", @@ -8314,7 +8520,72 @@ "Scroller", "ColReorder" ], - "container": "\n \n \n
<\/th>\n Event<\/th>\n Systolic blood pressure<\/th>\n Diastolic blood pressure<\/th>\n Pulse<\/th>\n Resp<\/th>\n Temperature<\/th>\n Weight change since screening<\/th>\n BMI<\/th>\n Weight<\/th>\n <\/tr>\n <\/thead>\n<\/table>", + "data": [ + [ + { + "reviewed": false, + "ids": [ + 598, + 599, + 600, + 601, + 602, + 603, + 604, + 1423 + ] + }, + { + "reviewed": false, + "ids": [ + 605, + 606, + 607, + 608, + 609, + 610, + 1424 + ] + } + ], + [ + "Screening", + "Visit 1" + ], + [ + "133*<\/b> mmHg", + "115*<\/b> mmHg" + ], + [ + "69*<\/b> mmHg", + "62*<\/b> mmHg" + ], + [ + "56*<\/b> beats/min", + "84*<\/b> beats/min" + ], + [ + "18*<\/b> breaths/min", + null + ], + [ + "37*<\/b> °C", + "36.6*<\/b> °C" + ], + [ + "0.01*<\/b> %", + "0.01*<\/b> %" + ], + [ + "23.66*<\/b> kg/m2", + "23.66*<\/b> kg/m2" + ], + [ + "70*<\/b> kg", + "62*<\/b> kg" + ] + ], + "container": "\n \n \n
Review Status<\/th>\n Event<\/th>\n Systolic blood pressure<\/th>\n Diastolic blood pressure<\/th>\n Pulse<\/th>\n Resp<\/th>\n Temperature<\/th>\n Weight change since screening<\/th>\n BMI<\/th>\n Weight<\/th>\n <\/tr>\n <\/thead>\n<\/table>", "options": { "scrollY": 400, "scrollX": true, @@ -8323,22 +8594,13 @@ "scrollResize": true, "scrollCollapse": true, "colReorder": true, - "buttons": [ - { - "extend": "excel", - "text": "<\/i>", - "filename": "clinsight.Vital-signs.NLD_06_893" - } - ], - "initComplete": "function() {\n$(this.api().table().container()).find('.header').html(\"\")\n}", - "dom": "Bf<\"header h5\">ti", "columnDefs": [ { - "orderable": false, - "targets": 0 + "targets": 0, + "render": "function(data, type, row, meta) {\nvar reviewed = data.reviewed;\nvar updated = data.updated;\nreturn ``;\n}" }, { - "name": " ", + "name": "Review Status", "targets": 0 }, { @@ -8378,28 +8640,35 @@ "targets": 9 } ], + "createdRow": "function(row, data, dataIndex) {\nif (data[0].reviewed == null) {\nlet cb = row.cells[0].getElementsByTagName('input')[0]\ncb.indeterminate = true;\n}\n}", + "buttons": [ + { + "extend": "excel", + "text": "<\/i>", + "filename": "clinsight.Vital-signs.NLD_06_893" + } + ], + "initComplete": "function() {\n$(this.api().table().container()).find('.header').html(\"\")\n}", + "dom": "Bf<\"header h5\">ti", "order": [ ], "autoWidth": false, - "orderClasses": false, - "ajax": { - "type": "POST", - "data": "function(d) {\nd.search.caseInsensitive = true;\nd.search.smart = true;\nd.escape = false;\nvar encodeAmp = function(x) { x.value = x.value.replace(/&/g, \"%26\"); }\nencodeAmp(d.search);\n$.each(d.columns, function(i, v) {encodeAmp(v.search);});\n}" - }, - "serverSide": true, - "processing": true + "orderClasses": false }, + "callback": "function(table) {\ntable.on('click', 'input[type=\"checkbox\"]', function(){\nvar tblId = $(this).closest('.datatables').attr('id');\nvar cell = table.cell($(this).closest('td'));\nvar rowIdx = table.row($(this).closest('tr')).index();\nvar ids = cell.data().ids;\nvar review = $(this).is(':indeterminate') ? null : $(this).is(':checked');\ncell.data().updated = review;\nvar info = {review: review, ids: ids, row: tblId + '_row_' + rowIdx};\nShiny.setInputValue(tblId + '_review_selection:CS.reviewInfo', info);\n})\n}", "selection": { - "mode": "single", + "mode": "none", "selected": null, "target": "row", "selectable": null } }, "evals": [ + "options.columnDefs.0.render", + "options.createdRow", "options.initComplete", - "options.ajax.data" + "callback" ], "jsHooks": [ diff --git a/tests/testthat/_snaps/app_feature_02/app-feature-2-001.json b/tests/testthat/_snaps/app_feature_02/app-feature-2-001.json index b0c7209..b7c12cb 100644 --- a/tests/testthat/_snaps/app_feature_02/app-feature-2-001.json +++ b/tests/testthat/_snaps/app_feature_02/app-feature-2-001.json @@ -83,6 +83,12 @@ ] }, "main_sidebar_1-navigate_forms_1-form_name": "
Vital signs<\/center>", + "main_sidebar_1-review_forms_1-progress_bar": { + "completed": 8, + "unmarking": 0, + "marking": 0, + "total": 8 + }, "main_sidebar_1-review_forms_1-save_review_error": { "message": "Form already reviewed", "call": "NULL", diff --git a/tests/testthat/_snaps/app_feature_03/app-feature-3-001.json b/tests/testthat/_snaps/app_feature_03/app-feature-3-001.json index b0f0134..335abaa 100644 --- a/tests/testthat/_snaps/app_feature_03/app-feature-3-001.json +++ b/tests/testthat/_snaps/app_feature_03/app-feature-3-001.json @@ -188,6 +188,12 @@ ] }, "main_sidebar_1-navigate_forms_1-form_name": "
Vital signs<\/b><\/center>", + "main_sidebar_1-review_forms_1-progress_bar": { + "completed": 0, + "unmarking": 0, + "marking": 0, + "total": 8 + }, "main_sidebar_1-review_forms_1-save_review_error": { "message": "Requires review", "call": "NULL", diff --git a/tests/testthat/_snaps/app_feature_03/app-feature-3-002.json b/tests/testthat/_snaps/app_feature_03/app-feature-3-002.json index 644aea5..63696d4 100644 --- a/tests/testthat/_snaps/app_feature_03/app-feature-3-002.json +++ b/tests/testthat/_snaps/app_feature_03/app-feature-3-002.json @@ -146,6 +146,15 @@ "caseInsensitive": true } }, + { + "visible": true, + "search": { + "search": "", + "smart": true, + "regex": false, + "caseInsensitive": true + } + }, { "visible": true, "search": { @@ -169,7 +178,8 @@ 9, 10, 11, - 12 + 12, + 13 ], "scroller": { "topRow": 0, @@ -295,6 +305,15 @@ "caseInsensitive": true } }, + { + "visible": true, + "search": { + "search": "", + "smart": true, + "regex": false, + "caseInsensitive": true + } + }, { "visible": true, "search": { @@ -315,7 +334,8 @@ 6, 7, 8, - 9 + 9, + 10 ], "scroller": { "topRow": 0, @@ -463,7 +483,51 @@ "Scroller", "ColReorder" ], - "container": "\n \n \n
N<\/th>\n Name<\/th>\n AESI<\/th>\n Start date<\/th>\n End date<\/th>\n CTCAE severity<\/th>\n Treatment related<\/th>\n Treatment action<\/th>\n Other action<\/th>\n Category<\/th>\n Awareness date<\/th>\n Date of death<\/th>\n Death reason<\/th>\n <\/tr>\n <\/thead>\n<\/table>", + "data": [ + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + + ] + ], + "container": "\n \n \n
Review Status<\/th>\n N<\/th>\n Name<\/th>\n AESI<\/th>\n Start date<\/th>\n End date<\/th>\n CTCAE severity<\/th>\n Treatment related<\/th>\n Treatment action<\/th>\n Other action<\/th>\n Category<\/th>\n Awareness date<\/th>\n Date of death<\/th>\n Death reason<\/th>\n <\/tr>\n <\/thead>\n<\/table>", "options": { "scrollY": 400, "scrollX": true, @@ -472,88 +536,94 @@ "scrollResize": true, "scrollCollapse": true, "colReorder": true, - "initComplete": "function() {\n$(this.api().table().container()).find('.header').html(\"Serious Adverse Events\")\n}", - "dom": "f<\"header h5\">ti", "columnDefs": [ + { + "targets": 0, + "render": "function(data, type, row, meta) {\nvar reviewed = data.reviewed;\nvar updated = data.updated;\nreturn ``;\n}" + }, { "className": "dt-right", + "targets": 1 + }, + { + "name": "Review Status", "targets": 0 }, { "name": "N", - "targets": 0 + "targets": 1 }, { "name": "Name", - "targets": 1 + "targets": 2 }, { "name": "AESI", - "targets": 2 + "targets": 3 }, { "name": "Start date", - "targets": 3 + "targets": 4 }, { "name": "End date", - "targets": 4 + "targets": 5 }, { "name": "CTCAE severity", - "targets": 5 + "targets": 6 }, { "name": "Treatment related", - "targets": 6 + "targets": 7 }, { "name": "Treatment action", - "targets": 7 + "targets": 8 }, { "name": "Other action", - "targets": 8 + "targets": 9 }, { "name": "Category", - "targets": 9 + "targets": 10 }, { "name": "Awareness date", - "targets": 10 + "targets": 11 }, { "name": "Date of death", - "targets": 11 + "targets": 12 }, { "name": "Death reason", - "targets": 12 + "targets": 13 } ], + "createdRow": "function(row, data, dataIndex) {\nif (data[0].reviewed == null) {\nlet cb = row.cells[0].getElementsByTagName('input')[0]\ncb.indeterminate = true;\n}\n}", + "initComplete": "function() {\n$(this.api().table().container()).find('.header').html(\"Serious Adverse Events\")\n}", + "dom": "f<\"header h5\">ti", "order": [ ], "autoWidth": false, - "orderClasses": false, - "ajax": { - "type": "POST", - "data": "function(d) {\nd.search.caseInsensitive = true;\nd.search.smart = true;\nd.escape = false;\nvar encodeAmp = function(x) { x.value = x.value.replace(/&/g, \"%26\"); }\nencodeAmp(d.search);\n$.each(d.columns, function(i, v) {encodeAmp(v.search);});\n}" - }, - "serverSide": true, - "processing": true + "orderClasses": false }, + "callback": "function(table) {\ntable.on('click', 'input[type=\"checkbox\"]', function(){\nvar tblId = $(this).closest('.datatables').attr('id');\nvar cell = table.cell($(this).closest('td'));\nvar rowIdx = table.row($(this).closest('tr')).index();\nvar ids = cell.data().ids;\nvar review = $(this).is(':indeterminate') ? null : $(this).is(':checked');\ncell.data().updated = review;\nvar info = {review: review, ids: ids, row: tblId + '_row_' + rowIdx};\nShiny.setInputValue(tblId + '_review_selection:CS.reviewInfo', info);\n})\n}", "selection": { - "mode": "single", + "mode": "none", "selected": null, "target": "row", "selectable": null } }, "evals": [ + "options.columnDefs.0.render", + "options.createdRow", "options.initComplete", - "options.ajax.data" + "callback" ], "jsHooks": [ @@ -697,7 +767,151 @@ "Scroller", "ColReorder" ], - "container": "\n \n \n
N<\/th>\n Name<\/th>\n AESI<\/th>\n start date<\/th>\n end date<\/th>\n CTCAE severity<\/th>\n Treatment related<\/th>\n Treatment action<\/th>\n Other action<\/th>\n Serious Adverse Event<\/th>\n <\/tr>\n <\/thead>\n<\/table>", + "data": [ + [ + { + "reviewed": false, + "ids": [ + 2195, + 2196, + 2197, + 2198, + 2199, + 2200, + 2201, + 2202, + 2235 + ] + }, + { + "reviewed": false, + "ids": [ + 2203, + 2204, + 2205, + 2206, + 2207, + 2208, + 2209, + 2210, + 2236 + ] + }, + { + "reviewed": false, + "ids": [ + 2211, + 2212, + 2213, + 2214, + 2215, + 2216, + 2217, + 2218, + 2237 + ] + }, + { + "reviewed": false, + "ids": [ + 2219, + 2220, + 2221, + 2222, + 2223, + 2224, + 2225, + 2226, + 2238 + ] + }, + { + "reviewed": false, + "ids": [ + 2227, + 2228, + 2229, + 2230, + 2231, + 2232, + 2233, + 2234, + 2239 + ] + } + ], + [ + 1, + 2, + 3, + 4, + 5 + ], + [ + "Hypotension*<\/b>", + "Atrial Fibrillation*<\/b>", + "Tachycardia*<\/b>", + "Urinary Tract Infection*<\/b>", + "Atrial Fibrillation*<\/b>" + ], + [ + "None*<\/b>", + "None*<\/b>", + "None*<\/b>", + "None*<\/b>", + "None*<\/b>" + ], + [ + "2023-07-07*<\/b>", + "2023-07-05*<\/b>", + "2023-07-05*<\/b>", + "2023-07-05*<\/b>", + "2023-08-16*<\/b>" + ], + [ + null, + null, + null, + null, + null + ], + [ + "Grade 1*<\/b>", + "Grade 2*<\/b>", + "Grade 2*<\/b>", + "Grade 1*<\/b>", + "Grade 2*<\/b>" + ], + [ + "Not related*<\/b>", + "Not related*<\/b>", + "Not related*<\/b>", + "Not related*<\/b>", + "Not related*<\/b>" + ], + [ + "Not Applicable*<\/b>", + "Not Applicable*<\/b>", + "Not Applicable*<\/b>", + "Not Applicable*<\/b>", + "Not Applicable*<\/b>" + ], + [ + "None*<\/b>", + "Concomitant therapy/medication*<\/b>", + "Concomitant therapy/medication*<\/b>", + "Concomitant therapy/medication*<\/b>", + "Concomitant therapy/medication*<\/b>" + ], + [ + "No*<\/b>", + "No*<\/b>", + "No*<\/b>", + "No*<\/b>", + "No*<\/b>" + ] + ], + "container": "\n \n \n
Review Status<\/th>\n N<\/th>\n Name<\/th>\n AESI<\/th>\n start date<\/th>\n end date<\/th>\n CTCAE severity<\/th>\n Treatment related<\/th>\n Treatment action<\/th>\n Other action<\/th>\n Serious Adverse Event<\/th>\n <\/tr>\n <\/thead>\n<\/table>", "options": { "scrollY": 400, "scrollX": true, @@ -706,83 +920,89 @@ "scrollResize": true, "scrollCollapse": true, "colReorder": true, - "buttons": [ - { - "extend": "excel", - "text": "<\/i>", - "filename": "clinsight.Adverse-events.BEL_04_772" - } - ], - "initComplete": "function() {\n$(this.api().table().container()).find('.header').html(\"Adverse events\")\n}", - "dom": "Bf<\"header h5\">ti", "columnDefs": [ + { + "targets": 0, + "render": "function(data, type, row, meta) {\nvar reviewed = data.reviewed;\nvar updated = data.updated;\nreturn ``;\n}" + }, { "className": "dt-right", + "targets": 1 + }, + { + "name": "Review Status", "targets": 0 }, { "name": "N", - "targets": 0 + "targets": 1 }, { "name": "Name", - "targets": 1 + "targets": 2 }, { "name": "AESI", - "targets": 2 + "targets": 3 }, { "name": "start date", - "targets": 3 + "targets": 4 }, { "name": "end date", - "targets": 4 + "targets": 5 }, { "name": "CTCAE severity", - "targets": 5 + "targets": 6 }, { "name": "Treatment related", - "targets": 6 + "targets": 7 }, { "name": "Treatment action", - "targets": 7 + "targets": 8 }, { "name": "Other action", - "targets": 8 + "targets": 9 }, { "name": "Serious Adverse Event", - "targets": 9 + "targets": 10 + } + ], + "createdRow": "function(row, data, dataIndex) {\nif (data[0].reviewed == null) {\nlet cb = row.cells[0].getElementsByTagName('input')[0]\ncb.indeterminate = true;\n}\n}", + "buttons": [ + { + "extend": "excel", + "text": "<\/i>", + "filename": "clinsight.Adverse-events.BEL_04_772" } ], + "initComplete": "function() {\n$(this.api().table().container()).find('.header').html(\"Adverse events\")\n}", + "dom": "Bf<\"header h5\">ti", "order": [ ], "autoWidth": false, - "orderClasses": false, - "ajax": { - "type": "POST", - "data": "function(d) {\nd.search.caseInsensitive = true;\nd.search.smart = true;\nd.escape = false;\nvar encodeAmp = function(x) { x.value = x.value.replace(/&/g, \"%26\"); }\nencodeAmp(d.search);\n$.each(d.columns, function(i, v) {encodeAmp(v.search);});\n}" - }, - "serverSide": true, - "processing": true + "orderClasses": false }, + "callback": "function(table) {\ntable.on('click', 'input[type=\"checkbox\"]', function(){\nvar tblId = $(this).closest('.datatables').attr('id');\nvar cell = table.cell($(this).closest('td'));\nvar rowIdx = table.row($(this).closest('tr')).index();\nvar ids = cell.data().ids;\nvar review = $(this).is(':indeterminate') ? null : $(this).is(':checked');\ncell.data().updated = review;\nvar info = {review: review, ids: ids, row: tblId + '_row_' + rowIdx};\nShiny.setInputValue(tblId + '_review_selection:CS.reviewInfo', info);\n})\n}", "selection": { - "mode": "single", + "mode": "none", "selected": null, "target": "row", "selectable": null } }, "evals": [ + "options.columnDefs.0.render", + "options.createdRow", "options.initComplete", - "options.ajax.data" + "callback" ], "jsHooks": [ @@ -1269,6 +1489,12 @@ ] }, "main_sidebar_1-navigate_forms_1-form_name": "
Adverse events<\/b><\/center>", + "main_sidebar_1-review_forms_1-progress_bar": { + "completed": 0, + "unmarking": 0, + "marking": 0, + "total": 45 + }, "main_sidebar_1-review_forms_1-save_review_error": { "message": "Requires review", "call": "NULL", diff --git a/tests/testthat/_snaps/app_feature_04.md b/tests/testthat/_snaps/app_feature_04.md index 5ecee99..65ed690 100644 --- a/tests/testthat/_snaps/app_feature_04.md +++ b/tests/testthat/_snaps/app_feature_04.md @@ -25,17 +25,21 @@ Code print(table_data, width = Inf) Output - # A tibble: 2 x 9 - event_name `Systolic blood pressure` `Diastolic blood pressure` - - 1 Screening 99* mmHg 77* mmHg - 2 Visit 2 99* mmHg 77* mmHg - Pulse Resp Temperature - - 1 77* beats/min 9* breaths/min 37.5* °C - 2 77* beats/min 9* breaths/min 37.5* °C - `Weight change since screening` BMI Weight - - 1 22.09* kg/m2 70* kg - 2 + # A tibble: 2 x 10 + o_reviewed event_name `Systolic blood pressure` + + 1 Screening 99* mmHg + 2 Visit 2 99* mmHg + `Diastolic blood pressure` Pulse Resp + + 1 77* mmHg 77* beats/min 9* breaths/min + 2 77* mmHg 77* beats/min 9* breaths/min + Temperature `Weight change since screening` BMI + + 1 37.5* °C 22.09* kg/m2 + 2 37.5* °C + Weight + + 1 70* kg + 2 diff --git a/tests/testthat/_snaps/mod_study_forms/study_forms-001.json b/tests/testthat/_snaps/mod_study_forms/study_forms-001.json index 00e8a3c..2cbebe1 100644 --- a/tests/testthat/_snaps/mod_study_forms/study_forms-001.json +++ b/tests/testthat/_snaps/mod_study_forms/study_forms-001.json @@ -1244,7 +1244,92 @@ "Scroller", "ColReorder" ], - "container": "\n \n \n
<\/th>\n event_name<\/th>\n Systolic blood pressure<\/th>\n Diastolic blood pressure<\/th>\n Pulse<\/th>\n Resp<\/th>\n Temperature<\/th>\n Weight change since screening<\/th>\n BMI<\/th>\n Weight<\/th>\n <\/tr>\n <\/thead>\n<\/table>", + "data": [ + [ + { + "reviewed": null, + "ids": [ + 78, + 79, + 80, + 81, + 82, + 83, + 84, + 440 + ] + }, + { + "reviewed": null, + "ids": [ + 85, + 86, + 87, + 88, + 89, + 90, + 91, + 441 + ] + }, + { + "reviewed": false, + "ids": [ + 92, + 93, + 94, + 95, + 96 + ] + } + ], + [ + "Screening", + "Visit 1", + "Visit 2" + ], + [ + "121*<\/b> mmHg", + "120*<\/b> mmHg", + "149*<\/b> mmHg" + ], + [ + "68 mmHg", + "68 mmHg", + "77*<\/b> mmHg" + ], + [ + "82 beats/min", + "82 beats/min", + "63*<\/b> beats/min" + ], + [ + "18*<\/b> breaths/min", + "18 breaths/min", + "19*<\/b> breaths/min" + ], + [ + "36.7*<\/b> °C", + "36.7 °C", + "37*<\/b> °C" + ], + [ + "6.3*<\/b> %", + "0.01 %", + null + ], + [ + "20.96 kg/m2", + "21.87 kg/m2", + null + ], + [ + "77.2*<\/b> kg", + "61*<\/b> kg", + null + ] + ], + "container": "\n \n \n
Review Status<\/th>\n event_name<\/th>\n Systolic blood pressure<\/th>\n Diastolic blood pressure<\/th>\n Pulse<\/th>\n Resp<\/th>\n Temperature<\/th>\n Weight change since screening<\/th>\n BMI<\/th>\n Weight<\/th>\n <\/tr>\n <\/thead>\n<\/table>", "options": { "scrollY": 400, "scrollX": true, @@ -1253,22 +1338,13 @@ "scrollResize": true, "scrollCollapse": true, "colReorder": true, - "buttons": [ - { - "extend": "excel", - "text": "<\/i>", - "filename": "clinsight.Vital-signs.NLD_06_755" - } - ], - "initComplete": "function() {\n$(this.api().table().container()).find('.header').html(\"\")\n}", - "dom": "Bf<\"header h5\">ti", "columnDefs": [ { - "orderable": false, - "targets": 0 + "targets": 0, + "render": "function(data, type, row, meta) {\nvar reviewed = data.reviewed;\nvar updated = data.updated;\nreturn ``;\n}" }, { - "name": " ", + "name": "Review Status", "targets": 0 }, { @@ -1308,28 +1384,35 @@ "targets": 9 } ], + "createdRow": "function(row, data, dataIndex) {\nif (data[0].reviewed == null) {\nlet cb = row.cells[0].getElementsByTagName('input')[0]\ncb.indeterminate = true;\n}\n}", + "buttons": [ + { + "extend": "excel", + "text": "<\/i>", + "filename": "clinsight.Vital-signs.NLD_06_755" + } + ], + "initComplete": "function() {\n$(this.api().table().container()).find('.header').html(\"\")\n}", + "dom": "Bf<\"header h5\">ti", "order": [ ], "autoWidth": false, - "orderClasses": false, - "ajax": { - "type": "POST", - "data": "function(d) {\nd.search.caseInsensitive = true;\nd.search.smart = true;\nd.escape = false;\nvar encodeAmp = function(x) { x.value = x.value.replace(/&/g, \"%26\"); }\nencodeAmp(d.search);\n$.each(d.columns, function(i, v) {encodeAmp(v.search);});\n}" - }, - "serverSide": true, - "processing": true + "orderClasses": false }, + "callback": "function(table) {\ntable.on('click', 'input[type=\"checkbox\"]', function(){\nvar tblId = $(this).closest('.datatables').attr('id');\nvar cell = table.cell($(this).closest('td'));\nvar rowIdx = table.row($(this).closest('tr')).index();\nvar ids = cell.data().ids;\nvar review = $(this).is(':indeterminate') ? null : $(this).is(':checked');\ncell.data().updated = review;\nvar info = {review: review, ids: ids, row: tblId + '_row_' + rowIdx};\nShiny.setInputValue(tblId + '_review_selection:CS.reviewInfo', info);\n})\n}", "selection": { - "mode": "single", + "mode": "none", "selected": null, "target": "row", "selectable": null } }, "evals": [ + "options.columnDefs.0.render", + "options.createdRow", "options.initComplete", - "options.ajax.data" + "callback" ], "jsHooks": [ @@ -3532,6 +3615,44 @@ ] }, "test-table_data": { + "o_reviewed": [ + { + "reviewed": null, + "ids": [ + 78, + 79, + 80, + 81, + 82, + 83, + 84, + 440 + ] + }, + { + "reviewed": null, + "ids": [ + 85, + 86, + 87, + 88, + 89, + 90, + 91, + 441 + ] + }, + { + "reviewed": false, + "ids": [ + 92, + 93, + 94, + 95, + 96 + ] + } + ], "event_name": [ "Screening", "Visit 1", diff --git a/tests/testthat/_snaps/mod_study_forms/study_forms-002.json b/tests/testthat/_snaps/mod_study_forms/study_forms-002.json index 00e8a3c..2cbebe1 100644 --- a/tests/testthat/_snaps/mod_study_forms/study_forms-002.json +++ b/tests/testthat/_snaps/mod_study_forms/study_forms-002.json @@ -1244,7 +1244,92 @@ "Scroller", "ColReorder" ], - "container": "\n \n \n
<\/th>\n event_name<\/th>\n Systolic blood pressure<\/th>\n Diastolic blood pressure<\/th>\n Pulse<\/th>\n Resp<\/th>\n Temperature<\/th>\n Weight change since screening<\/th>\n BMI<\/th>\n Weight<\/th>\n <\/tr>\n <\/thead>\n<\/table>", + "data": [ + [ + { + "reviewed": null, + "ids": [ + 78, + 79, + 80, + 81, + 82, + 83, + 84, + 440 + ] + }, + { + "reviewed": null, + "ids": [ + 85, + 86, + 87, + 88, + 89, + 90, + 91, + 441 + ] + }, + { + "reviewed": false, + "ids": [ + 92, + 93, + 94, + 95, + 96 + ] + } + ], + [ + "Screening", + "Visit 1", + "Visit 2" + ], + [ + "121*<\/b> mmHg", + "120*<\/b> mmHg", + "149*<\/b> mmHg" + ], + [ + "68 mmHg", + "68 mmHg", + "77*<\/b> mmHg" + ], + [ + "82 beats/min", + "82 beats/min", + "63*<\/b> beats/min" + ], + [ + "18*<\/b> breaths/min", + "18 breaths/min", + "19*<\/b> breaths/min" + ], + [ + "36.7*<\/b> °C", + "36.7 °C", + "37*<\/b> °C" + ], + [ + "6.3*<\/b> %", + "0.01 %", + null + ], + [ + "20.96 kg/m2", + "21.87 kg/m2", + null + ], + [ + "77.2*<\/b> kg", + "61*<\/b> kg", + null + ] + ], + "container": "\n \n \n
Review Status<\/th>\n event_name<\/th>\n Systolic blood pressure<\/th>\n Diastolic blood pressure<\/th>\n Pulse<\/th>\n Resp<\/th>\n Temperature<\/th>\n Weight change since screening<\/th>\n BMI<\/th>\n Weight<\/th>\n <\/tr>\n <\/thead>\n<\/table>", "options": { "scrollY": 400, "scrollX": true, @@ -1253,22 +1338,13 @@ "scrollResize": true, "scrollCollapse": true, "colReorder": true, - "buttons": [ - { - "extend": "excel", - "text": "<\/i>", - "filename": "clinsight.Vital-signs.NLD_06_755" - } - ], - "initComplete": "function() {\n$(this.api().table().container()).find('.header').html(\"\")\n}", - "dom": "Bf<\"header h5\">ti", "columnDefs": [ { - "orderable": false, - "targets": 0 + "targets": 0, + "render": "function(data, type, row, meta) {\nvar reviewed = data.reviewed;\nvar updated = data.updated;\nreturn ``;\n}" }, { - "name": " ", + "name": "Review Status", "targets": 0 }, { @@ -1308,28 +1384,35 @@ "targets": 9 } ], + "createdRow": "function(row, data, dataIndex) {\nif (data[0].reviewed == null) {\nlet cb = row.cells[0].getElementsByTagName('input')[0]\ncb.indeterminate = true;\n}\n}", + "buttons": [ + { + "extend": "excel", + "text": "<\/i>", + "filename": "clinsight.Vital-signs.NLD_06_755" + } + ], + "initComplete": "function() {\n$(this.api().table().container()).find('.header').html(\"\")\n}", + "dom": "Bf<\"header h5\">ti", "order": [ ], "autoWidth": false, - "orderClasses": false, - "ajax": { - "type": "POST", - "data": "function(d) {\nd.search.caseInsensitive = true;\nd.search.smart = true;\nd.escape = false;\nvar encodeAmp = function(x) { x.value = x.value.replace(/&/g, \"%26\"); }\nencodeAmp(d.search);\n$.each(d.columns, function(i, v) {encodeAmp(v.search);});\n}" - }, - "serverSide": true, - "processing": true + "orderClasses": false }, + "callback": "function(table) {\ntable.on('click', 'input[type=\"checkbox\"]', function(){\nvar tblId = $(this).closest('.datatables').attr('id');\nvar cell = table.cell($(this).closest('td'));\nvar rowIdx = table.row($(this).closest('tr')).index();\nvar ids = cell.data().ids;\nvar review = $(this).is(':indeterminate') ? null : $(this).is(':checked');\ncell.data().updated = review;\nvar info = {review: review, ids: ids, row: tblId + '_row_' + rowIdx};\nShiny.setInputValue(tblId + '_review_selection:CS.reviewInfo', info);\n})\n}", "selection": { - "mode": "single", + "mode": "none", "selected": null, "target": "row", "selectable": null } }, "evals": [ + "options.columnDefs.0.render", + "options.createdRow", "options.initComplete", - "options.ajax.data" + "callback" ], "jsHooks": [ @@ -3532,6 +3615,44 @@ ] }, "test-table_data": { + "o_reviewed": [ + { + "reviewed": null, + "ids": [ + 78, + 79, + 80, + 81, + 82, + 83, + 84, + 440 + ] + }, + { + "reviewed": null, + "ids": [ + 85, + 86, + 87, + 88, + 89, + 90, + 91, + 441 + ] + }, + { + "reviewed": false, + "ids": [ + 92, + 93, + 94, + 95, + 96 + ] + } + ], "event_name": [ "Screening", "Visit 1", diff --git a/tests/testthat/test-mod_common_forms.R b/tests/testthat/test-mod_common_forms.R index 3762094..909a001 100644 --- a/tests/testthat/test-mod_common_forms.R +++ b/tests/testthat/test-mod_common_forms.R @@ -52,6 +52,7 @@ describe( }) rev_data <- get_review_data(bind_rows_custom(appdata)) |> dplyr::mutate( + id = dplyr::row_number(), reviewed = sample(c("Yes", "No"), dplyr::n(), replace = TRUE), status = sample(c("new", "old", "updated"), dplyr::n(), replace = TRUE) ) diff --git a/tests/testthat/test-mod_review_forms.R b/tests/testthat/test-mod_review_forms.R index 4ef1041..e8c797a 100644 --- a/tests/testthat/test-mod_review_forms.R +++ b/tests/testthat/test-mod_review_forms.R @@ -79,6 +79,9 @@ describe( mod_review_forms_server, args = testargs, { ns <- session$ns + session$userData$review_records <- reactiveValues() + session$userData$update_checkboxes <- reactiveValues() + ## patient has two rows: AF and Cystitis. AF is already reviewed by someone else: expect_equal( data.frame( @@ -94,6 +97,7 @@ describe( }) ) + session$setInputs(form_reviewed = FALSE) # Needs to be initialized to work session$setInputs(form_reviewed = TRUE, save_review = 1) db_reviewdata <- db_get_table(db_path) db_reviewlogdata <- db_get_table(db_path, "all_review_data_log") @@ -183,6 +187,9 @@ describe( ) } test_server <- function(input, output, session){ + session$userData$review_records <- reactiveValues() + session$userData$update_checkboxes <- reactiveValues() + mod_review_forms_server( id = "test", r = reactiveValues( @@ -489,10 +496,15 @@ describe( testServer( mod_review_forms_server, args = testargs, { ns <- session$ns + + session$userData$review_records <- reactiveValues() + session$userData$update_checkboxes <- reactiveValues() + + session$setInputs(form_reviewed = NULL) db_before_saving <- db_get_table(db_path) session$setInputs(form_reviewed = TRUE, save_review = 1) db_after_saving <- db_get_table(db_path) - + expect_true(review_save_error()) expect_equal(r$review_data, rev_data) expect_equal(db_after_saving, db_before_saving) diff --git a/tests/testthat/test-mod_study_forms.R b/tests/testthat/test-mod_study_forms.R index c4b868f..b2a715d 100644 --- a/tests/testthat/test-mod_study_forms.R +++ b/tests/testthat/test-mod_study_forms.R @@ -45,6 +45,7 @@ describe( appdata <- get_appdata(clinsightful_data) rev_data <- get_review_data(appdata[["Vital signs"]]) |> dplyr::mutate( + id = dplyr::row_number(), reviewed = sample(c("Yes", "No"), dplyr::n(), replace = TRUE), status = sample(c("new", "old", "updated"), dplyr::n(), replace = TRUE) ) @@ -98,7 +99,7 @@ describe( # only difference between the the data frame is some html tags around # some not yet reviewed data. However, because of these tags, we cannot # compare expected with actual directly. - expect_equal(names(table_data_active()), names(df_expected) ) + expect_equal(names(table_data_active()), c("o_reviewed", names(df_expected)) ) expect_equal(table_data_active()$event_name, df_expected$event_name ) expect_true(is.data.frame(table_data_active())) @@ -142,6 +143,7 @@ describe( appdata <- get_appdata(clinsightful_data) rev_data <- get_review_data(appdata[["Vital signs"]]) |> dplyr::mutate( + id = dplyr::row_number(), reviewed = sample(c("Yes", "No"), dplyr::n(), replace = TRUE), status = sample(c("new", "old", "updated"), dplyr::n(), replace = TRUE) )