Fixes #3889 - JS error on editing textareas admin object manager attributes.

This commit is contained in:
Rolf Schmidt 2022-01-18 15:37:16 +01:00
parent 4186b0faaf
commit d5a2890745
10 changed files with 456 additions and 41 deletions

View file

@ -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'] '^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'] '^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'] '^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 = operatorsName =
'_id$': ['show', 'hide', 'set_mandatory', 'set_optional', 'set_readonly', 'unset_readonly', 'add_option', 'remove_option', 'set_fixed_to', 'select', 'auto_select'] '_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 continue
for row in App[groupMeta.model].configure_attributes 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 _.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) 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)

View file

@ -33,6 +33,7 @@ class App.UiElement.object_manager_attribute extends App.UiElement.ApplicationUi
datetime: __('Datetime') datetime: __('Datetime')
date: __('Date') date: __('Date')
input: __('Text') input: __('Text')
textarea: __('Textarea')
select: __('Select') select: __('Select')
tree_select: __('Tree Select') tree_select: __('Tree Select')
boolean: __('Boolean') 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') 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) -> @datetime: (item, localParams, params) ->
configureAttributes = [ configureAttributes = [
{ name: 'data_option::future', display: __('Allow future'), tag: 'boolean', null: false, default: true }, { name: 'data_option::future', display: __('Allow future'), tag: 'boolean', null: false, default: true },

View file

@ -1 +1 @@
<textarea id="<%= @attribute.id %>" name="<%= @attribute.name %>" class="form-control <%= @attribute.class %>" rows="<%= @attribute.rows %>" <%= @attribute.required %> <%= @attribute.autofocus %> <% if @attribute.disabled: %> disabled<% end %>><%= @attribute.value %></textarea> <textarea id="<%= @attribute.id %>" name="<%= @attribute.name %>" class="form-control <%= @attribute.class %>" rows="<%= @attribute.rows %>"<% if @attribute.maxlength: %> maxlength="<%= @attribute.maxlength %>"<% end %> <%= @attribute.required %> <%= @attribute.autofocus %> <% if @attribute.disabled: %> disabled<% end %>><%= @attribute.value %></textarea>

View file

@ -0,0 +1,4 @@
<div>
<div class="js-inputDefault"></div>
<div class="js-inputMaxlength"></div>
</div>

View file

@ -910,49 +910,63 @@ is certain attribute used by triggers, overviews or schedulers
send("#{local_data_attr}=", val) send("#{local_data_attr}=", val)
end 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 def data_option_validations
case data_type case data_type
when 'input' when 'input'
[{ failed: %w[text password tel fax email url].exclude?(local_data_option[:type]), data_option_type_check + data_option_maxlength_check
message: 'must have one of text/password/tel/fax/email/url for :type' }, when %r{^(textarea|richtext)$}
{ failed: !local_data_option[:maxlength].to_s.match?(%r{^\d+$}), data_option_maxlength_check
message: 'must have integer for :maxlength' }]
when 'richtext'
[{ failed: !local_data_option[:maxlength].to_s.match?(%r{^\d+$}),
message: 'must have integer for :maxlength' }]
when 'integer' when 'integer'
min = local_data_option[:min] data_option_min_max_check
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' }]
when %r{^((tree_)?select|checkbox)$} when %r{^((tree_)?select|checkbox)$}
[{ failed: !local_data_option.key?(:default), data_option_default_check + data_option_relation_check
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' }]
when 'boolean' when 'boolean'
[{ failed: !local_data_option.key?(:default), data_option_default_check + data_option_nil_check
message: 'must have boolean/undefined value for :default' },
{ failed: local_data_option[:options].nil?,
message: 'must have non-nil value for :options' }]
when 'datetime' when 'datetime'
[{ failed: local_data_option[:future].nil?, data_option_future_check + data_option_past_check
message: 'must have boolean value for :future' },
{ failed: local_data_option[:past].nil?,
message: 'must have boolean value for :past' }]
else else
[] []
end end

View file

@ -8721,6 +8721,10 @@ msgstr ""
msgid "TextModule" msgid "TextModule"
msgstr "" msgstr ""
#: app/assets/javascripts/app/controllers/_ui_element/object_manager_attribute.coffee
msgid "Textarea"
msgstr ""
#: app/assets/javascripts/app/views/channel/form.jst.eco #: app/assets/javascripts/app/views/channel/form.jst.eco
msgid "Thank you for your inquiry (#%s)! We'll contact you as soon as possible." msgid "Thank you for your inquiry (#%s)! We'll contact you as soon as possible."
msgstr "" msgstr ""

View file

@ -65,7 +65,7 @@ FactoryBot.define do
'maxlength' => 255, 'maxlength' => 255,
'null' => true, 'null' => true,
'translate' => false, 'translate' => false,
'default' => default || '', 'default' => default,
'options' => {}, 'options' => {},
'relation' => '', 'relation' => '',
} }

View file

@ -3,6 +3,7 @@
require 'rails_helper' require 'rails_helper'
DEFAULT_VALUES = { DEFAULT_VALUES = {
textarea: 'rspec',
text: 'rspec', text: 'rspec',
boolean: true, boolean: true,
date: 1, date: 1,

View file

@ -172,4 +172,175 @@ RSpec.describe ObjectManager::Attribute, type: :model do
end end
end 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 end

View file

@ -25,7 +25,7 @@ RSpec.shared_examples 'core workflow' do
} }
end 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 def authenticate
create(:object_manager_attribute_text, object_name: object_name, name: field_name, display: field_name, screens: screens) create(:object_manager_attribute_text, object_name: object_name, name: field_name, display: field_name, screens: screens)
ObjectManager::Attribute.migration_execute ObjectManager::Attribute.migration_execute
@ -223,6 +223,204 @@ RSpec.shared_examples 'core workflow' do
end end
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 describe 'modify select attribute', authenticated_as: :authenticate, db_strategy: :reset do
def authenticate def authenticate
create(:object_manager_attribute_select, object_name: object_name, name: field_name, display: field_name, screens: screens) create(:object_manager_attribute_select, object_name: object_name, name: field_name, display: field_name, screens: screens)