Split of ticket zoom controller into separate files.
This commit is contained in:
parent
c360fabbb3
commit
a206644a85
8 changed files with 1329 additions and 1296 deletions
File diff suppressed because it is too large
Load diff
|
@ -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 } )
|
|
@ -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()
|
||||
)
|
|
@ -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
|
|
@ -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 )
|
|
@ -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
|
|
@ -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
|
||||
)
|
|
@ -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 )
|
Loading…
Reference in a new issue