diff --git a/R/app_ui.R b/R/app_ui.R index 4b39fcc..60e7879 100644 --- a/R/app_ui.R +++ b/R/app_ui.R @@ -15,7 +15,8 @@ app_ui <- function(request) { path = app_sys("app/www"), app_title = "ESQapp" ), - tags$script(src = "www/app_exit_protection.js") + tags$script(src = "www/app_exit_protection.js"), + tags$script(src = "www/enhanced_file_input.js") ), # Your application UI logic bslib::page_navbar( diff --git a/R/mod_import.R b/R/mod_import.R index 033371f..28f9a2d 100644 --- a/R/mod_import.R +++ b/R/mod_import.R @@ -10,13 +10,40 @@ mod_import_ui <- function(id) { ns <- NS(id) tagList( - shinyFiles::shinyFilesButton( - ns("projectConfigurationFile"), - "Select Project Configuration", - "Please select the projectConfiguration excel file", - multiple = FALSE + tags$div( + id = ns("dropzone"), # enhanced_file_input.js targets this ID + bslib::card( + height = "auto", + style = "border: 2px dashed #007bff; margin-bottom: 15px;", + bslib::card_body( + class = "text-center p-4", + fileInput( + ns("projectConfigurationFile"), + label = NULL, + accept = c(".xlsx", ".xls"), + buttonLabel = "Browse Files", + placeholder = "No file selected", + width = "100%" + ), + div( + style = "margin-top: 15px;", + tags$div(style = "font-size: 2.5rem; color: #007bff; margin-bottom: 10px;", "📁"), + tags$h6("Project Configuration File", + style = "margin: 10px 0 5px 0; color: #495057; font-weight: 600;"), + tags$small("Select an Excel file (.xlsx, .xls)", + style = "color: #6c757d; display: block; margin-bottom: 5px;"), + tags$small("Drag and drop supported", + style = "color: #28a745; font-style: italic;") + ) + ) + ) ), - uiOutput(ns("selected_file_path")) + uiOutput(ns("selected_file_path")), + # Visual state while dragging over the dropzone + tags$style(HTML(sprintf( + "#%s.dragover { border-color:#28a745 !important; background-color:#e8f5e8; }", + ns("dropzone") + ))) ) } @@ -27,30 +54,25 @@ mod_import_server <- function(id, r, DROPDOWNS) { moduleServer(id, function(input, output, session) { ns <- session$ns - volumes <- c( - "Current Project" = getwd(), - # "Test Project" = testthat::test_path("data"), - Home = Sys.getenv("R_USER"), - shinyFiles::getVolumes()() - ) - - shinyFiles::shinyFileChoose(input, - id = "projectConfigurationFile", - roots = volumes, - filetypes = c("xlsx"), - session = session - ) - projectConfiguration <- reactive({ req(input$projectConfigurationFile) - projectConfigurationFilePath <- shinyFiles::parseFilePaths( - volumes, - input$projectConfigurationFile - ) - - req(projectConfigurationFilePath$datapath) - esqlabsR::createDefaultProjectConfiguration(path = projectConfigurationFilePath$datapath) + # Get the uploaded file info + file_info <- input$projectConfigurationFile + req(file_info$datapath) + + # Validate file extension + file_ext <- tools::file_ext(file_info$name) + if (!file_ext %in% c("xlsx", "xls")) { + showNotification( + "Please select an Excel file (.xlsx or .xls)", + type = "error", + duration = 5 + ) + return(NULL) + } + + esqlabsR::createDefaultProjectConfiguration(path = file_info$datapath) }) observeEvent(projectConfiguration(), { @@ -107,14 +129,39 @@ mod_import_server <- function(id, r, DROPDOWNS) { }) output$selected_file_path <- renderUI({ - req(projectConfiguration()) + req(input$projectConfigurationFile) - verbatimTextOutput(ns("selected_file_path_text")) + file_info <- input$projectConfigurationFile + + # Create a nice display for the selected file + bslib::card( + style = "margin-top: 10px; border-left: 4px solid #28a745;", + bslib::card_body( + style = "padding: 10px 15px;", + div( + style = "display: flex; align-items: center;", + span( + style = "color: #28a745; margin-right: 8px; font-size: 1.2em;", + "✓" + ), + div( + tags$strong("Selected File:", style = "color: #495057;"), + br(), + tags$small(file_info$name, style = "color: #6c757d;"), + br(), + tags$small( + paste("Size:", round(file_info$size / 1024, 1), "KB"), + style = "color: #6c757d;" + ) + ) + ) + ) + ) }) output$selected_file_path_text <- renderPrint({ - req(projectConfiguration()) - projectConfiguration()$projectConfigurationFilePath + req(input$projectConfigurationFile) + input$projectConfigurationFile$name }) # Share project configuration path with the export module diff --git a/inst/app/www/enhanced_file_input.js b/inst/app/www/enhanced_file_input.js new file mode 100644 index 0000000..2b177ed --- /dev/null +++ b/inst/app/www/enhanced_file_input.js @@ -0,0 +1,89 @@ +// Enhanced File Input with Drag and Drop +$(function() { + function initDropzones() { + $("[id$='-dropzone']").each(function() { + var dropzone = $(this); + if (dropzone.data("dropzone-init")) return; // already initialized + dropzone.data("dropzone-init", true); + + var fileInput = dropzone.find("input[type='file']").first(); + if (!fileInput.length) return; + + // Prevent double opening after drop + var suppressNextClick = false; + + // Highlight on drag + dropzone.on("dragover dragenter", function(e) { + e.preventDefault(); e.stopPropagation(); + dropzone.addClass("dragover"); + }); + + dropzone.on("dragleave dragend", function(e) { + e.preventDefault(); e.stopPropagation(); + dropzone.removeClass("dragover"); + }); + + // Drop handler + dropzone.on("drop", function(e) { + e.preventDefault(); e.stopPropagation(); + dropzone.removeClass("dragover"); + + var dt = e.originalEvent && e.originalEvent.dataTransfer; + if (!dt || !dt.files || !dt.files.length) return; + + var file = dt.files[0]; + var ext = (file.name.split(".").pop() || "").toLowerCase(); + if (["xlsx","xls"].indexOf(ext) === -1) { + showFeedback(dropzone, "Invalid file type. Please select .xlsx or .xls", "error"); + return; + } + + try { + var list = new DataTransfer(); + list.items.add(file); + fileInput[0].files = list.files; + fileInput.trigger("change"); + showFeedback(dropzone, "File selected: " + file.name + " • " + (file.size/1024).toFixed(1) + " KB", "success"); + } catch (err) { + // Fallback + showFeedback(dropzone, "Drag-drop not supported; click to choose file.", "error"); + // Only click the input if we really need to + fileInput.trigger("click"); + } + + // Prevent the "ghost click" that some browsers emit after drop + suppressNextClick = true; + setTimeout(function(){ suppressNextClick = false; }, 250); + }); + + // Only clicks on the dropzone *itself* (empty area) open the picker + dropzone.on("click", function(e) { + // If click originated from a child (button/label/input), do nothing + if (e.target !== e.currentTarget) return; + if (suppressNextClick) return; + fileInput.trigger("click"); + }); + + // If user clicks the native input/button/label, stop bubbling to zone + fileInput.on("click", function(e) { + e.stopPropagation(); + }); + + function showFeedback(zone, msg, type) { + zone.find(".upload-feedback").remove(); + var el = $('
'); + el.css("color", type === "success" ? "#28a745" : "#dc3545").text(msg); + zone.append(el); + if (type === "error") { + setTimeout(function(){ el.fadeOut(300, function(){ el.remove(); }); }, 3000); + } + } + }); + } + + // run once on page load + initDropzones(); + + // re-run when Shiny inserts/replaces UI + $(document).on("shiny:value", function(){ initDropzones(); }); +}); diff --git a/inst/app/www/style.css b/inst/app/www/style.css index 3ce0a8b..3c6f238 100644 --- a/inst/app/www/style.css +++ b/inst/app/www/style.css @@ -12,3 +12,87 @@ /* .tab-content>.active.html-fill-container { width: calc(100% + 15%) !important; } */ + +/* Enhanced File Input Styling */ +.modern-file-input { + transition: all 0.3s ease; + cursor: pointer; +} + +.modern-file-input:hover { + border-color: #007bff !important; + background-color: #f0f8ff !important; + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0,123,255,0.15); +} + +/* Style the file input container */ +.form-group.shiny-input-container { + margin-bottom: 0; +} + +/* Style the file input button */ +.btn-default { + background: linear-gradient(135deg, #007bff 0%, #0056b3 100%); + border: none; + color: white; + padding: 10px 20px; + border-radius: 6px; + font-weight: 500; + transition: all 0.3s ease; +} + +.btn-default:hover { + background: linear-gradient(135deg, #0056b3 0%, #004085 100%); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0,123,255,0.3); +} + +/* File input drag and drop enhancement */ +.file-input-enhanced { + position: relative; + border: 2px dashed #dee2e6; + border-radius: 8px; + padding: 20px; + text-align: center; + background: #f8f9fa; + transition: all 0.3s ease; +} + +.file-input-enhanced.dragover { + border-color: #28a745; + background-color: #e8f5e8; +} + +/* Selected file display styling */ +.selected-file-display { + border-left: 4px solid #28a745; + background: linear-gradient(135deg, #f8fff8 0%, #e8f5e8 100%); + border-radius: 0 6px 6px 0; +} + +/* Card hover effects */ +.card { + transition: all 0.3s ease; +} + +.card:hover { + box-shadow: 0 4px 12px rgba(0,0,0,0.1); +} + +/* File upload progress indicator */ +.upload-progress { + width: 100%; + height: 4px; + background: #e9ecef; + border-radius: 2px; + overflow: hidden; + margin-top: 10px; +} + +.upload-progress-bar { + height: 100%; + background: linear-gradient(90deg, #007bff, #28a745); + width: 0%; + transition: width 0.3s ease; +} diff --git a/tests/testthat/test-mod_import_improvements.R b/tests/testthat/test-mod_import_improvements.R new file mode 100644 index 0000000..75c71d5 --- /dev/null +++ b/tests/testthat/test-mod_import_improvements.R @@ -0,0 +1,54 @@ +# Test for the improved file selection dialog +# This file tests the new functionality in mod_import.R + +test_that("mod_import_ui creates modern file input interface", { + # Test the UI function + ui_output <- mod_import_ui("test") + + # Should contain a bslib::card + expect_true(any(grepl("card", as.character(ui_output)))) + + # Should contain fileInput instead of shinyFilesButton + expect_true(any(grepl("fileInput", as.character(ui_output)))) + + # Should not contain shinyFilesButton + expect_false(any(grepl("shinyFilesButton", as.character(ui_output)))) + + # Should contain drag and drop text + expect_true(any(grepl("Drag and drop", as.character(ui_output)))) + + # Should contain Excel file acceptance + expect_true(any(grepl("\\.xlsx", as.character(ui_output)))) +}) + +test_that("file input accepts correct file types", { + # Test that the fileInput accepts .xlsx and .xls files + ui_output <- mod_import_ui("test") + ui_string <- as.character(ui_output) + + # Should accept both xlsx and xls + expect_true(any(grepl("accept.*xlsx", ui_string))) + expect_true(any(grepl("accept.*xls", ui_string))) +}) + +test_that("modern styling is applied", { + # Test that modern styling classes and attributes are present + ui_output <- mod_import_ui("test") + ui_string <- as.character(ui_output) + + # Should have blue dashed border styling + expect_true(any(grepl("border.*dashed.*007bff", ui_string))) + + # Should have center text alignment + expect_true(any(grepl("text-center", ui_string))) + + # Should contain file icon emoji + expect_true(any(grepl("📁", ui_string))) +}) + +# Note: Server function tests would require setting up a Shiny test environment +# These would test: +# - File validation logic +# - Error notifications for invalid files +# - Proper file info extraction +# - Integration with esqlabsR::createDefaultProjectConfiguration \ No newline at end of file