Split of ticket zoom controller into separate files.

This commit is contained in:
Martin Edenhofer 2015-05-31 18:48:02 +02:00
parent c360fabbb3
commit a206644a85
8 changed files with 1329 additions and 1296 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,212 @@
class App.TicketZoomArticleActions extends App.Controller
events:
'click [data-type=public]': 'public_internal'
'click [data-type=internal]': 'public_internal'
'click [data-type=reply]': 'reply'
'click [data-type=replyAll]': 'replyAll'
constructor: ->
super
@render()
render: ->
actions = @actionRow(@article)
if actions
@html App.view('ticket_zoom/article_view_actions')(
article: @article
actions: actions
)
else
@html ''
public_internal: (e) ->
e.preventDefault()
article_id = $(e.target).parents('[data-id]').data('id')
# storage update
article = App.TicketArticle.find(article_id)
internal = true
if article.internal == true
internal = false
article.updateAttributes(
internal: internal
)
# runntime update
if internal
$(e.target).closest('.ticket-article-item').addClass('is-internal')
else
$(e.target).closest('.ticket-article-item').removeClass('is-internal')
@render()
actionRow: (article) ->
if @isRole('Customer')
return []
actions = []
if article.internal is true
actions = [
{
name: 'set to public'
type: 'public'
icon: 'lock-open'
}
]
else
actions = [
{
name: 'set to internal'
type: 'internal'
icon: 'lock'
}
]
#if @article.type.name is 'note'
# actions.push []
if article.type.name is 'email' || article.type.name is 'phone' || article.type.name is 'web'
actions.push {
name: 'reply'
type: 'reply'
icon: 'reply'
href: '#'
}
recipients = []
if article.sender.name is 'Agent'
if article.to
localRecipients = emailAddresses.parseAddressList(article.to)
if localRecipients
recipients = recipients.concat localRecipients
else
if article.from
localRecipients = emailAddresses.parseAddressList(article.from)
if localRecipients
recipients = recipients.concat localRecipients
if article.cc
localRecipients = emailAddresses.parseAddressList(article.cc)
if localRecipients
recipients = recipients.concat localRecipients
if recipients.length > 1
actions.push {
name: 'reply all'
type: 'replyAll'
icon: 'reply-all'
href: '#'
}
actions.push {
name: 'split'
type: 'split'
icon: 'split'
href: '#ticket/create/' + article.ticket_id + '/' + article.id
}
actions
replyAll: (e) =>
@reply(e, true)
reply: (e, all = false) =>
e.preventDefault()
# get reference article
article_id = $(e.target).parents('[data-id]').data('id')
article = App.TicketArticle.fullLocal( article_id )
type = App.TicketArticleType.find( article.type_id )
customer = App.User.find( article.created_by_id )
@el.closest('.article-add').ScrollTo()
# empty form
articleNew = {
to: ''
cc: ''
body: ''
in_reply_to: ''
}
#@el.closest('[name="in_reply_to"]').val('')
if article.message_id
articleNew.in_reply_to = article.message_id
if type.name is 'twitter status'
# set to in body
to = customer.accounts['twitter'].username || customer.accounts['twitter'].uid
articleNew.body = '@' + to
else if type.name is 'twitter direct-message'
# show to
to = customer.accounts['twitter'].username || customer.accounts['twitter'].uid
articleNew.to = to
else if type.name is 'email' || type.name is 'phone' || type.name is 'web'
if article.sender.name is 'Agent'
articleNew.to = article.to
else
articleNew.to = article.from
# if sender is customer but in article.from is no email, try to get
# customers email via customer user
if articleNew.to && !articleNew.to.match(/@/)
articleNew.to = article.created_by.email
# filter for uniq recipients
recipientAddresses = {}
recipient = emailAddresses.parseAddressList(articleNew.to)
if recipient && recipient[0]
recipientAddresses[ recipient[0].address.toString().toLowerCase() ] = true
if all
addAddresses = (lineNew, addressLine) ->
localAddresses = App.EmailAddress.all()
recipients = emailAddresses.parseAddressList(addressLine)
if recipients
for recipient in recipients
if recipient.address
# check if addess is not local
localAddess = false
for address in localAddresses
if recipient.address.toString().toLowerCase() == address.email.toString().toLowerCase()
localAddess = true
if !localAddess
# filter for uniq recipients
if !recipientAddresses[ recipient.address.toString().toLowerCase() ]
recipientAddresses[ recipient.address.toString().toLowerCase() ] = true
# add recipient
if lineNew
lineNew = lineNew + ', '
lineNew = lineNew + recipient.address
lineNew
if article.from
articleNew.cc = addAddresses(articleNew.cc, article.from)
if article.to
articleNew.cc = addAddresses(articleNew.cc, article.to)
if article.cc
articleNew.cc = addAddresses(articleNew.cc, article.cc)
# get current body
body = @el.closest('[data-name="body"]').html() || ''
# check if quote need to be added
selectedText = App.ClipBoard.getSelected()
if selectedText
# clean selection
selectedText = App.Utils.textCleanup( selectedText )
# convert to html
selectedText = App.Utils.text2html( selectedText )
if selectedText
selectedText = "<div><br><br/></div><div><blockquote type=\"cite\">#{selectedText}</blockquote></div><div><br></div>"
# add selected text to body
body = selectedText + body
articleNew.body = body
App.Event.trigger('ui::ticket::setArticleType', { ticket: @ticket, type: type, article: articleNew } )

View file

@ -0,0 +1,520 @@
class App.TicketZoomArticleNew extends App.Controller
elements:
'.js-textarea': 'textarea'
'.attachmentPlaceholder': 'attachmentPlaceholder'
'.attachmentPlaceholder-inputHolder': 'attachmentInputHolder'
'.attachmentPlaceholder-hint': 'attachmentHint'
'.article-add': 'articleNewEdit'
'.attachments': 'attachmentsHolder'
'.attachmentUpload': 'attachmentUpload'
'.attachmentUpload-progressBar': 'progressBar'
'.js-percentage': 'progressText'
'.js-cancel': 'cancelContainer'
'.textBubble': 'textBubble'
'.editControls-item': 'editControlItem'
#'.editControls': 'editControls'
#'.recipient-picker': 'recipientPicker'
#'.recipient-list': 'recipientList'
#'.recipient-list .list-arrow': 'recipientListArrow'
events:
'click .js-toggleVisibility': 'toggleVisibility'
'click .js-articleTypeItem': 'selectArticleType'
'click .js-selectedArticleType': 'showSelectableArticleType'
'click .recipient-picker': 'toggle_recipients'
'click .recipient-list': 'stopPropagation'
'click .list-entry-type div': 'change_type'
'submit .recipient-list form': 'add_recipient'
'focus .js-textarea': 'openTextarea'
'input .js-textarea': 'detectEmptyTextarea'
#'dragenter': 'onDragenter'
#'dragleave': 'onDragleave'
#'drop': 'onFileDrop'
#'change input[type=file]': 'onFilePick'
constructor: ->
super
# gets referenced in @setArticleType
@type = @defaults['type'] || 'note'
@articleTypes = [
{
name: 'note'
icon: 'note'
attributes: []
},
{
name: 'email'
icon: 'email'
attributes: ['to','cc']
},
{
name: 'facebook'
icon: 'facebook'
attributes: []
},
{
name: 'twitter'
icon: 'twitter'
attributes: []
},
{
name: 'phone'
icon: 'phone'
attributes: []
},
]
if @isRole('Customer')
@type = 'note'
@articleTypes = [
{
name: 'note'
icon: 'note'
attributes: []
},
]
@textareaHeight =
open: 148
closed: 20
@dragEventCounter = 0
@attachments = []
@render()
if @defaults.body or @isIE10()
@openTextarea(null, true)
# set article type and expand text area
@bind(
'ui::ticket::setArticleType'
(data) =>
if data.ticket.id is @ticket.id
#@setArticleType(data.type.name)
@openTextarea(null, true)
for key, value of data.article
if key is 'body'
@$('[data-name="' + key + '"]').html(value)
else
@$('[name="' + key + '"]').val(value)
# preselect article type
@setArticleType( 'email' )
)
# reset new article screen
@bind(
'ui::ticket::taskReset'
(data) =>
if data.ticket_id is @ticket.id
@type = 'note'
@defaults = {}
@render()
)
isIE10: ->
Function('/*@cc_on return document.documentMode===10@*/')()
stopPropagation: (e) ->
e.stopPropagation()
release: =>
if @subscribeIdTextModule
App.Ticket.unsubscribe(@subscribeIdTextModule)
render: ->
ticket = App.Ticket.fullLocal( @ticket.id )
@html App.view('ticket_zoom/article_new')(
ticket: ticket
articleTypes: @articleTypes
article: @defaults
isCustomer: @isRole('Customer')
)
@setArticleType(@type)
new App.WidgetAvatar(
el: @$('.js-avatar')
user_id: App.Session.get('id')
size: 40
position: 'right'
class: 'zIndex-5'
)
configure_attributes = [
{ name: 'customer_id', display: 'Recipients', tag: 'user_autocompletion', null: false, placeholder: 'Enter Person or Organization/Company', minLengt: 2, disableCreateUser: false },
]
controller = new App.ControllerForm(
el: @$('.recipients')
model:
configure_attributes: configure_attributes,
)
@$('[data-name="body"]').ce({
mode: 'richtext'
multiline: true
maxlength: 5000
})
html5Upload.initialize(
uploadUrl: App.Config.get('api_path') + '/ticket_attachment_upload',
dropContainer: @el.get(0),
cancelContainer: @cancelContainer,
inputField: @$('.article-attachment input').get(0),
key: 'File',
data: { form_id: @form_id },
maxSimultaneousUploads: 1,
onFileAdded: (file) =>
file.on(
onStart: =>
@attachmentPlaceholder.addClass('hide')
@attachmentUpload.removeClass('hide')
@cancelContainer.removeClass('hide')
console.log('upload start')
onAborted: =>
@attachmentPlaceholder.removeClass('hide')
@attachmentUpload.addClass('hide')
# Called after received response from the server
onCompleted: (response) =>
response = JSON.parse(response)
@attachments.push response.data
@attachmentPlaceholder.removeClass('hide')
@attachmentUpload.addClass('hide')
@renderAttachment(response.data)
console.log('upload complete', response.data )
# Called during upload progress, first parameter
# is decimal value from 0 to 100.
onProgress: (progress, fileSize, uploadedBytes) =>
@progressBar.width(parseInt(progress) + "%")
@progressText.text(parseInt(progress))
# hide cancel on 90%
if parseInt(progress) >= 90
@cancelContainer.addClass('hide')
console.log('uploadProgress ', parseInt(progress))
)
)
# show text module UI
if !@isRole('Customer')
textModule = new App.WidgetTextModule(
el: @$('.js-textarea').parent()
data:
ticket: ticket
)
callback = (ticket) =>
textModule.reload(
ticket: ticket
)
@subscribeIdTextModule = ticket.subscribe( callback )
toggle_recipients: =>
if !@pickRecipientsCatcher
@show_recipients()
else
@hide_recipients()
show_recipients: ->
padding = 15
@recipientPicker.addClass('is-open')
@recipientList.removeClass('hide')
pickerDimensions = @recipientPicker.get(0).getBoundingClientRect()
availableHeight = @recipientPicker.scrollParent().outerHeight()
top = pickerDimensions.height/2 - @recipientList.height()/2
bottomDistance = availableHeight - padding - (pickerDimensions.top + top + @recipientList.height())
if bottomDistance < 0
top += bottomDistance
arrowCenter = -top + pickerDimensions.height/2
@recipientListArrow.css('top', arrowCenter)
@recipientList.css('top', top)
$.Velocity.hook(@recipientList, 'transformOriginX', "0")
$.Velocity.hook(@recipientList, 'transformOriginY', "#{ arrowCenter }px")
@recipientList.velocity
properties:
scale: [ 1, 0 ]
opacity: [ 1, 0 ]
options:
speed: 300
easing: [ 0.34, 1.61, 0.7, 1 ]
@pickRecipientsCatcher = new App.clickCatcher
holder: @el.offsetParent()
callback: @hide_recipients
zIndexScale: 6
hide_recipients: =>
@pickRecipientsCatcher.remove()
@pickRecipientsCatcher = null
@recipientPicker.removeClass('is-open')
@recipientList.velocity
properties:
scale: [ 0, 1 ]
opacity: [ 0, 1 ]
options:
speed: 300
easing: [ 500, 20 ]
complete: -> @recipientList.addClass('hide')
change_type: (e) ->
$(e.target).addClass('active').siblings('.active').removeClass('active')
# store $(this).data('value')
add_recipient: (e) ->
e.stopPropagation()
e.preventDefault()
console.log "add recipient", e
# store recipient
toggleVisibility: ->
if @articleNewEdit.hasClass 'is-public'
@articleNewEdit
.removeClass 'is-public'
.addClass 'is-internal'
@$('[name="internal"]').val 'true'
else
@articleNewEdit
.addClass 'is-public'
.removeClass 'is-internal'
@$('[name="internal"]').val ''
showSelectableArticleType: =>
@el.find('.js-articleTypes').removeClass('is-hidden')
@selectTypeCatcher = new App.clickCatcher
holder: @el.offsetParent()
callback: @hideSelectableArticleType
zIndexScale: 6
selectArticleType: (e) =>
articleTypeToSet = $(e.target).closest('.pop-selectable').data('value')
@setArticleType( articleTypeToSet )
@hideSelectableArticleType()
@selectTypeCatcher.remove()
@selectTypeCatcher = null
hideSelectableArticleType: =>
@el.find('.js-articleTypes').addClass('is-hidden')
setArticleType: (type) ->
typeIcon = @$('.js-selectedType')
@type = type
@$('[name="type"]').val(type)
@articleNewEdit.attr('data-type', type)
typeIcon.find('use').attr 'xlink:href', '#icon-'+ @type
# show/hide attributes
for articleType in @articleTypes
if articleType.name is type
@$('.form-group').addClass('hide')
for name in articleType.attributes
@$("[name=#{name}]").closest('.form-group').removeClass('hide')
# check if signature need to be added
body = @$('[data-name="body"]').html() || ''
signature = undefined
if @ticket.group.signature_id
signature = App.Signature.find( @ticket.group.signature_id )
if signature && signature.body && @type is 'email'
signatureFinished = App.Utils.text2html(
App.Utils.replaceTags( signature.body, { user: App.Session.get(), ticket: @ticket } )
)
if App.Utils.signatureCheck( body, signatureFinished )
if !App.Utils.lastLineEmpty(body)
body = body + '<br>'
body = body + "<div data-signature=\"true\" data-signature-id=\"#{signature.id}\">#{signatureFinished}</div>"
@$('[data-name="body"]').html(body)
# remove old signature
else
@$('[data-name="body"]').find("[data-signature=true]").remove()
detectEmptyTextarea: =>
if !@textarea.text().trim()
@addTextareaCatcher()
else
@removeTextareaCatcher()
openTextarea: (event, withoutAnimation) =>
if !@articleNewEdit.hasClass('is-open')
duration = 300
if withoutAnimation
duration = 0
@articleNewEdit.addClass('is-open')
@textarea.velocity
properties:
minHeight: "#{ @textareaHeight.open - 38 }px"
options:
duration: duration
easing: 'easeOutQuad'
complete: => @addTextareaCatcher()
@textBubble.velocity
properties:
paddingBottom: 28
options:
duration: duration
easing: 'easeOutQuad'
# scroll to bottom
@textarea.velocity "scroll",
container: @textarea.scrollParent()
offset: 99999
duration: 300
easing: 'easeOutQuad'
queue: false
@editControlItem
.removeClass('is-hidden')
.velocity
properties:
opacity: [ 1, 0 ]
translateX: [ 0, 20 ]
translateZ: 0
options:
duration: 300
stagger: 50
drag: true
# move attachment text to the left bottom (bottom happens automatically)
@attachmentPlaceholder.velocity
properties:
translateX: -@attachmentInputHolder.position().left + "px"
options:
duration: duration
easing: 'easeOutQuad'
@attachmentHint.velocity
properties:
opacity: 0
options:
duration: duration
addTextareaCatcher: =>
if @articleNewEdit.is(':visible')
@textareaCatcher = new App.clickCatcher
holder: @articleNewEdit.offsetParent()
callback: @closeTextarea
zIndexScale: 4
removeTextareaCatcher: ->
return if !@textareaCatcher
@textareaCatcher.remove()
@textareaCatcher = null
closeTextarea: =>
@removeTextareaCatcher()
if !@textarea.text().trim() && !@attachments.length && not @isIE10()
@textarea.velocity
properties:
minHeight: "#{ @textareaHeight.closed }px"
options:
duration: 300
easing: 'easeOutQuad'
complete: => @articleNewEdit.removeClass('is-open')
@textBubble.velocity
properties:
paddingBottom: 10
options:
duration: 300
easing: 'easeOutQuad'
@attachmentPlaceholder.velocity
properties:
translateX: 0
options:
duration: 300
easing: 'easeOutQuad'
@attachmentHint.velocity
properties:
opacity: 1
options:
duration: 300
@editControlItem
.velocity
properties:
opacity: [ 0, 1 ]
translateX: [ 20, 0 ]
translateZ: 0
options:
duration: 100
stagger: 50
drag: true
complete: (elements) => $(elements).addClass('is-hidden')
onDragenter: (event) =>
# on the first event,
# open textarea (it will only open if its closed)
@openTextarea() if @dragEventCounter is 0
@dragEventCounter++
@articleNewEdit.parent().addClass('is-dropTarget')
onDragleave: (event) =>
@dragEventCounter--
@articleNewEdit.parent().removeClass('is-dropTarget') if @dragEventCounter is 0
renderAttachment: (file) =>
@attachmentsHolder.append App.view('generic/attachment_item')
fileName: file.filename
fileSize: @humanFileSize( file.size )
store_id: file.store_id
@attachmentsHolder.on(
'click'
"[data-id=#{file.store_id}]", (e) =>
@attachments = _.filter(
@attachments,
(item) ->
return if item.id isnt file.store_id
item
)
store_id = $(e.currentTarget).data('id')
# delete attachment from storage
App.Ajax.request(
type: 'DELETE'
url: App.Config.get('api_path') + '/ticket_attachment_upload'
data: JSON.stringify( { store_id: store_id } ),
processData: false
success: (data, status, xhr) =>
)
# remove attachment from dom
element = $(e.currentTarget).closest('.attachments')
$(e.currentTarget).closest('.attachment').remove()
# empty .attachment (remove spaces) to keep css working, thanks @mrflix :-o
if element.find('.attachment').length == 0
element.empty()
)

View file

@ -0,0 +1,251 @@
class App.TicketZoomArticleView extends App.Controller
constructor: ->
super
@article_controller = {}
execute: (params) ->
for ticket_article_id in params.ticket_article_ids
if !@article_controller[ticket_article_id]
el = $('<div></div>')
@article_controller[ticket_article_id] = new ArticleViewItem(
ticket: @ticket
ticket_article_id: ticket_article_id
el: el
ui: @ui
)
@el.append( el )
class ArticleViewItem extends App.Controller
events:
'click .show_toogle': 'show_toogle'
'click .textBubble': 'toggle_meta_with_delay'
'click .textBubble a': 'stopPropagation'
'click .js-unfold': 'unfold'
constructor: ->
super
@seeMore = false
@render()
# set article type and expand text area
@bind(
'ui::ticket::shown'
(data) =>
if data.ticket_id is @ticket.id
@setSeeMore()
)
# subscribe to changes
@subscribeId = App.TicketArticle.full( @ticket_article_id, @render, false, true )
release: =>
App.User.TicketArticle(@subscribeId)
render: (article) =>
# get articles
@article = App.TicketArticle.fullLocal( @ticket_article_id )
# prepare html body
if @article.content_type is 'text/html'
@article['html'] = @article.body
else
@article['html'] = App.Utils.textCleanup( @article.body )
@article['html'] = App.Utils.text2html( @article.body )
@html App.view('ticket_zoom/article_view')(
ticket: @ticket
article: @article
isCustomer: @isRole('Customer')
)
new App.WidgetAvatar(
el: @$('.js-avatar')
user_id: @article.created_by_id
size: 40
)
new App.TicketZoomArticleActions(
el: @$('.js-article-actions')
ticket: @ticket
article: @article
)
# show frontend times
@frontendTimeUpdate()
# set see more option
@setSeeMore()
# set see more options
setSeeMore: =>
maxHeight = 560
bubble = @$('.textBubble-content')
# expand if see more is already clicked
if @seeMore
bubble.css('height', 'auto')
bubble.parent().find('.textBubble-overflowContainer').addClass('hide')
return
# reset bubble heigth and "see more" opacity
bubble.css('height', '')
bubble.parent().find('.textBubble-overflowContainer').css('opacity', '')
# remember offset of "see more"
offsetTop = bubble.find('.js-signatureMarker').position()
# remember bubble heigth
heigth = bubble.height()
if offsetTop
bubble.attr('data-height', heigth)
bubble.css('height', "#{offsetTop.top + 30}px")
bubble.parent().find('.textBubble-overflowContainer').removeClass('hide')
else if heigth > maxHeight
bubble.attr('data-height', heigth)
bubble.css('height', "#{maxHeight}px")
bubble.parent().find('.textBubble-overflowContainer').removeClass('hide')
else
bubble.parent().find('.textBubble-overflowContainer').addClass('hide')
show_toogle: (e) ->
e.stopPropagation()
e.preventDefault()
#$(e.target).hide()
if $(e.target).next('div')[0]
if $(e.target).next('div').hasClass('hide')
$(e.target).next('div').removeClass('hide')
$(e.target).text( App.i18n.translateContent('Fold in') )
else
$(e.target).text( App.i18n.translateContent('See more') )
$(e.target).next('div').addClass('hide')
stopPropagation: (e) ->
e.stopPropagation()
toggle_meta_with_delay: (e) =>
# allow double click select
# by adding a delay to the toggle
if @lastClick and +new Date - @lastClick < 150
clearTimeout(@toggleMetaTimeout)
else
@toggleMetaTimeout = setTimeout(@toggle_meta, 150, e)
@lastClick = +new Date
toggle_meta: (e) =>
e.preventDefault()
animSpeed = 300
article = $(e.target).closest('.ticket-article-item')
metaTopClip = article.find('.article-meta-clip.top')
metaBottomClip = article.find('.article-meta-clip.bottom')
metaTop = article.find('.article-content-meta.top')
metaBottom = article.find('.article-content-meta.bottom')
if @elementContainsSelection( article.get(0) )
@stopPropagation(e)
return false
if !metaTop.hasClass('hide')
article.removeClass('state--folde-out')
# scroll back up
article.velocity "scroll",
container: article.scrollParent()
offset: -article.offset().top - metaTop.outerHeight()
duration: animSpeed
easing: 'easeOutQuad'
metaTop.velocity
properties:
translateY: 0
opacity: [ 0, 1 ]
options:
speed: animSpeed
easing: 'easeOutQuad'
complete: -> metaTop.addClass('hide')
metaBottom.velocity
properties:
translateY: [ -metaBottom.outerHeight(), 0 ]
opacity: [ 0, 1 ]
options:
speed: animSpeed
easing: 'easeOutQuad'
complete: -> metaBottom.addClass('hide')
metaTopClip.velocity({ height: 0 }, animSpeed, 'easeOutQuad')
metaBottomClip.velocity({ height: 0 }, animSpeed, 'easeOutQuad')
else
article.addClass('state--folde-out')
metaBottom.removeClass('hide')
metaTop.removeClass('hide')
# balance out the top meta height by scrolling down
article.velocity("scroll",
container: article.scrollParent()
offset: -article.offset().top + metaTop.outerHeight()
duration: animSpeed
easing: 'easeOutQuad'
)
metaTop.velocity
properties:
translateY: [ 0, metaTop.outerHeight() ]
opacity: [ 1, 0 ]
options:
speed: animSpeed
easing: 'easeOutQuad'
metaBottom.velocity
properties:
translateY: [ 0, -metaBottom.outerHeight() ]
opacity: [ 1, 0 ]
options:
speed: animSpeed
easing: 'easeOutQuad'
metaTopClip.velocity({ height: metaTop.outerHeight() }, animSpeed, 'easeOutQuad')
metaBottomClip.velocity({ height: metaBottom.outerHeight() }, animSpeed, 'easeOutQuad')
unfold: (e) ->
e.preventDefault()
e.stopPropagation()
@seeMore = true
container = $(e.currentTarget).parents('.textBubble-content')
overflowContainer = container.find('.textBubble-overflowContainer')
overflowContainer.velocity
properties:
opacity: 0
options:
duration: 300
container.velocity
properties:
height: container.attr('data-height')
options:
duration: 300
complete: -> overflowContainer.addClass('hide');
isOrContains: (node, container) ->
while node
if node is container
return true
node = node.parentNode
false
elementContainsSelection: (el) ->
sel = window.getSelection()
if sel.rangeCount > 0 && sel.toString()
for i in [0..sel.rangeCount-1]
if !@isOrContains(sel.getRangeAt(i).commonAncestorContainer, el)
return false
return true
false

View file

@ -0,0 +1,19 @@
class App.TicketZoomMeta extends App.Controller
constructor: ->
super
@ticket = App.Ticket.fullLocal( @ticket.id )
@subscribeId = @ticket.subscribe(@render)
@render(@ticket)
render: (ticket) =>
@html App.view('ticket_zoom/meta')(
ticket: ticket
isCustomer: @isRole('Customer')
)
# show frontend times
@frontendTimeUpdate()
release: =>
App.Ticket.unsubscribe( @subscribeId )

View file

@ -0,0 +1,71 @@
class App.TicketZoomOverviewNavigator extends App.Controller
events:
'click a': 'open'
constructor: ->
super
# rebuild overview navigator if overview has changed
@bind 'ticket_overview_rebuild', (data) =>
execute = =>
@render()
@delay(execute, 600, 'overview-navigator')
@render()
render: (overview) =>
if !@overview_id
@html('')
return
# get overview data
worker = App.TaskManager.worker( 'TicketOverview' )
return if !worker
overview = worker.overview(@overview_id)
return if !overview
current_position = 0
next = false
previous = false
for ticket_id in overview.ticket_ids
current_position += 1
next = overview.ticket_ids[current_position]
previous = overview.ticket_ids[current_position-2]
break if ticket_id is @ticket_id
# get next/previous ticket
if next
next = App.Ticket.find(next)
if previous
previous = App.Ticket.find(previous)
@html App.view('ticket_zoom/overview_navigator')(
title: overview.overview.name
total_count: overview.tickets_count
current_position: current_position
next: next
previous: previous
)
open: (e) =>
e.preventDefault()
# get requested object and location
id = $(e.target).data('id')
url = $(e.target).attr('href')
if !id
id = $(e.target).closest('a').data('id')
url = $(e.target).closest('a').attr('href')
# return if we are unable to get id
return if !id
# open task via task manager to get overview information
App.TaskManager.execute(
key: 'Ticket-' + id
controller: 'TicketZoom'
params:
ticket_id: id
overview_id: @overview_id
show: true
)
@navigate url

View file

@ -0,0 +1,179 @@
class App.TicketZoomSidebar extends App.Controller
constructor: ->
super
ticket = App.Ticket.fullLocal( @ticket.id )
@subscribeId = ticket.subscribe(@render)
@render(ticket)
release: =>
App.Ticket.unsubscribe( @subscribeId )
render: (ticket) =>
editTicket = (el) =>
el.append('<form class="edit"></form>')
@editEl = el
show = (ticket) =>
el.find('.edit').html('')
defaults = ticket.attributes()
task_state = @taskGet('ticket')
modelDiff = App.Utils.formDiff( task_state, defaults )
#if @isRole('Customer')
# delete defaults['state_id']
# delete defaults['state']
if !_.isEmpty( task_state )
defaults = _.extend( defaults, task_state )
new App.ControllerForm(
el: el.find('.edit')
model: App.Ticket
screen: 'edit'
params: App.Ticket.find(ticket.id)
handlers: [
@ticketFormChanges
]
filter: @form_meta.filter
params: defaults
#bookmarkable: true
)
#console.log('Ichanges', modelDiff, task_state, ticket.attributes())
#@markFormDiff( modelDiff )
show(ticket)
@bind(
'ui::ticket::taskReset'
(data) =>
if data.ticket_id is ticket.id
show(ticket)
)
if !@isRole('Customer')
el.append('<div class="tags"></div>')
new App.WidgetTag(
el: el.find('.tags')
object_type: 'Ticket'
object: ticket
tags: @tags
)
el.append('<div class="links"></div>')
new App.WidgetLink(
el: el.find('.links')
object_type: 'Ticket'
object: ticket
links: @links
container: @el.closest('.content')
)
showTicketHistory = =>
new App.TicketHistory(
ticket_id: ticket.id
container: @el.closest('.content')
)
showTicketMerge = =>
new App.TicketMerge(
ticket: ticket
task_key: @task_key
container: @el.closest('.content')
)
changeCustomer = (e, el) =>
new App.TicketCustomer(
ticket: ticket
container: @el.closest('.content')
)
@sidebarItems = [
{
head: 'Ticket'
name: 'ticket'
icon: 'message'
callback: editTicket
}
]
if !@isRole('Customer')
@sidebarItems[0]['actions'] = [
{
name: 'ticket-history'
title: 'History'
callback: showTicketHistory
},
{
name: 'ticket-merge'
title: 'Merge'
callback: showTicketMerge
},
{
title: 'Change Customer'
name: 'customer-change'
callback: changeCustomer
},
]
if !@isRole('Customer')
editCustomer = (e, el) =>
new App.ControllerGenericEdit(
id: ticket.customer_id
genericObject: 'User'
screen: 'edit'
pageData:
title: 'Users'
object: 'User'
objects: 'Users'
container: @el.closest('.content')
)
showCustomer = (el) =>
new App.WidgetUser(
el: el
user_id: ticket.customer_id
)
@sidebarItems.push {
head: 'Customer'
name: 'customer'
icon: 'person'
actions: [
{
title: 'Change Customer'
name: 'customer-change'
callback: changeCustomer
},
{
title: 'Edit Customer'
name: 'customer-edit'
callback: editCustomer
},
]
callback: showCustomer
}
if ticket.organization_id
editOrganization = (e, el) =>
new App.ControllerGenericEdit(
id: ticket.organization_id,
genericObject: 'Organization'
pageData:
title: 'Organizations'
object: 'Organization'
objects: 'Organizations'
container: @el.closest('.content')
)
showOrganization = (el) =>
new App.WidgetOrganization(
el: el
organization_id: ticket.organization_id
)
@sidebarItems.push {
head: 'Organization'
name: 'organization'
icon: 'group'
actions: [
{
title: 'Edit Organization'
name: 'organization-edit'
callback: editOrganization
},
]
callback: showOrganization
}
new App.Sidebar(
el: @el
sidebarState: @sidebarState
items: @sidebarItems
)

View file

@ -0,0 +1,47 @@
class App.TicketZoomTitle extends App.Controller
events:
'blur .ticket-title-update': 'update'
constructor: ->
super
@ticket = App.Ticket.fullLocal( @ticket.id )
@subscribeId = @ticket.subscribe(@render)
@render(@ticket)
render: (ticket) =>
# check if render is needed
if @lastTitle && @lastTitle is ticket.title
return
@lastTitle = ticket.title
@html App.view('ticket_zoom/title')(
ticket: ticket
)
@$('.ticket-title-update').ce({
mode: 'textonly'
multiline: false
maxlength: 250
})
update: (e) =>
title = $(e.target).ceg() || ''
# update title
if title isnt @ticket.title
@ticket.title = title
# reset article - should not be resubmited on next ticket update
@ticket.article = undefined
@ticket.save()
App.TaskManager.mute( @task_key )
# update taskbar with new meta data
App.Event.trigger 'task:render'
release: =>
App.Ticket.unsubscribe( @subscribeId )