From be840bc26fcbbaee24ca559bda21fcebfa3c14bd Mon Sep 17 00:00:00 2001 From: raortegar Date: Sun, 31 Aug 2025 21:44:29 +0200 Subject: [PATCH] MDL-80447 core_repository: Add upload button to filepicker repository --- lang/en/repository.php | 3 + .../filemanager_modal_generallayout.mustache | 5 + repository/filepicker.js | 218 ++++++++++++++++++ 3 files changed, 226 insertions(+) diff --git a/lang/en/repository.php b/lang/en/repository.php index 62ff53560c8..0d3309f64b4 100644 --- a/lang/en/repository.php +++ b/lang/en/repository.php @@ -117,6 +117,7 @@ $string['errordoublereference'] = 'Unable to overwrite file with a link because $string['errornotyourfile'] = 'You can only pick files which you added.'; $string['erroruniquename'] = 'Repository instance name should be unique'; $string['errorpostmaxsize'] = 'The file you tried to upload is too large for the server to process.'; +$string['erroruploadfailed'] = 'File upload failed. Please check your files and try again.'; $string['errorwhilecommunicatingwith'] = 'Error while communicating with the repository \'{$a}\'.'; $string['errorwhiledownload'] = 'An error occurred while downloading the file: {$a}'; $string['existingrepository'] = 'This repository already exists'; @@ -244,6 +245,8 @@ $string['type'] = 'Type'; $string['typenotvisible'] = 'Type not visible'; $string['unknownoriginal'] = 'Unknown'; $string['upload'] = 'Upload this file'; +$string['uploaderror'] = 'File upload error'; +$string['uploadfilesdrop'] = 'Drag and drop a file to upload, or click to select'; $string['uploading'] = 'Uploading...'; $string['uploadsucc'] = 'The file has been uploaded successfully'; $string['unknownsource'] = 'Unknown source'; diff --git a/lib/templates/filemanager_modal_generallayout.mustache b/lib/templates/filemanager_modal_generallayout.mustache index 3657e112262..120c555fb22 100644 --- a/lib/templates/filemanager_modal_generallayout.mustache +++ b/lib/templates/filemanager_modal_generallayout.mustache @@ -56,6 +56,11 @@ {{#pix}}a/setting{{/pix}} +
+ + {{#pix}}i/upload{{/pix}} + +
{{#pix}}a/help{{/pix}} diff --git a/repository/filepicker.js b/repository/filepicker.js index c5388767822..ef0c6609841 100644 --- a/repository/filepicker.js +++ b/repository/filepicker.js @@ -1632,6 +1632,8 @@ M.core_filepicker.init = function(Y, options) { this.active_repo.message = (data.message || ''); this.active_repo.help = data.help?data.help:null; this.active_repo.manage = data.manage?data.manage:null; + this.active_repo.uploadfile = data.uploadfile ? data.uploadfile : null; + this.active_repo.uploadurl = data.uploadurl ? data.uploadurl : null; // Warning message related to the file reference option, if applicable to the given repository. this.active_repo.filereferencewarning = data.filereferencewarning ? data.filereferencewarning : null; this.print_header(); @@ -2003,6 +2005,219 @@ M.core_filepicker.init = function(Y, options) { managelnk.simulate('click') }); + // New Upload functionality. + toolbar.one('.fp-tb-uploadfile').one('a,button').on('click', function(e) { + e.preventDefault(); + var repoId = this.active_repo.id; + var contextId = this.options.context.id; + var draftidRef = {value: null}; // Store draftid between uploads. + var uploadurl = this.active_repo.uploadurl; + + require([ + 'core/modal_factory', + 'core/modal_events', + 'core/dropzone', + 'core/notification', + 'core/str', + 'core/templates' + ], + function(ModalFactory, ModalEvents, DropZone, Notification, Str, Templates) { + // Create a Modal box with a dropzone where files can be uploaded. + function openUploadModal() { + Templates.render('core/dropzone', {}).then(function(bodyHtml) { + ModalFactory.create({ + type: ModalFactory.types.SAVE_CANCEL, + title: Str.get_string('upload'), + body: bodyHtml, + large: true + }).then(function(modal) { + modal.getRoot().on(ModalEvents.shown, function() { + initDropzone(modal); + }); + modal.getRoot().on(ModalEvents.hidden, function() { + modal.destroy(); + }); + modal.getRoot().on(ModalEvents.save, function(ev) { + commitFiles(ev, modal, toolbar); + }); + modal.getRoot().on(ModalEvents.cancel, function() { + modal.hide(); + }); + modal.show(); + }); + }); + } + openUploadModal(); + + // Create dropzone inside the modal. + function initDropzone(modal) { + const $body = modal.getBody(); + + const dropzoneContainer = $body.find('.dropzone-container').get(0); + const dz = new DropZone(dropzoneContainer, '*', function(files) { + handleDroppedFiles(files, modal); + }); + + // Asynchronously load and set the label. + Str.get_string('dropfiles', 'repository').then(function(label) { + dz.setLabel(label); + }); + + dz.init(); + } + + // Handle file upload to save it as draft. + function handleDroppedFiles(files, modal) { + var currentIndex = 0; + + function uploadNextFile() { + if (currentIndex >= files.length) { + return; + } + + var file = files[currentIndex]; + currentIndex++; + + var formData = new FormData(); + formData.append('repo_upload_file', file); + formData.append('repo_id', repoId); + formData.append('contextid', contextId); + formData.append('sesskey', M.cfg.sesskey); + formData.append('action', 'upload'); + + if (draftidRef.value) { + formData.append('itemid', draftidRef.value); + } + + var xhr = new XMLHttpRequest(); + xhr.open('POST', M.cfg.wwwroot + uploadurl, true); + + xhr.onload = function() { + var response = JSON.parse(this.responseText); + if (response.error) { + Notification.alert( + Str.get_string('uploaderror', 'repository'), + response.error, + Str.get_string('close', 'repository') + ); + } else { + if (response.draftid && !draftidRef.value) { + draftidRef.value = response.draftid; + } + renderUploadedFile(modal, response); + + // Upload next file after this one finishes. + uploadNextFile(); + } + }; + + xhr.send(formData); + } + + // Start uploading files. + uploadNextFile(); + } + + // Render the uploaded file in a given modal. + function renderUploadedFile(modal, response) { + const $body = modal.getBody(); + let uploadFilesClass = 'uploaded-files'; + let $fileList = $body.find(`.${uploadFilesClass} ul`); + + if ($fileList.length === 0) { + // Load the string asynchronously. + Str.get_string('attachedfiles', 'repository').then(function(label) { + const uploadedFiles = document.createElement('div'); + uploadedFiles.className = uploadFilesClass + ' pt-4'; + const headingElement = document.createElement('h5'); + headingElement.textContent = label; + uploadedFiles.appendChild(headingElement); + uploadedFiles.appendChild(document.createElement('ul')); + $body.append(uploadedFiles); + + $fileList = $body.find(`.${uploadFilesClass} ul`); + $fileList.append(`
  • ${response.file}
  • `); + }); + } + + $fileList.append(`
  • ${response.file}
  • `); + } + + // Commit uploaded draft files when user clicks save. + function commitFiles(ev, modal, toolbar) { + ev.preventDefault(); + + if (!draftidRef.value) { + Notification.alert( + Str.get_string('uploaderror', 'repository'), + Str.get_string('nofilesattached', 'repository'), + Str.get_string('close', 'repository') + ); + return; + } + + // Show loading spinner using Moodle's core/loading template. + Templates.render('core/loading', {size: 'lg'}).then(function(html) { + // Create a full-page overlay with Moodle classes. + const loadingOverlay = document.createElement('div'); + loadingOverlay.className = 'loading-overlay'; + loadingOverlay.className = 'fixed-top w-100 h-100 d-flex justify-content-center align-items-center'; + loadingOverlay.style.zIndex = '9999'; + loadingOverlay.style.backgroundColor = 'rgba(255, 255, 255, 0.8)'; + + // Create container for the loading spinner. + const loadingContainer = document.createElement('div'); + loadingContainer.className = 'd-flex flex-column align-items-center'; + loadingContainer.innerHTML = html; + + // Add text below the spinner. + Str.get_string('uploading', 'repository').then(function(loadingText) { + const loadingTextElement = document.createElement('p'); + loadingTextElement.className = 'mt-2'; + loadingTextElement.textContent = loadingText; + loadingContainer.appendChild(loadingTextElement); + }); + + loadingOverlay.appendChild(loadingContainer); + document.body.appendChild(loadingOverlay); + + var formData = new FormData(); + formData.append('action', 'commit'); + formData.append('repo_id', repoId); + formData.append('contextid', contextId); + formData.append('sesskey', M.cfg.sesskey); + formData.append('itemid', draftidRef.value); + + var xhr = new XMLHttpRequest(); + xhr.open('POST', M.cfg.wwwroot + uploadurl, true); + + xhr.onload = function() { + // Remove loading overlay. + if (loadingOverlay && loadingOverlay.parentNode) { + loadingOverlay.parentNode.removeChild(loadingOverlay); + } + + let response = JSON.parse(xhr.responseText); + if (response.error) { + Notification.alert( + Str.get_string('uploaderror', 'repository'), + response.error, + Str.get_string('close', 'repository') + ); + } else { + modal.hide(); + var refreshButton = toolbar.one('.fp-tb-refresh').one('a,button'); + if (refreshButton) { + refreshButton.simulate('click'); + } + } + }; + xhr.send(formData); + }); + } + }); + }, this); + // same with .fp-tb-help var helplnk = Y.Node.create('
    '). setAttrs({id:'fp-tb-help-'+client_id+'-link', target:'_blank'}). @@ -2079,6 +2294,9 @@ M.core_filepicker.init = function(Y, options) { enable_tb_control(toolbar.one('.fp-tb-manage'), r.manage); Y.one('#fp-tb-manage-'+client_id+'-link').set('href', r.manage); + // Upload file. + enable_tb_control(toolbar.one('.fp-tb-uploadfile'), r.uploadfile); + // help url enable_tb_control(toolbar.one('.fp-tb-help'), r.help); Y.one('#fp-tb-help-'+client_id+'-link').set('href', r.help);