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 @@
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 @@