diff --git a/.gitlab/merge_request_templates/Default.md b/.gitlab/merge_request_templates/Default.md index 941bbfadc..477469157 100644 --- a/.gitlab/merge_request_templates/Default.md +++ b/.gitlab/merge_request_templates/Default.md @@ -1,8 +1,9 @@ ## What does this MR do? - + +[Issue Link]() -## Screenshots +## Screenshots ### Before @@ -12,7 +13,7 @@ ![alt text](https://example.com/after.png) -## Notes +## Code Changes * This MR **does** @@ -58,9 +59,36 @@ How do your performance changes scale on a system of this size? they are really big customers, and we want to keep their business!) --> -### Follow-up Required +### Documentation Follow-up Required? + + +This MR may require follow-up by the documentation team. +/label ~Documentation + + +This MR does not require any follow-up. + +## QA Checklist (to be filled by the reviewer) + +- [ ] Implementation satisfies specification +- [ ] Changes confirmed by manual testing +- [ ] [Code style](https://git.znuny.com/zammad/zammad/-/wikis/Coding-style-guide) is appropriate +- [ ] Performance will not degrade +- [ ] Code is properly covered with tests +- If follow-up by the documentation team is needed: + - [ ] Add a comment with this text +> @MrGeneration please check if this MR requires changes to the documentation. Thanks! diff --git a/.pkgr.yml b/.pkgr.yml index 4e4e94567..e4468d5b5 100644 --- a/.pkgr.yml +++ b/.pkgr.yml @@ -116,6 +116,6 @@ env: - ZAMMAD_RAILS_PORT=3000 - ZAMMAD_WEBSOCKET_PORT=6042 services: - - postgres + - postgres:13 before_install: contrib/packager.io/preinstall.sh after_install: contrib/packager.io/postinstall.sh diff --git a/.rubocop/default.yml b/.rubocop/default.yml index bf0656c06..f08267c10 100644 --- a/.rubocop/default.yml +++ b/.rubocop/default.yml @@ -6,6 +6,8 @@ require: - rubocop-performance - rubocop-rails - rubocop-rspec + - rubocop-inflector + - ../config/initializers/inflections.rb - ./rubocop_zammad.rb inherit_from: diff --git a/.rubocop/todo.yml b/.rubocop/todo.yml index c841e288d..419601015 100644 --- a/.rubocop/todo.yml +++ b/.rubocop/todo.yml @@ -887,10 +887,6 @@ Metrics/PerceivedComplexity: - 'test/browser_test_helper.rb' - 'test/integration/slack_test.rb' -Rails/AssertNot: - Exclude: - - 'test/browser/admin_permissions_granular_vs_full_test.rb' - Rails/CreateTableWithTimestamps: Exclude: - 'db/migrate/20120101000001_create_base.rb' diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e98c7b8a..1fe3332c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Change Log -## [5.0.0](https://github.com/zammad/zammad/tree/5.0.0) (2021-xx-xx) -[Full Changelog](https://github.com/zammad/zammad/compare/4.1.0...5.0.0) +## [5.1.0](https://github.com/zammad/zammad/tree/5.1.0) (2021-xx-xx) +[Full Changelog](https://github.com/zammad/zammad/compare/5.0.0...5.1.0) **Implemented enhancements:** diff --git a/Gemfile b/Gemfile index 95d906228..594bc7bd6 100644 --- a/Gemfile +++ b/Gemfile @@ -198,6 +198,7 @@ group :development, :test do gem 'overcommit' gem 'rubocop' gem 'rubocop-faker' + gem 'rubocop-inflector' gem 'rubocop-performance' gem 'rubocop-rails' gem 'rubocop-rspec' diff --git a/Gemfile.lock b/Gemfile.lock index decaedc3f..f05e70e95 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -90,7 +90,7 @@ GEM activerecord (>= 4.2) addressable (2.8.0) public_suffix (>= 2.0.2, < 5.0) - argon2 (2.0.3) + argon2 (2.1.1) ffi (~> 1.14) ffi-compiler (~> 1.0) argon2 (2.0.3-x86_64-linux-musl) @@ -113,7 +113,7 @@ GEM faraday async-io (1.32.2) async - async-pool (0.3.8) + async-pool (0.3.9) async (>= 1.25) autoprefixer-rails (10.3.3.0) execjs (~> 2) @@ -188,7 +188,7 @@ GEM docile (1.4.0) domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) - doorkeeper (5.5.3) + doorkeeper (5.5.4) railties (>= 5) dotenv (2.7.6) eco (1.0.0) @@ -302,9 +302,8 @@ GEM inflection (1.0.0) iniparse (1.5.0) interception (0.5) - json (2.5.1) json (2.5.1-x86_64-linux-musl) - jwt (2.2.3) + jwt (2.3.0) kgio (2.11.4) kgio (2.11.4-x86_64-linux-musl) koala (3.0.0) @@ -444,7 +443,7 @@ GEM binding_of_caller (~> 1.0) pry (~> 0.13) public_suffix (4.0.6) - puma (4.3.8) + puma (4.3.10) nio4r (~> 2.0) puma (4.3.8-x86_64-linux-musl) nio4r (~> 2.0) @@ -522,15 +521,14 @@ GEM rspec-mocks (~> 3.10) rspec-support (~> 3.10) rspec-support (3.10.2) - rszr (0.5.2) rszr (0.5.2-x86_64-linux-musl) - rubocop (1.21.0) + rubocop (1.22.1) parallel (~> 1.10) parser (>= 3.0.0.0) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml - rubocop-ast (>= 1.9.1, < 2.0) + rubocop-ast (>= 1.12.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 3.0) rubocop-ast (1.12.0) @@ -538,10 +536,14 @@ GEM rubocop-faker (1.1.0) faker (>= 2.12.0) rubocop (>= 0.82.0) + rubocop-inflector (0.1.1) + activesupport + rubocop + rubocop-rspec rubocop-performance (1.11.5) rubocop (>= 1.7.0, < 2.0) rubocop-ast (>= 0.4.0) - rubocop-rails (2.12.2) + rubocop-rails (2.12.3) activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.7.0, < 2.0) @@ -617,7 +619,7 @@ GEM timers (4.3.3) tins (1.29.1) sync - twilio-ruby (5.58.3) + twilio-ruby (5.59.0) faraday (>= 0.9, < 2.0) jwt (>= 1.5, <= 2.5) nokogiri (>= 1.6, < 2.0) @@ -764,6 +766,7 @@ DEPENDENCIES rszr (= 0.5.2) rubocop rubocop-faker + rubocop-inflector rubocop-performance rubocop-rails rubocop-rspec @@ -797,4 +800,4 @@ RUBY VERSION ruby 2.7.3p183 BUNDLED WITH - 2.2.20 + 2.2.27 diff --git a/VERSION b/VERSION index 660b079cf..fed0ee9e4 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -5.0.x +5.1.x diff --git a/app/assets/javascripts/app/controllers/_application_controller/table.coffee b/app/assets/javascripts/app/controllers/_application_controller/table.coffee index 64e809fe3..3056a6a32 100644 --- a/app/assets/javascripts/app/controllers/_application_controller/table.coffee +++ b/app/assets/javascripts/app/controllers/_application_controller/table.coffee @@ -493,6 +493,25 @@ class App.ControllerTable extends App.Controller sortable: @dndCallback )) + getGroupByKeyName: (object, groupBy) -> + reference_key = groupBy + '_id' + + if reference_key of object + attribute = _.findWhere(object.constructor.configure_attributes, { name: reference_key }) + + return App[attribute.relation]?.find(object[reference_key])?.displayName() || reference_key + + groupBy + + sortObjectKeys: (objects, direction) -> + sorted = Object.keys(objects).sort() + + switch direction + when 'DESC' + sorted.reverse() + else + sorted + renderTableRows: (sort = false) => if sort is true @sortList() @@ -506,11 +525,11 @@ class App.ControllerTable extends App.Controller objectsToShow = @objectsOfPage(@pagerShownPage) if @groupBy # group by raw (and not printable) value so dates work also - objectsGrouped = _.groupBy(objectsToShow, (object) => object[@groupBy]) + objectsGrouped = _.groupBy(objectsToShow, (object) => object[@getGroupByKeyName(object, @groupBy)]) else objectsGrouped = { '': objectsToShow } - for groupValue in Object.keys(objectsGrouped).sort() + for groupValue in @sortObjectKeys(objectsGrouped, @groupDirection) groupObjects = objectsGrouped[groupValue] for object in groupObjects diff --git a/app/assets/javascripts/app/controllers/_ui_element/_application_ui_element.coffee b/app/assets/javascripts/app/controllers/_ui_element/_application_ui_element.coffee index 9c48d193b..4a950bc02 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/_application_ui_element.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/_application_ui_element.coffee @@ -46,7 +46,7 @@ class App.UiElement.ApplicationUiElement result = [] for row in selection if attribute.translate - row.name = App.i18n.translateInline(row.name) + row.name = App.i18n.translatePlain(row.name) if !_.isEmpty(row.children) row.children = @getConfigOptionListArray(attribute, row.children) result.push row @@ -65,7 +65,7 @@ class App.UiElement.ApplicationUiElement for key in order name_new = selection[key] if attribute.translate - name_new = App.i18n.translateInline(name_new) + name_new = App.i18n.translatePlain(name_new) attribute.options.push { name: name_new value: key @@ -162,7 +162,7 @@ class App.UiElement.ApplicationUiElement nameNew = item.name if attribute.translate - nameNew = App.i18n.translateInline(nameNew) + nameNew = App.i18n.translatePlain(nameNew) row = value: item.id, diff --git a/app/assets/javascripts/app/controllers/_ui_element/core_workflow_condition.coffee b/app/assets/javascripts/app/controllers/_ui_element/core_workflow_condition.coffee index 10634e7a4..6cedd5b90 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/core_workflow_condition.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/core_workflow_condition.coffee @@ -23,7 +23,7 @@ class App.UiElement.core_workflow_condition extends App.UiElement.ApplicationSel organization: name: 'Organization' model: 'Organization' - model_show: ['Organization'] + model_show: ['User', 'Organization'] 'customer.organization': name: 'Organization' model: 'Organization' diff --git a/app/assets/javascripts/app/controllers/_ui_element/core_workflow_perform.coffee b/app/assets/javascripts/app/controllers/_ui_element/core_workflow_perform.coffee index 4d0e3110c..b20780c62 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/core_workflow_perform.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/core_workflow_perform.coffee @@ -128,9 +128,10 @@ class App.UiElement.core_workflow_perform extends App.UiElement.ApplicationSelec @buildValueConfigMultiple: (config, meta) -> if _.contains(['add_option', 'remove_option', 'set_fixed_to'], meta.operator) config.multiple = true + config.nulloption = true else config.multiple = false - config.nulloption = false + config.nulloption = false return config @HasPreCondition: -> 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/core_workflow.coffee b/app/assets/javascripts/app/controllers/core_workflow.coffee index a537a9077..a39a2a61b 100644 --- a/app/assets/javascripts/app/controllers/core_workflow.coffee +++ b/app/assets/javascripts/app/controllers/core_workflow.coffee @@ -1,6 +1,6 @@ class CoreWorkflow extends App.ControllerSubContent requiredPermission: 'admin.core_workflow' - header: 'Core Workflow' + header: 'Core Workflows' constructor: -> super @@ -54,4 +54,4 @@ class CoreWorkflow extends App.ControllerSubContent } return mapping[screen] || screen -App.Config.set('CoreWorkflowObject', { prio: 1750, parent: '#system', name: 'Core Workflow', target: '#system/core_workflow', controller: CoreWorkflow, permission: ['admin.core_workflow'] }, 'NavBarAdmin') +App.Config.set('CoreWorkflowObject', { prio: 1750, parent: '#system', name: 'Core Workflows', target: '#system/core_workflow', controller: CoreWorkflow, permission: ['admin.core_workflow'] }, 'NavBarAdmin') diff --git a/app/assets/javascripts/app/controllers/knowledge_base/public_menu_manager.coffee b/app/assets/javascripts/app/controllers/knowledge_base/public_menu_manager.coffee index 879497e6d..34941450c 100644 --- a/app/assets/javascripts/app/controllers/knowledge_base/public_menu_manager.coffee +++ b/app/assets/javascripts/app/controllers/knowledge_base/public_menu_manager.coffee @@ -27,11 +27,13 @@ class App.KnowledgeBasePublicMenuManager extends App.Controller { headline: 'Header menu', identifier: 'header', - color: kb.color_header + color: kb.color_header, + color_link: kb.color_header_link }, { headline: 'Footer menu', - identifier: 'footer' + identifier: 'footer', + color_link: 'hsl(207,12%,50%)' } ] diff --git a/app/assets/javascripts/app/controllers/ticket_overview.coffee b/app/assets/javascripts/app/controllers/ticket_overview.coffee index d9231b347..aa254c6e3 100644 --- a/app/assets/javascripts/app/controllers/ticket_overview.coffee +++ b/app/assets/javascripts/app/controllers/ticket_overview.coffee @@ -1314,7 +1314,7 @@ class Table extends App.Controller return if ticketListShow[0] || @permissionCheck('ticket.agent') tickets_count = user.lifetimeCustomerTicketsCount() - @html App.view('customer_not_ticket_exists')(has_any_tickets: tickets_count > 0) + @html App.view('customer_not_ticket_exists')(has_any_tickets: tickets_count > 0, is_allowed_to_create_ticket: @Config.get('customer_ticket_create')) if tickets_count == 0 @listenTo user, 'refresh', => diff --git a/app/assets/javascripts/app/controllers/ticket_zoom.coffee b/app/assets/javascripts/app/controllers/ticket_zoom.coffee index 9a9bad539..6b560691b 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom.coffee @@ -200,10 +200,10 @@ class App.TicketZoom extends App.Controller formMeta = data.form_meta # on the following states we want to rerender the ticket: - # - if the object attribute configuration has changed (attribute values, restrictions, filters) + # - if the object attribute configuration has changed (attribute values, dependecies, filters) # - if the user view has changed (agent/customer) # - if the ticket permission has changed (read/write/full) - if @view && ( !_.isEqual(@formMeta, formMeta) || @view isnt view || @readable isnt readable || @changeable isnt changeable || @fullable isnt fullable ) + if @view && ( !_.isEqual(@formMeta.configure_attributes, formMeta.configure_attributes) || !_.isEqual(@formMeta.dependencies, formMeta.dependencies) || !_.isEqual(@formMeta.filter, formMeta.filter) || @view isnt view || @readable isnt readable || @changeable isnt changeable || @fullable isnt fullable ) @renderDone = false @view = view @@ -214,6 +214,7 @@ class App.TicketZoom extends App.Controller # render page @render(local) + App.Event.trigger('ui::ticket::load', data) meta: => 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..e8802b8e3 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee @@ -98,7 +98,7 @@ class App.TicketZoomArticleNew extends App.Controller @controllerBind('ui:rerender', => @adjustedTextarea = false @defaults = @ui.taskGet('article') - @attachments = @defaults.attachments + @attachments = @defaults.attachments || [] @render() ) @@ -117,7 +117,7 @@ class App.TicketZoomArticleNew extends App.Controller @tokanice(@type) - if @defaults.body or @isIE10() + if @defaults.body or @attachments.length > 0 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/controllers/ticket_zoom/form_handler_core_workflow.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/form_handler_core_workflow.coffee index e1548091c..d0ca39503 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom/form_handler_core_workflow.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom/form_handler_core_workflow.coffee @@ -119,7 +119,9 @@ class App.FormHandlerCoreWorkflow valueFound = false for value in values - if value && paramValue + + # false values are valid values e.g. for boolean fields (be careful) + if value isnt undefined && paramValue isnt undefined if value.toString() == paramValue.toString() valueFound = true break diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_ticket.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_ticket.coffee index 5f033dd86..bdb18819b 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_ticket.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_ticket.coffee @@ -1,12 +1,21 @@ -class Edit extends App.ControllerObserver - model: 'Ticket' - observeNot: - created_at: true - updated_at: true - globalRerender: false +# No usage of a ControllerObserver here because we want to use +# the data of the ticket zoom ajax request which is using the all=true parameter +# and contain the core workflow information as well. Without observer we also +# dont have double rendering because of the zoom (all=true) and observer (full=true) render callback +class Edit extends App.Controller + constructor: (params) -> + super + @controllerBind('ui::ticket::load', (data) => + return if data.ticket_id.toString() isnt @ticket.id.toString() - render: (ticket, diff) => - defaults = ticket.attributes() + @ticket = App.Ticket.find(@ticket.id) + @formMeta = data.form_meta + @render() + ) + @render() + + render: => + defaults = @ticket.attributes() delete defaults.article # ignore article infos followUpPossible = App.Group.find(defaults.group_id).follow_up_possible ticketState = App.TicketState.find(defaults.state_id).name @@ -16,10 +25,13 @@ class Edit extends App.ControllerObserver if !_.isEmpty(taskState) defaults = _.extend(defaults, taskState) + # remove core workflow data because it should trigger a request to get data + # for the new ticket + eventually changed task state + @formMeta.core_workflow = undefined if followUpPossible == 'new_ticket' && ticketState != 'closed' || followUpPossible != 'new_ticket' || - @permissionCheck('admin') || ticket.currentView() is 'agent' + @permissionCheck('admin') || @ticket.currentView() is 'agent' @controllerFormSidebarTicket = new App.ControllerForm( elReplace: @el model: { className: 'Ticket', configure_attributes: @formMeta.configure_attributes || App.Ticket.configure_attributes } @@ -28,7 +40,7 @@ class Edit extends App.ControllerObserver filter: @formMeta.filter formMeta: @formMeta params: defaults - isDisabled: !ticket.editable() + isDisabled: !@ticket.editable() taskKey: @taskKey core_workflow: { callbacks: [@markForm] @@ -44,7 +56,7 @@ class Edit extends App.ControllerObserver filter: @formMeta.filter formMeta: @formMeta params: defaults - isDisabled: ticket.editable() + isDisabled: @ticket.editable() taskKey: @taskKey core_workflow: { callbacks: [@markForm] @@ -57,8 +69,8 @@ class Edit extends App.ControllerObserver return if @resetBind @resetBind = true @controllerBind('ui::ticket::taskReset', (data) => - return if data.ticket_id.toString() isnt ticket.id.toString() - @render(ticket) + return if data.ticket_id.toString() isnt @ticket.id.toString() + @render() ) class SidebarTicket extends App.Controller @@ -128,6 +140,7 @@ class SidebarTicket extends App.Controller @edit = new Edit( object_id: @ticket.id + ticket: @ticket el: localEl.find('.edit') taskGet: @taskGet formMeta: @formMeta 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/models/knowledge_base.coffee b/app/assets/javascripts/app/models/knowledge_base.coffee index a48f55bef..8bf326ba7 100644 --- a/app/assets/javascripts/app/models/knowledge_base.coffee +++ b/app/assets/javascripts/app/models/knowledge_base.coffee @@ -1,5 +1,5 @@ class App.KnowledgeBase extends App.Model - @configure 'KnowledgeBase', 'iconset', 'color_highlight', 'color_header', 'translation_ids', 'locale_ids', 'homepage_layout', 'category_layout', 'custom_address' + @configure 'KnowledgeBase', 'iconset', 'color_highlight', 'color_header', 'color_header_link', 'translation_ids', 'locale_ids', 'homepage_layout', 'category_layout', 'custom_address' @extend Spine.Model.Ajax @extend App.KnowledgeBaseActions @url: @apiPath + '/knowledge_bases' @@ -148,6 +148,17 @@ class App.KnowledgeBase extends App.Model display: false horizontal: true shown: true + }, { + name: 'color_header_link' + display: 'Header Link Color' + tag: 'color' + style: 'block' + null: false + screen: + admin_style_color_header_link: + display: false + horizontal: true + shown: true # Layout picker is disabled in V1 #}, { # name: 'homepage_layout' diff --git a/app/assets/javascripts/app/models/user.coffee b/app/assets/javascripts/app/models/user.coffee index f1ed97298..423035030 100644 --- a/app/assets/javascripts/app/models/user.coffee +++ b/app/assets/javascripts/app/models/user.coffee @@ -344,9 +344,12 @@ class App.User extends App.Model @sameOrganization?(requester) isChangeableBy: (requester) -> + # full access for admins return true if requester.permission('admin.user') - # allow agents to change customers + # forbid non-agents to change users return false if !requester.permission('ticket.agent') + # allow agents to change customers only + return false if @permission(['admin.user', 'ticket.agent']) @permission('ticket.customer') isDeleteableBy: (requester) -> diff --git a/app/assets/javascripts/app/views/customer_not_ticket_exists.jst.eco b/app/assets/javascripts/app/views/customer_not_ticket_exists.jst.eco index dfd72889b..17aa5bfbe 100644 --- a/app/assets/javascripts/app/views/customer_not_ticket_exists.jst.eco +++ b/app/assets/javascripts/app/views/customer_not_ticket_exists.jst.eco @@ -6,11 +6,15 @@ <% if @has_any_tickets: %>

<%- @T('You have no tickets to display in this overview.') %>

<% else: %> -

<%- @T('You have not created a ticket yet.') %>

-

<%- @T('The way to communicate with us is this thing called "ticket".') %>

-

<%- @T('Please click the button below to create your first one.') %>

+ <% if @is_allowed_to_create_ticket: %> +

<%- @T('You have not created a ticket yet.') %>

+

<%- @T('The way to communicate with us is this thing called "ticket".') %>

+

<%- @T('Please click the button below to create your first one.') %>

-

<%- @T('Create your first ticket') %>

+

<%- @T('Create your first ticket') %>

+ <% else: %> +

<%- @T('You currently don\'t have any tickets.') %>

+ <% end %> <% end %> diff --git a/app/assets/javascripts/app/views/data_privacy/preview.jst.eco b/app/assets/javascripts/app/views/data_privacy/preview.jst.eco index 02856323a..90a19c95d 100644 --- a/app/assets/javascripts/app/views/data_privacy/preview.jst.eco +++ b/app/assets/javascripts/app/views/data_privacy/preview.jst.eco @@ -14,7 +14,7 @@

<%- @T('Warning') %>

-

<%- @T('There is no rollback of this deletion possible. If you are absolutely sure to do this, then type in "%s" into the input.', App.i18n.translateInline('delete').toUpperCase()) %>

+

<%- @T('There is no rollback of this deletion possible. If you are absolutely sure to do this, then type in "%s" into the input.', App.i18n.translatePlain('delete').toUpperCase()) %>

<%- @sure_html %>
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/javascripts/app/views/knowledge_base/new_modal.coffee b/app/assets/javascripts/app/views/knowledge_base/new_modal.coffee index 3dbb91fd8..bfd1905db 100644 --- a/app/assets/javascripts/app/views/knowledge_base/new_modal.coffee +++ b/app/assets/javascripts/app/views/knowledge_base/new_modal.coffee @@ -26,11 +26,12 @@ class App.KnowledgeBaseNewModal extends App.ControllerModal App.UiElement[attribute.tag].prepareParams?(attribute, dom, params) applyDefaults: (params) -> - params['iconset'] = 'FontAwesome' - params['color_highlight'] = '#38ae6a' - params['color_header'] = '#f9fafb' - params['homepage_layout'] = 'grid' - params['category_layout'] = 'grid' + params['iconset'] = 'FontAwesome' + params['color_highlight'] = '#38ae6a' + params['color_header'] = '#f9fafb' + params['color_header_link'] = 'hsl(206,8%,50%)' + params['homepage_layout'] = 'grid' + params['category_layout'] = 'grid' onSubmit: (e) -> params = @formParams(@el) diff --git a/app/assets/javascripts/app/views/knowledge_base/public_menu_manager.jst.eco b/app/assets/javascripts/app/views/knowledge_base/public_menu_manager.jst.eco index 3ccb6b454..7922cf902 100644 --- a/app/assets/javascripts/app/views/knowledge_base/public_menu_manager.jst.eco +++ b/app/assets/javascripts/app/views/knowledge_base/public_menu_manager.jst.eco @@ -14,7 +14,7 @@
<%= kb_locale.systemLocale().name %>
-
+
<% menu_items = App.KnowledgeBaseMenuItem.using_kb_locale_location(kb_locale, location.identifier) %> <% if menu_items.length == 0: %> diff --git a/app/assets/javascripts/app/views/login.jst.eco b/app/assets/javascripts/app/views/login.jst.eco index 3b1506dc9..865ba4d13 100644 --- a/app/assets/javascripts/app/views/login.jst.eco +++ b/app/assets/javascripts/app/views/login.jst.eco @@ -1,7 +1,7 @@