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)