diff --git a/.gitignore b/.gitignore index 2073ab2..bf2c9c6 100644 --- a/.gitignore +++ b/.gitignore @@ -58,10 +58,13 @@ py/__pycache__ static_files/ ../env1 logs.log - +# Google OAuth tokens +.secrets/ +google-service-account*.json <<<<<<< HEAD venv/ ======= static_files/* !static_files/labelling_file.xlsx >>>>>>> f7f1675 (Add labelling file) +.secrets diff --git a/WWW/manuals/user_manual.pdf b/WWW/manuals/user_manual.pdf new file mode 100644 index 0000000..154dfe1 Binary files /dev/null and b/WWW/manuals/user_manual.pdf differ diff --git a/server.R b/server.R index 7833ecd..bc46acd 100644 --- a/server.R +++ b/server.R @@ -17,6 +17,9 @@ function(input, output, session){ source("server/auth.R") source("server/homepage_translation_labels.R", local = TRUE) + source("server/google_sheets_feedback.R", local = TRUE) + source("server/feedback_form_server.R", local = TRUE) + register_homepage_labels(output, session, get_rv_labels) USER = user_auth(input, output, session) @@ -68,6 +71,8 @@ function(input, output, session){ source("server/history_visualize_auto_actions.R", local = TRUE) source("server/history_visualize_custom_actions.R", local = TRUE) + add_resources_translation(output, session) + feedback_form_server(input, output, session, USER) api_base <- reactive({ val <- input$fastapi_base if (is.null(val) || !nzchar(trimws(val))) { diff --git a/server/feedback_form_server.R b/server/feedback_form_server.R new file mode 100644 index 0000000..ffe4242 --- /dev/null +++ b/server/feedback_form_server.R @@ -0,0 +1,533 @@ +# server/feedback_form_server.R + +feedback_form_server <- function(input, output, session, USER = NULL) { + + feedback_status <- reactiveVal(NULL) + submission_in_progress <- reactiveVal(FALSE) + + + # Retrieve translated labels from the platform translation system. + lbl <- function(key) { + + value <- tryCatch( + get_rv_labels(key), + error = function(e) NULL + ) + + if ( + is.null(value) || + length(value) == 0 || + is.na(value[1]) || + !nzchar(trimws(as.character(value[1]))) + ) { + return(key) + } + + as.character(value[1]) + } + + + # Safely convert input values to a single character value. + safe_character <- function(value) { + + if (is.null(value) || length(value) == 0) { + return("") + } + + value <- as.character(value[1]) + + if (is.na(value)) { + return("") + } + + trimws(value) + } + + + output$feedback_status_ui <- renderUI({ + + status <- feedback_status() + + if (is.null(status)) { + return(NULL) + } + + if (identical(status$type, "success")) { + + div( + style = " + padding: 14px 18px; + border-radius: 12px; + background: #e8f7ee; + color: #176b3a; + font-weight: 700; + border: 1px solid #bce5c8; + ", + icon("circle-check"), + status$message + ) + + } else if (identical(status$type, "warning")) { + + div( + style = " + padding: 14px 18px; + border-radius: 12px; + background: #fff8e1; + color: #8a5a00; + font-weight: 700; + border: 1px solid #f0d98c; + ", + icon("triangle-exclamation"), + status$message + ) + + } else { + + div( + style = " + padding: 14px 18px; + border-radius: 12px; + background: #fdecea; + color: #b42318; + font-weight: 700; + border: 1px solid #f5c2c0; + ", + icon("circle-exclamation"), + status$message + ) + } + }) + + + # Disable the submit button while a submission is being processed. + observe({ + + shinyjs::toggleState( + id = "submit_feedback", + condition = !submission_in_progress() + ) + }) + + + observeEvent(input$submit_feedback, { + + if (submission_in_progress()) { + return() + } + + feedback_status(NULL) + + + # Read and clean selected form values. + task <- safe_character(input$feedback_task) + + contact_permission <- safe_character( + input$feedback_contact_permission + ) + + email_address <- safe_character( + input$feedback_email + ) + + + # Validate required task description. + if (!nzchar(task)) { + + feedback_status(list( + type = "error", + message = lbl("feedback_required_message") + )) + + return() + } + + + # Validate email when the user asks to be contacted. + yes_label <- lbl("yes") + + if ( + identical(contact_permission, yes_label) && + !nzchar(email_address) + ) { + + feedback_status(list( + type = "error", + message = lbl("feedback_email_required_message") + )) + + return() + } + + + # Validate email format when an email is entered. + if ( + nzchar(email_address) && + !grepl( + "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$", + email_address + ) + ) { + + feedback_status(list( + type = "error", + message = lbl("feedback_invalid_email_message") + )) + + return() + } + + + submission_in_progress(TRUE) + + on.exit( + submission_in_progress(FALSE), + add = TRUE + ) + + + # Local feedback storage directory. + feedback_dir <- file.path("feedback") + + if (!dir.exists(feedback_dir)) { + + dir.create( + feedback_dir, + recursive = TRUE, + showWarnings = FALSE + ) + } + + + feedback_file <- file.path( + feedback_dir, + "feedback_responses.csv" + ) + + + # Logged-in username. + username <- tryCatch({ + + if (!is.null(USER) && !is.null(USER$username)) { + safe_character(USER$username) + } else { + "" + } + + }, error = function(e) { + "" + }) + + + # Current application language. + current_language <- tryCatch({ + + safe_character(rv_lang$selected_language) + + }, error = function(e) { + "" + }) + + + # Current active application tab. + active_tab <- tryCatch({ + + safe_character(input$tabs) + + }, error = function(e) { + "" + }) + + + # Multi-select issue responses. + issues <- input$feedback_issues + + if (is.null(issues) || length(issues) == 0) { + + issues <- "" + + } else { + + issues <- paste( + as.character(issues), + collapse = "; " + ) + } + + + # Create one submission row. + feedback_row <- data.frame( + + timestamp = format( + Sys.time(), + "%Y-%m-%d %H:%M:%S %Z" + ), + + username = username, + + language = current_language, + + active_tab = active_tab, + + role = safe_character( + input$feedback_role + ), + + use_frequency = safe_character( + input$feedback_use_frequency + ), + + module = safe_character( + input$feedback_module + ), + + task = task, + + ease_of_use = as.integer( + input$feedback_ease + ), + + expected_results = safe_character( + input$feedback_expected_results + ), + + speed_responsiveness = as.integer( + input$feedback_speed + ), + + issues_experienced = issues, + + issue_description = safe_character( + input$feedback_issue_description + ), + + recommend_likelihood = as.integer( + input$feedback_recommend + ), + + contact_permission = contact_permission, + + email_address = email_address, + + session_token = safe_character( + session$token + ), + + stringsAsFactors = FALSE + ) + + + # Save a local backup before sending to Google Sheets. + local_save_success <- FALSE + + tryCatch({ + + if (file.exists(feedback_file)) { + + existing <- read.csv( + feedback_file, + stringsAsFactors = FALSE, + check.names = FALSE, + fileEncoding = "UTF-8" + ) + + expected_columns <- names(feedback_row) + + # If an older CSV has a different structure, preserve it as a backup + # and start a new file using the current structure. + if (!identical(names(existing), expected_columns)) { + + backup_name <- file.path( + feedback_dir, + paste0( + "feedback_responses_old_", + format(Sys.time(), "%Y%m%d_%H%M%S"), + ".csv" + ) + ) + + file.copy( + feedback_file, + backup_name, + overwrite = FALSE + ) + + existing <- data.frame() + } + + + combined <- if (nrow(existing) > 0) { + rbind(existing, feedback_row) + } else { + feedback_row + } + + + write.csv( + combined, + feedback_file, + row.names = FALSE, + na = "", + fileEncoding = "UTF-8" + ) + + } else { + + write.csv( + feedback_row, + feedback_file, + row.names = FALSE, + na = "", + fileEncoding = "UTF-8" + ) + } + + local_save_success <- TRUE + + }, error = function(e) { + + warning( + paste( + lbl("feedback_local_save_warning"), + conditionMessage(e) + ) + ) + }) + + + # Append the same submission to Google Sheets. + sheet_result <- tryCatch({ + + append_feedback_to_google_sheet( + feedback_row + ) + + list( + success = TRUE, + error = NULL + ) + + }, error = function(e) { + + warning( + paste( + lbl("feedback_google_sheet_warning"), + conditionMessage(e) + ) + ) + + list( + success = FALSE, + error = conditionMessage(e) + ) + }) + + + # Handle Google Sheets failure. + if (!sheet_result$success) { + + if (local_save_success) { + + feedback_status(list( + type = "warning", + message = paste( + lbl("feedback_sheet_sync_warning"), + sheet_result$error + ) + )) + + } else { + + feedback_status(list( + type = "error", + message = paste( + lbl("feedback_error_message"), + sheet_result$error + ) + )) + } + + return() + } + + + # Reset form fields after successful submission. + updateSelectInput( + session, + "feedback_role", + selected = character(0) + ) + + updateSelectInput( + session, + "feedback_use_frequency", + selected = character(0) + ) + + updateSelectInput( + session, + "feedback_module", + selected = character(0) + ) + + updateTextAreaInput( + session, + "feedback_task", + value = "" + ) + + updateSliderInput( + session, + "feedback_ease", + value = 3 + ) + + updateRadioButtons( + session, + "feedback_expected_results", + selected = character(0) + ) + + updateSliderInput( + session, + "feedback_speed", + value = 3 + ) + + updateCheckboxGroupInput( + session, + "feedback_issues", + selected = character(0) + ) + + updateTextAreaInput( + session, + "feedback_issue_description", + value = "" + ) + + updateSliderInput( + session, + "feedback_recommend", + value = 3 + ) + + updateRadioButtons( + session, + "feedback_contact_permission", + selected = character(0) + ) + + updateTextInput( + session, + "feedback_email", + value = "" + ) + + + feedback_status(list( + type = "success", + message = lbl("feedback_success_message") + )) + }) +} \ No newline at end of file diff --git a/server/google_sheets_feedback.R b/server/google_sheets_feedback.R new file mode 100644 index 0000000..01051f8 --- /dev/null +++ b/server/google_sheets_feedback.R @@ -0,0 +1,396 @@ +# server/google_sheets_feedback.R +# +# Handles Google Sheets configuration, authentication, +# validation, and feedback submission. + +get_google_feedback_label <- function(key) { + + value <- tryCatch( + get_rv_labels(key), + error = function(e) NULL + ) + + if ( + is.null(value) || + length(value) == 0 || + is.na(value[1]) || + !nzchar(trimws(as.character(value[1]))) + ) { + return(key) + } + + as.character(value[1]) +} + + +get_feedback_sheet_config <- function() { + + sheet_id <- trimws( + Sys.getenv( + "GOOGLE_FEEDBACK_SHEET_ID", + unset = "" + ) + ) + + google_account <- trimws( + Sys.getenv( + "GOOGLE_FEEDBACK_ACCOUNT", + unset = "" + ) + ) + + token_cache <- trimws( + Sys.getenv( + "GOOGLE_FEEDBACK_TOKEN_CACHE", + unset = ".secrets" + ) + ) + + worksheet <- trimws( + Sys.getenv( + "GOOGLE_FEEDBACK_WORKSHEET", + unset = "1" + ) + ) + + + if (!nzchar(sheet_id)) { + stop( + get_google_feedback_label( + "feedback_sheet_id_not_configured" + ), + call. = FALSE + ) + } + + + if (!nzchar(google_account)) { + stop( + get_google_feedback_label( + "feedback_google_account_not_configured" + ), + call. = FALSE + ) + } + + + if (!nzchar(token_cache)) { + stop( + get_google_feedback_label( + "feedback_token_cache_not_configured" + ), + call. = FALSE + ) + } + + + if (!nzchar(worksheet)) { + stop( + get_google_feedback_label( + "feedback_worksheet_not_configured" + ), + call. = FALSE + ) + } + + + list( + sheet_id = sheet_id, + google_account = google_account, + token_cache = token_cache, + worksheet = worksheet + ) +} + + +authenticate_feedback_sheet <- function() { + + if (!requireNamespace("googlesheets4", quietly = TRUE)) { + stop( + get_google_feedback_label( + "feedback_googlesheets_package_missing" + ), + call. = FALSE + ) + } + + + config <- get_feedback_sheet_config() + + + app_root <- normalizePath( + ".", + winslash = "/", + mustWork = TRUE + ) + + + token_cache_path <- if ( + grepl( + "^(?:[A-Za-z]:[/\\\\]|/)", + config$token_cache + ) + ) { + config$token_cache + } else { + file.path( + app_root, + config$token_cache + ) + } + + + if (!dir.exists(token_cache_path)) { + + created <- dir.create( + token_cache_path, + recursive = TRUE, + showWarnings = FALSE + ) + + if (!created && !dir.exists(token_cache_path)) { + stop( + get_google_feedback_label( + "feedback_token_cache_creation_failed" + ), + call. = FALSE + ) + } + } + + + authentication_result <- tryCatch({ + + googlesheets4::gs4_auth( + email = config$google_account, + cache = token_cache_path, + scopes = "https://www.googleapis.com/auth/spreadsheets" + ) + + TRUE + + }, error = function(e) { + + stop( + paste( + get_google_feedback_label( + "feedback_google_authentication_failed" + ), + conditionMessage(e) + ), + call. = FALSE + ) + }) + + + if (!isTRUE(authentication_result)) { + stop( + get_google_feedback_label( + "feedback_google_authentication_failed" + ), + call. = FALSE + ) + } + + + authenticated_user <- tryCatch( + googlesheets4::gs4_user(), + error = function(e) NULL + ) + + + if (is.null(authenticated_user)) { + stop( + get_google_feedback_label( + "feedback_google_user_not_authenticated" + ), + call. = FALSE + ) + } + + + config$token_cache <- token_cache_path + + invisible(config) +} + + +normalise_feedback_for_sheet <- function(feedback_row) { + + if (!is.data.frame(feedback_row)) { + stop( + get_google_feedback_label( + "feedback_row_not_dataframe" + ), + call. = FALSE + ) + } + + + if (nrow(feedback_row) != 1L) { + stop( + get_google_feedback_label( + "feedback_row_invalid_count" + ), + call. = FALSE + ) + } + + + if (ncol(feedback_row) == 0L) { + stop( + get_google_feedback_label( + "feedback_row_has_no_columns" + ), + call. = FALSE + ) + } + + + if ( + is.null(names(feedback_row)) || + any(!nzchar(names(feedback_row))) + ) { + stop( + get_google_feedback_label( + "feedback_row_missing_column_names" + ), + call. = FALSE + ) + } + + + feedback_row[] <- lapply( + feedback_row, + function(column) { + + if ( + inherits( + column, + c( + "POSIXct", + "POSIXlt", + "Date" + ) + ) + ) { + return(as.character(column)) + } + + + if (is.factor(column)) { + return(as.character(column)) + } + + + if (is.list(column)) { + return( + vapply( + column, + function(value) { + + if ( + is.null(value) || + length(value) == 0 + ) { + return("") + } + + paste( + as.character(value), + collapse = "; " + ) + }, + character(1) + ) + ) + } + + + column + } + ) + + + feedback_row[] <- lapply( + feedback_row, + function(column) { + + if (is.character(column)) { + column[is.na(column)] <- "" + } + + if (is.numeric(column)) { + column[is.na(column)] <- NA_real_ + } + + if (is.logical(column)) { + column[is.na(column)] <- FALSE + } + + column + } + ) + + + feedback_row +} + + +append_feedback_to_google_sheet <- function(feedback_row) { + + config <- authenticate_feedback_sheet() + + feedback_row <- normalise_feedback_for_sheet( + feedback_row + ) + + + worksheet <- if ( + grepl( + "^[0-9]+$", + config$worksheet + ) + ) { + as.integer(config$worksheet) + } else { + config$worksheet + } + + + append_result <- tryCatch({ + + googlesheets4::sheet_append( + ss = config$sheet_id, + data = feedback_row, + sheet = worksheet + ) + + TRUE + + }, error = function(e) { + + stop( + paste( + get_google_feedback_label( + "feedback_google_sheet_append_failed" + ), + conditionMessage(e) + ), + call. = FALSE + ) + }) + + + if (!isTRUE(append_result)) { + stop( + get_google_feedback_label( + "feedback_google_sheet_append_failed" + ), + call. = FALSE + ) + } + + + invisible(TRUE) +} \ No newline at end of file diff --git a/server/header_footer_configs.R b/server/header_footer_configs.R index 5c6aee6..8dfc9ed 100644 --- a/server/header_footer_configs.R +++ b/server/header_footer_configs.R @@ -184,7 +184,41 @@ menu_translation = function(){ history_sidebar_command_item("save", get_rv_labels("save"), icon("floppy-disk", lib = "font-awesome")) ), - menuItem(HTML(paste0("", get_rv_labels("menu_additional_resources"), "")), tabName = "addResources", icon = icon("book")), + menuItem( + text = HTML( + paste0( + "", + get_rv_labels("menu_additional_resources"), + "" + ) + ), + icon = icon("book"), + startExpanded = FALSE, + + menuSubItem( + text = HTML( + paste0( + "", + get_rv_labels("feedback_form"), + "" + ) + ), + tabName = "feedbackForm", + icon = icon("comment-dots") + ), + + menuSubItem( + text = HTML( + paste0( + "", + get_rv_labels("user_manual"), + "" + ) + ), + tabName = "userManual", + icon = icon("book-open") + ) + ), if (isTRUE(isolate(USER$username) %in% c("scygu@aphrc.org", "guest565@aphrc.org"))) { menuItem(get_rv_labels("admin_menu_label"), tabName = "adminPanel", icon = icon("shield-halved")) diff --git a/static_files/labelling_file.xlsx b/static_files/labelling_file.xlsx index f1d3156..e2841dc 100644 Binary files a/static_files/labelling_file.xlsx and b/static_files/labelling_file.xlsx differ diff --git a/ui/add_resources_ui.R b/ui/add_resources_ui.R index 26ed7d3..3892e95 100644 --- a/ui/add_resources_ui.R +++ b/ui/add_resources_ui.R @@ -1,5 +1,512 @@ -add_resources_ui = function() { - tabItem(tabName = "addResources", - fluidRow() - ) -} +# ui/add_resources_ui.R + + +feedback_form_ui <- function() { + + tabItem( + tabName = "feedbackForm", + + div( + style = "padding: 25px;", + + div( + style = " + background: #ffffff; + border-radius: 20px; + padding: 28px; + box-shadow: 0 10px 30px rgba(0,0,0,0.08); + border: 1px solid #e8eef7; + width: 100%; + ", + + uiOutput("feedback_form_title_ui"), + uiOutput("feedback_form_description_ui"), + + tags$hr(), + + fluidRow( + column( + width = 6, + uiOutput("feedback_role_ui") + ), + column( + width = 6, + uiOutput("feedback_use_frequency_ui") + ) + ), + + fluidRow( + column( + width = 6, + uiOutput("feedback_module_ui") + ), + column( + width = 6, + uiOutput("feedback_task_ui") + ) + ), + + fluidRow( + column( + width = 6, + uiOutput("feedback_ease_ui") + ), + column( + width = 6, + uiOutput("feedback_expected_results_ui") + ) + ), + + fluidRow( + column( + width = 6, + uiOutput("feedback_speed_ui") + ), + column( + width = 6, + uiOutput("feedback_recommend_ui") + ) + ), + + uiOutput("feedback_issues_ui"), + + uiOutput("feedback_issue_description_ui"), + uiOutput("feedback_additional_comments_ui"), + + fluidRow( + column( + width = 6, + uiOutput("feedback_contact_permission_ui") + ), + column( + width = 6, + uiOutput("feedback_email_ui") + ) + ), + + br(), + + actionButton( + inputId = "submit_feedback", + label = NULL, + icon = icon("paper-plane"), + class = "btn-primary", + style = " + padding: 12px 22px; + border-radius: 12px; + font-weight: 700; + background: #2c7be5; + border: none; + " + ), + + br(), + br(), + + uiOutput("feedback_status_ui") + ) + ) + ) +} + + +user_manual_ui <- function() { + + tabItem( + tabName = "userManual", + + div( + style = "padding: 25px;", + + div( + style = " + background: #ffffff; + border-radius: 20px; + padding: 28px; + box-shadow: 0 10px 30px rgba(0,0,0,0.08); + border: 1px solid #e8eef7; + ", + + uiOutput("user_manual_title_ui"), + uiOutput("user_manual_description_ui"), + + tags$iframe( + src = "manuals/user_manual.pdf", + width = "100%", + height = "850px", + style = " + border: none; + border-radius: 16px; + ", + loading = "lazy" + ), + + br(), + + uiOutput("user_manual_button_ui") + ) + ) + ) +} + + +add_resources_translation <- function(output, session) { + + lbl <- function(key) { + + value <- tryCatch( + get_rv_labels(key), + error = function(e) NULL + ) + + if ( + is.null(value) || + length(value) == 0 || + is.na(value[1]) || + !nzchar(trimws(as.character(value[1]))) + ) { + return(key) + } + + as.character(value[1]) + } + + + output$feedback_form_title_ui <- renderUI({ + + h2( + icon("comment-dots"), + lbl("feedback_form_title"), + style = " + font-weight: 800; + color: #243447; + margin-top: 0; + " + ) + }) + + + output$feedback_form_description_ui <- renderUI({ + + p( + lbl("feedback_form_description"), + style = " + font-size: 16px; + color: #5f6b7a; + " + ) + }) + + + output$feedback_role_ui <- renderUI({ + + selectInput( + inputId = "feedback_role", + label = lbl("feedback_role"), + choices = c( + lbl("feedback_role_researcher"), + lbl("feedback_role_data_scientist"), + lbl("feedback_role_statistician"), + lbl("feedback_role_epidemiologist"), + lbl("feedback_role_public_health"), + lbl("feedback_role_monitoring_evaluation"), + lbl("feedback_role_student"), + lbl("feedback_role_policy_maker"), + lbl("feedback_other") + ), + selected = character(0), + width = "100%" + ) + }) + + + output$feedback_use_frequency_ui <- renderUI({ + + selectInput( + inputId = "feedback_use_frequency", + label = lbl("feedback_use_frequency"), + choices = c( + lbl("feedback_frequency_first_time"), + lbl("feedback_frequency_occasionally"), + lbl("feedback_frequency_monthly"), + lbl("feedback_frequency_weekly"), + lbl("feedback_frequency_daily") + ), + selected = character(0), + width = "100%" + ) + }) + + + output$feedback_module_ui <- renderUI({ + + checkboxGroupInput( + inputId = "feedback_module", + label = lbl("feedback_module"), + choices = c( + lbl("feedback_module_data_management"), + lbl("feedback_module_data_visualization"), + lbl("feedback_module_data_anonymization"), + lbl("feedback_module_omop"), + lbl("feedback_module_research_questions"), + lbl("feedback_module_machine_learning"), + lbl("feedback_module_deep_learning"), + lbl("feedback_module_overall"), + lbl("feedback_other") + ), + selected = character(0), + width = "100%" + ) + }) + + + output$feedback_task_ui <- renderUI({ + + textAreaInput( + inputId = "feedback_task", + label = lbl("feedback_task"), + placeholder = lbl("feedback_your_answer"), + width = "100%", + height = "120px" + ) + }) + + + output$feedback_ease_ui <- renderUI({ + + tagList( + tags$label( + lbl("feedback_ease"), + class = "control-label" + ), + + div( + style = " + display: flex; + justify-content: space-between; + margin-bottom: 4px; + color: #5f6b7a; + font-size: 13px; + ", + span(lbl("feedback_ease_very_difficult")), + span(lbl("feedback_ease_very_easy")) + ), + + sliderInput( + inputId = "feedback_ease", + label = NULL, + min = 1, + max = 5, + value = 3, + step = 1, + width = "100%" + ) + ) + }) + + + output$feedback_expected_results_ui <- renderUI({ + + radioButtons( + inputId = "feedback_expected_results", + label = lbl("feedback_expected_results"), + choices = c( + lbl("feedback_expected_completely"), + lbl("feedback_expected_mostly"), + lbl("feedback_expected_partially"), + lbl("feedback_expected_not_at_all") + ), + selected = character(0) + ) + }) + + + output$feedback_speed_ui <- renderUI({ + + tagList( + tags$label( + lbl("feedback_speed"), + class = "control-label" + ), + + div( + style = " + display: flex; + justify-content: space-between; + margin-bottom: 4px; + color: #5f6b7a; + font-size: 13px; + ", + span(lbl("feedback_speed_very_poor")), + span(lbl("feedback_speed_excellent")) + ), + + sliderInput( + inputId = "feedback_speed", + label = NULL, + min = 1, + max = 5, + value = 3, + step = 1, + width = "100%" + ) + ) + }) + + + output$feedback_issues_ui <- renderUI({ + + checkboxGroupInput( + inputId = "feedback_issues", + label = lbl("feedback_issues"), + choices = c( + lbl("feedback_issue_slow_loading"), + lbl("feedback_issue_errors"), + lbl("feedback_issue_incorrect_outputs"), + lbl("feedback_issue_visualization"), + lbl("feedback_issue_navigation"), + lbl("feedback_issue_missing_functionality"), + lbl("feedback_issue_none"), + lbl("feedback_other") + ), + selected = character(0), + width = "100%" + ) + }) + + + output$feedback_issue_description_ui <- renderUI({ + + textAreaInput( + inputId = "feedback_issue_description", + label = lbl("feedback_issue_description"), + placeholder = lbl("feedback_your_answer"), + width = "100%", + height = "130px" + ) + }) + + + output$feedback_recommend_ui <- renderUI({ + + tagList( + tags$label( + lbl("feedback_recommend"), + class = "control-label" + ), + + div( + style = " + display: flex; + justify-content: space-between; + margin-bottom: 4px; + color: #5f6b7a; + font-size: 13px; + ", + span(lbl("feedback_recommend_not_likely")), + span(lbl("feedback_recommend_extremely_likely")) + ), + + sliderInput( + inputId = "feedback_recommend", + label = NULL, + min = 1, + max = 5, + value = 3, + step = 1, + width = "100%" + ) + ) + }) + + output$feedback_additional_comments_ui <- renderUI({ + + textAreaInput( + inputId = "feedback_additional_comments", + label = lbl("feedback_additional_comments"), + placeholder = lbl("feedback_your_answer"), + width = "100%", + height = "130px" + ) + }) + + output$feedback_contact_permission_ui <- renderUI({ + + radioButtons( + inputId = "feedback_contact_permission", + label = lbl("feedback_contact_permission"), + choices = c( + lbl("yes"), + lbl("no") + ), + selected = character(0) + ) + }) + + + output$feedback_email_ui <- renderUI({ + + textInput( + inputId = "feedback_email", + label = lbl("feedback_email"), + placeholder = lbl("feedback_your_answer"), + width = "100%" + ) + }) + + + observe({ + + updateActionButton( + session = session, + inputId = "submit_feedback", + label = lbl("submit_feedback") + ) + }) + + + output$user_manual_title_ui <- renderUI({ + + h2( + icon("book-open"), + lbl("user_manual_title"), + style = " + font-weight: 800; + color: #243447; + margin-top: 0; + " + ) + }) + + + output$user_manual_description_ui <- renderUI({ + + p( + lbl("user_manual_description"), + style = " + font-size: 16px; + color: #5f6b7a; + " + ) + }) + + + output$user_manual_button_ui <- renderUI({ + + tags$a( + lbl("open_user_manual"), + href = "manuals/user_manual.pdf", + target = "_blank", + style = " + display: inline-block; + margin-top: 18px; + padding: 12px 20px; + border-radius: 12px; + background: #2c7be5; + color: white; + text-decoration: none; + font-weight: 700; + " + ) + }) +} \ No newline at end of file diff --git a/ui/dashboard_body.R b/ui/dashboard_body.R index 2ebb4c4..77a542d 100644 --- a/ui/dashboard_body.R +++ b/ui/dashboard_body.R @@ -148,8 +148,9 @@ aphrcBody <- dashboardBody( , deeplearning_ui() ## OMOP resources - , add_resources_ui() - + , feedback_form_ui() + , user_manual_ui() + ## Admin dashboard (only rendered for admin user) , admin_ui() diff --git a/ui/load_r_packages.R b/ui/load_r_packages.R index 5a7e35e..58813ed 100644 --- a/ui/load_r_packages.R +++ b/ui/load_r_packages.R @@ -96,7 +96,8 @@ libraries <- c( "fs", "rpart", "RSNNS", - "naivebayes" + "naivebayes", + "googlesheets4" ) diff --git a/ui/tooltip.R b/ui/tooltip.R index f9f4bd9..7396552 100644 --- a/ui/tooltip.R +++ b/ui/tooltip.R @@ -21,6 +21,7 @@ nocode_tooltip_keys <- function() { omop_visualizations = "tooltip_omop_visualizations", CohortConstructor = "tooltip_CohortConstructor", FeatureExtraction = "tooltip_FeatureExtraction", + anonymization = "tooltip_anonymization", anonymization_quant = "tooltip_anonymization_quant", anonymization_qual = "tooltip_anonymization_qual", researchQuestions = "tooltip_researchQuestions",