Fixes #3709, #Fixes 1262 - Core Workflow implementation

This commit is contained in:
Rolf Schmidt 2021-08-25 14:24:42 +02:00 committed by Thorsten Eckel
parent 60a3758d38
commit 05a471f90d
145 changed files with 6338 additions and 1910 deletions

View File

@ -10,16 +10,13 @@ class App.ControllerForm extends App.Controller
@[key] = value
if !@handlers
@handlers = []
@handlers = [ App.FormHandlerCoreWorkflow.run ]
if @handlersConfig
for key, value of @handlersConfig
if value && value.run
@handlers.push value.run
@handlers.push @showHideToggle
@handlers.push @requiredMandantoryToggle
if !@model
@model = {}
if !@attributes
@ -312,51 +309,20 @@ class App.ControllerForm extends App.Controller
else
throw "Invalid UiElement.#{attribute.tag}"
if attribute.only_shown_if_selectable
count = Object.keys(attribute.options).length
if !attribute.null && (attribute.nulloption && count is 2) || (!attribute.nulloption && count is 1)
attribute.transparent = true
attributesNew = clone(attribute)
attributesNew.type = 'hidden'
attributesNew.value = ''
for item in attribute.options
if item.value && item.value isnt ''
attributesNew.value = item.value
item = $( App.view('generic/input')(attribute: attributesNew) )
if @handlers
item.bind('change', (e) =>
params = App.ControllerForm.params($(e.target))
item_bind = item
item_event = 'change'
if item.find('.richtext-content').length > 0
item_bind = item.find('.richtext-content')
item_event = 'blur'
item_bind.bind(item_event, (e) =>
@lastChangedAttribute = attribute.name
params = App.ControllerForm.params(@form)
for handler in @handlers
handler(params, attribute, @attributes, idPrefix, form, @)
)
# bind dependency
if @dependency
for action in @dependency
# bind on element if name is matching
if action.bind && action.bind.name is attribute.name
ui = @
do (action, attribute) ->
item.bind('change', ->
value = $(@).val()
if !value
value = $(@).find('select, input').val()
# lookup relation if needed
if action.bind.relation
data = App[action.bind.relation].find(value)
value = data.name
# check if value is used in condition
if _.contains(action.bind.value, value)
if action.change.action is 'hide'
ui.hide(action.change.name)
else
ui.show(action.change.name)
)
if !attribute.display || attribute.transparent
# hide/show item
@ -387,82 +353,96 @@ class App.ControllerForm extends App.Controller
return fullItem
@findFieldByName: (key, el) ->
return el.find('[name="' + key + '"]')
@findFieldByData: (key, el) ->
return el.find('[data-name="' + key + '"]')
@findFieldByGroup: (key, el) ->
return el.find('.form-group[data-attribute-name="' + key + '"]')
@fieldIsShown: (field) ->
return !field.closest('.form-group').hasClass('is-hidden')
@fieldIsMandatory: (field) ->
return field.closest('.form-group').hasClass('is-required')
@fieldIsRemoved: (field) ->
return field.closest('.form-group').hasClass('is-removed')
attributeIsMandatory: (name) ->
field_by_name = @constructor.findFieldByName(name, @form)
if field_by_name.length > 0
return @constructor.fieldIsMandatory(field_by_name)
field_by_data = @constructor.findFieldByData(name, @form)
if field_by_data.length > 0
return @constructor.fieldIsMandatory(field_by_data)
return false
show: (name, el = @form) ->
if !_.isArray(name)
name = [name]
for key in name
el.find('[name="' + key + '"]').closest('.form-group').removeClass('hide')
el.find('[name="' + key + '"]').removeClass('is-hidden')
el.find('[data-name="' + key + '"]').closest('.form-group').removeClass('hide')
el.find('[data-name="' + key + '"]').removeClass('is-hidden')
field_by_group = @constructor.findFieldByGroup(key, el)
field_by_group.removeClass('hide')
field_by_group.removeClass('is-hidden')
field_by_group.removeClass('is-removed')
# hide old validation states
if el
el.find('.has-error').removeClass('has-error')
el.find('.help-inline').html('')
hide: (name, el = @form) ->
hide: (name, el = @form, remove = false) ->
if !_.isArray(name)
name = [name]
for key in name
el.find('[name="' + key + '"]').closest('.form-group').addClass('hide')
el.find('[name="' + key + '"]').addClass('is-hidden')
el.find('[data-name="' + key + '"]').closest('.form-group').addClass('hide')
el.find('[data-name="' + key + '"]').addClass('is-hidden')
field_by_group = @constructor.findFieldByGroup(key, el)
field_by_group.addClass('hide')
field_by_group.addClass('is-hidden')
if remove
field_by_group.addClass('is-removed')
mandantory: (name, el = @form) ->
if !_.isArray(name)
name = [name]
for key in name
el.find('[name="' + key + '"]').attr('required', true)
el.find('[name="' + key + '"]').parents('.form-group').find('label span').html('*')
field_by_name = @constructor.findFieldByName(key, el)
field_by_data = @constructor.findFieldByData(key, el)
if !@constructor.fieldIsMandatory(field_by_name)
field_by_name.attr('required', true)
field_by_name.parents('.form-group').find('label span').html('*')
field_by_name.closest('.form-group').addClass('is-required')
if !@constructor.fieldIsMandatory(field_by_data)
field_by_data.attr('required', true)
field_by_data.parents('.form-group').find('label span').html('*')
field_by_data.closest('.form-group').addClass('is-required')
optional: (name, el = @form) ->
if !_.isArray(name)
name = [name]
for key in name
el.find('[name="' + key + '"]').attr('required', false)
el.find('[name="' + key + '"]').parents('.form-group').find('label span').html('')
field_by_name = @constructor.findFieldByName(key, el)
field_by_data = @constructor.findFieldByData(key, el)
showHideToggle: (params, changedAttribute, attributes, _classname, form, ui) ->
for attribute in attributes
if attribute.shown_if
hit = false
for refAttribute, refValue of attribute.shown_if
if params[refAttribute]
if _.isArray(refValue)
for item in refValue
if params[refAttribute].toString() is item.toString()
hit = true
else if params[refAttribute].toString() is refValue.toString()
hit = true
if hit
ui.show(attribute.name, form)
else
ui.hide(attribute.name, form)
requiredMandantoryToggle: (params, changedAttribute, attributes, _classname, form, ui) ->
for attribute in attributes
if attribute.required_if
hit = false
for refAttribute, refValue of attribute.required_if
if params[refAttribute]
if _.isArray(refValue)
for item in refValue
if params[refAttribute].toString() is item.toString()
hit = true
else if params[refAttribute].toString() is refValue.toString()
hit = true
if hit
ui.mandantory(attribute.name, form)
else
ui.optional(attribute.name, form)
if @constructor.fieldIsMandatory(field_by_name)
field_by_name.attr('required', false)
field_by_name.parents('.form-group').find('label span').html('')
field_by_name.closest('.form-group').removeClass('is-required')
if @constructor.fieldIsMandatory(field_by_data)
field_by_data.attr('required', false)
field_by_data.parents('.form-group').find('label span').html('')
field_by_data.closest('.form-group').removeClass('is-required')
validate: (params) ->
App.Model.validate(
model: @model
params: params
screen: @screen
controllerForm: @
)
# get all params of the form
@ -488,9 +468,10 @@ class App.ControllerForm extends App.Controller
# array to names
for item in array
field = @findFieldByName(item.name, lookupForm)
# check if item is-hidden and should not be used
if lookupForm.find('[name="' + item.name + '"]').hasClass('is-hidden') || lookupForm.find('div[data-name="' + item.name + '"]').hasClass('is-hidden')
# check if item is-removed and should not be used
if @fieldIsRemoved(field)
delete param[item.name]
continue
@ -562,7 +543,7 @@ class App.ControllerForm extends App.Controller
# get {date}
if key.substr(0,6) is '{date}'
newKey = key.substr(6, key.length)
if lookupForm.find("[data-name=\"#{newKey}\"]").hasClass('is-hidden')
if lookupForm.find("[data-name=\"#{newKey}\"]").hasClass('is-removed')
param[newKey] = null
else if param[key]
try
@ -584,7 +565,7 @@ class App.ControllerForm extends App.Controller
# get {datetime}
else if key.substr(0,10) is '{datetime}'
newKey = key.substr(10, key.length)
if lookupForm.find("[data-name=\"#{newKey}\"]").hasClass('is-hidden')
if lookupForm.find("[data-name=\"#{newKey}\"]").hasClass('is-removed')
param[newKey] = null
else if param[key]
try
@ -606,6 +587,9 @@ class App.ControllerForm extends App.Controller
if parts[0] && parts[1] isnt undefined
if parts[1] isnt undefined && !inputSelectObject[ parts[0] ]
inputSelectObject[ parts[0] ] = {}
if parts[1] is ''
delete param[ key ]
continue
if parts[2] isnt undefined && !inputSelectObject[ parts[0] ][ parts[1] ]
inputSelectObject[ parts[0] ][ parts[1] ] = {}
if parts[3] isnt undefined && !inputSelectObject[ parts[0] ][ parts[1] ][ parts[2] ]
@ -631,7 +615,7 @@ class App.ControllerForm extends App.Controller
# get {business_hours}
if key.substr(0,16) is '{business_hours}'
newKey = key.substr(16, key.length)
if lookupForm.find("[data-name=\"#{newKey}\"]").hasClass('is-hidden')
if lookupForm.find("[data-name=\"#{newKey}\"]").hasClass('is-removed')
param[newKey] = null
else if param[key]
newParams = {}
@ -737,6 +721,9 @@ class App.ControllerForm extends App.Controller
form.prop('disabled', false)
@validate: (data) ->
if data.errors && Object.keys(data.errors).length == 1 && data.errors._core_workflow isnt undefined
App.FormHandlerCoreWorkflow.delaySubmit(data.errors._core_workflow.controllerForm, data.errors._core_workflow.target || data.form)
return
lookupForm = @findForm(data.form)

View File

@ -27,7 +27,9 @@ class App.ControllerGenericEdit extends App.ControllerModal
return false
# validate
errors = @item.validate()
errors = @item.validate(
controllerForm: @controller
)
if errors
@log 'error', errors
@formValidate( form: e.target, errors: errors )

View File

@ -175,6 +175,7 @@ class App.ControllerGenericIndex extends App.Controller
small: @small
large: @large
veryLarge: @veryLarge
handlers: @handlers
)
new: (e) ->
@ -186,6 +187,7 @@ class App.ControllerGenericIndex extends App.Controller
small: @small
large: @large
veryLarge: @veryLarge
handlers: @handlers
)
payload: (e) ->

View File

@ -10,7 +10,7 @@ class App.ControllerGenericNew extends App.ControllerModal
@controller = new App.ControllerForm(
model: App[ @genericObject ]
params: @item
screen: @screen || 'edit'
screen: @screen || 'create'
autofocus: true
handlers: @handlers
)
@ -28,7 +28,9 @@ class App.ControllerGenericNew extends App.ControllerModal
return false
# validate
errors = object.validate()
errors = object.validate(
controllerForm: @controller
)
if errors
@log 'error', errors
@formValidate( form: e.target, errors: errors )

View File

@ -0,0 +1,575 @@
# coffeelint: disable=camel_case_classes
class App.UiElement.ApplicationSelector
@defaults: (attribute = {}, params = {}) ->
defaults = ['ticket.state_id']
groups =
ticket:
name: 'Ticket'
model: 'Ticket'
article:
name: 'Article'
model: 'TicketArticle'
customer:
name: 'Customer'
model: 'User'
organization:
name: 'Organization'
model: 'Organization'
if attribute.executionTime
groups.execution_time =
name: 'Execution Time'
operators_type =
'^datetime$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)', 'within next (relative)', 'within last (relative)', 'till (relative)', 'from (relative)']
'^timestamp$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)', 'within next (relative)', 'within last (relative)', 'till (relative)', 'from (relative)']
'^date$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)', 'within next (relative)', 'within last (relative)']
'boolean$': ['is', 'is not']
'integer$': ['is', 'is not']
'^radio$': ['is', 'is not']
'^select$': ['is', 'is not']
'^tree_select$': ['is', 'is not']
'^input$': ['contains', 'contains not']
'^richtext$': ['contains', 'contains not']
'^textarea$': ['contains', 'contains not']
'^tag$': ['contains all', 'contains one', 'contains all not', 'contains one not']
if attribute.hasChanged
operators_type =
'^datetime$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)', 'within next (relative)', 'within last (relative)', 'till (relative)', 'from (relative)', 'has changed']
'^timestamp$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)', 'within next (relative)', 'within last (relative)', 'till (relative)', 'from (relative)', 'has changed']
'^date$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)', 'within next (relative)', 'within last (relative)', 'till (relative)', 'from (relative)', 'has changed']
'boolean$': ['is', 'is not', 'has changed']
'integer$': ['is', 'is not', 'has changed']
'^radio$': ['is', 'is not', 'has changed']
'^select$': ['is', 'is not', 'has changed']
'^tree_select$': ['is', 'is not', 'has changed']
'^input$': ['contains', 'contains not', 'has changed']
'^richtext$': ['contains', 'contains not', 'has changed']
'^textarea$': ['contains', 'contains not', 'has changed']
'^tag$': ['contains all', 'contains one', 'contains all not', 'contains one not']
operators_name =
'_id$': ['is', 'is not']
'_ids$': ['is', 'is not']
if attribute.hasChanged
operators_name =
'_id$': ['is', 'is not', 'has changed']
'_ids$': ['is', 'is not', 'has changed']
# merge config
elements = {}
if attribute.article is false
delete groups.article
if attribute.action
elements['ticket.action'] =
name: 'action'
display: 'Action'
tag: 'select'
null: false
translate: true
options:
create: 'created'
update: 'updated'
'update.merged_into': 'merged into'
'update.received_merge': 'received merge'
operator: ['is', 'is not']
for groupKey, groupMeta of groups
if groupKey is 'execution_time'
if attribute.executionTime
elements['execution_time.calendar_id'] =
name: 'calendar_id'
display: 'Calendar'
tag: 'select'
relation: 'Calendar'
null: false
translate: false
operator: ['is in working time', 'is not in working time']
else
for row in App[groupMeta.model].configure_attributes
# ignore passwords and relations
if row.type isnt 'password' && row.name.substr(row.name.length-4,4) isnt '_ids' && row.searchable isnt false
config = _.clone(row)
if config.type is 'email' || config.type is 'tel'
config.type = 'text'
for operatorRegEx, operator of operators_type
myRegExp = new RegExp(operatorRegEx, 'i')
if config.tag && config.tag.match(myRegExp)
config.operator = operator
elements["#{groupKey}.#{config.name}"] = config
for operatorRegEx, operator of operators_name
myRegExp = new RegExp(operatorRegEx, 'i')
if config.name && config.name.match(myRegExp)
config.operator = operator
elements["#{groupKey}.#{config.name}"] = config
if config.tag == 'select'
config.multiple = true
if attribute.out_of_office
elements['ticket.out_of_office_replacement_id'] =
name: 'out_of_office_replacement_id'
display: 'Out of office replacement'
tag: 'autocompletion_ajax'
relation: 'User'
null: false
translate: true
operator: ['is', 'is not']
# Remove 'has changed' operator from attributes which don't support the operator.
['ticket.created_at', 'ticket.updated_at'].forEach (element_name) ->
elements[element_name]['operator'] = elements[element_name]['operator'].filter (item) -> item != 'has changed'
elements['ticket.mention_user_ids'] =
name: 'mention_user_ids'
display: 'Subscribe'
tag: 'autocompletion_ajax'
relation: 'User'
null: false
translate: true
operator: ['is', 'is not']
[defaults, groups, elements]
@rowContainer: (groups, elements, attribute) ->
row = $( App.view('generic/application_selector_row')(
attribute: attribute
pre_condition: @HasPreCondition()
) )
selector = @buildAttributeSelector(groups, elements)
row.find('.js-attributeSelector').prepend(selector)
row
@emptyBody: (attribute) ->
return $( App.view('generic/application_selector_empty')(
attribute: attribute
) )
@render: (attribute, params = {}) ->
[defaults, groups, elements] = @defaults(attribute, params)
item = $( App.view('generic/application_selector')(attribute: attribute) )
# add filter
item.delegate('.js-add', 'click', (e) =>
element = $(e.target).closest('.js-filterElement')
# add first available attribute
field = undefined
for groupAndAttribute, _config of elements
if @hasDuplicateSelector()
field = groupAndAttribute
break
else if !item.find(".js-attributeSelector [value=\"#{groupAndAttribute}\"]:selected").get(0)
field = groupAndAttribute
break
return if !field
row = @rowContainer(groups, elements, attribute)
emptyRow = item.find('div.horizontal-filter-body')
if emptyRow.find('input.empty:hidden').length > 0 && @hasEmptySelectorAtStart()
emptyRow.parent().replaceWith(row)
else
element.after(row)
row.find('.js-attributeSelector select').trigger('change')
@rebuildAttributeSelectors(item, row, field, elements, {}, attribute)
if attribute.preview isnt false
@preview(item)
)
# remove filter
item.delegate('.js-remove', 'click', (e) =>
return if $(e.currentTarget).hasClass('is-disabled')
if @hasEmptySelectorAtStart()
if item.find('.js-remove').length > 1
$(e.target).closest('.js-filterElement').remove()
else
$(e.target).closest('.js-filterElement').find('div.horizontal-filter-body').html(@emptyBody(attribute))
else
$(e.target).closest('.js-filterElement').remove()
@updateAttributeSelectors(item)
if attribute.preview isnt false
@preview(item)
)
paramValue = {}
for groupAndAttribute, meta of params[attribute.name]
continue if !elements[groupAndAttribute]
paramValue[groupAndAttribute] = meta
# build initial params
if !_.isEmpty(paramValue)
@renderParamValue(item, attribute, params, paramValue)
else
if @hasEmptySelectorAtStart()
row = @rowContainer(groups, elements, attribute)
row.find('.horizontal-filter-body').html(@emptyBody(attribute))
item.filter('.js-filter').append(row)
else
for groupAndAttribute in defaults
# build and append
row = @rowContainer(groups, elements, attribute)
@rebuildAttributeSelectors(item, row, groupAndAttribute, elements, {}, attribute)
item.filter('.js-filter').append(row)
# change attribute selector
item.delegate('.js-attributeSelector select', 'change', (e) =>
elementRow = $(e.target).closest('.js-filterElement')
groupAndAttribute = elementRow.find('.js-attributeSelector option:selected').attr('value')
return if !groupAndAttribute
@rebuildAttributeSelectors(item, elementRow, groupAndAttribute, elements, {}, attribute)
@updateAttributeSelectors(item)
)
# change operator selector
item.delegate('.js-operator select', 'change', (e) =>
elementRow = $(e.target).closest('.js-filterElement')
groupAndAttribute = elementRow.find('.js-attributeSelector option:selected').attr('value')
return if !groupAndAttribute
@buildOperator(item, elementRow, groupAndAttribute, elements, {}, attribute)
)
# bind for preview
if attribute.preview isnt false
search = =>
@preview(item)
triggerSearch = ->
item.find('.js-previewCounterContainer').addClass('hide')
item.find('.js-previewLoader').removeClass('hide')
App.Delay.set(
search,
600,
'preview',
)
item.on('change', 'select', (e) ->
triggerSearch()
)
item.on('change keyup', 'input', (e) ->
triggerSearch()
)
@disableRemoveForOneAttribute(item)
item
@renderParamValue: (item, attribute, params, paramValue) ->
[defaults, groups, elements] = @defaults(attribute, params)
for groupAndAttribute, meta of paramValue
# build and append
row = @rowContainer(groups, elements, attribute)
@rebuildAttributeSelectors(item, row, groupAndAttribute, elements, meta, attribute)
item.filter('.js-filter').append(row)
@preview: (item) ->
params = App.ControllerForm.params(item)
App.Ajax.request(
id: 'application_selector'
type: 'POST'
url: "#{App.Config.get('api_path')}/tickets/selector"
data: JSON.stringify(params)
processData: true,
success: (data, status, xhr) =>
App.Collection.loadAssets(data.assets)
item.find('.js-previewCounterContainer').removeClass('hide')
item.find('.js-previewLoader').addClass('hide')
@ticketTable(data.ticket_ids, data.ticket_count, item)
)
@ticketTable: (ticket_ids, ticket_count, item) ->
item.find('.js-previewCounter').html(ticket_count)
new App.TicketList(
tableId: 'ticket-selector'
el: item.find('.js-previewTable')
ticket_ids: ticket_ids
)
@buildAttributeSelector: (groups, elements) ->
selection = $('<select class="form-control"></select>')
for groupKey, groupMeta of groups
groupKeyClass = groupKey.replace('.', '-')
displayName = App.i18n.translateInline(groupMeta.name)
selection.closest('select').append("<optgroup label=\"#{displayName}\" class=\"js-#{groupKeyClass}\"></optgroup>")
optgroup = selection.find("optgroup.js-#{groupKeyClass}")
for elementKey, elementGroup of elements
spacer = elementKey.split(/\./).slice(0, -1).join('.')
if spacer is groupKey
attributeConfig = elements[elementKey]
if attributeConfig.operator
displayName = App.i18n.translateInline(attributeConfig.display)
optgroup.append("<option value=\"#{elementKey}\">#{displayName}</option>")
selection
# disable - if we only have one attribute
@disableRemoveForOneAttribute: (elementFull) ->
if @hasEmptySelectorAtStart()
if elementFull.find('div.horizontal-filter-body input.empty:hidden').length > 0 && elementFull.find('.js-remove').length < 2
elementFull.find('.js-remove').addClass('is-disabled')
else
elementFull.find('.js-remove').removeClass('is-disabled')
else
if elementFull.find('.js-attributeSelector select').length > 1
elementFull.find('.js-remove').removeClass('is-disabled')
else
elementFull.find('.js-remove').addClass('is-disabled')
@updateAttributeSelectors: (elementFull) ->
if !@hasDuplicateSelector()
# enable all
elementFull.find('.js-attributeSelector select option').removeAttr('disabled')
# disable all used attributes
elementFull.find('.js-attributeSelector select').each(->
keyLocal = $(@).val()
elementFull.find('.js-attributeSelector select option[value="' + keyLocal + '"]').attr('disabled', true)
)
# disable - if we only have one attribute
@disableRemoveForOneAttribute(elementFull)
@rebuildAttributeSelectors: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
# set attribute
if groupAndAttribute
elementRow.find('.js-attributeSelector select').val(groupAndAttribute)
@buildOperator(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
@buildOperator: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
currentOperator = elementRow.find('.js-operator option:selected').attr('value')
name = "#{attribute.name}::#{groupAndAttribute}::operator"
if !meta.operator && currentOperator
meta.operator = currentOperator
selection = $("<select class=\"form-control\" name=\"#{name}\"></select>")
attributeConfig = elements[groupAndAttribute]
if attributeConfig.operator
# check if operator exists
operatorExists = false
for operator in attributeConfig.operator
if meta.operator is operator
operatorExists = true
break
if !operatorExists
for operator in attributeConfig.operator
meta.operator = operator
break
for operator in attributeConfig.operator
operatorName = App.i18n.translateInline(operator.replace(/_/g, ' '))
selected = ''
if !groupAndAttribute.match(/^ticket/) && operator is 'has changed'
# do nothing, only show "has changed" in ticket attributes
else
if meta.operator is operator
selected = 'selected="selected"'
selection.append("<option value=\"#{operator}\" #{selected}>#{operatorName}</option>")
selection
elementRow.find('.js-operator select').replaceWith(selection)
if @HasPreCondition()
@buildPreCondition(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
else
@buildValue(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
@buildPreCondition: (elementFull, elementRow, groupAndAttribute, elements, meta, attributeConfig) ->
currentOperator = elementRow.find('.js-operator option:selected').attr('value')
currentPreCondition = elementRow.find('.js-preCondition option:selected').attr('value')
if !meta.pre_condition
meta.pre_condition = currentPreCondition
toggleValue = =>
preCondition = elementRow.find('.js-preCondition option:selected').attr('value')
if preCondition isnt 'specific'
elementRow.find('.js-value select').html('')
elementRow.find('.js-value').addClass('hide')
else
elementRow.find('.js-value').removeClass('hide')
@buildValue(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
# force to use auto completion on user lookup
attribute = _.clone(attributeConfig)
name = "#{attribute.name}::#{groupAndAttribute}::value"
attributeSelected = elements[groupAndAttribute]
preCondition = false
if attributeSelected.relation is 'User'
preCondition = 'user'
attribute.tag = 'user_autocompletion'
if attributeSelected.relation is 'Organization'
preCondition = 'org'
attribute.tag = 'autocompletion_ajax'
if !preCondition
elementRow.find('.js-preCondition select').html('')
elementRow.find('.js-preCondition').closest('.controls').addClass('hide')
toggleValue()
@buildValue(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
return
elementRow.find('.js-preCondition').removeClass('hide')
name = "#{attribute.name}::#{groupAndAttribute}::pre_condition"
selection = $("<select class=\"form-control\" name=\"#{name}\" ></select>")
options = {}
if preCondition is 'user'
options =
'current_user.id': App.i18n.translateInline('current user')
'specific': App.i18n.translateInline('specific user')
'not_set': App.i18n.translateInline('not set (not defined)')
else if preCondition is 'org'
options =
'current_user.organization_id': App.i18n.translateInline('current user organization')
'specific': App.i18n.translateInline('specific organization')
'not_set': App.i18n.translateInline('not set (not defined)')
for key, value of options
selected = ''
if key is meta.pre_condition
selected = 'selected="selected"'
selection.append("<option value=\"#{key}\" #{selected}>#{App.i18n.translateInline(value)}</option>")
elementRow.find('.js-preCondition').closest('.controls').removeClass('hide')
elementRow.find('.js-preCondition select').replaceWith(selection)
elementRow.find('.js-preCondition select').bind('change', (e) ->
toggleValue()
)
@buildValue(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
toggleValue()
@buildValueConfigValue: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
return _.clone(attribute.value[groupAndAttribute]['value'])
@buildValueName: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
return "#{attribute.name}::#{groupAndAttribute}::value"
@buildValue: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
name = @buildValueName(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
# build new item
attributeConfig = elements[groupAndAttribute]
config = _.clone(attributeConfig)
if config.relation is 'User'
config.tag = 'user_autocompletion'
if config.relation is 'Organization'
config.tag = 'autocompletion_ajax'
# render ui element
item = ''
if config && App.UiElement[config.tag]
config['name'] = name
if attribute.value && attribute.value[groupAndAttribute]
config['value'] = @buildValueConfigValue(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
if 'multiple' of config
config.multiple = true
config.nulloption = false
if config.relation is 'User'
config.multiple = false
config.nulloption = false
config.guess = false
config.disableCreateObject = true
if config.relation is 'Organization'
config.multiple = false
config.nulloption = false
config.guess = false
if config.tag is 'checkbox'
config.tag = 'select'
tagSearch = "#{config.tag}_search"
if App.UiElement[tagSearch]
item = App.UiElement[tagSearch].render(config, {})
else
item = App.UiElement[config.tag].render(config, {})
if meta.operator is 'before (relative)' || meta.operator is 'within next (relative)' || meta.operator is 'within last (relative)' || meta.operator is 'after (relative)' || meta.operator is 'from (relative)' || meta.operator is 'till (relative)'
config['name'] = "#{attribute.name}::#{groupAndAttribute}"
if attribute.value && attribute.value[groupAndAttribute]
config['value'] = _.clone(attribute.value[groupAndAttribute])
item = App.UiElement['time_range'].render(config, {})
elementRow.find('.js-value').removeClass('hide').html(item)
if meta.operator is 'has changed'
elementRow.find('.js-value').addClass('hide')
elementRow.find('.js-preCondition').closest('.controls').addClass('hide')
else
elementRow.find('.js-value').removeClass('hide')
@humanText: (condition) ->
none = App.i18n.translateContent('No filter.')
return [none] if _.isEmpty(condition)
[defaults, groups, elements] = @defaults()
rules = []
for attribute, meta of condition
objectAttribute = attribute.split(/\./)
# get stored params
if meta && objectAttribute[1]
operator = meta.operator
value = meta.value
model = toCamelCase(objectAttribute[0])
config = elements[attribute]
valueHuman = []
if _.isArray(value)
for data in value
r = @humanTextLookup(config, data)
valueHuman.push r
else
valueHuman.push @humanTextLookup(config, value)
if valueHuman.join
valueHuman = valueHuman.join(', ')
rules.push "#{App.i18n.translateContent('Where')} <b>#{App.i18n.translateContent(model)} -> #{App.i18n.translateContent(config.display)}</b> #{App.i18n.translateContent(operator)} <b>#{valueHuman}</b>."
return [none] if _.isEmpty(rules)
rules
@humanTextLookup: (config, value) ->
return value if !App[config.relation]
return value if !App[config.relation].exists(value)
data = App[config.relation].fullLocal(value)
return value if !data
if data.displayName
return App.i18n.translateContent(data.displayName())
valueHuman.push App.i18n.translateContent(data.name)
@HasPreCondition: ->
return true
@hasEmptySelectorAtStart: ->
return false
@hasDuplicateSelector: ->
return false
@coreWorkflowCustomModulesActive: ->
enabled = false
for workflow in App.CoreWorkflow.all()
continue if !workflow.changeable
continue if !workflow.condition_saved['custom.module'] && !workflow.condition_selected['custom.module'] && !workflow.perform['custom.module']
enabled = true
break
return enabled

View File

@ -186,10 +186,20 @@ class App.UiElement.ApplicationUiElement
return if !attribute.filter
return if _.isEmpty(attribute.options)
return if typeof attribute.filter isnt 'function'
App.Log.debug 'ControllerForm', '_filterOption:filter-function'
if typeof attribute.filter is 'function'
App.Log.debug 'ControllerForm', '_filterOption:filter-function'
attribute.options = attribute.filter(attribute.options, attribute)
else if !attribute.relation && attribute.filter && _.isArray(attribute.filter)
@filterOptionArray(attribute)
attribute.options = attribute.filter(attribute.options, attribute)
@filterOptionArray: (attribute) ->
result = []
for option in attribute.options
for value in attribute.filter
if value.toString() == option.value.toString()
result.push(option)
attribute.options = result
# set selected attributes
@selectedOptions: (attribute) ->

View File

@ -0,0 +1,188 @@
# coffeelint: disable=camel_case_classes
class App.UiElement.core_workflow_condition extends App.UiElement.ApplicationSelector
@defaults: (attribute = {}, params = {}) ->
defaults = []
groups =
ticket:
name: 'Ticket'
model: 'Ticket'
model_show: ['Ticket']
group:
name: 'Group'
model: 'Group'
model_show: ['Group']
user:
name: 'User'
model: 'User'
model_show: ['User']
customer:
name: 'Customer'
model: 'User'
model_show: ['Ticket']
organization:
name: 'Organization'
model: 'Organization'
model_show: ['Organization']
'customer.organization':
name: 'Organization'
model: 'Organization'
model_show: ['Ticket']
session:
name: 'Session'
model: 'User'
model_show: ['Ticket']
showCustomModules = @coreWorkflowCustomModulesActive()
if showCustomModules
groups['custom'] =
name: 'Custom'
model_show: ['Ticket', 'User', 'Organization', 'Sla']
currentObject = params.object
if attribute.workflow_object isnt undefined
currentObject = attribute.workflow_object
if !_.isEmpty(currentObject)
for key, data of groups
continue if _.contains(data.model_show, currentObject)
delete groups[key]
operatorsType =
'active$': ['is']
'boolean$': ['is', 'is not', 'is set', 'not set']
'integer$': ['is', 'is not', 'is set', 'not set']
'^select$': ['is', 'is not', 'is set', 'not set']
'^tree_select$': ['is', 'is not', 'is set', 'not set']
'^(input|textarea|richtext)$': ['is', 'is not', 'is set', 'not set', 'regex match', 'regex mismatch']
operatorsName =
'_id$': ['is', 'is not', 'is set', 'not set']
'_ids$': ['is', 'is not', 'is set', 'not set']
# merge config
elements = {}
for groupKey, groupMeta of groups
if groupKey is 'custom'
continue if !showCustomModules
options = {}
for module in App.CoreWorkflowCustomModule.all()
options[module.name] = module.name
elements['custom.module'] = {
name: 'module',
display: 'Module',
tag: 'select',
multiple: true,
options: options,
null: false,
operator: ['match one module', 'match all modules', 'match no modules']
}
continue
if groupKey is 'session'
elements['session.role_ids'] = {
name: 'role_ids',
display: 'Role',
tag: 'select',
relation: 'Role',
null: false,
operator: ['is', 'is not'],
multiple: true
}
elements['session.group_ids_read'] = {
name: 'group_ids_read',
display: 'Group (read)',
tag: 'select',
relation: 'Group',
null: false,
operator: ['is', 'is not'],
multiple: true
}
elements['session.group_ids_create'] = {
name: 'group_ids_create',
display: 'Group (create)',
tag: 'select',
relation: 'Group',
null: false,
operator: ['is', 'is not'],
multiple: true
}
elements['session.group_ids_change'] = {
name: 'group_ids_change',
display: 'Group (change)',
tag: 'select',
relation: 'Group',
null: false,
operator: ['is', 'is not'],
multiple: true
}
elements['session.group_ids_overview'] = {
name: 'group_ids_overview',
display: 'Group (overview)',
tag: 'select',
relation: 'Group',
null: false,
operator: ['is', 'is not'],
multiple: true
}
elements['session.group_ids_full'] = {
name: 'group_ids_full',
display: 'Group (full)',
tag: 'select',
relation: 'Group',
null: false,
operator: ['is', 'is not'],
multiple: true
}
elements['session.permission_ids'] = {
name: 'permission_ids',
display: 'Permissions',
tag: 'select',
relation: 'Permission',
null: false,
operator: ['is', 'is not'],
multiple: true
}
for row in App[groupMeta.model].configure_attributes
continue if !_.contains(['input', 'textarea', 'richtext', 'select', 'integer', 'boolean', 'active', 'tree_select', 'autocompletion_ajax'], row.tag)
continue if groupKey is 'ticket' && _.contains(['number', 'title'], row.name)
# ignore passwords and relations
if row.type isnt 'password' && row.name.substr(row.name.length-4,4) isnt '_ids' && row.searchable isnt false
config = _.clone(row)
if config.tag is 'select'
config.multiple = true
config.default = undefined
if config.type is 'email' || config.type is 'tel'
config.type = 'text'
for operatorRegEx, operator of operatorsType
myRegExp = new RegExp(operatorRegEx, 'i')
if config.tag && config.tag.match(myRegExp)
config.operator = operator
elements["#{groupKey}.#{config.name}"] = config
for operatorRegEx, operator of operatorsName
myRegExp = new RegExp(operatorRegEx, 'i')
if config.name && config.name.match(myRegExp)
config.operator = operator
elements["#{groupKey}.#{config.name}"] = config
[defaults, groups, elements]
@buildValue: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
currentOperator = elementRow.find('.js-operator option:selected').attr('value')
name = @buildValueName(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
if _.contains(['is set', 'not set'], currentOperator)
elementRow.find('.js-value').addClass('hide').html('<input type="hidden" name="' + name + '" value="true" />')
return
super(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
@HasPreCondition: ->
return false
@hasEmptySelectorAtStart: ->
return true

View File

@ -0,0 +1,135 @@
# coffeelint: disable=camel_case_classes
class App.UiElement.core_workflow_perform extends App.UiElement.ApplicationSelector
@defaults: (attribute = {}, params = {}) ->
defaults = []
groups =
ticket:
name: 'Ticket'
model: 'Ticket'
model_show: ['Ticket']
group:
name: 'Group'
model: 'Group'
model_show: ['Group']
customer:
name: 'Customer'
model: 'User'
model_show: ['User']
organization:
name: 'Organization'
model: 'Organization'
model_show: ['Organization']
showCustomModules = @coreWorkflowCustomModulesActive()
if showCustomModules
groups['custom'] =
name: 'Custom'
model_show: ['Ticket', 'User', 'Organization', 'Sla']
currentObject = params.object
if attribute.workflow_object isnt undefined
currentObject = attribute.workflow_object
if !_.isEmpty(currentObject)
for key, data of groups
continue if _.contains(data.model_show, currentObject)
delete groups[key]
operatorsType =
'boolean$': ['show', 'hide', 'remove', 'set_mandatory', 'set_optional', 'add_option', 'remove_option', 'set_fixed_to']
'integer$': ['show', 'hide', 'remove', 'set_mandatory', 'set_optional']
'^select$': ['show', 'hide', 'remove', 'set_mandatory', 'set_optional', 'add_option', 'remove_option', 'set_fixed_to', 'select', 'auto_select']
'^tree_select$': ['show', 'hide', 'remove', 'set_mandatory', 'set_optional', 'select', 'auto_select']
'^input$': ['show', 'hide', 'remove', 'set_mandatory', 'set_optional', 'fill_in', 'fill_in_empty']
operatorsName =
'_id$': ['show', 'hide', 'set_mandatory', 'set_optional', 'add_option', 'remove_option', 'set_fixed_to', 'select', 'auto_select']
'_ids$': ['show', 'hide', 'set_mandatory', 'set_optional']
'organization_id$': ['show', 'hide', 'set_mandatory', 'set_optional', 'add_option', 'remove_option']
'owner_id$': ['show', 'hide', 'set_mandatory', 'set_optional', 'add_option', 'remove_option', 'select', 'auto_select']
# merge config
elements = {}
for groupKey, groupMeta of groups
if groupKey is 'custom'
continue if !showCustomModules
options = {}
for module in App.CoreWorkflowCustomModule.all()
options[module.name] = module.name
elements['custom.module'] = { name: 'module', display: 'Module', tag: 'select', multiple: true, options: options, null: false, operator: ['execute'] }
continue
for row in App[groupMeta.model].configure_attributes
continue if !_.contains(['input', 'select', 'integer', 'boolean', 'tree_select'], row.tag)
continue if groupKey is 'ticket' && _.contains(['number', 'organization_id', 'title'], row.name)
# ignore passwords and relations
if row.type isnt 'password' && row.name.substr(row.name.length-4,4) isnt '_ids' && row.searchable isnt false
config = _.clone(row)
if config.tag is 'boolean'
config.tag = 'select'
if config.tag is 'select'
config.multiple = true
config.default = undefined
if config.type is 'email' || config.type is 'tel'
config.type = 'text'
for operatorRegEx, operator of operatorsType
myRegExp = new RegExp(operatorRegEx, 'i')
if config.tag && config.tag.match(myRegExp)
config.operator = operator
elements["#{groupKey}.#{config.name}"] = config
for operatorRegEx, operator of operatorsName
myRegExp = new RegExp(operatorRegEx, 'i')
if config.name && config.name.match(myRegExp)
config.operator = operator
elements["#{groupKey}.#{config.name}"] = config
[defaults, groups, elements]
@renderParamValue: (item, attribute, params, paramValue) ->
[defaults, groups, elements] = @defaults(attribute, params)
for groupAndAttribute, meta of paramValue
if !_.isArray(meta.operator)
meta.operator = [meta.operator]
for operator in meta.operator
operatorMeta = {}
operatorMeta['operator'] = operator
operatorMeta[operator] = meta[operator]
# build and append
row = @rowContainer(groups, elements, attribute)
@rebuildAttributeSelectors(item, row, groupAndAttribute, elements, operatorMeta, attribute)
item.filter('.js-filter').append(row)
@buildValueConfigValue: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
currentOperator = elementRow.find('.js-operator option:selected').attr('value')
return _.clone(attribute.value[groupAndAttribute][currentOperator])
@buildValueName: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
currentOperator = elementRow.find('.js-operator option:selected').attr('value')
return "#{attribute.name}::#{groupAndAttribute}::#{currentOperator}"
@buildValue: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
currentOperator = elementRow.find('.js-operator option:selected').attr('value')
name = @buildValueName(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
if !_.contains(['add_option', 'remove_option', 'set_fixed_to', 'select', 'execute', 'fill_in', 'fill_in_empty'], currentOperator)
elementRow.find('.js-value').addClass('hide').html('<input type="hidden" name="' + name + '" value="true" />')
return
super(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
@HasPreCondition: ->
return false
@hasEmptySelectorAtStart: ->
return true
@hasDuplicateSelector: ->
return true

View File

@ -1,505 +1,2 @@
# coffeelint: disable=camel_case_classes
class App.UiElement.ticket_selector
@defaults: (attribute = {}) ->
defaults = ['ticket.state_id']
groups =
ticket:
name: 'Ticket'
model: 'Ticket'
article:
name: 'Article'
model: 'TicketArticle'
customer:
name: 'Customer'
model: 'User'
organization:
name: 'Organization'
model: 'Organization'
if attribute.executionTime
groups.execution_time =
name: 'Execution Time'
operators_type =
'^datetime$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)', 'within next (relative)', 'within last (relative)', 'till (relative)', 'from (relative)']
'^timestamp$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)', 'within next (relative)', 'within last (relative)', 'till (relative)', 'from (relative)']
'^date$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)', 'within next (relative)', 'within last (relative)']
'boolean$': ['is', 'is not']
'integer$': ['is', 'is not']
'^radio$': ['is', 'is not']
'^select$': ['is', 'is not']
'^tree_select$': ['is', 'is not']
'^input$': ['contains', 'contains not']
'^richtext$': ['contains', 'contains not']
'^textarea$': ['contains', 'contains not']
'^tag$': ['contains all', 'contains one', 'contains all not', 'contains one not']
if attribute.hasChanged
operators_type =
'^datetime$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)', 'within next (relative)', 'within last (relative)', 'till (relative)', 'from (relative)', 'has changed']
'^timestamp$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)', 'within next (relative)', 'within last (relative)', 'till (relative)', 'from (relative)', 'has changed']
'^date$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)', 'within next (relative)', 'within last (relative)', 'till (relative)', 'from (relative)', 'has changed']
'boolean$': ['is', 'is not', 'has changed']
'integer$': ['is', 'is not', 'has changed']
'^radio$': ['is', 'is not', 'has changed']
'^select$': ['is', 'is not', 'has changed']
'^tree_select$': ['is', 'is not', 'has changed']
'^input$': ['contains', 'contains not', 'has changed']
'^richtext$': ['contains', 'contains not', 'has changed']
'^textarea$': ['contains', 'contains not', 'has changed']
'^tag$': ['contains all', 'contains one', 'contains all not', 'contains one not']
operators_name =
'_id$': ['is', 'is not']
'_ids$': ['is', 'is not']
if attribute.hasChanged
operators_name =
'_id$': ['is', 'is not', 'has changed']
'_ids$': ['is', 'is not', 'has changed']
# merge config
elements = {}
if attribute.article is false
delete groups.article
if attribute.action
elements['ticket.action'] =
name: 'action'
display: 'Action'
tag: 'select'
null: false
translate: true
options:
create: 'created'
update: 'updated'
'update.merged_into': 'merged into'
'update.received_merge': 'received merge'
operator: ['is', 'is not']
for groupKey, groupMeta of groups
if groupKey is 'execution_time'
if attribute.executionTime
elements['execution_time.calendar_id'] =
name: 'calendar_id'
display: 'Calendar'
tag: 'select'
relation: 'Calendar'
null: false
translate: false
operator: ['is in working time', 'is not in working time']
else
for row in App[groupMeta.model].configure_attributes
# ignore passwords and relations
if row.type isnt 'password' && row.name.substr(row.name.length-4,4) isnt '_ids' && row.searchable isnt false
config = _.clone(row)
if config.type is 'email' || config.type is 'tel'
config.type = 'text'
for operatorRegEx, operator of operators_type
myRegExp = new RegExp(operatorRegEx, 'i')
if config.tag && config.tag.match(myRegExp)
config.operator = operator
elements["#{groupKey}.#{config.name}"] = config
for operatorRegEx, operator of operators_name
myRegExp = new RegExp(operatorRegEx, 'i')
if config.name && config.name.match(myRegExp)
config.operator = operator
elements["#{groupKey}.#{config.name}"] = config
if config.tag == 'select'
config.multiple = true
if attribute.out_of_office
elements['ticket.out_of_office_replacement_id'] =
name: 'out_of_office_replacement_id'
display: 'Out of office replacement'
tag: 'autocompletion_ajax'
relation: 'User'
null: false
translate: true
operator: ['is', 'is not']
# Remove 'has changed' operator from attributes which don't support the operator.
['ticket.created_at', 'ticket.updated_at'].forEach (element_name) ->
elements[element_name]['operator'] = elements[element_name]['operator'].filter (item) -> item != 'has changed'
elements['ticket.mention_user_ids'] =
name: 'mention_user_ids'
display: 'Subscribe'
tag: 'autocompletion_ajax'
relation: 'User'
null: false
translate: true
operator: ['is', 'is not']
[defaults, groups, elements]
@rowContainer: (groups, elements, attribute) ->
row = $( App.view('generic/ticket_selector_row')(attribute: attribute) )
selector = @buildAttributeSelector(groups, elements)
row.find('.js-attributeSelector').prepend(selector)
row
@render: (attribute, params = {}) ->
[defaults, groups, elements] = @defaults(attribute)
item = $( App.view('generic/ticket_selector')(attribute: attribute) )
# add filter
item.delegate('.js-add', 'click', (e) =>
element = $(e.target).closest('.js-filterElement')
# add first available attribute
field = undefined
for groupAndAttribute, _config of elements
if !item.find(".js-attributeSelector [value=\"#{groupAndAttribute}\"]:selected").get(0)
field = groupAndAttribute
break
return if !field
row = @rowContainer(groups, elements, attribute)
element.after(row)
row.find('.js-attributeSelector select').trigger('change')
@rebuildAttributeSelectors(item, row, field, elements, {}, attribute)
if attribute.preview isnt false
@preview(item)
)
# remove filter
item.delegate('.js-remove', 'click', (e) =>
return if $(e.currentTarget).hasClass('is-disabled')
$(e.target).closest('.js-filterElement').remove()
@updateAttributeSelectors(item)
if attribute.preview isnt false
@preview(item)
)
# build initial params
if !_.isEmpty(params[attribute.name])
selectorExists = false
for groupAndAttribute, meta of params[attribute.name]
selectorExists = true
# build and append
row = @rowContainer(groups, elements, attribute)
@rebuildAttributeSelectors(item, row, groupAndAttribute, elements, meta, attribute)
item.filter('.js-filter').append(row)
else
for groupAndAttribute in defaults
# build and append
row = @rowContainer(groups, elements, attribute)
@rebuildAttributeSelectors(item, row, groupAndAttribute, elements, {}, attribute)
item.filter('.js-filter').append(row)
# change attribute selector
item.delegate('.js-attributeSelector select', 'change', (e) =>
elementRow = $(e.target).closest('.js-filterElement')
groupAndAttribute = elementRow.find('.js-attributeSelector option:selected').attr('value')
return if !groupAndAttribute
@rebuildAttributeSelectors(item, elementRow, groupAndAttribute, elements, {}, attribute)
@updateAttributeSelectors(item)
)
# change operator selector
item.delegate('.js-operator select', 'change', (e) =>
elementRow = $(e.target).closest('.js-filterElement')
groupAndAttribute = elementRow.find('.js-attributeSelector option:selected').attr('value')
return if !groupAndAttribute
@buildOperator(item, elementRow, groupAndAttribute, elements, {}, attribute)
)
# bind for preview
if attribute.preview isnt false
search = =>
@preview(item)
triggerSearch = ->
item.find('.js-previewCounterContainer').addClass('hide')
item.find('.js-previewLoader').removeClass('hide')
App.Delay.set(
search,
600,
'preview',
)
item.on('change', 'select', (e) ->
triggerSearch()
)
item.on('change keyup', 'input', (e) ->
triggerSearch()
)
@disableRemoveForOneAttribute(item)
item
@preview: (item) ->
params = App.ControllerForm.params(item)
App.Ajax.request(
id: 'ticket_selector'
type: 'POST'
url: "#{App.Config.get('api_path')}/tickets/selector"
data: JSON.stringify(params)
processData: true,
success: (data, status, xhr) =>
App.Collection.loadAssets(data.assets)
item.find('.js-previewCounterContainer').removeClass('hide')
item.find('.js-previewLoader').addClass('hide')
@ticketTable(data.ticket_ids, data.ticket_count, item)
)
@ticketTable: (ticket_ids, ticket_count, item) ->
item.find('.js-previewCounter').html(ticket_count)
new App.TicketList(
tableId: 'ticket-selector'
el: item.find('.js-previewTable')
ticket_ids: ticket_ids
)
@buildAttributeSelector: (groups, elements) ->
selection = $('<select class="form-control"></select>')
for groupKey, groupMeta of groups
displayName = App.i18n.translateInline(groupMeta.name)
selection.closest('select').append("<optgroup label=\"#{displayName}\" class=\"js-#{groupKey}\"></optgroup>")
optgroup = selection.find("optgroup.js-#{groupKey}")
for elementKey, elementGroup of elements
spacer = elementKey.split(/\./)
if spacer[0] is groupKey
attributeConfig = elements[elementKey]
if attributeConfig.operator
displayName = App.i18n.translateInline(attributeConfig.display)
optgroup.append("<option value=\"#{elementKey}\">#{displayName}</option>")
selection
# disable - if we only have one attribute
@disableRemoveForOneAttribute: (elementFull) ->
if elementFull.find('.js-attributeSelector select').length > 1
elementFull.find('.js-remove').removeClass('is-disabled')
else
elementFull.find('.js-remove').addClass('is-disabled')
@updateAttributeSelectors: (elementFull) ->
# enable all
elementFull.find('.js-attributeSelector select option').removeAttr('disabled')
# disable all used attributes
elementFull.find('.js-attributeSelector select').each(->
keyLocal = $(@).val()
elementFull.find('.js-attributeSelector select option[value="' + keyLocal + '"]').attr('disabled', true)
)
# disable - if we only have one attribute
@disableRemoveForOneAttribute(elementFull)
@rebuildAttributeSelectors: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
# set attribute
if groupAndAttribute
elementRow.find('.js-attributeSelector select').val(groupAndAttribute)
@buildOperator(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
@buildOperator: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
currentOperator = elementRow.find('.js-operator option:selected').attr('value')
name = "#{attribute.name}::#{groupAndAttribute}::operator"
if !meta.operator && currentOperator
meta.operator = currentOperator
selection = $("<select class=\"form-control\" name=\"#{name}\"></select>")
attributeConfig = elements[groupAndAttribute]
if attributeConfig.operator
# check if operator exists
operatorExists = false
for operator in attributeConfig.operator
if meta.operator is operator
operatorExists = true
break
if !operatorExists
for operator in attributeConfig.operator
meta.operator = operator
break
for operator in attributeConfig.operator
operatorName = App.i18n.translateInline(operator)
selected = ''
if !groupAndAttribute.match(/^ticket/) && operator is 'has changed'
# do nothing, only show "has changed" in ticket attributes
else
if meta.operator is operator
selected = 'selected="selected"'
selection.append("<option value=\"#{operator}\" #{selected}>#{operatorName}</option>")
selection
elementRow.find('.js-operator select').replaceWith(selection)
@buildPreCondition(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
@buildPreCondition: (elementFull, elementRow, groupAndAttribute, elements, meta, attributeConfig) ->
currentOperator = elementRow.find('.js-operator option:selected').attr('value')
currentPreCondition = elementRow.find('.js-preCondition option:selected').attr('value')
if !meta.pre_condition
meta.pre_condition = currentPreCondition
toggleValue = =>
preCondition = elementRow.find('.js-preCondition option:selected').attr('value')
if preCondition isnt 'specific'
elementRow.find('.js-value select').html('')
elementRow.find('.js-value').addClass('hide')
else
elementRow.find('.js-value').removeClass('hide')
@buildValue(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
# force to use auto completion on user lookup
attribute = _.clone(attributeConfig)
name = "#{attribute.name}::#{groupAndAttribute}::value"
attributeSelected = elements[groupAndAttribute]
preCondition = false
if attributeSelected.relation is 'User'
preCondition = 'user'
attribute.tag = 'user_autocompletion'
if attributeSelected.relation is 'Organization'
preCondition = 'org'
attribute.tag = 'autocompletion_ajax'
if !preCondition
elementRow.find('.js-preCondition select').html('')
elementRow.find('.js-preCondition').closest('.controls').addClass('hide')
toggleValue()
@buildValue(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
return
elementRow.find('.js-preCondition').closest('.controls').removeClass('hide')
name = "#{attribute.name}::#{groupAndAttribute}::pre_condition"
selection = $("<select class=\"form-control\" name=\"#{name}\" ></select>")
options = {}
if preCondition is 'user'
options =
'current_user.id': App.i18n.translateInline('current user')
'specific': App.i18n.translateInline('specific user')
'not_set': App.i18n.translateInline('not set (not defined)')
else if preCondition is 'org'
options =
'current_user.organization_id': App.i18n.translateInline('current user organization')
'specific': App.i18n.translateInline('specific organization')
'not_set': App.i18n.translateInline('not set (not defined)')
for key, value of options
selected = ''
if key is meta.pre_condition
selected = 'selected="selected"'
selection.append("<option value=\"#{key}\" #{selected}>#{App.i18n.translateInline(value)}</option>")
elementRow.find('.js-preCondition').closest('.controls').removeClass('hide')
elementRow.find('.js-preCondition select').replaceWith(selection)
elementRow.find('.js-preCondition select').bind('change', (e) ->
toggleValue()
)
@buildValue(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
toggleValue()
@buildValue: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
name = "#{attribute.name}::#{groupAndAttribute}::value"
# build new item
attributeConfig = elements[groupAndAttribute]
config = _.clone(attributeConfig)
if config.relation is 'User'
config.tag = 'user_autocompletion'
if config.relation is 'Organization'
config.tag = 'autocompletion_ajax'
# render ui element
item = ''
if config && App.UiElement[config.tag]
config['name'] = name
if attribute.value && attribute.value[groupAndAttribute]
config['value'] = _.clone(attribute.value[groupAndAttribute]['value'])
if 'multiple' of config
config.multiple = true
config.nulloption = false
if config.relation is 'User'
config.multiple = false
config.nulloption = false
config.guess = false
if config.relation is 'Organization'
config.multiple = false
config.nulloption = false
config.guess = false
if config.tag is 'checkbox'
config.tag = 'select'
if config.tag is 'datetime'
config.validationContainer = 'self'
tagSearch = "#{config.tag}_search"
if App.UiElement[tagSearch]
item = App.UiElement[tagSearch].render(config, {})
else
item = App.UiElement[config.tag].render(config, {})
if meta.operator is 'before (relative)' || meta.operator is 'within next (relative)' || meta.operator is 'within last (relative)' || meta.operator is 'after (relative)' || meta.operator is 'from (relative)' || meta.operator is 'till (relative)'
config['name'] = "#{attribute.name}::#{groupAndAttribute}"
if attribute.value && attribute.value[groupAndAttribute]
config['value'] = _.clone(attribute.value[groupAndAttribute])
item = App.UiElement['time_range'].render(config, {})
elementRow.find('.js-value').removeClass('hide').html(item)
if meta.operator is 'has changed'
elementRow.find('.js-value').addClass('hide')
elementRow.find('.js-preCondition').closest('.controls').addClass('hide')
else
elementRow.find('.js-value').removeClass('hide')
@humanText: (condition) ->
none = App.i18n.translateContent('No filter.')
return [none] if _.isEmpty(condition)
[defaults, groups, elements] = @defaults()
rules = []
for attribute, meta of condition
objectAttribute = attribute.split(/\./)
# get stored params
if meta && objectAttribute[1]
operator = meta.operator
value = meta.value
model = toCamelCase(objectAttribute[0])
config = elements[attribute]
valueHuman = []
if _.isArray(value)
for data in value
r = @humanTextLookup(config, data)
valueHuman.push r
else
valueHuman.push @humanTextLookup(config, value)
if valueHuman.join
valueHuman = valueHuman.join(', ')
rules.push "#{App.i18n.translateContent('Where')} <b>#{App.i18n.translateContent(model)} -> #{App.i18n.translateContent(config.display)}</b> #{App.i18n.translateContent(operator)} <b>#{valueHuman}</b>."
return [none] if _.isEmpty(rules)
rules
@humanTextLookup: (config, value) ->
return value if !App[config.relation]
return value if !App[config.relation].exists(value)
data = App[config.relation].fullLocal(value)
return value if !data
if data.displayName
return App.i18n.translateContent(data.displayName())
valueHuman.push App.i18n.translateContent(data.name)
class App.UiElement.ticket_selector extends App.UiElement.ApplicationSelector

View File

@ -8,6 +8,35 @@ class App.UiElement.tree_select extends App.UiElement.ApplicationUiElement
if child.children
@optionsSelect(child.children, value)
@filterTreeOptions: (values, valueDepth, options, nullExists) ->
newOptions = []
nullFound = false
for option, index in options
enabled = false
for value in values
valueArray = value.split('::')
optionArray = option['value'].split('::')
continue if valueArray[valueDepth] isnt optionArray[valueDepth]
enabled = true
break
if nullExists && !option.value && !nullFound
nullFound = true
enabled = true
if !enabled
continue
if option['children'] && option['children'].length
option['children'] = @filterTreeOptions(values, valueDepth + 1, option['children'], nullExists)
newOptions.push(option)
return newOptions
@filterOptionArray: (attribute) ->
attribute.options = @filterTreeOptions(attribute.filter, 0, attribute.options, attribute.null)
@render: (attribute, params) ->
# set multiple option

View File

@ -52,7 +52,8 @@ class App.TicketCreate extends App.Controller
App.Collection.loadAssets(data.assets)
@formMeta = data.form_meta
@buildScreen(params)
@bindId = App.TicketCreateCollection.one(load)
@bindId = App.TicketCreateCollection.bind(load, false)
App.TicketCreateCollection.fetch()
# rerender view, e. g. on langauge change
@controllerBind('ui:rerender', =>
@ -123,6 +124,7 @@ class App.TicketCreate extends App.Controller
@$('[name="formSenderType"]').val(type)
# force changing signature
# skip on initialization because it will trigger core workflow
@$('[name="group_id"]').trigger('change')
# add observer to change options
@ -319,40 +321,12 @@ class App.TicketCreate extends App.Controller
handlers = @Config.get('TicketCreateFormHandler')
new App.ControllerForm(
el: @$('.ticket-form-top')
form_id: @formId
model: App.Ticket
screen: 'create_top'
events:
'change [name=customer_id]': @localUserInfo
handlersConfig: handlers
filter: @formMeta.filter
formMeta: @formMeta
autofocus: true
params: params
taskKey: @taskKey
)
new App.ControllerForm(
el: @$('.article-form-top')
form_id: @formId
model: App.TicketArticle
screen: 'create_top'
events:
'fileUploadStart .richtext': => @submitDisable()
'fileUploadStop .richtext': => @submitEnable()
params: params
taskKey: @taskKey
)
new App.ControllerForm(
el: @$('.ticket-form-middle')
form_id: @formId
model: App.Ticket
screen: 'create_middle'
events:
'change [name=customer_id]': @localUserInfo
handlersConfig: handlers
@controllerFormCreateMiddle = new App.ControllerForm(
el: @$('.ticket-form-middle')
form_id: @formId
model: App.Ticket
screen: 'create_middle'
handlersConfig: handlers
filter: @formMeta.filter
formMeta: @formMeta
params: params
@ -360,14 +334,52 @@ class App.TicketCreate extends App.Controller
taskKey: @taskKey
rejectNonExistentValues: true
)
new App.ControllerForm(
# tunnel events to make sure core workflow does know
# about every change of all attributes (like subject)
tunnelController = @controllerFormCreateMiddle
class TicketCreateFormHandlerControllerFormCreateMiddle
@run: (params, attribute, attributes, classname, form, ui) ->
return if !ui.lastChangedAttribute
tunnelController.lastChangedAttribute = ui.lastChangedAttribute
params = App.ControllerForm.params(tunnelController.form)
App.FormHandlerCoreWorkflow.run(params, tunnelController.attributes[0], tunnelController.attributes, tunnelController.idPrefix, tunnelController.form, tunnelController)
handlersTunnel = _.clone(handlers)
handlersTunnel['000-TicketCreateFormHandlerControllerFormCreateMiddle'] = TicketCreateFormHandlerControllerFormCreateMiddle
@controllerFormCreateTop = new App.ControllerForm(
el: @$('.ticket-form-top')
form_id: @formId
model: App.Ticket
screen: 'create_top'
events:
'change [name=customer_id]': @localUserInfo
handlersConfig: handlersTunnel
filter: @formMeta.filter
formMeta: @formMeta
autofocus: true
params: params
taskKey: @taskKey
)
@controllerFormCreateTopArticle = new App.ControllerForm(
el: @$('.article-form-top')
form_id: @formId
model: App.TicketArticle
screen: 'create_top'
events:
'fileUploadStart .richtext': => @submitDisable()
'fileUploadStop .richtext': => @submitEnable()
handlersConfig: handlersTunnel
params: params
taskKey: @taskKey
)
@controllerFormCreateBottom = new App.ControllerForm(
el: @$('.ticket-form-bottom')
form_id: @formId
model: App.Ticket
screen: 'create_bottom'
events:
'change [name=customer_id]': @localUserInfo
handlersConfig: handlers
handlersConfig: handlersTunnel
filter: @formMeta.filter
formMeta: @formMeta
params: params
@ -513,19 +525,23 @@ class App.TicketCreate extends App.Controller
ticket.load(params)
ticketErrorsTop = ticket.validate(
screen: 'create_top'
controllerForm: @controllerFormCreateTop
target: e.target
)
ticketErrorsMiddle = ticket.validate(
screen: 'create_middle'
controllerForm: @controllerFormCreateMiddle
target: e.target
)
ticketErrorsBottom = ticket.validate(
screen: 'create_bottom'
controllerForm: @controllerFormCreateBottom
target: e.target
)
article = new App.TicketArticle
article.load(params['article'])
articleErrors = article.validate(
screen: 'create_top'
controllerForm: @controllerFormCreateTopArticle
target: e.target
)
# collect whole validation result

View File

@ -0,0 +1,57 @@
class CoreWorkflow extends App.ControllerSubContent
requiredPermission: 'admin.core_workflow'
header: 'Core Workflow'
constructor: ->
super
@setAttributes()
@genericController = new App.ControllerGenericIndex(
el: @el
id: @id
genericObject: 'CoreWorkflow'
defaultSortBy: 'name'
pageData:
home: 'core_workflow'
object: 'Workflow'
objects: 'Workflows'
pagerAjax: true
pagerBaseUrl: '#manage/core_workflow/'
pagerSelected: ( @page || 1 )
pagerPerPage: 150
navupdate: '#core_workflow'
notes: [
'Core Workflows are actions or constraints on selections in forms. Depending on an action, it is possible to hide or restrict fields or to change the obligation to fill them in.'
]
buttons: [
{ name: 'New Workflow', 'data-type': 'new', class: 'btn--success' }
]
container: @el.closest('.content')
veryLarge: true
handlers: [
App.FormHandlerCoreWorkflow.run
App.FormHandlerAdminCoreWorkflow.run
]
)
setAttributes: ->
for field in App.CoreWorkflow.configure_attributes
if field.name is 'object'
field.options = {}
for value in App.FormHandlerCoreWorkflow.getObjects()
field.options[value] = value
else if field.name is 'preferences::screen'
field.options = {}
for value in App.FormHandlerCoreWorkflow.getScreens()
field.options[value] = @screen2displayName(value)
screen2displayName: (screen) ->
mapping = {
create: 'Creation mask',
create_middle: 'Creation mask',
edit: 'Edit mask',
overview_bulk: 'Overview bulk mask',
}
return mapping[screen] || screen
App.Config.set('CoreWorkflowObject', { prio: 1750, parent: '#system', name: 'Core Workflow', target: '#system/core_workflow', controller: CoreWorkflow, permission: ['admin.core_workflow'] }, 'NavBarAdmin')

View File

@ -18,7 +18,8 @@ class CustomerTicketCreate extends App.ControllerAppContent
App.Collection.loadAssets(data.assets)
@formMeta = data.form_meta
@render()
@bindId = App.TicketCreateCollection.one(load)
@bindId = App.TicketCreateCollection.bind(load, false)
App.TicketCreateCollection.fetch()
render: (template = {}) ->
if !@Config.get('customer_ticket_create')
@ -43,32 +44,7 @@ class CustomerTicketCreate extends App.ControllerAppContent
form_id: @form_id
)
new App.ControllerForm(
el: @el.find('.ticket-form-top')
form_id: @form_id
model: App.Ticket
screen: 'create_top'
handlersConfig: handlers
filter: @formMeta.filter
formMeta: @formMeta
autofocus: true
params: defaults
)
new App.ControllerForm(
el: @el.find('.article-form-top')
form_id: @form_id
model: App.TicketArticle
screen: 'create_top'
events:
'fileUploadStart .richtext': => @submitDisable()
'fileUploadStop .richtext': => @submitEnable()
filter: @formMeta.filter
formMeta: @formMeta
params: defaults
handlersConfig: handlers
)
new App.ControllerForm(
@controllerFormCreateMiddle = new App.ControllerForm(
el: @el.find('.ticket-form-middle')
form_id: @form_id
model: App.Ticket
@ -80,13 +56,51 @@ class CustomerTicketCreate extends App.ControllerAppContent
handlersConfig: handlers
rejectNonExistentValues: true
)
# tunnel events to make sure core workflow does know
# about every change of all attributes (like subject)
tunnelController = @controllerFormCreateMiddle
class TicketCreateFormHandlerControllerFormCreateMiddle
@run: (params, attribute, attributes, classname, form, ui) ->
return if !ui.lastChangedAttribute
tunnelController.lastChangedAttribute = ui.lastChangedAttribute
params = App.ControllerForm.params(tunnelController.form)
App.FormHandlerCoreWorkflow.run(params, tunnelController.attributes[0], tunnelController.attributes, tunnelController.idPrefix, tunnelController.form, tunnelController)
handlersTunnel = _.clone(handlers)
handlersTunnel['000-TicketCreateFormHandlerControllerFormCreateMiddle'] = TicketCreateFormHandlerControllerFormCreateMiddle
@controllerFormCreateTop = new App.ControllerForm(
el: @el.find('.ticket-form-top')
form_id: @form_id
model: App.Ticket
screen: 'create_top'
handlersConfig: handlersTunnel
filter: @formMeta.filter
formMeta: @formMeta
autofocus: true
params: defaults
)
@controllerFormCreateTopArticle = new App.ControllerForm(
el: @el.find('.article-form-top')
form_id: @form_id
model: App.TicketArticle
screen: 'create_top'
events:
'fileUploadStart .richtext': => @submitDisable()
'fileUploadStop .richtext': => @submitEnable()
filter: @formMeta.filter
formMeta: @formMeta
params: defaults
handlersConfig: handlersTunnel
)
if !_.isEmpty(App.Ticket.attributesGet('create_bottom', false, true))
new App.ControllerForm(
@controllerFormCreateBottom = new App.ControllerForm(
el: @el.find('.ticket-form-bottom')
form_id: @form_id
model: App.Ticket
screen: 'create_bottom'
handlersConfig: handlers
handlersConfig: handlersTunnel
filter: @formMeta.filter
formMeta: @formMeta
params: defaults
@ -151,15 +165,18 @@ class CustomerTicketCreate extends App.ControllerAppContent
# validate form
ticketErrorsTop = ticket.validate(
screen: 'create_top'
controllerForm: @controllerFormCreateTop
target: e.target
)
ticketErrorsMiddle = ticket.validate(
screen: 'create_middle'
controllerForm: @controllerFormCreateMiddle
target: e.target
)
article = new App.TicketArticle
article.load(params['article'])
articleErrors = article.validate(
screen: 'create_top'
controllerForm: @controllerFormCreateTop
target: e.target
)
# collect whole validation

View File

@ -67,7 +67,7 @@ class GettingStartedAdmin extends App.ControllerWizardFullScreen
user.load(@params)
errors = user.validate(
screen: 'signup'
controllerForm: @form
)
if errors
@log 'error new', errors

View File

@ -61,7 +61,7 @@ class GettingStartedAgent extends App.ControllerWizardFullScreen
user.load(@params)
errors = user.validate(
screen: 'invite_agent'
controllerForm: @form
)
if errors
@log 'error new', errors

View File

@ -273,7 +273,9 @@ class Edit extends App.ControllerGenericEdit
@item.load(params)
# validate
errors = @item.validate()
errors = @item.validate(
controllerForm: @controller
)
if errors
@log 'error', errors
@formValidate(form: e.target, errors: errors)

View File

@ -50,7 +50,7 @@ class Signup extends App.ControllerFullPage
user.load(@params)
errors = user.validate(
screen: 'signup'
controllerForm: @form
)
if errors

View File

@ -8,19 +8,21 @@ class App.TicketCustomer extends App.ControllerModal
configure_attributes = [
{ name: 'customer_id', display: 'Customer', tag: 'user_autocompletion', null: false, placeholder: 'Enter Person or Organization/Company', minLengt: 2, disableCreateObject: false },
]
controller = new App.ControllerForm(
@controller = new App.ControllerForm(
model:
configure_attributes: configure_attributes,
autofocus: true
)
controller.form
@controller.form
onSubmit: (e) =>
params = @formParam(e.target)
ticket = App.Ticket.find(@ticket_id)
ticket.customer_id = params['customer_id']
errors = ticket.validate()
errors = ticket.validate(
controllerForm: @controller
)
if !_.isEmpty(errors)
@log 'error', errors

View File

@ -39,7 +39,7 @@ class App.TicketOverview extends App.Controller
load = (data) =>
App.Collection.loadAssets(data.assets)
@formMeta = data.form_meta
@bindId = App.TicketCreateCollection.bind(load)
@bindId = App.TicketOverviewCollection.bind(load)
startDragItem: (event) =>
return if !@batchSupport
@ -206,13 +206,22 @@ class App.TicketOverview extends App.Controller
article: article
)
ticket.article = article
ticket.save(
ticket.ajax().update(
ticket.attributes()
# this option will prevent callbacks and invalid data states in case of an error
failResponseNoTrigger: true
done: (r) =>
@batchCountIndex++
# refresh view after all tickets are proceeded
if @batchCountIndex == @batchCount
App.Event.trigger('overview:fetch')
fail: (record, settings, details) ->
console.log('record, settings, details', record, settings, details)
App.Event.trigger('notify', {
type: 'error'
msg: App.i18n.translateContent('Bulk action stopped %s!', error)
})
)
return
@ -225,13 +234,21 @@ class App.TicketOverview extends App.Controller
ticket.owner_id = id
if !_.isEmpty(groupId)
ticket.group_id = groupId
ticket.save(
ticket.ajax().update(
ticket.attributes()
# this option will prevent callbacks and invalid data states in case of an error
failResponseNoTrigger: true
done: (r) =>
@batchCountIndex++
# refresh view after all tickets are proceeded
if @batchCountIndex == @batchCount
App.Event.trigger('overview:fetch')
fail: (record, settings, details) ->
App.Event.trigger('notify', {
type: 'error'
msg: App.i18n.translateContent('Bulk action stopped %s!', settings.error)
})
)
return
@ -242,13 +259,21 @@ class App.TicketOverview extends App.Controller
#console.log "perform action #{action} with id #{id} on ", $(item).val()
ticket = App.Ticket.find($(item).val())
ticket.group_id = id
ticket.save(
ticket.ajax().update(
ticket.attributes()
# this option will prevent callbacks and invalid data states in case of an error
failResponseNoTrigger: true
done: (r) =>
@batchCountIndex++
# refresh view after all tickets are proceeded
if @batchCountIndex == @batchCount
App.Event.trigger('overview:fetch')
fail: (record, settings, details) ->
App.Event.trigger('notify', {
type: 'error'
msg: App.i18n.translateContent('Bulk action stopped %s!', error)
})
)
return
@ -673,6 +698,8 @@ class App.TicketOverview extends App.Controller
@contentController.show()
return
App.TicketOverviewCollection.fetch()
# remember last view
@viewLast = @view
@ -707,7 +734,7 @@ class App.TicketOverview extends App.Controller
release: =>
@keyboardOff()
super
App.TicketCreateCollection.unbindById(@bindId)
App.TicketOverviewCollection.unbindById(@bindId)
keyboardOn: =>
$(window).off 'keydown.overview_navigation'
@ -1134,6 +1161,8 @@ class Table extends App.Controller
@lastChecked = e.currentTarget
@updateTicketIdsBulkForm()
callbackIconHeader = (headers) ->
attribute =
name: 'icon'
@ -1273,6 +1302,11 @@ class Table extends App.Controller
bulkAll.prop('indeterminate', true)
)
updateTicketIdsBulkForm: =>
items = $('.content.active .table-overview .table').find('[name="bulk"]:checked')
ticket_ids = _.map(items, (el) -> $(el).val() )
@bulkForm.el.find('input[name=ticket_ids]').val(ticket_ids.join(',')).trigger('change')
renderCustomerNotTicketExistIfNeeded: (ticketListShow) =>
user = App.User.current()
@stopListening user, 'refresh'
@ -1319,7 +1353,6 @@ class Table extends App.Controller
onCloseCallback: @keyboardOn
)
class App.OverviewSettings extends App.ControllerModal
buttonClose: true
buttonCancel: true

View File

@ -856,7 +856,8 @@ class App.TicketZoom extends App.Controller
# validate ticket by model
errors = ticket.validate(
screen: 'edit'
controllerForm: @sidebarWidget?.get('100-TicketEdit')?.edit?.controllerFormSidebarTicket
target: e.target
)
if errors
@log 'error', 'update', errors

View File

@ -0,0 +1,15 @@
class App.FormHandlerAdminCoreWorkflow
@run: (params, attribute, attributes, classname, form, ui) ->
return if attribute.name isnt 'object'
return if ui.FormHandlerAdminCoreWorkflowDone
ui.FormHandlerAdminCoreWorkflowDone = true
$(form).find('select[name=object]').off('change.core_workflow_conditions').on('change.change.core_workflow_conditions', (e) ->
for attribute in attributes
continue if attribute.name isnt 'condition_saved' && attribute.name isnt 'condition_selected' && attribute.name isnt 'perform'
attribute.workflow_object = $(e.target).val()
newElement = ui.formGenItem(attribute, classname, form)
form.find('div.form-group[data-attribute-name="' + attribute.name + '"]').replaceWith(newElement)
)

View File

@ -0,0 +1,320 @@
class App.FormHandlerCoreWorkflow
# contains the current form params state to prevent mass requests
coreWorkflowParams = {}
# contains the running requests of each form
coreWorkflowRequests = {}
# contains the restriction values for each attribute of each form
coreWorkflowRestrictions = {}
# defines the objects and screen for which Core Workflow is active
coreWorkflowScreens = {
Ticket: ['create_middle', 'edit', 'overview_bulk']
User: ['create', 'edit']
Organization: ['create', 'edit']
Sla: ['create', 'edit']
CoreWorkflow: ['create', 'edit']
Group: ['create', 'edit']
}
# returns the objects for which Core Workflow is active
@getObjects: ->
return Object.keys(coreWorkflowScreens)
# returns the screens for which Core Workflow is active
@getScreens: ->
result = []
for object, screens of coreWorkflowScreens
for screen in screens
continue if screen in result
result.push(screen)
return result
# returns active Core Workflow requests. it is used to stabilize tests
@getRequests: ->
return coreWorkflowRequests
# Based on the model validation result the controller form
# will delay the submit if a request of the Core Workflow is running
@delaySubmit: (controllerForm, target) ->
for key, value of coreWorkflowRequests
if controllerForm.idPrefix is value.ui.idPrefix
coreWorkflowRequests[key].triggerSubmit = target
return true
App.FormHandlerCoreWorkflow.triggerSubmit(target)
# the saved submit target will be executed after the request
@triggerSubmit: (target) ->
if $(target).get(0).tagName == 'FORM'
target = $(target).find('button[type=submit]').first()
$(target).click()
# checks if the controller has a running Core Workflow request
@requestsRunning: (controllerForm) ->
for key, value of coreWorkflowRequests
if controllerForm.idPrefix is value.ui.idPrefix
return true
return false
# checks if the Core Workflow should get activated for the screen
@screenValid: (ui) ->
return false if !ui.model
return false if !ui.model.className
return false if !ui.screen
return false if coreWorkflowScreens[ui.model.className] is undefined
return false if !_.contains(coreWorkflowScreens[ui.model.className], ui.screen)
return true
# checks if the ajax or websocket endpoint should be used
@useWebSockets: ->
return !App.Config.get('core_workflow_ajax_mode')
# restricts the dropdown and tree select values of a form
@restrictValues: (classname, form, ui, attributes, params, data) ->
return if _.isEmpty(data.restrict_values)
for field, values of data.restrict_values
for attribute in attributes
continue if attribute.name isnt field
item = $.extend(true, {}, attribute)
el = App.ControllerForm.findFieldByName(field, form)
shown = App.ControllerForm.fieldIsShown(el)
mandatory = App.ControllerForm.fieldIsMandatory(el)
# get deep value if needed for store attributes
paramValue = params[item.name]
if data.select[item.name]
paramValue = data.select[item.name]
coreWorkflowParams[classname][item.name] = paramValue
delete coreWorkflowRestrictions[classname]
parts = attribute.name.split '::'
if parts.length > 1
deepValue = parts.reduce((memo, elem) ->
memo?[elem]
, params)
if deepValue isnt undefined
paramValue = deepValue
# cache state for performance and only run
# if values or param differ
if coreWorkflowRestrictions?[classname]?[item.name]
compare = values
continue if _.isEqual(coreWorkflowRestrictions[classname][item.name], compare)
coreWorkflowRestrictions[classname] ||= {}
coreWorkflowRestrictions[classname][item.name] = values
valueFound = false
for value in values
if value && paramValue
if value.toString() == paramValue.toString()
valueFound = true
break
if _.isArray(paramValue) && _.contains(paramValue, value.toString())
valueFound = true
break
item.filter = values
if valueFound
item.default = paramValue
item.newValue = paramValue
else
item.default = ''
item.newValue = ''
if attribute.relation
item.rejectNonExistentValues = true
ui.params ||= {}
newElement = ui.formGenItem(item, classname, form)
# copy existing events to new rendered element
form.find('[name="' + field + '"]').closest('.form-group').find("[name!=''][name]").each(->
target_name = $(@).attr('name')
$.each($._data(@, 'events'), (eventType, eventArray) ->
$.each(eventArray, (index, event) ->
eventToBind = event.type
if event.namespace.length > 0
eventToBind = event.type + '.' + event.namespace
target = newElement.find("[name='" + target_name + "']")
if target.length > 0
target.bind(eventToBind, event.data, event.handler)
)
)
)
form.find('[name="' + field + '"]').closest('.form-group').replaceWith(newElement)
if shown
ui.show(field, form)
else
ui.hide(field, form)
if mandatory
ui.mandantory(field, form)
else
ui.optional(field, form)
# fill in data in input fields
@select: (classname, form, ui, attributes, params, data) ->
return if _.isEmpty(data)
for field, values of data
form.find('[name="' + field + '"]').val(data[field])
coreWorkflowParams[classname][field] = data[field]
# fill in data in input fields
@fillIn: (classname, form, ui, attributes, params, data) ->
return if _.isEmpty(data)
for field, values of data
form.find('[name="' + field + '"]').val(data[field])
coreWorkflowParams[classname][field] = data[field]
# changes the visibility of form elements
@changeVisibility: (form, ui, data) ->
return if _.isEmpty(data)
for field, state of data
if state is 'show'
ui.show(field, form)
else if state is 'hide'
ui.hide(field, form)
else if state is 'remove'
ui.hide(field, form, true)
# changes the mandatory flag of form elements
@changeMandatory: (form, ui, data) ->
return if _.isEmpty(data)
for field, state of data
if state
ui.mandantory(field, form)
else
ui.optional(field, form)
# executes individual js commands of the Core Workflow engine
@executeEval: (form, ui, data) ->
return if _.isEmpty(data)
for statement in data
eval(statement)
# runs callbacks which are defined for the controller form
@runCallbacks: (ui) ->
callbacks = ui?.core_workflow?.callbacks || []
for callback in callbacks
callback()
# runs a complete workflow based on a request result and the form params of the form handler
@runWorkflow: (data, classname, form, ui, attributes, params) ->
App.Collection.loadAssets(data.assets)
App.FormHandlerCoreWorkflow.restrictValues(classname, form, ui, attributes, params, data)
App.FormHandlerCoreWorkflow.select(classname, form, ui, attributes, params, data.select)
App.FormHandlerCoreWorkflow.fillIn(classname, form, ui, attributes, params, data.fill_in)
App.FormHandlerCoreWorkflow.changeVisibility(form, ui, data.visibility)
App.FormHandlerCoreWorkflow.changeMandatory(form, ui, data.mandatory)
App.FormHandlerCoreWorkflow.executeEval(form, ui, data.eval)
App.FormHandlerCoreWorkflow.runCallbacks(ui)
# loads the request data and prepares the run of the workflow data
@runRequest: (data) ->
return if !coreWorkflowRequests[data.request_id]
triggerSubmit = coreWorkflowRequests[data.request_id].triggerSubmit
classname = coreWorkflowRequests[data.request_id].classname
form = coreWorkflowRequests[data.request_id].form
ui = coreWorkflowRequests[data.request_id].ui
attributes = coreWorkflowRequests[data.request_id].attributes
params = coreWorkflowRequests[data.request_id].params
App.FormHandlerCoreWorkflow.runWorkflow(data, classname, form, ui, attributes, params)
delete coreWorkflowRequests[data.request_id]
if triggerSubmit
App.FormHandlerCoreWorkflow.triggerSubmit(triggerSubmit)
# this will set the hook for the websocket if activated
@setHook: =>
return if @hooked
return if !App.FormHandlerCoreWorkflow.useWebSockets()
@hooked = true
App.Event.bind(
'core_workflow'
(data) =>
@runRequest(data)
'ws:core_workflow'
)
# this will return the needed form element
@getForm: (form) ->
return form.closest('form') if form.get(0).tagName != 'FORM'
return $(form)
# cleanup of some bad params
@cleanParams: (params_ref) ->
params = $.extend(true, {}, params_ref)
delete params.customer_id_completion
delete params.tags
delete params.formSenderType
return params
# this will use the form handler information to send the data to the backend via ajax/websockets
@request: (classname, form, ui, attributes, params) ->
requestID = "CoreWorkflow-#{Math.floor( Math.random() * 999999 ).toString()}"
coreWorkflowRequests[requestID] = { classname: classname, form: form, ui: ui, attributes: attributes, params: params }
requestData = {
event: 'core_workflow',
request_id: requestID,
params: params,
class_name: ui.model.className,
screen: ui.screen
}
if App.FormHandlerCoreWorkflow.useWebSockets()
App.WebSocket.send(requestData)
else
ui.ajax(
id: "core_workflow-#{requestData.request_id}"
type: 'POST'
url: "#{ui.apiPath}/core_workflows/perform"
data: JSON.stringify(requestData)
success: (data, status, xhr) =>
@runRequest(data)
error: (data) ->
delete coreWorkflowRequests[requestID]
return
)
@run: (params_ref, attribute, attributes, classname, form, ui) ->
# skip on blacklisted tags
return if _.contains(['ticket_selector', 'core_workflow_condition', 'core_workflow_perform'], attribute.tag)
# check if Core Workflow screen
return if !App.FormHandlerCoreWorkflow.screenValid(ui)
# get params and add id from ui if needed
params = App.FormHandlerCoreWorkflow.cleanParams(params_ref)
if ui?.params?.id
params.id = ui.params.id
# skip double checks
return if _.isEqual(coreWorkflowParams[classname], params)
coreWorkflowParams[classname] = params
# render intial state provided by screen options if given
# for more performance and less requests
if ui.formMeta && ui.formMeta.core_workflow && !ui.lastChangedAttribute
App.FormHandlerCoreWorkflow.runWorkflow(ui.formMeta.core_workflow, classname, form, ui, attributes, params)
return
App.FormHandlerCoreWorkflow.setHook()
App.FormHandlerCoreWorkflow.request(classname, form, ui, attributes, params)

View File

@ -1,33 +0,0 @@
class TicketZoomFormHandlerDependencies
# central method, is getting called on every ticket form change
@run: (params, attribute, attributes, classname, form, ui) ->
return if !ui.formMeta
return if !ui.formMeta.dependencies
return if !ui.formMeta.dependencies[attribute.name]
dependency = ui.formMeta.dependencies[attribute.name][ parseInt(params[attribute.name]) ]
if !dependency
dependency = ui.formMeta.dependencies[attribute.name][ params[attribute.name] ]
if dependency
for fieldNameToChange of dependency
filter = []
if dependency[fieldNameToChange]
filter = dependency[fieldNameToChange]
# find element to replace
for item in attributes
if item.name is fieldNameToChange
item['filter'] = {}
item['filter'][ fieldNameToChange ] = filter
item.default = params[item.name]
item.newValue = params[item.name]
#if !item.default
# delete item['default']
newElement = ui.formGenItem(item, classname, form)
# replace new option list
if newElement
form.find('[name="' + fieldNameToChange + '"]').closest('.form-group').replaceWith(newElement)
App.Config.set('100-ticketFormChanges', TicketZoomFormHandlerDependencies, 'TicketZoomFormHandler')
App.Config.set('100-ticketFormChanges', TicketZoomFormHandlerDependencies, 'TicketCreateFormHandler')

View File

@ -1,26 +0,0 @@
class OwnerFormHandlerDependencies
# central method, is getting called on every ticket form change
@run: (params, attribute, attributes, classname, form, ui) ->
return if 'group_id' not of params
return if 'owner_id' not of params
owner_attribute = _.find(attributes, (o) -> o.name == 'owner_id')
return if !owner_attribute
return if 'possible_groups_owners' not of owner_attribute
# fetch contents using User relation if a Group has been selected, otherwise render possible_groups_owners
if params.group_id
owner_attribute.relation = 'User'
delete owner_attribute['options']
else
owner_attribute.options = owner_attribute.possible_groups_owners
delete owner_attribute['relation']
# replace new option list
owner_attribute.default = params[owner_attribute.name]
owner_attribute.newValue = params[owner_attribute.name]
newElement = ui.formGenItem(owner_attribute, classname, form)
form.find('select[name="owner_id"]').closest('.form-group').replaceWith(newElement)
App.Config.set('150-ticketFormChanges', OwnerFormHandlerDependencies, 'TicketZoomFormHandler')

View File

@ -4,6 +4,9 @@ class App.TicketZoomSidebar extends App.ControllerObserver
customer_id: true
organization_id: true
get: (key) ->
return @sidebarBackends[key]
reload: (args) =>
for key, backend of @sidebarBackends
if backend && backend.reload

View File

@ -20,7 +20,7 @@ class Edit extends App.ControllerObserver
if followUpPossible == 'new_ticket' && ticketState != 'closed' ||
followUpPossible != 'new_ticket' ||
@permissionCheck('admin') || ticket.currentView() is 'agent'
new App.ControllerForm(
@controllerFormSidebarTicket = new App.ControllerForm(
elReplace: @el
model: { className: 'Ticket', configure_attributes: @formMeta.configure_attributes || App.Ticket.configure_attributes }
screen: 'edit'
@ -30,10 +30,13 @@ class Edit extends App.ControllerObserver
params: defaults
isDisabled: !ticket.editable()
taskKey: @taskKey
core_workflow: {
callbacks: [@markForm]
}
#bookmarkable: true
)
else
new App.ControllerForm(
@controllerFormSidebarTicket = new App.ControllerForm(
elReplace: @el
model: { configure_attributes: @formMeta.configure_attributes || App.Ticket.configure_attributes }
screen: 'edit'
@ -43,6 +46,9 @@ class Edit extends App.ControllerObserver
params: defaults
isDisabled: ticket.editable()
taskKey: @taskKey
core_workflow: {
callbacks: [@markForm]
}
#bookmarkable: true
)

View File

@ -27,7 +27,7 @@ class App.InviteUser extends App.ControllerWizardModal
modal = $(App.view('widget/invite_user')(
head: @head
))
new App.ControllerForm(
@controller = new App.ControllerForm(
el: modal.find('.js-form')
model: App.User
screen: @screen
@ -60,7 +60,7 @@ class App.InviteUser extends App.ControllerWizardModal
user.load(@params)
errors = user.validate(
screen: @screen
controllerForm: @controller
)
if errors
@log 'error new', errors

View File

@ -26,7 +26,7 @@ class App.WidgetTemplate extends App.Controller
@html App.view('widget/template')(
template: template
)
new App.ControllerForm(
@controller = new App.ControllerForm(
el: @el.find('#form-template')
model:
configure_attributes: @configure_attributes
@ -98,7 +98,9 @@ class App.WidgetTemplate extends App.Controller
)
# validate form
errors = template.validate()
errors = template.validate(
controllerForm: @controller
)
# show errors in form
if errors

View File

@ -24,6 +24,9 @@ class App.TicketBulkForm extends App.Controller
localAttribute.null = true
@configure_attributes_ticket.push localAttribute
# add field for ticket ids
ticket_ids_attribute = { name: 'ticket_ids', display: false, tag: 'input', type: 'hidden', limit: 100, null: false }
@configure_attributes_ticket.push ticket_ids_attribute
time_attribute = _.findWhere(@configure_attributes_ticket, {'name': 'pending_time'})
if time_attribute
@ -37,10 +40,10 @@ class App.TicketBulkForm extends App.Controller
App.Collection.loadAssets(data.assets)
@formMeta = data.form_meta
@render()
@bindId = App.TicketCreateCollection.bind(load)
@bindId = App.TicketOverviewCollection.bind(load)
release: =>
App.TicketCreateCollection.unbind(@bindId)
App.TicketOverviewCollection.unbind(@bindId)
render: ->
@el.css('right', App.Utils.getScrollBarWidth())
@ -50,31 +53,27 @@ class App.TicketBulkForm extends App.Controller
handlers = @Config.get('TicketZoomFormHandler')
for attribute in @configure_attributes_ticket
continue if attribute.name != 'owner_id'
{users, groups} = @validUsersForTicketSelection()
options = _.map(users, (user) -> {value: user.id, name: user.displayName()} )
attribute.possible_groups_owners = options
new App.ControllerForm(
@controllerFormBulk = new App.ControllerForm(
el: @$('#form-ticket-bulk')
model:
configure_attributes: @configure_attributes_ticket
className: 'create'
className: 'Ticket'
labelClass: 'input-group-addon'
screen: 'overview_bulk'
handlersConfig: handlers
params: {}
filter: @formMeta.filter
formMeta: @formMeta
noFieldset: true
params: {}
filter: @formMeta.filter
formMeta: @formMeta
noFieldset: true
)
new App.ControllerForm(
el: @$('#form-ticket-bulk-comment')
model:
configure_attributes: [{ name: 'body', display: 'Comment', tag: 'textarea', rows: 4, null: true, upload: false, item_class: 'flex' }]
className: 'create'
className: 'Ticket'
labelClass: 'input-group-addon'
screen: 'overview_bulk_comment'
noFieldset: true
)
@ -87,8 +86,9 @@ class App.TicketBulkForm extends App.Controller
el: @$('#form-ticket-bulk-typeVisibility')
model:
configure_attributes: @confirm_attributes
className: 'create'
className: 'Ticket'
labelClass: 'input-group-addon'
screen: 'overview_bulk_visibility'
noFieldset: true
)
@ -183,7 +183,7 @@ class App.TicketBulkForm extends App.Controller
# validate ticket
errors = ticket.validate(
screen: 'edit'
controllerForm: @controllerFormBulk
)
if errors
@log 'error', 'update', errors

View File

@ -0,0 +1,27 @@
class _Singleton extends App._CollectionSingletonBase
event: 'ticket_overview_attributes'
restEndpoint: '/ticket_overview'
class App.TicketOverviewCollection
_instance = new _Singleton
@get: ->
_instance.get()
@one: (callback, init = true) ->
_instance.bind(callback, init, true)
@bind: (callback, init = true) ->
_instance.bind(callback, init, false)
@unbind: (callback) ->
_instance.unbind(callback)
@unbindById: (id) ->
_instance.unbindById(id)
@trigger: ->
_instance.trigger()
@fetch: ->
_instance.fetch()

View File

@ -40,7 +40,7 @@ class UserNew extends App.ControllerModal
content: ->
@controller = new App.ControllerForm(
model: App.User
screen: 'edit'
screen: 'create'
autofocus: true
)
@controller.form
@ -64,7 +64,9 @@ class UserNew extends App.ControllerModal
user = new App.User
user.load(params)
errors = user.validate()
errors = user.validate(
controllerForm: @controller
)
if errors
@log 'error', errors
@formValidate(form: e.target, errors: errors)

View File

@ -11,7 +11,7 @@ App.ValidUsersForTicketSelectionMethods =
users = @usersInGroups(ticket_group_ids)
# get the list of possible groups for the current user
# from the TicketCreateCollection
# from the TicketOverviewCollection
# (filled for e.g. the TicketCreation or TicketZoom assignment)
# and order them by name
group_ids = _.keys(@formMeta?.dependencies?.group_id)
@ -19,7 +19,7 @@ App.ValidUsersForTicketSelectionMethods =
groups_sorted = _.sortBy(groups, (group) -> group.name)
# get the number of visible users per group
# from the TicketCreateCollection
# from the TicketOverviewCollection
# (filled for e.g. the TicketCreation or TicketZoom assignment)
for group in groups
group.valid_users_count = @formMeta?.dependencies?.group_id?[group.id]?.owner_id.length || 0

View File

@ -234,17 +234,21 @@ class Singleton extends Base
failResponse: (options) =>
(xhr, statusText, error, settings) =>
switch settings.type
when 'POST' then @createFailed()
when 'DELETE' then @destroyFailed()
# add errors to calllback
@record.trigger('ajaxError', @record, xhr, statusText, error, settings)
if options.failResponseNoTrigger isnt true
switch settings.type
when 'POST' then @createFailed()
when 'DELETE' then @destroyFailed()
# add errors to calllback
@record.trigger('ajaxError', @record, xhr, statusText, error, settings)
#options.fail?.call(@record, settings)
detailsRaw = xhr.responseText
if !_.isEmpty(detailsRaw)
details = JSON.parse(detailsRaw)
options.fail?.call(@record, settings, details)
@record.trigger('destroy', @record)
if options.failResponseNoTrigger isnt true
@record.trigger('destroy', @record)
# /add errors to calllback
createFailed: ->

View File

@ -82,35 +82,15 @@ class App.Model extends Spine.Model
''
@validate: (data = {}) ->
screen = data?.controllerForm?.screen
# based on model attributes
if App[ data['model'] ] && App[ data['model'] ].attributesGet
attributes = App[ data['model'] ].attributesGet(data['screen'])
attributes = App[ data['model'] ].attributesGet(screen)
# based on custom attributes
else if data['model'].configure_attributes
attributes = App.Model.attributesGet(data['screen'], data['model'].configure_attributes)
# check required_if attributes
for attributeName, attribute of attributes
if attribute['required_if']
for key, values of attribute['required_if']
localValues = data['params'][key]
if !_.isArray( localValues )
localValues = [ localValues ]
match = false
for value in values
if localValues
for localValue in localValues
if value && localValue && value.toString() is localValue.toString()
match = true
if match is true
attribute['null'] = false
else
attribute['null'] = true
attributes = App.Model.attributesGet(screen, data['model'].configure_attributes)
# check attributes/each attribute of object
errors = {}
@ -120,7 +100,7 @@ class App.Model extends Spine.Model
if !attribute.readonly
# check required // if null is defined && null is false
if 'null' of attribute && !attribute['null']
if data.controllerForm && data.controllerForm.attributeIsMandatory(attribute.name)
# check :: fields
parts = attribute.name.split '::'
@ -168,9 +148,13 @@ class App.Model extends Spine.Model
# validate value
if data?.controllerForm && App.FormHandlerCoreWorkflow.requestsRunning(data.controllerForm)
errors['_core_workflow'] = { target: data.target, controllerForm: data.controllerForm }
# return error object
if !_.isEmpty(errors)
App.Log.error('Model', 'validation failed', errors)
if !errors['_core_workflow']
App.Log.error('Model', 'validation failed', errors)
return errors
# return no errors
@ -256,7 +240,8 @@ set new attributes of model (remove already available attributes)
App.Model.validate(
model: @constructor.className
params: @
screen: params.screen
controllerForm: params.controllerForm
target: params.target
)
isOnline: ->

View File

@ -0,0 +1,27 @@
class App.CoreWorkflow extends App.Model
@configure 'CoreWorkflow', 'name', 'object', 'preferences', 'condition_saved', 'condition_selected', 'perform', 'priority', 'active'
@extend Spine.Model.Ajax
@url: @apiPath + '/core_workflows'
@configure_attributes = [
{ name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, null: false },
{ name: 'object', display: 'Object', tag: 'select', null: false, nulloption: true },
{ name: 'preferences::screen', display: 'Action', tag: 'select', translate: true, null: true, multiple: true, nulloption: true },
{ name: 'condition_selected', display: 'Selected conditions', tag: 'core_workflow_condition', null: true, preview: false },
{ name: 'condition_saved', display: 'Saved conditions', tag: 'core_workflow_condition', null: true, preview: false },
{ name: 'perform', display: 'Action', tag: 'core_workflow_perform', null: true, preview: false },
{ name: 'stop_after_match', display: 'Stop after match', tag: 'boolean', null: false, default: false },
{ name: 'priority', display: 'Priority', tag: 'integer', type: 'text', limit: 100, null: false, default: 500 },
{ name: 'active', display: 'Active', tag: 'active', default: true },
{ name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 },
]
@configure_delete = true
@configure_clone = true
@configure_overview = [
'name',
'priority',
]
@description = '''
Core Workflows are actions or constraints on selections in forms. Depending on an action, it is possible to hide or restrict fields or to change the obligation to fill them in.
'''

View File

@ -0,0 +1,6 @@
class App.CoreWorkflowCustomModule extends App.Model
@configure 'CoreWorkflowCustomModule', 'name'
@configure_attributes = [
{ name: 'name', display: 'Name', tag: 'input', type: 'text', null: false },
]

View File

@ -11,9 +11,9 @@ class App.Sla extends App.Model
{ name: 'created_at', display: 'Created', tag: 'datetime', readonly: 1 },
{ name: 'updated_by_id', display: 'Updated by', relation: 'User', readonly: 1 },
{ name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 },
{ name: 'first_response_time', null: false, skipRendering: true, required_if: { 'first_response_time_enabled': ['on'] } },
{ name: 'update_time', null: false, skipRendering: true, required_if: { 'update_time_enabled': ['on'] } },
{ name: 'solution_time', null: false, skipRendering: true, required_if: { 'solution_time_enabled': ['on'] } },
{ name: 'first_response_time',skipRendering: true },
{ name: 'update_time', skipRendering: true },
{ name: 'solution_time', skipRendering: true },
]
@configure_delete = true
@configure_overview = [

View File

@ -0,0 +1 @@
<input type="hidden" class="empty" name="<%= @attribute.name %>::" value="" />

View File

@ -11,12 +11,14 @@
<%- @Icon('arrow-down', 'dropdown-arrow') %>
</div>
</div>
<% if @pre_condition: %>
<div class="controls">
<div class="u-positionOrigin js-preCondition">
<select></select>
<%- @Icon('arrow-down', 'dropdown-arrow') %>
</div>
</div>
<% end %>
<div class="controls js-value horizontal horizontal-filter-value"></div>
</div>
<div class="filter-controls">

View File

@ -1,4 +1,4 @@
<div data-attribute-name="<%= @attribute.name %>" class="<%= @attribute.tag %> form-group<%= " form-group--block" if @attribute.style == 'block' %><%= " #{ @attribute.item_class }" if @attribute.item_class %>"<%= " data-width=#{ @attribute.grid_width }" if @attribute.grid_width %>>
<div data-attribute-name="<%= @attribute.name %>" class="<%= @attribute.tag %> form-group<%= " form-group--block" if @attribute.style == 'block' %><%= " #{ @attribute.item_class }" if @attribute.item_class %><%= " is-required" if !@attribute.null %>"<%= " data-width=#{ @attribute.grid_width }" if @attribute.grid_width %>>
<% if @attribute.style == 'block': %>
<h2>
<% end %>

View File

@ -12,6 +12,9 @@ module ApplicationController::RendersModels
clean_params = object.association_name_to_id_convert(params)
clean_params = object.param_cleanup(clean_params, true)
if object.included_modules.include?(ChecksCoreWorkflow)
clean_params[:screen] = 'create'
end
# create object
generic_object = object.new(clean_params)
@ -46,6 +49,9 @@ module ApplicationController::RendersModels
clean_params = object.association_name_to_id_convert(params)
clean_params = object.param_cleanup(clean_params, true)
if object.included_modules.include?(ChecksCoreWorkflow)
clean_params[:screen] = 'update'
end
generic_object.with_lock do

View File

@ -0,0 +1,30 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflowsController < ApplicationController
prepend_before_action { authentication_check && authorize! }
def index
model_index_render(CoreWorkflow.changeable, params)
end
def show
model_show_render(CoreWorkflow.changeable, params)
end
def create
model_create_render(CoreWorkflow.changeable, params)
end
def update
model_update_render(CoreWorkflow.changeable, params)
end
def destroy
model_destroy_render(CoreWorkflow.changeable, params)
end
def perform
render json: CoreWorkflow.perform(payload: params, user: current_user)
end
end

View File

@ -3,6 +3,17 @@
class TicketOverviewsController < ApplicationController
prepend_before_action :authentication_check
# GET /api/v1/ticket_overview
def data
# get attributes to update
attributes_to_change = Ticket::ScreenOptions.attributes_to_change(
view: 'ticket_overview',
current_user: current_user,
)
render json: attributes_to_change
end
# GET /api/v1/ticket_overviews
def show

View File

@ -136,6 +136,7 @@ class TicketsController < ApplicationController
end
clean_params = Ticket.param_cleanup(clean_params, true)
clean_params[:screen] = 'create_middle'
ticket = Ticket.new(clean_params)
authorize!(ticket, :create?)
@ -232,6 +233,7 @@ class TicketsController < ApplicationController
# only apply preferences changes (keep not updated keys/values)
clean_params = ticket.param_preferences_merge(clean_params)
clean_params[:screen] = 'edit'
# disable changes on ticket number
clean_params.delete('number')
@ -426,6 +428,7 @@ class TicketsController < ApplicationController
# get attributes to update
attributes_to_change = Ticket::ScreenOptions.attributes_to_change(
view: 'ticket_create',
screen: 'create_middle',
current_user: current_user,
)
render json: attributes_to_change
@ -659,7 +662,8 @@ class TicketsController < ApplicationController
# get attributes to update
attributes_to_change = Ticket::ScreenOptions.attributes_to_change(
current_user: current_user,
ticket: ticket
ticket: ticket,
screen: 'edit',
)
# get related users

View File

@ -120,6 +120,7 @@ class UsersController < ApplicationController
user.with_lock do
clean_params = User.association_name_to_id_convert(params)
clean_params = User.param_cleanup(clean_params, true)
clean_params[:screen] = 'update'
user.update!(clean_params)
# presence and permissions were checked via `check_attributes_by_current_user_permission`
@ -887,7 +888,7 @@ curl http://localhost/api/v1/users/avatar -v -u #{login}:#{password} -H "Content
private
def clean_user_params
User.param_cleanup(User.association_name_to_id_convert(params), true)
User.param_cleanup(User.association_name_to_id_convert(params), true).merge(screen: 'create')
end
# @summary Creates a User record with the provided attribute values.

View File

@ -65,11 +65,14 @@ get assets and record_ids of selector
attribute = item.split('.')
next if !attribute[1]
if attribute[0] == 'customer' || attribute[0] == 'session'
attribute[0] = 'user'
end
begin
attribute_class = attribute[0].to_classname.constantize
rescue => e
next if attribute[0] == 'article'
next if attribute[0] == 'customer'
next if attribute[0] == 'execution_time'
logger.error "Unable to get asset for '#{attribute[0]}': #{e.inspect}"

View File

@ -22,7 +22,7 @@ returns
=end
def param_cleanup(params, new_object = false, inside_nested = false)
def param_cleanup(params, new_object = false, inside_nested = false, exceptions = true)
if params.respond_to?(:permit!)
params = params.permit!.to_h
@ -52,6 +52,8 @@ returns
new.attributes.each_key do |attribute|
next if !data.key?(attribute)
invalid = false
# check reference records, referenced by _id attributes
reflect_on_all_associations.map do |assoc|
class_name = assoc.options[:class_name]
@ -62,8 +64,14 @@ returns
next if data[name].blank?
next if assoc.klass.lookup(id: data[name])
raise Exceptions::UnprocessableEntity, "Invalid value for param '#{name}': #{data[name].inspect}"
raise Exceptions::UnprocessableEntity, "Invalid value for param '#{name}': #{data[name].inspect}" if exceptions
invalid = true
break
end
next if invalid
clean_params[attribute] = data[attribute]
end

View File

@ -11,6 +11,7 @@ module ApplicationModel::HasCache
def cache_update(other)
cache_delete if respond_to?('cache_delete')
other.cache_delete if other.respond_to?('cache_delete')
ActiveSupport::CurrentAttributes.clear_all
true
end

View File

@ -22,21 +22,43 @@ returns
=end
def permissions?(auth_query)
verbatim, wildcards = acceptable_permissions_for(auth_query)
permissions.where(name: verbatim).then do |base_query|
wildcards.reduce(base_query) do |query, name|
query.or(permissions.where('permissions.name LIKE ?', name.sub('.*', '.%')))
end
end.exists?
RequestCache.permissions?(self, auth_query)
end
private
class RequestCache < ActiveSupport::CurrentAttributes
attribute :permission_cache
def acceptable_permissions_for(auth_query)
Array(auth_query)
.reject { |name| Permission.lookup(name: name)&.active == false } # See "chain-of-ancestry quirk" in spec file
.flat_map { |name| Permission.with_parents(name) }.uniq
.partition { |name| name.end_with?('.*') }.reverse
def self.permissions?(authorizable, auth_query)
self.permission_cache ||= {}
begin
authorizable_key = authorizable.to_global_id.to_s
rescue
return instance.permissions?(authorizable, auth_query)
end
auth_query_key = Array(auth_query).join('|')
self.permission_cache[authorizable_key] ||= {}
self.permission_cache[authorizable_key][auth_query_key] ||= instance.permissions?(authorizable, auth_query)
end
def permissions?(authorizable, auth_query)
verbatim, wildcards = acceptable_permissions_for(auth_query)
authorizable.permissions.where(name: verbatim).then do |base_query|
wildcards.reduce(base_query) do |query, name|
query.or(authorizable.permissions.where('permissions.name LIKE ?', name.sub('.*', '.%')))
end
end.exists?
end
private
def acceptable_permissions_for(auth_query)
Array(auth_query)
.reject { |name| Permission.lookup(name: name)&.active == false } # See "chain-of-ancestry quirk" in spec file
.flat_map { |name| Permission.with_parents(name) }.uniq
.partition { |name| name.end_with?('.*') }.reverse
end
end
end

View File

@ -0,0 +1,59 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
module ChecksCoreWorkflow
extend ActiveSupport::Concern
included do
before_validation :validate_workflows
attr_accessor :screen
end
private
def validate_workflows
return if !screen
return if !UserInfo.current_user_id
perform_result = CoreWorkflow.perform(payload: {
'event' => 'core_workflow',
'request_id' => 'ChecksCoreWorkflow.validate_workflows',
'class_name' => self.class.to_s,
'screen' => screen,
'params' => attributes
}, user: User.find(UserInfo.current_user_id))
check_restrict_values(perform_result)
check_visibility(perform_result)
check_mandatory(perform_result)
end
def check_restrict_values(perform_result)
changes.each_key do |key|
next if perform_result[:restrict_values][key].blank?
next if self[key].blank?
value_found = perform_result[:restrict_values][key].any? { |value| value.to_s == self[key].to_s }
next if value_found
raise Exceptions::UnprocessableEntity, "Invalid value '#{self[key]}' for field '#{key}'!"
end
end
def check_visibility(perform_result)
perform_result[:visibility].each_key do |key|
next if perform_result[:visibility][key] != 'remove'
self[key] = nil
end
end
def check_mandatory(perform_result)
perform_result[:mandatory].each_key do |key|
next if !perform_result[:mandatory][key]
next if self[key].present?
raise Exceptions::UnprocessableEntity, "Missing required value for field '#{key}'!"
end
end
end

View File

@ -0,0 +1,29 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow < ApplicationModel
include ChecksClientNotification
include CoreWorkflow::Assets
default_scope { order(priority: :asc, id: :asc) }
scope :active, -> { where(active: true) }
scope :changeable, -> { where(changeable: true) }
scope :object, ->(object) { where(object: [object, nil]) }
store :preferences
store :condition_saved
store :condition_selected
store :perform
validates :name, presence: true
def self.perform(payload:, user:, assets: {}, assets_in_result: true, result: {})
CoreWorkflow::Result.new(payload: payload, user: user, assets: assets, assets_in_result: assets_in_result, result: result).run
rescue => e
return {} if e.is_a?(ArgumentError)
raise e if !Rails.env.production?
Rails.logger.error 'Error performing Core Workflow engine.'
Rails.logger.error e
{}
end
end

View File

@ -0,0 +1,41 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow
module Assets
extend ActiveSupport::Concern
def assets(data)
app_model_workflow = CoreWorkflow.to_app_model
data[ app_model_workflow ] ||= {}
return data if data[ app_model_workflow ][ id ]
data = assets_object(data)
assets_user(data)
end
end
def assets_object(data)
app_model_workflow = CoreWorkflow.to_app_model
data[ app_model_workflow ][ id ] = attributes_with_association_ids
data = assets_of_selector('condition_selected', data)
data = assets_of_selector('condition_saved', data)
assets_of_selector('perform', data)
end
def assets_user(data)
app_model_user = User.to_app_model
data[ app_model_user ] ||= {}
%w[created_by_id updated_by_id].each do |local_user_id|
next if !self[ local_user_id ]
next if data[ app_model_user ][ self[ local_user_id ] ]
user = User.lookup(id: self[ local_user_id ])
next if !user
data = user.assets(data)
end
data
end
end

View File

@ -0,0 +1,198 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
require 'digest/md5'
class CoreWorkflow::Attributes
attr_accessor :user, :payload, :assets
def initialize(result_object:)
@result_object = result_object
@user = result_object.user
@payload = result_object.payload
@assets = result_object.assets
end
def payload_class
@payload['class_name'].constantize
end
def selected_only
# params loading and preparing is very expensive so cache it
checksum = Digest::MD5.hexdigest(Marshal.dump(@payload['params']))
return @selected_only[checksum] if @selected_only.present? && @selected_only[checksum]
@selected_only = {}
@selected_only[checksum] = begin
clean_params = payload_class.association_name_to_id_convert(@payload['params'])
clean_params = payload_class.param_cleanup(clean_params, true, false, false)
payload_class.new(clean_params)
end
end
def overwrite_selected(result)
selected_attributes = selected_only.attributes
selected_attributes.each_key do |key|
next if selected_attributes[key].nil?
result[key.to_sym] = selected_attributes[key]
end
result
end
def selected
if @payload['params']['id'] && payload_class.exists?(id: @payload['params']['id'])
result = saved_only
overwrite_selected(result)
else
selected_only
end
end
def saved_only
return if @payload['params']['id'].blank?
# dont use lookup here because the cache will not
# know about new attributes and make crashes
@saved_only ||= payload_class.find_by(id: @payload['params']['id'])
end
def saved
@saved ||= saved_only || payload_class.new
end
def object_elements
@object_elements ||= ObjectManager::Object.new(@payload['class_name']).attributes(@user, saved_only, data_only: false).each_with_object([]) do |element, result|
result << element.data.merge(screens: element.screens)
end
end
def screen_value(attribute, type)
attribute[:screens].dig(@payload['screen'], type)
end
# dont cache this else the result object will work with references and cache bugs occur
def shown_default
object_elements.each_with_object({}) do |attribute, result|
result[ attribute[:name] ] = if @payload['request_id'] == 'ChecksCoreWorkflow.validate_workflows'
'show'
else
screen_value(attribute, 'shown') == false ? 'hide' : 'show'
end
end
end
# dont cache this else the result object will work with references and cache bugs occur
def mandatory_default
object_elements.each_with_object({}) do |attribute, result|
result[ attribute[:name] ] = if @payload['request_id'] == 'ChecksCoreWorkflow.validate_workflows'
false
elsif screen_value(attribute, 'required').nil?
!screen_value(attribute, 'null')
else
screen_value(attribute, 'required')
end
end
end
# dont cache this else the result object will work with references and cache bugs occur
def auto_select_default
object_elements.each_with_object({}) do |attribute, result|
next if !attribute[:only_shown_if_selectable]
result[ attribute[:name] ] = true
end
end
def options_array(options)
result = []
options.each do |option|
result << option['value']
if option['children'].present?
result += options_array(option['children'])
end
end
result
end
def options_hash(options)
options.keys
end
def options_relation(attribute)
key = "#{attribute[:relation]}_#{attribute[:name]}"
@options_relation ||= {}
@options_relation[key] ||= "CoreWorkflow::Attributes::#{attribute[:relation]}".constantize.new(attributes: self, attribute: attribute)
@options_relation[key].values
end
def attribute_filter?(attribute)
screen_value(attribute, 'filter').present?
end
def attribute_options_array?(attribute)
attribute[:options].present? && attribute[:options].instance_of?(Array)
end
def attribute_options_hash?(attribute)
attribute[:options].present? && attribute[:options].instance_of?(Hash)
end
def attribute_options_relation?(attribute)
attribute[:relation].present?
end
def values(attribute)
values = nil
if attribute_filter?(attribute)
values = screen_value(attribute, 'filter')
elsif attribute_options_array?(attribute)
values = options_array(attribute[:options])
elsif attribute_options_hash?(attribute)
values = options_hash(attribute[:options])
elsif attribute_options_relation?(attribute)
values = options_relation(attribute)
end
values
end
def values_empty(attribute, values)
return values if values == ['']
saved_value = saved_attribute_value(attribute)
if saved_value.present? && values.exclude?(saved_value)
values |= Array(saved_value.to_s)
end
if attribute[:nulloption] && values.exclude?('')
values.unshift('')
end
values
end
def restrict_values_default
result = {}
object_elements.each do |attribute|
values = values(attribute)
next if values.blank?
values = values_empty(attribute, values)
result[ attribute[:name] ] = values.map(&:to_s)
end
result
end
def saved_attribute_value(attribute)
saved_attribute_value = saved_only&.try(attribute[:name])
# special case for owner_id
if saved_only&.class == Ticket && attribute[:name] == 'owner_id' && saved_attribute_value == 1
saved_attribute_value = nil
end
saved_attribute_value
end
end

View File

@ -0,0 +1,12 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow::Attributes::Base
def initialize(attributes:, attribute:)
@attributes = attributes
@attribute = attribute
end
def values
[]
end
end

View File

@ -0,0 +1,7 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow::Attributes::EmailAddress < CoreWorkflow::Attributes::Base
def values
@values ||= EmailAddress.all.pluck(:id)
end
end

View File

@ -0,0 +1,33 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow::Attributes::Group < CoreWorkflow::Attributes::Base
def values
groups.each do |group|
assets(group)
end
if groups.blank?
['']
else
groups.pluck(:id)
end
end
def groups
@groups ||= if @attributes.user.permissions?('ticket.agent')
if @attributes.payload['screen'] == 'create_middle'
@attributes.user.groups_access(%w[create])
else
@attributes.user.groups_access(%w[create change])
end
else
Group.where(active: true)
end
end
def assets(group)
return if @attributes.assets[Group.to_app_model] && @attributes.assets[Group.to_app_model][group.id]
@attributes.assets = group.assets(@attributes.assets)
end
end

View File

@ -0,0 +1,4 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow::Attributes::Organization < CoreWorkflow::Attributes::Base
end

View File

@ -0,0 +1,7 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow::Attributes::Signature < CoreWorkflow::Attributes::Base
def values
@values ||= Signature.all.pluck(:id)
end
end

View File

@ -0,0 +1,12 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow::Attributes::TicketPriority < CoreWorkflow::Attributes::Base
def values
@values ||= begin
Ticket::Priority.where(active: true).each_with_object([]) do |priority, priority_ids|
@attributes.assets = priority.assets(@attributes.assets)
priority_ids.push priority.id
end
end
end
end

View File

@ -0,0 +1,36 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow::Attributes::TicketState < CoreWorkflow::Attributes::Base
def values
@values ||= begin
state_ids = []
if state_type && state_types.exclude?(state_type.name)
state_ids.push @attributes.saved.state_id
end
Ticket::State.joins(:state_type).where(ticket_state_types: { name: state_types }).each do |state|
state_ids.push state.id
assets(state)
end
state_ids
end
end
def state_type
return if @attributes.saved.id.blank?
@attributes.saved.state.state_type
end
def state_types
state_types = ['open', 'closed', 'pending action', 'pending reminder']
return state_types if @attributes.payload['screen'] != 'create_middle'
state_types.unshift('new')
end
def assets(state)
return if @attributes.assets[Ticket::State.to_app_model] && @attributes.assets[Ticket::State.to_app_model][state.id]
@attributes.assets = state.assets(@attributes.assets)
end
end

View File

@ -0,0 +1,101 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow::Attributes::User < CoreWorkflow::Attributes::Base
def values
return ticket_owner_id_bulk if @attributes.payload['screen'] == 'overview_bulk'
return ticket_owner_id if @attributes.payload['class_name'] == 'Ticket' && @attribute[:name] == 'owner_id'
[]
end
def group_agent_user_ids(group_id)
@group_agent_user_ids ||= {}
@group_agent_user_ids[group_id] ||= User.joins(', groups_users').where("users.id = groups_users.user_id AND groups_users.access = 'full' AND groups_users.group_id = ? AND users.id IN (?)", group_id, agent_user_ids).pluck(:id)
end
def group_agent_roles_ids(group_id)
@group_agent_roles_ids ||= {}
@group_agent_roles_ids[group_id] ||= Role.joins(', roles_groups').where("roles.id = roles_groups.role_id AND roles_groups.access = 'full' AND roles_groups.group_id = ? AND roles.id IN (?)", group_id, agent_role_ids).pluck(:id)
end
def agent_user_ids
@agent_user_ids ||= User.joins(:roles).where(users: { active: true }).where('roles_users.role_id' => agent_role_ids).pluck(:id)
end
def agent_role_ids
@agent_role_ids ||= Role.with_permissions('ticket.agent').pluck(:id)
end
def group_agent_role_user_ids(group_id)
@group_agent_role_user_ids ||= {}
@group_agent_role_user_ids[group_id] ||= User.joins(:roles).where(roles: { id: group_agent_roles_ids(group_id) }).pluck(:id)
end
def ticket_owner_id
return [''] if @attributes.selected_only.group_id.blank?
group_owner_ids
end
def group_owner_ids
user_ids = []
# dont show system user in frontend but allow to reset it to 1 on update/create of the ticket
if @attributes.payload['request_id'] == 'ChecksCoreWorkflow.validate_workflows'
user_ids = [1]
end
User.where(id: group_owner_ids_user_ids, active: true).each do |user|
user_ids << user.id
assets(user)
end
user_ids
end
def group_owner_ids_user_ids
group_agent_user_ids(@attributes.selected.group_id).concat(group_agent_role_user_ids(@attributes.selected.group_id)).uniq
end
def group_ids_bulk
@group_ids_bulk ||= begin
ticket_ids = String(@attributes.payload['params']['ticket_ids']).split(',').map(&:to_i)
Ticket.distinct.where(id: ticket_ids).pluck(:group_id)
end
end
def group_users_bulk
@group_users_bulk ||= begin
group_users_bulk_user_count.keys.select { |user| group_users_bulk_user_count[user] == group_ids_bulk.count }
end
end
def group_users_bulk_user_count
@group_users_bulk_user_count ||= begin
user_count = {}
group_ids_bulk.each do |group_id|
User.where(id: group_agent_user_ids(group_id).concat(group_agent_role_user_ids(group_id)).uniq, active: true).each do |user|
user_count[user] ||= 0
user_count[user] += 1
end
end
user_count
end
end
def ticket_owner_id_bulk
return group_owner_ids if @attributes.selected.group_id.present?
return [''] if group_users_bulk.blank?
group_users_bulk.each { |user| assets(user) }
group_users_bulk.map(&:id)
end
def assets(user)
return if @attributes.assets[User.to_app_model] && @attributes.assets[User.to_app_model][user.id]
@attributes.assets = user.assets(@attributes.assets)
end
end

View File

@ -0,0 +1,105 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow::Condition
include ::Mixin::HasBackends
attr_accessor :user, :payload, :workflow, :attribute_object, :result_object, :check
def initialize(result_object:, workflow:)
@user = result_object.user
@payload = result_object.payload
@workflow = workflow
@attribute_object = result_object.attributes
@result_object = result_object
@check = nil
end
def attributes
@attribute_object.send(@check)
end
def condition_key_value_object(key_split)
case key_split[0]
when 'session'
key_split.shift
obj = user
when attributes.class.to_s.downcase
key_split.shift
obj = attributes
else
obj = attributes
end
obj
end
def condition_key_value(key)
return Array(key) if key == 'custom.module'
key_split = key.split('.')
obj = condition_key_value_object(key_split)
key_split.each do |attribute|
if obj.instance_of?(User) && attribute =~ %r{^group_ids_(full|create|change|read|overview)$}
obj = obj.group_ids_access($1)
break
end
obj = obj.try(attribute.to_sym)
break if obj.blank?
end
condition_value_result(obj)
end
def condition_value_result(obj)
Array(obj).map(&:to_s).map(&:html2text)
end
def condition_value_match?(key, condition, value)
"CoreWorkflow::Condition::#{condition['operator'].tr(' ', '_').camelize}".constantize&.new(condition_object: self, key: key, condition: condition, value: value)&.match
end
def condition_match?(key, condition)
value_key = condition_key_value(key)
condition_value_match?(key, condition, value_key)
end
def condition_attributes_match?(check)
@check = check
condition = @workflow.send(:"condition_#{@check}")
return true if condition.blank?
result = true
condition.each do |key, value|
next if condition_match?(key, value)
result = false
break
end
result
end
def object_match?
return true if @workflow.object.blank?
@workflow.object.include?(@payload['class_name'])
end
def screen_match?
return true if @workflow.preferences['screen'].blank?
Array(@workflow.preferences['screen']).include?(@payload['screen'])
end
def match_all?
return if !object_match?
return if !screen_match?
return if !condition_attributes_match?('saved')
return if !condition_attributes_match?('selected')
true
end
end

View File

@ -0,0 +1,24 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow::Condition::Backend
def initialize(condition_object:, key:, condition:, value:)
@key = key
@condition_object = condition_object
@condition = condition
@value = value
end
attr_reader :value
def object?(object)
@condition_object.attributes.instance_of?(object)
end
def condition_value
Array(@condition['value']).map(&:to_s)
end
def match
false
end
end

View File

@ -0,0 +1,24 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow::Condition::Contains < CoreWorkflow::Condition::Backend
def match
result = false
value.each do |current_value|
current_match = false
condition_value.each do |current_condition_value|
next if current_condition_value.exclude?(current_value)
current_match = true
break
end
next if !current_match
result = true
break
end
result
end
end

View File

@ -0,0 +1,22 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow::Condition::ContainsAll < CoreWorkflow::Condition::Backend
def match
result = false
value.each do |current_value|
current_match = 0
condition_value.each do |current_condition_value|
next if current_condition_value.exclude?(current_value)
current_match += 1
end
next if current_match != condition_value.count
result = true
break
end
result
end
end

View File

@ -0,0 +1,24 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow::Condition::ContainsAllNot < CoreWorkflow::Condition::Backend
def match
return true if value.blank?
result = false
value.each do |current_value|
current_match = 0
condition_value.each do |current_condition_value|
next if current_condition_value.include?(current_value)
current_match += 1
end
next if current_match != condition_value.count
result = true
break
end
result
end
end

View File

@ -0,0 +1,26 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow::Condition::ContainsNot < CoreWorkflow::Condition::Backend
def match
return true if value.blank?
result = false
value.each do |current_value|
current_match = false
condition_value.each do |current_condition_value|
next if current_condition_value.include?(current_value)
current_match = true
break
end
next if !current_match
result = true
break
end
result
end
end

View File

@ -0,0 +1,15 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow::Condition::Is < CoreWorkflow::Condition::Backend
def match
result = false
value.each do |current_value|
next if condition_value.exclude?(current_value)
result = true
break
end
result
end
end

View File

@ -0,0 +1,17 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow::Condition::IsNot < CoreWorkflow::Condition::Backend
def match
return true if value.blank?
result = false
value.each do |current_value|
next if condition_value.include?(current_value)
result = true
break
end
result
end
end

View File

@ -0,0 +1,11 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow::Condition::IsSet < CoreWorkflow::Condition::Backend
def match
return false if object?(Ticket) && @key == 'ticket.owner_id' && value == ['1']
return false if value == ['']
return true if value.present?
false
end
end

View File

@ -0,0 +1,27 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow::Condition::MatchAllModules < CoreWorkflow::Condition::Backend
def match
return true if condition_value.blank?
result = false
value.each do |_current_value|
current_match = 0
condition_value.each do |current_condition_value|
custom_module = current_condition_value.constantize.new(condition_object: @condition_object, result_object: @result_object)
check = custom_module.send(:"#{@condition_object.check}_attribute_match?")
next if !check
current_match += 1
end
next if current_match != condition_value.count
result = true
break
end
result
end
end

View File

@ -0,0 +1,20 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow::Condition::MatchNoModules < CoreWorkflow::Condition::Backend
def match
result = true
value.each do |_current_value|
condition_value.each do |current_condition_value|
custom_module = current_condition_value.constantize.new(condition_object: @condition_object, result_object: @result_object)
check = custom_module.send(:"#{@condition_object.check}_attribute_match?")
next if !check
result = false
break
end
end
result
end
end

View File

@ -0,0 +1,20 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow::Condition::MatchOneModule < CoreWorkflow::Condition::Backend
def match
return true if condition_value.blank?
result = false
value.each do |_current_value|
condition_value.each do |current_condition_value|
custom_module = current_condition_value.constantize.new(condition_object: @condition_object, result_object: @result_object)
result = custom_module.send(:"#{@condition_object.check}_attribute_match?")
next if !result
break
end
end
result
end
end

View File

@ -0,0 +1,11 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow::Condition::NotSet < CoreWorkflow::Condition::Backend
def match
return true if value.blank?
return true if value == ['']
return true if object?(Ticket) && @key == 'ticket.owner_id' && value == ['1']
false
end
end

View File

@ -0,0 +1,24 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow::Condition::RegexMatch < CoreWorkflow::Condition::Backend
def match
result = false
value.each do |current_value|
current_match = false
condition_value.each do |current_condition_value|
next if !%r{#{current_condition_value}}.match?(current_value)
current_match = true
break
end
next if !current_match
result = true
break
end
result
end
end

View File

@ -0,0 +1,26 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow::Condition::RegexMismatch < CoreWorkflow::Condition::Backend
def match
return true if value.blank?
result = false
value.each do |current_value|
current_match = false
condition_value.each do |current_condition_value|
next if %r{#{current_condition_value}}.match?(current_value)
current_match = true
break
end
next if !current_match
result = true
break
end
result
end
end

View File

@ -0,0 +1,9 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow::Custom
include ::Mixin::HasBackends
def self.list
backends.map(&:to_s)
end
end

View File

@ -0,0 +1,37 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow::Custom::AdminCoreWorkflow < CoreWorkflow::Custom::Backend
def saved_attribute_match?
object?(CoreWorkflow)
end
def selected_attribute_match?
object?(CoreWorkflow)
end
def perform
perform_object_defaults
perform_screen_by_object
end
def perform_object_defaults
result('set_fixed_to', 'object', ['', 'Ticket', 'Organization', 'User', 'Group'])
end
def perform_screen_by_object
if selected.object.blank?
result('set_fixed_to', 'preferences::screen', [''])
return
end
result('set_fixed_to', 'preferences::screen', screens_by_object.uniq)
end
def screens_by_object
result = []
ObjectManager::Object.new(selected.object).attributes(@condition_object.user).each do |field|
result += field[:screen].keys
end
result
end
end

View File

@ -0,0 +1,37 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow::Custom::AdminSla < CoreWorkflow::Custom::Backend
def saved_attribute_match?
object?(Sla)
end
def selected_attribute_match?
object?(Sla)
end
def first_response_time_enabled
return 'set_mandatory' if params['first_response_time_enabled'].present?
'set_optional'
end
def update_time_enabled
return 'set_mandatory' if params['update_time_enabled'].present?
'set_optional'
end
def solution_time_enabled
return 'set_mandatory' if params['solution_time_enabled'].present?
'set_optional'
end
def perform
# make fields mandatory if checkbox is checked
result(first_response_time_enabled, 'first_response_time_in_text')
result(update_time_enabled, 'update_time_in_text')
result(solution_time_enabled, 'solution_time_in_text')
end
end

View File

@ -0,0 +1,46 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow::Custom::Backend
def initialize(condition_object:, result_object:)
@condition_object = condition_object
@result_object = result_object
end
def saved_attribute_match?
false
end
def selected_attribute_match?
false
end
def perform; end
def object?(object)
@condition_object.attributes.instance_of?(object)
end
def selected
@condition_object.attribute_object.selected
end
def selected_only
@condition_object.attribute_object.selected_only
end
def saved
@condition_object.attribute_object.saved
end
def saved_only
@condition_object.attribute_object.saved_only
end
def params
@condition_object.payload['params']
end
def result(backend, field, value = nil)
@result_object.run_backend_value(backend, field, value)
end
end

View File

@ -0,0 +1,32 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow::Custom::PendingTime < CoreWorkflow::Custom::Backend
def saved_attribute_match?
object?(Ticket)
end
def selected_attribute_match?
object?(Ticket)
end
def perform
result(visibility, 'pending_time')
result(mandatory, 'pending_time')
end
def visibility
return 'show' if pending?
'remove'
end
def mandatory
return 'set_mandatory' if pending?
'set_optional'
end
def pending?
['pending reminder', 'pending action'].include?(selected&.state&.state_type&.name)
end
end

View File

@ -0,0 +1,138 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow::Result
include ::Mixin::HasBackends
attr_accessor :payload, :user, :assets, :assets_in_result, :result, :rerun
def initialize(payload:, user:, assets: {}, assets_in_result: true, result: {})
raise ArgumentError, 'No payload->class_name given!' if !payload['class_name']
raise ArgumentError, 'No payload->screen given!' if !payload['screen']
@payload = payload
@user = user
@assets = assets
@assets_in_result = assets_in_result
@result = result
@rerun = false
end
def attributes
@attributes ||= CoreWorkflow::Attributes.new(result_object: self)
end
def workflows
CoreWorkflow.active.object(payload['class_name'])
end
def set_default
@rerun = false
@result = {
request_id: payload['request_id'],
restrict_values: {},
visibility: attributes.shown_default,
mandatory: attributes.mandatory_default,
select: @result[:select] || {},
fill_in: @result[:fill_in] || {},
eval: [],
matched_workflows: @result[:matched_workflows] || [],
rerun_count: @result[:rerun_count] || 0,
}
# restrict init defaults to make sure param values to removed if not allowed
attributes.restrict_values_default.each do |field, values|
run_backend_value('set_fixed_to', field, values)
end
set_default_only_shown_if_selectable
end
def set_default_only_shown_if_selectable
# only_shown_if_selectable should not work on bulk feature
return if @payload['screen'] == 'overview_bulk'
auto_hide = {}
attributes.auto_select_default.each do |field, state|
result = run_backend_value('auto_select', field, state)
next if result.compact.blank?
auto_hide[field] = true
end
auto_hide.each do |field, state|
run_backend_value('hide', field, state)
end
end
def run
set_default
workflows.each do |workflow|
condition = CoreWorkflow::Condition.new(result_object: self, workflow: workflow)
next if !condition.match_all?
run_workflow(workflow)
run_custom(workflow, condition)
match_workflow(workflow)
break if workflow.stop_after_match
end
consider_rerun
end
def run_workflow(workflow)
Array(workflow.perform).each do |field, config|
run_backend(field, config)
end
end
def run_custom(workflow, condition)
Array(workflow.perform.dig('custom.module', 'execute')).each do |module_path|
custom_module = module_path.constantize.new(condition_object: condition, result_object: self)
custom_module.perform
end
end
def run_backend(field, perform_config)
result = []
Array(perform_config['operator']).each do |backend|
result << "CoreWorkflow::Result::#{backend.classify}".constantize.new(result_object: self, field: field, perform_config: perform_config).run
end
result
end
def run_backend_value(backend, field, value)
perform_config = {
'operator' => backend,
backend => value,
}
run_backend(field, perform_config)
end
def match_workflow(workflow)
@result[:matched_workflows] |= Array(workflow.id)
end
def assets_in_result?
return false if !@assets_in_result
@result[:assets] = assets
true
end
def consider_rerun
if @rerun && @result[:rerun_count] < 25
@result[:rerun_count] += 1
return run
end
assets_in_result?
@result
end
end

View File

@ -0,0 +1,8 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow::Result::AddOption < CoreWorkflow::Result::BaseOption
def run
@result_object.result[:restrict_values][field] |= Array(@perform_config['add_option'])
true
end
end

View File

@ -0,0 +1,26 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow::Result::AutoSelect < CoreWorkflow::Result::Backend
def run
return true if params_set? && !too_many_values?
return if params_set?
return if too_many_values?
@result_object.result[:select][field] = last_value
@result_object.payload['params'][field] = last_value
set_rerun
true
end
def last_value
@result_object.result[:restrict_values][field].last
end
def params_set?
@result_object.payload['params'][field] == last_value
end
def too_many_values?
@result_object.result[:restrict_values][field].count { |v| v != '' } != 1
end
end

View File

@ -0,0 +1,21 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow::Result::Backend
def initialize(result_object:, field:, perform_config:)
@result_object = result_object
@field = field
@perform_config = perform_config
end
def field
@field.sub(%r{.*\.}, '')
end
def set_rerun
@result_object.rerun = true
end
def result(backend, field, value = nil)
@result_object.run_backend_value(backend, field, value)
end
end

View File

@ -0,0 +1,36 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow::Result::BaseOption < CoreWorkflow::Result::Backend
def remove_excluded_param_values
return if skip?
if @result_object.payload['params'][field].is_a?(Array)
remove_array
elsif excluded_by_restrict_values?(@result_object.payload['params'][field])
remove_string
end
end
def skip?
@result_object.payload['params'][field].blank?
end
def remove_array
@result_object.payload['params'][field] = @result_object.payload['params'][field].reject do |v|
excluded = excluded_by_restrict_values?(v)
if excluded
set_rerun
end
excluded
end
end
def remove_string
@result_object.payload['params'][field] = nil
set_rerun
end
def excluded_by_restrict_values?(value)
@result_object.result[:restrict_values][field].exclude?(value.to_s)
end
end

View File

@ -0,0 +1,32 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow::Result::FillIn < CoreWorkflow::Result::Backend
def run
return if skip?
@result_object.result[:fill_in][field] = fill_in_value
@result_object.payload['params'][field] = @result_object.result[:fill_in][field]
set_rerun
true
end
def skip?
return true if fill_in_value.blank?
return true if params_set?
return true if fill_in_set?
false
end
def fill_in_value
@perform_config['fill_in']
end
def params_set?
@result_object.payload['params'][field] && fill_in_value == @result_object.payload['params'][field]
end
def fill_in_set?
@result_object.result[:fill_in][field] && fill_in_value == @result_object.result[:fill_in][field]
end
end

View File

@ -0,0 +1,32 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow::Result::FillInEmpty < CoreWorkflow::Result::Backend
def run
return if skip?
@result_object.result[:fill_in][field] = fill_in_value
@result_object.payload['params'][field] = @result_object.result[:fill_in][field]
set_rerun
true
end
def skip?
return true if fill_in_value.blank?
return true if params_set?
return true if fill_in_set?
false
end
def fill_in_value
@perform_config['fill_in_empty']
end
def params_set?
@result_object.payload['params'][field].present?
end
def fill_in_set?
@result_object.result[:fill_in][field]
end
end

View File

@ -0,0 +1,8 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow::Result::Hide < CoreWorkflow::Result::Backend
def run
@result_object.result[:visibility][field] = 'hide'
true
end
end

View File

@ -0,0 +1,8 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow::Result::Remove < CoreWorkflow::Result::Backend
def run
@result_object.result[:visibility][field] = 'remove'
true
end
end

View File

@ -0,0 +1,10 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow::Result::RemoveOption < CoreWorkflow::Result::BaseOption
def run
@result_object.result[:restrict_values][field] ||= Array(@result_object.payload['params'][field])
@result_object.result[:restrict_values][field] -= Array(@perform_config['remove_option'])
remove_excluded_param_values
true
end
end

View File

@ -0,0 +1,32 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow::Result::Select < CoreWorkflow::Result::Backend
def run
return if skip?
@result_object.result[:select][field] = select_value
@result_object.payload['params'][field] = @result_object.result[:select][field]
set_rerun
true
end
def skip?
return true if select_value.blank?
return true if params_set?
return true if select_set?
false
end
def select_value
@select_value ||= Array(@perform_config['select']).reject { |v| @result_object.result[:restrict_values][field].exclude?(v) }.first
end
def params_set?
@result_object.payload['params'][field] && select_value == @result_object.payload['params'][field]
end
def select_set?
@result_object.result[:select][field] && select_value == @result_object.result[:select][field]
end
end

View File

@ -0,0 +1,25 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow::Result::SetFixedTo < CoreWorkflow::Result::BaseOption
def run
@result_object.result[:restrict_values][field] = if restriction_set?
restrict_values
else
replace_values
end
remove_excluded_param_values
true
end
def restriction_set?
@result_object.result[:restrict_values][field]
end
def restrict_values
@result_object.result[:restrict_values][field].reject { |v| Array(@perform_config['set_fixed_to']).exclude?(v) }
end
def replace_values
Array(@perform_config['set_fixed_to'])
end
end

View File

@ -0,0 +1,8 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow::Result::SetMandatory < CoreWorkflow::Result::Backend
def run
@result_object.result[:mandatory][field] = true
true
end
end

View File

@ -0,0 +1,8 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow::Result::SetOptional < CoreWorkflow::Result::Backend
def run
@result_object.result[:mandatory][field] = false
true
end
end

View File

@ -0,0 +1,8 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow::Result::Show < CoreWorkflow::Result::Backend
def run
@result_object.result[:visibility][field] = 'show'
true
end
end

View File

@ -4,6 +4,7 @@ class Group < ApplicationModel
include CanBeImported
include HasActivityStreamLog
include ChecksClientNotification
include ChecksCoreWorkflow
include ChecksHtmlSanitized
include ChecksLatestChangeObserved
include HasHistory

View File

@ -24,7 +24,7 @@ returns:
=end
def attributes(user, record = nil)
def attributes(user, record = nil, data_only: true)
@attributes ||= begin
attribute_records.each_with_object([]) do |attribute_record, result|
@ -36,7 +36,11 @@ returns:
next if !element.visible?
result.push element.data
if data_only
result.push element.data
else
result.push element
end
end
end
end

View File

@ -3,6 +3,7 @@
class Organization < ApplicationModel
include HasActivityStreamLog
include ChecksClientNotification
include ChecksCoreWorkflow
include ChecksLatestChangeObserved
include HasHistory
include HasSearchIndexBackend

Some files were not shown because too many files have changed in this diff Show More