Fixes #208 - Notify / Mention other agents / Subscribe to Tickets.
This commit is contained in:
parent
d0dcae4776
commit
2354b5f53f
59 changed files with 1388 additions and 102 deletions
|
@ -399,16 +399,22 @@ App.Config.set(
|
||||||
shortcuts: [
|
shortcuts: [
|
||||||
{
|
{
|
||||||
key: '::'
|
key: '::'
|
||||||
hotkeys: false,
|
hotkeys: false
|
||||||
description: 'Inserts Text module'
|
description: 'Inserts Text module'
|
||||||
globalEvent: 'richtext-insert-text-module'
|
globalEvent: 'richtext-insert-text-module'
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
key: '??'
|
key: '??'
|
||||||
hotkeys: false,
|
hotkeys: false
|
||||||
description: 'Inserts Knowledge Base answer'
|
description: 'Inserts Knowledge Base answer'
|
||||||
globalEvent: 'richtext-insert-kb-answer'
|
globalEvent: 'richtext-insert-kb-answer'
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
key: '@@'
|
||||||
|
hotkeys: false
|
||||||
|
description: 'Inserts a mention for a user'
|
||||||
|
globalEvent: 'richtext-insert-mention-user'
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -50,10 +50,7 @@ class ProfileNotification extends App.ControllerSubContent
|
||||||
|
|
||||||
render: =>
|
render: =>
|
||||||
|
|
||||||
# matrix
|
matrix =
|
||||||
config =
|
|
||||||
group_ids: []
|
|
||||||
matrix:
|
|
||||||
create:
|
create:
|
||||||
name: 'New Ticket'
|
name: 'New Ticket'
|
||||||
update:
|
update:
|
||||||
|
@ -63,6 +60,10 @@ class ProfileNotification extends App.ControllerSubContent
|
||||||
escalation:
|
escalation:
|
||||||
name: 'Ticket escalation'
|
name: 'Ticket escalation'
|
||||||
|
|
||||||
|
config =
|
||||||
|
group_ids: []
|
||||||
|
matrix: {}
|
||||||
|
|
||||||
user_config = @Session.get('preferences').notification_config
|
user_config = @Session.get('preferences').notification_config
|
||||||
if user_config
|
if user_config
|
||||||
config = $.extend(true, {}, config, user_config)
|
config = $.extend(true, {}, config, user_config)
|
||||||
|
@ -89,6 +90,7 @@ class ProfileNotification extends App.ControllerSubContent
|
||||||
sound.selected = sound.file is App.OnlineNotification.soundFile() ? true : false
|
sound.selected = sound.file is App.OnlineNotification.soundFile() ? true : false
|
||||||
|
|
||||||
@html App.view('profile/notification')
|
@html App.view('profile/notification')
|
||||||
|
matrix: matrix
|
||||||
groups: groups
|
groups: groups
|
||||||
config: config
|
config: config
|
||||||
sounds: @sounds
|
sounds: @sounds
|
||||||
|
@ -102,6 +104,7 @@ class ProfileNotification extends App.ControllerSubContent
|
||||||
params.notification_config = {}
|
params.notification_config = {}
|
||||||
|
|
||||||
form_params = @formParam(e.target)
|
form_params = @formParam(e.target)
|
||||||
|
|
||||||
for key, value of form_params
|
for key, value of form_params
|
||||||
if key is 'group_ids'
|
if key is 'group_ids'
|
||||||
if typeof value isnt 'object'
|
if typeof value isnt 'object'
|
||||||
|
@ -118,11 +121,12 @@ class ProfileNotification extends App.ControllerSubContent
|
||||||
if !params.notification_config[area[0]][area[1]]
|
if !params.notification_config[area[0]][area[1]]
|
||||||
params.notification_config[area[0]][area[1]] = {}
|
params.notification_config[area[0]][area[1]] = {}
|
||||||
if !params.notification_config[area[0]][area[1]][area[2]]
|
if !params.notification_config[area[0]][area[1]][area[2]]
|
||||||
params.notification_config[area[0]][area[1]][area[2]] = {
|
params.notification_config[area[0]][area[1]][area[2]] = {}
|
||||||
owned_by_me: false
|
|
||||||
owned_by_nobody: false
|
for recipientKey in ['owned_by_me', 'owned_by_nobody', 'mentioned', 'no']
|
||||||
no: false
|
if params.notification_config[area[0]][area[1]][area[2]][recipientKey] == undefined
|
||||||
}
|
params.notification_config[area[0]][area[1]][area[2]][recipientKey] = false
|
||||||
|
|
||||||
params.notification_config[area[0]][area[1]][area[2]][area[3]] = value
|
params.notification_config[area[0]][area[1]][area[2]][area[3]] = value
|
||||||
if area[2] is 'channel'
|
if area[2] is 'channel'
|
||||||
if !params.notification_config[area[0]]
|
if !params.notification_config[area[0]]
|
||||||
|
|
|
@ -118,6 +118,15 @@ class App.UiElement.ticket_selector
|
||||||
translate: true
|
translate: true
|
||||||
operator: ['is', 'is not']
|
operator: ['is', 'is not']
|
||||||
|
|
||||||
|
elements['ticket.mention_user_ids'] =
|
||||||
|
name: 'mention_user_ids'
|
||||||
|
display: 'Mention'
|
||||||
|
tag: 'autocompletion_ajax'
|
||||||
|
relation: 'User'
|
||||||
|
null: false
|
||||||
|
translate: true
|
||||||
|
operator: ['is', 'is not']
|
||||||
|
|
||||||
[defaults, groups, elements]
|
[defaults, groups, elements]
|
||||||
|
|
||||||
@rowContainer: (groups, elements, attribute) ->
|
@rowContainer: (groups, elements, attribute) ->
|
||||||
|
|
|
@ -158,6 +158,10 @@ class App.TicketZoom extends App.Controller
|
||||||
else
|
else
|
||||||
@ticketUpdatedAtLastCall = newTicketRaw.updated_at
|
@ticketUpdatedAtLastCall = newTicketRaw.updated_at
|
||||||
|
|
||||||
|
# make sure to load assets for mentions if cache is not up to date
|
||||||
|
if !_.isEqual(data.mentions, @mentions)
|
||||||
|
loadAssets = true
|
||||||
|
|
||||||
# load assets
|
# load assets
|
||||||
if loadAssets
|
if loadAssets
|
||||||
|
|
||||||
|
@ -180,6 +184,9 @@ class App.TicketZoom extends App.Controller
|
||||||
# remember tags
|
# remember tags
|
||||||
@tags = data.tags
|
@tags = data.tags
|
||||||
|
|
||||||
|
# remember mentions
|
||||||
|
@mentions = data.mentions
|
||||||
|
|
||||||
App.Collection.loadAssets(data.assets, targetModel: 'Ticket')
|
App.Collection.loadAssets(data.assets, targetModel: 'Ticket')
|
||||||
|
|
||||||
# get ticket
|
# get ticket
|
||||||
|
@ -515,6 +522,7 @@ class App.TicketZoom extends App.Controller
|
||||||
formMeta: @formMeta
|
formMeta: @formMeta
|
||||||
markForm: @markForm
|
markForm: @markForm
|
||||||
tags: @tags
|
tags: @tags
|
||||||
|
mentions: @mentions
|
||||||
links: @links
|
links: @links
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -551,6 +559,7 @@ class App.TicketZoom extends App.Controller
|
||||||
if @sidebarWidget
|
if @sidebarWidget
|
||||||
@sidebarWidget.reload(
|
@sidebarWidget.reload(
|
||||||
tags: @tags
|
tags: @tags
|
||||||
|
mentions: @mentions
|
||||||
links: @links
|
links: @links
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -893,6 +902,13 @@ class App.TicketZoom extends App.Controller
|
||||||
# verify if time accounting is active for ticket
|
# verify if time accounting is active for ticket
|
||||||
selector = ticket.clone()
|
selector = ticket.clone()
|
||||||
selector.tags = @tags
|
selector.tags = @tags
|
||||||
|
# always have a empy value to make sure that the condition gets checked
|
||||||
|
selector.mentions = ['']
|
||||||
|
for id in @mentions
|
||||||
|
mention = App.Mention.find(id)
|
||||||
|
continue if !mention
|
||||||
|
selector.mentions.push(mention.user_id)
|
||||||
|
|
||||||
time_accounting_selector = @Config.get('time_accounting_selector')
|
time_accounting_selector = @Config.get('time_accounting_selector')
|
||||||
if !App.Ticket.selector(selector, time_accounting_selector['condition'])
|
if !App.Ticket.selector(selector, time_accounting_selector['condition'])
|
||||||
@submitPost(e, ticket, macro)
|
@submitPost(e, ticket, macro)
|
||||||
|
|
|
@ -34,6 +34,7 @@ class App.TicketZoomSidebar extends App.ControllerObserver
|
||||||
formMeta: @formMeta
|
formMeta: @formMeta
|
||||||
markForm: @markForm
|
markForm: @markForm
|
||||||
tags: @tags
|
tags: @tags
|
||||||
|
mentions: @mentions
|
||||||
links: @links
|
links: @links
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
|
@ -43,6 +44,7 @@ class App.TicketZoomSidebar extends App.ControllerObserver
|
||||||
formMeta: @formMeta
|
formMeta: @formMeta
|
||||||
markForm: @markForm
|
markForm: @markForm
|
||||||
tags: @tags
|
tags: @tags
|
||||||
|
mentions: @mentions
|
||||||
links: @links
|
links: @links
|
||||||
)
|
)
|
||||||
@sidebarItems.push @sidebarBackends[key]
|
@sidebarItems.push @sidebarBackends[key]
|
||||||
|
|
|
@ -102,6 +102,8 @@ class SidebarTicket extends App.Controller
|
||||||
if @tagWidget
|
if @tagWidget
|
||||||
if args.tags
|
if args.tags
|
||||||
@tagWidget.reload(args.tags)
|
@tagWidget.reload(args.tags)
|
||||||
|
if args.mentions
|
||||||
|
@mentionWidget.reload(args.mentions)
|
||||||
if args.tagAdd
|
if args.tagAdd
|
||||||
@tagWidget.add(args.tagAdd, args.source)
|
@tagWidget.add(args.tagAdd, args.source)
|
||||||
if args.tagRemove
|
if args.tagRemove
|
||||||
|
@ -128,6 +130,11 @@ class SidebarTicket extends App.Controller
|
||||||
)
|
)
|
||||||
|
|
||||||
if @ticket.currentView() is 'agent'
|
if @ticket.currentView() is 'agent'
|
||||||
|
@mentionWidget = new App.WidgetMention(
|
||||||
|
el: localEl.filter('.mentions')
|
||||||
|
object: @ticket
|
||||||
|
mentions: @mentions
|
||||||
|
)
|
||||||
@tagWidget = new App.WidgetTag(
|
@tagWidget = new App.WidgetTag(
|
||||||
el: localEl.filter('.tags')
|
el: localEl.filter('.tags')
|
||||||
object_type: 'Ticket'
|
object_type: 'Ticket'
|
||||||
|
|
102
app/assets/javascripts/app/controllers/widget/mention.coffee
Normal file
102
app/assets/javascripts/app/controllers/widget/mention.coffee
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
class App.WidgetMention extends App.Controller
|
||||||
|
events:
|
||||||
|
'click .js-subscribe': 'subscribe'
|
||||||
|
'click .js-unsubscribe': 'unsubscribe'
|
||||||
|
elements:
|
||||||
|
'.js-subscribe input[type=button]': 'subscribeButton'
|
||||||
|
'.js-unsubscribe input[type=button]': 'unsubscribeButton'
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
|
||||||
|
@mentions = []
|
||||||
|
App.Event.bind('Mention:create Mention:destroy',
|
||||||
|
(data) =>
|
||||||
|
return if !data
|
||||||
|
return if data.mentionable_type isnt 'Ticket'
|
||||||
|
return if data.mentionable_id isnt @object.id
|
||||||
|
@fetch()
|
||||||
|
)
|
||||||
|
@render()
|
||||||
|
|
||||||
|
fetch: =>
|
||||||
|
App.Mention.fetchMentionable(
|
||||||
|
'Ticket',
|
||||||
|
@object.id,
|
||||||
|
(data) =>
|
||||||
|
@mentions = data.record_ids
|
||||||
|
App.Collection.loadAssets(data.assets)
|
||||||
|
@render()
|
||||||
|
)
|
||||||
|
|
||||||
|
reload: (mentions) =>
|
||||||
|
@mentions = mentions
|
||||||
|
@render()
|
||||||
|
|
||||||
|
render: =>
|
||||||
|
subscribed = false
|
||||||
|
mentions = []
|
||||||
|
counter = 1
|
||||||
|
for id in @mentions
|
||||||
|
mention = App.Mention.find(id)
|
||||||
|
continue if !mention
|
||||||
|
|
||||||
|
user = App.User.find(mention.user_id)
|
||||||
|
continue if !user
|
||||||
|
|
||||||
|
if mention.user_id is App.Session.get().id
|
||||||
|
subscribed = true
|
||||||
|
|
||||||
|
# no break because we need to check if user is subscribed
|
||||||
|
continue if counter > 10
|
||||||
|
|
||||||
|
mention.avatar = user.avatar('30', '', '')
|
||||||
|
|
||||||
|
mentions.push(mention)
|
||||||
|
counter++
|
||||||
|
|
||||||
|
@html App.view('widget/mention')(
|
||||||
|
subscribed: subscribed
|
||||||
|
mentions: mentions
|
||||||
|
)
|
||||||
|
|
||||||
|
subscribe: (e) =>
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
@subscribeButton.prop('readonly', true)
|
||||||
|
@subscribeButton.prop('disabled', true)
|
||||||
|
|
||||||
|
mention = new App.Mention
|
||||||
|
mention.load(
|
||||||
|
mentionable_type: 'Ticket'
|
||||||
|
mentionable_id: @object.id
|
||||||
|
user_id: App.Session.get().id
|
||||||
|
)
|
||||||
|
mention.save(
|
||||||
|
done: =>
|
||||||
|
@subscribeButton.prop('readonly', false)
|
||||||
|
@subscribeButton.prop('disabled', false)
|
||||||
|
$(e.currentTarget).addClass('hidden')
|
||||||
|
$(e.currentTarget).closest('form').find('.js-unsubscribe').removeClass('hidden')
|
||||||
|
)
|
||||||
|
|
||||||
|
unsubscribe: (e) =>
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
@unsubscribeButton.prop('readonly', true)
|
||||||
|
@unsubscribeButton.prop('disabled', true)
|
||||||
|
|
||||||
|
for id in @mentions
|
||||||
|
mention = App.Mention.find(id)
|
||||||
|
continue if !mention
|
||||||
|
continue if mention.user_id isnt App.Session.get().id
|
||||||
|
|
||||||
|
mention.destroy(
|
||||||
|
done: =>
|
||||||
|
@unsubscribeButton.prop('readonly', false)
|
||||||
|
@unsubscribeButton.prop('disabled', false)
|
||||||
|
$(e.currentTarget).addClass('hidden')
|
||||||
|
$(e.currentTarget).closest('form').find('.js-subscribe').removeClass('hidden')
|
||||||
|
)
|
||||||
|
|
||||||
|
break
|
|
@ -609,6 +609,82 @@
|
||||||
|
|
||||||
KbAnswer.trigger = '??'
|
KbAnswer.trigger = '??'
|
||||||
|
|
||||||
Plugin.prototype.helpers = [Collection, KbAnswer]
|
function Mention() {}
|
||||||
|
|
||||||
|
Mention.renderValue = function(textmodule, elem, callback) {
|
||||||
|
textmodule.emptyResultsContainer()
|
||||||
|
|
||||||
|
var element = $('<li>').text(App.i18n.translateInline('Please wait...'))
|
||||||
|
textmodule.appendResults(element)
|
||||||
|
|
||||||
|
var form_id = textmodule.$element.closest('form').find('[name=form_id]').val()
|
||||||
|
|
||||||
|
var user_id = $(elem).data('id')
|
||||||
|
var user = App.User.find(user_id)
|
||||||
|
if (!user) {
|
||||||
|
callback('')
|
||||||
|
}
|
||||||
|
|
||||||
|
fqdn = App.Config.get('fqdn')
|
||||||
|
http_type = App.Config.get('http_type')
|
||||||
|
|
||||||
|
$replace = $('<a></a>', {
|
||||||
|
href: http_type + '://' + fqdn + '/' + user.uiUrl(),
|
||||||
|
'data-mention-user-id': user_id,
|
||||||
|
text: user.firstname + ' ' + user.lastname
|
||||||
|
})
|
||||||
|
|
||||||
|
callback($replace[0].outerHTML)
|
||||||
|
}
|
||||||
|
|
||||||
|
Mention.renderResults = function(textmodule, term) {
|
||||||
|
textmodule.emptyResultsContainer()
|
||||||
|
|
||||||
|
if(!term) {
|
||||||
|
var element = $('<li>').text(App.i18n.translateInline('Start typing to search for users...'))
|
||||||
|
textmodule.appendResults(element)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var element = $('<li>').text(App.i18n.translateInline('Loading...'))
|
||||||
|
textmodule.appendResults(element)
|
||||||
|
|
||||||
|
App.Delay.set(function() {
|
||||||
|
items = []
|
||||||
|
|
||||||
|
App.Mention.searchUser(term, function(data) {
|
||||||
|
textmodule.emptyResultsContainer()
|
||||||
|
|
||||||
|
activeSet = false
|
||||||
|
$.each(data.user_ids, function(index, user_id) {
|
||||||
|
user = App.User.find(user_id)
|
||||||
|
if (!user) return true
|
||||||
|
if (!user.active) return true
|
||||||
|
|
||||||
|
item = $('<li>', {
|
||||||
|
'data-id': user_id,
|
||||||
|
text: user.firstname + ' ' + user.lastname + ' <' + user.email + '>'
|
||||||
|
})
|
||||||
|
if (!activeSet) {
|
||||||
|
activeSet = true
|
||||||
|
item.addClass('is-active')
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push(item)
|
||||||
|
})
|
||||||
|
|
||||||
|
if(items.length == 0) {
|
||||||
|
items.push($('<li>').text(App.i18n.translateInline('No results found')))
|
||||||
|
}
|
||||||
|
|
||||||
|
textmodule.appendResults(items)
|
||||||
|
})
|
||||||
|
}, 200, 'textmoduleMentionDelay', 'textmodule')
|
||||||
|
}
|
||||||
|
|
||||||
|
Mention.trigger = '@@'
|
||||||
|
|
||||||
|
Plugin.prototype.helpers = [Collection, KbAnswer, Mention]
|
||||||
|
|
||||||
}(jQuery, window));
|
}(jQuery, window));
|
||||||
|
|
41
app/assets/javascripts/app/models/mention.coffee
Normal file
41
app/assets/javascripts/app/models/mention.coffee
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
class App.Mention extends App.Model
|
||||||
|
@configure 'Mention', 'mentionable_id', 'mentionable_type'
|
||||||
|
@extend Spine.Model.Ajax
|
||||||
|
@url: @apiPath + '/mentions'
|
||||||
|
@configure_attributes = [
|
||||||
|
{ name: 'user_id', display: 'User', tag: 'select', multiple: false, limit: 100, null: true, relation: 'User', width: '12%', edit: true },
|
||||||
|
]
|
||||||
|
|
||||||
|
@fetchMentionable: (mentionable_type, mentionable_id, callback) ->
|
||||||
|
App.Ajax.request(
|
||||||
|
type: 'GET'
|
||||||
|
url: "#{@apiPath}/mentions"
|
||||||
|
data:
|
||||||
|
mentionable_type: mentionable_type
|
||||||
|
mentionable_id: mentionable_id
|
||||||
|
full: true
|
||||||
|
processData: true
|
||||||
|
success: (data, status, xhr) ->
|
||||||
|
if data.assets
|
||||||
|
App.Collection.loadAssets(data.assets, targetModel: @className)
|
||||||
|
callback(data)
|
||||||
|
)
|
||||||
|
|
||||||
|
@searchUser: (query, callback) ->
|
||||||
|
roles = App.Role.withPermissions('ticket.agent')
|
||||||
|
role_ids = roles.map (role) -> role.id
|
||||||
|
|
||||||
|
App.Ajax.request(
|
||||||
|
type: 'GET'
|
||||||
|
url: "#{@apiPath}/users/search"
|
||||||
|
data:
|
||||||
|
limit: 10
|
||||||
|
query: query
|
||||||
|
role_ids: role_ids
|
||||||
|
full: true
|
||||||
|
processData: true
|
||||||
|
success: (data, status, xhr) ->
|
||||||
|
if data.assets
|
||||||
|
App.Collection.loadAssets(data.assets, targetModel: @className)
|
||||||
|
callback(data)
|
||||||
|
)
|
|
@ -35,3 +35,21 @@ class App.Role extends App.Model
|
||||||
data['permissions'].push permission
|
data['permissions'].push permission
|
||||||
|
|
||||||
data
|
data
|
||||||
|
|
||||||
|
@withPermissions: (permissions) ->
|
||||||
|
if !_.isArray(permissions)
|
||||||
|
permissions = [permissions]
|
||||||
|
|
||||||
|
roles = []
|
||||||
|
for role in App.Role.all()
|
||||||
|
found = false
|
||||||
|
for permission in permissions
|
||||||
|
id = App.Permission.findByAttribute('name', permission)?.id
|
||||||
|
continue if !id
|
||||||
|
continue if !_.contains(role.permission_ids, id)
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
continue if !found
|
||||||
|
roles.push(role)
|
||||||
|
roles
|
||||||
|
|
||||||
|
|
|
@ -196,6 +196,15 @@ class App.Ticket extends App.Model
|
||||||
objectName = 'ticket'
|
objectName = 'ticket'
|
||||||
attributeName = 'title'
|
attributeName = 'title'
|
||||||
|
|
||||||
|
if objectAttribute == 'ticket.mention_user_ids'
|
||||||
|
if condition['pre_condition'] isnt 'not_set'
|
||||||
|
if condition['pre_condition'] is 'specific'
|
||||||
|
condition.value = parseInt(condition.value)
|
||||||
|
if condition.operator is 'is'
|
||||||
|
condition.operator = 'contains one'
|
||||||
|
else if condition.operator is 'is not'
|
||||||
|
condition.operator = 'contains all not'
|
||||||
|
|
||||||
# for new articles there is no created_by_id so we set the current user
|
# for new articles there is no created_by_id so we set the current user
|
||||||
# if no id is given
|
# if no id is given
|
||||||
if objectAttribute == 'article.created_by_id' && !ticket['article']['created_by_id']
|
if objectAttribute == 'article.created_by_id' && !ticket['article']['created_by_id']
|
||||||
|
|
|
@ -9,38 +9,47 @@
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>
|
<th>
|
||||||
<th width="18%" style="text-align: center;"><%- @T('My Tickets') %>
|
<th width="16%" style="text-align: center;"><%- @T('My Tickets') %>
|
||||||
<th width="18%" style="text-align: center;"><%- @T('Not Assigned') %>*
|
<th width="16%" style="text-align: center;"><%- @T('Not Assigned') %>*
|
||||||
<th width="18%" style="text-align: center;"><%- @T('All Tickets') %>*
|
<th width="16%" style="text-align: center;"><%- @T('Mentioned Tickets') %>
|
||||||
|
<th width="16%" style="text-align: center;"><%- @T('All Tickets') %>*
|
||||||
<th width="120px" class="settings-list-separator" style="text-align: center;"><%- @T('Also notify via email') %>
|
<th width="120px" class="settings-list-separator" style="text-align: center;"><%- @T('Also notify via email') %>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<% if @config.matrix: %>
|
<% if @matrix: %>
|
||||||
<% for key, value of @config.matrix: %>
|
<% for key, value of @matrix: %>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<%- @T(value.name) %>
|
<%- @T(value.name) %>
|
||||||
|
<% criteria = @config.matrix[key]?.criteria %>
|
||||||
|
<% channel = @config.matrix[key]?.channel %>
|
||||||
<td class="u-positionOrigin">
|
<td class="u-positionOrigin">
|
||||||
<label class="checkbox-replacement checkbox-replacement--fullscreen">
|
<label class="checkbox-replacement checkbox-replacement--fullscreen">
|
||||||
<input type="checkbox" name="matrix.<%= key %>.criteria.owned_by_me" value="true" <% if value.criteria && value.criteria.owned_by_me: %>checked<% end %>/>
|
<input type="checkbox" name="matrix.<%= key %>.criteria.owned_by_me" value="true"<% if criteria && criteria.owned_by_me: %> checked<% end %> />
|
||||||
<%- @Icon('checkbox', 'icon-unchecked') %>
|
<%- @Icon('checkbox', 'icon-unchecked') %>
|
||||||
<%- @Icon('checkbox-checked', 'icon-checked') %>
|
<%- @Icon('checkbox-checked', 'icon-checked') %>
|
||||||
</label>
|
</label>
|
||||||
<td class="u-positionOrigin">
|
<td class="u-positionOrigin">
|
||||||
<label class="checkbox-replacement checkbox-replacement--fullscreen">
|
<label class="checkbox-replacement checkbox-replacement--fullscreen">
|
||||||
<input type="checkbox" name="matrix.<%= key %>.criteria.owned_by_nobody" value="true" <% if value.criteria && value.criteria.owned_by_nobody: %>checked<% end %>/>
|
<input type="checkbox" name="matrix.<%= key %>.criteria.owned_by_nobody" value="true"<% if criteria && criteria.owned_by_nobody: %> checked<% end %> />
|
||||||
<%- @Icon('checkbox', 'icon-unchecked') %>
|
<%- @Icon('checkbox', 'icon-unchecked') %>
|
||||||
<%- @Icon('checkbox-checked', 'icon-checked') %>
|
<%- @Icon('checkbox-checked', 'icon-checked') %>
|
||||||
</label>
|
</label>
|
||||||
<td class="u-positionOrigin">
|
<td class="u-positionOrigin">
|
||||||
<label class="checkbox-replacement checkbox-replacement--fullscreen">
|
<label class="checkbox-replacement checkbox-replacement--fullscreen">
|
||||||
<input type="checkbox" name="matrix.<%= key %>.criteria.no" value="true" <% if value.criteria && value.criteria.no: %>checked<% end %>/>
|
<input type="checkbox" name="matrix.<%= key %>.criteria.mentioned" value="true"<% if criteria && criteria.mentioned: %> checked<% end %> />
|
||||||
|
<%- @Icon('checkbox', 'icon-unchecked') %>
|
||||||
|
<%- @Icon('checkbox-checked', 'icon-checked') %>
|
||||||
|
</label>
|
||||||
|
<td class="u-positionOrigin">
|
||||||
|
<label class="checkbox-replacement checkbox-replacement--fullscreen">
|
||||||
|
<input type="checkbox" name="matrix.<%= key %>.criteria.no" value="true"<% if criteria && criteria.no: %> checked<% end %> />
|
||||||
<%- @Icon('checkbox', 'icon-unchecked') %>
|
<%- @Icon('checkbox', 'icon-unchecked') %>
|
||||||
<%- @Icon('checkbox-checked', 'icon-checked') %>
|
<%- @Icon('checkbox-checked', 'icon-checked') %>
|
||||||
</label>
|
</label>
|
||||||
<td class="u-positionOrigin settings-list-separator">
|
<td class="u-positionOrigin settings-list-separator">
|
||||||
<label class="checkbox-replacement checkbox-replacement--fullscreen">
|
<label class="checkbox-replacement checkbox-replacement--fullscreen">
|
||||||
<input type="checkbox" name="matrix.<%= key %>.channel" value="email" <% if value.channel && value.channel.email: %>checked<% end %>/>
|
<input type="checkbox" name="matrix.<%= key %>.channel" value="email"<% if channel && channel.email: %> checked<% end %> />
|
||||||
<%- @Icon('checkbox', 'icon-unchecked') %>
|
<%- @Icon('checkbox', 'icon-unchecked') %>
|
||||||
<%- @Icon('checkbox-checked', 'icon-checked') %>
|
<%- @Icon('checkbox-checked', 'icon-checked') %>
|
||||||
</label>
|
</label>
|
||||||
|
|
|
@ -5,3 +5,4 @@
|
||||||
<div class="links"></div>
|
<div class="links"></div>
|
||||||
<div class="link_kb_answers"></div>
|
<div class="link_kb_answers"></div>
|
||||||
<div class="js-timeUnit"></div>
|
<div class="js-timeUnit"></div>
|
||||||
|
<div class="mentions"></div>
|
||||||
|
|
14
app/assets/javascripts/app/views/widget/mention.jst.eco
Normal file
14
app/assets/javascripts/app/views/widget/mention.jst.eco
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<label><%- @T('Mentions') %></label>
|
||||||
|
<form class="ui-front mentionWidget">
|
||||||
|
<div class="js-subscribe<% if @subscribed: %> hidden<% end %>">
|
||||||
|
<input type="button" class="btn btn--fullWidth" name="subscribe" value="<%- @T('Subscribe') %>">
|
||||||
|
</div>
|
||||||
|
<div class="js-unsubscribe<% if !@subscribed: %> hidden<% end %>">
|
||||||
|
<input type="button" class="btn btn--fullWidth" name="unsubscribe" value="<%- @T('Unsubscribe') %>">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="controls-label">
|
||||||
|
<% for mention in @mentions: %>
|
||||||
|
<a href="#user/profile/<%= mention.user_id %>"><%- mention.avatar %></a>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
|
@ -5613,6 +5613,10 @@ footer {
|
||||||
cursor: help;
|
cursor: help;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notification-icon-help {
|
||||||
|
opacity: .2;
|
||||||
|
}
|
||||||
|
|
||||||
.stat-label {
|
.stat-label {
|
||||||
color: #444a4f;
|
color: #444a4f;
|
||||||
@extend .u-textTruncate;
|
@extend .u-textTruncate;
|
||||||
|
|
100
app/controllers/mentions_controller.rb
Normal file
100
app/controllers/mentions_controller.rb
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
|
class MentionsController < ApplicationController
|
||||||
|
prepend_before_action -> { authorize! }
|
||||||
|
prepend_before_action { authentication_check }
|
||||||
|
|
||||||
|
# GET /api/v1/mentions
|
||||||
|
def list
|
||||||
|
list = Mention.where(condition).order(created_at: :desc)
|
||||||
|
|
||||||
|
if response_full?
|
||||||
|
assets = {}
|
||||||
|
item_ids = []
|
||||||
|
list.each do |item|
|
||||||
|
item_ids.push item.id
|
||||||
|
assets = item.assets(assets)
|
||||||
|
end
|
||||||
|
render json: {
|
||||||
|
record_ids: item_ids,
|
||||||
|
assets: assets,
|
||||||
|
}, status: :ok
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
# return result
|
||||||
|
render json: {
|
||||||
|
mentions: list,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# POST /api/v1/mentions
|
||||||
|
def create
|
||||||
|
success = Mention.create!(
|
||||||
|
mentionable: mentionable!,
|
||||||
|
user: current_user,
|
||||||
|
)
|
||||||
|
if success
|
||||||
|
render json: success, status: :created
|
||||||
|
else
|
||||||
|
render json: success.errors, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# DELETE /api/v1/mentions
|
||||||
|
def destroy
|
||||||
|
success = Mention.find_by(user: current_user, id: params[:id]).destroy
|
||||||
|
if success
|
||||||
|
render json: success, status: :ok
|
||||||
|
else
|
||||||
|
render json: success.errors, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def ensure_mentionable_type!
|
||||||
|
return if ['Ticket'].include?(params[:mentionable_type])
|
||||||
|
|
||||||
|
raise 'Invalid mentionable_type!'
|
||||||
|
end
|
||||||
|
|
||||||
|
def mentionable!
|
||||||
|
ensure_mentionable_type!
|
||||||
|
|
||||||
|
object = params[:mentionable_type].constantize.find(params[:mentionable_id])
|
||||||
|
authorize!(object, :update?)
|
||||||
|
object
|
||||||
|
end
|
||||||
|
|
||||||
|
def fill_condition_mentionable(condition)
|
||||||
|
condition[:mentionable_type] = params[:mentionable_type]
|
||||||
|
return if params[:mentionable_id].blank?
|
||||||
|
|
||||||
|
condition[:mentionable_id] = params[:mentionable_id]
|
||||||
|
end
|
||||||
|
|
||||||
|
def fill_condition_id(condition)
|
||||||
|
return if params[:id].blank?
|
||||||
|
|
||||||
|
condition[:id] = params[:id]
|
||||||
|
end
|
||||||
|
|
||||||
|
def fill_condition_user(condition)
|
||||||
|
return if params[:user_id].blank?
|
||||||
|
|
||||||
|
condition[:user] = User.find(params[:user_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def condition
|
||||||
|
condition = {}
|
||||||
|
fill_condition_id(condition)
|
||||||
|
fill_condition_user(condition)
|
||||||
|
|
||||||
|
return condition if params[:mentionable_type].blank?
|
||||||
|
|
||||||
|
mentionable!
|
||||||
|
fill_condition_mentionable(condition)
|
||||||
|
condition
|
||||||
|
end
|
||||||
|
end
|
|
@ -163,6 +163,14 @@ class TicketsController < ApplicationController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# create mentions if given
|
||||||
|
if params[:mentions].present?
|
||||||
|
authorize!(Mention.new, :create?)
|
||||||
|
Array(params[:mentions]).each do |user_id|
|
||||||
|
Mention.where(mentionable: ticket, user_id: user_id).first_or_create(mentionable: ticket, user_id: user_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# create article if given
|
# create article if given
|
||||||
if params[:article]
|
if params[:article]
|
||||||
article_create(ticket, params[:article])
|
article_create(ticket, params[:article])
|
||||||
|
@ -702,6 +710,12 @@ class TicketsController < ApplicationController
|
||||||
# get tags
|
# get tags
|
||||||
tags = ticket.tag_list
|
tags = ticket.tag_list
|
||||||
|
|
||||||
|
# get mentions
|
||||||
|
mentions = Mention.where(mentionable: ticket).order(created_at: :desc)
|
||||||
|
mentions.each do |mention|
|
||||||
|
assets = mention.assets(assets)
|
||||||
|
end
|
||||||
|
|
||||||
# return result
|
# return result
|
||||||
{
|
{
|
||||||
ticket_id: ticket.id,
|
ticket_id: ticket.id,
|
||||||
|
@ -709,6 +723,7 @@ class TicketsController < ApplicationController
|
||||||
assets: assets,
|
assets: assets,
|
||||||
links: links,
|
links: links,
|
||||||
tags: tags,
|
tags: tags,
|
||||||
|
mentions: mentions.pluck(:id),
|
||||||
form_meta: attributes_to_change[:form_meta],
|
form_meta: attributes_to_change[:form_meta],
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
|
@ -17,12 +17,19 @@ module ChecksClientNotification
|
||||||
{
|
{
|
||||||
message: {
|
message: {
|
||||||
event: "#{class_name}:#{event}",
|
event: "#{class_name}:#{event}",
|
||||||
data: { id: id, updated_at: updated_at }
|
data: notify_clients_data_attributes
|
||||||
},
|
},
|
||||||
type: 'authenticated',
|
type: 'authenticated',
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def notify_clients_data_attributes
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
updated_at: updated_at
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def notify_clients_send(data)
|
def notify_clients_send(data)
|
||||||
return notify_clients_send_to(data[:message]) if client_notification_send_to.present?
|
return notify_clients_send_to(data[:message]) if client_notification_send_to.present?
|
||||||
|
|
||||||
|
@ -104,38 +111,28 @@ module ChecksClientNotification
|
||||||
# methods defined here are going to extend the class, not the instance of it
|
# methods defined here are going to extend the class, not the instance of it
|
||||||
class_methods do
|
class_methods do
|
||||||
|
|
||||||
=begin
|
# serve method to ignore events
|
||||||
|
#
|
||||||
serve method to ignore events
|
# @example
|
||||||
|
# class Model < ApplicationModel
|
||||||
class Model < ApplicationModel
|
# include ChecksClientNotification
|
||||||
include ChecksClientNotification
|
# client_notification_events_ignored :create, :update, :touch
|
||||||
client_notification_events_ignored :create, :update, :touch
|
# end
|
||||||
end
|
|
||||||
|
|
||||||
=end
|
|
||||||
|
|
||||||
def client_notification_events_ignored(*attributes)
|
def client_notification_events_ignored(*attributes)
|
||||||
@client_notification_events_ignored ||= []
|
@client_notification_events_ignored ||= []
|
||||||
@client_notification_events_ignored |= attributes
|
@client_notification_events_ignored |= attributes
|
||||||
end
|
end
|
||||||
|
|
||||||
=begin
|
# serve method to define recipient user ids
|
||||||
|
#
|
||||||
serve method to define recipient user ids
|
# @example
|
||||||
|
# class Model < ApplicationModel
|
||||||
class Model < ApplicationModel
|
# include ChecksClientNotification
|
||||||
include ChecksClientNotification
|
# client_notification_send_to :user_id
|
||||||
client_notification_send_to :user_id
|
# end
|
||||||
end
|
|
||||||
|
|
||||||
=end
|
|
||||||
|
|
||||||
def client_notification_send_to(*attributes)
|
def client_notification_send_to(*attributes)
|
||||||
@client_notification_send_to ||= []
|
@client_notification_send_to ||= []
|
||||||
@client_notification_send_to |= attributes
|
@client_notification_send_to |= attributes
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -204,7 +204,7 @@ returns
|
||||||
=end
|
=end
|
||||||
|
|
||||||
def history_get(fulldata = false)
|
def history_get(fulldata = false)
|
||||||
relation_object = self.class.instance_variable_get(:@history_relation_object) || nil
|
relation_object = history_relation_object
|
||||||
|
|
||||||
if !fulldata
|
if !fulldata
|
||||||
return History.list(self.class.name, self['id'], relation_object)
|
return History.list(self.class.name, self['id'], relation_object)
|
||||||
|
@ -213,12 +213,16 @@ returns
|
||||||
# get related objects
|
# get related objects
|
||||||
history = History.list(self.class.name, self['id'], relation_object, true)
|
history = History.list(self.class.name, self['id'], relation_object, true)
|
||||||
history[:list].each do |item|
|
history[:list].each do |item|
|
||||||
record = item['object'].constantize.find(item['o_id'])
|
record = item['object'].constantize.lookup(id: item['o_id'])
|
||||||
|
|
||||||
|
if record.present?
|
||||||
history[:assets] = record.assets(history[:assets])
|
history[:assets] = record.assets(history[:assets])
|
||||||
|
end
|
||||||
|
|
||||||
if item['related_object']
|
next if !item['related_object']
|
||||||
record = item['related_object'].constantize.find(item['related_o_id'])
|
|
||||||
|
record = item['related_object'].constantize.lookup(id: item['related_o_id'])
|
||||||
|
if record.present?
|
||||||
history[:assets] = record.assets(history[:assets])
|
history[:assets] = record.assets(history[:assets])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -228,6 +232,10 @@ returns
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def history_relation_object
|
||||||
|
@history_relation_object ||= self.class.instance_variable_get(:@history_relation_object) || []
|
||||||
|
end
|
||||||
|
|
||||||
# methods defined here are going to extend the class, not the instance of it
|
# methods defined here are going to extend the class, not the instance of it
|
||||||
class_methods do
|
class_methods do
|
||||||
=begin
|
=begin
|
||||||
|
@ -256,8 +264,9 @@ end
|
||||||
|
|
||||||
=end
|
=end
|
||||||
|
|
||||||
def history_relation_object(attribute)
|
def history_relation_object(*attributes)
|
||||||
@history_relation_object = attribute
|
@history_relation_object ||= []
|
||||||
|
@history_relation_object |= attributes
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -124,7 +124,7 @@ returns
|
||||||
|
|
||||||
return all history entries of an object and it's related history objects
|
return all history entries of an object and it's related history objects
|
||||||
|
|
||||||
history_list = History.list('Ticket', 123, true)
|
history_list = History.list('Ticket', 123, 'Ticket::Article')
|
||||||
|
|
||||||
returns
|
returns
|
||||||
|
|
||||||
|
@ -137,7 +137,7 @@ returns
|
||||||
|
|
||||||
return all history entries of an object and it's assets
|
return all history entries of an object and it's assets
|
||||||
|
|
||||||
history = History.list('Ticket', 123, nil, true)
|
history = History.list('Ticket', 123, nil, ['Ticket::Article'])
|
||||||
|
|
||||||
returns
|
returns
|
||||||
|
|
||||||
|
@ -148,16 +148,21 @@ returns
|
||||||
|
|
||||||
=end
|
=end
|
||||||
|
|
||||||
def self.list(requested_object, requested_object_id, related_history_object = nil, assets = nil)
|
def self.list(requested_object, requested_object_id, related_history_object = [], assets = nil)
|
||||||
histories = History.where(
|
histories = History.where(
|
||||||
history_object_id: object_lookup(requested_object).id,
|
history_object_id: object_lookup(requested_object).id,
|
||||||
o_id: requested_object_id
|
o_id: requested_object_id
|
||||||
)
|
)
|
||||||
|
|
||||||
if related_history_object.present?
|
if related_history_object.present?
|
||||||
|
object_ids = []
|
||||||
|
Array(related_history_object).each do |object|
|
||||||
|
object_ids << object_lookup(object).id
|
||||||
|
end
|
||||||
|
|
||||||
histories = histories.or(
|
histories = histories.or(
|
||||||
History.where(
|
History.where(
|
||||||
history_object_id: object_lookup(related_history_object).id,
|
history_object_id: object_ids,
|
||||||
related_o_id: requested_object_id
|
related_o_id: requested_object_id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
54
app/models/mention.rb
Normal file
54
app/models/mention.rb
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
|
class Mention < ApplicationModel
|
||||||
|
include ChecksClientNotification
|
||||||
|
include HasHistory
|
||||||
|
|
||||||
|
include Mention::Assets
|
||||||
|
|
||||||
|
after_create :update_mentionable
|
||||||
|
after_destroy :update_mentionable
|
||||||
|
|
||||||
|
belongs_to :created_by, class_name: 'User'
|
||||||
|
belongs_to :updated_by, class_name: 'User'
|
||||||
|
belongs_to :user, class_name: 'User'
|
||||||
|
belongs_to :mentionable, polymorphic: true
|
||||||
|
|
||||||
|
association_attributes_ignored :created_by, :updated_by
|
||||||
|
client_notification_events_ignored :update, :touch
|
||||||
|
|
||||||
|
validates_with Mention::Validation
|
||||||
|
|
||||||
|
def notify_clients_data_attributes
|
||||||
|
super.merge(
|
||||||
|
'mentionable_id' => mentionable_id,
|
||||||
|
'mentionable_type' => mentionable_type,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def history_log_attributes
|
||||||
|
{
|
||||||
|
related_o_id: mentionable_id,
|
||||||
|
related_history_object: mentionable_type,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def history_destroy
|
||||||
|
history_log('removed', created_by_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.duplicates(mentionable1, mentionable2)
|
||||||
|
Mention.joins(', mentions as mentionsb').where('
|
||||||
|
mentions.user_id = mentionsb.user_id
|
||||||
|
AND mentions.mentionable_type = ?
|
||||||
|
AND mentions.mentionable_id = ?
|
||||||
|
AND mentionsb.mentionable_type = ?
|
||||||
|
AND mentionsb.mentionable_id = ?
|
||||||
|
', mentionable1.class.to_s, mentionable1.id, mentionable2.class.to_s, mentionable2.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_mentionable
|
||||||
|
mentionable.update(updated_by: updated_by)
|
||||||
|
mentionable.touch # rubocop:disable Rails/SkipsModelValidations
|
||||||
|
end
|
||||||
|
end
|
28
app/models/mention/assets.rb
Normal file
28
app/models/mention/assets.rb
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
|
class Mention
|
||||||
|
module Assets
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
def assets_attributes(data)
|
||||||
|
app_model = self.class.to_app_model
|
||||||
|
|
||||||
|
data[ app_model ] ||= {}
|
||||||
|
return data if data[ app_model ][ id ]
|
||||||
|
|
||||||
|
data[ app_model ][ id ] = attributes_with_association_ids
|
||||||
|
|
||||||
|
data
|
||||||
|
end
|
||||||
|
|
||||||
|
def assets(data)
|
||||||
|
assets_attributes(data)
|
||||||
|
|
||||||
|
if mentionable.present?
|
||||||
|
data = mentionable.assets(data)
|
||||||
|
end
|
||||||
|
|
||||||
|
user.assets(data)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
21
app/models/mention/validation.rb
Normal file
21
app/models/mention/validation.rb
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
class Mention::Validation < ActiveModel::Validator
|
||||||
|
attr_reader :record
|
||||||
|
|
||||||
|
def validate(record)
|
||||||
|
@record = record
|
||||||
|
check_user_permission
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def check_user_permission
|
||||||
|
return if MentionPolicy.new(record.user, record).create?
|
||||||
|
|
||||||
|
invalid_because(:user, 'has no ticket.agent permissions')
|
||||||
|
end
|
||||||
|
|
||||||
|
def invalid_because(attribute, message)
|
||||||
|
record.errors.add attribute, message
|
||||||
|
end
|
||||||
|
end
|
|
@ -66,7 +66,7 @@ class Ticket < ApplicationModel
|
||||||
:article_count,
|
:article_count,
|
||||||
:preferences
|
:preferences
|
||||||
|
|
||||||
history_relation_object 'Ticket::Article'
|
history_relation_object 'Ticket::Article', 'Mention'
|
||||||
|
|
||||||
sanitized_html :note
|
sanitized_html :note
|
||||||
|
|
||||||
|
@ -75,6 +75,7 @@ class Ticket < ApplicationModel
|
||||||
has_many :articles, class_name: 'Ticket::Article', after_add: :cache_update, after_remove: :cache_update, dependent: :destroy, inverse_of: :ticket
|
has_many :articles, class_name: 'Ticket::Article', after_add: :cache_update, after_remove: :cache_update, dependent: :destroy, inverse_of: :ticket
|
||||||
has_many :ticket_time_accounting, class_name: 'Ticket::TimeAccounting', dependent: :destroy, inverse_of: :ticket
|
has_many :ticket_time_accounting, class_name: 'Ticket::TimeAccounting', dependent: :destroy, inverse_of: :ticket
|
||||||
has_many :flags, class_name: 'Ticket::Flag', dependent: :destroy
|
has_many :flags, class_name: 'Ticket::Flag', dependent: :destroy
|
||||||
|
has_many :mentions, as: :mentionable, dependent: :destroy
|
||||||
belongs_to :state, class_name: 'Ticket::State', optional: true
|
belongs_to :state, class_name: 'Ticket::State', optional: true
|
||||||
belongs_to :priority, class_name: 'Ticket::Priority', optional: true
|
belongs_to :priority, class_name: 'Ticket::Priority', optional: true
|
||||||
belongs_to :owner, class_name: 'User', optional: true
|
belongs_to :owner, class_name: 'User', optional: true
|
||||||
|
@ -84,7 +85,7 @@ class Ticket < ApplicationModel
|
||||||
belongs_to :create_article_type, class_name: 'Ticket::Article::Type', optional: true
|
belongs_to :create_article_type, class_name: 'Ticket::Article::Type', optional: true
|
||||||
belongs_to :create_article_sender, class_name: 'Ticket::Article::Sender', optional: true
|
belongs_to :create_article_sender, class_name: 'Ticket::Article::Sender', optional: true
|
||||||
|
|
||||||
association_attributes_ignored :flags
|
association_attributes_ignored :flags, :mentions
|
||||||
|
|
||||||
self.inheritance_column = nil
|
self.inheritance_column = nil
|
||||||
|
|
||||||
|
@ -364,6 +365,10 @@ returns
|
||||||
updated_by_id: data[:user_id],
|
updated_by_id: data[:user_id],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# search for mention duplicates and destroy them before moving mentions
|
||||||
|
Mention.duplicates(self, target_ticket).destroy_all
|
||||||
|
Mention.where(mentionable: self).update_all(mentionable_id: target_ticket.id) # rubocop:disable Rails/SkipsModelValidations
|
||||||
|
|
||||||
# reassign links to the new ticket
|
# reassign links to the new ticket
|
||||||
# rubocop:disable Rails/SkipsModelValidations
|
# rubocop:disable Rails/SkipsModelValidations
|
||||||
ticket_source_id = Link::Object.find_by(name: 'Ticket').id
|
ticket_source_id = Link::Object.find_by(name: 'Ticket').id
|
||||||
|
@ -574,17 +579,19 @@ condition example
|
||||||
|
|
||||||
# get tables to join
|
# get tables to join
|
||||||
tables = ''
|
tables = ''
|
||||||
selectors.each_key do |attribute|
|
selectors.each do |attribute, selector_raw|
|
||||||
selector = attribute.split('.')
|
attributes = attribute.split('.')
|
||||||
next if !selector[1]
|
selector = selector_raw.stringify_keys
|
||||||
next if selector[0] == 'ticket'
|
next if !attributes[1]
|
||||||
next if selector[0] == 'execution_time'
|
next if attributes[0] == 'execution_time'
|
||||||
next if tables.include?(selector[0])
|
next if tables.include?(attributes[0])
|
||||||
|
next if attributes[0] == 'ticket' && attributes[1] != 'mention_user_ids'
|
||||||
|
next if attributes[0] == 'ticket' && attributes[1] == 'mention_user_ids' && selector['pre_condition'] == 'not_set'
|
||||||
|
|
||||||
if query != ''
|
if query != ''
|
||||||
query += ' AND '
|
query += ' AND '
|
||||||
end
|
end
|
||||||
case selector[0]
|
case attributes[0]
|
||||||
when 'customer'
|
when 'customer'
|
||||||
tables += ', users customers'
|
tables += ', users customers'
|
||||||
query += 'tickets.customer_id = customers.id'
|
query += 'tickets.customer_id = customers.id'
|
||||||
|
@ -600,8 +607,13 @@ condition example
|
||||||
when 'ticket_state'
|
when 'ticket_state'
|
||||||
tables += ', ticket_states'
|
tables += ', ticket_states'
|
||||||
query += 'tickets.state_id = ticket_states.id'
|
query += 'tickets.state_id = ticket_states.id'
|
||||||
|
when 'ticket'
|
||||||
|
if attributes[1] == 'mention_user_ids'
|
||||||
|
tables += ', mentions'
|
||||||
|
query += "tickets.id = mentions.mentionable_id AND mentions.mentionable_type = 'Ticket'"
|
||||||
|
end
|
||||||
else
|
else
|
||||||
raise "invalid selector #{attribute.inspect}->#{selector.inspect}"
|
raise "invalid selector #{attribute.inspect}->#{attributes.inspect}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -662,6 +674,29 @@ condition example
|
||||||
query += ' AND '
|
query += ' AND '
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# because of no grouping support we select not_set by sub select for mentions
|
||||||
|
if attributes[0] == 'ticket' && attributes[1] == 'mention_user_ids'
|
||||||
|
if selector['pre_condition'] == 'not_set'
|
||||||
|
query += if selector['operator'] == 'is'
|
||||||
|
"(SELECT 1 FROM mentions mentions_sub WHERE mentions_sub.mentionable_type = 'Ticket' AND mentions_sub.mentionable_id = tickets.id) IS NULL"
|
||||||
|
else
|
||||||
|
"1 = (SELECT 1 FROM mentions mentions_sub WHERE mentions_sub.mentionable_type = 'Ticket' AND mentions_sub.mentionable_id = tickets.id)"
|
||||||
|
end
|
||||||
|
else
|
||||||
|
query += if selector['operator'] == 'is'
|
||||||
|
'mentions.user_id IN (?)'
|
||||||
|
else
|
||||||
|
'mentions.user_id NOT IN (?)'
|
||||||
|
end
|
||||||
|
if selector['pre_condition'] == 'current_user.id'
|
||||||
|
bind_params.push current_user_id
|
||||||
|
else
|
||||||
|
bind_params.push selector['value']
|
||||||
|
end
|
||||||
|
end
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
if selector['operator'] == 'is'
|
if selector['operator'] == 'is'
|
||||||
if selector['pre_condition'] == 'not_set'
|
if selector['pre_condition'] == 'not_set'
|
||||||
if attributes[1].match?(/^(created_by|updated_by|owner|customer|user)_id/)
|
if attributes[1].match?(/^(created_by|updated_by|owner|customer|user)_id/)
|
||||||
|
|
|
@ -31,6 +31,7 @@ class Ticket::Article < ApplicationModel
|
||||||
belongs_to :updated_by, class_name: 'User', optional: true
|
belongs_to :updated_by, class_name: 'User', optional: true
|
||||||
belongs_to :origin_by, class_name: 'User', optional: true
|
belongs_to :origin_by, class_name: 'User', optional: true
|
||||||
|
|
||||||
|
before_validation :check_mentions, on: :create
|
||||||
before_save :touch_ticket_if_needed
|
before_save :touch_ticket_if_needed
|
||||||
before_create :check_subject, :check_body, :check_message_id_md5
|
before_create :check_subject, :check_body, :check_message_id_md5
|
||||||
before_update :check_subject, :check_body, :check_message_id_md5
|
before_update :check_subject, :check_body, :check_message_id_md5
|
||||||
|
@ -323,6 +324,26 @@ returns
|
||||||
self.body = body[0, limit]
|
self.body = body[0, limit]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def check_mentions
|
||||||
|
begin
|
||||||
|
mention_user_ids = Nokogiri::HTML(body).css('a[data-mention-user-id]').map do |link|
|
||||||
|
link['data-mention-user-id']
|
||||||
|
end
|
||||||
|
rescue => e
|
||||||
|
Rails.logger.error "Can't parse body '#{body}' as HTML for extracting Mentions."
|
||||||
|
Rails.logger.error e
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
return if mention_user_ids.blank?
|
||||||
|
raise "User #{updated_by_id} has no permission to mention other Users!" if !MentionPolicy.new(updated_by, Mention.new).create?
|
||||||
|
|
||||||
|
user_ids = User.where(id: mention_user_ids).pluck(:id)
|
||||||
|
user_ids.each do |user_id|
|
||||||
|
Mention.where(mentionable: ticket, user_id: user_id).first_or_create(mentionable: ticket, user_id: user_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def history_log_attributes
|
def history_log_attributes
|
||||||
{
|
{
|
||||||
related_o_id: self['ticket_id'],
|
related_o_id: self['ticket_id'],
|
||||||
|
|
|
@ -8,10 +8,10 @@ module Ticket::SearchIndex
|
||||||
|
|
||||||
# collect article data
|
# collect article data
|
||||||
# add tags
|
# add tags
|
||||||
tags = tag_list
|
attributes['tags'] = tag_list
|
||||||
if tags.present?
|
|
||||||
attributes[:tags] = tags
|
# mentions
|
||||||
end
|
attributes['mention_user_ids'] = mentions.pluck(:user_id)
|
||||||
|
|
||||||
# current payload size
|
# current payload size
|
||||||
total_size_current = 0
|
total_size_current = 0
|
||||||
|
|
|
@ -50,9 +50,22 @@ class Transaction::Notification
|
||||||
recipients_and_channels = []
|
recipients_and_channels = []
|
||||||
recipients_reason = {}
|
recipients_reason = {}
|
||||||
|
|
||||||
# loop through all users
|
# loop through all group users
|
||||||
possible_recipients = possible_recipients_of_group(ticket.group_id)
|
possible_recipients = possible_recipients_of_group(ticket.group_id)
|
||||||
|
|
||||||
|
# loop through all mention users
|
||||||
|
mention_users = Mention.where(mentionable_type: @item[:object], mentionable_id: @item[:object_id]).map(&:user)
|
||||||
|
if mention_users.present?
|
||||||
|
|
||||||
|
# only notify if read permission on group are given
|
||||||
|
mention_users.each do |mention_user|
|
||||||
|
next if !mention_user.group_access?(ticket.group_id, 'read')
|
||||||
|
|
||||||
|
possible_recipients.push mention_user
|
||||||
|
recipients_reason[mention_user.id] = 'are mentioned'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# apply owner
|
# apply owner
|
||||||
if ticket.owner_id != 1
|
if ticket.owner_id != 1
|
||||||
possible_recipients.push ticket.owner
|
possible_recipients.push ticket.owner
|
||||||
|
|
|
@ -34,6 +34,7 @@ class User < ApplicationModel
|
||||||
has_one :chat_agent_updated_by, class_name: 'Chat::Agent', foreign_key: :updated_by_id, dependent: :destroy, inverse_of: :updated_by
|
has_one :chat_agent_updated_by, class_name: 'Chat::Agent', foreign_key: :updated_by_id, dependent: :destroy, inverse_of: :updated_by
|
||||||
has_many :chat_sessions, class_name: 'Chat::Session', dependent: :destroy
|
has_many :chat_sessions, class_name: 'Chat::Session', dependent: :destroy
|
||||||
has_many :karma_user, class_name: 'Karma::User', dependent: :destroy
|
has_many :karma_user, class_name: 'Karma::User', dependent: :destroy
|
||||||
|
has_many :mentions, dependent: :destroy
|
||||||
has_many :karma_activity_logs, class_name: 'Karma::ActivityLog', dependent: :destroy
|
has_many :karma_activity_logs, class_name: 'Karma::ActivityLog', dependent: :destroy
|
||||||
has_many :cti_caller_ids, class_name: 'Cti::CallerId', dependent: :destroy
|
has_many :cti_caller_ids, class_name: 'Cti::CallerId', dependent: :destroy
|
||||||
has_many :customer_tickets, class_name: 'Ticket', foreign_key: :customer_id, dependent: :destroy, inverse_of: :customer
|
has_many :customer_tickets, class_name: 'Ticket', foreign_key: :customer_id, dependent: :destroy, inverse_of: :customer
|
||||||
|
@ -54,7 +55,7 @@ class User < ApplicationModel
|
||||||
|
|
||||||
store :preferences
|
store :preferences
|
||||||
|
|
||||||
association_attributes_ignored :online_notifications, :templates, :taskbars, :user_devices, :chat_sessions, :karma_activity_logs, :cti_caller_ids, :text_modules, :customer_tickets, :owner_tickets, :created_recent_views, :chat_agents, :data_privacy_tasks, :overviews
|
association_attributes_ignored :online_notifications, :templates, :taskbars, :user_devices, :chat_sessions, :karma_activity_logs, :cti_caller_ids, :text_modules, :customer_tickets, :owner_tickets, :created_recent_views, :chat_agents, :data_privacy_tasks, :overviews, :mentions
|
||||||
|
|
||||||
activity_stream_permission 'admin.user'
|
activity_stream_permission 'admin.user'
|
||||||
|
|
||||||
|
|
3
app/policies/controllers/mentions_controller_policy.rb
Normal file
3
app/policies/controllers/mentions_controller_policy.rb
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
class Controllers::MentionsControllerPolicy < Controllers::ApplicationControllerPolicy
|
||||||
|
default_permit!('ticket.agent')
|
||||||
|
end
|
5
app/policies/mention_policy.rb
Normal file
5
app/policies/mention_policy.rb
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
class MentionPolicy < ApplicationPolicy
|
||||||
|
def create?
|
||||||
|
user.permissions?('ticket.agent')
|
||||||
|
end
|
||||||
|
end
|
|
@ -25,5 +25,5 @@ Ticket (#{ticket.title}) byl aktualizován uživatelem "<b>#{current_user.longna
|
||||||
<% end %>
|
<% end %>
|
||||||
<br>
|
<br>
|
||||||
<div>
|
<div>
|
||||||
<a href="#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}" target="zammad_app">#{t('View this in Zammad')}</a>
|
<a href="#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}<% if @objects[:article] %>/#{article.id}<% end %>" target="zammad_app">#{t('View this in Zammad')}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -25,5 +25,5 @@ Ticket (#{ticket.title}) wurde von "<b>#{current_user.longname}</b>" aktualisier
|
||||||
<% end %>
|
<% end %>
|
||||||
<br>
|
<br>
|
||||||
<div>
|
<div>
|
||||||
<a href="#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}" target="zammad_app">#{t('View this in Zammad')}</a>
|
<a href="#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}<% if @objects[:article] %>/#{article.id}<% end %>" target="zammad_app">#{t('View this in Zammad')}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -25,5 +25,5 @@ Ticket (#{ticket.title}) has been updated by "<b>#{current_user.longname}</b>".
|
||||||
<% end %>
|
<% end %>
|
||||||
<br>
|
<br>
|
||||||
<div>
|
<div>
|
||||||
<a href="#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}" target="zammad_app">#{t('View this in Zammad')}</a>
|
<a href="#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}<% if @objects[:article] %>/#{article.id}<% end %>" target="zammad_app">#{t('View this in Zammad')}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -25,5 +25,5 @@ Ticket (#{ticket.title}) ha sido actualizado por "<b>#{current_user.longname}</b
|
||||||
<% end %>
|
<% end %>
|
||||||
<br>
|
<br>
|
||||||
<div>
|
<div>
|
||||||
<a href="#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}" target="zammad_app">#{t('View this in Zammad')}</a>
|
<a href="#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}<% if @objects[:article] %>/#{article.id}<% end %>" target="zammad_app">#{t('View this in Zammad')}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -25,5 +25,5 @@ Le ticket (#{ticket.title}) a été mis à jour par "<b>#{current_user.longname}
|
||||||
<% end %>
|
<% end %>
|
||||||
<br>
|
<br>
|
||||||
<div>
|
<div>
|
||||||
<a href="#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}" target="zammad_app">#{t('View this in Zammad')}</a>
|
<a href="#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}<% if @objects[:article] %>/#{article.id}<% end %>" target="zammad_app">#{t('View this in Zammad')}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -25,5 +25,5 @@ Il ticket (#{ticket.title}) è stato aggiornato da "<b>#{current_user.longname}<
|
||||||
<% end %>
|
<% end %>
|
||||||
<br>
|
<br>
|
||||||
<div>
|
<div>
|
||||||
<a href="#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}" target="zammad_app">#{t('View this in Zammad')}</a>
|
<a href="#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}<% if @objects[:article] %>/#{article.id}<% end %>" target="zammad_app">#{t('View this in Zammad')}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -25,5 +25,5 @@ O chamado (#{ticket.title}) foi atualizado por "<b>#{current_user.longname}</b>"
|
||||||
<% end %>
|
<% end %>
|
||||||
<br>
|
<br>
|
||||||
<div>
|
<div>
|
||||||
<a href="#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}" target="zammad_app">#{t('Veja mais informações no Zammad')}</a>
|
<a href="#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}<% if @objects[:article] %>/#{article.id}<% end %>" target="zammad_app">#{t('Veja mais informações no Zammad')}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -25,5 +25,5 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
<br>
|
<br>
|
||||||
<div>
|
<div>
|
||||||
<a href="#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}" target="zammad_app">#{t('View this in Zammad')}</a>
|
<a href="#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}<% if @objects[:article] %>/#{article.id}<% end %>" target="zammad_app">#{t('View this in Zammad')}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -25,5 +25,5 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
<br>
|
<br>
|
||||||
<div>
|
<div>
|
||||||
<a href="#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}" target="zammad_app">#{t('View this in Zammad')}</a>
|
<a href="#{config.http_type}://#{config.fqdn}/#ticket/zoom/#{ticket.id}<% if @objects[:article] %>/#{article.id}<% end %>" target="zammad_app">#{t('View this in Zammad')}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -49,6 +49,7 @@ module Zammad
|
||||||
criteria: {
|
criteria: {
|
||||||
owned_by_me: true,
|
owned_by_me: true,
|
||||||
owned_by_nobody: true,
|
owned_by_nobody: true,
|
||||||
|
mentioned: true,
|
||||||
no: false,
|
no: false,
|
||||||
},
|
},
|
||||||
channel: {
|
channel: {
|
||||||
|
@ -60,6 +61,7 @@ module Zammad
|
||||||
criteria: {
|
criteria: {
|
||||||
owned_by_me: true,
|
owned_by_me: true,
|
||||||
owned_by_nobody: true,
|
owned_by_nobody: true,
|
||||||
|
mentioned: true,
|
||||||
no: false,
|
no: false,
|
||||||
},
|
},
|
||||||
channel: {
|
channel: {
|
||||||
|
@ -71,6 +73,7 @@ module Zammad
|
||||||
criteria: {
|
criteria: {
|
||||||
owned_by_me: true,
|
owned_by_me: true,
|
||||||
owned_by_nobody: false,
|
owned_by_nobody: false,
|
||||||
|
mentioned: false,
|
||||||
no: false,
|
no: false,
|
||||||
},
|
},
|
||||||
channel: {
|
channel: {
|
||||||
|
@ -82,6 +85,7 @@ module Zammad
|
||||||
criteria: {
|
criteria: {
|
||||||
owned_by_me: true,
|
owned_by_me: true,
|
||||||
owned_by_nobody: false,
|
owned_by_nobody: false,
|
||||||
|
mentioned: false,
|
||||||
no: false,
|
no: false,
|
||||||
},
|
},
|
||||||
channel: {
|
channel: {
|
||||||
|
|
|
@ -26,7 +26,7 @@ Rails.application.config.html_sanitizer_tags_whitelist = %w[
|
||||||
# attributes allowed for tags
|
# attributes allowed for tags
|
||||||
Rails.application.config.html_sanitizer_attributes_whitelist = {
|
Rails.application.config.html_sanitizer_attributes_whitelist = {
|
||||||
:all => %w[class dir lang title translate data-signature data-signature-id],
|
:all => %w[class dir lang title translate data-signature data-signature-id],
|
||||||
'a' => %w[href hreflang name rel data-target-id data-target-type],
|
'a' => %w[href hreflang name rel data-target-id data-target-type data-mention-user-id],
|
||||||
'abbr' => %w[title],
|
'abbr' => %w[title],
|
||||||
'blockquote' => %w[type cite],
|
'blockquote' => %w[type cite],
|
||||||
'col' => %w[span width],
|
'col' => %w[span width],
|
||||||
|
|
7
config/routes/mention.rb
Normal file
7
config/routes/mention.rb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
Zammad::Application.routes.draw do
|
||||||
|
api_path = Rails.configuration.api_path
|
||||||
|
|
||||||
|
match api_path + '/mentions', to: 'mentions#list', via: :get
|
||||||
|
match api_path + '/mentions', to: 'mentions#create', via: :post
|
||||||
|
match api_path + '/mentions/:id', to: 'mentions#destroy', via: :delete
|
||||||
|
end
|
|
@ -747,5 +747,17 @@ class CreateBase < ActiveRecord::Migration[4.2]
|
||||||
t.timestamps limit: 3, null: false
|
t.timestamps limit: 3, null: false
|
||||||
end
|
end
|
||||||
add_index :data_privacy_tasks, [:state]
|
add_index :data_privacy_tasks, [:state]
|
||||||
|
|
||||||
|
create_table :mentions do |t|
|
||||||
|
t.references :mentionable, polymorphic: true, null: false
|
||||||
|
t.column :user_id, :integer, null: false
|
||||||
|
t.column :updated_by_id, :integer, null: false
|
||||||
|
t.column :created_by_id, :integer, null: false
|
||||||
|
t.timestamps limit: 3, null: false
|
||||||
|
end
|
||||||
|
add_index :mentions, %i[mentionable_id mentionable_type user_id], unique: true, name: 'index_mentions_mentionable_user'
|
||||||
|
add_foreign_key :mentions, :users, column: :created_by_id
|
||||||
|
add_foreign_key :mentions, :users, column: :updated_by_id
|
||||||
|
add_foreign_key :mentions, :users, column: :user_id
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
66
db/migrate/20201110000001_mention_init.rb
Normal file
66
db/migrate/20201110000001_mention_init.rb
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
class MentionInit < ActiveRecord::Migration[5.2]
|
||||||
|
def change
|
||||||
|
|
||||||
|
# return if it's a new setup
|
||||||
|
return if !Setting.exists?(name: 'system_init_done')
|
||||||
|
|
||||||
|
create_table :mentions do |t|
|
||||||
|
t.references :mentionable, polymorphic: true, null: false
|
||||||
|
t.column :user_id, :integer, null: false
|
||||||
|
t.column :updated_by_id, :integer, null: false
|
||||||
|
t.column :created_by_id, :integer, null: false
|
||||||
|
t.timestamps limit: 3, null: false
|
||||||
|
end
|
||||||
|
add_index :mentions, %i[mentionable_id mentionable_type user_id], unique: true, name: 'index_mentions_mentionable_user'
|
||||||
|
add_foreign_key :mentions, :users, column: :created_by_id
|
||||||
|
add_foreign_key :mentions, :users, column: :updated_by_id
|
||||||
|
add_foreign_key :mentions, :users, column: :user_id
|
||||||
|
|
||||||
|
Mention.reset_column_information
|
||||||
|
create_overview
|
||||||
|
update_user_matrix
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_overview
|
||||||
|
Overview.create_if_not_exists(
|
||||||
|
name: 'My mentioned Tickets',
|
||||||
|
link: 'my_mentioned_tickets',
|
||||||
|
prio: 1025,
|
||||||
|
role_ids: Role.with_permissions('ticket.agent').pluck(:id),
|
||||||
|
condition: { 'ticket.mention_user_ids'=>{ 'operator' => 'is', 'pre_condition' => 'current_user.id', 'value' => '', 'value_completion' => '' } },
|
||||||
|
order: {
|
||||||
|
by: 'created_at',
|
||||||
|
direction: 'ASC',
|
||||||
|
},
|
||||||
|
view: {
|
||||||
|
d: %w[title customer group created_at],
|
||||||
|
s: %w[title customer group created_at],
|
||||||
|
m: %w[number title customer group created_at],
|
||||||
|
view_mode_default: 's',
|
||||||
|
},
|
||||||
|
created_by_id: 1,
|
||||||
|
updated_by_id: 1,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_user_matrix
|
||||||
|
User.with_permissions('ticket.agent').each do |user|
|
||||||
|
next if user.preferences.blank?
|
||||||
|
next if user.preferences['notification_config'].blank?
|
||||||
|
next if user.preferences['notification_config']['matrix'].blank?
|
||||||
|
|
||||||
|
update_user_matrix_by_user(user)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_user_matrix_by_user(user)
|
||||||
|
%w[create update].each do |type|
|
||||||
|
user.preferences['notification_config']['matrix'][type]['criteria']['mentioned'] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
%w[reminder_reached escalation].each do |type|
|
||||||
|
user.preferences['notification_config']['matrix'][type]['criteria']['mentioned'] = false
|
||||||
|
end
|
||||||
|
user.save!
|
||||||
|
end
|
||||||
|
end
|
|
@ -85,6 +85,24 @@ Overview.create_if_not_exists(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Overview.create_if_not_exists(
|
||||||
|
name: 'My mentioned Tickets',
|
||||||
|
link: 'my_mentioned_tickets',
|
||||||
|
prio: 1025,
|
||||||
|
role_ids: [overview_role.id],
|
||||||
|
condition: { 'ticket.mention_user_ids'=>{ 'operator' => 'is', 'pre_condition' => 'current_user.id', 'value' => '', 'value_completion' => '' } },
|
||||||
|
order: {
|
||||||
|
by: 'created_at',
|
||||||
|
direction: 'ASC',
|
||||||
|
},
|
||||||
|
view: {
|
||||||
|
d: %w[title customer group created_at],
|
||||||
|
s: %w[title customer group created_at],
|
||||||
|
m: %w[number title customer group created_at],
|
||||||
|
view_mode_default: 's',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
Overview.create_if_not_exists(
|
Overview.create_if_not_exists(
|
||||||
name: 'Open',
|
name: 'Open',
|
||||||
link: 'all_open',
|
link: 'all_open',
|
||||||
|
|
|
@ -14,6 +14,8 @@ satinize html string based on whiltelist
|
||||||
def self.strict(string, external = false, timeout: true)
|
def self.strict(string, external = false, timeout: true)
|
||||||
Timeout.timeout(timeout ? PROCESSING_TIMEOUT : nil) do
|
Timeout.timeout(timeout ? PROCESSING_TIMEOUT : nil) do
|
||||||
@fqdn = Setting.get('fqdn')
|
@fqdn = Setting.get('fqdn')
|
||||||
|
http_type = Setting.get('http_type')
|
||||||
|
web_app_url_prefix = "#{http_type}://#{@fqdn}/\#".downcase
|
||||||
|
|
||||||
# config
|
# config
|
||||||
tags_remove_content = Rails.configuration.html_sanitizer_tags_remove_content
|
tags_remove_content = Rails.configuration.html_sanitizer_tags_remove_content
|
||||||
|
@ -179,8 +181,12 @@ satinize html string based on whiltelist
|
||||||
|
|
||||||
node.set_attribute('href', href)
|
node.set_attribute('href', href)
|
||||||
node.set_attribute('rel', 'nofollow noreferrer noopener')
|
node.set_attribute('rel', 'nofollow noreferrer noopener')
|
||||||
|
|
||||||
|
# do not "target=_blank" WebApp URLs (e.g. mentions)
|
||||||
|
if !href.downcase.start_with?(web_app_url_prefix)
|
||||||
node.set_attribute('target', '_blank')
|
node.set_attribute('target', '_blank')
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
if node.name == 'a' && node['href'].blank?
|
if node.name == 'a' && node['href'].blank?
|
||||||
node.replace node.children.to_s
|
node.replace node.children.to_s
|
||||||
|
|
|
@ -48,6 +48,7 @@ returns
|
||||||
|
|
||||||
owned_by_nobody = false
|
owned_by_nobody = false
|
||||||
owned_by_me = false
|
owned_by_me = false
|
||||||
|
mentioned = false
|
||||||
case ticket.owner_id
|
case ticket.owner_id
|
||||||
when 1
|
when 1
|
||||||
owned_by_nobody = true
|
owned_by_nobody = true
|
||||||
|
@ -69,6 +70,11 @@ returns
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# always trigger notifications for user if he is mentioned
|
||||||
|
if owned_by_me == false && ticket.mentions.exists?(user: user)
|
||||||
|
mentioned = true
|
||||||
|
end
|
||||||
|
|
||||||
# check if group is in selected groups
|
# check if group is in selected groups
|
||||||
if !owned_by_me
|
if !owned_by_me
|
||||||
selected_group_ids = user_preferences['notification_config']['group_ids']
|
selected_group_ids = user_preferences['notification_config']['group_ids']
|
||||||
|
@ -109,6 +115,12 @@ returns
|
||||||
channels: channels
|
channels: channels
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
if data['criteria']['mentioned'] && mentioned
|
||||||
|
return {
|
||||||
|
user: user,
|
||||||
|
channels: channels
|
||||||
|
}
|
||||||
|
end
|
||||||
return if !data['criteria']['no']
|
return if !data['criteria']['no']
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
|
@ -489,7 +489,10 @@ example for aggregations within one year
|
||||||
minute: 'm',
|
minute: 'm',
|
||||||
}
|
}
|
||||||
if selector.present?
|
if selector.present?
|
||||||
|
operators_is_isnot = ['is', 'is not']
|
||||||
|
|
||||||
selector.each do |key, data|
|
selector.each do |key, data|
|
||||||
|
|
||||||
data = data.clone
|
data = data.clone
|
||||||
table, key_tmp = key.split('.')
|
table, key_tmp = key.split('.')
|
||||||
if key_tmp.blank?
|
if key_tmp.blank?
|
||||||
|
@ -510,8 +513,6 @@ example for aggregations within one year
|
||||||
when 'not_set'
|
when 'not_set'
|
||||||
data['value'] = if key_tmp.match?(/^(created_by|updated_by|owner|customer|user)_id/)
|
data['value'] = if key_tmp.match?(/^(created_by|updated_by|owner|customer|user)_id/)
|
||||||
1
|
1
|
||||||
else
|
|
||||||
'NULL'
|
|
||||||
end
|
end
|
||||||
when 'current_user.id'
|
when 'current_user.id'
|
||||||
raise "Use current_user.id in selector, but no current_user is set #{data.inspect}" if !current_user_id
|
raise "Use current_user.id in selector, but no current_user is set #{data.inspect}" if !current_user_id
|
||||||
|
@ -562,6 +563,22 @@ example for aggregations within one year
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# for pre condition not_set we want to check if values are defined for the object by exists
|
||||||
|
if data['pre_condition'] == 'not_set' && operators_is_isnot.include?(data['operator']) && data['value'].nil?
|
||||||
|
t['exists'] = {
|
||||||
|
field: key_tmp,
|
||||||
|
}
|
||||||
|
|
||||||
|
case data['operator']
|
||||||
|
when 'is'
|
||||||
|
query_must_not.push t
|
||||||
|
when 'is not'
|
||||||
|
query_must.push t
|
||||||
|
end
|
||||||
|
next
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
if table != 'ticket'
|
if table != 'ticket'
|
||||||
key_tmp = "#{table}.#{key_tmp}"
|
key_tmp = "#{table}.#{key_tmp}"
|
||||||
end
|
end
|
||||||
|
|
|
@ -131,6 +131,7 @@ window.onload = function() {
|
||||||
"id": 434
|
"id": 434
|
||||||
},
|
},
|
||||||
"tags": ["tag a", "tag b"],
|
"tags": ["tag a", "tag b"],
|
||||||
|
"mention_user_ids": [1,3,5,6],
|
||||||
"escalation_at": "2017-02-09T09:16:56.192Z",
|
"escalation_at": "2017-02-09T09:16:56.192Z",
|
||||||
"last_contact_agent_at": "2017-02-09T09:16:56.192Z",
|
"last_contact_agent_at": "2017-02-09T09:16:56.192Z",
|
||||||
"last_contact_agent_at": "2017-02-09T09:16:56.192Z",
|
"last_contact_agent_at": "2017-02-09T09:16:56.192Z",
|
||||||
|
@ -1106,4 +1107,12 @@ window.onload = function() {
|
||||||
|
|
||||||
testContains('organization.domain', 'cool', ticket);
|
testContains('organization.domain', 'cool', ticket);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("ticket mention user_id", function() {
|
||||||
|
ticket = new App.Ticket();
|
||||||
|
ticket.load(ticketData);
|
||||||
|
|
||||||
|
testPreConditionUser('ticket.mention_user_ids', '6', ticket, sessionData);
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
8
spec/factories/mention.rb
Normal file
8
spec/factories/mention.rb
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
FactoryBot.define do
|
||||||
|
factory :mention do
|
||||||
|
mentionable { create(:ticket) }
|
||||||
|
user_id { 1 }
|
||||||
|
created_by_id { 1 }
|
||||||
|
updated_by_id { 1 }
|
||||||
|
end
|
||||||
|
end
|
|
@ -193,6 +193,7 @@ RSpec.describe SearchIndexBackend, searchindex: true do
|
||||||
|
|
||||||
before do
|
before do
|
||||||
Ticket.destroy_all # needed to remove not created tickets
|
Ticket.destroy_all # needed to remove not created tickets
|
||||||
|
create(:mention, mentionable: ticket1, user: agent1)
|
||||||
ticket1.search_index_update_backend
|
ticket1.search_index_update_backend
|
||||||
travel 1.second
|
travel 1.second
|
||||||
ticket2.search_index_update_backend
|
ticket2.search_index_update_backend
|
||||||
|
@ -631,5 +632,99 @@ RSpec.describe SearchIndexBackend, searchindex: true do
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'mentions' do
|
||||||
|
it 'finds records with pre_condition is not_set' do
|
||||||
|
result = described_class.selectors('Ticket',
|
||||||
|
{
|
||||||
|
'ticket.mention_user_ids' => {
|
||||||
|
'pre_condition' => 'not_set',
|
||||||
|
'operator' => 'is',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ current_user: agent1 },
|
||||||
|
{
|
||||||
|
field: 'created_at', # sort to verify result
|
||||||
|
})
|
||||||
|
expect(result).to eq({ count: 7, ticket_ids: [ticket8.id.to_s, ticket7.id.to_s, ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s, ticket3.id.to_s, ticket2.id.to_s] })
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'finds records with pre_condition is not not_set' do
|
||||||
|
result = described_class.selectors('Ticket',
|
||||||
|
{
|
||||||
|
'ticket.mention_user_ids' => {
|
||||||
|
'pre_condition' => 'not_set',
|
||||||
|
'operator' => 'is not',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ current_user: agent1 },
|
||||||
|
{
|
||||||
|
field: 'created_at', # sort to verify result
|
||||||
|
})
|
||||||
|
expect(result).to eq({ count: 1, ticket_ids: [ticket1.id.to_s] })
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'finds records with pre_condition is current_user.id' do
|
||||||
|
result = described_class.selectors('Ticket',
|
||||||
|
{
|
||||||
|
'ticket.mention_user_ids' => {
|
||||||
|
'pre_condition' => 'current_user.id',
|
||||||
|
'operator' => 'is',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ current_user: agent1 },
|
||||||
|
{
|
||||||
|
field: 'created_at', # sort to verify result
|
||||||
|
})
|
||||||
|
expect(result).to eq({ count: 1, ticket_ids: [ticket1.id.to_s] })
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'finds records with pre_condition is not current_user.id' do
|
||||||
|
result = described_class.selectors('Ticket',
|
||||||
|
{
|
||||||
|
'ticket.mention_user_ids' => {
|
||||||
|
'pre_condition' => 'current_user.id',
|
||||||
|
'operator' => 'is not',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ current_user: agent1 },
|
||||||
|
{
|
||||||
|
field: 'created_at', # sort to verify result
|
||||||
|
})
|
||||||
|
expect(result).to eq({ count: 7, ticket_ids: [ticket8.id.to_s, ticket7.id.to_s, ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s, ticket3.id.to_s, ticket2.id.to_s] })
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'finds records with pre_condition is specific' do
|
||||||
|
result = described_class.selectors('Ticket',
|
||||||
|
{
|
||||||
|
'ticket.mention_user_ids' => {
|
||||||
|
'pre_condition' => 'specific',
|
||||||
|
'operator' => 'is',
|
||||||
|
'value' => agent1.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
field: 'created_at', # sort to verify result
|
||||||
|
})
|
||||||
|
expect(result).to eq({ count: 1, ticket_ids: [ticket1.id.to_s] })
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'finds records with pre_condition is not specific' do
|
||||||
|
result = described_class.selectors('Ticket',
|
||||||
|
{
|
||||||
|
'ticket.mention_user_ids' => {
|
||||||
|
'pre_condition' => 'specific',
|
||||||
|
'operator' => 'is not',
|
||||||
|
'value' => agent1.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
field: 'created_at', # sort to verify result
|
||||||
|
})
|
||||||
|
expect(result).to eq({ count: 7, ticket_ids: [ticket8.id.to_s, ticket7.id.to_s, ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s, ticket3.id.to_s, ticket2.id.to_s] })
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
RSpec.shared_examples 'HasHistory' do |history_relation_object: nil|
|
RSpec.shared_examples 'HasHistory' do |history_relation_object: []|
|
||||||
describe 'auto-creation of history records' do
|
describe 'auto-creation of history records' do
|
||||||
let(:histories) { History.where(history_object_id: History::Object.find_by(name: described_class.name)) }
|
let(:histories) { History.where(history_object_id: History::Object.find_by(name: described_class.name)) }
|
||||||
|
|
||||||
|
|
11
spec/models/mention_spec.rb
Normal file
11
spec/models/mention_spec.rb
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Mention, type: :model do
|
||||||
|
let(:ticket) { create(:ticket) }
|
||||||
|
|
||||||
|
describe 'validation' do
|
||||||
|
it 'does not allow mentions for customers' do
|
||||||
|
expect { create(:mention, mentionable: ticket, user: create(:customer)) }.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: User has no ticket.agent permissions')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -21,7 +21,7 @@ RSpec.describe Ticket, type: :model do
|
||||||
it_behaves_like 'ApplicationModel'
|
it_behaves_like 'ApplicationModel'
|
||||||
it_behaves_like 'CanBeImported'
|
it_behaves_like 'CanBeImported'
|
||||||
it_behaves_like 'CanCsvImport'
|
it_behaves_like 'CanCsvImport'
|
||||||
it_behaves_like 'HasHistory', history_relation_object: 'Ticket::Article'
|
it_behaves_like 'HasHistory', history_relation_object: ['Ticket::Article', 'Mention']
|
||||||
it_behaves_like 'HasTags'
|
it_behaves_like 'HasTags'
|
||||||
it_behaves_like 'TagWritesToTicketHistory'
|
it_behaves_like 'TagWritesToTicketHistory'
|
||||||
it_behaves_like 'HasTaskbars'
|
it_behaves_like 'HasTaskbars'
|
||||||
|
@ -196,6 +196,20 @@ RSpec.describe Ticket, type: :model do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when both tickets having mentions to the same user' do
|
||||||
|
let(:watcher) { create(:agent) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
create(:mention, mentionable: ticket, user: watcher)
|
||||||
|
create(:mention, mentionable: target_ticket, user: watcher)
|
||||||
|
ticket.merge_to(ticket_id: target_ticket.id, user_id: 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does remove the link from the merged ticket' do
|
||||||
|
expect(target_ticket.mentions.count).to eq(1) # one mention to watcher user
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when merging' do
|
context 'when merging' do
|
||||||
let(:merge_user) { create(:user) }
|
let(:merge_user) { create(:user) }
|
||||||
|
|
||||||
|
@ -1509,6 +1523,210 @@ RSpec.describe Ticket, type: :model do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'Mentions:', sends_notification_emails: true do
|
||||||
|
context 'when notifications' do
|
||||||
|
let(:prefs_matrix_no_mentions) do
|
||||||
|
{ 'notification_config' =>
|
||||||
|
{ 'matrix' =>
|
||||||
|
{ 'create' => { 'criteria' => { 'owned_by_me' => true, 'owned_by_nobody' => true, 'mentioned' => false, 'no' => true }, 'channel' => { 'email' => true, 'online' => true } },
|
||||||
|
'update' => { 'criteria' => { 'owned_by_me' => true, 'owned_by_nobody' => true, 'mentioned' => false, 'no' => true }, 'channel' => { 'email' => true, 'online' => true } },
|
||||||
|
'reminder_reached' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'mentioned' => false, 'no' => false }, 'channel' => { 'email' => false, 'online' => false } },
|
||||||
|
'escalation' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'mentioned' => false, 'no' => false }, 'channel' => { 'email' => false, 'online' => false } } } } }
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:prefs_matrix_only_mentions) do
|
||||||
|
{ 'notification_config' =>
|
||||||
|
{ 'matrix' =>
|
||||||
|
{ 'create' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'mentioned' => true, 'no' => false }, 'channel' => { 'email' => true, 'online' => true } },
|
||||||
|
'update' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'mentioned' => true, 'no' => false }, 'channel' => { 'email' => true, 'online' => true } },
|
||||||
|
'reminder_reached' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'mentioned' => true, 'no' => false }, 'channel' => { 'email' => false, 'online' => false } },
|
||||||
|
'escalation' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'mentioned' => true, 'no' => false }, 'channel' => { 'email' => false, 'online' => false } } } } }
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:mention_group) { create(:group) }
|
||||||
|
let(:no_access_group) { create(:group) }
|
||||||
|
let(:user_only_mentions) { create(:agent, groups: [mention_group], preferences: prefs_matrix_only_mentions) }
|
||||||
|
let(:user_no_mentions) { create(:agent, groups: [mention_group], preferences: prefs_matrix_no_mentions) }
|
||||||
|
let(:ticket) { create(:ticket, group: mention_group, owner: user_no_mentions) }
|
||||||
|
|
||||||
|
it 'does inform mention user about the ticket update' do
|
||||||
|
create(:mention, mentionable: ticket, user: user_only_mentions)
|
||||||
|
create(:mention, mentionable: ticket, user: user_no_mentions)
|
||||||
|
Observer::Transaction.commit
|
||||||
|
Scheduler.worker(true)
|
||||||
|
|
||||||
|
check_notification do
|
||||||
|
ticket.update(priority: Ticket::Priority.find_by(name: '3 high'))
|
||||||
|
Observer::Transaction.commit
|
||||||
|
Scheduler.worker(true)
|
||||||
|
sent(
|
||||||
|
template: 'ticket_update',
|
||||||
|
user: user_no_mentions,
|
||||||
|
)
|
||||||
|
sent(
|
||||||
|
template: 'ticket_update',
|
||||||
|
user: user_only_mentions,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not inform mention user about the ticket update' do
|
||||||
|
ticket
|
||||||
|
Observer::Transaction.commit
|
||||||
|
Scheduler.worker(true)
|
||||||
|
|
||||||
|
check_notification do
|
||||||
|
ticket.update(priority: Ticket::Priority.find_by(name: '3 high'))
|
||||||
|
Observer::Transaction.commit
|
||||||
|
Scheduler.worker(true)
|
||||||
|
sent(
|
||||||
|
template: 'ticket_update',
|
||||||
|
user: user_no_mentions,
|
||||||
|
)
|
||||||
|
not_sent(
|
||||||
|
template: 'ticket_update',
|
||||||
|
user: user_only_mentions,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does inform mention user about ticket creation' do
|
||||||
|
check_notification do
|
||||||
|
ticket = create(:ticket, owner: user_no_mentions, group: mention_group)
|
||||||
|
create(:mention, mentionable: ticket, user: user_only_mentions)
|
||||||
|
Observer::Transaction.commit
|
||||||
|
Scheduler.worker(true)
|
||||||
|
sent(
|
||||||
|
template: 'ticket_create',
|
||||||
|
user: user_no_mentions,
|
||||||
|
)
|
||||||
|
sent(
|
||||||
|
template: 'ticket_create',
|
||||||
|
user: user_only_mentions,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not inform mention user about ticket creation' do
|
||||||
|
check_notification do
|
||||||
|
create(:ticket, owner: user_no_mentions, group: mention_group)
|
||||||
|
Observer::Transaction.commit
|
||||||
|
Scheduler.worker(true)
|
||||||
|
sent(
|
||||||
|
template: 'ticket_create',
|
||||||
|
user: user_no_mentions,
|
||||||
|
)
|
||||||
|
not_sent(
|
||||||
|
template: 'ticket_create',
|
||||||
|
user: user_only_mentions,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not inform mention user about ticket creation because of no permissions' do
|
||||||
|
check_notification do
|
||||||
|
ticket = create(:ticket, group: no_access_group)
|
||||||
|
create(:mention, mentionable: ticket, user: user_only_mentions)
|
||||||
|
Observer::Transaction.commit
|
||||||
|
Scheduler.worker(true)
|
||||||
|
not_sent(
|
||||||
|
template: 'ticket_create',
|
||||||
|
user: user_only_mentions,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'selectors' do
|
||||||
|
let(:mention_group) { create(:group) }
|
||||||
|
let(:ticket_mentions) { create(:ticket, group: mention_group) }
|
||||||
|
let(:ticket_normal) { create(:ticket, group: mention_group) }
|
||||||
|
let(:user_mentions) { create(:agent, groups: [mention_group]) }
|
||||||
|
let(:user_no_mentions) { create(:agent, groups: [mention_group]) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
described_class.destroy_all
|
||||||
|
ticket_normal
|
||||||
|
user_no_mentions
|
||||||
|
create(:mention, mentionable: ticket_mentions, user: user_mentions)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'pre condition is not_set' do
|
||||||
|
condition = {
|
||||||
|
'ticket.mention_user_ids' => {
|
||||||
|
pre_condition: 'not_set',
|
||||||
|
operator: 'is',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(described_class.selectors(condition, limit: 100, access: 'full'))
|
||||||
|
.to match_array([1, [ticket_normal].to_a])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'pre condition is not not_set' do
|
||||||
|
condition = {
|
||||||
|
'ticket.mention_user_ids' => {
|
||||||
|
pre_condition: 'not_set',
|
||||||
|
operator: 'is not',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(described_class.selectors(condition, limit: 100, access: 'full'))
|
||||||
|
.to match_array([1, [ticket_mentions].to_a])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'pre condition is current_user.id' do
|
||||||
|
condition = {
|
||||||
|
'ticket.mention_user_ids' => {
|
||||||
|
pre_condition: 'current_user.id',
|
||||||
|
operator: 'is',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(described_class.selectors(condition, limit: 100, access: 'full', current_user: user_mentions))
|
||||||
|
.to match_array([1, [ticket_mentions].to_a])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'pre condition is not current_user.id' do
|
||||||
|
condition = {
|
||||||
|
'ticket.mention_user_ids' => {
|
||||||
|
pre_condition: 'current_user.id',
|
||||||
|
operator: 'is not',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(described_class.selectors(condition, limit: 100, access: 'full', current_user: user_mentions))
|
||||||
|
.to match_array([0, []])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'pre condition is specific' do
|
||||||
|
condition = {
|
||||||
|
'ticket.mention_user_ids' => {
|
||||||
|
pre_condition: 'specific',
|
||||||
|
operator: 'is',
|
||||||
|
value: user_mentions.id
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(described_class.selectors(condition, limit: 100, access: 'full'))
|
||||||
|
.to match_array([1, [ticket_mentions].to_a])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'pre condition is not specific' do
|
||||||
|
condition = {
|
||||||
|
'ticket.mention_user_ids' => {
|
||||||
|
pre_condition: 'specific',
|
||||||
|
operator: 'is not',
|
||||||
|
value: user_mentions.id
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(described_class.selectors(condition, limit: 100, access: 'full'))
|
||||||
|
.to match_array([0, []])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '.search_index_attribute_lookup_oversized?' do
|
describe '.search_index_attribute_lookup_oversized?' do
|
||||||
subject!(:ticket) { create(:ticket) }
|
subject!(:ticket) { create(:ticket) }
|
||||||
|
|
||||||
|
|
|
@ -869,9 +869,10 @@ RSpec.describe User, type: :model do
|
||||||
'User' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
'User' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||||
'Organization' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
'Organization' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||||
'Macro' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
'Macro' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||||
|
'Mention' => { 'created_by_id' => 1, 'updated_by_id' => 0, 'user_id' => 1 },
|
||||||
'Channel' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
'Channel' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||||
'Role' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
'Role' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||||
'History' => { 'created_by_id' => 2 },
|
'History' => { 'created_by_id' => 3 },
|
||||||
'Webhook' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
'Webhook' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||||
'Overview' => { 'created_by_id' => 1, 'updated_by_id' => 0 },
|
'Overview' => { 'created_by_id' => 1, 'updated_by_id' => 0 },
|
||||||
'ActivityStream' => { 'created_by_id' => 0 },
|
'ActivityStream' => { 'created_by_id' => 0 },
|
||||||
|
@ -893,6 +894,8 @@ RSpec.describe User, type: :model do
|
||||||
recent_view = create(:recent_view, created_by: user)
|
recent_view = create(:recent_view, created_by: user)
|
||||||
avatar = create(:avatar, o_id: user.id)
|
avatar = create(:avatar, o_id: user.id)
|
||||||
overview = create(:overview, created_by_id: user.id, user_ids: [user.id])
|
overview = create(:overview, created_by_id: user.id, user_ids: [user.id])
|
||||||
|
mention = create(:mention, mentionable: create(:ticket), user: user)
|
||||||
|
mention_created_by = create(:mention, mentionable: create(:ticket), user: create(:agent), created_by: user)
|
||||||
expect(overview.reload.user_ids).to eq([user.id])
|
expect(overview.reload.user_ids).to eq([user.id])
|
||||||
|
|
||||||
# create a chat agent for admin user (id=1) before agent user
|
# create a chat agent for admin user (id=1) before agent user
|
||||||
|
@ -930,6 +933,8 @@ RSpec.describe User, type: :model do
|
||||||
expect { customer_ticket2.reload }.to raise_exception(ActiveRecord::RecordNotFound)
|
expect { customer_ticket2.reload }.to raise_exception(ActiveRecord::RecordNotFound)
|
||||||
expect { customer_ticket3.reload }.to raise_exception(ActiveRecord::RecordNotFound)
|
expect { customer_ticket3.reload }.to raise_exception(ActiveRecord::RecordNotFound)
|
||||||
expect { chat_agent_user.reload }.to raise_exception(ActiveRecord::RecordNotFound)
|
expect { chat_agent_user.reload }.to raise_exception(ActiveRecord::RecordNotFound)
|
||||||
|
expect { mention.reload }.to raise_exception(ActiveRecord::RecordNotFound)
|
||||||
|
expect(mention_created_by.reload.created_by_id).not_to eq(user.id)
|
||||||
expect(overview.reload.user_ids).to eq([])
|
expect(overview.reload.user_ids).to eq([])
|
||||||
|
|
||||||
# move ownership objects
|
# move ownership objects
|
||||||
|
|
72
spec/requests/mention_spec.rb
Normal file
72
spec/requests/mention_spec.rb
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Mention', type: :request, authenticated_as: -> { user } do
|
||||||
|
let(:group) { create(:group) }
|
||||||
|
let(:ticket1) { create(:ticket, group: group) }
|
||||||
|
let(:ticket2) { create(:ticket, group: group) }
|
||||||
|
let(:ticket3) { create(:ticket, group: group) }
|
||||||
|
let(:ticket4) { create(:ticket, group: group) }
|
||||||
|
let(:user) { create(:agent, groups: [group]) }
|
||||||
|
|
||||||
|
describe 'GET /api/v1/mentions' do
|
||||||
|
before do
|
||||||
|
create(:mention, mentionable: ticket1, user: user)
|
||||||
|
create(:mention, mentionable: ticket2, user: user)
|
||||||
|
create(:mention, mentionable: ticket3, user: user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns good status code' do
|
||||||
|
get '/api/v1/mentions', params: {}, as: :json
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns mentions by user' do
|
||||||
|
get '/api/v1/mentions', params: {}, as: :json
|
||||||
|
expect(json_response['mentions'].count).to eq(3)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns mentions by mentionable' do
|
||||||
|
get '/api/v1/mentions', params: { mentionable_type: 'Ticket', mentionable_id: ticket3.id }, as: :json
|
||||||
|
expect(json_response['mentions'].count).to eq(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns mentions by id' do
|
||||||
|
mention = create(:mention, mentionable: ticket4, user: user)
|
||||||
|
get '/api/v1/mentions', params: { id: mention.id }, as: :json
|
||||||
|
expect(json_response['mentions'].count).to eq(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'POST /api/v1/mentions' do
|
||||||
|
|
||||||
|
let(:params) do
|
||||||
|
{
|
||||||
|
mentionable_type: 'Ticket',
|
||||||
|
mentionable_id: ticket1.id
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns good status code for subscribe' do
|
||||||
|
post '/api/v1/mentions', params: params, as: :json
|
||||||
|
expect(response).to have_http_status(:created)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates mention count' do
|
||||||
|
expect { post '/api/v1/mentions', params: params, as: :json }.to change(Mention, :count).from(0).to(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'DELETE /api/v1/mentions/:id' do
|
||||||
|
|
||||||
|
let!(:mention) { create(:mention, user: user) }
|
||||||
|
|
||||||
|
it 'returns good status code' do
|
||||||
|
delete "/api/v1/mentions/#{mention.id}", params: {}, as: :json
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'clears mention count' do
|
||||||
|
expect { delete "/api/v1/mentions/#{mention.id}", params: {}, as: :json }.to change(Mention, :count).from(1).to(0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -486,6 +486,36 @@ AAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO
|
||||||
expect(json_response['attachments']).to be_truthy
|
expect(json_response['attachments']).to be_truthy
|
||||||
expect(json_response['attachments'].count).to eq(0)
|
expect(json_response['attachments'].count).to eq(0)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'does ticket create with mentions' do
|
||||||
|
params = {
|
||||||
|
title: 'a new ticket #1',
|
||||||
|
group: 'Users',
|
||||||
|
customer_id: customer.id,
|
||||||
|
article: {
|
||||||
|
body: "some body <a data-mention-user-id=\"#{agent.id}\">agent</a>",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
authenticated_as(agent)
|
||||||
|
post '/api/v1/tickets', params: params, as: :json
|
||||||
|
expect(response).to have_http_status(:created)
|
||||||
|
expect(Mention.where(mentionable: Ticket.last).count).to eq(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not ticket create with mentions when customer' do
|
||||||
|
params = {
|
||||||
|
title: 'a new ticket #1',
|
||||||
|
group: 'Users',
|
||||||
|
customer_id: customer.id,
|
||||||
|
article: {
|
||||||
|
body: "some body <a data-mention-user-id=\"#{agent.id}\">agent</a>",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
authenticated_as(customer)
|
||||||
|
post '/api/v1/tickets', params: params, as: :json
|
||||||
|
expect(response).to have_http_status(:internal_server_error)
|
||||||
|
expect(Mention.count).to eq(0)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'DELETE /api/v1/ticket_articles/:id', authenticated_as: -> { user } do
|
describe 'DELETE /api/v1/ticket_articles/:id', authenticated_as: -> { user } do
|
||||||
|
|
|
@ -2200,6 +2200,47 @@ RSpec.describe 'Ticket', type: :request do
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'mentions' do
|
||||||
|
let(:user1) { create(:agent, groups: [ticket_group]) }
|
||||||
|
let(:user2) { create(:agent, groups: [ticket_group]) }
|
||||||
|
let(:user3) { create(:agent, groups: [ticket_group]) }
|
||||||
|
|
||||||
|
def new_ticket_with_mentions
|
||||||
|
params = {
|
||||||
|
title: 'a new ticket #11',
|
||||||
|
group: ticket_group.name,
|
||||||
|
customer: {
|
||||||
|
firstname: 'some firstname',
|
||||||
|
lastname: 'some lastname',
|
||||||
|
email: 'some_new_customer@example.com',
|
||||||
|
},
|
||||||
|
article: {
|
||||||
|
body: 'some test 123',
|
||||||
|
},
|
||||||
|
mentions: [user1.id, user2.id, user3.id]
|
||||||
|
}
|
||||||
|
authenticated_as(agent)
|
||||||
|
post '/api/v1/tickets', params: params, as: :json
|
||||||
|
expect(response).to have_http_status(:created)
|
||||||
|
|
||||||
|
json_response
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'create ticket with mentions' do
|
||||||
|
new_ticket_with_mentions
|
||||||
|
expect(Mention.all.count).to eq(3)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'check ticket get' do
|
||||||
|
ticket = new_ticket_with_mentions
|
||||||
|
|
||||||
|
get "/api/v1/tickets/#{ticket['id']}?all=true", params: {}, as: :json
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(json_response['mentions'].count).to eq(3)
|
||||||
|
expect(json_response['assets']['Mention'].count).to eq(3)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'stats' do
|
describe 'stats' do
|
||||||
let(:ticket1) { create(:ticket, customer: customer, organization: organization, group: ticket_group) }
|
let(:ticket1) { create(:ticket, customer: customer, organization: organization, group: ticket_group) }
|
||||||
let(:ticket2) { create(:ticket, customer: customer, organization: organization, group: ticket_group) }
|
let(:ticket2) { create(:ticket, customer: customer, organization: organization, group: ticket_group) }
|
||||||
|
|
|
@ -1277,6 +1277,36 @@ RSpec.describe 'Ticket zoom', type: :system do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'mentions' do
|
||||||
|
context 'when logged in as agent' do
|
||||||
|
let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
|
||||||
|
let!(:other_agent) { create(:agent, groups: [Group.find_by(name: 'Users')]) }
|
||||||
|
|
||||||
|
it 'can subscribe and unsubscribe' do
|
||||||
|
ensure_websocket do
|
||||||
|
visit "ticket/zoom/#{ticket.id}"
|
||||||
|
|
||||||
|
click '.mentions .js-subscribe input'
|
||||||
|
expect(page).to have_selector('.mentions .js-unsubscribe input', wait: 10)
|
||||||
|
expect(page).to have_selector('.mentions span.avatar', wait: 10)
|
||||||
|
|
||||||
|
click '.mentions .js-unsubscribe input'
|
||||||
|
expect(page).to have_selector('.mentions .js-subscribe input', wait: 10)
|
||||||
|
expect(page).to have_no_selector('.mentions span.avatar', wait: 10)
|
||||||
|
|
||||||
|
create(:mention, mentionable: ticket, user: other_agent)
|
||||||
|
expect(page).to have_selector('.mentions span.avatar', wait: 10)
|
||||||
|
|
||||||
|
# check history for mention entries
|
||||||
|
click 'h2.sidebar-header-headline.js-headline'
|
||||||
|
click 'li[data-type=ticket-history] a'
|
||||||
|
expect(page).to have_text('created Mention', wait: 10)
|
||||||
|
expect(page).to have_text('removed Mention', wait: 10)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# https://github.com/zammad/zammad/issues/2671
|
# https://github.com/zammad/zammad/issues/2671
|
||||||
describe 'Pending time field in ticket sidebar', authenticated_as: :customer do
|
describe 'Pending time field in ticket sidebar', authenticated_as: :customer do
|
||||||
let(:customer) { create(:customer) }
|
let(:customer) { create(:customer) }
|
||||||
|
|
Loading…
Reference in a new issue