From d5a28907451d9d541a7b503b2db5e79117c358e0 Mon Sep 17 00:00:00 2001 From: Rolf Schmidt Date: Tue, 18 Jan 2022 15:37:16 +0100 Subject: [PATCH] Fixes #3889 - JS error on editing textareas admin object manager attributes. --- .../_ui_element/core_workflow_perform.coffee | 4 +- .../object_manager_attribute.coffee | 23 ++ .../app/views/generic/textarea.jst.eco | 2 +- .../object_manager/attribute/textarea.jst.eco | 4 + app/models/object_manager/attribute.rb | 86 ++++---- i18n/zammad.pot | 4 + spec/factories/object_manager_attribute.rb | 2 +- .../attribute/set_defaults_spec.rb | 1 + spec/models/object_manager/attribute_spec.rb | 171 +++++++++++++++ .../system/examples/core_workflow_examples.rb | 200 +++++++++++++++++- 10 files changed, 456 insertions(+), 41 deletions(-) create mode 100644 app/assets/javascripts/app/views/object_manager/attribute/textarea.jst.eco 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 index 71c0c7684..8d114cac8 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/core_workflow_perform.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/core_workflow_perform.coffee @@ -42,7 +42,7 @@ class App.UiElement.core_workflow_perform extends App.UiElement.ApplicationSelec '^date': ['show', 'hide', 'remove', 'set_mandatory', 'set_optional', 'set_readonly', 'unset_readonly'] '^select$': ['show', 'hide', 'remove', 'set_mandatory', 'set_optional', 'set_readonly', 'unset_readonly', 'add_option', 'remove_option', 'set_fixed_to', 'select', 'auto_select'] '^tree_select$': ['show', 'hide', 'remove', 'set_mandatory', 'set_optional', 'set_readonly', 'unset_readonly', 'add_option', 'remove_option', 'set_fixed_to', 'select', 'auto_select'] - '^input$': ['show', 'hide', 'remove', 'set_mandatory', 'set_optional', 'set_readonly', 'unset_readonly', 'fill_in', 'fill_in_empty'] + '^(input|textarea)$': ['show', 'hide', 'remove', 'set_mandatory', 'set_optional', 'set_readonly', 'unset_readonly', 'fill_in', 'fill_in_empty'] operatorsName = '_id$': ['show', 'hide', 'set_mandatory', 'set_optional', 'set_readonly', 'unset_readonly', 'add_option', 'remove_option', 'set_fixed_to', 'select', 'auto_select'] @@ -64,7 +64,7 @@ class App.UiElement.core_workflow_perform extends App.UiElement.ApplicationSelec continue for row in App[groupMeta.model].configure_attributes - continue if !_.contains(['input', 'select', 'integer', 'boolean', 'tree_select', 'date', 'datetime'], row.tag) + continue if !_.contains(['input', 'textarea', 'select', 'integer', 'boolean', 'tree_select', 'date', 'datetime'], row.tag) continue if _.contains(['created_at', 'updated_at'], row.name) continue if groupKey is 'ticket' && _.contains(['number', 'organization_id', 'title', 'escalation_at', 'first_response_escalation_at', 'update_escalation_at', 'close_escalation_at', 'last_contact_at', 'last_contact_agent_at', 'last_contact_customer_at', 'first_response_at', 'close_at'], row.name) diff --git a/app/assets/javascripts/app/controllers/_ui_element/object_manager_attribute.coffee b/app/assets/javascripts/app/controllers/_ui_element/object_manager_attribute.coffee index 17aa92041..237f5f064 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/object_manager_attribute.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/object_manager_attribute.coffee @@ -33,6 +33,7 @@ class App.UiElement.object_manager_attribute extends App.UiElement.ApplicationUi datetime: __('Datetime') date: __('Date') input: __('Text') + textarea: __('Textarea') select: __('Select') tree_select: __('Tree Select') boolean: __('Boolean') @@ -224,6 +225,28 @@ class App.UiElement.object_manager_attribute extends App.UiElement.ApplicationUi ) item.find("select[name='data_option::type']").trigger('change') + @textarea: (item, localParams, params) -> + configureAttributes = [ + { name: 'data_option::default', display: __('Default'), tag: 'input', type: 'text', null: true, default: '' }, + ] + inputDefault = new App.ControllerForm( + model: + configure_attributes: configureAttributes + noFieldset: true + params: params + ) + configureAttributes = [ + { name: 'data_option::maxlength', display: __('Maxlength'), tag: 'integer', null: false, default: 500 }, + ] + inputMaxlength = new App.ControllerForm( + model: + configure_attributes: configureAttributes + noFieldset: true + params: params + ) + item.find('.js-inputDefault').html(inputDefault.form) + item.find('.js-inputMaxlength').html(inputMaxlength.form) + @datetime: (item, localParams, params) -> configureAttributes = [ { name: 'data_option::future', display: __('Allow future'), tag: 'boolean', null: false, default: true }, diff --git a/app/assets/javascripts/app/views/generic/textarea.jst.eco b/app/assets/javascripts/app/views/generic/textarea.jst.eco index 4c375e769..c86e7eb0f 100644 --- a/app/assets/javascripts/app/views/generic/textarea.jst.eco +++ b/app/assets/javascripts/app/views/generic/textarea.jst.eco @@ -1 +1 @@ - + diff --git a/app/assets/javascripts/app/views/object_manager/attribute/textarea.jst.eco b/app/assets/javascripts/app/views/object_manager/attribute/textarea.jst.eco new file mode 100644 index 000000000..f21309214 --- /dev/null +++ b/app/assets/javascripts/app/views/object_manager/attribute/textarea.jst.eco @@ -0,0 +1,4 @@ +
+
+
+
diff --git a/app/models/object_manager/attribute.rb b/app/models/object_manager/attribute.rb index e905c799c..7bd209fa9 100644 --- a/app/models/object_manager/attribute.rb +++ b/app/models/object_manager/attribute.rb @@ -910,49 +910,63 @@ is certain attribute used by triggers, overviews or schedulers send("#{local_data_attr}=", val) end + def data_option_maxlength_check + [{ failed: !local_data_option[:maxlength].to_s.match?(%r{^\d+$}), message: 'must have integer for :maxlength' }] + end + + def data_option_type_check + [{ failed: %w[text password tel fax email url].exclude?(local_data_option[:type]), message: 'must have one of text/password/tel/fax/email/url for :type' }] + end + + def data_option_min_max_check + min = local_data_option[:min] + max = local_data_option[:max] + + [ + { failed: !VALIDATE_INTEGER_REGEXP.match?(min.to_s), message: 'must have integer for :min' }, + { failed: !VALIDATE_INTEGER_REGEXP.match?(max.to_s), message: 'must have integer for :max' }, + { failed: !(min.is_a?(Integer) && min >= VALIDATE_INTEGER_MIN), message: 'min must be higher than -2147483648' }, + { failed: !(min.is_a?(Integer) && min <= VALIDATE_INTEGER_MAX), message: 'min must be lower than 2147483648' }, + { failed: !(max.is_a?(Integer) && max >= VALIDATE_INTEGER_MIN), message: 'max must be higher than -2147483648' }, + { failed: !(max.is_a?(Integer) && max <= VALIDATE_INTEGER_MAX), message: 'max must be lower than 2147483648' }, + { failed: !(max.is_a?(Integer) && min.is_a?(Integer) && min <= max), message: 'min must be lower than max' } + ] + end + + def data_option_default_check + [{ failed: !local_data_option.key?(:default), message: 'must have value for :default' }] + end + + def data_option_relation_check + [{ failed: local_data_option[:options].nil? && local_data_option[:relation].nil?, message: 'must have non-nil value for either :options or :relation' }] + end + + def data_option_nil_check + [{ failed: local_data_option[:options].nil?, message: 'must have non-nil value for :options' }] + end + + def data_option_future_check + [{ failed: local_data_option[:future].nil?, message: 'must have boolean value for :future' }] + end + + def data_option_past_check + [{ failed: local_data_option[:past].nil?, message: 'must have boolean value for :past' }] + end + def data_option_validations case data_type when 'input' - [{ failed: %w[text password tel fax email url].exclude?(local_data_option[:type]), - message: 'must have one of text/password/tel/fax/email/url for :type' }, - { failed: !local_data_option[:maxlength].to_s.match?(%r{^\d+$}), - message: 'must have integer for :maxlength' }] - when 'richtext' - [{ failed: !local_data_option[:maxlength].to_s.match?(%r{^\d+$}), - message: 'must have integer for :maxlength' }] + data_option_type_check + data_option_maxlength_check + when %r{^(textarea|richtext)$} + data_option_maxlength_check when 'integer' - min = local_data_option[:min] - max = local_data_option[:max] - - [{ failed: !VALIDATE_INTEGER_REGEXP.match?(min.to_s), - message: 'must have integer for :min' }, - { failed: !VALIDATE_INTEGER_REGEXP.match?(max.to_s), - message: 'must have integer for :max' }, - { failed: !(min.is_a?(Integer) && min >= VALIDATE_INTEGER_MIN), - message: 'min must be higher than -2147483648' }, - { failed: !(min.is_a?(Integer) && min <= VALIDATE_INTEGER_MAX), - message: 'min must be lower than 2147483648' }, - { failed: !(max.is_a?(Integer) && max >= VALIDATE_INTEGER_MIN), - message: 'max must be higher than -2147483648' }, - { failed: !(max.is_a?(Integer) && max <= VALIDATE_INTEGER_MAX), - message: 'max must be lower than 2147483648' }, - { failed: !(max.is_a?(Integer) && min.is_a?(Integer) && min <= max), - message: 'min must be lower than max' }] + data_option_min_max_check when %r{^((tree_)?select|checkbox)$} - [{ failed: !local_data_option.key?(:default), - message: 'must have value for :default' }, - { failed: local_data_option[:options].nil? && local_data_option[:relation].nil?, - message: 'must have non-nil value for either :options or :relation' }] + data_option_default_check + data_option_relation_check when 'boolean' - [{ failed: !local_data_option.key?(:default), - message: 'must have boolean/undefined value for :default' }, - { failed: local_data_option[:options].nil?, - message: 'must have non-nil value for :options' }] + data_option_default_check + data_option_nil_check when 'datetime' - [{ failed: local_data_option[:future].nil?, - message: 'must have boolean value for :future' }, - { failed: local_data_option[:past].nil?, - message: 'must have boolean value for :past' }] + data_option_future_check + data_option_past_check else [] end diff --git a/i18n/zammad.pot b/i18n/zammad.pot index c7e0e9609..099d34e50 100644 --- a/i18n/zammad.pot +++ b/i18n/zammad.pot @@ -8721,6 +8721,10 @@ msgstr "" msgid "TextModule" msgstr "" +#: app/assets/javascripts/app/controllers/_ui_element/object_manager_attribute.coffee +msgid "Textarea" +msgstr "" + #: app/assets/javascripts/app/views/channel/form.jst.eco msgid "Thank you for your inquiry (#%s)! We'll contact you as soon as possible." msgstr "" diff --git a/spec/factories/object_manager_attribute.rb b/spec/factories/object_manager_attribute.rb index 8f674db27..f4e5f58fc 100644 --- a/spec/factories/object_manager_attribute.rb +++ b/spec/factories/object_manager_attribute.rb @@ -65,7 +65,7 @@ FactoryBot.define do 'maxlength' => 255, 'null' => true, 'translate' => false, - 'default' => default || '', + 'default' => default, 'options' => {}, 'relation' => '', } diff --git a/spec/models/object_manager/attribute/set_defaults_spec.rb b/spec/models/object_manager/attribute/set_defaults_spec.rb index fc475ccd9..fdfc62c8d 100644 --- a/spec/models/object_manager/attribute/set_defaults_spec.rb +++ b/spec/models/object_manager/attribute/set_defaults_spec.rb @@ -3,6 +3,7 @@ require 'rails_helper' DEFAULT_VALUES = { + textarea: 'rspec', text: 'rspec', boolean: true, date: 1, diff --git a/spec/models/object_manager/attribute_spec.rb b/spec/models/object_manager/attribute_spec.rb index 368c5f883..806650bcc 100644 --- a/spec/models/object_manager/attribute_spec.rb +++ b/spec/models/object_manager/attribute_spec.rb @@ -172,4 +172,175 @@ RSpec.describe ObjectManager::Attribute, type: :model do end end end + + describe '#data_option_validations' do + context 'when maxlength is checked for non-integers' do + shared_examples 'tests the exception on invalid maxlength values' do |type| + context "when type '#{type}'" do + subject(:attr) { described_class.new(data_type: type, data_option: { maxlength: 'brbrbr' }) } + + it 'does throw an exception' do + expect { attr.save! }.to raise_error(ActiveRecord::RecordInvalid, %r{Data option must have integer for :maxlength}) + end + end + end + + include_examples 'tests the exception on invalid maxlength values', 'input' + include_examples 'tests the exception on invalid maxlength values', 'textarea' + include_examples 'tests the exception on invalid maxlength values', 'richtext' + end + + context 'when type is checked' do + shared_examples 'tests the exception on invalid types' do |type| + context "when type '#{type}'" do + subject(:attr) { described_class.new(data_type: type, data_option: { type: 'brbrbr' }) } + + it 'does throw an exception' do + expect { attr.save! }.to raise_error(ActiveRecord::RecordInvalid, %r{must have one of text/password/tel/fax/email/url for :type}) + end + end + end + + include_examples 'tests the exception on invalid types', 'input' + end + + context 'when min max values are checked' do + shared_examples 'tests the exception on invalid min max values' do |type| + context "when type '#{type}'" do + context 'when no integer for min' do + subject(:attr) { described_class.new(data_type: type, data_option: { min: 'brbrbr' }) } + + it 'does throw an exception' do + expect { attr.save! }.to raise_error(ActiveRecord::RecordInvalid, %r{must have integer for :min}) + end + end + + context 'when no integer for max' do + subject(:attr) { described_class.new(data_type: type, data_option: { max: 'brbrbr' }) } + + it 'does throw an exception' do + expect { attr.save! }.to raise_error(ActiveRecord::RecordInvalid, %r{must have integer for :max}) + end + end + + context 'when high integer for min' do + subject(:attr) { described_class.new(data_type: type, data_option: { min: 999_999_999_999 }) } + + it 'does throw an exception' do + expect { attr.save! }.to raise_error(ActiveRecord::RecordInvalid, %r{min must be lower than 2147483648}) + end + end + + context 'when high integer for max' do + subject(:attr) { described_class.new(data_type: type, data_option: { max: 999_999_999_999 }) } + + it 'does throw an exception' do + expect { attr.save! }.to raise_error(ActiveRecord::RecordInvalid, %r{max must be lower than 2147483648}) + end + end + + context 'when negative high integer for min' do + subject(:attr) { described_class.new(data_type: type, data_option: { min: -999_999_999_999 }) } + + it 'does throw an exception' do + expect { attr.save! }.to raise_error(ActiveRecord::RecordInvalid, %r{min must be higher than -2147483648}) + end + end + + context 'when negative high integer for max' do + subject(:attr) { described_class.new(data_type: type, data_option: { max: -999_999_999_999 }) } + + it 'does throw an exception' do + expect { attr.save! }.to raise_error(ActiveRecord::RecordInvalid, %r{max must be higher than -2147483648}) + end + end + + context 'when min is greater than max' do + subject(:attr) { described_class.new(data_type: type, data_option: { min: 5, max: 2 }) } + + it 'does throw an exception' do + expect { attr.save! }.to raise_error(ActiveRecord::RecordInvalid, %r{min must be lower than max}) + end + end + end + end + + include_examples 'tests the exception on invalid min max values', 'integer' + end + + context 'when default is checked' do + shared_examples 'tests the exception on missing default' do |type| + context "when type '#{type}'" do + subject(:attr) { described_class.new(data_type: type, data_option: {}) } + + it 'does throw an exception' do + expect { attr.save! }.to raise_error(ActiveRecord::RecordInvalid, %r{must have value for :default}) + end + end + end + + include_examples 'tests the exception on missing default', 'select' + include_examples 'tests the exception on missing default', 'tree_select' + include_examples 'tests the exception on missing default', 'checkbox' + include_examples 'tests the exception on missing default', 'boolean' + end + + context 'when relation is checked' do + shared_examples 'tests the exception on missing relation' do |type| + context "when type '#{type}'" do + subject(:attr) { described_class.new(data_type: type, data_option: {}) } + + it 'does throw an exception' do + expect { attr.save! }.to raise_error(ActiveRecord::RecordInvalid, %r{must have non-nil value for either :options or :relation}) + end + end + end + + include_examples 'tests the exception on missing relation', 'select' + include_examples 'tests the exception on missing relation', 'tree_select' + include_examples 'tests the exception on missing relation', 'checkbox' + end + + context 'when nil options are checked' do + shared_examples 'tests the exception on missing nil options' do |type| + context "when type '#{type}'" do + subject(:attr) { described_class.new(data_type: type, data_option: {}) } + + it 'does throw an exception' do + expect { attr.save! }.to raise_error(ActiveRecord::RecordInvalid, %r{must have non-nil value for :options}) + end + end + end + + include_examples 'tests the exception on missing nil options', 'boolean' + end + + context 'when future is checked' do + shared_examples 'tests the exception on missing future' do |type| + context "when type '#{type}'" do + subject(:attr) { described_class.new(data_type: type, data_option: {}) } + + it 'does throw an exception' do + expect { attr.save! }.to raise_error(ActiveRecord::RecordInvalid, %r{must have boolean value for :future}) + end + end + end + + include_examples 'tests the exception on missing future', 'datetime' + end + + context 'when past is checked' do + shared_examples 'tests the exception on missing past' do |type| + context "when type '#{type}'" do + subject(:attr) { described_class.new(data_type: type, data_option: {}) } + + it 'does throw an exception' do + expect { attr.save! }.to raise_error(ActiveRecord::RecordInvalid, %r{must have boolean value for :past}) + end + end + end + + include_examples 'tests the exception on missing past', 'datetime' + end + end end diff --git a/spec/system/examples/core_workflow_examples.rb b/spec/system/examples/core_workflow_examples.rb index a9e9f47b4..a04049404 100644 --- a/spec/system/examples/core_workflow_examples.rb +++ b/spec/system/examples/core_workflow_examples.rb @@ -25,7 +25,7 @@ RSpec.shared_examples 'core workflow' do } end - describe 'modify text attribute', authenticated_as: :authenticate, db_strategy: :reset do + describe 'modify input 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 @@ -223,6 +223,204 @@ RSpec.shared_examples 'core workflow' do end end + describe 'modify textarea attribute', authenticated_as: :authenticate, db_strategy: :reset do + def authenticate + create(:object_manager_attribute_textarea, 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("textarea[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 - unset_readonly' do + before do + create(:core_workflow, + object: object_name, + perform: { + "#{object_name.downcase}.#{field_name}": { + operator: 'unset_readonly', + unset_readonly: 'true' + }, + }) + end + + it 'does perform' do + before_it.call + expect(page).to have_no_selector("div[data-attribute-name='#{field_name}'].is-readonly", wait: 10) + end + end + + describe 'action - set_readonly' do + before do + create(:core_workflow, + object: object_name, + perform: { + "#{object_name.downcase}.#{field_name}": { + operator: 'set_readonly', + set_readonly: 'true' + }, + }) + end + + it 'does perform' do + before_it.call + expect(page).to have_selector("div[data-attribute-name='#{field_name}'].is-readonly", 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)