diff --git a/files/renderer.php b/files/renderer.php index f48feb76ffd..6e168e16e6a 100644 --- a/files/renderer.php +++ b/files/renderer.php @@ -113,7 +113,8 @@ class core_files_renderer extends plugin_renderer_base { array('invalidjson', 'repository'), array('popupblockeddownload', 'repository'), array('unknownoriginal', 'repository'), array('confirmdeletefolder', 'repository'), array('confirmdeletefilewithhref', 'repository'), array('confirmrenamefolder', 'repository'), - array('confirmrenamefile', 'repository'), array('newfolder', 'repository'), array('edit', 'moodle') + array('confirmrenamefile', 'repository'), array('newfolder', 'repository'), array('edit', 'moodle'), + ['nofilesselected', 'repository'], ['confirmdeleteselectedfile', 'repository'] ) ); if ($this->page->requires->should_create_one_time_item_now('core_file_managertemplate')) { diff --git a/lang/en/repository.php b/lang/en/repository.php index c01d5899959..f636ee7a742 100644 --- a/lang/en/repository.php +++ b/lang/en/repository.php @@ -67,6 +67,7 @@ $string['configsyncfiletimeout'] = 'Timeout in seconds for synchronising the ext $string['configsyncimagetimeout'] = 'Timeout in seconds for downloading an image file from external repository during synchronisation.'; $string['confirmdelete'] = 'Are you sure you want to delete the repository {$a}? If you choose "Continue and download", file references to external contents will be downloaded to Moodle. This could take a long time to process.'; $string['confirmdeletefile'] = 'Are you sure you want to delete this file?'; +$string['confirmdeleteselectedfile'] = 'Are you sure you want to delete the selected {$a} file(s)?'; $string['confirmrenamefile'] = 'Are you sure you want to rename/move this file? There are {$a} alias/shortcut files that use this file as their source. If you proceed then those aliases will be converted to true copies.'; $string['confirmdeletefilewithhref'] = 'Are you sure you want to delete this file? There are {$a} alias/shortcut files that use this file as their source. If you proceed then those aliases will be converted to true copies.'; $string['confirmdeletefolder'] = 'Are you sure you want to delete this folder? All files and subfolders will be deleted.'; @@ -97,6 +98,7 @@ $string['displaytree'] = 'Display folder as file tree'; $string['download'] = 'Download'; $string['downloadallfiles'] = 'Download all files'; $string['downloadfolder'] = 'Download all'; +$string['deleteselected'] = 'Delete selected'; $string['downloadsucc'] = 'The file has been downloaded successfully'; $string['draftareanofiles'] = 'Cannot be downloaded because there is no files attached'; $string['editrepositoryinstance'] = 'Edit repository instance'; @@ -174,6 +176,7 @@ $string['newfoldername'] = 'New folder name'; $string['noenter'] = 'Nothing entered'; $string['nofilesattached'] = 'No files attached'; $string['nofilesavailable'] = 'No files available'; +$string['nofilesselected'] = 'No files selected'; $string['nomorefiles'] = 'No more attachments allowed'; $string['nopathselected'] = 'No destination path select yet (double click tree node to select)'; $string['nopermissiontoaccess'] = 'No permission to access this repository.'; diff --git a/lib/form/filemanager.js b/lib/form/filemanager.js index 7a9a6209c67..200dca1c27a 100644 --- a/lib/form/filemanager.js +++ b/lib/form/filemanager.js @@ -273,6 +273,7 @@ M.form_filemanager.init = function(Y, options) { var button_download = this.filemanager.one('.fp-btn-download'); var button_create = this.filemanager.one('.fp-btn-mkdir'); var button_addfile = this.filemanager.one('.fp-btn-add'); + var buttonDeleteFile = this.filemanager.one('.fp-btn-delete'); // setup 'add file' button button_addfile.on('click', this.show_filepicker, this); @@ -403,6 +404,54 @@ M.form_filemanager.init = function(Y, options) { }); }, this); + buttonDeleteFile.on('click', function(e) { + e.preventDefault(); + var dialogOptions = {}; + var markedForDeletion = this.filemanager.all('.mark-for-deletion:checked'); + var filenames = []; + markedForDeletion.each(function(item) { + var fileinfo = this.options.list.find(function(element) { + return item.getData().fullname == element.fullname; + }); + if (fileinfo && fileinfo != undefined) { + filenames.push({ + filepath: fileinfo.filepath, + filename: fileinfo.filename + }); + } + }, this); + + if (!filenames.length) { + this.print_msg(M.util.get_string('nofilesselected', 'repository'), 'error'); + return; + } + + dialogOptions.scope = this; + var params = { + selected: Y.JSON.stringify(filenames) + }; + dialogOptions.message = M.util.get_string('confirmdeleteselectedfile', 'repository', filenames.length); + dialogOptions.callbackargs = [params]; + dialogOptions.callback = function(params) { + this.request({ + action: 'deleteselected', + scope: this, + params: params, + callback: function(id, obj, args) { + // Do something here + args.scope.filecount -= params.length; + if (obj && obj.length) { + args.scope.refresh(obj[0]); + } + if (typeof M.core_formchangechecker != 'undefined') { + M.core_formchangechecker.set_form_changed(); + } + } + }); + }; + this.show_confirm_dialog(dialogOptions); + }, this); + this.filemanager.all('.fp-vb-icons,.fp-vb-tree,.fp-vb-details'). on('click', function(e) { e.preventDefault(); @@ -569,6 +618,12 @@ M.form_filemanager.init = function(Y, options) { this.viewmode = 1; element_template = Y.Node.create(M.form_filemanager.templates.iconfilename); } + + if (this.viewmode == 1 || this.viewmode == 2) { + this.filemanager.one('.fp-btn-delete').addClass('d-none'); + } else { + this.filemanager.one('.fp-btn-delete').removeClass('d-none'); + } var options = { viewmode : this.viewmode, appendonly : appendfiles != null, diff --git a/lib/templates/filemanager_page_generallayout.mustache b/lib/templates/filemanager_page_generallayout.mustache index 678f40bdf1c..22d9ffcfb81 100644 --- a/lib/templates/filemanager_page_generallayout.mustache +++ b/lib/templates/filemanager_page_generallayout.mustache @@ -49,6 +49,11 @@ {{#pix}}a/download_all{{/pix}} +
+ + {{#pix}}i/trash{{/pix}} + +
{{#str}}loadinghelp{{/str}} {{#pix}}i/loading_small{{/pix}} diff --git a/repository/draftfiles_ajax.php b/repository/draftfiles_ajax.php index dbd96675fa5..eb8c77de023 100644 --- a/repository/draftfiles_ajax.php +++ b/repository/draftfiles_ajax.php @@ -41,7 +41,7 @@ $action = required_param('action', PARAM_ALPHA); $draftid = required_param('itemid', PARAM_INT); $filepath = optional_param('filepath', '/', PARAM_PATH); -$user_context = context_user::instance($USER->id); +$usercontext = context_user::instance($USER->id); echo $OUTPUT->header(); // send headers @@ -73,7 +73,7 @@ switch ($action) { $newdirname = required_param('newdirname', PARAM_FILE); $fs = get_file_storage(); - $fs->create_directory($user_context->id, 'user', 'draft', $draftid, file_correct_filepath(file_correct_filepath($filepath).$newdirname)); + $fs->create_directory($usercontext->id, 'user', 'draft', $draftid, file_correct_filepath(file_correct_filepath($filepath).$newdirname)); $return = new stdClass(); $return->filepath = $filepath; echo json_encode($return); @@ -82,31 +82,28 @@ switch ($action) { case 'delete': $filename = required_param('filename', PARAM_FILE); $filepath = required_param('filepath', PARAM_PATH); + $selectedfile = (object)[ + 'filename' => $filename, + 'filepath' => $filepath + ]; + $return = repository_delete_selected_files($usercontext, 'user', 'draft', $draftid, [$selectedfile]); - $fs = get_file_storage(); - $filepath = file_correct_filepath($filepath); - $return = new stdClass(); - if ($stored_file = $fs->get_file($user_context->id, 'user', 'draft', $draftid, $filepath, $filename)) { - $parent_path = $stored_file->get_parent_directory()->get_filepath(); - if ($stored_file->is_directory()) { - $files = $fs->get_directory_files($user_context->id, 'user', 'draft', $draftid, $filepath, true); - foreach ($files as $file) { - $file->delete(); - } - $stored_file->delete(); - $return->filepath = $parent_path; - echo json_encode($return); - } else { - if($result = $stored_file->delete()) { - $return->filepath = $parent_path; - echo json_encode($return); - } else { - echo json_encode(false); - } - } - } else { - echo json_encode(false); + if ($return) { + $response = new stdClass(); + $response->filepath = array_keys($return)[0]; + echo json_encode($response); + die; } + + echo json_encode(false); + die; + + case 'deleteselected': + $selected = required_param('selected', PARAM_RAW); + $return = []; + $selectedfiles = json_decode($selected); + $return = repository_delete_selected_files($usercontext, 'user', 'draft', $draftid, $selectedfiles); + echo (json_encode($return ? array_keys($return) : false)); die; case 'setmainfile': @@ -115,9 +112,9 @@ switch ($action) { $filepath = file_correct_filepath($filepath); // reset sort order - file_reset_sortorder($user_context->id, 'user', 'draft', $draftid); + file_reset_sortorder($usercontext->id, 'user', 'draft', $draftid); // set main file - $return = file_set_sortorder($user_context->id, 'user', 'draft', $draftid, $filepath, $filename, 1); + $return = file_set_sortorder($usercontext->id, 'user', 'draft', $draftid, $filepath, $filename, 1); echo json_encode($return); die; @@ -159,7 +156,7 @@ switch ($action) { $zipper = get_file_packer('application/zip'); $fs = get_file_storage(); - $file = $fs->get_file($user_context->id, 'user', 'draft', $draftid, $filepath, '.'); + $file = $fs->get_file($usercontext->id, 'user', 'draft', $draftid, $filepath, '.'); $parent_path = $file->get_parent_directory()->get_filepath(); @@ -167,7 +164,7 @@ switch ($action) { $filepath = array_pop($filepath); $zipfile = repository::get_unused_filename($draftid, $parent_path, $filepath . '.zip'); - if ($newfile = $zipper->archive_to_storage(array($filepath => $file), $user_context->id, 'user', 'draft', $draftid, $parent_path, $zipfile, $USER->id)) { + if ($newfile = $zipper->archive_to_storage([$filepath => $file], $usercontext->id, 'user', 'draft', $draftid, $parent_path, $zipfile, $USER->id)) { $return = new stdClass(); $return->filepath = $parent_path; echo json_encode($return); @@ -187,7 +184,7 @@ switch ($action) { die; } - $stored_file = $fs->get_file($user_context->id, 'user', 'draft', $draftid, $filepath, '.'); + $stored_file = $fs->get_file($usercontext->id, 'user', 'draft', $draftid, $filepath, '.'); if ($filepath === '/') { $filename = get_string('files').'.zip'; } else { @@ -197,7 +194,7 @@ switch ($action) { // archive compressed file to an unused draft area $newdraftitemid = file_get_unused_draft_itemid(); - if ($newfile = $zipper->archive_to_storage(array('/' => $stored_file), $user_context->id, 'user', 'draft', $newdraftitemid, '/', $filename, $USER->id)) { + if ($newfile = $zipper->archive_to_storage(['/' => $stored_file], $usercontext->id, 'user', 'draft', $newdraftitemid, '/', $filename, $USER->id)) { $return = new stdClass(); $return->fileurl = moodle_url::make_draftfile_url($newdraftitemid, '/', $filename)->out(); $return->filepath = $filepath; @@ -215,15 +212,15 @@ switch ($action) { $fs = get_file_storage(); - $file = $fs->get_file($user_context->id, 'user', 'draft', $draftid, $filepath, $filename); + $file = $fs->get_file($usercontext->id, 'user', 'draft', $draftid, $filepath, $filename); // Find unused name for directory to extract the archive. - $temppath = $fs->get_unused_dirname($user_context->id, 'user', 'draft', $draftid, $filepath. pathinfo($filename, PATHINFO_FILENAME). '/'); + $temppath = $fs->get_unused_dirname($usercontext->id, 'user', 'draft', $draftid, $filepath. pathinfo($filename, PATHINFO_FILENAME). '/'); $donotremovedirs = array(); $doremovedirs = array($temppath); // Extract archive and move all files from $temppath to $filepath - if ($file->extract_to_storage($zipper, $user_context->id, 'user', 'draft', $draftid, $temppath, $USER->id) !== false) { - $extractedfiles = $fs->get_directory_files($user_context->id, 'user', 'draft', $draftid, $temppath, true); + if ($file->extract_to_storage($zipper, $usercontext->id, 'user', 'draft', $draftid, $temppath, $USER->id) !== false) { + $extractedfiles = $fs->get_directory_files($usercontext->id, 'user', 'draft', $draftid, $temppath, true); $xtemppath = preg_quote($temppath, '|'); foreach ($extractedfiles as $file) { $realpath = preg_replace('|^'.$xtemppath.'|', $filepath, $file->get_filepath()); @@ -231,7 +228,7 @@ switch ($action) { // Set the source to the extracted file to indicate that it came from archive. $file->set_source(serialize((object)array('source' => $filepath))); } - if (!$fs->file_exists($user_context->id, 'user', 'draft', $draftid, $realpath, $file->get_filename())) { + if (!$fs->file_exists($usercontext->id, 'user', 'draft', $draftid, $realpath, $file->get_filename())) { // File or directory did not exist, just move it. $file->rename($realpath, $file->get_filename()); } else if (!$file->is_directory()) { @@ -250,7 +247,7 @@ switch ($action) { } // Remove remaining temporary directories. foreach (array_diff($doremovedirs, $donotremovedirs) as $filepath) { - if ($file = $fs->get_file($user_context->id, 'user', 'draft', $draftid, $filepath, '.')) { + if ($file = $fs->get_file($usercontext->id, 'user', 'draft', $draftid, $filepath, '.')) { $file->delete(); } } @@ -261,7 +258,7 @@ switch ($action) { $filepath = required_param('filepath', PARAM_PATH); $fs = get_file_storage(); - $file = $fs->get_file($user_context->id, 'user', 'draft', $draftid, $filepath, $filename); + $file = $fs->get_file($usercontext->id, 'user', 'draft', $draftid, $filepath, $filename); if (!$file) { echo json_encode(false); } else { @@ -275,7 +272,7 @@ switch ($action) { $filepath = required_param('filepath', PARAM_PATH); $fs = get_file_storage(); - $file = $fs->get_file($user_context->id, 'user', 'draft', $draftid, $filepath, $filename); + $file = $fs->get_file($usercontext->id, 'user', 'draft', $draftid, $filepath, $filename); if (!$file) { echo json_encode(false); } else { diff --git a/repository/filepicker.js b/repository/filepicker.js index 934085f2e07..cf4555c892f 100644 --- a/repository/filepicker.js +++ b/repository/filepicker.js @@ -317,6 +317,16 @@ YUI.add('moodle-core_filepicker', function(Y) { // TODO add tooltip with o.data['title'] (o.value) or o.data['thumbnail_title'] return el.getContent(); } + var formatCheckbox = function(o) { + var el = Y.Node.create('
'); + var checkbox = Y.Node.create(''); + checkbox.setAttribute('type', 'checkbox') + .setAttribute('class', 'mark-for-deletion') + .setAttribute('data-fullname', o.data.fullname); + + el.appendChild(checkbox); + return el.getContent(); + }; /** sorting function for table view */ var sortFoldersFirst = function(a, b, desc) { if (a.get('isfolder') && !b.get('isfolder')) { @@ -331,6 +341,9 @@ YUI.add('moodle-core_filepicker', function(Y) { /** initialize table view */ var initialize_table_view = function() { var cols = [ + {key: "", label: "", + allowHTML: true, formatter: formatCheckbox, + sortable: false}, {key: "displayname", label: M.util.get_string('name', 'moodle'), allowHTML: true, formatter: formatTitle, sortable: true, sortFn: sortFoldersFirst}, {key: "datemodified", label: M.util.get_string('lastmodified', 'moodle'), allowHTML: true, formatter: formatValue, @@ -350,7 +363,15 @@ YUI.add('moodle-core_filepicker', function(Y) { } Y.bind(callback, this)(e, record.getAttrs()); } - }, 'tr', options.callbackcontext, scope.tableview); + }, 'tr td:not(:first-child)', options.callbackcontext, scope.tableview); + scope.tableview.delegate('change', function(e) { + e.preventDefault(); + if (e.target.get('checked')) { + e.container.all('.mark-for-deletion').setAttribute('checked', true); + } else { + e.container.all('.mark-for-deletion').removeAttribute('checked'); + } + }, '#select-all', options.callbackcontext, scope.tableview); if (options.rightclickcallback) { scope.tableview.delegate('contextmenu', function (e, tableview) { var record = tableview.getRecord(e.currentTarget.get('id')); diff --git a/repository/lib.php b/repository/lib.php index eddb0c6a8bf..db377d4e7c3 100644 --- a/repository/lib.php +++ b/repository/lib.php @@ -3209,3 +3209,43 @@ function initialise_filepicker($args) { } return $return; } + +/** + * Convenience function to handle deletion of files. + * + * @param object $context The context where the delete is called + * @param string $component component + * @param string $filearea filearea + * @param int $itemid the item id + * @param array $files Array of files object with each item having filename/filepath as values + * @return array $return Array of strings matching up to the parent directory of the deleted files + * @throws coding_exception + */ +function repository_delete_selected_files($context, string $component, string $filearea, $itemid, array $files) { + $fs = get_file_storage(); + $return = []; + + foreach ($files as $selectedfile) { + $filename = clean_filename($selectedfile->filename); + $filepath = clean_param($selectedfile->filepath, PARAM_PATH); + $filepath = file_correct_filepath($filepath); + + if ($storedfile = $fs->get_file($context->id, $component, $filearea, $itemid, $filepath, $filename)) { + $parentpath = $storedfile->get_parent_directory()->get_filepath(); + if ($storedfile->is_directory()) { + $files = $fs->get_directory_files($context->id, $component, $filearea, $itemid, $filepath, true); + foreach ($files as $file) { + $file->delete(); + } + $storedfile->delete(); + $return[$parentpath] = ""; + } else { + if ($result = $storedfile->delete()) { + $return[$parentpath] = ""; + } + } + } + } + + return $return; +} diff --git a/repository/tests/behat/behat_filepicker.php b/repository/tests/behat/behat_filepicker.php index 368dcb49dab..58660fd9903 100644 --- a/repository/tests/behat/behat_filepicker.php +++ b/repository/tests/behat/behat_filepicker.php @@ -176,6 +176,38 @@ class behat_filepicker extends behat_base { $okbutton->click(); } + /** + * Marks for deletion the specified file or folder from the specified filemanager field. + * + * @Given /^I mark for deletion "(?P(?:[^"]|\\")*)" from filemanager$/ + * @throws ExpectationException Thrown by behat_base::find + * @param string $name + */ + public function i_mark_for_deletion_from_filemanager($name) { + $name = behat_context_helper::escape($name); + $okbutton = $this->find('css', "input.mark-for-deletion[data-fullname=$name]"); + $okbutton->click(); + } + + /** + * Executes delete function and confirms delete + * + * @Given /^I confirm deletion$/ + * @throws ExpectationException Thrown by behat_base::find + */ + public function i_confirm_deletion() { + $name = get_string('deleteselected'); + + // Execute the action. + $okbutton = $this->find('css', "a[title='$name']"); + $okbutton->click(); + + // Yes, we are sure. + // Using xpath + click instead of pressButton as 'Ok' it is a common string. + $okbutton = $this->find('css', 'div.fp-dlg button.fp-dlg-butconfirm'); + $okbutton->click(); + } + /** * Makes sure user can see the exact number of elements (files in folders) in the filemanager. * diff --git a/repository/tests/behat/delete_files.feature b/repository/tests/behat/delete_files.feature index 7bd3db23f13..418bf32590f 100644 --- a/repository/tests/behat/delete_files.feature +++ b/repository/tests/behat/delete_files.feature @@ -19,3 +19,42 @@ Feature: Delete files and folders from the file manager And I delete "Delete me" from "Files" filemanager And I press "Save changes" And I should not see "Delete me" + + @javascript + Scenario: Delete a file and a folder using bulk functionality (individually) + Given I log in as "admin" + And I follow "Manage private files" + And I upload "lib/tests/fixtures/empty.txt" file to "Files" filemanager + And I create "Delete me" folder in "Files" filemanager + And I press "Save changes" + And I follow "Manage private files" + And I click on "[title='Display folder with file details']" "css_element" + And I mark for deletion "empty.txt" from filemanager + And I confirm deletion + Then I should not see "empty.txt" + And I press "Save changes" + And I follow "Manage private files" + Then I should not see "empty.txt" + And I mark for deletion "Delete me" from filemanager + And I confirm deletion + Then I should not see "Delete me" + And I press "Save changes" + And I should not see "Delete me" + + @javascript + Scenario: Delete a file and a folder using bulk functionality (multiple) + Given I log in as "admin" + And I follow "Manage private files" + And I upload "lib/tests/fixtures/empty.txt" file to "Files" filemanager + And I create "Delete me" folder in "Files" filemanager + And I press "Save changes" + And I follow "Manage private files" + And I click on "[title='Display folder with file details']" "css_element" + And I mark for deletion "empty.txt" from filemanager + And I mark for deletion "Delete me" from filemanager + And I confirm deletion + Then I should not see "Delete me" + Then I should not see "empty.txt" + And I press "Save changes" + And I should not see "Delete me" + Then I should not see "empty.txt" diff --git a/repository/tests/repositorylib_test.php b/repository/tests/repositorylib_test.php index a65c5bb30e6..d8885c8df99 100644 --- a/repository/tests/repositorylib_test.php +++ b/repository/tests/repositorylib_test.php @@ -154,6 +154,60 @@ class core_repositorylib_testcase extends advanced_testcase { } } + public function test_delete_selected_files() { + global $USER; + + $this->resetAfterTest(true); + + $this->setAdminUser(); + $fs = get_file_storage(); + + $draftitemid = file_get_unused_draft_itemid(); + $context = context_user::instance($USER->id); + + $dummy = [ + 'contextid' => $context->id, + 'component' => 'user', + 'filearea' => 'draft', + 'itemid' => $draftitemid, + 'filepath' => '/', + 'filename' => '' + ]; + + // Create some files. + $existingfiles = [ + 'The Matrix.movie', + 'Astalavista.txt', + 'foobar', + ]; + + $selectedfiles = [ + 'The Matrix.movie' => [], + 'Astalavista.txt' => [] + ]; + foreach ($existingfiles as $filename) { + $dummy['filename'] = $filename; + $file = $fs->create_file_from_string($dummy, 'Content of ' . $filename); + if (array_key_exists($filename, $selectedfiles)) { + $selectedfiles[$filename] = (object)[ + 'filename' => $filename, + 'filepath' => $file->get_filepath() + ]; + } + } + + // Get area files with default options. + $areafiles = $fs->get_area_files($context->id, 'user', 'draft', $draftitemid); + // Should be the 3 files we added plus the folder. + $this->assertEquals(4, count($areafiles)); + + repository_delete_selected_files($context, 'user', 'draft', $draftitemid, $selectedfiles); + + $areafiles = $fs->get_area_files($context->id, 'user', 'draft', $draftitemid); + // Should be the 1 file left plus the folder. + $this->assertEquals(2, count($areafiles)); + } + public function test_can_be_edited_by_user() { $this->resetAfterTest(true); diff --git a/theme/boost/scss/moodle/filemanager.scss b/theme/boost/scss/moodle/filemanager.scss index 664fa79bc38..c1f03d9e35d 100644 --- a/theme/boost/scss/moodle/filemanager.scss +++ b/theme/boost/scss/moodle/filemanager.scss @@ -553,6 +553,7 @@ a.ygtvspacer:hover { .filemanager.fm-maxfiles .fp-btn-add, .filemanager.fm-maxfiles .dndupload-message, .filemanager.fm-noitems .fp-btn-download, +.filemanager.fm-noitems .fp-btn-delete, .filemanager .fm-empty-container, .filemanager.fm-noitems .filemanager-container .fp-content { display: none; diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css index 91f87b5841d..31c1ce826c8 100644 --- a/theme/boost/style/moodle.css +++ b/theme/boost/style/moodle.css @@ -13930,6 +13930,7 @@ a.ygtvspacer:hover { .filemanager.fm-maxfiles .fp-btn-add, .filemanager.fm-maxfiles .dndupload-message, .filemanager.fm-noitems .fp-btn-download, +.filemanager.fm-noitems .fp-btn-delete, .filemanager .fm-empty-container, .filemanager.fm-noitems .filemanager-container .fp-content { display: none; } diff --git a/theme/classic/style/moodle.css b/theme/classic/style/moodle.css index 9a252daf903..548335990c1 100644 --- a/theme/classic/style/moodle.css +++ b/theme/classic/style/moodle.css @@ -14145,6 +14145,7 @@ a.ygtvspacer:hover { .filemanager.fm-maxfiles .fp-btn-add, .filemanager.fm-maxfiles .dndupload-message, .filemanager.fm-noitems .fp-btn-download, +.filemanager.fm-noitems .fp-btn-delete, .filemanager .fm-empty-container, .filemanager.fm-noitems .filemanager-container .fp-content { display: none; }