diff --git a/app/assets/javascripts/app/controllers/_application_controller_form.js.coffee b/app/assets/javascripts/app/controllers/_application_controller_form.js.coffee
index d0ad04638..5c769b8df 100644
--- a/app/assets/javascripts/app/controllers/_application_controller_form.js.coffee
+++ b/app/assets/javascripts/app/controllers/_application_controller_form.js.coffee
@@ -193,12 +193,6 @@ class App.ControllerForm extends App.Controller
else
attribute.required = ''
- # set multible option
- if attribute.multiple
- attribute.multiple = 'multiple'
- else
- attribute.multiple = ''
-
# set autocapitalize option
if attribute.autocapitalize is undefined || attribute.autocapitalize
attribute.autocapitalize = ''
@@ -228,932 +222,10 @@ class App.ControllerForm extends App.Controller
if attribute.name of @params
attribute.value = @params[attribute.name]
- if attribute.tag is 'autocompletion'
- if @params[ attribute.name + '_autocompletion_value_shown' ]
- attribute.valueShown = @params[ attribute.name + '_autocompletion_value_shown' ]
-
App.Log.debug 'ControllerForm', 'formGenItem-before', attribute
- # build options list based on config
- @_getConfigOptionList( attribute )
-
- # build options list based on relation
- @_getRelationOptionList( attribute )
-
- # add null selection if needed
- @_addNullOption( attribute )
-
- # sort attribute.options
- @_sortOptions( attribute )
-
- # finde selected/checked item of list
- @_selectedOptions( attribute )
-
- # disable item of list
- @_disabledOptions( attribute )
-
- # filter attributes
- @_filterOption( attribute )
-
- if attribute.tag is 'boolean'
-
- # build options list
- if _.isEmpty(attribute.options)
- attribute.options = [
- { name: 'yes', value: true }
- { name: 'no', value: false }
- ]
-
- # set data type
- if attribute.name
- attribute.name = '{boolean}' + attribute.name
-
- # finde selected item of list
- for record in attribute.options
- if record.value is attribute.value
- record.selected = 'selected'
-
- # return item
- item = $( App.view('generic/select')( attribute: attribute ) )
-
- else if attribute.tag is 'active'
-
- # active attribute is always required
- attribute.null = false
-
- # build options list
- attribute.options = [
- { name: 'active', value: true }
- { name: 'inactive', value: false }
- ]
-
- # set data type
- if attribute.name
- attribute.name = '{boolean}' + attribute.name
-
- # finde selected item of list
- for record in attribute.options
- if record.value is attribute.value
- record.selected = 'selected'
-
- # return item
- item = $( App.view('generic/select')( attribute: attribute ) )
-
- # select
- else if attribute.tag is 'select'
- item = $( App.view('generic/select')( attribute: attribute ) )
-
- # date
- else if attribute.tag is 'date'
-
- # set data type
- if attribute.name
- attribute.nameRaw = attribute.name
- attribute.name = '{date}' + attribute.name
- if attribute.value
- if typeof( attribute.value ) is 'string'
- unixtime = new Date( Date.parse( "#{attribute.value}T00:00:00Z" ) )
- else
- unixtime = new Date( attribute.value )
- year = unixtime.getUTCFullYear()
- month = unixtime.getUTCMonth() + 1
- day = unixtime.getUTCDate()
- hour = unixtime.getUTCHours()
- minute = unixtime.getUTCMinutes()
- item = $( App.view('generic/date')(
- attribute: attribute
- year: year
- month: month
- day: day
- ) )
-
- setNewTime = (diff, el, reset) ->
- name = $(el).closest('.form-group').find('[data-name]').attr('data-name')
-
- # remove old validation
- item.find('.has-error').removeClass('has-error')
- item.closest('.form-group').find('.help-inline').html('')
-
- day = item.closest('.form-group').find("[name=\"{date}#{name}___day\"]").val()
- month = item.closest('.form-group').find("[name=\"{date}#{name}___month\"]").val()
- year = item.closest('.form-group').find("[name=\"{date}#{name}___year\"]").val()
- format = (number) ->
- if parseInt(number) < 10
- number = "0#{number}"
- number
- if !reset && (year isnt '' && month isnt '' && day isnt '')
- time = new Date( Date.parse( "#{year}-#{format(month)}-#{format(day)}T00:00:00Z" ) )
- time.setMinutes( time.getMinutes() + diff + time.getTimezoneOffset() )
- else
- time = new Date()
- time.setMinutes( time.getMinutes() + diff )
- item.closest('.form-group').find("[name=\"{date}#{name}___day\"]").val( time.getDate() )
- item.closest('.form-group').find("[name=\"{date}#{name}___month\"]").val( time.getMonth()+1 )
- item.closest('.form-group').find("[name=\"{date}#{name}___year\"]").val( time.getFullYear() )
-
- item.find('.js-today').bind('click', (e) ->
- e.preventDefault()
- setNewTime(0, @, true)
- )
- item.find('.js-plus-day').bind('click', (e) ->
- e.preventDefault()
- setNewTime(60 * 24, @)
- )
- item.find('.js-minus-day').bind('click', (e) ->
- e.preventDefault()
- setNewTime(-60 * 24, @)
- )
- item.find('.js-plus-week').bind('click', (e) ->
- e.preventDefault()
- setNewTime(60 * 24 * 7, @)
- )
- item.find('.js-minus-week').bind('click', (e) ->
- e.preventDefault()
- setNewTime(-60 * 24 * 7, @)
- )
-
- item.find('input').bind('keyup blur focus change', (e) ->
-
- # do validation
- name = $(@).attr('name')
- if name
- fieldPrefix = name.split('___')[0]
-
- # remove old validation
- item.find('.has-error').removeClass('has-error')
- item.closest('.form-group').find('.help-inline').html('')
-
- day = item.closest('.form-group').find("[name=\"#{fieldPrefix}___day\"]").val()
- month = item.closest('.form-group').find("[name=\"#{fieldPrefix}___month\"]").val()
- year = item.closest('.form-group').find("[name=\"#{fieldPrefix}___year\"]").val()
-
- # validate exists
- errors = {}
- if !day
- errors.day = 'missing'
- if !month
- errors.month = 'missing'
- if !year
- errors.year = 'missing'
-
- # ranges
- if day
- daysInMonth = 31
- if month && year
- daysInMonth = new Date(year, month, 0).getDate();
-
- if parseInt(day).toString() is 'NaN'
- errors.day = 'invalid'
- else if parseInt(day) > daysInMonth || parseInt(day) < 1
- errors.day = 'invalid'
-
- if month
- if parseInt(month).toString() is 'NaN'
- errors.month = 'invalid'
- else if parseInt(month) > 12 || parseInt(month) < 1
- errors.month = 'invalid'
-
- if year
- if parseInt(year).toString() is 'NaN'
- errors.year = 'invalid'
- else if parseInt(year) > 2100 || parseInt(year) < 2001
- errors.year = 'invalid'
-
- if !_.isEmpty(errors)
-
- # if field is required, if not do not show error
- if year is '' && day is '' && month
- if attribute.null
- e.preventDefault()
- e.stopPropagation()
- return
- else
- item.closest('.form-group').find('.help-inline').text( 'is required' )
-
- # show invalid options
- for key, value of errors
- item.closest('.form-group').addClass('has-error')
- item.closest('.form-group').find("[name=\"#{fieldPrefix}___#{key}\"]").addClass('has-error')
- #item.closest('.form-group').find('.help-inline').text( value )
-
- e.preventDefault()
- e.stopPropagation()
- return
- )
-
-
- # date
- else if attribute.tag is 'datetime'
-
- # set data type
- if attribute.name
- attribute.nameRaw = attribute.name
- attribute.name = '{datetime}' + attribute.name
- if attribute.value
- if typeof( attribute.value ) is 'string'
- unixtime = new Date( Date.parse( attribute.value ) )
- else
- unixtime = new Date( attribute.value )
- year = unixtime.getFullYear()
- month = unixtime.getMonth() + 1
- day = unixtime.getDate()
- hour = unixtime.getHours()
- minute = unixtime.getMinutes()
- item = $( App.view('generic/datetime')(
- attribute: attribute
- year: year
- month: month
- day: day
- hour: hour
- minute: minute
- ) )
-
- setNewTime = (diff, el, reset) ->
- name = $(el).closest('.form-group').find('[data-name]').attr('data-name')
-
- # remove old validation
- item.find('.has-error').removeClass('has-error')
- item.closest('.form-group').find('.help-inline').html('')
-
- day = item.closest('.form-group').find("[name=\"{datetime}#{name}___day\"]").val()
- month = item.closest('.form-group').find("[name=\"{datetime}#{name}___month\"]").val()
- year = item.closest('.form-group').find("[name=\"{datetime}#{name}___year\"]").val()
- hour = item.closest('.form-group').find("[name=\"{datetime}#{name}___hour\"]").val()
- minute = item.closest('.form-group').find("[name=\"{datetime}#{name}___minute\"]").val()
- format = (number) ->
- if parseInt(number) < 10
- number = "0#{number}"
- number
- if !reset && (year isnt '' && month isnt '' && day isnt '' && hour isnt '' && day isnt '')
- time = new Date( Date.parse( "#{year}-#{format(month)}-#{format(day)}T#{format(hour)}:#{format(minute)}:00Z" ) )
- time.setMinutes( time.getMinutes() + diff + time.getTimezoneOffset() )
- else
- time = new Date()
- time.setMinutes( time.getMinutes() + diff )
- #console.log('T', time, time.getHours(), time.getMinutes())
- item.closest('.form-group').find("[name=\"{datetime}#{name}___day\"]").val( time.getDate() )
- item.closest('.form-group').find("[name=\"{datetime}#{name}___month\"]").val( time.getMonth()+1 )
- item.closest('.form-group').find("[name=\"{datetime}#{name}___year\"]").val( time.getFullYear() )
- item.closest('.form-group').find("[name=\"{datetime}#{name}___hour\"]").val( time.getHours() )
- item.closest('.form-group').find("[name=\"{datetime}#{name}___minute\"]").val( time.getMinutes() )
-
- item.find('.js-today').bind('click', (e) ->
- e.preventDefault()
- setNewTime(0, @, true)
- )
- item.find('.js-plus-hour').bind('click', (e) ->
- e.preventDefault()
- setNewTime(60, @)
- )
- item.find('.js-minus-hour').bind('click', (e) ->
- e.preventDefault()
- setNewTime(-60, @)
- )
- item.find('.js-plus-day').bind('click', (e) ->
- e.preventDefault()
- setNewTime(60 * 24, @)
- )
- item.find('.js-minus-day').bind('click', (e) ->
- e.preventDefault()
- setNewTime(-60 * 24, @)
- )
- item.find('.js-plus-week').bind('click', (e) ->
- e.preventDefault()
- setNewTime(60 * 24 * 7, @)
- )
- item.find('.js-minus-week').bind('click', (e) ->
- e.preventDefault()
- setNewTime(-60 * 24 * 7, @)
- )
-
- item.find('input').bind('keyup blur focus change', (e) ->
-
- # do validation
- name = $(@).attr('name')
- if name
- fieldPrefix = name.split('___')[0]
-
- # remove old validation
- item.find('.has-error').removeClass('has-error')
- item.closest('.form-group').find('.help-inline').html('')
-
- day = item.closest('.form-group').find("[name=\"#{fieldPrefix}___day\"]").val()
- month = item.closest('.form-group').find("[name=\"#{fieldPrefix}___month\"]").val()
- year = item.closest('.form-group').find("[name=\"#{fieldPrefix}___year\"]").val()
- hour = item.closest('.form-group').find("[name=\"#{fieldPrefix}___hour\"]").val()
- minute = item.closest('.form-group').find("[name=\"#{fieldPrefix}___minute\"]").val()
-
- # validate exists
- errors = {}
- if !day
- errors.day = 'missing'
- if !month
- errors.month = 'missing'
- if !year
- errors.year = 'missing'
- if !hour
- errors.hour = 'missing'
- if !minute
- errors.minute = 'missing'
-
- # ranges
- if day
- daysInMonth = 31
- if month && year
- daysInMonth = new Date(year, month, 0).getDate();
-
- if parseInt(day).toString() is 'NaN'
- errors.day = 'invalid'
- else if parseInt(day) > daysInMonth || parseInt(day) < 1
- errors.day = 'invalid'
-
- if month
- if parseInt(month).toString() is 'NaN'
- errors.month = 'invalid'
- else if parseInt(month) > 12 || parseInt(month) < 1
- errors.month = 'invalid'
-
- if year
- if parseInt(year).toString() is 'NaN'
- errors.year = 'invalid'
- else if parseInt(year) > 2100 || parseInt(year) < 2001
- errors.year = 'invalid'
-
- if hour
- if parseInt(hour).toString() is 'NaN'
- errors.hour = 'invalid'
- else if parseInt(hour) > 23 || parseInt(hour) < 0
- errors.hour = 'invalid'
-
- if minute
- if parseInt(minute).toString() is 'NaN'
- errors.minute = 'invalid'
- else if parseInt(minute) > 59
- errors.minute = 'invalid'
-
- if !_.isEmpty(errors)
-
- # if field is required, if not do not show error
- if year is '' && day is '' && month is '' && hour is '' && minute is ''
- if attribute.null
- e.preventDefault()
- e.stopPropagation()
- return
- else
- item.closest('.form-group').find('.help-inline').text( 'is required' )
-
- # show invalid options
- for key, value of errors
- item.closest('.form-group').addClass('has-error')
- item.closest('.form-group').find("[name=\"#{fieldPrefix}___#{key}\"]").addClass('has-error')
- #item.closest('.form-group').find('.help-inline').text( value )
-
- e.preventDefault()
- e.stopPropagation()
- return
- )
-
- # timezone
- else if attribute.tag is 'timezone'
- attribute.options = []
- timezones = App.Config.get('timezones')
-
- # build list based on config
- for timezone_value, timezone_diff of timezones
- if timezone_diff > 0
- timezone_diff = '+' + timezone_diff
- item =
- name: "#{timezone_value} (GMT#{timezone_diff})"
- value: timezone_value
- attribute.options.push item
-
- # finde selected item of list
- for record in attribute.options
- if record.value is attribute.value
- record.selected = 'selected'
-
- item = $( App.view('generic/select')( attribute: attribute ) )
-
- # postmaster_match
- else if attribute.tag is 'postmaster_match'
- addItem = (key, displayName, el, defaultValue = '') =>
- add = { name: key, display: displayName, tag: 'input', null: false, default: defaultValue }
- itemInput = $( @formGenItem( add ).append('' ) )
-
- # remove on click
- itemInput.find('.remove').bind('click', (e) ->
- e.preventDefault()
- key = $(e.target).closest('.form-group').find('select, input').attr('name')
- return if !key
- $(e.target).closest('.controls').find('.addSelection select option[value="' + key + '"]').show()
- $(e.target).closest('.controls').find('.addSelection select option[value="' + key + '"]').prop('disabled', false)
- $(e.target).closest('.form-group').remove()
- )
-
- # add new item
- control = el.closest('.postmaster_match')
- control.find('.list').append(itemInput)
- control.find('.addSelection select').val('')
- control.find('.addSelection select option[value="' + key + '"]').prop('disabled', true)
- control.find('.addSelection select option[value="' + key + '"]').hide()
-
- # scaffold of match elements
- item = $('
-
')
-
- # select shown attributes
- loopData = [
- {
- value: 'from'
- name: 'From'
- },
- {
- value: 'to'
- name: 'To'
- },
- {
- value: 'cc'
- name: 'Cc'
- },
- {
- value: 'subject'
- name: 'Subject'
- },
- {
- value: 'body'
- name: 'Body'
- },
- {
- value: ''
- name: '-'
- disable: true
- },
- {
- value: 'x-any-recipient'
- name: 'Any Recipient'
- },
- {
- value: ''
- name: '-'
- disable: true
- },
- {
- value: ''
- name: '- ' + App.i18n.translateInline('expert settings') + ' -'
- disable: true
- },
- {
- value: ''
- name: '-'
- disable: true
- },
- {
- value: 'x-spam-flag'
- name: 'X-Spam-Flag'
- },
- {
- value: 'x-spam-level'
- name: 'X-Spam-Level'
- },
- {
- value: 'x-spam-score'
- name: 'X-Spam-Score'
- },
- {
- value: 'x-spam-status'
- name: 'X-Spam-Status'
- },
- {
- value: 'importance'
- name: 'Importance'
- },
- {
- value: 'x-priority'
- name: 'X-Priority'
- },
-
- {
- value: 'organization'
- name: 'Organization'
- },
-
- {
- value: 'x-original-to'
- name: 'X-Original-To'
- },
- {
- value: 'delivered-to'
- name: 'Delivered-To'
- },
- {
- value: 'envelope-to'
- name: 'Envelope-To'
- },
- {
- value: 'return-path'
- name: 'Return-Path'
- },
- {
- value: 'mailing-list'
- name: 'Mailing-List'
- },
- {
- value: 'list-id'
- name: 'List-Id'
- },
- {
- value: 'list-archive'
- name: 'List-Archive'
- },
- {
- value: 'mailing-list'
- name: 'Mailing-List'
- },
- {
- value: 'auto-submitted'
- name: 'Auto-Submitted'
- },
- {
- value: 'x-loop'
- name: 'X-Loop'
- },
- ]
- for listItem in loopData
- listItem.value = "#{ attribute.name }::#{listItem.value}"
- add = { name: '', display: '', tag: 'select', multiple: false, null: false, nulloption: true, options: loopData, translate: true, required: false }
- item.find('.addSelection').append( @formGenItem( add ) )
-
- # bind add click
- item.find('.add').bind('click', (e) ->
- e.preventDefault()
- name = $(@).closest('.controls').find('.addSelection').find('select').val()
- displayName = $(@).closest('.controls').find('.addSelection').find('select option:selected').html()
- return if !name
- addItem( name, displayName, $(@) )
- )
-
- # show default values
- loopDataValue = {}
- if attribute.value
- for key, value of attribute.value
- displayName = key
- for listItem in loopData
- if listItem.value is "#{ attribute.name }::#{key}"
- addItem( "#{ attribute.name }::#{key}", listItem.name, item.find('.add a'), value )
-
- # postmaster_set
- else if attribute.tag is 'postmaster_set'
- addItem = (key, displayName, el, defaultValue = '') =>
- collection = undefined
- for listItem in loopData
- if listItem.value is key
- collection = listItem
- if collection.relation
- add = { name: key, display: displayName, tag: 'select', multiple: false, null: false, nulloption: true, relation: collection.relation, translate: true, default: defaultValue }
- else if collection.options
- add = { name: key, display: displayName, tag: 'select', multiple: false, null: false, nulloption: true, options: collection.options, translate: true, default: defaultValue }
- else
- add = { name: key, display: displayName, tag: 'input', null: false, default: defaultValue }
- itemInput = $( @formGenItem( add ).append('' ) )
-
- # remove on click
- itemInput.find('.remove').bind('click', (e) ->
- e.preventDefault()
- key = $(e.target).closest('.form-group').find('select, input').attr('name')
- return if !key
- $(e.target).closest('.controls').find('.addSelection select option[value="' + key + '"]').show()
- $(e.target).closest('.controls').find('.addSelection select option[value="' + key + '"]').prop('disabled', false)
- $(e.target).closest('.form-group').remove()
- )
-
- # add new item
- control = el.closest('.perform_set')
- control.find('.list').append(itemInput)
- control.find('.addSelection select').val('')
- control.find('.addSelection select option[value="' + key + '"]').prop('disabled', true)
- control.find('.addSelection select option[value="' + key + '"]').hide()
-
- # scaffold of perform elements
- item = $('
- ')
-
-
- # select shown attributes
- loopData = [
- {
- value: 'x-zammad-ticket-priority_id'
- name: 'Ticket Priority'
- relation: 'TicketPriority'
- },
- {
- value: 'x-zammad-ticket-state_id'
- name: 'Ticket State'
- relation: 'TicketState'
- },
- {
- value: 'x-zammad-ticket-customer'
- name: 'Ticket Customer'
- },
- {
- value: 'x-zammad-ticket-group_id'
- name: 'Ticket Group'
- relation: 'Group'
- },
- {
- value: 'x-zammad-ticket-owner'
- name: 'Ticket Owner'
- },
- {
- value: ''
- name: '-'
- disable: true
- },
- {
- value: 'x-zammad-article-internal'
- name: 'Article Internal'
- options: { true: 'Yes', false: 'No'}
- },
- {
- value: 'x-zammad-article-type_id'
- name: 'Article Type'
- relation: 'TicketArticleType'
- },
- {
- value: 'x-zammad-article-sender_id'
- name: 'Article Sender'
- relation: 'TicketArticleSender'
- },
- {
- value: ''
- name: '-'
- disable: true
- },
- {
- value: 'x-zammad-ignore'
- name: 'Ignore Message'
- options: { true: 'Yes', false: 'No'}
- },
- ]
- for listItem in loopData
- listItem.value = "#{ attribute.name }::#{listItem.value}"
- add = { name: '', display: '', tag: 'select', multiple: false, null: false, nulloption: true, options: loopData, translate: true, required: false }
- item.find('.addSelection').append( @formGenItem( add ) )
-
- item.find('.add').bind('click', (e) ->
- e.preventDefault()
- name = $(@).closest('.controls').find('.addSelection').find('select').val()
- displayName = $(@).closest('.controls').find('.addSelection').find('select option:selected').html()
- return if !name
- addItem( name, displayName, $(@) )
- )
-
- # show default values
- loopDataValue = {}
- if attribute.value
- for key, value of attribute.value
- displayName = key
- for listItem in loopData
- if listItem.value is "#{ attribute.name }::#{key}"
- addItem( "#{ attribute.name }::#{key}", listItem.name, item.find('.add a'), value )
-
- # checkbox
- else if attribute.tag is 'checkbox'
- item = $( App.view('generic/checkbox')( attribute: attribute ) )
-
- # radio
- else if attribute.tag is 'radio'
- item = $( App.view('generic/radio')( attribute: attribute ) )
-
- # richtext
- else if attribute.tag is 'richtext'
- item = $( App.view('generic/richtext')( attribute: attribute ) )
- item.find('[contenteditable]').ce(
- mode: attribute.type
- maxlength: attribute.maxlength
- )
- if attribute.upload
- item.append( $( App.view('generic/attachment')( attribute: attribute ) ) )
-
- renderAttachment = (file) =>
- item.find('.attachments').append( App.view('generic/attachment_item')(
- fileName: file.filename
- fileSize: @humanFileSize( file.size )
- store_id: file.store_id
- ))
- item.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()
- )
-
- @attachments = []
- @progressBar = item.find('.attachmentUpload-progressBar')
- @progressText = item.find('.js-percentage')
- @attachmentPlaceholder = item.find('.attachmentPlaceholder')
- @attachmentUpload = item.find('.attachmentUpload')
- @attachmentsHolder = item.find('.attachments')
- @cancelContainer = item.find('.js-cancel')
-
- u = => html5Upload.initialize(
- uploadUrl: App.Config.get('api_path') + '/ticket_attachment_upload',
- dropContainer: item.closest('form').get(0),
- cancelContainer: @cancelContainer,
- inputField: item.find( '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))
- )
- )
- App.Delay.set( u, 100, undefined, 'form_upload' )
-
- # textarea
- else if attribute.tag is 'textarea'
- fileUploaderId = 'file-uploader-' + new Date().getTime() + '-' + Math.floor( Math.random() * 99999 )
- item = $( App.view('generic/textarea')( attribute: attribute ) + '' )
-
- a = =>
- visible = $( item[0] ).is(":visible")
- if visible && !$( item[0] ).expanding('active')
- $( item[0] ).expanding()
- $( item[0] ).on('focus', ->
- visible = $( item[0] ).is(":visible")
- if visible && !$( item[0] ).expanding('active')
- $( item[0] ).expanding().focus()
- )
- App.Delay.set( a, 80 )
-
- if attribute.upload
-
- # add file uploader
- u = =>
- # only add upload item if html element exists
- if @el.find('#' + fileUploaderId )[0]
- @el.find('#' + fileUploaderId ).fineUploader(
- request:
- endpoint: App.Config.get('api_path') + '/ticket_attachment_upload'
- params:
- form_id: @form_id
- text:
- uploadButton: ''
- template: '' +
- '
{dragZoneText}
' +
- '
{uploadButtonText}
' +
- '
' +
- '
',
- classes:
- success: ''
- fail: ''
- debug: false
- )
- App.Delay.set( u, 100, undefined, 'form_upload' )
-
- # article
- else if attribute.tag is 'article'
- item = $( App.view('generic/article')( attribute: attribute ) )
-
- # tag
- else if attribute.tag is 'tag'
- item = $( App.view('generic/input')( attribute: attribute ) )
- a = =>
- $('#' + attribute.id ).tokenfield()
- $('#' + attribute.id ).parent().css('height', 'auto')
- App.Delay.set( a, 120, undefined, 'tags' )
-
- # user
- else if attribute.tag is 'user_autocompletion'
- completion = new App.UserOrganizationAutocompletion( attribute: attribute )
- item = completion.element()
-
- # searchable select
- else if attribute.tag is 'searchable_select'
- select = new App.SearchableSelect( attribute: attribute )
- item = select.element()
-
- # autocompletion
- else if attribute.tag is 'autocompletion'
- item = $( App.view('generic/autocompletion')( attribute: attribute ) )
-
- a = =>
- local_attribute = '#' + attribute.id
- local_attribute_full = '#' + attribute.id + '_autocompletion'
- @callback = attribute.callback
-
- # call calback on init
- if @callback && attribute.value && @params
- @callback( @params )
-
- b = (event, item) =>
- # set html form attribute
- $(local_attribute).val(item.id).trigger('change')
- $(local_attribute + '_autocompletion_value_shown').val(item.value)
-
- # call calback
- if @callback
- params = App.ControllerForm.params(form)
- @callback( params )
- ###
- $(@local_attribute_full).tagsInput(
- autocomplete_url: '/users/search',
- height: '30px',
- width: '530px',
- auto: {
- source: '/users/search',
- minLength: 2,
- select: ( event, ui ) ->
- #@log 'notice', 'selected', event, ui
- b(event, ui.item)
- }
- )
- ###
- source = attribute.source
- if typeof(source) is 'string'
- source = source.replace('#{@apiPath}', App.Config.get('api_path') );
- $(local_attribute_full).autocomplete(
- source: source,
- minLength: attribute.minLengt || 3,
- select: ( event, ui ) =>
- b(event, ui.item)
- )
- App.Delay.set( a, 280, undefined, 'form_autocompletion' )
-
- # working_hour
- else if attribute.tag is 'working_hour'
- if !attribute.value
- attribute.value = {}
- item = $( App.view('generic/working_hour')( attribute: attribute ) )
+ if App.UiElement[attribute.tag]
+ item = App.UiElement[attribute.tag].render(attribute, @params, @)
# working_hour
else if attribute.tag is 'time_before_last'
@@ -2090,226 +1162,6 @@ class App.ControllerForm extends App.Controller
else
ui._optional(attribute.name)
- # sort attribute.options
- _sortOptions: (attribute) ->
-
- # skip sorting if it is disabled by config
- return if attribute.sortBy == null
-
- return if !attribute.options
-
- if _.isArray( attribute.options )
- # reverse if we have to exit early, if configured
- if attribute.order
- if attribute.order == 'DESC'
- attribute.options = attribute.options.reverse()
- return
-
- options_by_name = []
- for i in attribute.options
- options_by_name.push i['name'].toString().toLowerCase()
- options_by_name = options_by_name.sort()
-
- options_new = []
- options_new_used = {}
- for i in options_by_name
- for ii, vv in attribute.options
- if !options_new_used[ ii['value'] ] && i.toString().toLowerCase() is ii['name'].toString().toLowerCase()
- options_new_used[ ii['value'] ] = 1
- options_new.push ii
- attribute.options = options_new
-
- # do a final reverse, if configured
- if attribute.order
- if attribute.order == 'DESC'
- attribute.options = attribute.options.reverse()
-
- _addNullOption: (attribute) ->
- return if !attribute.options
- return if !attribute.nulloption
- if _.isArray( attribute.options )
- attribute.options.unshift( { name: '-', value: '' } )
- else
- attribute.options[''] = '-'
-
- _getConfigOptionList: (attribute) ->
- return if !attribute.options
- selection = attribute.options
- attribute.options = []
- if _.isArray( selection )
- for row in selection
- if attribute.translate
- row.name = App.i18n.translateInline( row.name )
- attribute.options.push row
- else
- order = _.sortBy(
- _.keys(selection), (item) ->
- selection[item].toString().toLowerCase()
- )
- for key in order
- name_new = selection[key]
- if attribute.translate
- name_new = App.i18n.translateInline( name_new )
- attribute.options.push {
- name: name_new
- value: key
- }
-
- _getRelationOptionList: (attribute) ->
-
- # build options list based on relation
- return if !attribute.relation
- return if !App[attribute.relation]
-
- attribute.options = []
- list = []
- if attribute.filter
-
- App.Log.debug 'ControllerForm', '_getRelationOptionList:filter', attribute.filter
-
- # function based filter
- if typeof attribute.filter is 'function'
- App.Log.debug 'ControllerForm', '_getRelationOptionList:filter-function'
-
- all = App[ attribute.relation ].search( sortBy: attribute.sortBy )
-
- list = attribute.filter( all, 'collection', @params )
-
- # data based filter
- else if attribute.filter[ attribute.name ]
- filter = attribute.filter[ attribute.name ]
-
- App.Log.debug 'ControllerForm', '_getRelationOptionList:filter-data', filter
-
- # check all records
- for record in App[ attribute.relation ].search( sortBy: attribute.sortBy )
-
- # check all filter attributes
- for key in filter
-
- # check all filter values as array
- # if it's matching, use it for selection
- if record['id'] is key
- list.push record
-
- # data based filter
- else if attribute.filter && _.isArray attribute.filter
-
- App.Log.debug 'ControllerForm', '_getRelationOptionList:filter-array', attribute.filter
-
- # check all records
- for record in App[ attribute.relation ].search( sortBy: attribute.sortBy )
-
- # check all filter attributes
- for key in attribute.filter
-
- # check all filter values as array
- # if it's matching, use it for selection
- if record['id'] is key || ( record['id'] && key && record['id'].toString() is key.toString() )
- list.push record
-
- # check if current value need to be added
- if @params[ attribute.name ]
- hit = false
- for value in list
- if value['id'].toString() is @params[ attribute.name ].toString()
- hit = true
- if !hit
- currentRecord = App[ attribute.relation ].find( @params[ attribute.name ] )
- list.push currentRecord
-
- # no data filter matched
- else
- App.Log.debug 'ControllerForm', '_getRelationOptionList:filter-data no filter matched'
- list = App[ attribute.relation ].search( sortBy: attribute.sortBy )
- else
- App.Log.debug 'ControllerForm', '_getRelationOptionList:filter-no filter defined'
- list = App[ attribute.relation ].search( sortBy: attribute.sortBy )
-
- App.Log.debug 'ControllerForm', '_getRelationOptionList', attribute, list
-
- # build options list
- @_buildOptionList( list, attribute )
-
- # build options list
- _buildOptionList: (list, attribute) ->
-
- for item in list
-
- # if active or if active doesn't exist
- if item.active || !( 'active' of item )
- name_new = '?'
- if item.displayName
- name_new = item.displayName()
- else if item.name
- name_new = item.name
- if attribute.translate
- name_new = App.i18n.translateInline(name_new)
- attribute.options.push {
- name: name_new,
- value: item.id,
- note: item.note,
- }
-
- # execute filter
- _filterOption: (attribute) ->
- return if !attribute.filter
- return if !attribute.options
-
- return if typeof attribute.filter isnt 'function'
- App.Log.debug 'ControllerForm', '_filterOption:filter-function'
-
- attribute.options = attribute.filter( attribute.options, attribute )
-
- # set selected attributes
- _selectedOptions: (attribute) ->
- return if !attribute.options
-
- # check if selected / checked need to be set
- check = (value, record) ->
- if typeof value is 'string' || typeof value is 'number' || typeof value is 'boolean'
-
- # if name or value is matching
- if record.value.toString() is value.toString() || record.name.toString() is value.toString()
- record.selected = 'selected'
- record.checked = 'checked'
-
- else if ( value && record.value && _.include( value, record.value ) ) || ( value && record.name && _.include( value, record.name ) )
- record.selected = 'selected'
- record.checked = 'checked'
-
- # lookup of any record
- for record in attribute.options
-
- if _.isArray( attribute.value )
- for value in attribute.value
- check( value, record )
-
- if typeof attribute.value is 'string' || typeof attribute.value is 'number' || typeof attribute.value is 'boolean'
- check( attribute.value, record )
-
- # if noting is selected / checked, use default as selected / checked
- selected = false
- for record in attribute.options
- if record.selected || record.checked
- selected = true
- if !selected
- for record in attribute.options
- if typeof attribute.default is 'string' || typeof attribute.default is 'number' || typeof attribute.default is 'boolean'
- check( attribute.default, record )
-
- # set disabled attributes
- _disabledOptions: (attribute) ->
-
- return if !attribute.options
- return if !_.isArray( attribute.options )
-
- for record in attribute.options
- if record.disable is true
- record.disabled = 'disabled'
- else
- record.disabled = ''
-
validate: (params) ->
App.Model.validate(
model: @model
diff --git a/app/assets/javascripts/app/controllers/_ui_element/_application_ui_element.js.coffee b/app/assets/javascripts/app/controllers/_ui_element/_application_ui_element.js.coffee
new file mode 100644
index 000000000..909e6edf8
--- /dev/null
+++ b/app/assets/javascripts/app/controllers/_ui_element/_application_ui_element.js.coffee
@@ -0,0 +1,221 @@
+class App.UiElement.ApplicationUiElement
+
+ # sort attribute.options
+ @sortOptions: (attribute) ->
+
+ # skip sorting if it is disabled by config
+ return if attribute.sortBy == null
+
+ return if !attribute.options
+
+ if _.isArray( attribute.options )
+ # reverse if we have to exit early, if configured
+ if attribute.order
+ if attribute.order == 'DESC'
+ attribute.options = attribute.options.reverse()
+ return
+
+ options_by_name = []
+ for i in attribute.options
+ options_by_name.push i['name'].toString().toLowerCase()
+ options_by_name = options_by_name.sort()
+
+ options_new = []
+ options_new_used = {}
+ for i in options_by_name
+ for ii, vv in attribute.options
+ if !options_new_used[ ii['value'] ] && i.toString().toLowerCase() is ii['name'].toString().toLowerCase()
+ options_new_used[ ii['value'] ] = 1
+ options_new.push ii
+ attribute.options = options_new
+
+ # do a final reverse, if configured
+ if attribute.order
+ if attribute.order == 'DESC'
+ attribute.options = attribute.options.reverse()
+
+ @addNullOption: (attribute) ->
+ return if !attribute.options
+ return if !attribute.nulloption
+ if _.isArray( attribute.options )
+ attribute.options.unshift( { name: '-', value: '' } )
+ else
+ attribute.options[''] = '-'
+
+ @getConfigOptionList: (attribute) ->
+ return if !attribute.options
+ selection = attribute.options
+ attribute.options = []
+ if _.isArray( selection )
+ for row in selection
+ if attribute.translate
+ row.name = App.i18n.translateInline( row.name )
+ attribute.options.push row
+ else
+ order = _.sortBy(
+ _.keys(selection), (item) ->
+ selection[item].toString().toLowerCase()
+ )
+ for key in order
+ name_new = selection[key]
+ if attribute.translate
+ name_new = App.i18n.translateInline( name_new )
+ attribute.options.push {
+ name: name_new
+ value: key
+ }
+
+ @getRelationOptionList: (attribute, params) ->
+
+ # build options list based on relation
+ return if !attribute.relation
+ return if !App[attribute.relation]
+
+ attribute.options = []
+ list = []
+ if attribute.filter
+
+ App.Log.debug 'ControllerForm', '_getRelationOptionList:filter', attribute.filter
+
+ # function based filter
+ if typeof attribute.filter is 'function'
+ App.Log.debug 'ControllerForm', '_getRelationOptionList:filter-function'
+
+ all = App[ attribute.relation ].search( sortBy: attribute.sortBy )
+
+ list = attribute.filter( all, 'collection', params )
+
+ # data based filter
+ else if attribute.filter[ attribute.name ]
+ filter = attribute.filter[ attribute.name ]
+
+ App.Log.debug 'ControllerForm', '_getRelationOptionList:filter-data', filter
+
+ # check all records
+ for record in App[ attribute.relation ].search( sortBy: attribute.sortBy )
+
+ # check all filter attributes
+ for key in filter
+
+ # check all filter values as array
+ # if it's matching, use it for selection
+ if record['id'] is key
+ list.push record
+
+ # data based filter
+ else if attribute.filter && _.isArray attribute.filter
+
+ App.Log.debug 'ControllerForm', '_getRelationOptionList:filter-array', attribute.filter
+
+ # check all records
+ for record in App[ attribute.relation ].search( sortBy: attribute.sortBy )
+
+ # check all filter attributes
+ for key in attribute.filter
+
+ # check all filter values as array
+ # if it's matching, use it for selection
+ if record['id'] is key || ( record['id'] && key && record['id'].toString() is key.toString() )
+ list.push record
+
+ # check if current value need to be added
+ if params[ attribute.name ]
+ hit = false
+ for value in list
+ if value['id'].toString() is params[ attribute.name ].toString()
+ hit = true
+ if !hit
+ currentRecord = App[ attribute.relation ].find( params[ attribute.name ] )
+ list.push currentRecord
+
+ # no data filter matched
+ else
+ App.Log.debug 'ControllerForm', '_getRelationOptionList:filter-data no filter matched'
+ list = App[ attribute.relation ].search( sortBy: attribute.sortBy )
+ else
+ App.Log.debug 'ControllerForm', '_getRelationOptionList:filter-no filter defined'
+ list = App[ attribute.relation ].search( sortBy: attribute.sortBy )
+
+ App.Log.debug 'ControllerForm', '_getRelationOptionList', attribute, list
+
+ # build options list
+ @buildOptionList( list, attribute )
+
+ # build options list
+ @buildOptionList: (list, attribute) ->
+
+ for item in list
+
+ # if active or if active doesn't exist
+ if item.active || !( 'active' of item )
+ name_new = '?'
+ if item.displayName
+ name_new = item.displayName()
+ else if item.name
+ name_new = item.name
+ if attribute.translate
+ name_new = App.i18n.translateInline(name_new)
+ attribute.options.push {
+ name: name_new,
+ value: item.id,
+ note: item.note,
+ }
+
+ # execute filter
+ @filterOption: (attribute) ->
+ return if !attribute.filter
+ return if !attribute.options
+
+ return if typeof attribute.filter isnt 'function'
+ App.Log.debug 'ControllerForm', '_filterOption:filter-function'
+
+ attribute.options = attribute.filter( attribute.options, attribute )
+
+ # set selected attributes
+ @selectedOptions: (attribute) ->
+ return if !attribute.options
+
+ # check if selected / checked need to be set
+ check = (value, record) ->
+ if typeof value is 'string' || typeof value is 'number' || typeof value is 'boolean'
+
+ # if name or value is matching
+ if record.value.toString() is value.toString() || record.name.toString() is value.toString()
+ record.selected = 'selected'
+ record.checked = 'checked'
+
+ else if ( value && record.value && _.include( value, record.value ) ) || ( value && record.name && _.include( value, record.name ) )
+ record.selected = 'selected'
+ record.checked = 'checked'
+
+ # lookup of any record
+ for record in attribute.options
+
+ if _.isArray( attribute.value )
+ for value in attribute.value
+ check( value, record )
+
+ if typeof attribute.value is 'string' || typeof attribute.value is 'number' || typeof attribute.value is 'boolean'
+ check( attribute.value, record )
+
+ # if noting is selected / checked, use default as selected / checked
+ selected = false
+ for record in attribute.options
+ if record.selected || record.checked
+ selected = true
+ if !selected
+ for record in attribute.options
+ if typeof attribute.default is 'string' || typeof attribute.default is 'number' || typeof attribute.default is 'boolean'
+ check( attribute.default, record )
+
+ # set disabled attributes
+ @disabledOptions: (attribute) ->
+
+ return if !attribute.options
+ return if !_.isArray( attribute.options )
+
+ for record in attribute.options
+ if record.disable is true
+ record.disabled = 'disabled'
+ else
+ record.disabled = ''
diff --git a/app/assets/javascripts/app/controllers/_ui_element/active.js.coffee b/app/assets/javascripts/app/controllers/_ui_element/active.js.coffee
new file mode 100644
index 000000000..820a76fd1
--- /dev/null
+++ b/app/assets/javascripts/app/controllers/_ui_element/active.js.coffee
@@ -0,0 +1,28 @@
+class App.UiElement.active extends App.UiElement.ApplicationUiElement
+ @render: (attribute, params) ->
+
+ # set attributes
+ attribute.null = false
+ attribute.translation = true
+
+ # build options list
+ attribute.options = [
+ { name: 'active', value: true }
+ { name: 'inactive', value: false }
+ ]
+
+ # set data type
+ if attribute.name
+ attribute.name = '{boolean}' + attribute.name
+
+ # build options list based on config
+ @getConfigOptionList( attribute, params )
+
+ # sort attribute.options
+ @sortOptions( attribute, params )
+
+ # finde selected/checked item of list
+ @selectedOptions( attribute, params )
+
+ # return item
+ $( App.view('generic/select')( attribute: attribute ) )
\ No newline at end of file
diff --git a/app/assets/javascripts/app/controllers/_ui_element/autocompletion.js.coffee b/app/assets/javascripts/app/controllers/_ui_element/autocompletion.js.coffee
new file mode 100644
index 000000000..65d11b1c0
--- /dev/null
+++ b/app/assets/javascripts/app/controllers/_ui_element/autocompletion.js.coffee
@@ -0,0 +1,51 @@
+class App.UiElement.autocompletion
+ @render: (attribute, params) ->
+
+ if params[ attribute.name + '_autocompletion_value_shown' ]
+ attribute.valueShown = params[ attribute.name + '_autocompletion_value_shown' ]
+
+ item = $( App.view('generic/autocompletion')( attribute: attribute ) )
+
+ a = =>
+ local_attribute = '#' + attribute.id
+ local_attribute_full = '#' + attribute.id + '_autocompletion'
+ @callback = attribute.callback
+
+ # call calback on init
+ if @callback && attribute.value && @params
+ @callback( @params )
+
+ b = (event, item) =>
+ # set html form attribute
+ $(local_attribute).val(item.id).trigger('change')
+ $(local_attribute + '_autocompletion_value_shown').val(item.value)
+
+ # call calback
+ if @callback
+ params = App.ControllerForm.params(form)
+ @callback( params )
+ ###
+ $(@local_attribute_full).tagsInput(
+ autocomplete_url: '/users/search',
+ height: '30px',
+ width: '530px',
+ auto: {
+ source: '/users/search',
+ minLength: 2,
+ select: ( event, ui ) ->
+ #@log 'notice', 'selected', event, ui
+ b(event, ui.item)
+ }
+ )
+ ###
+ source = attribute.source
+ if typeof(source) is 'string'
+ source = source.replace('#{@apiPath}', App.Config.get('api_path') );
+ $(local_attribute_full).autocomplete(
+ source: source,
+ minLength: attribute.minLengt || 3,
+ select: ( event, ui ) =>
+ b(event, ui.item)
+ )
+ App.Delay.set( a, 280, undefined, 'form_autocompletion' )
+ item
diff --git a/app/assets/javascripts/app/controllers/_ui_element/boolean.js.coffee b/app/assets/javascripts/app/controllers/_ui_element/boolean.js.coffee
new file mode 100644
index 000000000..c201e6bea
--- /dev/null
+++ b/app/assets/javascripts/app/controllers/_ui_element/boolean.js.coffee
@@ -0,0 +1,25 @@
+class App.UiElement.boolean extends App.UiElement.ApplicationUiElement
+ @render: (attribute, params) ->
+
+ # build options list
+ if _.isEmpty(attribute.options)
+ attribute.options = [
+ { name: 'yes', value: true }
+ { name: 'no', value: false }
+ ]
+
+ # set data type
+ if attribute.name
+ attribute.name = '{boolean}' + attribute.name
+
+ # build options list based on config
+ @getConfigOptionList( attribute, params )
+
+ # sort attribute.options
+ @sortOptions( attribute, params )
+
+ # finde selected/checked item of list
+ @selectedOptions( attribute, params )
+
+ # return item
+ $( App.view('generic/select')( attribute: attribute ) )
\ No newline at end of file
diff --git a/app/assets/javascripts/app/controllers/_ui_element/checkbox.js.coffee b/app/assets/javascripts/app/controllers/_ui_element/checkbox.js.coffee
new file mode 100644
index 000000000..5f16de1eb
--- /dev/null
+++ b/app/assets/javascripts/app/controllers/_ui_element/checkbox.js.coffee
@@ -0,0 +1,25 @@
+class App.UiElement.checkbox extends App.UiElement.ApplicationUiElement
+ @render: (attribute, params) ->
+
+ # build options list based on config
+ @getConfigOptionList( attribute, params )
+
+ # build options list based on relation
+ @getRelationOptionList( attribute, params )
+
+ # add null selection if needed
+ @addNullOption( attribute, params )
+
+ # sort attribute.options
+ @sortOptions( attribute, params )
+
+ # finde selected/checked item of list
+ @selectedOptions( attribute, params )
+
+ # disable item of list
+ @disabledOptions( attribute, params )
+
+ # filter attributes
+ @filterOption( attribute, params )
+
+ $( App.view('generic/checkbox')( attribute: attribute ) )
diff --git a/app/assets/javascripts/app/controllers/_ui_element/date.js.coffee b/app/assets/javascripts/app/controllers/_ui_element/date.js.coffee
new file mode 100644
index 000000000..f8b9db2ac
--- /dev/null
+++ b/app/assets/javascripts/app/controllers/_ui_element/date.js.coffee
@@ -0,0 +1,139 @@
+class App.UiElement.date
+ @render: (attribute) ->
+
+ # set data type
+ if attribute.name
+ attribute.nameRaw = attribute.name
+ attribute.name = '{date}' + attribute.name
+ if attribute.value
+ if typeof( attribute.value ) is 'string'
+ unixtime = new Date( Date.parse( "#{attribute.value}T00:00:00Z" ) )
+ else
+ unixtime = new Date( attribute.value )
+ year = unixtime.getUTCFullYear()
+ month = unixtime.getUTCMonth() + 1
+ day = unixtime.getUTCDate()
+ hour = unixtime.getUTCHours()
+ minute = unixtime.getUTCMinutes()
+ item = $( App.view('generic/date')(
+ attribute: attribute
+ year: year
+ month: month
+ day: day
+ ) )
+
+ setNewTime = (diff, el, reset) ->
+ name = $(el).closest('.form-group').find('[data-name]').attr('data-name')
+
+ # remove old validation
+ item.find('.has-error').removeClass('has-error')
+ item.closest('.form-group').find('.help-inline').html('')
+
+ day = item.closest('.form-group').find("[name=\"{date}#{name}___day\"]").val()
+ month = item.closest('.form-group').find("[name=\"{date}#{name}___month\"]").val()
+ year = item.closest('.form-group').find("[name=\"{date}#{name}___year\"]").val()
+ format = (number) ->
+ if parseInt(number) < 10
+ number = "0#{number}"
+ number
+ if !reset && (year isnt '' && month isnt '' && day isnt '')
+ time = new Date( Date.parse( "#{year}-#{format(month)}-#{format(day)}T00:00:00Z" ) )
+ time.setMinutes( time.getMinutes() + diff + time.getTimezoneOffset() )
+ else
+ time = new Date()
+ time.setMinutes( time.getMinutes() + diff )
+ item.closest('.form-group').find("[name=\"{date}#{name}___day\"]").val( time.getDate() )
+ item.closest('.form-group').find("[name=\"{date}#{name}___month\"]").val( time.getMonth()+1 )
+ item.closest('.form-group').find("[name=\"{date}#{name}___year\"]").val( time.getFullYear() )
+
+ item.find('.js-today').bind('click', (e) ->
+ e.preventDefault()
+ setNewTime(0, @, true)
+ )
+ item.find('.js-plus-day').bind('click', (e) ->
+ e.preventDefault()
+ setNewTime(60 * 24, @)
+ )
+ item.find('.js-minus-day').bind('click', (e) ->
+ e.preventDefault()
+ setNewTime(-60 * 24, @)
+ )
+ item.find('.js-plus-week').bind('click', (e) ->
+ e.preventDefault()
+ setNewTime(60 * 24 * 7, @)
+ )
+ item.find('.js-minus-week').bind('click', (e) ->
+ e.preventDefault()
+ setNewTime(-60 * 24 * 7, @)
+ )
+
+ item.find('input').bind('keyup blur focus change', (e) ->
+
+ # do validation
+ name = $(@).attr('name')
+ if name
+ fieldPrefix = name.split('___')[0]
+
+ # remove old validation
+ item.find('.has-error').removeClass('has-error')
+ item.closest('.form-group').find('.help-inline').html('')
+
+ day = item.closest('.form-group').find("[name=\"#{fieldPrefix}___day\"]").val()
+ month = item.closest('.form-group').find("[name=\"#{fieldPrefix}___month\"]").val()
+ year = item.closest('.form-group').find("[name=\"#{fieldPrefix}___year\"]").val()
+
+ # validate exists
+ errors = {}
+ if !day
+ errors.day = 'missing'
+ if !month
+ errors.month = 'missing'
+ if !year
+ errors.year = 'missing'
+
+ # ranges
+ if day
+ daysInMonth = 31
+ if month && year
+ daysInMonth = new Date(year, month, 0).getDate();
+
+ if parseInt(day).toString() is 'NaN'
+ errors.day = 'invalid'
+ else if parseInt(day) > daysInMonth || parseInt(day) < 1
+ errors.day = 'invalid'
+
+ if month
+ if parseInt(month).toString() is 'NaN'
+ errors.month = 'invalid'
+ else if parseInt(month) > 12 || parseInt(month) < 1
+ errors.month = 'invalid'
+
+ if year
+ if parseInt(year).toString() is 'NaN'
+ errors.year = 'invalid'
+ else if parseInt(year) > 2100 || parseInt(year) < 2001
+ errors.year = 'invalid'
+
+ if !_.isEmpty(errors)
+
+ # if field is required, if not do not show error
+ if year is '' && day is '' && month
+ if attribute.null
+ e.preventDefault()
+ e.stopPropagation()
+ return
+ else
+ item.closest('.form-group').find('.help-inline').text( 'is required' )
+
+ # show invalid options
+ for key, value of errors
+ item.closest('.form-group').addClass('has-error')
+ item.closest('.form-group').find("[name=\"#{fieldPrefix}___#{key}\"]").addClass('has-error')
+ #item.closest('.form-group').find('.help-inline').text( value )
+
+ e.preventDefault()
+ e.stopPropagation()
+ return
+ )
+
+ item
\ No newline at end of file
diff --git a/app/assets/javascripts/app/controllers/_ui_element/datetime.js.coffee b/app/assets/javascripts/app/controllers/_ui_element/datetime.js.coffee
new file mode 100644
index 000000000..e806b76f4
--- /dev/null
+++ b/app/assets/javascripts/app/controllers/_ui_element/datetime.js.coffee
@@ -0,0 +1,172 @@
+class App.UiElement.datetime
+ @render: (attribute) ->
+
+ # set data type
+ if attribute.name
+ attribute.nameRaw = attribute.name
+ attribute.name = '{datetime}' + attribute.name
+ if attribute.value
+ if typeof( attribute.value ) is 'string'
+ unixtime = new Date( Date.parse( attribute.value ) )
+ else
+ unixtime = new Date( attribute.value )
+ year = unixtime.getFullYear()
+ month = unixtime.getMonth() + 1
+ day = unixtime.getDate()
+ hour = unixtime.getHours()
+ minute = unixtime.getMinutes()
+ item = $( App.view('generic/datetime')(
+ attribute: attribute
+ year: year
+ month: month
+ day: day
+ hour: hour
+ minute: minute
+ ) )
+
+ setNewTime = (diff, el, reset) ->
+ name = $(el).closest('.form-group').find('[data-name]').attr('data-name')
+
+ # remove old validation
+ item.find('.has-error').removeClass('has-error')
+ item.closest('.form-group').find('.help-inline').html('')
+
+ day = item.closest('.form-group').find("[name=\"{datetime}#{name}___day\"]").val()
+ month = item.closest('.form-group').find("[name=\"{datetime}#{name}___month\"]").val()
+ year = item.closest('.form-group').find("[name=\"{datetime}#{name}___year\"]").val()
+ hour = item.closest('.form-group').find("[name=\"{datetime}#{name}___hour\"]").val()
+ minute = item.closest('.form-group').find("[name=\"{datetime}#{name}___minute\"]").val()
+ format = (number) ->
+ if parseInt(number) < 10
+ number = "0#{number}"
+ number
+ if !reset && (year isnt '' && month isnt '' && day isnt '' && hour isnt '' && day isnt '')
+ time = new Date( Date.parse( "#{year}-#{format(month)}-#{format(day)}T#{format(hour)}:#{format(minute)}:00Z" ) )
+ time.setMinutes( time.getMinutes() + diff + time.getTimezoneOffset() )
+ else
+ time = new Date()
+ time.setMinutes( time.getMinutes() + diff )
+ #console.log('T', time, time.getHours(), time.getMinutes())
+ item.closest('.form-group').find("[name=\"{datetime}#{name}___day\"]").val( time.getDate() )
+ item.closest('.form-group').find("[name=\"{datetime}#{name}___month\"]").val( time.getMonth()+1 )
+ item.closest('.form-group').find("[name=\"{datetime}#{name}___year\"]").val( time.getFullYear() )
+ item.closest('.form-group').find("[name=\"{datetime}#{name}___hour\"]").val( time.getHours() )
+ item.closest('.form-group').find("[name=\"{datetime}#{name}___minute\"]").val( time.getMinutes() )
+
+ item.find('.js-today').bind('click', (e) ->
+ e.preventDefault()
+ setNewTime(0, @, true)
+ )
+ item.find('.js-plus-hour').bind('click', (e) ->
+ e.preventDefault()
+ setNewTime(60, @)
+ )
+ item.find('.js-minus-hour').bind('click', (e) ->
+ e.preventDefault()
+ setNewTime(-60, @)
+ )
+ item.find('.js-plus-day').bind('click', (e) ->
+ e.preventDefault()
+ setNewTime(60 * 24, @)
+ )
+ item.find('.js-minus-day').bind('click', (e) ->
+ e.preventDefault()
+ setNewTime(-60 * 24, @)
+ )
+ item.find('.js-plus-week').bind('click', (e) ->
+ e.preventDefault()
+ setNewTime(60 * 24 * 7, @)
+ )
+ item.find('.js-minus-week').bind('click', (e) ->
+ e.preventDefault()
+ setNewTime(-60 * 24 * 7, @)
+ )
+
+ item.find('input').bind('keyup blur focus change', (e) ->
+
+ # do validation
+ name = $(@).attr('name')
+ if name
+ fieldPrefix = name.split('___')[0]
+
+ # remove old validation
+ item.find('.has-error').removeClass('has-error')
+ item.closest('.form-group').find('.help-inline').html('')
+
+ day = item.closest('.form-group').find("[name=\"#{fieldPrefix}___day\"]").val()
+ month = item.closest('.form-group').find("[name=\"#{fieldPrefix}___month\"]").val()
+ year = item.closest('.form-group').find("[name=\"#{fieldPrefix}___year\"]").val()
+ hour = item.closest('.form-group').find("[name=\"#{fieldPrefix}___hour\"]").val()
+ minute = item.closest('.form-group').find("[name=\"#{fieldPrefix}___minute\"]").val()
+
+ # validate exists
+ errors = {}
+ if !day
+ errors.day = 'missing'
+ if !month
+ errors.month = 'missing'
+ if !year
+ errors.year = 'missing'
+ if !hour
+ errors.hour = 'missing'
+ if !minute
+ errors.minute = 'missing'
+
+ # ranges
+ if day
+ daysInMonth = 31
+ if month && year
+ daysInMonth = new Date(year, month, 0).getDate();
+
+ if parseInt(day).toString() is 'NaN'
+ errors.day = 'invalid'
+ else if parseInt(day) > daysInMonth || parseInt(day) < 1
+ errors.day = 'invalid'
+
+ if month
+ if parseInt(month).toString() is 'NaN'
+ errors.month = 'invalid'
+ else if parseInt(month) > 12 || parseInt(month) < 1
+ errors.month = 'invalid'
+
+ if year
+ if parseInt(year).toString() is 'NaN'
+ errors.year = 'invalid'
+ else if parseInt(year) > 2100 || parseInt(year) < 2001
+ errors.year = 'invalid'
+
+ if hour
+ if parseInt(hour).toString() is 'NaN'
+ errors.hour = 'invalid'
+ else if parseInt(hour) > 23 || parseInt(hour) < 0
+ errors.hour = 'invalid'
+
+ if minute
+ if parseInt(minute).toString() is 'NaN'
+ errors.minute = 'invalid'
+ else if parseInt(minute) > 59
+ errors.minute = 'invalid'
+
+ if !_.isEmpty(errors)
+
+ # if field is required, if not do not show error
+ if year is '' && day is '' && month is '' && hour is '' && minute is ''
+ if attribute.null
+ e.preventDefault()
+ e.stopPropagation()
+ return
+ else
+ item.closest('.form-group').find('.help-inline').text( 'is required' )
+
+ # show invalid options
+ for key, value of errors
+ item.closest('.form-group').addClass('has-error')
+ item.closest('.form-group').find("[name=\"#{fieldPrefix}___#{key}\"]").addClass('has-error')
+ #item.closest('.form-group').find('.help-inline').text( value )
+
+ e.preventDefault()
+ e.stopPropagation()
+ return
+ )
+
+ item
diff --git a/app/assets/javascripts/app/controllers/_ui_element/postmaster_match.js.coffee b/app/assets/javascripts/app/controllers/_ui_element/postmaster_match.js.coffee
new file mode 100644
index 000000000..2065ffb86
--- /dev/null
+++ b/app/assets/javascripts/app/controllers/_ui_element/postmaster_match.js.coffee
@@ -0,0 +1,176 @@
+class App.UiElement.postmaster_match
+ @render: (attribute, params, form_controller) ->
+ addItem = (key, displayName, el, defaultValue = '') =>
+ add = { name: key, display: displayName, tag: 'input', null: false, default: defaultValue }
+ itemInput = $( form_controller.formGenItem( add ).append('' ) )
+
+ # remove on click
+ itemInput.find('.remove').bind('click', (e) ->
+ e.preventDefault()
+ key = $(e.target).closest('.form-group').find('select, input').attr('name')
+ return if !key
+ $(e.target).closest('.controls').find('.addSelection select option[value="' + key + '"]').show()
+ $(e.target).closest('.controls').find('.addSelection select option[value="' + key + '"]').prop('disabled', false)
+ $(e.target).closest('.form-group').remove()
+ )
+
+ # add new item
+ control = el.closest('.postmaster_match')
+ control.find('.list').append(itemInput)
+ control.find('.addSelection select').val('')
+ control.find('.addSelection select option[value="' + key + '"]').prop('disabled', true)
+ control.find('.addSelection select option[value="' + key + '"]').hide()
+
+ # scaffold of match elements
+ item = $('
+ ')
+
+ # select shown attributes
+ loopData = [
+ {
+ value: 'from'
+ name: 'From'
+ },
+ {
+ value: 'to'
+ name: 'To'
+ },
+ {
+ value: 'cc'
+ name: 'Cc'
+ },
+ {
+ value: 'subject'
+ name: 'Subject'
+ },
+ {
+ value: 'body'
+ name: 'Body'
+ },
+ {
+ value: ''
+ name: '-'
+ disable: true
+ },
+ {
+ value: 'x-any-recipient'
+ name: 'Any Recipient'
+ },
+ {
+ value: ''
+ name: '-'
+ disable: true
+ },
+ {
+ value: ''
+ name: '- ' + App.i18n.translateInline('expert settings') + ' -'
+ disable: true
+ },
+ {
+ value: ''
+ name: '-'
+ disable: true
+ },
+ {
+ value: 'x-spam-flag'
+ name: 'X-Spam-Flag'
+ },
+ {
+ value: 'x-spam-level'
+ name: 'X-Spam-Level'
+ },
+ {
+ value: 'x-spam-score'
+ name: 'X-Spam-Score'
+ },
+ {
+ value: 'x-spam-status'
+ name: 'X-Spam-Status'
+ },
+ {
+ value: 'importance'
+ name: 'Importance'
+ },
+ {
+ value: 'x-priority'
+ name: 'X-Priority'
+ },
+
+ {
+ value: 'organization'
+ name: 'Organization'
+ },
+
+ {
+ value: 'x-original-to'
+ name: 'X-Original-To'
+ },
+ {
+ value: 'delivered-to'
+ name: 'Delivered-To'
+ },
+ {
+ value: 'envelope-to'
+ name: 'Envelope-To'
+ },
+ {
+ value: 'return-path'
+ name: 'Return-Path'
+ },
+ {
+ value: 'mailing-list'
+ name: 'Mailing-List'
+ },
+ {
+ value: 'list-id'
+ name: 'List-Id'
+ },
+ {
+ value: 'list-archive'
+ name: 'List-Archive'
+ },
+ {
+ value: 'mailing-list'
+ name: 'Mailing-List'
+ },
+ {
+ value: 'auto-submitted'
+ name: 'Auto-Submitted'
+ },
+ {
+ value: 'x-loop'
+ name: 'X-Loop'
+ },
+ ]
+ for listItem in loopData
+ listItem.value = "#{ attribute.name }::#{listItem.value}"
+ add = { name: '', display: '', tag: 'select', multiple: false, null: false, nulloption: true, options: loopData, translate: true, required: false }
+ item.find('.addSelection').append( form_controller.formGenItem( add ) )
+
+ # bind add click
+ item.find('.add').bind('click', (e) ->
+ e.preventDefault()
+ name = $(@).closest('.controls').find('.addSelection').find('select').val()
+ displayName = $(@).closest('.controls').find('.addSelection').find('select option:selected').html()
+ return if !name
+ addItem( name, displayName, $(@) )
+ )
+
+ # show default values
+ loopDataValue = {}
+ if attribute.value
+ for key, value of attribute.value
+ displayName = key
+ for listItem in loopData
+ if listItem.value is "#{ attribute.name }::#{key}"
+ addItem( "#{ attribute.name }::#{key}", listItem.name, item.find('.add'), value )
+
+ item
diff --git a/app/assets/javascripts/app/controllers/_ui_element/postmaster_set.js.coffee b/app/assets/javascripts/app/controllers/_ui_element/postmaster_set.js.coffee
new file mode 100644
index 000000000..9c4170950
--- /dev/null
+++ b/app/assets/javascripts/app/controllers/_ui_element/postmaster_set.js.coffee
@@ -0,0 +1,124 @@
+class App.UiElement.postmaster_set
+ @render: (attribute, params, form_controller) ->
+ addItem = (key, displayName, el, defaultValue = '') =>
+ collection = undefined
+ for listItem in loopData
+ if listItem.value is key
+ collection = listItem
+ if collection.relation
+ add = { name: key, display: displayName, tag: 'select', multiple: false, null: false, nulloption: true, relation: collection.relation, translate: true, default: defaultValue }
+ else if collection.options
+ add = { name: key, display: displayName, tag: 'select', multiple: false, null: false, nulloption: true, options: collection.options, translate: true, default: defaultValue }
+ else
+ add = { name: key, display: displayName, tag: 'input', null: false, default: defaultValue }
+ itemInput = $( form_controller.formGenItem( add ).append('' ) )
+
+ # remove on click
+ itemInput.find('.remove').bind('click', (e) ->
+ e.preventDefault()
+ key = $(e.target).closest('.form-group').find('select, input').attr('name')
+ return if !key
+ $(e.target).closest('.controls').find('.addSelection select option[value="' + key + '"]').show()
+ $(e.target).closest('.controls').find('.addSelection select option[value="' + key + '"]').prop('disabled', false)
+ $(e.target).closest('.form-group').remove()
+ )
+
+ # add new item
+ control = el.closest('.perform_set')
+ control.find('.list').append(itemInput)
+ control.find('.addSelection select').val('')
+ control.find('.addSelection select option[value="' + key + '"]').prop('disabled', true)
+ control.find('.addSelection select option[value="' + key + '"]').hide()
+
+ # scaffold of perform elements
+ item = $('
+ ')
+
+
+ # select shown attributes
+ loopData = [
+ {
+ value: 'x-zammad-ticket-priority_id'
+ name: 'Ticket Priority'
+ relation: 'TicketPriority'
+ },
+ {
+ value: 'x-zammad-ticket-state_id'
+ name: 'Ticket State'
+ relation: 'TicketState'
+ },
+ {
+ value: 'x-zammad-ticket-customer'
+ name: 'Ticket Customer'
+ },
+ {
+ value: 'x-zammad-ticket-group_id'
+ name: 'Ticket Group'
+ relation: 'Group'
+ },
+ {
+ value: 'x-zammad-ticket-owner'
+ name: 'Ticket Owner'
+ },
+ {
+ value: ''
+ name: '-'
+ disable: true
+ },
+ {
+ value: 'x-zammad-article-internal'
+ name: 'Article Internal'
+ options: { true: 'Yes', false: 'No'}
+ },
+ {
+ value: 'x-zammad-article-type_id'
+ name: 'Article Type'
+ relation: 'TicketArticleType'
+ },
+ {
+ value: 'x-zammad-article-sender_id'
+ name: 'Article Sender'
+ relation: 'TicketArticleSender'
+ },
+ {
+ value: ''
+ name: '-'
+ disable: true
+ },
+ {
+ value: 'x-zammad-ignore'
+ name: 'Ignore Message'
+ options: { true: 'Yes', false: 'No'}
+ },
+ ]
+ for listItem in loopData
+ listItem.value = "#{ attribute.name }::#{listItem.value}"
+ add = { name: '', display: '', tag: 'select', multiple: false, null: false, nulloption: true, options: loopData, translate: true, required: false }
+ item.find('.addSelection').append( form_controller.formGenItem( add ) )
+
+ item.find('.add').bind('click', (e) ->
+ e.preventDefault()
+ name = $(@).closest('.controls').find('.addSelection').find('select').val()
+ displayName = $(@).closest('.controls').find('.addSelection').find('select option:selected').html()
+ return if !name
+ addItem( name, displayName, $(@) )
+ )
+
+ # show default values
+ loopDataValue = {}
+ if attribute.value
+ for key, value of attribute.value
+ displayName = key
+ for listItem in loopData
+ if listItem.value is "#{ attribute.name }::#{key}"
+ addItem( "#{ attribute.name }::#{key}", listItem.name, item.find('.add'), value )
+
+ item
\ No newline at end of file
diff --git a/app/assets/javascripts/app/controllers/_ui_element/radio.js.coffee b/app/assets/javascripts/app/controllers/_ui_element/radio.js.coffee
new file mode 100644
index 000000000..8f94a4b49
--- /dev/null
+++ b/app/assets/javascripts/app/controllers/_ui_element/radio.js.coffee
@@ -0,0 +1,25 @@
+class App.UiElement.radio extends App.UiElement.ApplicationUiElement
+ @render: (attribute, params) ->
+
+ # build options list based on config
+ @getConfigOptionList( attribute, params )
+
+ # build options list based on relation
+ @getRelationOptionList( attribute, params )
+
+ # add null selection if needed
+ @addNullOption( attribute, params )
+
+ # sort attribute.options
+ @sortOptions( attribute, params )
+
+ # finde selected/checked item of list
+ @selectedOptions( attribute, params )
+
+ # disable item of list
+ @disabledOptions( attribute, params )
+
+ # filter attributes
+ @filterOption( attribute, params )
+
+ $( App.view('generic/radio')( attribute: attribute ) )
diff --git a/app/assets/javascripts/app/controllers/_ui_element/richtext.js.coffee b/app/assets/javascripts/app/controllers/_ui_element/richtext.js.coffee
new file mode 100644
index 000000000..058c4a1cb
--- /dev/null
+++ b/app/assets/javascripts/app/controllers/_ui_element/richtext.js.coffee
@@ -0,0 +1,100 @@
+class App.UiElement.richtext
+ @render: (attribute) ->
+
+ item = $( App.view('generic/richtext')( attribute: attribute ) )
+ item.find('[contenteditable]').ce(
+ mode: attribute.type
+ maxlength: attribute.maxlength
+ )
+ if attribute.upload
+ item.append( $( App.view('generic/attachment')( attribute: attribute ) ) )
+
+ renderAttachment = (file) =>
+ item.find('.attachments').append( App.view('generic/attachment_item')(
+ fileName: file.filename
+ fileSize: @humanFileSize( file.size )
+ store_id: file.store_id
+ ))
+ item.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()
+ )
+
+ @attachments = []
+ @progressBar = item.find('.attachmentUpload-progressBar')
+ @progressText = item.find('.js-percentage')
+ @attachmentPlaceholder = item.find('.attachmentPlaceholder')
+ @attachmentUpload = item.find('.attachmentUpload')
+ @attachmentsHolder = item.find('.attachments')
+ @cancelContainer = item.find('.js-cancel')
+
+ u = => html5Upload.initialize(
+ uploadUrl: App.Config.get('api_path') + '/ticket_attachment_upload',
+ dropContainer: item.closest('form').get(0),
+ cancelContainer: @cancelContainer,
+ inputField: item.find( '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))
+ )
+ )
+ App.Delay.set( u, 100, undefined, 'form_upload' )
+ item
\ No newline at end of file
diff --git a/app/assets/javascripts/app/controllers/_ui_element/searchable_select.js.coffee b/app/assets/javascripts/app/controllers/_ui_element/searchable_select.js.coffee
new file mode 100644
index 000000000..bc44fbd8c
--- /dev/null
+++ b/app/assets/javascripts/app/controllers/_ui_element/searchable_select.js.coffee
@@ -0,0 +1,3 @@
+class App.UiElement.searchable_select
+ @render: (attribute) ->
+ new App.SearchableSelect( attribute: attribute ).element()
diff --git a/app/assets/javascripts/app/controllers/_ui_element/select.js.coffee b/app/assets/javascripts/app/controllers/_ui_element/select.js.coffee
new file mode 100644
index 000000000..bf1f79976
--- /dev/null
+++ b/app/assets/javascripts/app/controllers/_ui_element/select.js.coffee
@@ -0,0 +1,32 @@
+class App.UiElement.select extends App.UiElement.ApplicationUiElement
+ @render: (attribute, params, form_controller) ->
+
+ # set multible option
+ if attribute.multiple
+ attribute.multiple = 'multiple'
+ else
+ attribute.multiple = ''
+
+ # build options list based on config
+ @getConfigOptionList( attribute, params )
+
+ # build options list based on relation
+ @getRelationOptionList( attribute, params )
+
+ # add null selection if needed
+ @addNullOption( attribute, params )
+
+ # sort attribute.options
+ @sortOptions( attribute, params )
+
+ # finde selected/checked item of list
+ @selectedOptions( attribute, params )
+
+ # disable item of list
+ @disabledOptions( attribute, params )
+
+ # filter attributes
+ @filterOption( attribute, params )
+
+ # return item
+ $( App.view('generic/select')( attribute: attribute ) )
\ No newline at end of file
diff --git a/app/assets/javascripts/app/controllers/_ui_element/sla_times.js.coffee b/app/assets/javascripts/app/controllers/_ui_element/sla_times.js.coffee
new file mode 100644
index 000000000..76b18f04d
--- /dev/null
+++ b/app/assets/javascripts/app/controllers/_ui_element/sla_times.js.coffee
@@ -0,0 +1,4 @@
+class App.UiElement.sla_times
+ @render: (attribute) ->
+
+ $( App.view('generic/sla_times')( attribute: attribute ) )
diff --git a/app/assets/javascripts/app/controllers/_ui_element/tag.js.coffee b/app/assets/javascripts/app/controllers/_ui_element/tag.js.coffee
new file mode 100644
index 000000000..19c66ccc6
--- /dev/null
+++ b/app/assets/javascripts/app/controllers/_ui_element/tag.js.coffee
@@ -0,0 +1,8 @@
+class App.UiElement.tag
+ @render: (attribute) ->
+ item = $( App.view('generic/input')( attribute: attribute ) )
+ a = =>
+ $('#' + attribute.id ).tokenfield()
+ $('#' + attribute.id ).parent().css('height', 'auto')
+ App.Delay.set( a, 120, undefined, 'tags' )
+ item
\ No newline at end of file
diff --git a/app/assets/javascripts/app/controllers/_ui_element/textarea.js.coffee b/app/assets/javascripts/app/controllers/_ui_element/textarea.js.coffee
new file mode 100644
index 000000000..e993837f8
--- /dev/null
+++ b/app/assets/javascripts/app/controllers/_ui_element/textarea.js.coffee
@@ -0,0 +1,41 @@
+class App.UiElement.textarea
+ @render: (attribute) ->
+ fileUploaderId = 'file-uploader-' + new Date().getTime() + '-' + Math.floor( Math.random() * 99999 )
+ item = $( App.view('generic/textarea')( attribute: attribute ) + '' )
+
+ a = =>
+ visible = $( item[0] ).is(":visible")
+ if visible && !$( item[0] ).expanding('active')
+ $( item[0] ).expanding()
+ $( item[0] ).on('focus', ->
+ visible = $( item[0] ).is(":visible")
+ if visible && !$( item[0] ).expanding('active')
+ $( item[0] ).expanding().focus()
+ )
+ App.Delay.set( a, 80 )
+
+ if attribute.upload
+
+ # add file uploader
+ u = =>
+ # only add upload item if html element exists
+ if @el.find('#' + fileUploaderId )[0]
+ @el.find('#' + fileUploaderId ).fineUploader(
+ request:
+ endpoint: App.Config.get('api_path') + '/ticket_attachment_upload'
+ params:
+ form_id: @form_id
+ text:
+ uploadButton: ''
+ template: '' +
+ '
{dragZoneText}
' +
+ '
{uploadButtonText}
' +
+ '
' +
+ '
',
+ classes:
+ success: ''
+ fail: ''
+ debug: false
+ )
+ App.Delay.set( u, 100, undefined, 'form_upload' )
+ item
\ No newline at end of file
diff --git a/app/assets/javascripts/app/controllers/_ui_element/timezone.js.coffee b/app/assets/javascripts/app/controllers/_ui_element/timezone.js.coffee
new file mode 100644
index 000000000..d3a22f303
--- /dev/null
+++ b/app/assets/javascripts/app/controllers/_ui_element/timezone.js.coffee
@@ -0,0 +1,25 @@
+class App.UiElement.timezone extends App.UiElement.ApplicationUiElement
+ @render: (attribute) ->
+
+ attribute.options = []
+ timezones = App.Config.get('timezones')
+
+ # build list based on config
+ for timezone_value, timezone_diff of timezones
+ if timezone_diff > 0
+ timezone_diff = '+' + timezone_diff
+ item =
+ name: "#{timezone_value} (GMT#{timezone_diff})"
+ value: timezone_value
+ attribute.options.push item
+
+ # add null selection if needed
+ @addNullOption( attribute, params )
+
+ # sort attribute.options
+ @sortOptions( attribute, params )
+
+ # finde selected/checked item of list
+ @selectedOptions( attribute, params )
+
+ $( App.view('generic/select')( attribute: attribute ) )
diff --git a/app/assets/javascripts/app/controllers/_ui_element/user_autocompletion.js.coffee b/app/assets/javascripts/app/controllers/_ui_element/user_autocompletion.js.coffee
new file mode 100644
index 000000000..e5e7c3495
--- /dev/null
+++ b/app/assets/javascripts/app/controllers/_ui_element/user_autocompletion.js.coffee
@@ -0,0 +1,3 @@
+class App.UiElement.user_autocompletion
+ @render: (attribute) ->
+ new App.UserOrganizationAutocompletion( attribute: attribute ).element()
diff --git a/app/assets/javascripts/app/index.js.coffee b/app/assets/javascripts/app/index.js.coffee
index 903beea27..11f9c2732 100644
--- a/app/assets/javascripts/app/index.js.coffee
+++ b/app/assets/javascripts/app/index.js.coffee
@@ -254,4 +254,6 @@ class App extends Spine.Controller
JST["app/views/#{name}"](params)
template
+class App.UiElement
+
window.App = App