Skip to content
Snippets Groups Projects
Commit 5b0e0869 authored by blackst0ne's avatar blackst0ne
Browse files

Add an ability to cancel attaching file and redesign attaching files UI

parent 20987f4f
No related branches found
No related tags found
No related merge requests found
Loading
Loading
@@ -5,104 +5,154 @@ require('./preview_markdown');
 
window.DropzoneInput = (function() {
function DropzoneInput(form) {
var $mdArea, alertAttr, alertClass, appendToTextArea, btnAlert, child, closeAlertMessage, closeSpinner, divAlert, divHover, divSpinner, dropzone, form_dropzone, form_textarea, getFilename, handlePaste, iconPaperclip, iconSpinner, insertToTextArea, isImage, max_file_size, pasteText, uploads_path, showError, showSpinner, uploadFile, uploadProgress;
var updateAttachingMessage, $attachingFileMessage, $mdArea, $attachButton, $cancelButton, $retryLink, $uploadingErrorContainer, $uploadingErrorMessage, $uploadProgress, $uploadingProgressContainer, appendToTextArea, btnAlert, child, closeAlertMessage, closeSpinner, divHover, divSpinner, dropzone, $formDropzone, formTextarea, getFilename, handlePaste, iconPaperclip, iconSpinner, insertToTextArea, isImage, maxFileSize, pasteText, uploadsPath, showError, showSpinner, uploadFile;
Dropzone.autoDiscover = false;
alertClass = "alert alert-danger alert-dismissable div-dropzone-alert";
alertAttr = "class=\"close\" data-dismiss=\"alert\"" + "aria-hidden=\"true\"";
divHover = "<div class=\"div-dropzone-hover\"></div>";
divSpinner = "<div class=\"div-dropzone-spinner\"></div>";
divAlert = "<div class=\"" + alertClass + "\"></div>";
iconPaperclip = "<i class=\"fa fa-paperclip div-dropzone-icon\"></i>";
iconSpinner = "<i class=\"fa fa-spinner fa-spin div-dropzone-icon\"></i>";
uploadProgress = $("<div class=\"div-dropzone-progress\"></div>");
btnAlert = "<button type=\"button\"" + alertAttr + ">&times;</button>";
uploads_path = window.uploads_path || null;
max_file_size = gon.max_file_size || 10;
form_textarea = $(form).find(".js-gfm-input");
form_textarea.wrap("<div class=\"div-dropzone\"></div>");
form_textarea.on('paste', (function(_this) {
divHover = '<div class="div-dropzone-hover"></div>';
iconPaperclip = '<i class="fa fa-paperclip div-dropzone-icon"></i>';
$attachButton = form.find('.button-attach-file');
$attachingFileMessage = form.find('.attaching-file-message');
$cancelButton = form.find('.button-cancel-uploading-files');
$retryLink = form.find('.retry-uploading-link');
$uploadProgress = form.find('.uploading-progress');
$uploadingErrorContainer = form.find('.uploading-error-container');
$uploadingErrorMessage = form.find('.uploading-error-message');
$uploadingProgressContainer = form.find('.uploading-progress-container');
uploadsPath = window.uploads_path || null;
maxFileSize = gon.max_file_size || 10;
formTextarea = form.find('.js-gfm-input');
formTextarea.wrap('<div class="div-dropzone"></div>');
formTextarea.on('paste', (function(_this) {
return function(event) {
return handlePaste(event);
};
})(this));
$mdArea = $(form_textarea).closest('.md-area');
$(form).setupMarkdownPreview();
form_dropzone = $(form).find('.div-dropzone');
form_dropzone.parent().addClass("div-dropzone-wrapper");
form_dropzone.append(divHover);
form_dropzone.find(".div-dropzone-hover").append(iconPaperclip);
form_dropzone.append(divSpinner);
form_dropzone.find(".div-dropzone-spinner").append(iconSpinner);
form_dropzone.find(".div-dropzone-spinner").append(uploadProgress);
form_dropzone.find(".div-dropzone-spinner").css({
"opacity": 0,
"display": "none"
});
 
if (!uploads_path) return;
// Add dropzone area to the form.
$mdArea = formTextarea.closest('.md-area');
form.setupMarkdownPreview();
$formDropzone = form.find('.div-dropzone');
$formDropzone.parent().addClass('div-dropzone-wrapper');
$formDropzone.append(divHover);
$formDropzone.find('.div-dropzone-hover').append(iconPaperclip);
if (!uploadsPath) return;
 
dropzone = form_dropzone.dropzone({
url: uploads_path,
dictDefaultMessage: "",
dropzone = $formDropzone.dropzone({
url: uploadsPath,
dictDefaultMessage: '',
clickable: true,
paramName: "file",
maxFilesize: max_file_size,
paramName: 'file',
maxFilesize: maxFileSize,
uploadMultiple: false,
headers: {
"X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content")
'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content')
},
previewContainer: false,
processing: function() {
return $(".div-dropzone-alert").alert("close");
return $('.div-dropzone-alert').alert('close');
},
dragover: function() {
$mdArea.addClass('is-dropzone-hover');
form.find(".div-dropzone-hover").css("opacity", 0.7);
form.find('.div-dropzone-hover').css('opacity', 0.7);
},
dragleave: function() {
$mdArea.removeClass('is-dropzone-hover');
form.find(".div-dropzone-hover").css("opacity", 0);
form.find('.div-dropzone-hover').css('opacity', 0);
},
drop: function() {
$mdArea.removeClass('is-dropzone-hover');
form.find(".div-dropzone-hover").css("opacity", 0);
form_textarea.focus();
form.find('.div-dropzone-hover').css('opacity', 0);
formTextarea.focus();
},
success: function(header, response) {
const processingFileCount = this.getQueuedFiles().length + this.getUploadingFiles().length;
const shouldPad = processingFileCount >= 1;
 
pasteText(response.link.markdown, shouldPad);
// Show 'Attach a file' link only when all files have been uploaded.
if (!processingFileCount) $attachButton.removeClass('hide');
},
error: function(temp) {
var checkIfMsgExists, errorAlert;
errorAlert = $(form).find('.error-alert');
checkIfMsgExists = errorAlert.children().length;
if (checkIfMsgExists === 0) {
errorAlert.append(divAlert);
$(".div-dropzone-alert").append(btnAlert + "Attaching the file failed.");
}
error: function(file, errorMessage = 'Attaching the file failed.', xhr) {
// If 'error' event is fired by dropzone, the second parameter is error message.
// If the 'errorMessage' parameter is empty, the default error message is set.
// If the 'error' event is fired by backend (xhr) error response, the third parameter is
// xhr object (xhr.responseText is error message).
// On error we hide the 'Attach' and 'Cancel' buttons
// and show an error.
// If there's xhr error message, let's show it instead of dropzone's one.
const message = xhr ? xhr.responseText : errorMessage;
$uploadingErrorContainer.removeClass('hide');
$uploadingErrorMessage.html(message);
$attachButton.addClass('hide');
$cancelButton.addClass('hide');
},
totaluploadprogress: function(totalUploadProgress) {
uploadProgress.text(Math.round(totalUploadProgress) + "%");
updateAttachingMessage(this.files, $attachingFileMessage);
$uploadProgress.text(Math.round(totalUploadProgress) + '%');
},
sending: function(file) {
// DOM elements already exist.
// Instead of dynamically generating them,
// we just either hide or show them.
$attachButton.addClass('hide');
$uploadingErrorContainer.addClass('hide');
$uploadingProgressContainer.removeClass('hide');
$cancelButton.removeClass('hide');
},
sending: function() {
form_dropzone.find(".div-dropzone-spinner").css({
"opacity": 0.7,
"display": "inherit"
});
removedfile: function() {
$attachButton.removeClass('hide');
$cancelButton.addClass('hide');
$uploadingProgressContainer.addClass('hide');
$uploadingErrorContainer.addClass('hide');
},
queuecomplete: function() {
uploadProgress.text("");
$(".dz-preview").remove();
$(".markdown-area").trigger("input");
$(".div-dropzone-spinner").css({
"opacity": 0,
"display": "none"
});
$('.dz-preview').remove();
$('.markdown-area').trigger('input');
$uploadingProgressContainer.addClass('hide');
$cancelButton.addClass('hide');
}
});
child = $(dropzone[0]).children("textarea");
child = $(dropzone[0]).children('textarea');
// removeAllFiles(true) stops uploading files (if any)
// and remove them from dropzone files queue.
$cancelButton.on('click', (e) => {
const target = e.target.closest('form').querySelector('.div-dropzone');
e.preventDefault();
e.stopPropagation();
Dropzone.forElement(target).removeAllFiles(true);
});
// If 'error' event is fired, we store a failed files,
// clear dropzone files queue, change status of failed files to undefined,
// and add that files to the dropzone files queue again.
// addFile() adds file to dropzone files queue and upload it.
$retryLink.on('click', (e) => {
const dropzoneInstance = Dropzone.forElement(e.target.closest('form').querySelector('.div-dropzone'));
const failedFiles = dropzoneInstance.files;
e.preventDefault();
// 'true' parameter of removeAllFiles() cancels uploading of files that are being uploaded at the moment.
dropzoneInstance.removeAllFiles(true);
failedFiles.map((failedFile, i) => {
const file = failedFile;
if (file.status === Dropzone.ERROR) {
file.status = undefined;
file.accepted = undefined;
}
return dropzoneInstance.addFile(file);
});
});
handlePaste = function(event) {
var filename, image, pasteEvent, text;
pasteEvent = event.originalEvent;
Loading
Loading
@@ -110,25 +160,27 @@ window.DropzoneInput = (function() {
image = isImage(pasteEvent);
if (image) {
event.preventDefault();
filename = getFilename(pasteEvent) || "image.png";
text = "{{" + filename + "}}";
filename = getFilename(pasteEvent) || 'image.png';
text = `{{${filename}}}`;
pasteText(text);
return uploadFile(image.getAsFile(), filename);
}
}
};
isImage = function(data) {
var i, item;
i = 0;
while (i < data.clipboardData.items.length) {
item = data.clipboardData.items[i];
if (item.type.indexOf("image") !== -1) {
if (item.type.indexOf('image') !== -1) {
return item;
}
i += 1;
}
return false;
};
pasteText = function(text, shouldPad) {
var afterSelection, beforeSelection, caretEnd, caretStart, textEnd;
var formattedText = text;
Loading
Loading
@@ -142,31 +194,33 @@ window.DropzoneInput = (function() {
$(child).val(beforeSelection + formattedText + afterSelection);
textarea.setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length);
textarea.style.height = `${textarea.scrollHeight}px`;
return form_textarea.trigger("input");
return formTextarea.trigger('input');
};
getFilename = function(e) {
var value;
if (window.clipboardData && window.clipboardData.getData) {
value = window.clipboardData.getData("Text");
value = window.clipboardData.getData('Text');
} else if (e.clipboardData && e.clipboardData.getData) {
value = e.clipboardData.getData("text/plain");
value = e.clipboardData.getData('text/plain');
}
value = value.split("\r");
return value.first();
};
uploadFile = function(item, filename) {
var formData;
formData = new FormData();
formData.append("file", item, filename);
formData.append('file', item, filename);
return $.ajax({
url: uploads_path,
type: "POST",
url: uploadsPath,
type: 'POST',
data: formData,
dataType: "json",
dataType: 'json',
processData: false,
contentType: false,
headers: {
"X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content")
'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content')
},
beforeSend: function() {
showSpinner();
Loading
Loading
@@ -183,44 +237,54 @@ window.DropzoneInput = (function() {
}
});
};
updateAttachingMessage = (files, messageContainer) => {
let attachingMessage;
const filesCount = files.filter(function(file) {
return file.status === 'uploading' ||
file.status === 'queued';
}).length;
// Dinamycally change uploading files text depending on files number in
// dropzone files queue.
if (filesCount > 1) {
attachingMessage = 'Attaching ' + filesCount + ' files -';
} else {
attachingMessage = 'Attaching a file -';
}
messageContainer.text(attachingMessage);
};
insertToTextArea = function(filename, url) {
return $(child).val(function(index, val) {
return val.replace("{{" + filename + "}}", url);
return val.replace(`{{${filename}}}`, url);
});
};
appendToTextArea = function(url) {
return $(child).val(function(index, val) {
return val + url + "\n";
});
};
showSpinner = function(e) {
return form.find(".div-dropzone-spinner").css({
"opacity": 0.7,
"display": "inherit"
});
return $uploadingProgressContainer.removeClass('hide');
};
closeSpinner = function() {
return form.find(".div-dropzone-spinner").css({
"opacity": 0,
"display": "none"
});
return $uploadingProgressContainer.addClass('hide');
};
showError = function(message) {
var checkIfMsgExists, errorAlert;
errorAlert = $(form).find('.error-alert');
checkIfMsgExists = errorAlert.children().length;
if (checkIfMsgExists === 0) {
errorAlert.append(divAlert);
return $(".div-dropzone-alert").append(btnAlert + message);
}
$uploadingErrorContainer.removeClass('hide');
$uploadingErrorMessage.html(message);
};
closeAlertMessage = function() {
return form.find(".div-dropzone-alert").alert("close");
};
form.find(".markdown-selector").click(function(e) {
form.find('.markdown-selector').click(function(e) {
e.preventDefault();
$(this).closest('.gfm-form').find('.div-dropzone').click();
form_textarea.focus();
formTextarea.focus();
});
}
 
Loading
Loading
Loading
Loading
@@ -277,6 +277,7 @@
.toolbar-text {
font-size: 14px;
line-height: 16px;
margin-top: 2px;
 
@media (min-width: $screen-md-min) {
float: left;
Loading
Loading
@@ -402,3 +403,45 @@
}
}
}
.uploading-container {
float: right;
@media (max-width: $screen-xs-max) {
float: left;
margin-top: 5px;
}
}
.uploading-error-icon,
.uploading-error-message {
color: $gl-text-red;
}
.uploading-error-message {
@media (max-width: $screen-xs-max) {
&::after {
content: "\a";
white-space: pre;
}
}
}
.uploading-progress {
margin-right: 5px;
}
.attach-new-file,
.button-attach-file,
.retry-uploading-link {
color: $gl-link-color;
padding: 0;
background: none;
border: 0;
font-size: 14px;
line-height: 16px;
}
.markdown-selector {
color: $gl-link-color;
}
Loading
Loading
@@ -7,9 +7,10 @@ module IconsHelper
# font-awesome-rails gem, but should we ever use a different icon pack in the
# future we won't have to change hundreds of method calls.
def icon(names, options = {})
if (options.keys & %w[aria-hidden aria-label]).empty?
# Add `aria-hidden` if there are no aria's set
if (options.keys & %w[aria-hidden aria-label data-hidden]).empty?
# Add 'aria-hidden' and 'data-hidden' if they are not set in options.
options['aria-hidden'] = true
options['data-hidden'] = true
end
 
options.include?(:base) ? fa_stacked_icon(names, options) : fa_icon(names, options)
Loading
Loading
Loading
Loading
@@ -9,6 +9,27 @@
- else
is
supported
%button.toolbar-button.markdown-selector{ type: 'button', tabindex: '-1' }
= icon('file-image-o', class: 'toolbar-button-icon')
Attach a file
%span.uploading-container
%span.uploading-progress-container.hide
= icon('file-image-o', class: 'toolbar-button-icon')
%span.attaching-file-message
-# Populated by app/assets/javascripts/dropzone_input.js
%span.uploading-progress 0%
%span.uploading-spinner
= icon('spinner spin', class: 'toolbar-button-icon')
%span.uploading-error-container.hide
%span.uploading-error-icon
= icon('file-image-o', class: 'toolbar-button-icon')
%span.uploading-error-message
-# Populated by app/assets/javascripts/dropzone_input.js
%button.retry-uploading-link{ type: 'button' } Try again
or
%button.attach-new-file.markdown-selector{ type: 'button' } attach a new file
%button.markdown-selector.button-attach-file{ type: 'button', tabindex: '-1' }
= icon('file-image-o', class: 'toolbar-button-icon')
Attach a file
%button.btn.btn-default.btn-xs.hide.button-cancel-uploading-files{ type: 'button' } Cancel
---
title: Add an ability to cancel attaching file and redesign attaching files UI
merge_request: 9431
author: blackst0ne
Loading
Loading
@@ -5,18 +5,78 @@ feature 'User uploads file to note', feature: true do
 
let(:user) { create(:user) }
let(:project) { create(:empty_project, creator: user, namespace: user.namespace) }
let(:issue) { create(:issue, project: project, author: user) }
 
scenario 'they see the attached file', js: true do
issue = create(:issue, project: project, author: user)
before do
login_as(user)
visit namespace_project_issue_path(project.namespace, project, issue)
end
context 'before uploading' do
it 'shows "Attach a file" button', js: true do
expect(page).to have_button('Attach a file')
expect(page).not_to have_selector('.uploading-progress-container', visible: true)
end
end
context 'uploading is in progress' do
it 'shows "Cancel" button on uploading', js: true do
dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
expect(page).to have_button('Cancel')
end
it 'cancels uploading on clicking to "Cancel" button', js: true do
dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
click_button 'Cancel'
expect(page).to have_button('Attach a file')
expect(page).not_to have_button('Cancel')
expect(page).not_to have_selector('.uploading-progress-container', visible: true)
end
it 'shows "Attaching a file" message on uploading 1 file', js: true do
dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
expect(page).to have_selector('.attaching-file-message', visible: true, text: 'Attaching a file -')
end
it 'shows "Attaching 2 files" message on uploading 2 file', js: true do
dropzone_file([Rails.root.join('spec', 'fixtures', 'video_sample.mp4'),
Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
expect(page).to have_selector('.attaching-file-message', visible: true, text: 'Attaching 2 files -')
end
it 'shows error message, "retry" and "attach a new file" link a if file is too big', js: true do
dropzone_file([Rails.root.join('spec', 'fixtures', 'video_sample.mp4')], 0.01)
error_text = 'File is too big (0.06MiB). Max filesize: 0.01MiB.'
expect(page).to have_selector('.uploading-error-message', visible: true, text: error_text)
expect(page).to have_selector('.retry-uploading-link', visible: true, text: 'Try again')
expect(page).to have_selector('.attach-new-file', visible: true, text: 'attach a new file')
expect(page).not_to have_button('Attach a file')
end
end
context 'uploading is complete' do
it 'shows "Attach a file" button on uploading complete', js: true do
dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')])
wait_for_ajax
expect(page).to have_button('Attach a file')
expect(page).not_to have_selector('.uploading-progress-container', visible: true)
end
 
dropzone_file(Rails.root.join('spec', 'fixtures', 'dk.png'))
click_button 'Comment'
wait_for_ajax
scenario 'they see the attached file', js: true do
dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')])
click_button 'Comment'
wait_for_ajax
 
expect(find('a.no-attachment-icon img[alt="dk"]')['src'])
.to match(%r{/#{project.full_path}/uploads/\h{32}/dk\.png$})
expect(find('a.no-attachment-icon img[alt="dk"]')['src'])
.to match(%r{/#{project.full_path}/uploads/\h{32}/dk\.png$})
end
end
end
Loading
Loading
@@ -6,32 +6,52 @@ module DropzoneHelper
# Dropzone events to perform the actual upload.
#
# This method waits for the upload to complete before returning.
def dropzone_file(file_path)
# max_file_size is an optional parameter.
# If it's not 0, then it used in dropzone.maxFilesize parameter.
# wait_for_queuecomplete is an optional parameter.
# If it's 'false', then the helper will NOT wait for backend response
# It lets to test behaviors while AJAX is processing.
def dropzone_file(files, max_file_size = 0, wait_for_queuecomplete = true)
# Generate a fake file input that Capybara can attach to
page.execute_script <<-JS.strip_heredoc
$('#fakeFileInput').remove();
var fakeFileInput = window.$('<input/>').attr(
{id: 'fakeFileInput', type: 'file'}
{id: 'fakeFileInput', type: 'file', multiple: true}
).appendTo('body');
 
window._dropzoneComplete = false;
JS
 
# Attach the file to the fake input selector with Capybara
attach_file('fakeFileInput', file_path)
# Attach files to the fake input selector with Capybara
attach_file('fakeFileInput', files)
 
# Manually trigger a Dropzone "drop" event with the fake input's file list
page.execute_script <<-JS.strip_heredoc
var fileList = [$('#fakeFileInput')[0].files[0]];
var e = jQuery.Event('drop', { dataTransfer : { files : fileList } });
var dropzone = $('.div-dropzone')[0].dropzone;
dropzone.options.autoProcessQueue = false;
if (#{max_file_size} > 0) {
dropzone.options.maxFilesize = #{max_file_size};
}
dropzone.on('queuecomplete', function() {
window._dropzoneComplete = true;
});
dropzone.listeners[0].events.drop(e);
var fileList = [$('#fakeFileInput')[0].files];
$.map(fileList, function(file){
var e = jQuery.Event('drop', { dataTransfer : { files : file } });
dropzone.listeners[0].events.drop(e);
});
dropzone.processQueue();
JS
 
# Wait until Dropzone's fired `queuecomplete`
loop until page.evaluate_script('window._dropzoneComplete === true')
if wait_for_queuecomplete
# Wait until Dropzone's fired `queuecomplete`
loop until page.evaluate_script('window._dropzoneComplete === true')
end
end
end
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment