From 05a471f90ddf40bbe3d7693cde3f0be7ab5a6ed4 Mon Sep 17 00:00:00 2001 From: Rolf Schmidt Date: Wed, 25 Aug 2021 14:24:42 +0200 Subject: [PATCH] Fixes #3709, #Fixes 1262 - Core Workflow implementation --- .../_application_controller/form.coffee | 179 +- .../generic_edit.coffee | 4 +- .../generic_index.coffee | 2 + .../generic_new.coffee | 6 +- .../_ui_element/_application_selector.coffee | 575 +++++++ .../_application_ui_element.coffee | 16 +- .../core_workflow_condition.coffee | 188 ++ .../_ui_element/core_workflow_perform.coffee | 135 ++ .../_ui_element/ticket_selector.coffee | 505 +----- .../_ui_element/tree_select.coffee | 29 + .../controllers/agent_ticket_create.coffee | 102 +- .../app/controllers/core_workflow.coffee | 57 + .../controllers/customer_ticket_create.coffee | 81 +- .../controllers/getting_started/admin.coffee | 2 +- .../controllers/getting_started/agent.coffee | 2 +- .../app/controllers/object_manager.coffee | 4 +- .../javascripts/app/controllers/signup.coffee | 2 +- .../app/controllers/ticket_customer.coffee | 8 +- .../app/controllers/ticket_overview.coffee | 45 +- .../app/controllers/ticket_zoom.coffee | 3 +- .../form_handler_admin_core_workflow.coffee | 15 + .../form_handler_core_workflow.coffee | 320 ++++ .../form_handler_dependencies.coffee | 33 - .../owner_handler_dependencies.coffee | 26 - .../controllers/ticket_zoom/sidebar.coffee | 3 + .../ticket_zoom/sidebar_ticket.coffee | 10 +- .../app/controllers/widget/invite_user.coffee | 4 +- .../app/controllers/widget/template.coffee | 6 +- .../widget/ticket_bulk_form.coffee | 34 +- .../ticket_overview_collection.coffee | 27 + .../user_organization_autocompletion.coffee | 6 +- ..._users_for_ticket_selection_methods.coffee | 4 +- .../javascripts/app/lib/spine/ajax.coffee | 16 +- .../app/models/_application_model.coffee | 37 +- .../app/models/core_workflow.coffee | 27 + .../models/core_workflow_custom_module.coffee | 6 + app/assets/javascripts/app/models/sla.coffee | 6 +- ...r.jst.eco => application_selector.jst.eco} | 0 .../application_selector_empty.jst.eco | 1 + ...t.eco => application_selector_row.jst.eco} | 2 + .../app/views/generic/attribute.jst.eco | 2 +- .../application_controller/renders_models.rb | 6 + app/controllers/core_workflows_controller.rb | 30 + .../ticket_overviews_controller.rb | 11 + app/controllers/tickets_controller.rb | 6 +- app/controllers/users_controller.rb | 3 +- app/models/application_model/can_assets.rb | 5 +- .../application_model/can_cleanup_param.rb | 12 +- app/models/application_model/has_cache.rb | 1 + app/models/concerns/can_be_authorized.rb | 48 +- app/models/concerns/checks_core_workflow.rb | 59 + app/models/core_workflow.rb | 29 + app/models/core_workflow/assets.rb | 41 + app/models/core_workflow/attributes.rb | 198 +++ app/models/core_workflow/attributes/base.rb | 12 + .../core_workflow/attributes/email_address.rb | 7 + app/models/core_workflow/attributes/group.rb | 33 + .../core_workflow/attributes/organization.rb | 4 + .../core_workflow/attributes/signature.rb | 7 + .../attributes/ticket_priority.rb | 12 + .../core_workflow/attributes/ticket_state.rb | 36 + app/models/core_workflow/attributes/user.rb | 101 ++ app/models/core_workflow/condition.rb | 105 ++ app/models/core_workflow/condition/backend.rb | 24 + .../core_workflow/condition/contains.rb | 24 + .../core_workflow/condition/contains_all.rb | 22 + .../condition/contains_all_not.rb | 24 + .../core_workflow/condition/contains_not.rb | 26 + app/models/core_workflow/condition/is.rb | 15 + app/models/core_workflow/condition/is_not.rb | 17 + app/models/core_workflow/condition/is_set.rb | 11 + .../condition/match_all_modules.rb | 27 + .../condition/match_no_modules.rb | 20 + .../condition/match_one_module.rb | 20 + app/models/core_workflow/condition/not_set.rb | 11 + .../core_workflow/condition/regex_match.rb | 24 + .../core_workflow/condition/regex_mismatch.rb | 26 + app/models/core_workflow/custom.rb | 9 + .../custom/admin_core_workflow.rb | 37 + app/models/core_workflow/custom/admin_sla.rb | 37 + app/models/core_workflow/custom/backend.rb | 46 + .../core_workflow/custom/pending_time.rb | 32 + app/models/core_workflow/result.rb | 138 ++ app/models/core_workflow/result/add_option.rb | 8 + .../core_workflow/result/auto_select.rb | 26 + app/models/core_workflow/result/backend.rb | 21 + .../core_workflow/result/base_option.rb | 36 + app/models/core_workflow/result/fill_in.rb | 32 + .../core_workflow/result/fill_in_empty.rb | 32 + app/models/core_workflow/result/hide.rb | 8 + app/models/core_workflow/result/remove.rb | 8 + .../core_workflow/result/remove_option.rb | 10 + app/models/core_workflow/result/select.rb | 32 + .../core_workflow/result/set_fixed_to.rb | 25 + .../core_workflow/result/set_mandatory.rb | 8 + .../core_workflow/result/set_optional.rb | 8 + app/models/core_workflow/result/show.rb | 8 + app/models/group.rb | 1 + app/models/object_manager/object.rb | 8 +- app/models/organization.rb | 1 + app/models/scheduler.rb | 2 + app/models/sla.rb | 1 + app/models/ticket.rb | 1 + app/models/ticket/screen_options.rb | 107 +- app/models/user.rb | 1 + .../core_workflows_controller_policy.rb | 6 + app/views/tests/form_core_workflow.html.erb | 21 + config/routes/core_workflow.rb | 13 + config/routes/test.rb | 1 + config/routes/ticket.rb | 1 + db/migrate/20120101000001_create_base.rb | 19 + .../20210128131507_init_core_workflow.rb | 165 ++ db/seeds.rb | 2 +- db/seeds/core_workflow.rb | 62 + db/seeds/object_manager_attributes.rb | 171 +- db/seeds/settings.rb | 26 + lib/import/otrs/state_factory.rb | 17 - lib/session_helper/collection_base.rb | 4 + lib/sessions/event/core_workflow.rb | 13 + lib/websocket_server.rb | 2 + public/assets/tests/form.js | 70 +- public/assets/tests/form_core_workflow.js | 71 + public/assets/tests/form_sla_times.js | 36 - public/assets/tests/model.js | 201 --- spec/factories/core_workflow.rb | 10 + spec/lib/import/otrs/state_factory_spec.rb | 20 - .../concerns/checks_core_workflow_examples.rb | 94 + spec/models/core_workflow/attributes_spec.rb | 65 + spec/models/core_workflow_spec.rb | 1520 +++++++++++++++++ spec/models/ticket_spec.rb | 2 + spec/models/user_spec.rb | 1 + spec/support/current_attributes.rb | 8 + .../system/examples/core_workflow_examples.rb | 515 ++++++ spec/system/js/q_unit_spec.rb | 4 + spec/system/manage/groups_spec.rb | 15 + spec/system/manage/organizations_spec.rb | 2 +- spec/system/organization/profile_spec.rb | 26 + spec/system/system/core_workflow_spec.rb | 39 + spec/system/system/sla_spec.rb | 60 + spec/system/ticket/create_spec.rb | 14 + spec/system/ticket/zoom_spec.rb | 18 +- spec/system/user/profile_spec.rb | 18 + test/browser_test_helper.rb | 8 +- test/support/current_attributes_helper.rb | 11 + test/unit/ticket_screen_options_test.rb | 627 ------- 145 files changed, 6338 insertions(+), 1910 deletions(-) create mode 100644 app/assets/javascripts/app/controllers/_ui_element/_application_selector.coffee create mode 100644 app/assets/javascripts/app/controllers/_ui_element/core_workflow_condition.coffee create mode 100644 app/assets/javascripts/app/controllers/_ui_element/core_workflow_perform.coffee create mode 100644 app/assets/javascripts/app/controllers/core_workflow.coffee create mode 100644 app/assets/javascripts/app/controllers/ticket_zoom/form_handler_admin_core_workflow.coffee create mode 100644 app/assets/javascripts/app/controllers/ticket_zoom/form_handler_core_workflow.coffee delete mode 100644 app/assets/javascripts/app/controllers/ticket_zoom/form_handler_dependencies.coffee delete mode 100644 app/assets/javascripts/app/controllers/ticket_zoom/owner_handler_dependencies.coffee create mode 100644 app/assets/javascripts/app/lib/app_post/ticket_overview_collection.coffee create mode 100644 app/assets/javascripts/app/models/core_workflow.coffee create mode 100644 app/assets/javascripts/app/models/core_workflow_custom_module.coffee rename app/assets/javascripts/app/views/generic/{ticket_selector.jst.eco => application_selector.jst.eco} (100%) create mode 100644 app/assets/javascripts/app/views/generic/application_selector_empty.jst.eco rename app/assets/javascripts/app/views/generic/{ticket_selector_row.jst.eco => application_selector_row.jst.eco} (96%) create mode 100644 app/controllers/core_workflows_controller.rb create mode 100644 app/models/concerns/checks_core_workflow.rb create mode 100644 app/models/core_workflow.rb create mode 100644 app/models/core_workflow/assets.rb create mode 100644 app/models/core_workflow/attributes.rb create mode 100644 app/models/core_workflow/attributes/base.rb create mode 100644 app/models/core_workflow/attributes/email_address.rb create mode 100644 app/models/core_workflow/attributes/group.rb create mode 100644 app/models/core_workflow/attributes/organization.rb create mode 100644 app/models/core_workflow/attributes/signature.rb create mode 100644 app/models/core_workflow/attributes/ticket_priority.rb create mode 100644 app/models/core_workflow/attributes/ticket_state.rb create mode 100644 app/models/core_workflow/attributes/user.rb create mode 100644 app/models/core_workflow/condition.rb create mode 100644 app/models/core_workflow/condition/backend.rb create mode 100644 app/models/core_workflow/condition/contains.rb create mode 100644 app/models/core_workflow/condition/contains_all.rb create mode 100644 app/models/core_workflow/condition/contains_all_not.rb create mode 100644 app/models/core_workflow/condition/contains_not.rb create mode 100644 app/models/core_workflow/condition/is.rb create mode 100644 app/models/core_workflow/condition/is_not.rb create mode 100644 app/models/core_workflow/condition/is_set.rb create mode 100644 app/models/core_workflow/condition/match_all_modules.rb create mode 100644 app/models/core_workflow/condition/match_no_modules.rb create mode 100644 app/models/core_workflow/condition/match_one_module.rb create mode 100644 app/models/core_workflow/condition/not_set.rb create mode 100644 app/models/core_workflow/condition/regex_match.rb create mode 100644 app/models/core_workflow/condition/regex_mismatch.rb create mode 100644 app/models/core_workflow/custom.rb create mode 100644 app/models/core_workflow/custom/admin_core_workflow.rb create mode 100644 app/models/core_workflow/custom/admin_sla.rb create mode 100644 app/models/core_workflow/custom/backend.rb create mode 100644 app/models/core_workflow/custom/pending_time.rb create mode 100644 app/models/core_workflow/result.rb create mode 100644 app/models/core_workflow/result/add_option.rb create mode 100644 app/models/core_workflow/result/auto_select.rb create mode 100644 app/models/core_workflow/result/backend.rb create mode 100644 app/models/core_workflow/result/base_option.rb create mode 100644 app/models/core_workflow/result/fill_in.rb create mode 100644 app/models/core_workflow/result/fill_in_empty.rb create mode 100644 app/models/core_workflow/result/hide.rb create mode 100644 app/models/core_workflow/result/remove.rb create mode 100644 app/models/core_workflow/result/remove_option.rb create mode 100644 app/models/core_workflow/result/select.rb create mode 100644 app/models/core_workflow/result/set_fixed_to.rb create mode 100644 app/models/core_workflow/result/set_mandatory.rb create mode 100644 app/models/core_workflow/result/set_optional.rb create mode 100644 app/models/core_workflow/result/show.rb create mode 100644 app/policies/controllers/core_workflows_controller_policy.rb create mode 100644 app/views/tests/form_core_workflow.html.erb create mode 100644 config/routes/core_workflow.rb create mode 100644 db/migrate/20210128131507_init_core_workflow.rb create mode 100644 db/seeds/core_workflow.rb create mode 100644 lib/sessions/event/core_workflow.rb create mode 100644 public/assets/tests/form_core_workflow.js create mode 100644 spec/factories/core_workflow.rb create mode 100644 spec/models/concerns/checks_core_workflow_examples.rb create mode 100644 spec/models/core_workflow/attributes_spec.rb create mode 100644 spec/models/core_workflow_spec.rb create mode 100644 spec/support/current_attributes.rb create mode 100644 spec/system/examples/core_workflow_examples.rb create mode 100644 spec/system/organization/profile_spec.rb create mode 100644 spec/system/system/core_workflow_spec.rb create mode 100644 spec/system/system/sla_spec.rb create mode 100644 test/support/current_attributes_helper.rb delete mode 100644 test/unit/ticket_screen_options_test.rb diff --git a/app/assets/javascripts/app/controllers/_application_controller/form.coffee b/app/assets/javascripts/app/controllers/_application_controller/form.coffee index 39ca618d5..bcdc01a85 100644 --- a/app/assets/javascripts/app/controllers/_application_controller/form.coffee +++ b/app/assets/javascripts/app/controllers/_application_controller/form.coffee @@ -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) diff --git a/app/assets/javascripts/app/controllers/_application_controller/generic_edit.coffee b/app/assets/javascripts/app/controllers/_application_controller/generic_edit.coffee index ea1261de8..568737b6f 100644 --- a/app/assets/javascripts/app/controllers/_application_controller/generic_edit.coffee +++ b/app/assets/javascripts/app/controllers/_application_controller/generic_edit.coffee @@ -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 ) diff --git a/app/assets/javascripts/app/controllers/_application_controller/generic_index.coffee b/app/assets/javascripts/app/controllers/_application_controller/generic_index.coffee index 9312b5eb3..bbc41cbd2 100644 --- a/app/assets/javascripts/app/controllers/_application_controller/generic_index.coffee +++ b/app/assets/javascripts/app/controllers/_application_controller/generic_index.coffee @@ -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) -> diff --git a/app/assets/javascripts/app/controllers/_application_controller/generic_new.coffee b/app/assets/javascripts/app/controllers/_application_controller/generic_new.coffee index 23f594873..e965d3895 100644 --- a/app/assets/javascripts/app/controllers/_application_controller/generic_new.coffee +++ b/app/assets/javascripts/app/controllers/_application_controller/generic_new.coffee @@ -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 ) diff --git a/app/assets/javascripts/app/controllers/_ui_element/_application_selector.coffee b/app/assets/javascripts/app/controllers/_ui_element/_application_selector.coffee new file mode 100644 index 000000000..f39dd7eab --- /dev/null +++ b/app/assets/javascripts/app/controllers/_ui_element/_application_selector.coffee @@ -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 = $('') + for groupKey, groupMeta of groups + groupKeyClass = groupKey.replace('.', '-') + displayName = App.i18n.translateInline(groupMeta.name) + selection.closest('select').append("") + 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("") + 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 = $("") + + 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("") + 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 = $("") + 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("") + 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')} #{App.i18n.translateContent(model)} -> #{App.i18n.translateContent(config.display)} #{App.i18n.translateContent(operator)} #{valueHuman}." + + 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 diff --git a/app/assets/javascripts/app/controllers/_ui_element/_application_ui_element.coffee b/app/assets/javascripts/app/controllers/_ui_element/_application_ui_element.coffee index 605f1d052..9c48d193b 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/_application_ui_element.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/_application_ui_element.coffee @@ -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) -> diff --git a/app/assets/javascripts/app/controllers/_ui_element/core_workflow_condition.coffee b/app/assets/javascripts/app/controllers/_ui_element/core_workflow_condition.coffee new file mode 100644 index 000000000..10634e7a4 --- /dev/null +++ b/app/assets/javascripts/app/controllers/_ui_element/core_workflow_condition.coffee @@ -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('') + return + + super(elementFull, elementRow, groupAndAttribute, elements, meta, attribute) + + @HasPreCondition: -> + return false + + @hasEmptySelectorAtStart: -> + return true diff --git a/app/assets/javascripts/app/controllers/_ui_element/core_workflow_perform.coffee b/app/assets/javascripts/app/controllers/_ui_element/core_workflow_perform.coffee new file mode 100644 index 000000000..e30ff5191 --- /dev/null +++ b/app/assets/javascripts/app/controllers/_ui_element/core_workflow_perform.coffee @@ -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('') + return + + super(elementFull, elementRow, groupAndAttribute, elements, meta, attribute) + + @HasPreCondition: -> + return false + + @hasEmptySelectorAtStart: -> + return true + + @hasDuplicateSelector: -> + return true diff --git a/app/assets/javascripts/app/controllers/_ui_element/ticket_selector.coffee b/app/assets/javascripts/app/controllers/_ui_element/ticket_selector.coffee index 6a68478a6..373dd1ac7 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/ticket_selector.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/ticket_selector.coffee @@ -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 = $('') - for groupKey, groupMeta of groups - displayName = App.i18n.translateInline(groupMeta.name) - selection.closest('select').append("") - 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("") - 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 = $("") - - 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("") - 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 = $("") - 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("") - 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')} #{App.i18n.translateContent(model)} -> #{App.i18n.translateContent(config.display)} #{App.i18n.translateContent(operator)} #{valueHuman}." - - 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 diff --git a/app/assets/javascripts/app/controllers/_ui_element/tree_select.coffee b/app/assets/javascripts/app/controllers/_ui_element/tree_select.coffee index 1fca17e41..19ffac724 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/tree_select.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/tree_select.coffee @@ -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 diff --git a/app/assets/javascripts/app/controllers/agent_ticket_create.coffee b/app/assets/javascripts/app/controllers/agent_ticket_create.coffee index b3472e707..5e8a9299a 100644 --- a/app/assets/javascripts/app/controllers/agent_ticket_create.coffee +++ b/app/assets/javascripts/app/controllers/agent_ticket_create.coffee @@ -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 diff --git a/app/assets/javascripts/app/controllers/core_workflow.coffee b/app/assets/javascripts/app/controllers/core_workflow.coffee new file mode 100644 index 000000000..a537a9077 --- /dev/null +++ b/app/assets/javascripts/app/controllers/core_workflow.coffee @@ -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') diff --git a/app/assets/javascripts/app/controllers/customer_ticket_create.coffee b/app/assets/javascripts/app/controllers/customer_ticket_create.coffee index f7f7d5e76..79958e74d 100644 --- a/app/assets/javascripts/app/controllers/customer_ticket_create.coffee +++ b/app/assets/javascripts/app/controllers/customer_ticket_create.coffee @@ -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 diff --git a/app/assets/javascripts/app/controllers/getting_started/admin.coffee b/app/assets/javascripts/app/controllers/getting_started/admin.coffee index 37402ff10..21efe344c 100644 --- a/app/assets/javascripts/app/controllers/getting_started/admin.coffee +++ b/app/assets/javascripts/app/controllers/getting_started/admin.coffee @@ -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 diff --git a/app/assets/javascripts/app/controllers/getting_started/agent.coffee b/app/assets/javascripts/app/controllers/getting_started/agent.coffee index 6a1bb932c..86fe3ded6 100644 --- a/app/assets/javascripts/app/controllers/getting_started/agent.coffee +++ b/app/assets/javascripts/app/controllers/getting_started/agent.coffee @@ -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 diff --git a/app/assets/javascripts/app/controllers/object_manager.coffee b/app/assets/javascripts/app/controllers/object_manager.coffee index 2ddd69270..f3f8e03a2 100644 --- a/app/assets/javascripts/app/controllers/object_manager.coffee +++ b/app/assets/javascripts/app/controllers/object_manager.coffee @@ -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) diff --git a/app/assets/javascripts/app/controllers/signup.coffee b/app/assets/javascripts/app/controllers/signup.coffee index cfa57764f..c3973b3ce 100644 --- a/app/assets/javascripts/app/controllers/signup.coffee +++ b/app/assets/javascripts/app/controllers/signup.coffee @@ -50,7 +50,7 @@ class Signup extends App.ControllerFullPage user.load(@params) errors = user.validate( - screen: 'signup' + controllerForm: @form ) if errors diff --git a/app/assets/javascripts/app/controllers/ticket_customer.coffee b/app/assets/javascripts/app/controllers/ticket_customer.coffee index 713761a10..fad439156 100644 --- a/app/assets/javascripts/app/controllers/ticket_customer.coffee +++ b/app/assets/javascripts/app/controllers/ticket_customer.coffee @@ -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 diff --git a/app/assets/javascripts/app/controllers/ticket_overview.coffee b/app/assets/javascripts/app/controllers/ticket_overview.coffee index 89f391ec7..d9231b347 100644 --- a/app/assets/javascripts/app/controllers/ticket_overview.coffee +++ b/app/assets/javascripts/app/controllers/ticket_overview.coffee @@ -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 diff --git a/app/assets/javascripts/app/controllers/ticket_zoom.coffee b/app/assets/javascripts/app/controllers/ticket_zoom.coffee index 7eae1ab99..1f578c209 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom.coffee @@ -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 diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/form_handler_admin_core_workflow.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/form_handler_admin_core_workflow.coffee new file mode 100644 index 000000000..b09f7ad5e --- /dev/null +++ b/app/assets/javascripts/app/controllers/ticket_zoom/form_handler_admin_core_workflow.coffee @@ -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) + ) diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/form_handler_core_workflow.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/form_handler_core_workflow.coffee new file mode 100644 index 000000000..348ca3acd --- /dev/null +++ b/app/assets/javascripts/app/controllers/ticket_zoom/form_handler_core_workflow.coffee @@ -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) diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/form_handler_dependencies.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/form_handler_dependencies.coffee deleted file mode 100644 index 2515c6dc9..000000000 --- a/app/assets/javascripts/app/controllers/ticket_zoom/form_handler_dependencies.coffee +++ /dev/null @@ -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') diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/owner_handler_dependencies.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/owner_handler_dependencies.coffee deleted file mode 100644 index 74a853876..000000000 --- a/app/assets/javascripts/app/controllers/ticket_zoom/owner_handler_dependencies.coffee +++ /dev/null @@ -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') diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/sidebar.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/sidebar.coffee index 34f4c147f..e595807ad 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom/sidebar.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom/sidebar.coffee @@ -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 diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_ticket.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_ticket.coffee index b1f5b010b..2b1fdfaee 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_ticket.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_ticket.coffee @@ -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 ) diff --git a/app/assets/javascripts/app/controllers/widget/invite_user.coffee b/app/assets/javascripts/app/controllers/widget/invite_user.coffee index 852455f0c..8f0af712d 100644 --- a/app/assets/javascripts/app/controllers/widget/invite_user.coffee +++ b/app/assets/javascripts/app/controllers/widget/invite_user.coffee @@ -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 diff --git a/app/assets/javascripts/app/controllers/widget/template.coffee b/app/assets/javascripts/app/controllers/widget/template.coffee index daa991e11..2f02b9a37 100644 --- a/app/assets/javascripts/app/controllers/widget/template.coffee +++ b/app/assets/javascripts/app/controllers/widget/template.coffee @@ -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 diff --git a/app/assets/javascripts/app/controllers/widget/ticket_bulk_form.coffee b/app/assets/javascripts/app/controllers/widget/ticket_bulk_form.coffee index befe4da90..dad6e56be 100644 --- a/app/assets/javascripts/app/controllers/widget/ticket_bulk_form.coffee +++ b/app/assets/javascripts/app/controllers/widget/ticket_bulk_form.coffee @@ -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 diff --git a/app/assets/javascripts/app/lib/app_post/ticket_overview_collection.coffee b/app/assets/javascripts/app/lib/app_post/ticket_overview_collection.coffee new file mode 100644 index 000000000..4811bc25d --- /dev/null +++ b/app/assets/javascripts/app/lib/app_post/ticket_overview_collection.coffee @@ -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() diff --git a/app/assets/javascripts/app/lib/app_post/user_organization_autocompletion.coffee b/app/assets/javascripts/app/lib/app_post/user_organization_autocompletion.coffee index 6070589e4..2016263c1 100644 --- a/app/assets/javascripts/app/lib/app_post/user_organization_autocompletion.coffee +++ b/app/assets/javascripts/app/lib/app_post/user_organization_autocompletion.coffee @@ -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) diff --git a/app/assets/javascripts/app/lib/mixins/valid_users_for_ticket_selection_methods.coffee b/app/assets/javascripts/app/lib/mixins/valid_users_for_ticket_selection_methods.coffee index 9a828a4ec..1005ac792 100644 --- a/app/assets/javascripts/app/lib/mixins/valid_users_for_ticket_selection_methods.coffee +++ b/app/assets/javascripts/app/lib/mixins/valid_users_for_ticket_selection_methods.coffee @@ -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 diff --git a/app/assets/javascripts/app/lib/spine/ajax.coffee b/app/assets/javascripts/app/lib/spine/ajax.coffee index e4f5115d8..f04409b71 100644 --- a/app/assets/javascripts/app/lib/spine/ajax.coffee +++ b/app/assets/javascripts/app/lib/spine/ajax.coffee @@ -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: -> diff --git a/app/assets/javascripts/app/models/_application_model.coffee b/app/assets/javascripts/app/models/_application_model.coffee index f80aae679..501630619 100644 --- a/app/assets/javascripts/app/models/_application_model.coffee +++ b/app/assets/javascripts/app/models/_application_model.coffee @@ -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: -> diff --git a/app/assets/javascripts/app/models/core_workflow.coffee b/app/assets/javascripts/app/models/core_workflow.coffee new file mode 100644 index 000000000..a35eceb5f --- /dev/null +++ b/app/assets/javascripts/app/models/core_workflow.coffee @@ -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. +''' + diff --git a/app/assets/javascripts/app/models/core_workflow_custom_module.coffee b/app/assets/javascripts/app/models/core_workflow_custom_module.coffee new file mode 100644 index 000000000..7169c9c52 --- /dev/null +++ b/app/assets/javascripts/app/models/core_workflow_custom_module.coffee @@ -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 }, + ] + diff --git a/app/assets/javascripts/app/models/sla.coffee b/app/assets/javascripts/app/models/sla.coffee index 909f75da9..cf5512afc 100644 --- a/app/assets/javascripts/app/models/sla.coffee +++ b/app/assets/javascripts/app/models/sla.coffee @@ -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 = [ diff --git a/app/assets/javascripts/app/views/generic/ticket_selector.jst.eco b/app/assets/javascripts/app/views/generic/application_selector.jst.eco similarity index 100% rename from app/assets/javascripts/app/views/generic/ticket_selector.jst.eco rename to app/assets/javascripts/app/views/generic/application_selector.jst.eco diff --git a/app/assets/javascripts/app/views/generic/application_selector_empty.jst.eco b/app/assets/javascripts/app/views/generic/application_selector_empty.jst.eco new file mode 100644 index 000000000..d0cd62b58 --- /dev/null +++ b/app/assets/javascripts/app/views/generic/application_selector_empty.jst.eco @@ -0,0 +1 @@ + diff --git a/app/assets/javascripts/app/views/generic/ticket_selector_row.jst.eco b/app/assets/javascripts/app/views/generic/application_selector_row.jst.eco similarity index 96% rename from app/assets/javascripts/app/views/generic/ticket_selector_row.jst.eco rename to app/assets/javascripts/app/views/generic/application_selector_row.jst.eco index 34af5d8cc..3c6e696e8 100644 --- a/app/assets/javascripts/app/views/generic/ticket_selector_row.jst.eco +++ b/app/assets/javascripts/app/views/generic/application_selector_row.jst.eco @@ -11,12 +11,14 @@ <%- @Icon('arrow-down', 'dropdown-arrow') %> + <% if @pre_condition: %>
<%- @Icon('arrow-down', 'dropdown-arrow') %>
+ <% end %>
diff --git a/app/assets/javascripts/app/views/generic/attribute.jst.eco b/app/assets/javascripts/app/views/generic/attribute.jst.eco index 9cbfa468b..11288904f 100644 --- a/app/assets/javascripts/app/views/generic/attribute.jst.eco +++ b/app/assets/javascripts/app/views/generic/attribute.jst.eco @@ -1,4 +1,4 @@ -
<%= " #{ @attribute.item_class }" if @attribute.item_class %>"<%= " data-width=#{ @attribute.grid_width }" if @attribute.grid_width %>> +
<%= " #{ @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': %>

<% end %> diff --git a/app/controllers/application_controller/renders_models.rb b/app/controllers/application_controller/renders_models.rb index 0dc1e50f0..622384a14 100644 --- a/app/controllers/application_controller/renders_models.rb +++ b/app/controllers/application_controller/renders_models.rb @@ -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 diff --git a/app/controllers/core_workflows_controller.rb b/app/controllers/core_workflows_controller.rb new file mode 100644 index 000000000..8369ecd9d --- /dev/null +++ b/app/controllers/core_workflows_controller.rb @@ -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 diff --git a/app/controllers/ticket_overviews_controller.rb b/app/controllers/ticket_overviews_controller.rb index 8935f4524..78936ba74 100644 --- a/app/controllers/ticket_overviews_controller.rb +++ b/app/controllers/ticket_overviews_controller.rb @@ -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 diff --git a/app/controllers/tickets_controller.rb b/app/controllers/tickets_controller.rb index 95989b7f0..1f90c57ae 100644 --- a/app/controllers/tickets_controller.rb +++ b/app/controllers/tickets_controller.rb @@ -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 diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index ed1354ff3..d23bb2fb5 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -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. diff --git a/app/models/application_model/can_assets.rb b/app/models/application_model/can_assets.rb index 70fc28b87..9d43d8290 100644 --- a/app/models/application_model/can_assets.rb +++ b/app/models/application_model/can_assets.rb @@ -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}" diff --git a/app/models/application_model/can_cleanup_param.rb b/app/models/application_model/can_cleanup_param.rb index 8afe6a4e1..c2f3286fd 100644 --- a/app/models/application_model/can_cleanup_param.rb +++ b/app/models/application_model/can_cleanup_param.rb @@ -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 diff --git a/app/models/application_model/has_cache.rb b/app/models/application_model/has_cache.rb index b9b3c9dfa..02a54d042 100644 --- a/app/models/application_model/has_cache.rb +++ b/app/models/application_model/has_cache.rb @@ -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 diff --git a/app/models/concerns/can_be_authorized.rb b/app/models/concerns/can_be_authorized.rb index 1cdfd0641..7f5097a25 100644 --- a/app/models/concerns/can_be_authorized.rb +++ b/app/models/concerns/can_be_authorized.rb @@ -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 diff --git a/app/models/concerns/checks_core_workflow.rb b/app/models/concerns/checks_core_workflow.rb new file mode 100644 index 000000000..03066b468 --- /dev/null +++ b/app/models/concerns/checks_core_workflow.rb @@ -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 diff --git a/app/models/core_workflow.rb b/app/models/core_workflow.rb new file mode 100644 index 000000000..415c60dd4 --- /dev/null +++ b/app/models/core_workflow.rb @@ -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 diff --git a/app/models/core_workflow/assets.rb b/app/models/core_workflow/assets.rb new file mode 100644 index 000000000..1c4c522d2 --- /dev/null +++ b/app/models/core_workflow/assets.rb @@ -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 diff --git a/app/models/core_workflow/attributes.rb b/app/models/core_workflow/attributes.rb new file mode 100644 index 000000000..3e6e8498d --- /dev/null +++ b/app/models/core_workflow/attributes.rb @@ -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 diff --git a/app/models/core_workflow/attributes/base.rb b/app/models/core_workflow/attributes/base.rb new file mode 100644 index 000000000..c60115a8c --- /dev/null +++ b/app/models/core_workflow/attributes/base.rb @@ -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 diff --git a/app/models/core_workflow/attributes/email_address.rb b/app/models/core_workflow/attributes/email_address.rb new file mode 100644 index 000000000..736a5598d --- /dev/null +++ b/app/models/core_workflow/attributes/email_address.rb @@ -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 diff --git a/app/models/core_workflow/attributes/group.rb b/app/models/core_workflow/attributes/group.rb new file mode 100644 index 000000000..90339a337 --- /dev/null +++ b/app/models/core_workflow/attributes/group.rb @@ -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 diff --git a/app/models/core_workflow/attributes/organization.rb b/app/models/core_workflow/attributes/organization.rb new file mode 100644 index 000000000..1593b5211 --- /dev/null +++ b/app/models/core_workflow/attributes/organization.rb @@ -0,0 +1,4 @@ +# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ + +class CoreWorkflow::Attributes::Organization < CoreWorkflow::Attributes::Base +end diff --git a/app/models/core_workflow/attributes/signature.rb b/app/models/core_workflow/attributes/signature.rb new file mode 100644 index 000000000..50254ae57 --- /dev/null +++ b/app/models/core_workflow/attributes/signature.rb @@ -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 diff --git a/app/models/core_workflow/attributes/ticket_priority.rb b/app/models/core_workflow/attributes/ticket_priority.rb new file mode 100644 index 000000000..211230a12 --- /dev/null +++ b/app/models/core_workflow/attributes/ticket_priority.rb @@ -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 diff --git a/app/models/core_workflow/attributes/ticket_state.rb b/app/models/core_workflow/attributes/ticket_state.rb new file mode 100644 index 000000000..aabf40831 --- /dev/null +++ b/app/models/core_workflow/attributes/ticket_state.rb @@ -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 diff --git a/app/models/core_workflow/attributes/user.rb b/app/models/core_workflow/attributes/user.rb new file mode 100644 index 000000000..4700c0e46 --- /dev/null +++ b/app/models/core_workflow/attributes/user.rb @@ -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 diff --git a/app/models/core_workflow/condition.rb b/app/models/core_workflow/condition.rb new file mode 100644 index 000000000..f2dd9676b --- /dev/null +++ b/app/models/core_workflow/condition.rb @@ -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 diff --git a/app/models/core_workflow/condition/backend.rb b/app/models/core_workflow/condition/backend.rb new file mode 100644 index 000000000..fa7e474b2 --- /dev/null +++ b/app/models/core_workflow/condition/backend.rb @@ -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 diff --git a/app/models/core_workflow/condition/contains.rb b/app/models/core_workflow/condition/contains.rb new file mode 100644 index 000000000..31e3b371a --- /dev/null +++ b/app/models/core_workflow/condition/contains.rb @@ -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 diff --git a/app/models/core_workflow/condition/contains_all.rb b/app/models/core_workflow/condition/contains_all.rb new file mode 100644 index 000000000..e427fb592 --- /dev/null +++ b/app/models/core_workflow/condition/contains_all.rb @@ -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 diff --git a/app/models/core_workflow/condition/contains_all_not.rb b/app/models/core_workflow/condition/contains_all_not.rb new file mode 100644 index 000000000..ea78fba94 --- /dev/null +++ b/app/models/core_workflow/condition/contains_all_not.rb @@ -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 diff --git a/app/models/core_workflow/condition/contains_not.rb b/app/models/core_workflow/condition/contains_not.rb new file mode 100644 index 000000000..632d4603f --- /dev/null +++ b/app/models/core_workflow/condition/contains_not.rb @@ -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 diff --git a/app/models/core_workflow/condition/is.rb b/app/models/core_workflow/condition/is.rb new file mode 100644 index 000000000..f96286b8e --- /dev/null +++ b/app/models/core_workflow/condition/is.rb @@ -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 diff --git a/app/models/core_workflow/condition/is_not.rb b/app/models/core_workflow/condition/is_not.rb new file mode 100644 index 000000000..b13b16d7b --- /dev/null +++ b/app/models/core_workflow/condition/is_not.rb @@ -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 diff --git a/app/models/core_workflow/condition/is_set.rb b/app/models/core_workflow/condition/is_set.rb new file mode 100644 index 000000000..080d0e6dd --- /dev/null +++ b/app/models/core_workflow/condition/is_set.rb @@ -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 diff --git a/app/models/core_workflow/condition/match_all_modules.rb b/app/models/core_workflow/condition/match_all_modules.rb new file mode 100644 index 000000000..0747ba7a9 --- /dev/null +++ b/app/models/core_workflow/condition/match_all_modules.rb @@ -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 diff --git a/app/models/core_workflow/condition/match_no_modules.rb b/app/models/core_workflow/condition/match_no_modules.rb new file mode 100644 index 000000000..fc5ade722 --- /dev/null +++ b/app/models/core_workflow/condition/match_no_modules.rb @@ -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 diff --git a/app/models/core_workflow/condition/match_one_module.rb b/app/models/core_workflow/condition/match_one_module.rb new file mode 100644 index 000000000..791c1bb33 --- /dev/null +++ b/app/models/core_workflow/condition/match_one_module.rb @@ -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 diff --git a/app/models/core_workflow/condition/not_set.rb b/app/models/core_workflow/condition/not_set.rb new file mode 100644 index 000000000..83bdd63b9 --- /dev/null +++ b/app/models/core_workflow/condition/not_set.rb @@ -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 diff --git a/app/models/core_workflow/condition/regex_match.rb b/app/models/core_workflow/condition/regex_match.rb new file mode 100644 index 000000000..a4835ba51 --- /dev/null +++ b/app/models/core_workflow/condition/regex_match.rb @@ -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 diff --git a/app/models/core_workflow/condition/regex_mismatch.rb b/app/models/core_workflow/condition/regex_mismatch.rb new file mode 100644 index 000000000..c0d8f8e9a --- /dev/null +++ b/app/models/core_workflow/condition/regex_mismatch.rb @@ -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 diff --git a/app/models/core_workflow/custom.rb b/app/models/core_workflow/custom.rb new file mode 100644 index 000000000..dbb0d7b2d --- /dev/null +++ b/app/models/core_workflow/custom.rb @@ -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 diff --git a/app/models/core_workflow/custom/admin_core_workflow.rb b/app/models/core_workflow/custom/admin_core_workflow.rb new file mode 100644 index 000000000..db8f5bca7 --- /dev/null +++ b/app/models/core_workflow/custom/admin_core_workflow.rb @@ -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 diff --git a/app/models/core_workflow/custom/admin_sla.rb b/app/models/core_workflow/custom/admin_sla.rb new file mode 100644 index 000000000..23702b0b6 --- /dev/null +++ b/app/models/core_workflow/custom/admin_sla.rb @@ -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 diff --git a/app/models/core_workflow/custom/backend.rb b/app/models/core_workflow/custom/backend.rb new file mode 100644 index 000000000..7a84c148f --- /dev/null +++ b/app/models/core_workflow/custom/backend.rb @@ -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 diff --git a/app/models/core_workflow/custom/pending_time.rb b/app/models/core_workflow/custom/pending_time.rb new file mode 100644 index 000000000..b77a0b310 --- /dev/null +++ b/app/models/core_workflow/custom/pending_time.rb @@ -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 diff --git a/app/models/core_workflow/result.rb b/app/models/core_workflow/result.rb new file mode 100644 index 000000000..1e24ca193 --- /dev/null +++ b/app/models/core_workflow/result.rb @@ -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 diff --git a/app/models/core_workflow/result/add_option.rb b/app/models/core_workflow/result/add_option.rb new file mode 100644 index 000000000..570449001 --- /dev/null +++ b/app/models/core_workflow/result/add_option.rb @@ -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 diff --git a/app/models/core_workflow/result/auto_select.rb b/app/models/core_workflow/result/auto_select.rb new file mode 100644 index 000000000..3ff11601c --- /dev/null +++ b/app/models/core_workflow/result/auto_select.rb @@ -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 diff --git a/app/models/core_workflow/result/backend.rb b/app/models/core_workflow/result/backend.rb new file mode 100644 index 000000000..0a458f4b6 --- /dev/null +++ b/app/models/core_workflow/result/backend.rb @@ -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 diff --git a/app/models/core_workflow/result/base_option.rb b/app/models/core_workflow/result/base_option.rb new file mode 100644 index 000000000..5bf01932b --- /dev/null +++ b/app/models/core_workflow/result/base_option.rb @@ -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 diff --git a/app/models/core_workflow/result/fill_in.rb b/app/models/core_workflow/result/fill_in.rb new file mode 100644 index 000000000..cd9c00da4 --- /dev/null +++ b/app/models/core_workflow/result/fill_in.rb @@ -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 diff --git a/app/models/core_workflow/result/fill_in_empty.rb b/app/models/core_workflow/result/fill_in_empty.rb new file mode 100644 index 000000000..b50faae36 --- /dev/null +++ b/app/models/core_workflow/result/fill_in_empty.rb @@ -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 diff --git a/app/models/core_workflow/result/hide.rb b/app/models/core_workflow/result/hide.rb new file mode 100644 index 000000000..5f40380b5 --- /dev/null +++ b/app/models/core_workflow/result/hide.rb @@ -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 diff --git a/app/models/core_workflow/result/remove.rb b/app/models/core_workflow/result/remove.rb new file mode 100644 index 000000000..7a6b5c271 --- /dev/null +++ b/app/models/core_workflow/result/remove.rb @@ -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 diff --git a/app/models/core_workflow/result/remove_option.rb b/app/models/core_workflow/result/remove_option.rb new file mode 100644 index 000000000..a75e7f06c --- /dev/null +++ b/app/models/core_workflow/result/remove_option.rb @@ -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 diff --git a/app/models/core_workflow/result/select.rb b/app/models/core_workflow/result/select.rb new file mode 100644 index 000000000..24ce3e5fc --- /dev/null +++ b/app/models/core_workflow/result/select.rb @@ -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 diff --git a/app/models/core_workflow/result/set_fixed_to.rb b/app/models/core_workflow/result/set_fixed_to.rb new file mode 100644 index 000000000..45c481b10 --- /dev/null +++ b/app/models/core_workflow/result/set_fixed_to.rb @@ -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 diff --git a/app/models/core_workflow/result/set_mandatory.rb b/app/models/core_workflow/result/set_mandatory.rb new file mode 100644 index 000000000..e79d2b3cc --- /dev/null +++ b/app/models/core_workflow/result/set_mandatory.rb @@ -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 diff --git a/app/models/core_workflow/result/set_optional.rb b/app/models/core_workflow/result/set_optional.rb new file mode 100644 index 000000000..ab8634407 --- /dev/null +++ b/app/models/core_workflow/result/set_optional.rb @@ -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 diff --git a/app/models/core_workflow/result/show.rb b/app/models/core_workflow/result/show.rb new file mode 100644 index 000000000..4b5555f1c --- /dev/null +++ b/app/models/core_workflow/result/show.rb @@ -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 diff --git a/app/models/group.rb b/app/models/group.rb index 9686f1258..71a1b923a 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -4,6 +4,7 @@ class Group < ApplicationModel include CanBeImported include HasActivityStreamLog include ChecksClientNotification + include ChecksCoreWorkflow include ChecksHtmlSanitized include ChecksLatestChangeObserved include HasHistory diff --git a/app/models/object_manager/object.rb b/app/models/object_manager/object.rb index c5ba3eab8..f3758a0a9 100644 --- a/app/models/object_manager/object.rb +++ b/app/models/object_manager/object.rb @@ -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 diff --git a/app/models/organization.rb b/app/models/organization.rb index 66cf099c1..a9f587224 100644 --- a/app/models/organization.rb +++ b/app/models/organization.rb @@ -3,6 +3,7 @@ class Organization < ApplicationModel include HasActivityStreamLog include ChecksClientNotification + include ChecksCoreWorkflow include ChecksLatestChangeObserved include HasHistory include HasSearchIndexBackend diff --git a/app/models/scheduler.rb b/app/models/scheduler.rb index 283e31602..085456fc8 100644 --- a/app/models/scheduler.rb +++ b/app/models/scheduler.rb @@ -336,6 +336,8 @@ class Scheduler < ApplicationModel took = Time.zone.now - started_at logger.error "execute #{job.method} (try_count #{try_count}) exited with a non standard-error #{e.inspect} in: #{took} seconds." raise + ensure + ActiveSupport::CurrentAttributes.clear_all end def self.worker(foreground = false) diff --git a/app/models/sla.rb b/app/models/sla.rb index 2cc8d7ac4..ef68287cf 100644 --- a/app/models/sla.rb +++ b/app/models/sla.rb @@ -3,6 +3,7 @@ class Sla < ApplicationModel include ChecksClientNotification include ChecksConditionValidation + include ChecksCoreWorkflow include HasEscalationCalculationImpact include Sla::Assets diff --git a/app/models/ticket.rb b/app/models/ticket.rb index f4e454cd4..fe027e936 100644 --- a/app/models/ticket.rb +++ b/app/models/ticket.rb @@ -4,6 +4,7 @@ class Ticket < ApplicationModel include CanBeImported include HasActivityStreamLog include ChecksClientNotification + include ChecksCoreWorkflow include ChecksLatestChangeObserved include CanCsvImport include ChecksHtmlSanitized diff --git a/app/models/ticket/screen_options.rb b/app/models/ticket/screen_options.rb index 7019b407e..fe3e77f1b 100644 --- a/app/models/ticket/screen_options.rb +++ b/app/models/ticket/screen_options.rb @@ -11,6 +11,7 @@ list attributes ticket: ticket_model, current_user: User.find(123), + screen: 'create_middle', ) or only with user @@ -30,16 +31,7 @@ returns :group_id => [12] }, }, - :dependencies => { - :group_id => { - "" => { - : owner_id => [] - }, - 12 => { - : owner_id => [4, 5, 6, 7] - } - } - } + } =end @@ -50,36 +42,8 @@ returns params[:ticket] = Ticket.find(params[:ticket_id]) end - filter = {} assets = {} - - # get ticket states - state_ids = [] - if params[:ticket].present? - state_type = params[:ticket].state.state_type - end - state_types = ['open', 'closed', 'pending action', 'pending reminder'] - if state_type && state_types.exclude?(state_type.name) - state_ids.push params[:ticket].state_id - end - state_types.each do |type| - state_type = Ticket::StateType.find_by(name: type) - next if !state_type - - state_type.states.each do |state| - assets = state.assets(assets) - state_ids.push state.id - end - end - filter[:state_id] = state_ids - - # get priorities - priority_ids = [] - Ticket::Priority.where(active: true).each do |priority| - assets = priority.assets(assets) - priority_ids.push priority.id - end - filter[:priority_id] = priority_ids + filter = {} type_ids = [] if params[:ticket] @@ -96,45 +60,35 @@ returns end filter[:type_id] = type_ids - # get group / user relations - dependencies = { group_id: { '' => { owner_id: [] } } } + # get group / user relations (for bulk actions) + dependencies = nil + if params[:view] == 'ticket_overview' + dependencies = { group_id: { '' => { owner_id: [] } } } + groups = params[:current_user].groups_access(%w[create]) + agents = {} + agent_role_ids = Role.with_permissions('ticket.agent').pluck(:id) + agent_user_ids = User.joins(:roles).where(users: { active: true }).where('roles_users.role_id' => agent_role_ids).pluck(:id) + groups.each do |group| + assets = group.assets(assets) + dependencies[:group_id][group.id] = { owner_id: [] } - filter[:group_id] = [] - groups = if params[:current_user].permissions?('ticket.agent') - if params[:view] == 'ticket_create' - params[:current_user].groups_access(%w[create]) - else - params[:current_user].groups_access(%w[create change]) - end - else - Group.where(active: true) - end + group_agent_user_ids = 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) + group_agent_roles_ids = 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) + group_agent_role_user_ids = User.joins(:roles).where(roles: { id: group_agent_roles_ids }).pluck(:id) - agents = {} - agent_role_ids = Role.with_permissions('ticket.agent').pluck(:id) - agent_user_ids = User.joins(:roles).where(users: { active: true }).where('roles_users.role_id' => agent_role_ids).pluck(:id) - groups.each do |group| - filter[:group_id].push group.id - assets = group.assets(assets) - dependencies[:group_id][group.id] = { owner_id: [] } + User.where(id: group_agent_user_ids.concat(group_agent_role_user_ids).uniq, active: true).pluck(:id).each do |user_id| + dependencies[:group_id][group.id][:owner_id].push user_id + next if agents[user_id] - group_agent_user_ids = 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) - group_agent_roles_ids = 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) - group_agent_role_user_ids = User.joins(:roles).where(roles: { id: group_agent_roles_ids }).pluck(:id) + agents[user_id] = true + next if assets[:User] && assets[:User][user_id] - User.where(id: group_agent_user_ids.concat(group_agent_role_user_ids).uniq, active: true).pluck(:id).each do |user_id| - dependencies[:group_id][group.id][:owner_id].push user_id - next if agents[user_id] + user = User.lookup(id: user_id) + next if !user - agents[user_id] = true - next if assets[:User] && assets[:User][user_id] - - user = User.lookup(id: user_id) - next if !user - - assets = user.assets(assets) + assets = user.assets(assets) + end end - end configure_attributes = nil @@ -142,12 +96,21 @@ returns configure_attributes = ObjectManager::Object.new('Ticket').attributes(params[:current_user], params[:ticket]) end + core_workflow = CoreWorkflow.perform(payload: { + 'event' => 'core_workflow', + 'request_id' => 'default', + 'class_name' => 'Ticket', + 'screen' => params[:screen], + 'params' => Hash(params[:ticket]&.attributes) + }, user: params[:current_user], assets: assets, assets_in_result: false) + { assets: assets, form_meta: { filter: filter, dependencies: dependencies, configure_attributes: configure_attributes, + core_workflow: core_workflow } } end diff --git a/app/models/user.rb b/app/models/user.rb index 3b32f4841..f24dff18c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -5,6 +5,7 @@ class User < ApplicationModel include CanBeImported include HasActivityStreamLog include ChecksClientNotification + include ChecksCoreWorkflow include HasHistory include HasSearchIndexBackend include CanCsvImport diff --git a/app/policies/controllers/core_workflows_controller_policy.rb b/app/policies/controllers/core_workflows_controller_policy.rb new file mode 100644 index 000000000..42258bc38 --- /dev/null +++ b/app/policies/controllers/core_workflows_controller_policy.rb @@ -0,0 +1,6 @@ +# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ + +class Controllers::CoreWorkflowsControllerPolicy < Controllers::ApplicationControllerPolicy + permit! :perform, to: ['ticket.agent', 'ticket.customer'] + default_permit!('admin.core_workflow') +end diff --git a/app/views/tests/form_core_workflow.html.erb b/app/views/tests/form_core_workflow.html.erb new file mode 100644 index 000000000..832ef8215 --- /dev/null +++ b/app/views/tests/form_core_workflow.html.erb @@ -0,0 +1,21 @@ + + +<%= javascript_include_tag "/assets/tests/qunit-1.21.0.js", "/assets/tests/form_core_workflow.js", nonce: true %> + + + +<%= javascript_tag nonce: true do -%> +<% end -%> + +
+ +
+
+
+ +
+
diff --git a/config/routes/core_workflow.rb b/config/routes/core_workflow.rb new file mode 100644 index 000000000..de4d2684c --- /dev/null +++ b/config/routes/core_workflow.rb @@ -0,0 +1,13 @@ +# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ + +Zammad::Application.routes.draw do + api_path = Rails.configuration.api_path + + # core_workflows + match api_path + '/core_workflows', to: 'core_workflows#index', via: :get + match api_path + '/core_workflows/:id', to: 'core_workflows#show', via: :get + match api_path + '/core_workflows', to: 'core_workflows#create', via: :post + match api_path + '/core_workflows/:id', to: 'core_workflows#update', via: :put + match api_path + '/core_workflows/:id', to: 'core_workflows#destroy', via: :delete + match api_path + '/core_workflows/perform', to: 'core_workflows#perform', via: :post +end diff --git a/config/routes/test.rb b/config/routes/test.rb index c16c83d9d..eebd02eac 100644 --- a/config/routes/test.rb +++ b/config/routes/test.rb @@ -26,6 +26,7 @@ Zammad::Application.routes.draw do match '/tests_form_sla_times', to: 'tests#form_sla_times', via: :get match '/tests_form_skip_rendering', to: 'tests#form_skip_rendering', via: :get match '/tests_form_datetime', to: 'tests#form_datetime', via: :get + match '/tests_form_core_workflow', to: 'tests#form_core_workflow', via: :get match '/tests_table', to: 'tests#table', via: :get match '/tests_table_extended', to: 'tests#table_extended', via: :get match '/tests_html_utils', to: 'tests#html_utils', via: :get diff --git a/config/routes/ticket.rb b/config/routes/ticket.rb index d62181f11..2c9fa0bbb 100644 --- a/config/routes/ticket.rb +++ b/config/routes/ticket.rb @@ -21,6 +21,7 @@ Zammad::Application.routes.draw do match api_path + '/ticket_stats', to: 'tickets#stats', via: :get # ticket overviews + match api_path + '/ticket_overview', to: 'ticket_overviews#data', via: :get match api_path + '/ticket_overviews', to: 'ticket_overviews#show', via: :get # ticket priority diff --git a/db/migrate/20120101000001_create_base.rb b/db/migrate/20120101000001_create_base.rb index bb88f51bf..2ce9b29f2 100644 --- a/db/migrate/20120101000001_create_base.rb +++ b/db/migrate/20120101000001_create_base.rb @@ -761,5 +761,24 @@ class CreateBase < ActiveRecord::Migration[4.2] add_foreign_key :mentions, :users, column: :created_by_id add_foreign_key :mentions, :users, column: :updated_by_id add_foreign_key :mentions, :users, column: :user_id + + create_table :core_workflows do |t| + t.string :name, limit: 100, null: false + t.string :object, limit: 100, null: true + t.text :preferences, limit: 500.kilobytes + 1, null: true + t.text :condition_saved, limit: 500.kilobytes + 1, null: true + t.text :condition_selected, limit: 500.kilobytes + 1, null: true + t.text :perform, limit: 500.kilobytes + 1, null: true + t.boolean :active, null: false, default: true + t.boolean :stop_after_match, null: false, default: false + t.boolean :changeable, null: false, default: true + t.integer :priority, null: false, default: 0 + t.integer :updated_by_id, null: false + t.integer :created_by_id, null: false + t.timestamps limit: 3, null: false + end + add_index :core_workflows, [:name], unique: true + add_foreign_key :core_workflows, :users, column: :created_by_id + add_foreign_key :core_workflows, :users, column: :updated_by_id end end diff --git a/db/migrate/20210128131507_init_core_workflow.rb b/db/migrate/20210128131507_init_core_workflow.rb new file mode 100644 index 000000000..ad4782b78 --- /dev/null +++ b/db/migrate/20210128131507_init_core_workflow.rb @@ -0,0 +1,165 @@ +# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ + +class InitCoreWorkflow < ActiveRecord::Migration[5.2] + def change + return if !Setting.exists?(name: 'system_init_done') + + add_table + add_setting_ajax + fix_invalid_screens + fix_pending_time + fix_organization_screens + fix_user_screens + add_workflows + end + + def add_table # rubocop:disable Metrics/AbcSize + create_table :core_workflows do |t| + t.string :name, limit: 100, null: false + t.string :object, limit: 100, null: true + t.text :preferences, limit: 500.kilobytes + 1, null: true + t.text :condition_saved, limit: 500.kilobytes + 1, null: true + t.text :condition_selected, limit: 500.kilobytes + 1, null: true + t.text :perform, limit: 500.kilobytes + 1, null: true + t.boolean :active, null: false, default: true + t.boolean :stop_after_match, null: false, default: false + t.boolean :changeable, null: false, default: true + t.integer :priority, null: false, default: 0 + t.integer :updated_by_id, null: false + t.integer :created_by_id, null: false + t.timestamps limit: 3, null: false + end + add_index :core_workflows, [:name], unique: true + add_foreign_key :core_workflows, :users, column: :created_by_id + add_foreign_key :core_workflows, :users, column: :updated_by_id + end + + def add_setting_ajax + Setting.create_if_not_exists( + title: 'Core Workflow Ajax Mode', + name: 'core_workflow_ajax_mode', + area: 'System::UI', + description: 'Defines if the core workflow communication should run over AJAX instead of websockets.', + options: { + form: [ + { + display: '', + null: true, + name: 'core_workflow_ajax_mode', + tag: 'boolean', + options: { + true => 'yes', + false => 'no', + }, + }, + ], + }, + state: false, + preferences: { + prio: 3, + permission: ['admin.system'], + }, + frontend: true + ) + end + + def fix_invalid_screens + ObjectManager::Attribute.where(object_lookup_id: [ ObjectLookup.by_name('User'), ObjectLookup.by_name('Organization') ], editable: false).each do |attribute| + next if attribute.screens[:edit].blank? + next if attribute.screens[:create].present? + + attribute.screens[:create] = attribute.screens[:edit] + attribute.save + end + end + + def fix_pending_time + pending_time = ObjectManager::Attribute.find_by(name: 'pending_time', object_lookup: ObjectLookup.find_by(name: 'Ticket')) + pending_time.data_option.delete('required_if') + pending_time.data_option.delete('shown_if') + pending_time.save + end + + def fix_organization_screens + %w[domain note].each do |name| + field = ObjectManager::Attribute.find_by(name: name, object_lookup: ObjectLookup.find_by(name: 'Organization')) + field.screens['create'] ||= {} + field.screens['create']['-all-'] ||= {} + field.screens['create']['-all-']['null'] = true + field.save + end + end + + def fix_user_screens + %w[email web phone mobile organization_id fax department street zip city country address password vip note role_ids].each do |name| + field = ObjectManager::Attribute.find_by(name: name, object_lookup: ObjectLookup.find_by(name: 'User')) + field.screens['create'] ||= {} + field.screens['create']['-all-'] ||= {} + field.screens['create']['-all-']['null'] = true + field.save + end + end + + def add_workflows + CoreWorkflow.create_if_not_exists( + name: 'base - hide pending time on non pending states', + object: 'Ticket', + condition_saved: { + 'custom.module': { + operator: 'match all modules', + value: [ + 'CoreWorkflow::Custom::PendingTime', + ], + }, + }, + perform: { + 'custom.module': { + execute: ['CoreWorkflow::Custom::PendingTime'] + }, + }, + changeable: false, + created_by_id: 1, + updated_by_id: 1, + ) + CoreWorkflow.create_if_not_exists( + name: 'base - admin sla options', + object: 'Sla', + condition_saved: { + 'custom.module': { + operator: 'match all modules', + value: [ + 'CoreWorkflow::Custom::AdminSla', + ], + }, + }, + perform: { + 'custom.module': { + execute: ['CoreWorkflow::Custom::AdminSla'] + }, + }, + changeable: false, + created_by_id: 1, + updated_by_id: 1, + ) + CoreWorkflow.create_if_not_exists( + name: 'base - core workflow', + object: 'CoreWorkflow', + condition_saved: { + 'custom.module': { + operator: 'match all modules', + value: [ + 'CoreWorkflow::Custom::AdminCoreWorkflow', + ], + }, + }, + perform: { + 'custom.module': { + execute: ['CoreWorkflow::Custom::AdminCoreWorkflow'] + }, + }, + changeable: false, + created_by_id: 1, + updated_by_id: 1, + ) + end +end diff --git a/db/seeds.rb b/db/seeds.rb index a7e628383..2e8e2a11d 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -14,7 +14,7 @@ Cache.clear # this is the __ordered__ list of seed files # extend only if needed - try to add your changes # to the matching one of the existing files -seeds = %w[settings user_nr_1 signatures roles permissions groups links ticket_state_types ticket_states ticket_priorities ticket_article_types ticket_article_senders macros community_user_resources overviews channels report_profiles chats object_manager_attributes schedulers triggers karma_activities] +seeds = %w[settings user_nr_1 signatures roles permissions groups links ticket_state_types ticket_states ticket_priorities ticket_article_types ticket_article_senders macros community_user_resources overviews channels report_profiles chats object_manager_attributes schedulers triggers karma_activities core_workflow] # loop over and load all seedfiles # files will get executed automatically diff --git a/db/seeds/core_workflow.rb b/db/seeds/core_workflow.rb new file mode 100644 index 000000000..5f8cbbd12 --- /dev/null +++ b/db/seeds/core_workflow.rb @@ -0,0 +1,62 @@ +# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ + +CoreWorkflow.create_if_not_exists( + name: 'base - hide pending time on non pending states', + object: 'Ticket', + condition_saved: { + 'custom.module': { + operator: 'match all modules', + value: [ + 'CoreWorkflow::Custom::PendingTime', + ], + }, + }, + perform: { + 'custom.module': { + execute: ['CoreWorkflow::Custom::PendingTime'] + }, + }, + changeable: false, + created_by_id: 1, + updated_by_id: 1, +) +CoreWorkflow.create_if_not_exists( + name: 'base - admin sla options', + object: 'Sla', + condition_saved: { + 'custom.module': { + operator: 'match all modules', + value: [ + 'CoreWorkflow::Custom::AdminSla', + ], + }, + }, + perform: { + 'custom.module': { + execute: ['CoreWorkflow::Custom::AdminSla'] + }, + }, + changeable: false, + created_by_id: 1, + updated_by_id: 1, +) +CoreWorkflow.create_if_not_exists( + name: 'base - core workflow', + object: 'CoreWorkflow', + condition_saved: { + 'custom.module': { + operator: 'match all modules', + value: [ + 'CoreWorkflow::Custom::AdminCoreWorkflow', + ], + }, + }, + perform: { + 'custom.module': { + execute: ['CoreWorkflow::Custom::AdminCoreWorkflow'] + }, + }, + changeable: false, + created_by_id: 1, + updated_by_id: 1, +) diff --git a/db/seeds/object_manager_attributes.rb b/db/seeds/object_manager_attributes.rb index 703b32aa8..af7971427 100644 --- a/db/seeds/object_manager_attributes.rb +++ b/db/seeds/object_manager_attributes.rb @@ -230,18 +230,12 @@ ObjectManager::Attribute.add( display: 'Pending till', data_type: 'datetime', data_option: { - future: true, - past: false, - diff: 24, - null: true, - translate: true, - required_if: { - state_id: Ticket::State.by_category(:pending).pluck(:id), - }, - shown_if: { - state_id: Ticket::State.by_category(:pending).pluck(:id), - }, - permission: %w[ticket.agent], + future: true, + past: false, + diff: 24, + null: true, + translate: true, + permission: %w[ticket.agent], }, editable: false, active: true, @@ -542,6 +536,11 @@ ObjectManager::Attribute.add( null: false, }, }, + create: { + '-all-' => { + null: false, + }, + }, view: { '-all-' => { shown: true, @@ -589,6 +588,11 @@ ObjectManager::Attribute.add( null: false, }, }, + create: { + '-all-' => { + null: false, + }, + }, view: { '-all-' => { shown: true, @@ -636,6 +640,11 @@ ObjectManager::Attribute.add( null: true, }, }, + create: { + '-all-' => { + null: true, + }, + }, view: { '-all-' => { shown: true, @@ -671,6 +680,11 @@ ObjectManager::Attribute.add( null: true, }, }, + create: { + '-all-' => { + null: true, + }, + }, view: { '-all-' => { shown: true, @@ -706,6 +720,11 @@ ObjectManager::Attribute.add( null: true, }, }, + create: { + '-all-' => { + null: true, + }, + }, view: { '-all-' => { shown: true, @@ -741,6 +760,11 @@ ObjectManager::Attribute.add( null: true, }, }, + create: { + '-all-' => { + null: true, + }, + }, view: { '-all-' => { shown: true, @@ -776,6 +800,11 @@ ObjectManager::Attribute.add( null: true, }, }, + create: { + '-all-' => { + null: true, + }, + }, view: { '-all-' => { shown: true, @@ -816,6 +845,11 @@ ObjectManager::Attribute.add( null: true, }, }, + create: { + '-all-' => { + null: true, + }, + }, view: { '-all-' => { shown: true, @@ -851,6 +885,11 @@ ObjectManager::Attribute.add( null: true, }, }, + create: { + '-all-' => { + null: true, + }, + }, view: { '-all-' => { shown: true, @@ -885,6 +924,11 @@ ObjectManager::Attribute.add( null: true, }, }, + create: { + '-all-' => { + null: true, + }, + }, view: { '-all-' => { shown: true, @@ -920,6 +964,11 @@ ObjectManager::Attribute.add( null: true, }, }, + create: { + '-all-' => { + null: true, + }, + }, view: { '-all-' => { shown: true, @@ -955,6 +1004,11 @@ ObjectManager::Attribute.add( null: true, }, }, + create: { + '-all-' => { + null: true, + }, + }, view: { '-all-' => { shown: true, @@ -990,6 +1044,11 @@ ObjectManager::Attribute.add( null: true, }, }, + create: { + '-all-' => { + null: true, + }, + }, view: { '-all-' => { shown: true, @@ -1025,6 +1084,11 @@ ObjectManager::Attribute.add( null: true, }, }, + create: { + '-all-' => { + null: true, + }, + }, view: { '-all-' => { shown: true, @@ -1065,6 +1129,11 @@ ObjectManager::Attribute.add( null: true, }, }, + create: { + '-all-' => { + null: true, + }, + }, view: {} }, to_create: false, @@ -1093,12 +1162,17 @@ ObjectManager::Attribute.add( editable: false, active: true, screens: { - edit: { + edit: { '-all-' => { null: true, }, }, - view: { + create: { + '-all-' => { + null: true, + }, + }, + view: { '-all-' => { shown: false, }, @@ -1137,6 +1211,11 @@ ObjectManager::Attribute.add( null: true, }, }, + create: { + '-all-' => { + null: true, + }, + }, view: { '-all-' => { shown: true, @@ -1176,6 +1255,11 @@ ObjectManager::Attribute.add( null: true, }, }, + create: { + '-all-' => { + null: true, + }, + }, view: { '-all-' => { shown: false, @@ -1210,6 +1294,11 @@ ObjectManager::Attribute.add( null: false, }, }, + create: { + '-all-' => { + null: false, + }, + }, view: { '-all-' => { shown: false, @@ -1237,12 +1326,17 @@ ObjectManager::Attribute.add( editable: false, active: true, screens: { - edit: { + edit: { '-all-' => { null: false, }, }, - view: { + create: { + '-all-' => { + null: false, + }, + }, + view: { '-all-' => { shown: true, }, @@ -1275,12 +1369,17 @@ ObjectManager::Attribute.add( editable: false, active: true, screens: { - edit: { + edit: { '-all-' => { null: false, }, }, - view: { + create: { + '-all-' => { + null: false, + }, + }, + view: { '-all-' => { shown: true, }, @@ -1313,12 +1412,17 @@ ObjectManager::Attribute.add( editable: false, active: true, screens: { - edit: { + edit: { '-all-' => { null: false, }, }, - view: { + create: { + '-all-' => { + null: false, + }, + }, + view: { '-all-' => { shown: true, }, @@ -1345,12 +1449,17 @@ ObjectManager::Attribute.add( editable: false, active: true, screens: { - edit: { + edit: { '-all-' => { null: true, }, }, - view: { + create: { + '-all-' => { + null: true, + }, + }, + view: { '-all-' => { shown: true, }, @@ -1377,12 +1486,17 @@ ObjectManager::Attribute.add( editable: false, active: true, screens: { - edit: { + edit: { '-all-' => { null: true, }, }, - view: { + create: { + '-all-' => { + null: true, + }, + }, + view: { '-all-' => { shown: true, }, @@ -1408,12 +1522,17 @@ ObjectManager::Attribute.add( editable: false, active: true, screens: { - edit: { + edit: { '-all-' => { null: false, }, }, - view: { + create: { + '-all-' => { + null: false, + }, + }, + view: { '-all-' => { shown: false, }, diff --git a/db/seeds/settings.rb b/db/seeds/settings.rb index de8e54299..f54d49343 100644 --- a/db/seeds/settings.rb +++ b/db/seeds/settings.rb @@ -621,6 +621,32 @@ Setting.create_if_not_exists( }, frontend: true ) +Setting.create_if_not_exists( + title: 'Core Workflow Ajax Mode', + name: 'core_workflow_ajax_mode', + area: 'System::UI', + description: 'Defines if the core workflow communication should run over ajax instead of websockets.', + options: { + form: [ + { + display: '', + null: true, + name: 'core_workflow_ajax_mode', + tag: 'boolean', + options: { + true => 'yes', + false => 'no', + }, + }, + ], + }, + state: false, + preferences: { + prio: 3, + permission: ['admin.system'], + }, + frontend: true +) Setting.create_if_not_exists( title: 'User Organization Selector - email', name: 'ui_user_organization_selector_with_email', diff --git a/lib/import/otrs/state_factory.rb b/lib/import/otrs/state_factory.rb index 5c1b2494b..170ab60b0 100644 --- a/lib/import/otrs/state_factory.rb +++ b/lib/import/otrs/state_factory.rb @@ -61,7 +61,6 @@ module Import def update_ticket_attributes update_ticket_state - update_ticket_pending_time reseed_dependent_objects end @@ -95,22 +94,6 @@ module Import update_ticket_attribute(ticket_state_id) end - def update_ticket_pending_time - pending_state_ids = ::Ticket::State.where( - state_type_id: ::Ticket::StateType.where(name: ['pending reminder', 'pending action']) - ).pluck(:id) - - ticket_pending_time = ::ObjectManager::Attribute.get( - object: 'Ticket', - name: 'pending_time', - ) - - ticket_pending_time[:data_option][:required_if][:state_id] = pending_state_ids - ticket_pending_time[:data_option][:required_if][:state_id] = pending_state_ids - - update_ticket_attribute(ticket_pending_time) - end - def update_ticket_attribute(attribute) ::ObjectManager::Attribute.add( object_lookup_id: attribute[:object_lookup_id], diff --git a/lib/session_helper/collection_base.rb b/lib/session_helper/collection_base.rb index c55224354..a52b4a9c3 100644 --- a/lib/session_helper/collection_base.rb +++ b/lib/session_helper/collection_base.rb @@ -46,6 +46,10 @@ module SessionHelper::CollectionBase end end + if user.permissions?(['admin.core_workflow']) + collections['CoreWorkflowCustomModule'] = CoreWorkflow::Custom.list.map { |m| { name: m } } + end + [collections, assets] end end diff --git a/lib/sessions/event/core_workflow.rb b/lib/sessions/event/core_workflow.rb new file mode 100644 index 000000000..2d7610c48 --- /dev/null +++ b/lib/sessions/event/core_workflow.rb @@ -0,0 +1,13 @@ +# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ + +class Sessions::Event::CoreWorkflow < Sessions::Event::Base + database_connection_required + + def run + { + event: 'core_workflow', + data: CoreWorkflow.perform(payload: @payload, user: current_user) + } + end + +end diff --git a/lib/websocket_server.rb b/lib/websocket_server.rb index 50e7a8958..48a2b3822 100644 --- a/lib/websocket_server.rb +++ b/lib/websocket_server.rb @@ -114,6 +114,8 @@ class WebsocketServer else log 'error', "unknown message '#{data.inspect}'", client_id end + ensure + ActiveSupport::CurrentAttributes.clear_all end def self.websocket_send(client_id, data) diff --git a/public/assets/tests/form.js b/public/assets/tests/form.js index c8e6c0767..3eb0468d9 100644 --- a/public/assets/tests/form.js +++ b/public/assets/tests/form.js @@ -575,6 +575,7 @@ test("form dependend fields check", function() { var test_params = { input1: "", input2: "some used default", + input3: "some used default", select1: "false", select2: "false", selectmulti2: [ "true", "false" ], @@ -603,12 +604,13 @@ test("form dependend fields check", function() { params = App.ControllerForm.params(el) test_params = { input1: "", + input2: "some used default", input3: "some used default", select1: "true", select2: "false", selectmulti2: [ "true", "false" ], selectmultioption1: "false", - datetime1: null, + datetime1: '2015-01-11T12:40:00.000Z', datetime2: null, datetime3: '2015-01-11T12:40:00.000Z', datetime4: null, @@ -1002,72 +1004,6 @@ test("form selector", function() { }); -test("form required_if + shown_if", function() { - $('#forms').append('

form required_if + shown_if

') - var el = $('#form8') - var defaults = { - input2: 'some name66', - input3: 'some name77', - input4: 'some name88', - } - new App.ControllerForm({ - el: el, - model: { - configure_attributes: [ - { name: 'input1', display: 'Input1', tag: 'input', type: 'text', limit: 100, null: true, default: 'some not used default33' }, - { name: 'input2', display: 'Input2', tag: 'input', type: 'text', limit: 100, null: true, default: 'some used default', required_if: { active: true }, shown_if: { active: true } }, - { name: 'input3', display: 'Input3', tag: 'input', type: 'text', limit: 100, null: true, default: 'some used default', required_if: { active: [true,false] }, shown_if: { active: [true,false] } }, - { name: 'input4', display: 'Input4', tag: 'input', type: 'text', limit: 100, null: true, default: 'some used default', required_if: { active: [55,66] }, shown_if: { active: [55,66] } }, - { name: 'active', display: 'Active', tag: 'active', 'default': true }, - ], - }, - params: defaults, - }); - test_params = { - input1: "some not used default33", - input2: "some name66", - input3: "some name77", - active: true, - }; - params = App.ControllerForm.params(el) - deepEqual(params, test_params, 'form param check via $("#form")') - equal(el.find('[name="input2"]').attr('required'), 'required', 'check required attribute of input2 ') - equal(el.find('[name="input2"]').is(":visible"), true, 'check visible attribute of input2 ') - equal(el.find('[name="input3"]').attr('required'), 'required', 'check required attribute of input3 ') - equal(el.find('[name="input3"]').is(":visible"), true, 'check visible attribute of input3 ') - equal(el.find('[name="input4"]').is(":visible"), false, 'check visible attribute of input4 ') - - - el.find('[name="active"]').val('false').trigger('change') - test_params = { - input1: "some not used default33", - active: false, - }; - params = App.ControllerForm.params(el) - deepEqual(params, test_params, 'form param check via $("#form")') - equal(el.find('[name="input2"]').attr('required'), undefined, 'check required attribute of input2') - equal(el.find('[name="input2"]').is(":visible"), false, 'check visible attribute of input2') - equal(el.find('[name="input3"]').is(":visible"), false, 'check visible attribute of input3') - equal(el.find('[name="input4"]').is(":visible"), false, 'check visible attribute of input4') - - - el.find('[name="active"]').val('true').trigger('change') - test_params = { - input1: "some not used default33", - input2: "some name66", - input3: "some name77", - active: true, - }; - params = App.ControllerForm.params(el) - deepEqual(params, test_params, 'form param check via $("#form")') - equal(el.find('[name="input2"]').attr('required'), 'required', 'check required attribute of input2') - equal(el.find('[name="input2"]').is(":visible"), true, 'check visible attribute of input2') - equal(el.find('[name="input3"]').attr('required'), 'required', 'check required attribute of input3') - equal(el.find('[name="input3"]').is(":visible"), true, 'check visible attribute of input3') - equal(el.find('[name="input4"]').is(":visible"), false, 'check visible attribute of input4') - -}); - test("form params check", function() { $('#forms').append('

form params check

') diff --git a/public/assets/tests/form_core_workflow.js b/public/assets/tests/form_core_workflow.js new file mode 100644 index 000000000..acffeac13 --- /dev/null +++ b/public/assets/tests/form_core_workflow.js @@ -0,0 +1,71 @@ +test("core_workflow_condition", function(assert) { + var form = $('#forms') + + var el = $('
').attr('id', 'form1') + el.appendTo(form) + + form = new App.ControllerForm({ + el: el, + model: { + configure_attributes: [ + { name: 'condition_selected', display: 'Selected conditions', tag: 'core_workflow_condition', null: true, preview: false }, + ] + }, + autofocus: true + }); + + equal(el.find('.js-remove.is-disabled').length, 1, 'find disabled button') + el.find('.js-add').click() + equal(el.find('.js-remove.is-disabled').length, 0, 'find no disabled button after add') + el.find('.js-remove').click() + equal(el.find('.js-remove.is-disabled').length, 1, 'find disabled button after remove') + equal(typeof(App.ControllerForm.params(el).condition_selected), 'object', 'empty element results in a hash') + equal(_.isEmpty(App.ControllerForm.params(el).condition_selected), true, 'empty element results are empty') + + el.find('.js-add').click() + el.find("option[value='ticket.owner_id']").prop('selected', true) + equal(el.find('.js-preCondition').length, 0, 'pre condition not available') +}); + +test("core_workflow_perform", function(assert) { + var form = $('#forms') + + var el = $('
').attr('id', 'form1') + el.appendTo(form) + + form = new App.ControllerForm({ + el: el, + model: { + configure_attributes: [ + { name: 'perform', display: 'Action', tag: 'core_workflow_perform', null: true, preview: false }, + ] + }, + autofocus: true + }); + + equal(el.find('.js-remove.is-disabled').length, 1, 'find disabled button') + el.find('.js-add').click() + equal(el.find('.js-remove.is-disabled').length, 0, 'find no disabled button after add') + el.find('.js-remove').click() + equal(el.find('.js-remove.is-disabled').length, 1, 'find disabled button after remove') + equal(typeof(App.ControllerForm.params(el).perform), 'object', 'empty element results in a hash') + equal(_.isEmpty(App.ControllerForm.params(el).perform), true, 'empty element results are empty') + + el.find('.js-add').click() + el.find("option[value='ticket.owner_id']").prop('selected', true) + equal(el.find('.js-preCondition').length, 0, 'pre condition not available') + + el.find('.js-add:last').click() + el.find("option[value='ticket.group_id']:last").prop('selected', true) + el.find('.js-add:last').click() + el.find("option[value='ticket.group_id']:last").prop('selected', true) + el.find('.js-add:last').click() + el.find("option[value='ticket.group_id']:last").prop('selected', true) + + attribute_count = {} + el.find('.js-attributeSelector select').each(function() { + attribute_count[$(this).val()] ||= 0 + attribute_count[$(this).val()] += 1 + }) + equal(attribute_count['ticket.group_id'], 3, 'hasDuplicateSelector - its possible to select an attribute multiple times') +}); diff --git a/public/assets/tests/form_sla_times.js b/public/assets/tests/form_sla_times.js index edc5d4c3c..0c1518ed0 100644 --- a/public/assets/tests/form_sla_times.js +++ b/public/assets/tests/form_sla_times.js @@ -59,42 +59,6 @@ test("form SLA times highlights and shows settings accordingly", function(assert equal(secondRow.find('input[data-name=update_time]').val(), '04:00') }) -test("form SLA times highlights errors when submitting empty active row", function(assert) { - $('#forms').append('

SLA error handling

') - - var el = $('#form4') - - var item = new App.Sla() - item.id = '123' - item.update_time = 240 - - new App.ControllerForm({ - el: el, - model: item.constructor, - params: item - }); - - var row = el.find('.sla_times tbody > tr:nth-child(2)') - var input = row.find('input[data-name=update_time]') - input.val('').trigger('blur') - - item.load(App.ControllerForm.params(el)) - - App.ControllerForm.validate({form: el, errors: item.validate()}) - - equal(input.css('border-top-color'), 'rgb(255, 0, 0)', 'highlighted as error') // checking border-color fails on Firefox - - var anotherRow = el.find('.sla_times tbody > tr:nth-child(3)') - var anotherInput = anotherRow.find('input[data-name=update_time]') - - notEqual(anotherInput.css('border-color'), 'rgb(255, 0, 0)', 'not highlighted as error') - - row.find('td:nth-child(2)').click() - notOk(row.hasClass('is-active'), 'deactivates class by clicking on name cell)') - - notEqual(input.css('border-color'), 'rgb(255, 0, 0)', 'error cleared by deactivating') -}) - test("form SLA times clears field instead of 00:00", function(assert) { $('#forms').append('

SLA placeholder instead of 00:00

') diff --git a/public/assets/tests/model.js b/public/assets/tests/model.js index 22251f219..e823cdb1c 100644 --- a/public/assets/tests/model.js +++ b/public/assets/tests/model.js @@ -1,206 +1,5 @@ window.onload = function() { -// model -test( "model basic tests", function() { - - // define model - var configure_attributes_org = _.clone( App.Ticket.configure_attributes ) - var attribute1 = { - name: 'test1', display: 'Test 1', tag: 'input', type: 'text', limit: 200, 'null': false - }; - App.Ticket.configure_attributes.push( attribute1 ) - var attribute2 = { - name: 'test2', display: 'Test 2', tag: 'input', type: 'text', limit: 200, 'null': true - }; - App.Ticket.configure_attributes.push( attribute2 ) - var attribute3 = { - name: 'pending_time1', display: 'Pending till1', tag: 'input', type: 'text', limit: 200, 'null': false, required_if: { state_id: [3] }, - }; - App.Ticket.configure_attributes.push( attribute3 ) - var attribute4 = { - name: 'pending_time2', display: 'Pending till2', tag: 'input', type: 'text', limit: 200, 'null': true, required_if: { state_id: [3] }, - }; - App.Ticket.configure_attributes.push( attribute4 ) - - // check validation - - console.log('TEST 1') - var ticket = new App.Ticket() - ticket.load({title: 'some title'}) - - var error = ticket.validate() - ok( error['group_id'], 'group_id is required') - ok( !error['title'], 'title is required') - ok( error['state_id'], 'state_id is required') - ok( error['test1'], 'test1 is required') - ok( !error['test2'], 'test2 is not required') - ok( !error['pending_time1'], 'pending_time1 is not required') - ok( !error['pending_time2'], 'pending_time2 is not required') - - - console.log('TEST 2') - ticket.title = 'some new title' - ticket.state_id = [2,3] - ticket.test2 = 123 - error = ticket.validate() - ok( error['group_id'], 'group_id is required') - ok( !error['title'], 'title exists') - ok( !error['state_id'], 'state_id is') - ok( error['test1'], 'test1 is required') - ok( !error['test2'], 'test2 is not required') - ok( error['pending_time1'], 'pending_time1 is required') - ok( error['pending_time2'], 'pending_time2 is required') - - console.log('TEST 3') - ticket.title = 'some new title' - ticket.state_id = [2,1] - ticket.test2 = 123 - error = ticket.validate() - ok( error['group_id'], 'group_id is required') - ok( !error['title'], 'title exists') - ok( !error['state_id'], 'state_id is') - ok( error['test1'], 'test1 is required') - ok( !error['test2'], 'test2 is not required') - ok( !error['pending_time1'], 'pending_time1 is required') - ok( !error['pending_time2'], 'pending_time2 is required') - - console.log('TEST 4') - ticket.title = 'some new title' - ticket.state_id = [2,3] - ticket.test2 = 123 - error = ticket.validate() - ok( error['group_id'], 'group_id is required') - ok( !error['title'], 'title exists') - ok( !error['state_id'], 'state_id is') - ok( error['test1'], 'test1 is required') - ok( !error['test2'], 'test2 is not required') - ok( error['pending_time1'], 'pending_time1 is required') - ok( error['pending_time2'], 'pending_time2 is required') - - console.log('TEST 5') - ticket.title = 'some new title' - ticket.state_id = [2,3] - ticket.test2 = 123 - ticket.pending_time1 = '2014-10-10 09:00' - error = ticket.validate() - ok( error['group_id'], 'group_id is required') - ok( !error['title'], 'title exists') - ok( !error['state_id'], 'state_id is') - ok( error['test1'], 'test1 is required') - ok( !error['test2'], 'test2 is not required') - ok( !error['pending_time1'], 'pending_time1 is required') - ok( error['pending_time2'], 'pending_time2 is required') - - - // define model with screen - App.Ticket.configure_attributes = configure_attributes_org - var attribute1 = { - name: 'test1', display: 'Test 1', tag: 'input', type: 'text', limit: 200, 'null': false, screen: { some_screen: { required_if: { state_id: [3] } } }, - }; - App.Ticket.configure_attributes.push( attribute1 ) - var attribute2 = { - name: 'test2', display: 'Test 2', tag: 'input', type: 'text', limit: 200, 'null': true, screen: { some_screen: { required_if: { state_id: [3] } } }, - }; - App.Ticket.configure_attributes.push( attribute2 ) - var attribute3 = { - name: 'group_id', display: 'Group', tag: 'select', multiple: false, null: false, relation: 'Group', screen: { some_screen: { null: false } }, - }; - App.Ticket.configure_attributes.push( attribute3 ) - var attribute4 = { - name: 'owner_id', display: 'Owner', tag: 'select', multiple: false, null: false, relation: 'User', screen: { some_screen: { null: false } }, - }; - App.Ticket.configure_attributes.push( attribute4 ) - var attribute5 = { - name: 'state_id', display: 'State', tag: 'select', multiple: false, null: false, relation: 'TicketState', screen: { some_screen: { null: false } }, - }; - App.Ticket.configure_attributes.push( attribute5 ) - - // check validation with screen - console.log('TEST 6') - ticket = new App.Ticket() - ticket.load({title: 'some title'}) - - error = ticket.validate() - ok( error['group_id'], 'group_id is required') - ok( !error['title'], 'title is required') - ok( error['state_id'], 'state_id is required') - ok( error['test1'], 'test1 is required') - ok( !error['test2'], 'test2 is not required') - - console.log('TEST 7') - ticket.state_id = 3 - error = ticket.validate() - ok( error['group_id'], 'group_id is required') - ok( !error['title'], 'title is required') - ok( !error['state_id'], 'state_id is required') - ok( error['test1'], 'test1 is required') - ok( !error['test2'], 'test2 is not required') - - console.log('TEST 8') - ticket.state_id = 2 - error = ticket.validate() - ok( error['group_id'], 'group_id is required') - ok( !error['title'], 'title is required') - ok( !error['state_id'], 'state_id is required') - ok( error['test1'], 'test1 is required') - ok( !error['test2'], 'test2 is not required') - - console.log('TEST 9') - ticket.state_id = undefined - error = ticket.validate({screen: 'some_screen'}) - ok( error['group_id'], 'group_id is required') - ok( !error['title'], 'title is required') - ok( error['state_id'], 'state_id is required') - ok( !error['test1'], 'test1 is required') - ok( !error['test2'], 'test2 is required') - - console.log('TEST 10') - ticket.state_id = 2 - error = ticket.validate({screen: 'some_screen'}) - ok( error['group_id'], 'group_id is required') - ok( !error['title'], 'title is required') - ok( !error['state_id'], 'state_id is required') - ok( !error['test1'], 'test1 is required') - ok( !error['test2'], 'test2 is not required') - - console.log('TEST 11') - ticket.state_id = 3 - error = ticket.validate({screen: 'some_screen'}) - ok( error['group_id'], 'group_id is required') - ok( !error['title'], 'title is required') - ok( !error['state_id'], 'state_id is required') - ok( error['test1'], 'test1 is required') - ok( error['test2'], 'test2 is required') - - console.log('TEST 12') - ticket.state_id = 2 - error = ticket.validate() - ok( error['group_id'], 'group_id is required') - ok( !error['title'], 'title is required') - ok( !error['state_id'], 'state_id is required') - ok( error['test1'], 'test1 is required') - ok( !error['test2'], 'test2 is not required') - - console.log('TEST 13') - ticket.state_id = 3 - error = ticket.validate() - ok( error['group_id'], 'group_id is required') - ok( !error['title'], 'title is required') - ok( !error['state_id'], 'state_id is required') - ok( error['test1'], 'test1 is required') - ok( !error['test2'], 'test2 is required') - - console.log('TEST 14') - ticket.state_id = 2 - error = ticket.validate() - ok( error['group_id'], 'group_id is required') - ok( !error['title'], 'title is required') - ok( !error['state_id'], 'state_id is required') - ok( error['test1'], 'test1 is required') - ok( !error['test2'], 'test2 is not required') - -}); - // search test( "model search tests", function() { diff --git a/spec/factories/core_workflow.rb b/spec/factories/core_workflow.rb new file mode 100644 index 000000000..02c327f7d --- /dev/null +++ b/spec/factories/core_workflow.rb @@ -0,0 +1,10 @@ +# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ + +FactoryBot.define do + factory :core_workflow do + sequence(:name) { |n| "test - workflow #{n}" } + changeable { false } + created_by_id { 1 } + updated_by_id { 1 } + end +end diff --git a/spec/lib/import/otrs/state_factory_spec.rb b/spec/lib/import/otrs/state_factory_spec.rb index 138142333..e3b872ece 100644 --- a/spec/lib/import/otrs/state_factory_spec.rb +++ b/spec/lib/import/otrs/state_factory_spec.rb @@ -43,8 +43,6 @@ RSpec.describe Import::OTRS::StateFactory do ticket_state_id.data_option }.and change { ticket_state_id.screens - }.and change { - ticket_pending_time.data_option } end @@ -159,23 +157,5 @@ RSpec.describe Import::OTRS::StateFactory do trigger.condition['ticket.state_id'][:value] } end - - it 'updates ObjectManager::Attributes' do - - attribute = ObjectManager::Attribute.get( - object: 'Ticket', - name: 'pending_time', - ) - expect do - described_class.import(state_backend_param) - - attribute = ObjectManager::Attribute.get( - object: 'Ticket', - name: 'pending_time', - ) - end.to change { - attribute.data_option[:required_if][:state_id] - } - end end end diff --git a/spec/models/concerns/checks_core_workflow_examples.rb b/spec/models/concerns/checks_core_workflow_examples.rb new file mode 100644 index 000000000..71e77d041 --- /dev/null +++ b/spec/models/concerns/checks_core_workflow_examples.rb @@ -0,0 +1,94 @@ +# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ + +RSpec.shared_examples 'ChecksCoreWorkflow' do + + let(:agent_group) { create(:group) } + let(:agent) { create(:agent, groups: [agent_group]) } + + before do + UserInfo.current_user_id = agent.id + end + + context 'when pending time on open ticket' do + subject(:ticket) { create(:ticket, group: agent_group, screen: 'create_middle', state: Ticket::State.find_by(name: 'open'), pending_time: Time.zone.now + 5.days) } + + before { subject } + + it 'checks if the pending time got removed' do + expect(ticket.pending_time).to be nil + end + end + + context 'when creation of closed tickets are only allowed by type set' do + subject(:ticket) { create(:ticket, group: agent_group, screen: 'create_middle', state: Ticket::State.find_by(name: 'open'), pending_time: Time.zone.now + 5.days) } + + before do + create(:core_workflow, + object: 'Ticket', + perform: { + 'ticket.state_id': { + operator: 'set_fixed_to', + set_fixed_to: [ Ticket::State.find_by(name: 'closed').id.to_s ] + }, + }) + end + + it 'checks that workflow blocked creation' do + expect { ticket }.to raise_error(Exceptions::UnprocessableEntity, "Invalid value '#{Ticket::State.find_by(name: 'open').id}' for field 'state_id'!") + end + end + + context 'when creation of closed tickets are only allowed by type remove' do + subject(:ticket) { create(:ticket, group: agent_group, screen: 'create_middle', state: Ticket::State.find_by(name: 'open'), pending_time: Time.zone.now + 5.days) } + + before do + create(:core_workflow, + object: 'Ticket', + perform: { + 'ticket.state_id': { + operator: 'remove_option', + remove_option: [ Ticket::State.find_by(name: 'open').id.to_s ] + }, + }) + end + + it 'checks that workflow blocked creation' do + expect { ticket }.to raise_error(Exceptions::UnprocessableEntity, "Invalid value '#{Ticket::State.find_by(name: 'open').id}' for field 'state_id'!") + end + end + + context 'when creation of closed tickets are only allowed by type add' do + subject(:ticket) { create(:ticket, group: agent_group, screen: 'create_middle', state: Ticket::State.find_by(name: 'open'), pending_time: Time.zone.now + 5.days) } + + before do + create(:core_workflow, + object: 'Ticket', + perform: { + 'ticket.state_id': { + operator: 'remove_option', + remove_option: [ Ticket::State.find_by(name: 'open').id.to_s ] + }, + }) + create(:core_workflow, + object: 'Ticket', + perform: { + 'ticket.state_id': { + operator: 'add_option', + add_option: [ Ticket::State.find_by(name: 'open').id.to_s ] + }, + }) + end + + it 'checks ticket creation success' do + expect { ticket }.not_to raise_error + end + end + + context 'when pending time on pending ticket' do + subject(:ticket) { create(:ticket, group: agent_group, screen: 'create_middle', state: Ticket::State.find_by(name: 'pending reminder')) } + + it 'checks that the pending time is mandatory' do + expect { ticket }.to raise_error(Exceptions::UnprocessableEntity, "Missing required value for field 'pending_time'!") + end + end +end diff --git a/spec/models/core_workflow/attributes_spec.rb b/spec/models/core_workflow/attributes_spec.rb new file mode 100644 index 000000000..3e60eb7a9 --- /dev/null +++ b/spec/models/core_workflow/attributes_spec.rb @@ -0,0 +1,65 @@ +# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ + +require 'rails_helper' + +RSpec.describe CoreWorkflow::Attributes, type: :model do + let!(:ticket) { create(:ticket, state: Ticket::State.find_by(name: 'pending reminder'), pending_time: Time.zone.now + 5.days) } + let!(:base_payload) do + { + 'event' => 'core_workflow', + 'request_id' => 'default', + 'class_name' => 'Ticket', + 'screen' => 'create_middle', + 'params' => { + 'id' => ticket.id, + 'state_id' => Ticket::State.find_by(name: 'open').id, + }, + } + end + let(:payload) { base_payload } + let!(:action_user) { create(:agent, groups: [ticket.group]) } + let(:result) { described_class.new(result_object: CoreWorkflow::Result.new(payload: payload, user: action_user)) } + + describe '#payload_class' do + it 'returns class' do + expect(result.payload_class).to eq(Ticket) + end + end + + describe '#selected_only' do + it 'returns state open' do + expect(result.selected_only.state.name).to eq('open') + end + end + + describe '#selected' do + it 'returns state open' do + expect(result.selected.state.name).to eq('open') + end + end + + describe '#saved_only' do + it 'returns state pending reminder' do + expect(result.saved_only.state.name).to eq('pending reminder') + end + end + + describe '#saved' do + it 'returns state pending reminder' do + expect(result.saved.state.name).to eq('pending reminder') + end + end + + describe '#mandatory_default' do + it 'priority should be mandatory by default' do + expect(result.mandatory_default['priority_id']).to be true + end + end + + describe '#shown_default' do + it 'priority should be shown by default' do + expect(result.shown_default['priority_id']).to eq('show') + end + end + +end diff --git a/spec/models/core_workflow_spec.rb b/spec/models/core_workflow_spec.rb new file mode 100644 index 000000000..ffd213464 --- /dev/null +++ b/spec/models/core_workflow_spec.rb @@ -0,0 +1,1520 @@ +# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ + +require 'rails_helper' + +RSpec.describe CoreWorkflow, type: :model do + let(:group) { create(:group) } + let!(:ticket) { create(:ticket, state: Ticket::State.find_by(name: 'pending reminder'), pending_time: Time.zone.now + 5.days, group: group) } + let!(:base_payload) do + { + 'event' => 'core_workflow', + 'request_id' => 'default', + 'class_name' => 'Ticket', + 'screen' => 'create_middle', + 'params' => {}, + } + end + let(:payload) { base_payload } + let!(:action_user) { create(:agent, groups: [ticket.group]) } + let(:result) { described_class.perform(payload: payload, user: action_user) } + + describe '.perform - Default - Group' do + let!(:group_change) { create(:group) } + let!(:group_create) { create(:group) } + + describe 'for agent with full permissions on screen create_middle' do + let(:action_user) { create(:agent) } + + before do + action_user.group_names_access_map = { + group_create.name => ['full'], + group_change.name => ['change'], + } + end + + it 'does show group_create for agent with all permissions' do + expect(result[:restrict_values]['group_id']).to include(group_create.id.to_s) + end + + it 'does not show group_change for agent with all permissions' do + expect(result[:restrict_values]['group_id']).not_to include(group_change.id.to_s) + end + end + + describe 'for agent with full permissions on screen edit' do + let(:payload) do + base_payload.merge('screen' => 'edit') + end + let(:action_user) { create(:agent) } + + before do + action_user.group_names_access_map = { + group_create.name => ['full'], + group_change.name => ['change'], + } + end + + it 'does show group_create for agent with all permissions' do + expect(result[:restrict_values]['group_id']).to include(group_create.id.to_s) + end + + it 'does show group_change for agent with all permissions' do + expect(result[:restrict_values]['group_id']).to include(group_change.id.to_s) + end + end + + describe 'for agent with change permissions on screen create_middle' do + let(:action_user) { create(:agent) } + + before do + action_user.group_names_access_map = { + group_create.name => ['change'], + group_change.name => ['change'], + } + end + + it 'does not show group_create for agent with change permissions' do + expect(result[:restrict_values]['group_id']).not_to include(group_create.id.to_s) + end + + it 'does not show group_change for agent with change permissions' do + expect(result[:restrict_values]['group_id']).not_to include(group_change.id.to_s) + end + end + + describe 'for agent with change permissions on screen edit' do + let(:payload) do + base_payload.merge('screen' => 'edit') + end + let(:action_user) { create(:agent) } + + before do + action_user.group_names_access_map = { + group_create.name => ['change'], + group_change.name => ['change'], + } + end + + it 'does show group_create for agent with change permissions' do + expect(result[:restrict_values]['group_id']).to include(group_create.id.to_s) + end + + it 'does show group_change for agent with change permissions' do + expect(result[:restrict_values]['group_id']).to include(group_change.id.to_s) + end + end + + describe 'for customer on screen create_middle' do + let(:action_user) { create(:customer) } + + it 'does show group_create for customer' do + expect(result[:restrict_values]['group_id']).to include(group_create.id.to_s) + end + + it 'does show group_change for customer' do + expect(result[:restrict_values]['group_id']).to include(group_change.id.to_s) + end + end + + describe 'for customer on screen edit' do + let(:payload) do + base_payload.merge('screen' => 'edit') + end + let(:action_user) { create(:customer) } + + it 'does show group_create for customer' do + expect(result[:restrict_values]['group_id']).to include(group_create.id.to_s) + end + + it 'does show group_change for customer' do + expect(result[:restrict_values]['group_id']).to include(group_change.id.to_s) + end + end + end + + describe '.perform - Default - Owner' do + before do + another_group = create(:group) + + action_user.group_names_access_map = { + ticket.group.name => ['full'], + another_group.name => ['full'], + } + end + + it 'does not show any owners for no group' do + expect(result[:restrict_values]['owner_id']).to eq(['']) + end + + describe 'on group' do + let(:payload) do + base_payload.merge('params' => { 'group_id' => ticket.group.id }) + end + + it 'does show ticket agent' do + expect(result[:restrict_values]['owner_id']).to eq(['', action_user.id.to_s]) + end + end + + describe 'on group save' do + let(:payload) do + base_payload.merge('request_id' => 'ChecksCoreWorkflow.validate_workflows', 'params' => { 'group_id' => ticket.group.id }) + end + + it 'does show ticket agent and system user' do + expect(result[:restrict_values]['owner_id']).to eq(['', '1', action_user.id.to_s]) + end + end + end + + describe '.perform - Default - Bulk Owner' do + let(:payload) do + base_payload.merge('screen' => 'overview_bulk') + end + + it 'does not show any owners for no group' do + expect(result[:restrict_values]['owner_id']).to eq(['']) + end + + describe 'on ticket ids' do + let(:payload) do + base_payload.merge('screen' => 'overview_bulk', 'params' => { 'ticket_ids' => ticket.id.to_s }) + end + + it 'does show ticket agent' do + expect(result[:restrict_values]['owner_id']).to eq(['', action_user.id.to_s]) + end + end + + describe 'on ticket ids with no group overlap' do + let(:ticket2) { create(:ticket) } + let(:payload) do + base_payload.merge('screen' => 'overview_bulk', 'params' => { 'ticket_ids' => "#{ticket.id},#{ticket2.id}" }) + end + + it 'does not show ticket agent' do + expect(result[:restrict_values]['owner_id']).to eq(['']) + end + end + + describe 'on ticket ids with group overlap' do + let(:ticket2) { create(:ticket, group: ticket.group) } + let(:payload) do + base_payload.merge('screen' => 'overview_bulk', 'params' => { 'ticket_ids' => "#{ticket.id},#{ticket2.id}" }) + end + + it 'does show ticket agent' do + expect(result[:restrict_values]['owner_id']).to eq(['', action_user.id.to_s]) + end + end + end + + describe '.perform - Default - State' do + it 'does show state type new for create_middle' do + expect(result[:restrict_values]['state_id']).to include(Ticket::State.find_by(name: 'new').id.to_s) + end + + describe 'on edit' do + let(:payload) do + base_payload.merge('screen' => 'edit') + end + + it 'does not show state type new' do + expect(result[:restrict_values]['state_id']).not_to include(Ticket::State.find_by(name: 'new').id.to_s) + end + end + end + + describe '.perform - Default - Priority' do + let(:prio_invalid) { create(:ticket_priority, active: false) } + + it 'does show valid priority' do + expect(result[:restrict_values]['priority_id']).to include(Ticket::Priority.find_by(name: '3 high').id.to_s) + end + + it 'does not show invalid priority' do + expect(result[:restrict_values]['priority_id']).not_to include(prio_invalid.id.to_s) + end + end + + describe '.perform - Custom - Pending Time' do + it 'does not show pending time for non pending state' do + expect(result[:visibility]['pending_time']).to eq('remove') + end + + describe 'for ticket id with no state change' do + let(:payload) do + base_payload.merge('params' => { + 'id' => ticket.id, + }) + end + + it 'does show pending time for pending ticket' do + expect(result[:visibility]['pending_time']).to eq('show') + end + end + + describe 'for ticket id with state change' do + let(:payload) do + base_payload.merge('params' => { + 'id' => ticket.id, + 'state_id' => Ticket::State.find_by(name: 'open').id.to_s, + }) + end + + it 'does not show pending time for pending ticket' do + expect(result[:visibility]['pending_time']).to eq('remove') + end + end + end + + describe '.perform - Custom - Admin SLA' do + let(:payload) do + base_payload.merge( + 'screen' => 'edit', + 'class_name' => 'Sla', + ) + end + + it 'does set first_response_time_in_text optional' do + expect(result[:mandatory]['first_response_time_in_text']).to eq(false) + end + + it 'does set update_time_in_text optional' do + expect(result[:mandatory]['update_time_in_text']).to eq(false) + end + + it 'does set solution_time_in_text optional' do + expect(result[:mandatory]['solution_time_in_text']).to eq(false) + end + + describe 'on first_response_time_enabled' do + let(:payload) do + base_payload.merge( + 'screen' => 'edit', + 'class_name' => 'Sla', + 'params' => { 'first_response_time_enabled' => 'true' } + ) + end + + it 'does set first_response_time_in_text mandatory' do + expect(result[:mandatory]['first_response_time_in_text']).to eq(true) + end + + it 'does set update_time_in_text optional' do + expect(result[:mandatory]['update_time_in_text']).to eq(false) + end + + it 'does set solution_time_in_text optional' do + expect(result[:mandatory]['solution_time_in_text']).to eq(false) + end + end + + describe 'on update_time_enabled' do + let(:payload) do + base_payload.merge( + 'screen' => 'edit', + 'class_name' => 'Sla', + 'params' => { 'update_time_enabled' => 'true' } + ) + end + + it 'does set first_response_time_in_text optional' do + expect(result[:mandatory]['first_response_time_in_text']).to eq(false) + end + + it 'does set update_time_in_text mandatory' do + expect(result[:mandatory]['update_time_in_text']).to eq(true) + end + + it 'does set solution_time_in_text optional' do + expect(result[:mandatory]['solution_time_in_text']).to eq(false) + end + end + + describe 'on solution_time_enabled' do + let(:payload) do + base_payload.merge( + 'screen' => 'edit', + 'class_name' => 'Sla', + 'params' => { 'solution_time_enabled' => 'true' } + ) + end + + it 'does set first_response_time_in_text optional' do + expect(result[:mandatory]['first_response_time_in_text']).to eq(false) + end + + it 'does set update_time_in_text optional' do + expect(result[:mandatory]['update_time_in_text']).to eq(false) + end + + it 'does set solution_time_in_text mandatory' do + expect(result[:mandatory]['solution_time_in_text']).to eq(true) + end + end + end + + describe '.perform - Custom - Admin CoreWorkflow' do + let(:payload) do + base_payload.merge( + 'screen' => 'edit', + 'class_name' => 'CoreWorkflow', + ) + end + + it 'does not show screens for empty object' do + expect(result[:restrict_values]['preferences::screen']).to eq(['']) + end + + it 'does not show invalid objects' do + expect(result[:restrict_values]['object']).not_to include('CoreWorkflow') + end + + describe 'on object Ticket' do + let(:payload) do + base_payload.merge( + 'screen' => 'edit', + 'class_name' => 'CoreWorkflow', + 'params' => { 'object' => 'Ticket' }, + ) + end + + it 'does show screen create_middle' do + expect(result[:restrict_values]['preferences::screen']).to include('create_middle') + end + + it 'does show screen edit' do + expect(result[:restrict_values]['preferences::screen']).to include('edit') + end + end + + describe 'on saved object Ticket' do + let(:workflow) { create(:core_workflow, object: 'Ticket') } + let(:payload) do + base_payload.merge( + 'screen' => 'edit', + 'class_name' => 'CoreWorkflow', + 'params' => { 'id' => workflow.id }, + ) + end + + it 'does show screen create_middle' do + expect(result[:restrict_values]['preferences::screen']).to include('create_middle') + end + + it 'does show screen edit' do + expect(result[:restrict_values]['preferences::screen']).to include('edit') + end + end + end + + describe '.perform - Condition - owner_id not set' do + let!(:workflow) do + create(:core_workflow, + object: 'Ticket', + condition_selected: { + 'ticket.owner_id': { + operator: 'not_set', + }, + }) + end + + it 'does match' do + expect(result[:matched_workflows]).to include(workflow.id) + end + + describe 'for owner id 1' do + let(:payload) do + base_payload.merge( + 'params' => { 'owner_id' => '1' }, + ) + end + + it 'does match' do + expect(result[:matched_workflows]).to include(workflow.id) + end + end + end + + describe '.perform - Condition - session.role_ids' do + let!(:workflow) do + create(:core_workflow, + object: 'Ticket', + condition_selected: { + 'session.role_ids': { + operator: 'is', + value: [ Role.find_by(name: 'Agent').id.to_s ], + }, + }) + end + + it 'does match' do + expect(result[:matched_workflows]).to include(workflow.id) + end + + describe 'for customer' do + let!(:action_user) { create(:customer) } # rubocop:disable RSpec/LetSetup + + it 'does not match' do + expect(result[:matched_workflows]).not_to include(workflow.id) + end + end + end + + describe '.perform - Condition - session.group_ids_full' do + let!(:workflow) do + create(:core_workflow, + object: 'Ticket', + condition_selected: { + 'session.group_ids_full': { + operator: 'is', + value: [ ticket.group.id.to_s ], + }, + }) + end + + it 'does match' do + expect(result[:matched_workflows]).to include(workflow.id) + end + + describe 'for customer' do + let!(:action_user) { create(:customer) } # rubocop:disable RSpec/LetSetup + + it 'does not match' do + expect(result[:matched_workflows]).not_to include(workflow.id) + end + end + end + + describe '.perform - Condition - session.group_ids_change' do + let!(:workflow) do + create(:core_workflow, + object: 'Ticket', + condition_selected: { + 'session.group_ids_change': { + operator: 'is', + value: [ ticket.group.id.to_s ], + }, + }) + end + + it 'does match' do + expect(result[:matched_workflows]).to include(workflow.id) + end + + describe 'for customer' do + let!(:action_user) { create(:customer) } # rubocop:disable RSpec/LetSetup + + it 'does not match' do + expect(result[:matched_workflows]).not_to include(workflow.id) + end + end + end + + describe '.perform - Condition - session.group_ids_read' do + let!(:workflow) do + create(:core_workflow, + object: 'Ticket', + condition_selected: { + 'session.group_ids_read': { + operator: 'is', + value: [ ticket.group.id.to_s ], + }, + }) + end + + it 'does match' do + expect(result[:matched_workflows]).to include(workflow.id) + end + + describe 'for customer' do + let!(:action_user) { create(:customer) } # rubocop:disable RSpec/LetSetup + + it 'does not match' do + expect(result[:matched_workflows]).not_to include(workflow.id) + end + end + end + + describe '.perform - Condition - session.group_ids_overview' do + let!(:workflow) do + create(:core_workflow, + object: 'Ticket', + condition_selected: { + 'session.group_ids_overview': { + operator: 'is', + value: [ ticket.group.id.to_s ], + }, + }) + end + + it 'does match' do + expect(result[:matched_workflows]).to include(workflow.id) + end + + describe 'for customer' do + let!(:action_user) { create(:customer) } # rubocop:disable RSpec/LetSetup + + it 'does not match' do + expect(result[:matched_workflows]).not_to include(workflow.id) + end + end + end + + describe '.perform - Condition - session.group_ids_create' do + let!(:workflow) do + create(:core_workflow, + object: 'Ticket', + condition_selected: { + 'session.group_ids_create': { + operator: 'is', + value: [ ticket.group.id.to_s ], + }, + }) + end + + it 'does match' do + expect(result[:matched_workflows]).to include(workflow.id) + end + + describe 'for customer' do + let!(:action_user) { create(:customer) } # rubocop:disable RSpec/LetSetup + + it 'does not match' do + expect(result[:matched_workflows]).not_to include(workflow.id) + end + end + end + + describe '.perform - Condition - session.permission_ids' do + let!(:workflow) do + create(:core_workflow, + object: 'Ticket', + condition_selected: { + 'session.permission_ids': { + operator: 'is', + value: [ Permission.find_by(name: 'ticket.agent').id.to_s ], + }, + }) + end + + it 'does match' do + expect(result[:matched_workflows]).to include(workflow.id) + end + + describe 'for customer' do + let!(:action_user) { create(:customer) } # rubocop:disable RSpec/LetSetup + + it 'does not match' do + expect(result[:matched_workflows]).not_to include(workflow.id) + end + end + end + + describe '.perform - Condition - Regex match' do + let(:payload) do + base_payload.merge( + 'params' => { 'title' => 'workflow ticket' }, + ) + end + let!(:workflow) do + create(:core_workflow, + object: 'Ticket', + condition_selected: { + 'ticket.title': { + operator: 'regex match', + value: [ '^workflow' ], + }, + }) + end + + it 'does match' do + expect(result[:matched_workflows]).to include(workflow.id) + end + + describe 'for invalid regex' do + let!(:workflow) do + create(:core_workflow, + object: 'Ticket', + condition_selected: { + 'ticket.title': { + operator: 'regex match', + value: [ '^workfluw' ], + }, + }) + end + + it 'does not match' do + expect(result[:matched_workflows]).not_to include(workflow.id) + end + end + end + + describe '.perform - Condition - Regex mismatch' do + let(:payload) do + base_payload.merge( + 'params' => { 'title' => 'workflow ticket' }, + ) + end + let!(:workflow) do + create(:core_workflow, + object: 'Ticket', + condition_selected: { + 'ticket.title': { + operator: 'regex mismatch', + value: [ '^workfluw' ], + }, + }) + end + + it 'does match' do + expect(result[:matched_workflows]).to include(workflow.id) + end + + describe 'for invalid regex' do + let!(:workflow) do + create(:core_workflow, + object: 'Ticket', + condition_selected: { + 'ticket.title': { + operator: 'regex mismatch', + value: [ '^workflow' ], + }, + }) + end + + it 'does not match' do + expect(result[:matched_workflows]).not_to include(workflow.id) + end + end + end + + describe '.perform - Condition - Contains' do + let(:payload) do + base_payload.merge( + 'params' => { 'title' => 'workflow ticket' }, + ) + end + let!(:workflow) do + create(:core_workflow, + object: 'Ticket', + condition_selected: { + 'ticket.title': { + operator: 'contains', + value: [ 'workflow ticket', 'workflaw ticket' ], + }, + }) + end + + it 'does match' do + expect(result[:matched_workflows]).to include(workflow.id) + end + + describe 'for invalid value' do + let!(:workflow) do + create(:core_workflow, + object: 'Ticket', + condition_selected: { + 'ticket.title': { + operator: 'contains', + value: [ 'workfluw ticket', 'workflaw ticket' ], + }, + }) + end + + it 'does not match' do + expect(result[:matched_workflows]).not_to include(workflow.id) + end + end + end + + describe '.perform - Condition - Contains not' do + let(:payload) do + base_payload.merge( + 'params' => { 'title' => 'workflow ticket' }, + ) + end + let!(:workflow) do + create(:core_workflow, + object: 'Ticket', + condition_selected: { + 'ticket.title': { + operator: 'contains not', + value: [ 'workfluw ticket', 'workflaw ticket' ], + }, + }) + end + + it 'does match' do + expect(result[:matched_workflows]).to include(workflow.id) + end + + describe 'for invalid value' do + let!(:workflow) do + create(:core_workflow, + object: 'Ticket', + condition_selected: { + 'ticket.title': { + operator: 'contains not', + value: [ 'workflow ticket', 'workflow ticket' ], + }, + }) + end + + it 'does not match' do + expect(result[:matched_workflows]).not_to include(workflow.id) + end + end + end + + describe '.perform - Condition - Contains all' do + let(:payload) do + base_payload.merge( + 'params' => { 'title' => 'workflow ticket' }, + ) + end + let!(:workflow) do + create(:core_workflow, + object: 'Ticket', + condition_selected: { + 'ticket.title': { + operator: 'contains all', + value: [ 'workflow ticket', 'workflow ticket' ], + }, + }) + end + + it 'does match' do + expect(result[:matched_workflows]).to include(workflow.id) + end + + describe 'for invalid value' do + let!(:workflow) do + create(:core_workflow, + object: 'Ticket', + condition_selected: { + 'ticket.title': { + operator: 'contains all', + value: [ 'workflow ticket', 'workflaw ticket' ], + }, + }) + end + + it 'does not match' do + expect(result[:matched_workflows]).not_to include(workflow.id) + end + end + end + + describe '.perform - Condition - Contains all not' do + let(:payload) do + base_payload.merge( + 'params' => { 'title' => 'workflow ticket' }, + ) + end + let!(:workflow) do + create(:core_workflow, + object: 'Ticket', + condition_selected: { + 'ticket.title': { + operator: 'contains all not', + value: [ 'workfluw ticket', 'workflaw ticket' ], + }, + }) + end + + it 'does match' do + expect(result[:matched_workflows]).to include(workflow.id) + end + + describe 'for invalid value' do + let!(:workflow) do + create(:core_workflow, + object: 'Ticket', + condition_selected: { + 'ticket.title': { + operator: 'contains all not', + value: [ 'workflow ticket', 'workflaw ticket' ], + }, + }) + end + + it 'does not match' do + expect(result[:matched_workflows]).not_to include(workflow.id) + end + end + end + + describe '.perform - Stop after match' do + let(:stop_after_match) { false } + + before do + create(:core_workflow, + object: 'Ticket', + perform: { + 'ticket.priority_id': { + operator: 'hide', + hide: 'true' + }, + }) + create(:core_workflow, + object: 'Ticket', + perform: { + 'ticket.priority_id': { + operator: 'show', + show: 'true' + }, + }, + stop_after_match: stop_after_match) + create(:core_workflow, + object: 'Ticket', + perform: { + 'ticket.priority_id': { + operator: 'hide', + hide: 'true' + }, + }) + end + + it 'does not stop' do + expect(result[:visibility]['priority_id']).to eq('hide') + end + + describe 'with stop_after_match' do + let(:stop_after_match) { true } + + it 'does stop' do + expect(result[:visibility]['priority_id']).to eq('show') + end + end + end + + describe '.perform - Condition - Custom module' do + let(:modules) { ['CoreWorkflow::Custom::Testa', 'CoreWorkflow::Custom::Testb', 'CoreWorkflow::Custom::Testc'] } + let(:custom_class_false) do + Class.new(CoreWorkflow::Custom::Backend) do + def selected_attribute_match? + false + end + end + end + let(:custom_class_true) do + Class.new(CoreWorkflow::Custom::Backend) do + def selected_attribute_match? + true + end + end + end + let!(:workflow) do + create(:core_workflow, + object: 'Ticket', + condition_selected: { + 'custom.module': { + operator: operator, + value: modules, + }, + }) + end + + describe 'with "match all modules" false' do + let(:operator) { 'match all modules' } + + before do + stub_const 'CoreWorkflow::Custom::Testa', custom_class_false + stub_const 'CoreWorkflow::Custom::Testb', custom_class_false + stub_const 'CoreWorkflow::Custom::Testc', custom_class_false + end + + it 'does not match' do + expect(result[:matched_workflows]).not_to include(workflow.id) + end + end + + describe 'with "match all modules" true' do + let(:operator) { 'match all modules' } + + before do + stub_const 'CoreWorkflow::Custom::Testa', custom_class_true + stub_const 'CoreWorkflow::Custom::Testb', custom_class_true + stub_const 'CoreWorkflow::Custom::Testc', custom_class_true + end + + it 'does match' do + expect(result[:matched_workflows]).to include(workflow.id) + end + end + + describe 'with "match all modules" blank' do + let(:modules) { [] } + let(:operator) { 'match all modules' } + + it 'does match' do + expect(result[:matched_workflows]).to include(workflow.id) + end + end + + describe 'with "match one module" true' do + let(:operator) { 'match one module' } + + before do + stub_const 'CoreWorkflow::Custom::Testa', custom_class_false + stub_const 'CoreWorkflow::Custom::Testb', custom_class_false + stub_const 'CoreWorkflow::Custom::Testc', custom_class_true + end + + it 'does match' do + expect(result[:matched_workflows]).to include(workflow.id) + end + end + + describe 'with "match one module" false' do + let(:operator) { 'match one module' } + + before do + stub_const 'CoreWorkflow::Custom::Testa', custom_class_false + stub_const 'CoreWorkflow::Custom::Testb', custom_class_false + stub_const 'CoreWorkflow::Custom::Testc', custom_class_false + end + + it 'does not match' do + expect(result[:matched_workflows]).not_to include(workflow.id) + end + end + + describe 'with "match one module" blank' do + let(:modules) { [] } + let(:operator) { 'match one module' } + + it 'does match' do + expect(result[:matched_workflows]).to include(workflow.id) + end + end + + describe 'with "match no modules" true' do + let(:operator) { 'match no modules' } + + before do + stub_const 'CoreWorkflow::Custom::Testa', custom_class_false + stub_const 'CoreWorkflow::Custom::Testb', custom_class_false + stub_const 'CoreWorkflow::Custom::Testc', custom_class_false + end + + it 'does match' do + expect(result[:matched_workflows]).to include(workflow.id) + end + end + + describe 'with "match no modules" false' do + let(:operator) { 'match no modules' } + + before do + stub_const 'CoreWorkflow::Custom::Testa', custom_class_true + stub_const 'CoreWorkflow::Custom::Testb', custom_class_true + stub_const 'CoreWorkflow::Custom::Testc', custom_class_true + end + + it 'does not match' do + expect(result[:matched_workflows]).not_to include(workflow.id) + end + end + + describe 'with "match no modules" blank' do + let(:modules) { [] } + let(:operator) { 'match no modules' } + + it 'does match' do + expect(result[:matched_workflows]).to include(workflow.id) + end + end + end + + describe '.perform - Select' do + let!(:workflow1) do + create(:core_workflow, + object: 'Ticket', + perform: { + 'ticket.group_id': { + operator: 'select', + select: [ticket.group.id.to_s] + }, + }) + end + let!(:workflow2) do + create(:core_workflow, + object: 'Ticket', + condition_selected: { + 'ticket.group_id': { + operator: 'is', + value: ticket.group.id.to_s + }, + }, + perform: { + 'ticket.owner_id': { + operator: 'select', + select: [action_user.id.to_s] + }, + }) + end + + it 'does match workflows' do + expect(result[:matched_workflows]).to include(workflow1.id, workflow2.id) + end + + it 'does select group' do + expect(result[:select]['group_id']).to eq(ticket.group.id.to_s) + end + + it 'does select owner (recursion)' do + expect(result[:select]['owner_id']).to eq(action_user.id.to_s) + end + + it 'does rerun 2 times (group select + owner select)' do + expect(result[:rerun_count]).to eq(2) + end + end + + describe '.perform - Auto Select' do + let!(:workflow1) do + create(:core_workflow, + object: 'Ticket', + perform: { + 'ticket.group_id': { + operator: 'auto_select', + auto_select: true + }, + }) + end + let!(:workflow2) do + create(:core_workflow, + object: 'Ticket', + condition_selected: { + 'ticket.group_id': { + operator: 'is', + value: ticket.group.id.to_s + }, + }, + perform: { + 'ticket.owner_id': { + operator: 'auto_select', + auto_select: true + }, + }) + end + + it 'does match workflows' do + expect(result[:matched_workflows]).to include(workflow1.id, workflow2.id) + end + + it 'does select group' do + expect(result[:select]['group_id']).to eq(ticket.group.id.to_s) + end + + it 'does select owner (recursion)' do + expect(result[:select]['owner_id']).to eq(action_user.id.to_s) + end + + it 'does rerun 2 times (group select + owner select)' do + expect(result[:rerun_count]).to eq(2) + end + + describe 'with owner' do + let(:payload) do + base_payload.merge('params' => { + 'group_id' => ticket.group.id.to_s, + 'owner_id' => action_user.id.to_s, + }) + end + + it 'does not select owner' do + expect(result[:select]['owner_id']).to be nil + end + + it 'does rerun 0 times' do + expect(result[:rerun_count]).to eq(0) + end + end + end + + describe '.perform - Fill in' do + let!(:workflow1) do + create(:core_workflow, + object: 'Ticket', + perform: { + 'ticket.group_id': { + operator: 'select', + select: [ticket.group.id.to_s] + }, + }) + end + let!(:workflow2) do + create(:core_workflow, + object: 'Ticket', + condition_selected: { + 'ticket.group_id': { + operator: 'is', + value: ticket.group.id.to_s + }, + }, + perform: { + 'ticket.title': { + operator: 'fill_in', + fill_in: 'hello' + }, + }) + end + + it 'does match workflows' do + expect(result[:matched_workflows]).to include(workflow1.id, workflow2.id) + end + + it 'does select group' do + expect(result[:select]['group_id']).to eq(ticket.group.id.to_s) + end + + it 'does fill in title' do + expect(result[:fill_in]['title']).to eq('hello') + end + + it 'does rerun 1 time (group select + title fill in)' do + expect(result[:rerun_count]).to eq(1) + end + end + + describe '.perform - Fill in empty' do + let!(:workflow1) do + create(:core_workflow, + object: 'Ticket', + perform: { + 'ticket.group_id': { + operator: 'select', + select: [ticket.group.id.to_s] + }, + }) + end + let!(:workflow2) do + create(:core_workflow, + object: 'Ticket', + condition_selected: { + 'ticket.group_id': { + operator: 'is', + value: ticket.group.id.to_s + }, + }, + perform: { + 'ticket.title': { + operator: 'fill_in_empty', + fill_in_empty: 'hello' + }, + }) + end + + it 'does match workflows' do + expect(result[:matched_workflows]).to include(workflow1.id, workflow2.id) + end + + it 'does select group' do + expect(result[:select]['group_id']).to eq(ticket.group.id.to_s) + end + + it 'does fill in title' do + expect(result[:fill_in]['title']).to eq('hello') + end + + it 'does rerun 1 time (group select + title fill in)' do + expect(result[:rerun_count]).to eq(1) + end + + describe 'with title' do + let(:payload) do + base_payload.merge('params' => { + 'title' => 'ha!', + }) + end + + it 'does not fill in title' do + expect(result[:fill_in]['title']).to be nil + end + + it 'does rerun 1 times (group select)' do + expect(result[:rerun_count]).to eq(1) + end + end + end + + describe '.perform - Rerun attributes default cache bug' do + before do + create(:core_workflow, + object: 'Ticket', + perform: { + 'ticket.group_id': { + operator: 'select', + select: [ticket.group.id.to_s] + }, + }) + create(:core_workflow, + object: 'Ticket', + condition_selected: { + 'ticket.group_id': { + operator: 'is_set', + }, + }, + perform: { + 'ticket.owner_id': { + operator: 'select', + select: [action_user.id.to_s] + }, + }) + create(:core_workflow, + object: 'Ticket', + condition_selected: { + 'ticket.owner_id': { + operator: 'not_set', + }, + }, + perform: { + 'ticket.priority_id': { + operator: 'hide', + hide: 'true' + }, + }) + end + + it 'does not hide priority id' do + expect(result[:visibility]['priority_id']).to eq('show') + end + end + + describe '.perform - Clean up params after restrict values removed selected value by set_fixed_to' do + let(:payload) do + base_payload.merge('params' => { + 'owner_id' => action_user.id, + }) + end + + before do + create(:core_workflow, + object: 'Ticket', + perform: { + 'ticket.group_id': { + operator: 'select', + select: [ticket.group.id.to_s] + }, + }) + create(:core_workflow, + object: 'Ticket', + condition_selected: { + 'ticket.group_id': { + operator: 'is_set', + }, + }, + perform: { + 'ticket.owner_id': { + operator: 'set_fixed_to', + set_fixed_to: [''] + }, + }) + create(:core_workflow, + object: 'Ticket', + condition_selected: { + 'ticket.owner_id': { + operator: 'is_set', + }, + }, + perform: { + 'ticket.priority_id': { + operator: 'hide', + hide: 'true' + }, + }) + end + + it 'does not allow owner_id' do + expect(result[:restrict_values]['owner_id']).not_to include(action_user.id) + end + + it 'does not hide priority id' do + expect(result[:visibility]['priority_id']).to eq('show') + end + end + + describe '.perform - Clean up params after restrict values removed selected value by remove_option' do + let(:payload) do + base_payload.merge('params' => { + 'owner_id' => action_user.id, + }) + end + + before do + create(:core_workflow, + object: 'Ticket', + perform: { + 'ticket.group_id': { + operator: 'select', + select: [ticket.group.id.to_s] + }, + }) + create(:core_workflow, + object: 'Ticket', + condition_selected: { + 'ticket.group_id': { + operator: 'is_set', + }, + }, + perform: { + 'ticket.owner_id': { + operator: 'remove_option', + remove_option: [action_user.id] + }, + }) + create(:core_workflow, + object: 'Ticket', + condition_selected: { + 'ticket.owner_id': { + operator: 'is_set', + }, + }, + perform: { + 'ticket.priority_id': { + operator: 'hide', + hide: 'true' + }, + }) + end + + it 'does not allow owner_id' do + expect(result[:restrict_values]['owner_id']).not_to include(action_user.id) + end + + it 'does not hide priority id' do + expect(result[:visibility]['priority_id']).to eq('show') + end + end + + describe '.perform - Clean up params after restrict values removed selected value by default attributes' do + let(:payload) do + base_payload.merge('params' => { + 'owner_id' => action_user.id, + }) + end + + before do + create(:core_workflow, + object: 'Ticket', + condition_selected: { + 'ticket.owner_id': { + operator: 'is_set', + }, + }, + perform: { + 'ticket.priority_id': { + operator: 'hide', + hide: 'true' + }, + }) + end + + it 'does not allow owner_id' do + expect(result[:restrict_values]['owner_id']).not_to include(action_user.id) + end + + it 'does not hide priority id' do + expect(result[:visibility]['priority_id']).to eq('show') + end + end + + describe '.perform - Default - auto selection based on only_shown_if_selectable' do + it 'does auto select group' do + expect(result[:select]['group_id']).not_to be nil + end + + it 'does auto hide group' do + expect(result[:visibility]['group_id']).to eq('hide') + end + end + + describe '.perform - One field and two perform actions' do + before do + create(:core_workflow, + object: 'Ticket', + perform: { + 'ticket.owner_id': { + operator: %w[select set_optional], + select: [action_user.id.to_s], + set_optional: 'true', + }, + }) + end + + it 'does auto select owner' do + expect(result[:select]['owner_id']).to eq(action_user.id.to_s) + end + + it 'does set owner optional' do + expect(result[:mandatory]['owner_id']).to eq(false) + end + end + + describe '.perform - Hide mobile based on user login' do + let(:base_payload) do + { + 'event' => 'core_workflow', + 'request_id' => 'default', + 'class_name' => 'User', + 'screen' => 'create', + 'params' => { + 'login' => 'nicole.special@zammad.org', + }, + } + end + + before do + create(:core_workflow, + object: 'User', + condition_selected: { 'user.login'=>{ 'operator' => 'is', 'value' => 'nicole.special@zammad.org' } }, + perform: { 'user.mobile'=>{ 'operator' => 'hide', 'hide' => 'true' } },) + end + + it 'does hide mobile for user' do + expect(result[:visibility]['mobile']).to eq('hide') + end + end + + describe '.perform - Condition - group active is true' do + let(:payload) do + base_payload.merge('params' => { + 'group_id' => Group.first.id, + }) + end + + let!(:workflow) do + create(:core_workflow, + object: 'Ticket', + condition_selected: { 'group.active'=>{ 'operator' => 'is', 'value' => true } }) + end + + it 'does match' do + expect(result[:matched_workflows]).to include(workflow.id) + end + end + + describe '.perform - Condition - group.assignment_timeout (Integer) matches' do + let(:group) { create(:group, assignment_timeout: 10) } + let(:payload) do + base_payload.merge('params' => { + 'group_id' => group.id, + }) + end + + before do + create(:core_workflow, + object: 'Ticket', + condition_selected: { 'group.assignment_timeout'=>{ 'operator' => 'is', 'value' => 10 } }, + perform: { 'ticket.priority_id'=>{ 'operator' => 'hide', 'hide' => 'true' } },) + end + + it 'does match' do + expect(result[:visibility]['priority_id']).to eq('hide') + end + end +end diff --git a/spec/models/ticket_spec.rb b/spec/models/ticket_spec.rb index 9ed46d4bd..22d5736d3 100644 --- a/spec/models/ticket_spec.rb +++ b/spec/models/ticket_spec.rb @@ -4,6 +4,7 @@ require 'rails_helper' require 'models/application_model_examples' require 'models/concerns/can_be_imported_examples' require 'models/concerns/can_csv_import_examples' +require 'models/concerns/checks_core_workflow_examples' require 'models/concerns/has_history_examples' require 'models/concerns/has_tags_examples' require 'models/concerns/has_taskbars_examples' @@ -23,6 +24,7 @@ RSpec.describe Ticket, type: :model do it_behaves_like 'ApplicationModel' it_behaves_like 'CanBeImported' it_behaves_like 'CanCsvImport' + it_behaves_like 'ChecksCoreWorkflow' it_behaves_like 'HasHistory', history_relation_object: ['Ticket::Article', 'Mention'] it_behaves_like 'HasTags' it_behaves_like 'TagWritesToTicketHistory' diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 68feb3103..d9a9ea594 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -815,6 +815,7 @@ RSpec.describe User, type: :model do 'User' => { 'created_by_id' => 1, 'out_of_office_replacement_id' => 1, 'updated_by_id' => 1 }, 'Organization' => { 'created_by_id' => 0, 'updated_by_id' => 0 }, 'Macro' => { 'created_by_id' => 0, 'updated_by_id' => 0 }, + 'CoreWorkflow' => { 'created_by_id' => 0, 'updated_by_id' => 0 }, 'Mention' => { 'created_by_id' => 1, 'updated_by_id' => 0, 'user_id' => 1 }, 'Channel' => { 'created_by_id' => 0, 'updated_by_id' => 0 }, 'Role' => { 'created_by_id' => 0, 'updated_by_id' => 0 }, diff --git a/spec/support/current_attributes.rb b/spec/support/current_attributes.rb new file mode 100644 index 000000000..773d1d194 --- /dev/null +++ b/spec/support/current_attributes.rb @@ -0,0 +1,8 @@ +# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ + +RSpec.configure do |config| + # clear ActiveSupport::CurrentAttributes caches + config.after do + ActiveSupport::CurrentAttributes.clear_all + end +end diff --git a/spec/system/examples/core_workflow_examples.rb b/spec/system/examples/core_workflow_examples.rb new file mode 100644 index 000000000..ecee2bf3c --- /dev/null +++ b/spec/system/examples/core_workflow_examples.rb @@ -0,0 +1,515 @@ +# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ + +RSpec.shared_examples 'core workflow' do + let(:field_name) { SecureRandom.uuid } + let(:screens) do + { + create_middle: { + '-all-' => { + shown: true, + required: false, + }, + }, + create: { + '-all-' => { + shown: true, + required: false, + }, + }, + edit: { + '-all-' => { + shown: true, + required: false, + }, + }, + } + end + + describe 'modify text attribute', authenticated_as: :authenticate, db_strategy: :reset do + def authenticate + create(:object_manager_attribute_text, object_name: object_name, name: field_name, display: field_name, screens: screens) + ObjectManager::Attribute.migration_execute + true + end + + describe 'action - show' do + before do + create(:core_workflow, + object: object_name, + perform: { + "#{object_name.downcase}.#{field_name}": { + operator: 'show', + show: 'true' + }, + }) + end + + it 'does perform' do + before_it.call + expect(page).to have_selector("input[name='#{field_name}']", wait: 10) + end + end + + describe 'action - hide' do + before do + create(:core_workflow, + object: object_name, + perform: { + "#{object_name.downcase}.#{field_name}": { + operator: 'hide', + hide: 'true' + }, + }) + end + + it 'does perform' do + before_it.call + expect(page).to have_selector(".form-group[data-attribute-name='#{field_name}'].is-hidden", visible: :hidden, wait: 10) + end + end + + describe 'action - remove' do + before do + create(:core_workflow, + object: object_name, + perform: { + "#{object_name.downcase}.#{field_name}": { + operator: 'remove', + remove: 'true' + }, + }) + end + + it 'does perform' do + before_it.call + expect(page).to have_selector(".form-group[data-attribute-name='#{field_name}'].is-removed", visible: :hidden, wait: 10) + end + end + + describe 'action - set_optional' do + before do + create(:core_workflow, + object: object_name, + perform: { + "#{object_name.downcase}.#{field_name}": { + operator: 'set_optional', + set_optional: 'true' + }, + }) + end + + it 'does perform' do + before_it.call + expect(page.find("div[data-attribute-name='#{field_name}'] div.formGroup-label label")).to have_no_text('*', wait: 10) + end + end + + describe 'action - set_mandatory' do + before do + create(:core_workflow, + object: object_name, + perform: { + "#{object_name.downcase}.#{field_name}": { + operator: 'set_mandatory', + set_mandatory: 'true' + }, + }) + end + + it 'does perform' do + before_it.call + expect(page.find("div[data-attribute-name='#{field_name}'] div.formGroup-label label")).to have_text('*', wait: 10) + end + end + + describe 'action - fill_in' do + before do + create(:core_workflow, + object: object_name, + perform: { + "#{object_name.downcase}.#{field_name}": { + operator: 'fill_in', + fill_in: '4cddb2twza' + }, + }) + end + + it 'does perform' do + before_it.call + expect(page).to have_field(field_name, with: '4cddb2twza', wait: 10) + end + end + + describe 'action - fill_in_empty' do + describe 'with match' do + before do + create(:core_workflow, + object: object_name, + perform: { + "#{object_name.downcase}.#{field_name}": { + operator: 'fill_in_empty', + fill_in_empty: '9999' + }, + }) + end + + it 'does perform' do + before_it.call + expect(page).to have_field(field_name, with: '9999', wait: 10) + end + end + + describe 'without match' do + before do + create(:core_workflow, + object: object_name, + perform: { + "#{object_name.downcase}.#{field_name}": { + operator: 'fill_in', + fill_in: '4cddb2twza' + }, + }) + create(:core_workflow, + object: object_name, + perform: { + "#{object_name.downcase}.#{field_name}": { + operator: 'fill_in_empty', + fill_in_empty: '9999' + }, + }) + end + + it 'does perform' do + before_it.call + expect(page).to have_no_field(field_name, with: '9999', wait: 10) + end + end + end + end + + describe 'modify select attribute', authenticated_as: :authenticate, db_strategy: :reset do + def authenticate + create(:object_manager_attribute_select, object_name: object_name, name: field_name, display: field_name, screens: screens) + ObjectManager::Attribute.migration_execute + true + end + + describe 'action - show' do + before do + create(:core_workflow, + object: object_name, + perform: { + "#{object_name.downcase}.#{field_name}": { + operator: 'show', + show: 'true' + }, + }) + end + + it 'does perform' do + before_it.call + expect(page).to have_selector("select[name='#{field_name}']", wait: 10) + end + end + + describe 'action - hide' do + before do + create(:core_workflow, + object: object_name, + perform: { + "#{object_name.downcase}.#{field_name}": { + operator: 'hide', + hide: 'true' + }, + }) + end + + it 'does perform' do + before_it.call + expect(page).to have_selector(".form-group[data-attribute-name='#{field_name}'].is-hidden", visible: :hidden, wait: 10) + end + end + + describe 'action - remove' do + before do + create(:core_workflow, + object: object_name, + perform: { + "#{object_name.downcase}.#{field_name}": { + operator: 'remove', + remove: 'true' + }, + }) + end + + it 'does perform' do + before_it.call + expect(page).to have_selector(".form-group[data-attribute-name='#{field_name}'].is-removed", visible: :hidden, wait: 10) + end + end + + describe 'action - set_optional' do + before do + create(:core_workflow, + object: object_name, + perform: { + "#{object_name.downcase}.#{field_name}": { + operator: 'set_optional', + set_optional: 'true' + }, + }) + end + + it 'does perform' do + before_it.call + expect(page.find("div[data-attribute-name='#{field_name}'] div.formGroup-label label")).to have_no_text('*', wait: 10) + end + end + + describe 'action - set_mandatory' do + before do + create(:core_workflow, + object: object_name, + perform: { + "#{object_name.downcase}.#{field_name}": { + operator: 'set_mandatory', + set_mandatory: 'true' + }, + }) + end + + it 'does perform' do + before_it.call + expect(page.find("div[data-attribute-name='#{field_name}'] div.formGroup-label label")).to have_text('*', wait: 10) + end + end + + describe 'action - restrict values' do + before do + create(:core_workflow, + object: object_name, + perform: { + "#{object_name.downcase}.#{field_name}": { + operator: 'set_fixed_to', + set_fixed_to: %w[key_1 key_3] + }, + }) + end + + it 'does perform' do + before_it.call + expect(page).to have_selector("select[name='#{field_name}'] option[value='key_1']", wait: 10) + expect(page).to have_no_selector("select[name='#{field_name}'] option[value='key_2']", wait: 10) + expect(page).to have_selector("select[name='#{field_name}'] option[value='key_3']", wait: 10) + end + end + + describe 'action - select' do + before do + create(:core_workflow, + object: object_name, + perform: { + "#{object_name.downcase}.#{field_name}": { + operator: 'select', + select: ['key_3'] + }, + }) + end + + it 'does perform' do + before_it.call + expect(page).to have_selector("select[name='#{field_name}'] option[value='key_3'][selected]", wait: 10) + end + end + + describe 'action - auto select' do + before do + create(:core_workflow, + object: object_name, + perform: { + "#{object_name.downcase}.#{field_name}": { + operator: 'set_fixed_to', + set_fixed_to: ['', 'key_3'], + }, + }) + create(:core_workflow, + object: object_name, + perform: { + "#{object_name.downcase}.#{field_name}": { + operator: 'auto_select', + auto_select: 'true', + }, + }) + end + + it 'does perform' do + before_it.call + expect(page).to have_selector("select[name='#{field_name}'] option[value='key_3'][selected]", wait: 10) + end + end + end + + describe 'modify tree select attribute', authenticated_as: :authenticate, db_strategy: :reset do + def authenticate + create(:object_manager_attribute_tree_select, object_name: object_name, name: field_name, display: field_name, screens: screens) + ObjectManager::Attribute.migration_execute + true + end + + describe 'action - show' do + before do + create(:core_workflow, + object: object_name, + perform: { + "#{object_name.downcase}.#{field_name}": { + operator: 'show', + show: 'true' + }, + }) + end + + it 'does perform' do + before_it.call + expect(page).to have_selector("input[name='#{field_name}']", visible: :all, wait: 10) + end + end + + describe 'action - hide' do + before do + create(:core_workflow, + object: object_name, + perform: { + "#{object_name.downcase}.#{field_name}": { + operator: 'hide', + hide: 'true' + }, + }) + end + + it 'does perform' do + before_it.call + expect(page).to have_selector(".form-group[data-attribute-name='#{field_name}'].is-hidden", visible: :all, wait: 10) + end + end + + describe 'action - remove' do + before do + create(:core_workflow, + object: object_name, + perform: { + "#{object_name.downcase}.#{field_name}": { + operator: 'remove', + remove: 'true' + }, + }) + end + + it 'does perform' do + before_it.call + expect(page).to have_selector(".form-group[data-attribute-name='#{field_name}'].is-removed", visible: :hidden, wait: 10) + end + end + + describe 'action - set_optional' do + before do + create(:core_workflow, + object: object_name, + perform: { + "#{object_name.downcase}.#{field_name}": { + operator: 'set_optional', + set_optional: 'true' + }, + }) + end + + it 'does perform' do + before_it.call + expect(page.find("div[data-attribute-name='#{field_name}'] div.formGroup-label label")).to have_no_text('*', wait: 10) + end + end + + describe 'action - set_mandatory' do + before do + create(:core_workflow, + object: object_name, + perform: { + "#{object_name.downcase}.#{field_name}": { + operator: 'set_mandatory', + set_mandatory: 'true' + }, + }) + end + + it 'does perform' do + before_it.call + expect(page.find("div[data-attribute-name='#{field_name}'] div.formGroup-label label")).to have_text('*', wait: 10) + end + end + + describe 'action - restrict values' do + before do + create(:core_workflow, + object: object_name, + perform: { + "#{object_name.downcase}.#{field_name}": { + operator: 'set_fixed_to', + set_fixed_to: ['Incident', 'Incident::Hardware', 'Incident::Hardware::Monitor'] + }, + }) + end + + it 'does perform' do + before_it.call + expect(page).to have_selector('span.searchableSelect-option-text', text: 'Incident', visible: :all, wait: 10) + expect(page).to have_selector('span.searchableSelect-option-text', text: 'Hardware', visible: :all, wait: 10) + expect(page).to have_selector('span.searchableSelect-option-text', text: 'Monitor', visible: :all, wait: 10) + expect(page).to have_no_selector('span.searchableSelect-option-text', text: 'Mouse', visible: :all, wait: 10) + expect(page).to have_no_selector('span.searchableSelect-option-text', text: 'Softwareproblem', visible: :all, wait: 10) + end + end + + describe 'action - select' do + before do + create(:core_workflow, + object: object_name, + perform: { + "#{object_name.downcase}.#{field_name}": { + operator: 'select', + select: ['Incident::Hardware::Monitor'] + }, + }) + end + + it 'does perform' do + before_it.call + expect(page).to have_selector("input[name='#{field_name}'][value='Incident::Hardware::Monitor']", visible: :all, wait: 10) + end + end + + describe 'action - auto select' do + before do + create(:core_workflow, + object: object_name, + perform: { + "#{object_name.downcase}.#{field_name}": { + operator: 'set_fixed_to', + set_fixed_to: ['', 'Incident'], + }, + }) + create(:core_workflow, + object: object_name, + perform: { + "#{object_name.downcase}.#{field_name}": { + operator: 'auto_select', + auto_select: 'true', + }, + }) + end + + it 'does perform' do + before_it.call + expect(page).to have_selector("input[name='#{field_name}'][value='Incident']", visible: :all, wait: 10) + end + end + end +end diff --git a/spec/system/js/q_unit_spec.rb b/spec/system/js/q_unit_spec.rb index a1021cdf3..ffa1fe604 100644 --- a/spec/system/js/q_unit_spec.rb +++ b/spec/system/js/q_unit_spec.rb @@ -131,6 +131,10 @@ RSpec.describe 'QUnit', type: :system, authenticated_as: false, set_up: true, we it 'DateTime' do q_unit_tests('form_datetime') end + + it 'Core Workflow' do + q_unit_tests('form_core_workflow') + end end context 'Form AJAX', searchindex: true do diff --git a/spec/system/manage/groups_spec.rb b/spec/system/manage/groups_spec.rb index 42963cd65..6b0f6aa37 100644 --- a/spec/system/manage/groups_spec.rb +++ b/spec/system/manage/groups_spec.rb @@ -1,6 +1,7 @@ # Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ require 'rails_helper' +require 'system/examples/core_workflow_examples' require 'system/examples/pagination_examples' RSpec.describe 'Manage > Groups', type: :system do @@ -8,6 +9,20 @@ RSpec.describe 'Manage > Groups', type: :system do include_examples 'pagination', model: :group, klass: Group, path: 'manage/groups' end + describe 'Core Workflow' do + include_examples 'core workflow' do + let(:object_name) { 'Group' } + let(:before_it) do + lambda { + ensure_websocket(check_if_pinged: false) do + visit 'manage/groups' + click_on 'New Group' + end + } + end + end + end + context "Issue 2544 - Can't remove auto assignment timeout" do before do visit '/#manage/groups' diff --git a/spec/system/manage/organizations_spec.rb b/spec/system/manage/organizations_spec.rb index 2c248d531..0aa55553d 100644 --- a/spec/system/manage/organizations_spec.rb +++ b/spec/system/manage/organizations_spec.rb @@ -14,7 +14,7 @@ RSpec.describe 'Manage > Organizations', type: :system do it 'creates record', db_strategy: :reset do # required to edit attribute in admin interface - screens = { edit: { 'admin.organization': { shown: true, required: false } } } + screens = { create: { 'admin.organization': { shown: true, required: false } } } attribute = create(:object_manager_attribute_text, object_name: 'Organization', diff --git a/spec/system/organization/profile_spec.rb b/spec/system/organization/profile_spec.rb new file mode 100644 index 000000000..db7908782 --- /dev/null +++ b/spec/system/organization/profile_spec.rb @@ -0,0 +1,26 @@ +# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ + +require 'rails_helper' + +require 'system/examples/core_workflow_examples' + +RSpec.describe 'Organization Profile', type: :system do + let(:organization) { create(:organization) } + + describe 'Core Workflow' do + include_examples 'core workflow' do + let(:object_name) { 'Organization' } + let(:before_it) do + lambda { + ensure_websocket(check_if_pinged: false) do + visit "#organization/profile/#{organization.id}" + within(:active_content) do + page.find('.profile .js-action').click + page.find('.profile li[data-type=edit]').click + end + end + } + end + end + end +end diff --git a/spec/system/system/core_workflow_spec.rb b/spec/system/system/core_workflow_spec.rb new file mode 100644 index 000000000..ccbf5f6c2 --- /dev/null +++ b/spec/system/system/core_workflow_spec.rb @@ -0,0 +1,39 @@ +# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ + +require 'rails_helper' + +RSpec.describe 'System > Core Workflow', type: :system do + before do + ensure_websocket do + visit 'system/core_workflow' + end + end + + it 'shows correct screens and objects' do + click_link 'New Workflow' + expect(all("select[name='object'] option").map(&:text)).not_to include('Sla') + find_field('object').select 'Ticket' + expect(all("select[name='preferences::screen'] option").map(&:text)).to eq(['Creation mask', 'Edit mask']) + end + + describe 'for saved entry', authenticated_as: :authenticate do + def authenticate + create(:core_workflow, + name: 'special workflow', + object: 'Ticket', + changeable: true, + preferences: { + screen: ['edit'], + }) + true + end + + it 'shows correct screens and objects' do + first('tr.item').first('td').click + expect(all("select[name='object'] option").map(&:text)).not_to include('Sla') + expect(all("select[name='preferences::screen'] option").map(&:text)).to eq(['Creation mask', 'Edit mask']) + find_field('object').select '-' + expect(all("select[name='preferences::screen'] option").map(&:text)).to eq(['-']) + end + end +end diff --git a/spec/system/system/sla_spec.rb b/spec/system/system/sla_spec.rb new file mode 100644 index 000000000..9fe68616e --- /dev/null +++ b/spec/system/system/sla_spec.rb @@ -0,0 +1,60 @@ +# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ + +require 'rails_helper' + +RSpec.describe 'System > Sla', type: :system do + before do + ensure_websocket do + visit 'manage/slas' + end + end + + it 'shows correct required behaviour for checkboxes' do + page.find('.js-new').click + + # enable all checkboxes + page.find('input#update_time', visible: false).find(:xpath, './/..').click + page.find('input#solution_time', visible: false).find(:xpath, './/..').click + + # check if required + expect(page.find('input[name=first_response_time_in_text]')[:required]).to eq('true') + expect(page.find('input[name=update_time_in_text]')[:required]).to eq('true') + expect(page.find('input[name=solution_time_in_text]')[:required]).to eq('true') + + # drop all checkboxes + page.find('input#first_response_time', visible: false).find(:xpath, './/..').click + page.find('input#update_time', visible: false).find(:xpath, './/..').click + page.find('input#solution_time', visible: false).find(:xpath, './/..').click + + # check if optional + expect(page.find('input[name=first_response_time_in_text]')[:required]).not_to eq('true') + expect(page.find('input[name=update_time_in_text]')[:required]).not_to eq('true') + expect(page.find('input[name=solution_time_in_text]')[:required]).not_to eq('true') + end + + describe 'for saved entry', authenticated_as: :authenticate do + def authenticate + create(:sla, name: 'special sla', first_response_time: 3600, update_time: 3600, solution_time: 3600) + true + end + + it 'shows correct required behaviour for checkboxes' do + page.find('.js-edit').click + + # check if required + expect(page.find('input[name=first_response_time_in_text]')[:required]).to eq('true') + expect(page.find('input[name=update_time_in_text]')[:required]).to eq('true') + expect(page.find('input[name=solution_time_in_text]')[:required]).to eq('true') + + # drop all checkboxes + page.find('input#first_response_time', visible: false).find(:xpath, './/..').click + page.find('input#update_time', visible: false).find(:xpath, './/..').click + page.find('input#solution_time', visible: false).find(:xpath, './/..').click + + # check if optional + expect(page.find('input[name=first_response_time_in_text]')[:required]).not_to eq('true') + expect(page.find('input[name=update_time_in_text]')[:required]).not_to eq('true') + expect(page.find('input[name=solution_time_in_text]')[:required]).not_to eq('true') + end + end +end diff --git a/spec/system/ticket/create_spec.rb b/spec/system/ticket/create_spec.rb index fc64636a7..69df5b95d 100644 --- a/spec/system/ticket/create_spec.rb +++ b/spec/system/ticket/create_spec.rb @@ -2,6 +2,7 @@ require 'rails_helper' +require 'system/examples/core_workflow_examples' require 'system/examples/text_modules_examples' RSpec.describe 'Ticket Create', type: :system do @@ -430,6 +431,19 @@ RSpec.describe 'Ticket Create', type: :system do end end + describe 'Core Workflow' do + include_examples 'core workflow' do + let(:object_name) { 'Ticket' } + let(:before_it) do + lambda { + ensure_websocket(check_if_pinged: false) do + visit 'ticket/create' + end + } + end + end + end + # https://github.com/zammad/zammad/issues/2669 context 'when canceling new ticket creation' do it 'closes the dialog' do diff --git a/spec/system/ticket/zoom_spec.rb b/spec/system/ticket/zoom_spec.rb index 55943ef3e..335ee0be1 100644 --- a/spec/system/ticket/zoom_spec.rb +++ b/spec/system/ticket/zoom_spec.rb @@ -2,6 +2,8 @@ require 'rails_helper' +require 'system/examples/core_workflow_examples' + RSpec.describe 'Ticket zoom', type: :system do describe 'owner auto-assignment', authenticated_as: :authenticate do @@ -944,7 +946,7 @@ RSpec.describe 'Ticket zoom', type: :system do let(:user) { create(:customer) } let(:ticket) { create(:ticket, customer: user) } - it 'shows ticket state dropdown options in sorted order' do + it 'shows ticket state dropdown options in sorted translated alphabetically order' do visit "ticket/zoom/#{ticket.id}" await_empty_ajax_queue @@ -1658,6 +1660,20 @@ RSpec.describe 'Ticket zoom', type: :system do end end + describe 'Core Workflow' do + include_examples 'core workflow' do + let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) } + let(:object_name) { 'Ticket' } + let(:before_it) do + lambda { + ensure_websocket(check_if_pinged: false) do + visit "#ticket/zoom/#{ticket.id}" + end + } + end + end + end + context 'Sidebar - Open & Closed Tickets', searchindex: true, authenticated_as: :authenticate do let(:customer) { create(:customer, :with_org) } let(:ticket_open) { create(:ticket, group: Group.find_by(name: 'Users'), customer: customer, title: SecureRandom.uuid) } diff --git a/spec/system/user/profile_spec.rb b/spec/system/user/profile_spec.rb index f5860fdbf..eb984d2ec 100644 --- a/spec/system/user/profile_spec.rb +++ b/spec/system/user/profile_spec.rb @@ -2,6 +2,7 @@ require 'rails_helper' +require 'system/examples/core_workflow_examples' require 'system/examples/text_modules_examples' RSpec.describe 'User Profile', type: :system do @@ -34,6 +35,23 @@ RSpec.describe 'User Profile', type: :system do end end + describe 'Core Workflow' do + include_examples 'core workflow' do + let(:object_name) { 'User' } + let(:before_it) do + lambda { + ensure_websocket(check_if_pinged: false) do + visit "#user/profile/#{customer.id}" + within(:active_content) do + page.find('.profile .js-action').click + page.find('.profile li[data-type=edit]').click + end + end + } + end + end + end + it 'check that ignored attributes for user popover are not visible' do fill_in id: 'global-search', with: customer.email diff --git a/test/browser_test_helper.rb b/test/browser_test_helper.rb index 7808e4288..6df533809 100644 --- a/test/browser_test_helper.rb +++ b/test/browser_test_helper.rb @@ -2151,7 +2151,7 @@ wait untill text in selector disabppears log('ticket_create invalid group count', text: element.text) end end - assert_equal(0, count, 'owner selection should not be showm') + assert_equal(2, count, 'group_id selection should not be shown because of only one group exists (auto select + hide)') # check count of agents, should be only 3 / - selection + master + agent on init screen count = instance.find_elements(css: '.content.active .newTicket select[name="owner_id"] option').count @@ -2462,7 +2462,7 @@ wait untill text in selector disabppears # check if owner selection exists count = instance.find_elements(css: '.content.active .sidebar select[name="group_id"] option').count - assert_equal(0, count, 'owner selection should not be showm') + assert_equal(2, count, 'group_id selection should not be shown because of only one group exists (auto select + hide)') # check count of agents, should be only 3 / - selection + master + agent on init screen count = instance.find_elements(css: '.content.active .sidebar select[name="owner_id"] option').count @@ -4871,7 +4871,7 @@ wait untill text in selector disabppears =begin - This function waits for ajax requests and object form flow to be done + This function waits for ajax requests and core workflow to be done await_empty_ajax_queue @@ -4886,7 +4886,7 @@ wait untill text in selector disabppears sleep 0.5 break if instance.execute_script('return typeof(App) === "undefined"') - break if instance.execute_script('return App.Ajax.queue().length').zero? + break if instance.execute_script('return App.Ajax.queue().length').zero? && instance.execute_script('return Object.keys(App.FormHandlerCoreWorkflow.getRequests()).length').zero? end end diff --git a/test/support/current_attributes_helper.rb b/test/support/current_attributes_helper.rb new file mode 100644 index 000000000..335411a30 --- /dev/null +++ b/test/support/current_attributes_helper.rb @@ -0,0 +1,11 @@ +# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ + +module CurrentAttributesHelper + # clear ActiveSupport::CurrentAttributes caches + + def self.included(base) + base.teardown do + ActiveSupport::CurrentAttributes.clear_all + end + end +end diff --git a/test/unit/ticket_screen_options_test.rb b/test/unit/ticket_screen_options_test.rb deleted file mode 100644 index b5f0569e3..000000000 --- a/test/unit/ticket_screen_options_test.rb +++ /dev/null @@ -1,627 +0,0 @@ -# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ - -require 'test_helper' - -class TicketScreenOptionsTest < ActiveSupport::TestCase - - test 'base' do - - group1 = Group.create!( - name: 'Group 1', - active: true, - email_address: EmailAddress.first, - created_by_id: 1, - updated_by_id: 1, - ) - group2 = Group.create!( - name: 'Group 2', - active: true, - created_by_id: 1, - updated_by_id: 1, - ) - group3 = Group.create!( - name: 'Group 3', - active: true, - created_by_id: 1, - updated_by_id: 1, - ) - - agent1 = User.create!( - login: 'agent1@example.com', - firstname: 'Role', - lastname: 'Agent1', - email: 'agent1@example.com', - password: 'agentpw', - active: true, - roles: Role.where(name: %w[Admin Agent]), - updated_by_id: 1, - created_by_id: 1, - ) - agent1.group_names_access_map = { - group1.name => 'full', - group2.name => %w[read change], - group3.name => 'full', - } - - agent2 = User.create!( - login: 'agent2@example.com', - firstname: 'Role', - lastname: 'Agent2', - email: 'agent2@example.com', - password: 'agentpw', - active: true, - roles: Role.where(name: %w[Admin Agent]), - updated_by_id: 1, - created_by_id: 1, - ) - agent2.group_names_access_map = { - group1.name => 'full', - group2.name => %w[read change], - group3.name => ['create'], - } - - agent3 = User.create!( - login: 'agent3@example.com', - firstname: 'Role', - lastname: 'Agent3', - email: 'agent3@example.com', - password: 'agentpw', - active: true, - roles: Role.where(name: %w[Admin Agent]), - updated_by_id: 1, - created_by_id: 1, - ) - agent3.group_names_access_map = { - group1.name => 'full', - group2.name => ['full'], - } - - agent4 = User.create!( - login: 'agent4@example.com', - firstname: 'Role', - lastname: 'Agent4', - email: 'agent4@example.com', - password: 'agentpw', - active: true, - roles: Role.where(name: %w[Admin Agent]), - updated_by_id: 1, - created_by_id: 1, - ) - agent4.group_names_access_map = { - group1.name => 'full', - group2.name => %w[read overview change], - } - - agent5 = User.create!( - login: 'agent5@example.com', - firstname: 'Role', - lastname: 'Agent5', - email: 'agent5@example.com', - password: 'agentpw', - active: true, - roles: Role.where(name: %w[Admin Agent]), - updated_by_id: 1, - created_by_id: 1, - ) - agent5.group_names_access_map = { - group3.name => 'full', - } - - User.create!( - login: 'agent6@example.com', - firstname: 'Role', - lastname: 'Agent6', - email: 'agent6@example.com', - password: 'agentpw', - active: true, - roles: Role.where(name: %w[Admin Agent]), - updated_by_id: 1, - created_by_id: 1, - ) - - result = Ticket::ScreenOptions.attributes_to_change( - current_user: agent1, - ) - - assert(result[:form_meta]) - assert(result[:form_meta][:filter]) - assert(result[:form_meta][:filter][:state_id]) - assert_equal([ - Ticket::State.lookup(name: 'open').id, - Ticket::State.lookup(name: 'pending reminder').id, - Ticket::State.lookup(name: 'closed').id, - Ticket::State.lookup(name: 'pending close').id, - ], result[:form_meta][:filter][:state_id].sort) - assert(result[:form_meta][:filter][:priority_id]) - assert_equal([ - Ticket::Priority.lookup(name: '1 low').id, - Ticket::Priority.lookup(name: '2 normal').id, - Ticket::Priority.lookup(name: '3 high').id, - ], result[:form_meta][:filter][:priority_id].sort) - assert(result[:form_meta][:filter][:type_id]) - assert_equal([], result[:form_meta][:filter][:type_id].sort) - assert(result[:form_meta][:filter][:group_id]) - assert_equal([group1.id, group2.id, group3.id], result[:form_meta][:filter][:group_id].sort) - assert(result[:form_meta][:dependencies]) - assert(result[:form_meta][:dependencies][:group_id]) - assert_equal(4, result[:form_meta][:dependencies][:group_id].count) - assert(result[:form_meta][:dependencies][:group_id]['']) - assert(result[:form_meta][:dependencies][:group_id][''][:owner_id]) - assert_equal([], result[:form_meta][:dependencies][:group_id][''][:owner_id]) - assert(result[:form_meta][:dependencies][:group_id][group1.id]) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id]) - assert_equal(4, result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].count) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].include?(agent1.id)) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].include?(agent2.id)) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].include?(agent3.id)) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].include?(agent4.id)) - assert(result[:form_meta][:dependencies][:group_id][group3.id]) - assert(result[:form_meta][:dependencies][:group_id][group3.id][:owner_id]) - assert_equal(2, result[:form_meta][:dependencies][:group_id][group3.id][:owner_id].count) - assert(result[:form_meta][:dependencies][:group_id][group3.id][:owner_id].include?(agent1.id)) - assert(result[:form_meta][:dependencies][:group_id][group3.id][:owner_id].include?(agent5.id)) - - result = Ticket::ScreenOptions.attributes_to_change( - current_user: agent2, - ) - - assert(result[:form_meta]) - assert(result[:form_meta][:filter]) - assert(result[:form_meta][:filter][:state_id]) - assert_equal([ - Ticket::State.lookup(name: 'open').id, - Ticket::State.lookup(name: 'pending reminder').id, - Ticket::State.lookup(name: 'closed').id, - Ticket::State.lookup(name: 'pending close').id, - ], result[:form_meta][:filter][:state_id].sort) - assert(result[:form_meta][:filter][:priority_id]) - assert_equal([ - Ticket::Priority.lookup(name: '1 low').id, - Ticket::Priority.lookup(name: '2 normal').id, - Ticket::Priority.lookup(name: '3 high').id, - ], result[:form_meta][:filter][:priority_id].sort) - assert(result[:form_meta][:filter][:type_id]) - assert_equal([], result[:form_meta][:filter][:type_id].sort) - assert(result[:form_meta][:filter][:group_id]) - assert_equal([group1.id, group2.id, group3.id], result[:form_meta][:filter][:group_id].sort) - assert(result[:form_meta][:dependencies]) - assert(result[:form_meta][:dependencies][:group_id]) - assert_equal(4, result[:form_meta][:dependencies][:group_id].count) - assert(result[:form_meta][:dependencies][:group_id]['']) - assert(result[:form_meta][:dependencies][:group_id][''][:owner_id]) - assert_equal([], result[:form_meta][:dependencies][:group_id][''][:owner_id]) - assert(result[:form_meta][:dependencies][:group_id][group1.id]) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id]) - assert_equal(4, result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].count) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].include?(agent1.id)) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].include?(agent2.id)) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].include?(agent3.id)) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].include?(agent4.id)) - assert_equal(2, result[:form_meta][:dependencies][:group_id][group3.id][:owner_id].count) - assert(result[:form_meta][:dependencies][:group_id][group3.id][:owner_id].include?(agent1.id)) - assert(result[:form_meta][:dependencies][:group_id][group3.id][:owner_id].include?(agent5.id)) - - result = Ticket::ScreenOptions.attributes_to_change( - current_user: agent3, - ) - - assert(result[:form_meta]) - assert(result[:form_meta][:filter]) - assert(result[:form_meta][:filter][:state_id]) - assert_equal([ - Ticket::State.lookup(name: 'open').id, - Ticket::State.lookup(name: 'pending reminder').id, - Ticket::State.lookup(name: 'closed').id, - Ticket::State.lookup(name: 'pending close').id, - ], result[:form_meta][:filter][:state_id].sort) - assert(result[:form_meta][:filter][:priority_id]) - assert_equal([ - Ticket::Priority.lookup(name: '1 low').id, - Ticket::Priority.lookup(name: '2 normal').id, - Ticket::Priority.lookup(name: '3 high').id, - ], result[:form_meta][:filter][:priority_id].sort) - assert(result[:form_meta][:filter][:type_id]) - assert_equal([], result[:form_meta][:filter][:type_id].sort) - assert(result[:form_meta][:filter][:group_id]) - assert_equal([group1.id, group2.id], result[:form_meta][:filter][:group_id].sort) - assert(result[:form_meta][:dependencies]) - assert(result[:form_meta][:dependencies][:group_id]) - assert_equal(3, result[:form_meta][:dependencies][:group_id].count) - assert(result[:form_meta][:dependencies][:group_id]['']) - assert(result[:form_meta][:dependencies][:group_id][''][:owner_id]) - assert_equal([], result[:form_meta][:dependencies][:group_id][''][:owner_id]) - assert(result[:form_meta][:dependencies][:group_id][group1.id]) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id]) - assert_equal(4, result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].count) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].include?(agent1.id)) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].include?(agent2.id)) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].include?(agent3.id)) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].include?(agent4.id)) - assert_equal(1, result[:form_meta][:dependencies][:group_id][group2.id][:owner_id].count) - assert(result[:form_meta][:dependencies][:group_id][group2.id][:owner_id].include?(agent3.id)) - - ticket1 = Ticket.create!( - title: 'some title 1', - group: group1, - customer_id: 2, - state: Ticket::State.lookup(name: 'new'), - priority: Ticket::Priority.lookup(name: '2 normal'), - updated_by_id: 1, - created_by_id: 1, - ) - Ticket::Article.create!( - ticket_id: ticket1.id, - from: 'some_sender@example.com', - to: 'some_recipient@example.com', - subject: 'some subject', - message_id: 'some@id', - body: 'some message', - internal: false, - sender: Ticket::Article::Sender.find_by(name: 'Customer'), - type: Ticket::Article::Type.find_by(name: 'email'), - updated_by_id: 1, - created_by_id: 1, - ) - - ticket2 = Ticket.create!( - title: 'some title 2', - group: group2, - customer_id: 2, - state: Ticket::State.lookup(name: 'new'), - priority: Ticket::Priority.lookup(name: '2 normal'), - updated_by_id: 1, - created_by_id: 1, - ) - Ticket::Article.create!( - ticket_id: ticket2.id, - from: 'some_sender@example.com', - to: 'some_recipient@example.com', - subject: 'some subject', - message_id: 'some@id', - body: 'some message', - internal: false, - sender: Ticket::Article::Sender.find_by(name: 'Customer'), - type: Ticket::Article::Type.find_by(name: 'email'), - updated_by_id: 1, - created_by_id: 1, - ) - - result = Ticket::ScreenOptions.attributes_to_change( - ticket_id: ticket1.id, - current_user: agent1, - ) - - assert(result[:form_meta]) - assert(result[:form_meta][:filter]) - assert(result[:form_meta][:filter][:state_id]) - assert_equal([ - Ticket::State.lookup(name: 'new').id, - Ticket::State.lookup(name: 'open').id, - Ticket::State.lookup(name: 'pending reminder').id, - Ticket::State.lookup(name: 'closed').id, - Ticket::State.lookup(name: 'pending close').id, - ], result[:form_meta][:filter][:state_id].sort) - assert(result[:form_meta][:filter][:priority_id]) - assert_equal([ - Ticket::Priority.lookup(name: '1 low').id, - Ticket::Priority.lookup(name: '2 normal').id, - Ticket::Priority.lookup(name: '3 high').id, - ], result[:form_meta][:filter][:priority_id].sort) - assert(result[:form_meta][:filter][:type_id]) - assert_equal([ - Ticket::Article::Type.lookup(name: 'email').id, - Ticket::Article::Type.lookup(name: 'phone').id, - Ticket::Article::Type.lookup(name: 'note').id, - ], result[:form_meta][:filter][:type_id].sort) - assert(result[:form_meta][:filter][:group_id]) - assert_equal([group1.id, group2.id, group3.id], result[:form_meta][:filter][:group_id].sort) - assert(result[:form_meta][:dependencies]) - assert(result[:form_meta][:dependencies][:group_id]) - assert_equal(4, result[:form_meta][:dependencies][:group_id].count) - assert(result[:form_meta][:dependencies][:group_id]['']) - assert(result[:form_meta][:dependencies][:group_id][''][:owner_id]) - assert_equal([], result[:form_meta][:dependencies][:group_id][''][:owner_id]) - assert(result[:form_meta][:dependencies][:group_id][group1.id]) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id]) - assert_equal(4, result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].count) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].include?(agent1.id)) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].include?(agent2.id)) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].include?(agent3.id)) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].include?(agent4.id)) - assert(result[:form_meta][:dependencies][:group_id][group2.id]) - assert(result[:form_meta][:dependencies][:group_id][group2.id][:owner_id]) - assert_equal(1, result[:form_meta][:dependencies][:group_id][group2.id][:owner_id].count) - assert(result[:form_meta][:dependencies][:group_id][group2.id][:owner_id].include?(agent3.id)) - assert(result[:form_meta][:dependencies][:group_id][group3.id]) - assert(result[:form_meta][:dependencies][:group_id][group3.id][:owner_id]) - assert_equal(2, result[:form_meta][:dependencies][:group_id][group3.id][:owner_id].count) - assert(result[:form_meta][:dependencies][:group_id][group3.id][:owner_id].include?(agent1.id)) - assert(result[:form_meta][:dependencies][:group_id][group3.id][:owner_id].include?(agent5.id)) - - result = Ticket::ScreenOptions.attributes_to_change( - ticket_id: ticket2.id, - current_user: agent1, - ) - - assert(result[:form_meta]) - assert(result[:form_meta][:filter]) - assert(result[:form_meta][:filter][:state_id]) - assert_equal([ - Ticket::State.lookup(name: 'new').id, - Ticket::State.lookup(name: 'open').id, - Ticket::State.lookup(name: 'pending reminder').id, - Ticket::State.lookup(name: 'closed').id, - Ticket::State.lookup(name: 'pending close').id, - ], result[:form_meta][:filter][:state_id].sort) - assert(result[:form_meta][:filter][:priority_id]) - assert_equal([ - Ticket::Priority.lookup(name: '1 low').id, - Ticket::Priority.lookup(name: '2 normal').id, - Ticket::Priority.lookup(name: '3 high').id, - ], result[:form_meta][:filter][:priority_id].sort) - assert(result[:form_meta][:filter][:type_id]) - assert_equal([ - Ticket::Article::Type.lookup(name: 'phone').id, - Ticket::Article::Type.lookup(name: 'note').id, - ], result[:form_meta][:filter][:type_id].sort) - assert(result[:form_meta][:filter][:group_id]) - assert_equal([group1.id, group2.id, group3.id], result[:form_meta][:filter][:group_id].sort) - assert(result[:form_meta][:dependencies]) - assert(result[:form_meta][:dependencies][:group_id]) - assert_equal(4, result[:form_meta][:dependencies][:group_id].count) - assert(result[:form_meta][:dependencies][:group_id]['']) - assert(result[:form_meta][:dependencies][:group_id][''][:owner_id]) - assert_equal([], result[:form_meta][:dependencies][:group_id][''][:owner_id]) - assert(result[:form_meta][:dependencies][:group_id][group1.id]) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id]) - assert_equal(4, result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].count) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].include?(agent1.id)) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].include?(agent2.id)) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].include?(agent3.id)) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].include?(agent4.id)) - assert(result[:form_meta][:dependencies][:group_id][group2.id]) - assert(result[:form_meta][:dependencies][:group_id][group2.id][:owner_id]) - assert_equal(1, result[:form_meta][:dependencies][:group_id][group2.id][:owner_id].count) - assert(result[:form_meta][:dependencies][:group_id][group2.id][:owner_id].include?(agent3.id)) - assert(result[:form_meta][:dependencies][:group_id][group3.id]) - assert(result[:form_meta][:dependencies][:group_id][group3.id][:owner_id]) - assert_equal(2, result[:form_meta][:dependencies][:group_id][group3.id][:owner_id].count) - assert(result[:form_meta][:dependencies][:group_id][group3.id][:owner_id].include?(agent1.id)) - assert(result[:form_meta][:dependencies][:group_id][group3.id][:owner_id].include?(agent5.id)) - - result = Ticket::ScreenOptions.attributes_to_change( - ticket_id: ticket2.id, - current_user: agent1, - ) - - assert(result[:form_meta]) - assert(result[:form_meta][:filter]) - assert(result[:form_meta][:filter][:state_id]) - assert_equal([ - Ticket::State.lookup(name: 'new').id, - Ticket::State.lookup(name: 'open').id, - Ticket::State.lookup(name: 'pending reminder').id, - Ticket::State.lookup(name: 'closed').id, - Ticket::State.lookup(name: 'pending close').id, - ], result[:form_meta][:filter][:state_id].sort) - assert(result[:form_meta][:filter][:priority_id]) - assert_equal([ - Ticket::Priority.lookup(name: '1 low').id, - Ticket::Priority.lookup(name: '2 normal').id, - Ticket::Priority.lookup(name: '3 high').id, - ], result[:form_meta][:filter][:priority_id].sort) - assert(result[:form_meta][:filter][:type_id]) - assert_equal([ - Ticket::Article::Type.lookup(name: 'phone').id, - Ticket::Article::Type.lookup(name: 'note').id, - ], result[:form_meta][:filter][:type_id].sort) - assert(result[:form_meta][:filter][:group_id]) - assert_equal([group1.id, group2.id, group3.id], result[:form_meta][:filter][:group_id].sort) - assert(result[:form_meta][:dependencies]) - assert(result[:form_meta][:dependencies][:group_id]) - assert_equal(4, result[:form_meta][:dependencies][:group_id].count) - assert(result[:form_meta][:dependencies][:group_id]['']) - assert(result[:form_meta][:dependencies][:group_id][''][:owner_id]) - assert_equal([], result[:form_meta][:dependencies][:group_id][''][:owner_id]) - assert(result[:form_meta][:dependencies][:group_id][group1.id]) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id]) - assert_equal(4, result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].count) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].include?(agent1.id)) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].include?(agent2.id)) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].include?(agent3.id)) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].include?(agent4.id)) - assert(result[:form_meta][:dependencies][:group_id][group2.id]) - assert(result[:form_meta][:dependencies][:group_id][group2.id][:owner_id]) - assert_equal(1, result[:form_meta][:dependencies][:group_id][group2.id][:owner_id].count) - assert(result[:form_meta][:dependencies][:group_id][group2.id][:owner_id].include?(agent3.id)) - assert(result[:form_meta][:dependencies][:group_id][group3.id]) - assert(result[:form_meta][:dependencies][:group_id][group3.id][:owner_id]) - assert_equal(2, result[:form_meta][:dependencies][:group_id][group3.id][:owner_id].count) - assert(result[:form_meta][:dependencies][:group_id][group3.id][:owner_id].include?(agent1.id)) - assert(result[:form_meta][:dependencies][:group_id][group3.id][:owner_id].include?(agent5.id)) - - result = Ticket::ScreenOptions.attributes_to_change( - ticket_id: ticket1.id, - current_user: agent2, - ) - - assert(result[:form_meta]) - assert(result[:form_meta][:filter]) - assert(result[:form_meta][:filter][:state_id]) - assert_equal([ - Ticket::State.lookup(name: 'new').id, - Ticket::State.lookup(name: 'open').id, - Ticket::State.lookup(name: 'pending reminder').id, - Ticket::State.lookup(name: 'closed').id, - Ticket::State.lookup(name: 'pending close').id, - ], result[:form_meta][:filter][:state_id].sort) - assert(result[:form_meta][:filter][:priority_id]) - assert_equal([ - Ticket::Priority.lookup(name: '1 low').id, - Ticket::Priority.lookup(name: '2 normal').id, - Ticket::Priority.lookup(name: '3 high').id, - ], result[:form_meta][:filter][:priority_id].sort) - assert(result[:form_meta][:filter][:type_id]) - assert_equal([ - Ticket::Article::Type.lookup(name: 'email').id, - Ticket::Article::Type.lookup(name: 'phone').id, - Ticket::Article::Type.lookup(name: 'note').id, - ], result[:form_meta][:filter][:type_id].sort) - assert(result[:form_meta][:filter][:group_id]) - assert_equal([group1.id, group2.id, group3.id], result[:form_meta][:filter][:group_id].sort) - assert(result[:form_meta][:dependencies]) - assert(result[:form_meta][:dependencies][:group_id]) - assert_equal(4, result[:form_meta][:dependencies][:group_id].count) - assert(result[:form_meta][:dependencies][:group_id]['']) - assert(result[:form_meta][:dependencies][:group_id][''][:owner_id]) - assert_equal([], result[:form_meta][:dependencies][:group_id][''][:owner_id]) - assert(result[:form_meta][:dependencies][:group_id][group1.id]) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id]) - assert_equal(4, result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].count) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].include?(agent1.id)) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].include?(agent2.id)) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].include?(agent3.id)) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].include?(agent4.id)) - assert(result[:form_meta][:dependencies][:group_id][group2.id]) - assert(result[:form_meta][:dependencies][:group_id][group2.id][:owner_id]) - assert_equal(1, result[:form_meta][:dependencies][:group_id][group2.id][:owner_id].count) - assert(result[:form_meta][:dependencies][:group_id][group2.id][:owner_id].include?(agent3.id)) - - result = Ticket::ScreenOptions.attributes_to_change( - ticket_id: ticket2.id, - current_user: agent2, - ) - - assert(result[:form_meta]) - assert(result[:form_meta][:filter]) - assert(result[:form_meta][:filter][:state_id]) - assert_equal([ - Ticket::State.lookup(name: 'new').id, - Ticket::State.lookup(name: 'open').id, - Ticket::State.lookup(name: 'pending reminder').id, - Ticket::State.lookup(name: 'closed').id, - Ticket::State.lookup(name: 'pending close').id, - ], result[:form_meta][:filter][:state_id].sort) - assert(result[:form_meta][:filter][:priority_id]) - assert_equal([ - Ticket::Priority.lookup(name: '1 low').id, - Ticket::Priority.lookup(name: '2 normal').id, - Ticket::Priority.lookup(name: '3 high').id, - ], result[:form_meta][:filter][:priority_id].sort) - assert(result[:form_meta][:filter][:type_id]) - assert_equal([ - Ticket::Article::Type.lookup(name: 'phone').id, - Ticket::Article::Type.lookup(name: 'note').id, - ], result[:form_meta][:filter][:type_id].sort) - assert(result[:form_meta][:filter][:group_id]) - assert_equal([group1.id, group2.id, group3.id], result[:form_meta][:filter][:group_id].sort) - assert(result[:form_meta][:dependencies]) - assert(result[:form_meta][:dependencies][:group_id]) - assert_equal(4, result[:form_meta][:dependencies][:group_id].count) - assert(result[:form_meta][:dependencies][:group_id]['']) - assert(result[:form_meta][:dependencies][:group_id][''][:owner_id]) - assert_equal([], result[:form_meta][:dependencies][:group_id][''][:owner_id]) - assert(result[:form_meta][:dependencies][:group_id][group1.id]) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id]) - assert_equal(4, result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].count) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].include?(agent1.id)) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].include?(agent2.id)) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].include?(agent3.id)) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].include?(agent4.id)) - assert(result[:form_meta][:dependencies][:group_id][group2.id]) - assert(result[:form_meta][:dependencies][:group_id][group2.id][:owner_id]) - assert_equal(1, result[:form_meta][:dependencies][:group_id][group2.id][:owner_id].count) - assert(result[:form_meta][:dependencies][:group_id][group2.id][:owner_id].include?(agent3.id)) - - result = Ticket::ScreenOptions.attributes_to_change( - ticket_id: ticket1.id, - current_user: agent3, - ) - - assert(result[:form_meta]) - assert(result[:form_meta][:filter]) - assert(result[:form_meta][:filter][:state_id]) - assert_equal([ - Ticket::State.lookup(name: 'new').id, - Ticket::State.lookup(name: 'open').id, - Ticket::State.lookup(name: 'pending reminder').id, - Ticket::State.lookup(name: 'closed').id, - Ticket::State.lookup(name: 'pending close').id, - ], result[:form_meta][:filter][:state_id].sort) - assert(result[:form_meta][:filter][:priority_id]) - assert_equal([ - Ticket::Priority.lookup(name: '1 low').id, - Ticket::Priority.lookup(name: '2 normal').id, - Ticket::Priority.lookup(name: '3 high').id, - ], result[:form_meta][:filter][:priority_id].sort) - assert(result[:form_meta][:filter][:type_id]) - assert_equal([ - Ticket::Article::Type.lookup(name: 'email').id, - Ticket::Article::Type.lookup(name: 'phone').id, - Ticket::Article::Type.lookup(name: 'note').id, - ], result[:form_meta][:filter][:type_id].sort) - assert(result[:form_meta][:filter][:group_id]) - assert_equal([group1.id, group2.id], result[:form_meta][:filter][:group_id].sort) - assert(result[:form_meta][:dependencies]) - assert(result[:form_meta][:dependencies][:group_id]) - assert_equal(3, result[:form_meta][:dependencies][:group_id].count) - assert(result[:form_meta][:dependencies][:group_id]['']) - assert(result[:form_meta][:dependencies][:group_id][''][:owner_id]) - assert_equal([], result[:form_meta][:dependencies][:group_id][''][:owner_id]) - assert(result[:form_meta][:dependencies][:group_id][group1.id]) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id]) - assert_equal(4, result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].count) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].include?(agent1.id)) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].include?(agent2.id)) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].include?(agent3.id)) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].include?(agent4.id)) - assert_equal(1, result[:form_meta][:dependencies][:group_id][group2.id][:owner_id].count) - assert(result[:form_meta][:dependencies][:group_id][group2.id][:owner_id].include?(agent3.id)) - - result = Ticket::ScreenOptions.attributes_to_change( - ticket_id: ticket2.id, - current_user: agent3, - ) - - assert(result[:form_meta]) - assert(result[:form_meta][:filter]) - assert(result[:form_meta][:filter][:state_id]) - assert_equal([ - Ticket::State.lookup(name: 'new').id, - Ticket::State.lookup(name: 'open').id, - Ticket::State.lookup(name: 'pending reminder').id, - Ticket::State.lookup(name: 'closed').id, - Ticket::State.lookup(name: 'pending close').id, - ], result[:form_meta][:filter][:state_id].sort) - assert(result[:form_meta][:filter][:priority_id]) - assert_equal([ - Ticket::Priority.lookup(name: '1 low').id, - Ticket::Priority.lookup(name: '2 normal').id, - Ticket::Priority.lookup(name: '3 high').id, - ], result[:form_meta][:filter][:priority_id].sort) - assert(result[:form_meta][:filter][:type_id]) - assert_equal([ - Ticket::Article::Type.lookup(name: 'phone').id, - Ticket::Article::Type.lookup(name: 'note').id, - ], result[:form_meta][:filter][:type_id].sort) - assert(result[:form_meta][:filter][:group_id]) - assert_equal([group1.id, group2.id], result[:form_meta][:filter][:group_id].sort) - assert(result[:form_meta][:dependencies]) - assert(result[:form_meta][:dependencies][:group_id]) - assert_equal(3, result[:form_meta][:dependencies][:group_id].count) - assert(result[:form_meta][:dependencies][:group_id]['']) - assert(result[:form_meta][:dependencies][:group_id][''][:owner_id]) - assert_equal([], result[:form_meta][:dependencies][:group_id][''][:owner_id]) - assert(result[:form_meta][:dependencies][:group_id][group1.id]) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id]) - assert_equal(4, result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].count) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].include?(agent1.id)) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].include?(agent2.id)) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].include?(agent3.id)) - assert(result[:form_meta][:dependencies][:group_id][group1.id][:owner_id].include?(agent4.id)) - assert_equal(1, result[:form_meta][:dependencies][:group_id][group2.id][:owner_id].count) - assert(result[:form_meta][:dependencies][:group_id][group2.id][:owner_id].include?(agent3.id)) - - end - -end