From 90eca0f1ebea230eb77a338204ca40cc14cbb67c Mon Sep 17 00:00:00 2001 From: Mantas Masalskis Date: Mon, 4 Oct 2021 21:05:32 +0200 Subject: [PATCH] Fixes #3773 - Inconstant alignment in the listing of attachments/submit button in new article area Fixes #3774 - Broken dialog whiling uploading oversized attachment --- .../controllers/_ui_element/richtext.coffee | 94 ++++++------------ .../controllers/agent_ticket_create.coffee | 10 +- .../ticket_zoom/article_new.coffee | 92 ++++------------- .../app/lib/app_post/html5_upload.coffee | 98 +++++++++++++++++++ .../javascripts/app/lib/base/html5Upload.js | 3 +- .../app/views/generic/attachment.jst.eco | 2 +- app/assets/stylesheets/zammad.scss | 5 + spec/system/ticket/create_spec.rb | 30 ++++++ 8 files changed, 195 insertions(+), 139 deletions(-) create mode 100644 app/assets/javascripts/app/lib/app_post/html5_upload.coffee diff --git a/app/assets/javascripts/app/controllers/_ui_element/richtext.coffee b/app/assets/javascripts/app/controllers/_ui_element/richtext.coffee index e20b1479d..b7b027ae7 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/richtext.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/richtext.coffee @@ -6,7 +6,7 @@ class App.UiElement.richtext attribute.value = attribute.value.text item = $( App.view('generic/richtext')(attribute: attribute, toolButtons: @toolButtons) ) - @contenteditable = item.find('[contenteditable]').ce( + item.find('[contenteditable]').ce( mode: attribute.type maxlength: attribute.maxlength buttons: attribute.buttons @@ -21,12 +21,12 @@ class App.UiElement.richtext new App[plugin.controller](params) if attribute.upload - @attachments = [] + attachments = [] item.append( $( App.view('generic/attachment')(attribute: attribute) ) ) - renderFile = (file) => + renderFile = (file) -> item.find('.attachments').append(App.view('generic/attachment_item')(file)) - @attachments.push file + attachments.push file if params && params.attachments for file in params.attachments @@ -46,10 +46,10 @@ class App.UiElement.richtext , form.form_id) # remove items - item.find('.attachments').on('click', '.js-delete', (e) => + item.find('.attachments').on('click', '.js-delete', (e) -> id = $(e.currentTarget).data('id') - @attachments = _.filter( - @attachments, + attachments = _.filter( + attachments, (item) -> return if item.id.toString() is id.toString() item @@ -71,67 +71,35 @@ class App.UiElement.richtext element.empty() ) - @progressBar = item.find('.attachmentUpload-progressBar') - @progressText = item.find('.js-percentage') - @attachmentPlaceholder = item.find('.attachmentPlaceholder') - @attachmentUpload = item.find('.attachmentUpload') - @attachmentsHolder = item.find('.attachments') - @cancelContainer = item.find('.js-cancel') + App.Delay.set( -> + uploader = new App.Html5Upload( + uploadUrl: "#{App.Config.get('api_path')}/attachments" + dropContainer: item.closest('form') + cancelContainer: item.find('.js-cancel') + inputField: item.find('input') + data: + form_id: item.closest('form').find('[name=form_id]').val() - u = => html5Upload.initialize( - uploadUrl: "#{App.Config.get('api_path')}/attachments" - dropContainer: item.closest('form').get(0) - cancelContainer: @cancelContainer - inputField: item.find('input').get(0) - maxSimultaneousUploads: 1, - key: 'File' - data: - form_id: item.closest('form').find('[name=form_id]').val() - onFileAdded: (file) => + onFileStartCallback: -> + item.find('[contenteditable]').trigger('fileUploadStart') - file.on( - onStart: => - @attachmentPlaceholder.addClass('hide') - @attachmentUpload.removeClass('hide') - @cancelContainer.removeClass('hide') - item.find('[contenteditable]').trigger('fileUploadStart') - App.Log.debug 'UiElement.richtext', 'upload start' + onFileCompletedCallback: (response) -> + renderFile(response.data) + item.find('input').val('') + item.find('[contenteditable]').trigger('fileUploadStop', ['completed']) - onAborted: => - @attachmentPlaceholder.removeClass('hide') - @attachmentUpload.addClass('hide') - item.find('input').val('') - item.find('[contenteditable]').trigger('fileUploadStop', ['aborted']) + onFileAbortedCallback: -> + item.find('input').val('') + item.find('[contenteditable]').trigger('fileUploadStop', ['aborted']) - # Called after received response from the server - onCompleted: (response) => - response = JSON.parse(response) + attachmentPlaceholder: item.find('.attachmentPlaceholder') + attachmentUpload: item.find('.attachmentUpload') + progressBar: item.find('.attachmentUpload-progressBar') + progressText: item.find('.js-percentage') + ) - @attachmentPlaceholder.removeClass('hide') - @attachmentUpload.addClass('hide') - - # reset progress bar - @progressBar.width(parseInt(0) + '%') - @progressText.text('') - - renderFile(response.data) - item.find('input').val('') - item.find('[contenteditable]').trigger('fileUploadStop', ['completed']) - App.Log.debug 'UiElement.richtext', 'upload complete', response.data - - # Called during upload progress, first parameter - # is decimal value from 0 to 100. - onProgress: (progress, fileSize, uploadedBytes) => - @progressBar.width(parseInt(progress) + '%') - @progressText.text(parseInt(progress)) - # hide cancel on 90% - if parseInt(progress) >= 90 - @cancelContainer.addClass('hide') - App.Log.debug 'UiElement.richtext', 'uploadProgress ', parseInt(progress) - - ) - ) - App.Delay.set(u, 100, undefined, 'form_upload') + uploader.render() + , 100, undefined, 'form_upload') item diff --git a/app/assets/javascripts/app/controllers/agent_ticket_create.coffee b/app/assets/javascripts/app/controllers/agent_ticket_create.coffee index 5e8a9299a..954aa8859 100644 --- a/app/assets/javascripts/app/controllers/agent_ticket_create.coffee +++ b/app/assets/javascripts/app/controllers/agent_ticket_create.coffee @@ -8,7 +8,7 @@ class App.TicketCreate extends App.Controller events: 'click .type-tabs .tab': 'changeFormType' 'submit form': 'submit' - 'click .js-cancel': 'cancel' + 'click .form-controls .js-cancel': 'cancel' 'click .js-active-toggle': 'toggleButton' types: { @@ -184,8 +184,11 @@ class App.TicketCreate extends App.Controller @controllerUnbind('ticket_create_rerender', (template) => @renderQueue(template)) changed: => + return true if @hasAttachments() + formCurrent = @formParam( @$('.ticket-create') ) diff = difference(@formDefault, formCurrent) + return false if !diff || _.isEmpty(diff) return true @@ -461,6 +464,9 @@ class App.TicketCreate extends App.Controller params: => params = @formParam(@$('.main form')) + hasAttachments: => + @$('.richtext .attachments .attachment').length > 0 + submit: (e) => e.preventDefault() @@ -563,7 +569,7 @@ class App.TicketCreate extends App.Controller # save ticket, create article # check attachment if article['body'] - if @$('.richtext .attachments .attachment').length < 1 + if !@hasAttachments() matchingWord = App.Utils.checkAttachmentReference(article['body']) if matchingWord if !confirm(App.i18n.translateContent('You use %s in text but no attachment is attached. Do you want to continue?', matchingWord)) diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee index 53ef10e57..82411fe48 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee @@ -117,7 +117,7 @@ class App.TicketZoomArticleNew extends App.Controller @tokanice(@type) - if @defaults.body or @isIE10() + if @defaults.body or @attachments or @isIE10() @openTextarea(null, true) tokanice: (type = 'email') -> @@ -191,82 +191,30 @@ class App.TicketZoomArticleNew extends App.Controller maxlength: 150000 }) - html5Upload.initialize( - uploadUrl: "#{App.Config.get('api_path')}/upload_caches/#{@form_id}" - dropContainer: @$('.article-add').get(0) - cancelContainer: @cancelContainer - inputField: @$('.article-attachment input').get(0) - key: 'File' - maxSimultaneousUploads: 1 - onFileAdded: (file) => + new App.Html5Upload( + uploadUrl: "#{App.Config.get('api_path')}/upload_caches/#{@form_id}" + dropContainer: @$('.article-add') + cancelContainer: @cancelContainer + inputField: @$('.article-attachment input') - file.on( + onFileStartCallback: => + @callbackFileUploadStart?() - onStart: => - @attachmentPlaceholder.addClass('hide') - @attachmentUpload.removeClass('hide') - @cancelContainer.removeClass('hide') + onFileCompletedCallback: (response) => + @attachments.push response.data + @renderAttachment(response.data) + @$('.article-attachment input').val('') - if @callbackFileUploadStart - @callbackFileUploadStart() + @callbackFileUploadStop?() - onAborted: => - @attachmentPlaceholder.removeClass('hide') - @attachmentUpload.addClass('hide') - @$('.article-attachment input').val('') + onFileAbortedCallback: => + @callbackFileUploadStop?() - if @callbackFileUploadStop - @callbackFileUploadStop() - - # Called after received response from the server - onCompleted: (response) => - - response = JSON.parse(response) - @attachments.push response.data - - @attachmentPlaceholder.removeClass('hide') - @attachmentUpload.addClass('hide') - - # reset progress bar - @progressBar.width(parseInt(0) + '%') - @progressText.text('') - - @renderAttachment(response.data) - @$('.article-attachment input').val('') - - if @callbackFileUploadStop - @callbackFileUploadStop() - - # Called during upload progress, first parameter - # is decimal value from 0 to 100. - onProgress: (progress, fileSize, uploadedBytes) => - @progressBar.width(parseInt(progress) + '%') - @progressText.text(parseInt(progress)) - # hide cancel on 90% - if parseInt(progress) >= 90 - @cancelContainer.addClass('hide') - - # Called when upload failed - onError: (message) => - @attachmentPlaceholder.removeClass('hide') - @attachmentUpload.addClass('hide') - @$('.article-attachment input').val('') - - if @callbackFileUploadStop - @callbackFileUploadStop() - - new App.ControllerModal( - head: 'Upload Failed' - buttonCancel: 'Cancel' - buttonCancelClass: 'btn--danger' - buttonSubmit: false - message: message - shown: true - small: true - container: @el.closest('.content') - ) - ) - ) + attachmentPlaceholder: @attachmentPlaceholder + attachmentUpload: @attachmentUpload + progressBar: @progressBar + progressText: @progressText + ).render() @bindAttachmentDelete() diff --git a/app/assets/javascripts/app/lib/app_post/html5_upload.coffee b/app/assets/javascripts/app/lib/app_post/html5_upload.coffee new file mode 100644 index 000000000..4daca6510 --- /dev/null +++ b/app/assets/javascripts/app/lib/app_post/html5_upload.coffee @@ -0,0 +1,98 @@ +class App.Html5Upload extends App.Controller + uploadUrl: null + maxSimultaneousUploads: 1 + key: 'File' + data: null + + onFileStartCallback: null + onFileCompletedCallback: null + onFileAbortedCallback: null + + dropContainer: null + cancelContainer: null + inputField: null + attachmentPlaceholder: null + attachmentUpload: null + progressBar: null + progressText: null + + render: => + html5Upload.initialize( + uploadUrl: @uploadUrl + dropContainer: @dropContainer.get(0) + cancelContainer: @cancelContainer + inputField: @inputField.get(0) + maxSimultaneousUploads: @maxSimultaneousUploads + key: @key + data: @data + onFileAdded: @onFileAdded + ) + + onFileAdded: (file) => + file.on( + onStart: @onFileStart + onAborted: @onFileAborted + onCompleted: @onFileCompleted + onProgress: @onFileProgress + onError: @onFileError + ) + + onFileStart: => + @attachmentPlaceholder.addClass('hide') + @attachmentUpload.removeClass('hide') + @cancelContainer.removeClass('hide') + + App.Log.debug 'Html5Upload', 'upload start' + @onFileStartCallback?() + + onFileProgress: (progress, fileSize, uploadedBytes) => + progress = parseInt(progress) + + @progressBar.width(progress + '%') + @progressText.text(progress) + # hide cancel on 90% + if progress >= 90 + @cancelContainer.addClass('hide') + + App.Log.debug 'Html5Upload', 'uploadProgress ', progress + + + onFileCompleted: (response) => + response = JSON.parse(response) + + @hideFileUploading() + @onFileCompletedCallback?(response) + + App.Log.debug 'Html5Upload', 'upload complete', response.data + + onFileAborted: => + @hideFileUploading() + @onFileAbortedCallback?() + + App.Log.debug 'Html5Upload', 'upload aborted' + + onFileError: (message) => + @hideFileUploading() + @inputField.val('') + + @callbackFileUploadStop?() + + new App.ControllerModal( + head: 'Upload Failed' + buttonCancel: 'Cancel' + buttonCancelClass: 'btn--danger' + buttonSubmit: false + message: message || 'Cannot upload file' + shown: true + small: true + container: @inputField.closest('.content') + ) + + App.Log.debug 'Html5Upload', 'upload error' + + hideFileUploading: => + @attachmentPlaceholder.removeClass('hide') + @attachmentUpload.addClass('hide') + + @progressBar.width('0%') + @progressText.text('0') diff --git a/app/assets/javascripts/app/lib/base/html5Upload.js b/app/assets/javascripts/app/lib/base/html5Upload.js index 40287b23f..72a2e455e 100644 --- a/app/assets/javascripts/app/lib/base/html5Upload.js +++ b/app/assets/javascripts/app/lib/base/html5Upload.js @@ -255,7 +255,7 @@ manager.ajaxUpload(manager.uploadsQueue.shift()); } }; - xhr.abort = function (event) { + xhr.onabort = function (event) { console.log('Upload abort'); // Reduce number of active uploads: @@ -269,6 +269,7 @@ // Triggered when upload fails: xhr.onerror = function () { console.log('Upload failed: ', upload.fileName); + upload.events.onError('Upload failed: ' + upload.fileName); }; // Append additional data if provided: diff --git a/app/assets/javascripts/app/views/generic/attachment.jst.eco b/app/assets/javascripts/app/views/generic/attachment.jst.eco index 2ab26dae8..d2a5e2cc6 100644 --- a/app/assets/javascripts/app/views/generic/attachment.jst.eco +++ b/app/assets/javascripts/app/views/generic/attachment.jst.eco @@ -17,7 +17,7 @@ <%- @T('Uploading') %> (0%) ...
- <%- @Icon('diagonal-cross') %>
<%- @T('Cancel Upload') %> + <%- @Icon('diagonal-cross') %><%- @T('Cancel Upload') %>
diff --git a/app/assets/stylesheets/zammad.scss b/app/assets/stylesheets/zammad.scss index c82d33bef..025d2a488 100644 --- a/app/assets/stylesheets/zammad.scss +++ b/app/assets/stylesheets/zammad.scss @@ -7063,6 +7063,11 @@ footer { .ticket-create .attachments:not(:empty) { margin-left: 0; margin-right: 0; + margin-bottom: 56px; + } + + .ticket-create .attachment--row { + line-height: 1.45; } .attachment.attachment--row { diff --git a/spec/system/ticket/create_spec.rb b/spec/system/ticket/create_spec.rb index debac63c2..2988643d3 100644 --- a/spec/system/ticket/create_spec.rb +++ b/spec/system/ticket/create_spec.rb @@ -469,6 +469,36 @@ RSpec.describe 'Ticket Create', type: :system do expect(page).to have_no_selector(:task_with, task_key) end + + it 'asks for confirmation if attachment was added' do + visit 'ticket/create' + + within :active_content do + page.find('input#fileUpload_1', visible: :all).set(Rails.root.join('test/data/mail/mail001.box')) + await_empty_ajax_queue + + find('.js-cancel').click + end + + in_modal disappears: false do + expect(page).to have_text 'Tab has changed' + end + end + end + + context 'when uploading attachment' do + it 'shows an error if server throws an error' do + allow(Store).to receive(:add) { raise 'Error' } + visit 'ticket/create' + + within :active_content do + page.find('input#fileUpload_1', visible: :all).set(Rails.root.join('test/data/mail/mail001.box')) + end + + in_modal disappears: false do + expect(page).to have_text 'Error' + end + end end context 'when closing taskbar tab for new ticket creation' do