Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
Léo-Paul Géneau
gitlab-ce
Commits
5b0e0869
Commit
5b0e0869
authored
May 16, 2017
by
blackst0ne
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add an ability to cancel attaching file and redesign attaching files UI
parent
20987f4f
Changes
7
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
330 additions
and
117 deletions
+330
-117
app/assets/javascripts/dropzone_input.js
app/assets/javascripts/dropzone_input.js
+158
-94
app/assets/stylesheets/pages/note_form.scss
app/assets/stylesheets/pages/note_form.scss
+43
-0
app/helpers/icons_helper.rb
app/helpers/icons_helper.rb
+3
-2
app/views/shared/notes/_hints.html.haml
app/views/shared/notes/_hints.html.haml
+24
-3
changelogs/unreleased/add_ability_to_cancel_attaching_file_and_redesign_attaching_files_ui.yml
...cancel_attaching_file_and_redesign_attaching_files_ui.yml
+4
-0
spec/features/uploads/user_uploads_file_to_note_spec.rb
spec/features/uploads/user_uploads_file_to_note_spec.rb
+68
-8
spec/support/dropzone_helper.rb
spec/support/dropzone_helper.rb
+30
-10
No files found.
app/assets/javascripts/dropzone_input.js
View file @
5b0e0869
...
@@ -5,104 +5,154 @@ require('./preview_markdown');
...
@@ -5,104 +5,154 @@ require('./preview_markdown');
window
.
DropzoneInput
=
(
function
()
{
window
.
DropzoneInput
=
(
function
()
{
function
DropzoneInput
(
form
)
{
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
;
Dropzone
.
autoDiscover
=
false
;
alertClass
=
"
alert alert-danger alert-dismissable div-dropzone-alert
"
;
divHover
=
'
<div class="div-dropzone-hover"></div>
'
;
alertAttr
=
"
class=
\"
close
\"
data-dismiss=
\"
alert
\"
"
+
"
aria-hidden=
\"
true
\"
"
;
iconPaperclip
=
'
<i class="fa fa-paperclip div-dropzone-icon"></i>
'
;
divHover
=
"
<div class=
\"
div-dropzone-hover
\"
></div>
"
;
$attachButton
=
form
.
find
(
'
.button-attach-file
'
);
divSpinner
=
"
<div class=
\"
div-dropzone-spinner
\"
></div>
"
;
$attachingFileMessage
=
form
.
find
(
'
.attaching-file-message
'
);
divAlert
=
"
<div class=
\"
"
+
alertClass
+
"
\"
></div>
"
;
$cancelButton
=
form
.
find
(
'
.button-cancel-uploading-files
'
);
iconPaperclip
=
"
<i class=
\"
fa fa-paperclip div-dropzone-icon
\"
></i>
"
;
$retryLink
=
form
.
find
(
'
.retry-uploading-link
'
);
iconSpinner
=
"
<i class=
\"
fa fa-spinner fa-spin div-dropzone-icon
\"
></i>
"
;
$uploadProgress
=
form
.
find
(
'
.uploading-progress
'
);
uploadProgress
=
$
(
"
<div class=
\"
div-dropzone-progress
\"
></div>
"
);
$uploadingErrorContainer
=
form
.
find
(
'
.uploading-error-container
'
);
btnAlert
=
"
<button type=
\"
button
\"
"
+
alertAttr
+
"
>×</button>
"
;
$uploadingErrorMessage
=
form
.
find
(
'
.uploading-error-message
'
);
uploads_path
=
window
.
uploads_path
||
null
;
$uploadingProgressContainer
=
form
.
find
(
'
.uploading-progress-container
'
);
max_file_size
=
gon
.
max_file_size
||
10
;
uploadsPath
=
window
.
uploads_path
||
null
;
form_textarea
=
$
(
form
).
find
(
"
.js-gfm-input
"
);
maxFileSize
=
gon
.
max_file_size
||
10
;
form_textarea
.
wrap
(
"
<div class=
\"
div-dropzone
\"
></div>
"
);
formTextarea
=
form
.
find
(
'
.js-gfm-input
'
);
form_textarea
.
on
(
'
paste
'
,
(
function
(
_this
)
{
formTextarea
.
wrap
(
'
<div class="div-dropzone"></div>
'
);
formTextarea
.
on
(
'
paste
'
,
(
function
(
_this
)
{
return
function
(
event
)
{
return
function
(
event
)
{
return
handlePaste
(
event
);
return
handlePaste
(
event
);
};
};
})(
this
));
})(
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_d
ropzone
.
dropzone
({
dropzone
=
$formD
ropzone
.
dropzone
({
url
:
uploads
_p
ath
,
url
:
uploads
P
ath
,
dictDefaultMessage
:
""
,
dictDefaultMessage
:
''
,
clickable
:
true
,
clickable
:
true
,
paramName
:
"
file
"
,
paramName
:
'
file
'
,
maxFilesize
:
max
_file_s
ize
,
maxFilesize
:
max
FileS
ize
,
uploadMultiple
:
false
,
uploadMultiple
:
false
,
headers
:
{
headers
:
{
"
X-CSRF-Token
"
:
$
(
"
meta[name=
\"
csrf-token
\"
]
"
).
attr
(
"
content
"
)
'
X-CSRF-Token
'
:
$
(
'
meta[name="csrf-token"]
'
).
attr
(
'
content
'
)
},
},
previewContainer
:
false
,
previewContainer
:
false
,
processing
:
function
()
{
processing
:
function
()
{
return
$
(
"
.div-dropzone-alert
"
).
alert
(
"
close
"
);
return
$
(
'
.div-dropzone-alert
'
).
alert
(
'
close
'
);
},
},
dragover
:
function
()
{
dragover
:
function
()
{
$mdArea
.
addClass
(
'
is-dropzone-hover
'
);
$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
()
{
dragleave
:
function
()
{
$mdArea
.
removeClass
(
'
is-dropzone-hover
'
);
$mdArea
.
removeClass
(
'
is-dropzone-hover
'
);
form
.
find
(
"
.div-dropzone-hover
"
).
css
(
"
opacity
"
,
0
);
form
.
find
(
'
.div-dropzone-hover
'
).
css
(
'
opacity
'
,
0
);
},
},
drop
:
function
()
{
drop
:
function
()
{
$mdArea
.
removeClass
(
'
is-dropzone-hover
'
);
$mdArea
.
removeClass
(
'
is-dropzone-hover
'
);
form
.
find
(
"
.div-dropzone-hover
"
).
css
(
"
opacity
"
,
0
);
form
.
find
(
'
.div-dropzone-hover
'
).
css
(
'
opacity
'
,
0
);
form
_t
extarea
.
focus
();
form
T
extarea
.
focus
();
},
},
success
:
function
(
header
,
response
)
{
success
:
function
(
header
,
response
)
{
const
processingFileCount
=
this
.
getQueuedFiles
().
length
+
this
.
getUploadingFiles
().
length
;
const
processingFileCount
=
this
.
getQueuedFiles
().
length
+
this
.
getUploadingFiles
().
length
;
const
shouldPad
=
processingFileCount
>=
1
;
const
shouldPad
=
processingFileCount
>=
1
;
pasteText
(
response
.
link
.
markdown
,
shouldPad
);
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
)
{
error
:
function
(
file
,
errorMessage
=
'
Attaching the file failed.
'
,
xhr
)
{
var
checkIfMsgExists
,
errorAlert
;
// If 'error' event is fired by dropzone, the second parameter is error message.
errorAlert
=
$
(
form
).
find
(
'
.error-alert
'
);
// If the 'errorMessage' parameter is empty, the default error message is set.
checkIfMsgExists
=
errorAlert
.
children
().
length
;
// If the 'error' event is fired by backend (xhr) error response, the third parameter is
if
(
checkIfMsgExists
===
0
)
{
// xhr object (xhr.responseText is error message).
errorAlert
.
append
(
divAlert
);
// On error we hide the 'Attach' and 'Cancel' buttons
$
(
"
.div-dropzone-alert
"
).
append
(
btnAlert
+
"
Attaching the file failed.
"
);
// 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
)
{
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
()
{
removedfile
:
function
()
{
form_dropzone
.
find
(
"
.div-dropzone-spinner
"
).
css
({
$attachButton
.
removeClass
(
'
hide
'
);
"
opacity
"
:
0.7
,
$cancelButton
.
addClass
(
'
hide
'
);
"
display
"
:
"
inherit
"
$uploadingProgressContainer
.
addClass
(
'
hide
'
);
}
);
$uploadingErrorContainer
.
addClass
(
'
hide
'
);
},
},
queuecomplete
:
function
()
{
queuecomplete
:
function
()
{
uploadProgress
.
text
(
""
);
$
(
'
.dz-preview
'
).
remove
();
$
(
"
.dz-preview
"
).
remove
();
$
(
'
.markdown-area
'
).
trigger
(
'
input
'
);
$
(
"
.markdown-area
"
).
trigger
(
"
input
"
);
$
(
"
.div-dropzone-spinner
"
).
css
({
$uploadingProgressContainer
.
addClass
(
'
hide
'
);
"
opacity
"
:
0
,
$cancelButton
.
addClass
(
'
hide
'
);
"
display
"
:
"
none
"
});
}
}
});
});
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
)
{
handlePaste
=
function
(
event
)
{
var
filename
,
image
,
pasteEvent
,
text
;
var
filename
,
image
,
pasteEvent
,
text
;
pasteEvent
=
event
.
originalEvent
;
pasteEvent
=
event
.
originalEvent
;
...
@@ -110,25 +160,27 @@ window.DropzoneInput = (function() {
...
@@ -110,25 +160,27 @@ window.DropzoneInput = (function() {
image
=
isImage
(
pasteEvent
);
image
=
isImage
(
pasteEvent
);
if
(
image
)
{
if
(
image
)
{
event
.
preventDefault
();
event
.
preventDefault
();
filename
=
getFilename
(
pasteEvent
)
||
"
image.png
"
;
filename
=
getFilename
(
pasteEvent
)
||
'
image.png
'
;
text
=
"
{{
"
+
filename
+
"
}}
"
;
text
=
`{{
${
filename
}
}}`
;
pasteText
(
text
);
pasteText
(
text
);
return
uploadFile
(
image
.
getAsFile
(),
filename
);
return
uploadFile
(
image
.
getAsFile
(),
filename
);
}
}
}
}
};
};
isImage
=
function
(
data
)
{
isImage
=
function
(
data
)
{
var
i
,
item
;
var
i
,
item
;
i
=
0
;
i
=
0
;
while
(
i
<
data
.
clipboardData
.
items
.
length
)
{
while
(
i
<
data
.
clipboardData
.
items
.
length
)
{
item
=
data
.
clipboardData
.
items
[
i
];
item
=
data
.
clipboardData
.
items
[
i
];
if
(
item
.
type
.
indexOf
(
"
image
"
)
!==
-
1
)
{
if
(
item
.
type
.
indexOf
(
'
image
'
)
!==
-
1
)
{
return
item
;
return
item
;
}
}
i
+=
1
;
i
+=
1
;
}
}
return
false
;
return
false
;
};
};
pasteText
=
function
(
text
,
shouldPad
)
{
pasteText
=
function
(
text
,
shouldPad
)
{
var
afterSelection
,
beforeSelection
,
caretEnd
,
caretStart
,
textEnd
;
var
afterSelection
,
beforeSelection
,
caretEnd
,
caretStart
,
textEnd
;
var
formattedText
=
text
;
var
formattedText
=
text
;
...
@@ -142,31 +194,33 @@ window.DropzoneInput = (function() {
...
@@ -142,31 +194,33 @@ window.DropzoneInput = (function() {
$
(
child
).
val
(
beforeSelection
+
formattedText
+
afterSelection
);
$
(
child
).
val
(
beforeSelection
+
formattedText
+
afterSelection
);
textarea
.
setSelectionRange
(
caretStart
+
formattedText
.
length
,
caretEnd
+
formattedText
.
length
);
textarea
.
setSelectionRange
(
caretStart
+
formattedText
.
length
,
caretEnd
+
formattedText
.
length
);
textarea
.
style
.
height
=
`
${
textarea
.
scrollHeight
}
px`
;
textarea
.
style
.
height
=
`
${
textarea
.
scrollHeight
}
px`
;
return
form
_textarea
.
trigger
(
"
input
"
);
return
form
Textarea
.
trigger
(
'
input
'
);
};
};
getFilename
=
function
(
e
)
{
getFilename
=
function
(
e
)
{
var
value
;
var
value
;
if
(
window
.
clipboardData
&&
window
.
clipboardData
.
getData
)
{
if
(
window
.
clipboardData
&&
window
.
clipboardData
.
getData
)
{
value
=
window
.
clipboardData
.
getData
(
"
Text
"
);
value
=
window
.
clipboardData
.
getData
(
'
Text
'
);
}
else
if
(
e
.
clipboardData
&&
e
.
clipboardData
.
getData
)
{
}
else
if
(
e
.
clipboardData
&&
e
.
clipboardData
.
getData
)
{
value
=
e
.
clipboardData
.
getData
(
"
text/plain
"
);
value
=
e
.
clipboardData
.
getData
(
'
text/plain
'
);
}
}
value
=
value
.
split
(
"
\r
"
);
value
=
value
.
split
(
"
\r
"
);
return
value
.
first
();
return
value
.
first
();
};
};
uploadFile
=
function
(
item
,
filename
)
{
uploadFile
=
function
(
item
,
filename
)
{
var
formData
;
var
formData
;
formData
=
new
FormData
();
formData
=
new
FormData
();
formData
.
append
(
"
file
"
,
item
,
filename
);
formData
.
append
(
'
file
'
,
item
,
filename
);
return
$
.
ajax
({
return
$
.
ajax
({
url
:
uploads
_p
ath
,
url
:
uploads
P
ath
,
type
:
"
POST
"
,
type
:
'
POST
'
,
data
:
formData
,
data
:
formData
,
dataType
:
"
json
"
,
dataType
:
'
json
'
,
processData
:
false
,
processData
:
false
,
contentType
:
false
,
contentType
:
false
,
headers
:
{
headers
:
{
"
X-CSRF-Token
"
:
$
(
"
meta[name=
\"
csrf-token
\"
]
"
).
attr
(
"
content
"
)
'
X-CSRF-Token
'
:
$
(
'
meta[name="csrf-token"]
'
).
attr
(
'
content
'
)
},
},
beforeSend
:
function
()
{
beforeSend
:
function
()
{
showSpinner
();
showSpinner
();
...
@@ -183,44 +237,54 @@ window.DropzoneInput = (function() {
...
@@ -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
)
{
insertToTextArea
=
function
(
filename
,
url
)
{
return
$
(
child
).
val
(
function
(
index
,
val
)
{
return
$
(
child
).
val
(
function
(
index
,
val
)
{
return
val
.
replace
(
"
{{
"
+
filename
+
"
}}
"
,
url
);
return
val
.
replace
(
`{{
${
filename
}
}}`
,
url
);
});
});
};
};
appendToTextArea
=
function
(
url
)
{
appendToTextArea
=
function
(
url
)
{
return
$
(
child
).
val
(
function
(
index
,
val
)
{
return
$
(
child
).
val
(
function
(
index
,
val
)
{
return
val
+
url
+
"
\n
"
;
return
val
+
url
+
"
\n
"
;
});
});
};
};
showSpinner
=
function
(
e
)
{
showSpinner
=
function
(
e
)
{
return
form
.
find
(
"
.div-dropzone-spinner
"
).
css
({
return
$uploadingProgressContainer
.
removeClass
(
'
hide
'
);
"
opacity
"
:
0.7
,
"
display
"
:
"
inherit
"
});
};
};
closeSpinner
=
function
()
{
closeSpinner
=
function
()
{
return
form
.
find
(
"
.div-dropzone-spinner
"
).
css
({
return
$uploadingProgressContainer
.
addClass
(
'
hide
'
);
"
opacity
"
:
0
,
"
display
"
:
"
none
"
});
};
};
showError
=
function
(
message
)
{
showError
=
function
(
message
)
{
var
checkIfMsgExists
,
errorAlert
;
$uploadingErrorContainer
.
removeClass
(
'
hide
'
);
errorAlert
=
$
(
form
).
find
(
'
.error-alert
'
);
$uploadingErrorMessage
.
html
(
message
);
checkIfMsgExists
=
errorAlert
.
children
().
length
;
if
(
checkIfMsgExists
===
0
)
{
errorAlert
.
append
(
divAlert
);
return
$
(
"
.div-dropzone-alert
"
).
append
(
btnAlert
+
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
();
e
.
preventDefault
();
$
(
this
).
closest
(
'
.gfm-form
'
).
find
(
'
.div-dropzone
'
).
click
();
$
(
this
).
closest
(
'
.gfm-form
'
).
find
(
'
.div-dropzone
'
).
click
();
form
_t
extarea
.
focus
();
form
T
extarea
.
focus
();
});
});
}
}
...
...
app/assets/stylesheets/pages/note_form.scss
View file @
5b0e0869
...
@@ -277,6 +277,7 @@
...
@@ -277,6 +277,7 @@
.toolbar-text
{
.toolbar-text
{
font-size
:
14px
;
font-size
:
14px
;
line-height
:
16px
;
line-height
:
16px
;
margin-top
:
2px
;
@media
(
min-width
:
$screen-md-min
)
{
@media
(
min-width
:
$screen-md-min
)
{
float
:
left
;
float
:
left
;
...
@@ -402,3 +403,45 @@
...
@@ -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
;
}
app/helpers/icons_helper.rb
View file @
5b0e0869
...
@@ -7,9 +7,10 @@ module IconsHelper
...
@@ -7,9 +7,10 @@ module IconsHelper
# font-awesome-rails gem, but should we ever use a different icon pack in the
# 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.
# future we won't have to change hundreds of method calls.
def
icon
(
names
,
options
=
{})
def
icon
(
names
,
options
=
{})
if
(
options
.
keys
&
%w[aria-hidden aria-label]
).
empty?
if
(
options
.
keys
&
%w[aria-hidden aria-label
data-hidden
]
).
empty?
# Add
`aria-hidden` if there are no aria's set
# Add
'aria-hidden' and 'data-hidden' if they are not set in options.
options
[
'aria-hidden'
]
=
true
options
[
'aria-hidden'
]
=
true
options
[
'data-hidden'
]
=
true
end
end
options
.
include?
(
:base
)
?
fa_stacked_icon
(
names
,
options
)
:
fa_icon
(
names
,
options
)
options
.
include?
(
:base
)
?
fa_stacked_icon
(
names
,
options
)
:
fa_icon
(
names
,
options
)
...
...
app/views/shared/notes/_hints.html.haml
View file @
5b0e0869
...
@@ -9,6 +9,27 @@
...
@@ -9,6 +9,27 @@
-
else
-
else
is
is
supported
supported
%button
.toolbar-button.markdown-selector
{
type:
'button'
,
tabindex:
'-1'
}
=
icon
(
'file-image-o'
,
class:
'toolbar-button-icon'
)
%span
.uploading-container
Attach a file
%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
changelogs/unreleased/add_ability_to_cancel_attaching_file_and_redesign_attaching_files_ui.yml
0 → 100644
View file @
5b0e0869
---
title
:
Add an ability to cancel attaching file and redesign attaching files UI
merge_request
:
9431
author
:
blackst0ne
spec/features/uploads/user_uploads_file_to_note_spec.rb
View file @
5b0e0869
...
@@ -5,18 +5,78 @@ feature 'User uploads file to note', feature: true do
...
@@ -5,18 +5,78 @@ feature 'User uploads file to note', feature: true do
let
(
:user
)
{
create
(
:user
)
}
let
(
:user
)
{
create
(
:user
)
}
let
(
:project
)
{
create
(
:empty_project
,
creator:
user
,
namespace:
user
.
namespace
)
}
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
before
do
issue
=
create
(
:issue
,
project:
project
,
author:
user
)
login_as
(
user
)
login_as
(
user
)
visit
namespace_project_issue_path
(
project
.
namespace
,
project
,
issue
)
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'
))
scenario
'they see the attached file'
,
js:
true
do
click_button
'Comment'
dropzone_file
([
Rails
.
root
.
join
(
'spec'
,
'fixtures'
,
'dk.png'
)])
wait_for_ajax
click_button
'Comment'
wait_for_ajax
expect
(
find
(
'a.no-attachment-icon img[alt="dk"]'
)[
'src'
])
expect
(
find
(
'a.no-attachment-icon img[alt="dk"]'
)[
'src'
])
.
to
match
(
%r{/
#{
project
.
full_path
}
/uploads/
\h
{32}/dk
\.
png$}
)
.
to
match
(
%r{/
#{
project
.
full_path
}
/uploads/
\h
{32}/dk
\.
png$}
)
end
end
end
end
end
spec/support/dropzone_helper.rb
View file @
5b0e0869
...
@@ -6,32 +6,52 @@ module DropzoneHelper
...
@@ -6,32 +6,52 @@ module DropzoneHelper
# Dropzone events to perform the actual upload.
# Dropzone events to perform the actual upload.
#
#
# This method waits for the upload to complete before returning.
# 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
# Generate a fake file input that Capybara can attach to
page
.
execute_script
<<-
JS
.
strip_heredoc
page
.
execute_script
<<-
JS
.
strip_heredoc
$('#fakeFileInput').remove();
var fakeFileInput = window.$('<input/>').attr(
var fakeFileInput = window.$('<input/>').attr(
{id: 'fakeFileInput', type: 'file'}
{id: 'fakeFileInput', type: 'file'
, multiple: true
}
).appendTo('body');
).appendTo('body');
window._dropzoneComplete = false;
window._dropzoneComplete = false;
JS
JS
# Attach
the file
to the fake input selector with Capybara
# Attach
files
to the fake input selector with Capybara
attach_file
(
'fakeFileInput'
,
file
_path
)
attach_file
(
'fakeFileInput'
,
file
s
)
# Manually trigger a Dropzone "drop" event with the fake input's file list
# Manually trigger a Dropzone "drop" event with the fake input's file list
page
.
execute_script
<<-
JS
.
strip_heredoc
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;
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() {
dropzone.on('queuecomplete', function() {
window._dropzoneComplete = true;
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
JS
# Wait until Dropzone's fired `queuecomplete`
if
wait_for_queuecomplete
loop
until
page
.
evaluate_script
(
'window._dropzoneComplete === true'
)
# Wait until Dropzone's fired `queuecomplete`
loop
until
page
.
evaluate_script
(
'window._dropzoneComplete === true'
)
end
end
end
end
end
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment