api/delete strenghening, dashboard multi-select delete, obsolete site_files/delete (use api instead)

This commit is contained in:
Kyle Drake
2026-05-07 12:32:50 -05:00
parent 69263c0b5f
commit c2e9a121f6
9 changed files with 467 additions and 57 deletions
+18 -5
View File
@@ -203,6 +203,7 @@ post '/api/delete' do
require_api_credentials
api_error 400, 'missing_filenames', 'you must provide files to delete' if params[:filenames].nil? || params[:filenames].empty?
api_error 400, 'bad_filename', 'filenames must be an array, canceled deleting' unless params[:filenames].is_a?(Array)
paths = []
params[:filenames].each do |path|
@@ -210,18 +211,30 @@ post '/api/delete' do
api_error 400, 'bad_filename', "#{path} is not a valid filename, canceled deleting"
end
if current_site.files_path(path) == current_site.files_path
submitted_path = path
begin
path = current_site.scrubbed_path submitted_path
rescue ArgumentError
api_error 400, 'bad_filename', "#{submitted_path} is not a valid filename, canceled deleting"
end
if path.empty?
api_error 400, 'cannot_delete_site_directory', 'cannot delete the root directory of the site'
end
if current_site.invalid_path?(submitted_path) || current_site.invalid_path?(path)
api_error 400, 'bad_filename', "#{submitted_path} is not a valid filename, canceled deleting"
end
if path == 'index.html'
api_error 400, 'cannot_delete_index', 'you cannot delete your index.html file, canceled deleting'
end
if !current_site.file_exists?(path)
api_error 400, 'missing_files', "#{path} was not found on your site, canceled deleting"
end
if path == 'index.html' || path == '/index.html'
api_error 400, 'cannot_delete_index', 'you cannot delete your index.html file, canceled deleting'
end
paths << path
end
-16
View File
@@ -1,19 +1,3 @@
post '/site_files/delete' do
require_login
path = HTMLEntities.new.decode params[:filename]
begin
current_site.delete_file path
rescue Sequel::NoExistingObject
# the deed was presumably already done
end
flash[:success] = "Deleted #{Rack::Utils.escape_html params[:filename]}."
dirname = Pathname(path).dirname
dir_query = dirname.nil? || dirname.to_s == '.' ? '' : "?dir=#{Rack::Utils.escape dirname}"
redirect "/dashboard#{dir_query}"
end
post '/site_files/rename' do
require_login
path = HTMLEntities.new.decode params[:path]
+150 -2
View File
@@ -8,13 +8,157 @@ function confirmFileRename(path) {
}
function confirmFileDelete(name) {
$('#deleteConfirmModal').data('delete-paths', [name]);
$('#deleteFileName').text(name);
$('#deleteFileCount').text('1');
$('#deleteFileList').empty();
$('#deleteSingleMessage').show();
$('#deleteMultipleMessage').hide();
$('#deleteConfirmModal').modal();
}
function fileDelete() {
$('#deleteFilenameInput').val($('#deleteFileName').html());
$('#deleteFilenameForm').submit();
var paths = $('#deleteConfirmModal').data('delete-paths') || [$('#deleteFileName').text()];
var deleteButton = $('#deleteConfirmModal .btn-danger');
deleteButton.prop('disabled', true);
$.ajax({
url: '/api/delete',
type: 'POST',
data: {
csrf_token: $('#deleteCSRFToken').val(),
filenames: paths
},
success: function() {
$('#deleteConfirmModal').modal('hide');
setBulkSelectMode(false);
alertClear();
alertType('success');
if (paths.length === 1) {
alertAdd($('<div>').text(paths[0]).html() + ' has been deleted.');
} else {
alertAdd(paths.length + ' items have been deleted.');
}
reloadDashboardFiles();
},
error: function(xhr) {
var message = 'Failed to delete file(s).';
try {
message = JSON.parse(xhr.responseText).message || message;
} catch(e) {
}
alertClear();
alertType('error');
alertAdd($('<div>').text(message).html());
$('#deleteConfirmModal').modal('hide');
},
complete: function() {
deleteButton.prop('disabled', false);
}
});
}
function selectedFilePaths() {
return $('.bulk-select-checkbox:checked').map(function() {
return $(this).val();
}).get();
}
function updateBulkActions() {
var checkboxes = $('.bulk-select-checkbox');
var checked = $('.bulk-select-checkbox:checked');
var count = checked.length;
var selectAll = $('#bulkSelectAll').get(0);
$('#selectedFileCount').text(count);
$('#bulkDeleteButton').prop('disabled', count === 0);
checkboxes.each(function() {
$(this).closest('.file').toggleClass('bulk-selected', $(this).prop('checked'));
});
if (selectAll) {
selectAll.checked = count > 0 && count === checkboxes.length;
selectAll.indeterminate = count > 0 && count < checkboxes.length;
}
}
function setBulkSelectMode(enabled) {
$('#filesDisplay').toggleClass('bulk-selecting', enabled);
if (!enabled) {
$('.bulk-select-checkbox').prop('checked', false);
}
updateBulkActions();
}
function toggleBulkSelect(event) {
if (event) {
event.preventDefault();
}
setBulkSelectMode(!$('#filesDisplay').hasClass('bulk-selecting'));
}
function clearFileSelection() {
$('.bulk-select-checkbox').prop('checked', false);
updateBulkActions();
}
function toggleSelectAllFiles(checked) {
$('.bulk-select-checkbox').prop('checked', checked);
updateBulkActions();
}
function confirmBulkDelete() {
var paths = selectedFilePaths();
if (paths.length === 0) {
return;
}
$('#deleteConfirmModal').data('delete-paths', paths);
$('#deleteFileName').text(paths[0]);
$('#deleteFileCount').text(paths.length);
$('#deleteFileList').empty();
paths.forEach(function(path) {
$('<li>').text(path).appendTo('#deleteFileList');
});
$('#deleteSingleMessage').toggle(paths.length === 1);
$('#deleteMultipleMessage').toggle(paths.length > 1);
$('#deleteConfirmModal').modal();
}
function initBulkFileSelection() {
$('.bulk-select-checkbox').off('change.bulk').on('change.bulk', updateBulkActions);
$('.file').off('click.bulk').on('click.bulk', function(event) {
if (!$('#filesDisplay').hasClass('bulk-selecting')) {
return;
}
if ($(event.target).closest('a, button, label').length > 0) {
return;
}
var checkbox = $(this).find('.bulk-select-checkbox');
if (checkbox.length === 0) {
return;
}
checkbox.prop('checked', !checkbox.prop('checked')).trigger('change');
event.preventDefault();
});
updateBulkActions();
}
function clickUploadFiles() {
@@ -189,12 +333,16 @@ function reInitDashboardFiles() {
document.getElementById('uploadButton').addEventListener('click', function(event) {
event.preventDefault();
});
initBulkFileSelection();
}
function reloadDashboardFiles() {
var dir = $('#uploads input[name="dir"]').val();
var bulkSelecting = $('#filesDisplay').hasClass('bulk-selecting');
$.get('/dashboard/files?dir='+encodeURIComponent(dir), function(data) {
$('#filesDisplay').html(data);
$('#filesDisplay').toggleClass('bulk-selecting', bulkSelecting);
reInitDashboardFiles();
});
}
+194 -6
View File
@@ -328,6 +328,58 @@
}
}
}
.files.bulk-selecting .select-files-button {
background: #4F727B;
}
.files .bulk-actions {
clear: both;
display: none;
float: left;
width: 100%;
margin-top: 8px;
padding: 8px 10px;
background: rgba(255, 255, 255, .11);
border: 1px solid rgba(255, 255, 255, .18);
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
font-size: .9em;
line-height: 1;
strong {
margin-left: 8px;
vertical-align: middle;
}
.bulk-select-all {
display: inline-block;
margin: 0;
vertical-align: middle;
}
input[type='checkbox'] {
display: inline-block;
margin: 0 5px 0 0;
vertical-align: -1px;
}
.btn, .btn-Action {
margin-left: 8px;
padding: 5px 10px;
font-size: .9em;
}
.bulk-delete[disabled] {
background: #999;
cursor: default;
opacity: .65;
}
@media (max-device-width:480px), screen and (max-width:800px) {
.btn, .btn-Action {
margin: 7px 5px 0 0;
}
}
}
.files.bulk-selecting .bulk-actions {
display: block;
}
.files .btn-Action {
margin-left: 8px;
@@ -431,6 +483,82 @@
display: block;
text-overflow: ellipsis;
}
.file.bulk-selected {
background: #F8F4ED;
outline: 3px solid #77ABB8;
-webkit-border-radius: 8px;
-moz-border-radius: 8px;
border-radius: 8px;
}
.files.bulk-selecting .file {
cursor: pointer;
}
.files.bulk-selecting .link-overlay {
display: none;
}
.bulk-select-control {
display: none;
position: absolute;
top: 6px;
left: 6px;
width: 28px;
height: 28px;
margin: 0;
z-index: 5;
cursor: pointer;
}
.files.bulk-selecting .bulk-select-control {
display: block;
}
.bulk-select-control input[type='checkbox'] {
display: block;
position: absolute;
top: 0;
left: 0;
width: 28px;
height: 28px;
margin: 0;
opacity: 0;
}
.bulk-select-box {
display: block;
width: 28px;
height: 28px;
background: #fff;
border: 2px solid #77ABB8;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
color: #fff;
line-height: 24px;
text-align: center;
-webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, .25);
-moz-box-shadow: 0 1px 4px rgba(0, 0, 0, .25);
box-shadow: 0 1px 4px rgba(0, 0, 0, .25);
}
.bulk-select-box .fa {
display: none;
margin: 0;
}
.bulk-select-checkbox:checked + .bulk-select-box {
background: #E93250;
border-color: #B11F36;
}
.bulk-select-checkbox:checked + .bulk-select-box .fa {
display: inline-block;
}
.delete-file-list {
max-height: 160px;
overflow-y: auto;
margin: 10px 0 0;
padding-left: 18px;
li {
font-size: .9em;
margin-bottom: 4px;
word-break: break-word;
}
}
.html-thumbnail {
font-size: 11px;
margin-top: 5px;
@@ -553,12 +681,6 @@
width: 33%;
}
}
input[type='checkbox'] {
display: block;
float: left;
margin-top: 5px;
margin-right: 6px;
}
}
.html-thumbnail, .misc-icon {
margin: 0;
@@ -631,6 +753,72 @@
.files.list-view .list {
@include dashboard-list-view;
}
.files.bulk-selecting .list .file > .overlay,
.files.bulk-selecting .list .html-thumbnail > .overlay {
display: none;
}
.files.list-view .file.bulk-selected {
background: #F8F4ED;
outline: 0;
-webkit-border-radius: 0;
-moz-border-radius: 0;
border-radius: 0;
-webkit-box-shadow: inset 4px 0 0 #77ABB8;
-moz-box-shadow: inset 4px 0 0 #77ABB8;
box-shadow: inset 4px 0 0 #77ABB8;
}
.files.list-view.bulk-selecting .file {
padding-left: 48px;
}
@media (max-device-width:480px), screen and (max-width:800px) {
.files.bulk-selecting .file {
padding-left: 48px;
}
}
.files.list-view.bulk-selecting .bulk-select-control {
left: 20px;
top: 12px;
width: 20px;
height: 20px;
}
@media (max-device-width:480px), screen and (max-width:800px) {
.files.bulk-selecting .bulk-select-control {
left: 20px;
top: 12px;
width: 20px;
height: 20px;
}
}
.files.list-view.bulk-selecting .bulk-select-control input[type='checkbox'],
.files.list-view.bulk-selecting .bulk-select-box {
width: 20px;
height: 20px;
}
.files.list-view.bulk-selecting .bulk-select-box {
line-height: 16px;
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
}
.files.list-view.bulk-selecting .bulk-select-box .fa {
font-size: 11px;
}
@media (max-device-width:480px), screen and (max-width:800px) {
.files.bulk-selecting .bulk-select-control input[type='checkbox'],
.files.bulk-selecting .bulk-select-box {
width: 20px;
height: 20px;
}
.files.bulk-selecting .bulk-select-box {
line-height: 16px;
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
}
.files.bulk-selecting .bulk-select-box .fa {
font-size: 11px;
}
}
.site-actions {
float: left;
margin-top: 20px;
+28
View File
@@ -1,5 +1,6 @@
# frozen_string_literal: true
require_relative './environment.rb'
require 'rack/test'
describe 'dashboard' do
describe 'create directory' do
@@ -63,6 +64,33 @@ describe 'dashboard' do
_(page).must_have_content(/#{Regexp.escape(random)}\.html/)
_(File.exist?(@site.files_path("#{random}.html"))).must_equal true
end
it 'deletes multiple files through the API from the dashboard' do
Capybara.default_driver = :selenium_chrome_headless_largewindow
@site.store_files [
{filename: 'bulk-one.txt', tempfile: Rack::Test::UploadedFile.new('./tests/files/text-file', 'text/plain')},
{filename: 'bulk-two.txt', tempfile: Rack::Test::UploadedFile.new('./tests/files/text-file', 'text/plain')}
]
page.set_rack_session id: @site.id
visit '/dashboard'
click_button 'Select'
find('.bulk-select-control[title="Select bulk-one.txt"]').click
find('.bulk-select-control[title="Select bulk-two.txt"]').click
click_button 'Delete selected'
_(page).must_have_css('#deleteConfirmModal', visible: true)
within '#deleteConfirmModal' do
click_button 'Delete'
end
_(page).must_have_content('2 items have been deleted.')
_(page).wont_have_content('bulk-one.txt')
_(page).wont_have_content('bulk-two.txt')
_(File.exist?(@site.files_path('bulk-one.txt'))).must_equal false
_(File.exist?(@site.files_path('bulk-two.txt'))).must_equal false
end
end
end
end
+44 -2
View File
@@ -236,6 +236,17 @@ describe 'api' do
_(res[:error_type]).must_equal 'missing_filenames'
end
it 'rejects a non-array filenames argument' do
create_site
basic_authorize @user, @pass
@site.store_files [{filename: 'deletable.txt', tempfile: Rack::Test::UploadedFile.new('./tests/files/text-file', 'text/plain')}]
post '/api/delete', filenames: 'deletable.txt'
_(last_response.status).must_equal 400
_(res[:error_type]).must_equal 'bad_filename'
_(site_file_exists?('deletable.txt')).must_equal true
end
it 'fails to delete index.html' do
create_site
basic_authorize @user, @pass
@@ -243,6 +254,23 @@ describe 'api' do
_(res[:error_type]).must_equal 'cannot_delete_index'
end
it 'rejects unsafe delete paths before scrubbing' do
create_site
basic_authorize @user, @pass
@site.store_files [{filename: 'deletable.txt', tempfile: Rack::Test::UploadedFile.new('./tests/files/text-file', 'text/plain')}]
post '/api/delete', filenames: ['../index.html']
_(res[:error_type]).must_equal 'bad_filename'
_(site_file_exists?('index.html')).must_equal true
post '/api/delete', filenames: ['../deletable.txt']
_(res[:error_type]).must_equal 'bad_filename'
_(site_file_exists?('deletable.txt')).must_equal true
post '/api/delete', filenames: ['bad\path.txt']
_(res[:error_type]).must_equal 'bad_filename'
end
it 'succeeds with weird filenames' do
create_site
basic_authorize @user, @pass
@@ -279,10 +307,10 @@ describe 'api' do
basic_authorize @user, @pass
post '/api/delete', filenames: ["../#{@other_site.username}"]
_(File.exist?(@other_site.base_files_path)).must_equal true
_(res[:error_type]).must_equal 'missing_files'
_(res[:error_type]).must_equal 'bad_filename'
post '/api/delete', filenames: ["../#{@other_site.username}/index.html"]
_(File.exist?(@other_site.base_files_path+'/index.html')).must_equal true
_(res[:error_type]).must_equal 'missing_files'
_(res[:error_type]).must_equal 'bad_filename'
end
it 'succeeds with valid filenames' do
@@ -295,6 +323,20 @@ describe 'api' do
_(site_file_exists?('test.jpg')).must_equal false
_(site_file_exists?('test2.jpg')).must_equal false
end
it 'succeeds with valid user session' do
create_site
@site.store_files [{filename: 'test.jpg', tempfile: Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg')}]
@site.store_files [{filename: 'test2.jpg', tempfile: Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg')}]
post '/api/delete',
{filenames: ['test.jpg', 'test2.jpg'], csrf_token: 'abcd'},
{'rack.session' => {'id' => @site.id, '_csrf_token' => 'abcd'}}
_(res[:result]).must_equal 'success'
_(site_file_exists?('test.jpg')).must_equal false
_(site_file_exists?('test2.jpg')).must_equal false
end
end
describe 'create_directory' do
+7 -17
View File
@@ -13,8 +13,8 @@ describe 'site_files' do
post '/api/upload', hash.merge(csrf_token: 'abcd'), {'rack.session' => { 'id' => @site.id, '_csrf_token' => 'abcd' }}
end
def delete_file(hash)
post '/site_files/delete', hash.merge(csrf_token: 'abcd'), {'rack.session' => { 'id' => @site.id, '_csrf_token' => 'abcd' }}
def delete_file(filename)
@site.delete_file filename
end
before do
@@ -250,7 +250,7 @@ describe 'site_files' do
_(@site.actual_space_used).must_equal @site.space_used
file_path = @site.files_path 'test.jpg'
_(File.exists?(file_path)).must_equal true
delete_file filename: 'test.jpg'
delete_file 'test.jpg'
_(File.exists?(file_path)).must_equal false
_(SiteFile[site_id: @site.id, path: 'test.jpg']).must_be_nil
@@ -264,14 +264,14 @@ describe 'site_files' do
it 'property deletes directories with regexp special chars in them' do
upload '8)/test.jpg' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg')
delete_file filename: '8)'
delete_file '8)'
_(@site.reload.site_files.select {|f| f.path =~ /#{Regexp.quote '8)'}/}.length).must_equal 0
end
it 'deletes with escaped apostrophe' do
upload "test'ing/test.jpg" => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg')
_(@site.reload.site_files.select {|s| s.path == "test'ing"}.length).must_equal 1
delete_file filename: "test'ing"
delete_file "test'ing"
_(@site.reload.site_files.select {|s| s.path == "test'ing"}.length).must_equal 0
end
@@ -280,7 +280,7 @@ describe 'site_files' do
upload 'test.jpg' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg')
space_used = @site.reload.space_used
delete_file filename: 'test'
delete_file 'test'
_(@site.reload.space_used).must_equal(space_used - File.size('./tests/files/test.jpg'))
@@ -298,7 +298,7 @@ describe 'site_files' do
_(@site.site_files.select {|f| f.path == path}.length).must_equal 1
end
delete_file filename: 'derp'
delete_file 'derp'
@site.reload
@@ -307,16 +307,6 @@ describe 'site_files' do
end
end
it 'goes back to deleting directory' do
upload 'test/test.jpg' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg')
delete_file filename: 'test/test.jpg'
_(last_response.headers['Location']).must_equal "http://example.org/dashboard?dir=test"
upload 'test.jpg' => Rack::Test::UploadedFile.new('./tests/files/test.jpg', 'image/jpeg')
delete_file filename: 'test.jpg'
_(last_response.headers['Location']).must_equal "http://example.org/dashboard"
end
it 'deletes complex nested directory structure correctly' do
upload 'complex/level1/level2/file1.txt' => Rack::Test::UploadedFile.new('./tests/files/text-file', 'text/plain')
upload 'complex/level1/level2/file2.txt' => Rack::Test::UploadedFile.new('./tests/files/text-file', 'text/plain')
+22 -4
View File
@@ -34,10 +34,20 @@
<% end %>
</div>
<div class="actions">
<button type="button" id="selectFilesButton" class="btn-Action select-files-button" onclick="toggleBulkSelect(event)"><i class="fa fa-check-square-o"></i> Select</button>
<a href="#createFile" class="btn-Action" data-toggle="modal"><i class="fa fa-file"></i> New File</a>
<a href="#createDir" class="btn-Action" data-toggle="modal"><i class="fa fa-folder"></i> New Folder</a>
<a href="#" id="uploadButton" class="btn-Action"><i class="fa fa-arrow-circle-up"></i> Upload</a>
</div>
<div id="bulkActions" class="bulk-actions">
<label class="bulk-select-all">
<input type="checkbox" id="bulkSelectAll" onchange="toggleSelectAllFiles(this.checked)">
<span>Select all</span>
</label>
<strong><span id="selectedFileCount">0</span> selected</strong>
<button type="button" class="btn bulk-clear" onclick="clearFileSelection()">Clear</button>
<button type="button" class="btn-Action btn-danger bulk-delete" id="bulkDeleteButton" onclick="confirmBulkDelete()" disabled><i class="fa fa-trash"></i> Delete selected</button>
</div>
</div>
<div class="list">
<form action="/site_files/upload" class="dropzone" id="uploads">
@@ -46,6 +56,7 @@
<input name="dir" type="hidden" value="<%= @dir %>" id="dir">
<div class="upload-Boundary with-instruction">
<% @file_list.each do |file| %>
<% file_dom_id = Digest::SHA256.hexdigest file[:path] %>
<div class="file filehover">
<% if file[:is_html] && current_site.screenshot_exists?(file[:path], '210x158') %>
<div class="html-thumbnail html fileimagehover">
@@ -69,6 +80,13 @@
</div>
<% end %>
<% if !file[:is_root_index] %>
<label class="bulk-select-control" title="Select <%= file[:path] %>">
<input type="checkbox" class="bulk-select-checkbox" value="<%= file[:path] %>" aria-label="Select <%= file[:path] %>">
<span class="bulk-select-box"><i class="fa fa-check"></i></span>
</label>
<% end %>
<a class="title">
<%= file[:name] %>
</a>
@@ -84,7 +102,7 @@
</div>
<div class="overlay">
<div id="<%= Digest::SHA256.hexdigest file[:path] %>" style="display: none"><%= file[:path] %></div>
<div id="<%= file_dom_id %>" style="display: none"><%= file[:path] %></div>
<% if file[:is_editable] && !file[:is_directory] %>
<a href="/site_files/text_editor?filename=<%= Rack::Utils.escape file[:path] %>"><i class="fa fa-edit" title="Edit"></i> Edit</a>
<% end %>
@@ -92,8 +110,8 @@
<a href="?dir=<%= Rack::Utils.escape file[:path] %>"><i class="fa fa-edit" title="Manage"></i> Manage</a>
<% end %>
<% if !file[:is_root_index] %>
<a href="#" onclick="confirmFileRename($('#<%= Digest::SHA256.hexdigest file[:path] %>').text())"><i class="fa fa-file" title="Rename"></i> Rename</a>
<a href="#" onclick="confirmFileDelete($('#<%= Digest::SHA256.hexdigest file[:path] %>').text())"><i class="fa fa-trash" title="Delete"></i> Delete</a>
<a href="#" onclick="confirmFileRename($('#<%= file_dom_id %>').text())"><i class="fa fa-file" title="Rename"></i> Rename</a>
<a href="#" onclick="confirmFileDelete($('#<%= file_dom_id %>').text())"><i class="fa fa-trash" title="Delete"></i> Delete</a>
<% end %>
<% if file[:is_directory] %>
<a class="link-overlay" href="?dir=<%= Rack::Utils.escape file[:path] %>" title="View <%= file[:path] %>"></a>
@@ -105,4 +123,4 @@
<% end %>
</div>
</form>
</div>
</div>
+4 -5
View File
@@ -75,10 +75,7 @@
<%== erb :'dashboard/files' %>
</div>
<form method="POST" action="/site_files/delete" id="deleteFilenameForm">
<input name="csrf_token" type="hidden" value="<%= csrf_token %>">
<input type="hidden" id="deleteFilenameInput" name="filename">
</form>
<input id="deleteCSRFToken" type="hidden" value="<%= csrf_token %>">
<div class="modal hide" id="deleteConfirmModal" tabindex="-1" role="dialog" aria-labelledby="deleteConfirmModalLabel" aria-hidden="true">
<div class="modal-header">
@@ -86,7 +83,9 @@
<h3 id="deleteConfirmModalLabel">Confirm deletion</h3>
</div>
<div class="modal-body">
<p>You are about to delete <strong><span id="deleteFileName"></span></strong>. Are you sure?</p>
<p id="deleteSingleMessage">You are about to delete <strong><span id="deleteFileName"></span></strong>. Are you sure?</p>
<p id="deleteMultipleMessage" style="display: none">You are about to delete <strong><span id="deleteFileCount">0</span> items</strong>. Are you sure?</p>
<ul id="deleteFileList" class="delete-file-list"></ul>
</div>
<div class="modal-footer">
<button class="btn cancel" data-dismiss="modal" aria-hidden="true" type="button">Cancel</button>