diff --git a/.rubocop.yml b/.rubocop.yml index 49bf75ca1..513697910 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -260,6 +260,7 @@ Metrics/ClassLength: - 'packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/generator_action_field_widget.rb' - 'packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb' - 'packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/generator_action.rb' + - 'packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/action/action_collection_decorator.rb' Style/OpenStructUse: Exclude: diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/forest_value_converter.rb b/packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/forest_value_converter.rb index f2c08be63..3c4dbf4b2 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/forest_value_converter.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/forest_value_converter.rb @@ -58,7 +58,7 @@ def self.make_form_data_from_fields(datasource, fields) def self.make_form_data(datasource, raw_data, fields) data = {} raw_data.each do |key, value| - field = fields.find { |f| f.label == key } + field = fields.find { |f| f.id == key } # Skip fields from the default form next if Schema::GeneratorAction::DEFAULT_FIELDS.map { |f| f[:field] }.include?(key) diff --git a/packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/generator_action.rb b/packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/generator_action.rb index 0011798a5..4e2a50a97 100644 --- a/packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/generator_action.rb +++ b/packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/generator_action.rb @@ -7,6 +7,7 @@ class GeneratorAction DEFAULT_FIELDS = [ { field: 'Loading...', + label: 'Loading...', type: 'String', isReadOnly: true, defaultValue: 'Form is loading', @@ -79,7 +80,8 @@ def self.build_field_schema(datasource, field) description: field.description, isRequired: field.is_required, isReadOnly: field.is_read_only, - field: field.label, + field: field.id, + label: field.label, value: ForestValueConverter.value_to_forest(field), widgetEdit: GeneratorActionFieldWidget.build_widget_options(field) } @@ -146,7 +148,7 @@ def self.extract_fields_and_layout(form) else fields << element # frontend rule - layout << Actions::ActionLayoutElement::InputElement.new(component: 'Input', field_id: element.label) + layout << Actions::ActionLayoutElement::InputElement.new(component: 'Input', field_id: element.id) end end diff --git a/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/schema/generator_action_spec.rb b/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/schema/generator_action_spec.rb index d49794688..3d2b30bcb 100644 --- a/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/schema/generator_action_spec.rb +++ b/packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/schema/generator_action_spec.rb @@ -50,6 +50,7 @@ module Schema }, get_form: [ ActionField.new( + id: 'label', label: 'label', description: 'email', type: 'String', @@ -74,7 +75,16 @@ module Schema httpMethod: 'POST', redirect: nil, download: false, - fields: [{ description: 'email', isRequired: true, isReadOnly: false, field: 'label', widgetEdit: nil, type: 'String', defaultValue: '' }], + fields: [{ + description: 'email', + isRequired: true, + isReadOnly: false, + field: 'label', + widgetEdit: nil, + type: 'String', + defaultValue: '', + label: 'label' + }], hooks: { load: false, change: ['changeHook'] } } ) @@ -213,7 +223,7 @@ module Schema } }, get_form: [ - ActionField.new(label: 'label', type: 'String'), + ActionField.new(id: 'label', label: 'label', type: 'String'), ActionLayoutElement::SeparatorElement.new ] ) @@ -237,6 +247,7 @@ module Schema defaultValue: nil, description: nil, field: 'label', + label: 'label', isReadOnly: false, isRequired: false, type: 'String', @@ -264,7 +275,7 @@ module Schema } }, get_form: [ - ActionField.new(label: 'label', type: 'String'), + ActionField.new(id: 'label_id', label: 'label', type: 'String'), ActionLayoutElement::HtmlBlockElement.new(content: '
foo
') ] ) @@ -287,7 +298,8 @@ module Schema { defaultValue: nil, description: nil, - field: 'label', + field: 'label_id', + label: 'label', isReadOnly: false, isRequired: false, type: 'String', @@ -317,8 +329,8 @@ module Schema get_form: [ ActionLayoutElement::RowElement.new( fields: [ - ActionField.new(label: 'label', type: 'String'), - ActionField.new(label: 'amount', type: 'String') + ActionField.new(id: 'label_id', label: 'label', type: 'String'), + ActionField.new(id: 'amount_id', label: 'amount', type: 'String') ] ) ] @@ -340,7 +352,8 @@ module Schema download: false, fields: [ { - field: 'label', + field: 'label_id', + label: 'label', type: 'String', description: nil, isRequired: false, @@ -349,7 +362,8 @@ module Schema defaultValue: nil }, { - field: 'amount', + label: 'amount', + field: 'amount_id', type: 'String', description: nil, isRequired: false, @@ -378,7 +392,7 @@ module Schema } }, get_form: [ - ActionField.new(label: 'label', type: 'String'), + ActionField.new(id: 'label_id', label: 'label', type: 'String'), ActionLayoutElement::SeparatorElement.new ] ) diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/action/action_collection_decorator.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/action/action_collection_decorator.rb index d70206579..24a269fe0 100644 --- a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/action/action_collection_decorator.rb +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/action/action_collection_decorator.rb @@ -11,6 +11,7 @@ def initialize(child_collection, datasource) def add_action(name, action) action.build_elements + action.validate_fields_ids @actions[name] = action mark_schema_as_dirty @@ -43,24 +44,14 @@ def get_form(caller, name, data = nil, filter = nil, metas = {}) if metas[:search_field] # in the case of a search hook, # we don't want to rebuild all the fields. only the one searched - dynamic_fields = dynamic_fields.select { |field| field.label == metas[:search_field] } + dynamic_fields = dynamic_fields.select { |field| field.id == metas[:search_field] } end dynamic_fields = drop_defaults(context, dynamic_fields, form_values) dynamic_fields = drop_ifs(context, dynamic_fields) unless metas[:include_hidden_fields] fields = drop_deferred(context, metas[:search_values], dynamic_fields).compact - fields.each do |field| - next if field.type == 'Layout' - - if field.value.nil? - # customer did not define a handler to rewrite the previous value => reuse current one. - field.value = form_values[field.label] - end - - # fields that were accessed through the context.get_form_value(x) getter should be watched. - field.watch_changes = used.include?(field.label) - end + set_watch_changes_on_fields(form_values, used, fields) fields end @@ -73,9 +64,34 @@ def refine_schema(sub_schema) private + def set_watch_changes_on_fields(form_values, used, fields) + fields.each do |field| + if field.type != 'Layout' + + if field.value.nil? + # customer did not define a handler to rewrite the previous value => reuse current one. + field.value = form_values[field.id] + end + + # fields that were accessed through the context.get_form_value(x) getter should be watched. + field.watch_changes = used.include?(field.id) + elsif field.component == 'Row' + set_watch_changes_on_fields(form_values, used, field.fields) + end + end + end + + def execute_on_sub_fields(field) + return unless field.type == 'Layout' && field.component == 'Row' + + field.fields = yield(field.fields) + end + def drop_defaults(context, fields, data) fields.map do |field| if field.type == 'Layout' + execute_on_sub_fields(field) { |sub_fields| drop_defaults(context, sub_fields, data) } + field else drop_default(context, field, data) @@ -84,14 +100,25 @@ def drop_defaults(context, fields, data) end def drop_default(context, field, data) - data[field.label] = evaluate(context, field.default_value) unless data.key?(field.label) + data[field.id] = evaluate(context, field.default_value) unless data.key?(field.id) field.default_value = nil field end def drop_ifs(context, fields) - if_values = fields.map { |field| !field.if_condition || evaluate(context, field.if_condition) } + if_values = fields.map do |field| + if evaluate(context, field.if_condition) == false + false + elsif field.type == 'Layout' && field.component == 'Row' + field.fields = drop_ifs(context, field.fields || []) + + true unless field.fields.empty? + else + true + end + end + new_fields = fields.select.with_index { |_field, index| if_values[index] } new_fields.each do |field| field = field.dup @@ -105,6 +132,8 @@ def drop_deferred(context, search_values, fields) new_fields = [] fields.each do |field| field = field.dup + execute_on_sub_fields(field) { |sub_fields| drop_deferred(context, search_values, sub_fields) } + field.instance_variables.each do |key| key = key.to_s.delete('@').to_sym @@ -114,7 +143,7 @@ def drop_deferred(context, search_values, fields) value = field.send(key) key = key.to_s.concat('=').to_sym - search_value = field.type == 'Layout' ? nil : search_values&.dig(field.label) + search_value = field.type == 'Layout' ? nil : search_values&.dig(field.id) field.send(key, evaluate(context, value, search_value)) end diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/action/base_action.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/action/base_action.rb index cc5771161..ec49ff238 100644 --- a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/action/base_action.rb +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/action/base_action.rb @@ -23,6 +23,20 @@ def build_elements end end + def validate_fields_ids(form = @form, used = []) + form&.each do |element| + if element.type == 'Layout' && element.component == 'Row' + validate_fields_ids(element.fields, used) + else + if used.include?(element.id) + raise ForestAdminDatasourceToolkit::Exceptions::ForestException, + "All field must have different 'id'. Conflict come from field '#{element.id}'" + end + used << element.id + end + end + end + def build_widget(field) case field[:widget] when 'AddressAutocomplete' diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/action/dynamic_field.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/action/dynamic_field.rb index 8c5b5a76e..58a3417af 100644 --- a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/action/dynamic_field.rb +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/action/dynamic_field.rb @@ -3,11 +3,12 @@ module Decorators module Action class DynamicField < BaseFormElement attr_accessor :type, :label, :description, :is_required, :is_read_only, :if_condition, :value, :default_value, - :collection_name, :enum_values, :placeholder + :collection_name, :enum_values, :placeholder, :id def initialize( type:, - label:, + label: nil, + id: nil, description: nil, is_required: false, is_read_only: false, @@ -21,7 +22,13 @@ def initialize( ) super(type: type) - @label = label + if id.nil? && label.nil? + raise ForestAdminDatasourceToolkit::Exceptions::ForestException, + "A field must have an 'id' or a 'label' defined." + end + + @label = label.nil? ? id : label + @id = id.nil? ? label : id @description = description @is_required = is_required @is_read_only = is_read_only diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/action/form_layout_element.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/action/form_layout_element.rb index 3fa62a401..d74be0d92 100644 --- a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/action/form_layout_element.rb +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/action/form_layout_element.rb @@ -32,6 +32,7 @@ def initialize(content:, **options) class RowElement < LayoutElement include ForestAdminDatasourceToolkit::Exceptions + attr_accessor :fields def initialize(options) @@ -58,7 +59,7 @@ def validate_no_layout_subfields!(fields) def instantiate_subfields(fields) fields.map do |field| - ForestAdminDatasourceToolkit::Components::Actions::ActionFieldFactory.build(field.to_h) + DynamicField.new(**field.to_h) end end end diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/action/layout_element.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/action/layout_element.rb deleted file mode 100644 index 98791ecab..000000000 --- a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/action/layout_element.rb +++ /dev/null @@ -1,343 +0,0 @@ -module ForestAdminDatasourceCustomizer - module Decorators - module Action - module LayoutElement - include Types - - class TimePickerField < DynamicField - attr_accessor :widget - - def initialize(options) - super(**options) - WidgetField.validate_arg(options, :type, { type: 'contains', value: ['Time'] }) - @widget = 'TimePicker' - end - end - - class AddressAutocompleteField < DynamicField - attr_accessor :widget - - def initialize(options) - super(**options) - WidgetField.validate_arg(options, :type, { type: 'contains', value: ['String'] }) - - @widget = 'AddressAutocomplete' - end - end - - class CheckboxField < DynamicField - attr_accessor :widget - - def initialize(options) - super(**options) - WidgetField.validate_arg( - options, - :type, - { type: 'contains', value: [Types::FieldType::BOOLEAN] } - ) - - @widget = 'Checkbox' - end - end - - class CheckboxGroupField < DynamicField - attr_accessor :widget, :options - - def initialize(options) - super(**options) - - WidgetField.validate_arg(options, :options, { type: 'present' }) - WidgetField.validate_arg( - options, - :type, - { type: 'contains', value: [Types::FieldType::STRING_LIST, Types::FieldType::NUMBER_LIST] } - ) - - @widget = 'CheckboxGroup' - @options = options[:options] - end - end - - class ColorPickerField < DynamicField - attr_accessor :widget, :enable_opacity, :quick_palette - - def initialize(options) - super(**options) - - WidgetField.validate_arg(options, :enable_opacity, { type: 'contains', value: [Types::FieldType::STRING] }) - - @widget = 'ColorPicker' - @enable_opacity = options[:enable_opacity] || nil - @quick_palette = options[:quick_palette] || nil - end - end - - class CurrencyInputField < DynamicField - attr_accessor :widget, :currency, :base, :min, :max, :step - - def initialize(options) - super(**options) - - WidgetField.validate_arg(options, :type, { type: 'contains', value: [Types::FieldType::NUMBER] }) - WidgetField.validate_arg(options, :currency, { type: 'present' }) - - @widget = 'CurrencyInput' - @currency = options[:currency] - @base = options[:base] || 'Unit' - @min = options[:min] || nil - @max = options[:max] || nil - @step = options[:step] || nil - end - end - - class DatePickerField < DynamicField - attr_accessor :widget, :min, :max, :format, :step - - def initialize(options) - super(**options) - - WidgetField.validate_arg( - options, - 'type', - { type: 'contains', - value: [Types::FieldType::DATE, Types::FieldType::DATE_ONLY, Types::FieldType::STRING] } - ) - - @widget = 'DatePicker' - @format = options[:format] || nil - @min = options[:min] || nil - @max = options[:max] || nil - @step = options[:step] || nil - end - end - - class DropdownField < DynamicField - attr_accessor :widget, :options, :search - - def initialize(options) - super(**options) - WidgetField.validate_arg(options, :options, { type: 'present' }) - WidgetField.validate_arg( - options, - 'type', - { - type: 'contains', - value: [Types::FieldType::DATE, Types::FieldType::DATE_ONLY, Types::FieldType::STRING, - Types::FieldType::STRING_LIST] - } - ) - - @widget = 'Dropdown' - @options = options[:options] - @search = options[:search] || nil - end - end - - class FilePickerField < DynamicField - attr_accessor :widget, :extensions, :max_count, :max_size_mb - - def initialize(options) - super(**options) - WidgetField.validate_arg(options, :options, { type: 'present' }) - WidgetField.validate_arg( - options, - 'type', - { - type: 'contains', - value: [Types::FieldType::FILE, Types::FieldType::FILE_LIST] - } - ) - - @widget = 'FilePicker' - @extensions = options[:extensions] || nil - @max_size_mb = options[:max_size_mb] || nil - @max_count = options[:max_count] || nil - end - end - - class NumberInputField < DynamicField - attr_accessor :widget, :step, :min, :max - - def initialize(options) - super(**options) - WidgetField.validate_arg(options, :options, { type: 'present' }) - WidgetField.validate_arg( - options, - 'type', - { - type: 'contains', - value: [Types::FieldType::NUMBER] - } - ) - - @widget = 'NumberInput' - @step = options[:step] || nil - @min = options[:min] || nil - @max = options[:max] || nil - end - end - - class JsonEditorField < DynamicField - attr_accessor :widget - - def initialize(options) - super(**options) - WidgetField.validate_arg( - options, - 'type', - { - type: 'contains', - value: [Types::FieldType::DATE, Types::FieldType::DATE_ONLY, Types::FieldType::STRING, - Types::FieldType::STRING_LIST] - } - ) - - @widget = 'JsonEditor' - end - end - - class NumberInputListField < DynamicField - attr_accessor :widget, :allow_duplicates, :allow_empty_values, :enable_reorder, :min, :max, :step - - def initialize(options) - super(**options) - WidgetField.validate_arg(options, :options, { type: 'present' }) - WidgetField.validate_arg( - options, - 'type', - { - type: 'contains', - value: [Types::FieldType::NUMBER_LIST] - } - ) - - @widget = 'NumberInputList' - @allow_duplicates = options[:allow_duplicates] || nil - @allow_empty_values = options[:allow_empty_values] || nil - @enable_reorder = options[:enable_reorder] || nil - @min = options[:min] || nil - @max = options[:max] || nil - @step = options[:step] || nil - end - end - - class RadioGroupField < DynamicField - attr_accessor :widget, :options - - def initialize(options) - super(**options) - WidgetField.validate_arg(options, :options, { type: 'present' }) - WidgetField.validate_arg( - options, - 'type', - { - type: 'contains', - value: [Types::FieldType::DATE, Types::FieldType::DATEONLY, Types::FieldType::NUMBER, - Types::FieldType::STRING] - } - ) - - @widget = 'RadioGroup' - @options = options[:options] - end - end - - class RichTextField < DynamicField - attr_accessor :widget - - def initialize(options) - super(**options) - WidgetField.validate_arg(options, :options, { type: 'present' }) - WidgetField.validate_arg( - options, - 'type', - { - type: 'contains', - value: [Types::FieldType::STRING] - } - ) - - @widget = 'RichText' - end - end - - class TextAreaField < DynamicField - attr_accessor :widget, :rows - - def initialize(options) - super(**options) - WidgetField.validate_arg( - options, - 'type', - { - type: 'contains', - value: [Types::FieldType::STRING] - } - ) - - @widget = 'TextArea' - @rows = options[:rows] || nil - end - end - - class TextInputField < DynamicField - attr_accessor :widget - - def initialize(options) - super(**options) - WidgetField.validate_arg( - options, - 'type', - { - type: 'contains', - value: [Types::FieldType::STRING] - } - ) - - @widget = 'TextInput' - end - end - - class TextInputListField < DynamicField - attr_accessor :widget, :allow_duplicates, :allow_empty_values, :enable_reorder - - def initialize(options) - super(**options) - WidgetField.validate_arg( - options, - 'type', - { - type: 'contains', - value: [Types::FieldType::STRING_LIST] - } - ) - - @widget = 'TextInput' - @allow_duplicates = options[:allow_duplicates] || nil - @allow_empty_values = options[:allow_empty_values] || nil - @enable_reorder = options[:enable_reorder] || nil - end - end - - class UserDropdownField < DynamicField - attr_accessor :widget - - def initialize(options) - super(**options) - WidgetField.validate_arg(options, :options, { type: 'present' }) - WidgetField.validate_arg( - options, - 'type', - { - type: 'contains', - value: [Types::FieldType::STRING, Types::FieldType::STRING_LIST] - } - ) - - @widget = 'UserDropdown' - end - end - end - end - end -end diff --git a/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/action/action_collection_decorator_spec.rb b/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/action/action_collection_decorator_spec.rb index de0ce0ce2..7c62798c6 100644 --- a/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/action/action_collection_decorator_spec.rb +++ b/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/action/action_collection_decorator_spec.rb @@ -143,6 +143,33 @@ module Action label: 'dynamicIfTrue', type: Types::FieldType::STRING, if_condition: proc { true } + }, + { + type: 'Layout', + component: 'Row', + fields: [ + { + type: Types::FieldType::STRING, + label: 'sub_field_1', + if_condition: proc { false } + }, + { + type: Types::FieldType::STRING, + label: 'sub_field_2', + if_condition: proc { true } + } + ] + }, + { + type: 'Layout', + component: 'Row', + fields: [ + { + type: Types::FieldType::STRING, + label: 'sub_field_3', + if_condition: proc { false } + } + ] } ] ) { |_context, result_builder| result_builder.error(message: 'meeh') } @@ -158,6 +185,15 @@ module Action ) end + it 'drop row element if fields are empty and remove field not required in row' do + form = @decorated_book.get_form(caller, 'make photocopy', {}, Filter.new, { include_hidden_fields: false }) + + expect(form.size).to eq(3) + expect(form.last.fields).to include( + have_attributes(label: 'sub_field_2', type: 'String') + ) + end + it 'not dropIfs if required' do form = @decorated_book.get_form(caller, 'make photocopy', {}, Filter.new, { include_hidden_fields: true }) @@ -169,6 +205,44 @@ module Action end end + describe 'with a single action with default values' do + before do + @decorated_book.add_action( + 'make photocopy', + BaseAction.new( + scope: Types::ActionScope::GLOBAL, + form: [ + { label: 'field_1', type: Types::FieldType::STRING, default_value: proc { 'default value field_1' } }, + { + type: 'Layout', + component: 'Row', + fields: [ + { + type: Types::FieldType::STRING, + label: 'sub_field_1', + default_value: proc { 'default value sub_field_1' } + } + ] + } + ] + ) { |_context, result_builder| result_builder.error(message: 'meeh') } + ) + end + + it 'drop all default values' do + form = @decorated_book.get_form(caller, 'make photocopy', {}, Filter.new, { include_hidden_fields: false }) + + expect(form).to include( + have_attributes(label: 'field_1', type: 'String', value: 'default value field_1'), + have_attributes( + type: 'Layout', + component: 'Row', + fields: include(have_attributes(label: 'sub_field_1', type: 'String', value: 'default value sub_field_1')) + ) + ) + end + end + describe 'with single action with both load and change hooks' do before do @decorated_book.add_action( @@ -251,6 +325,25 @@ module Action value: proc do |context| context.get_form_value('change') if context.field_changed?('change') end + }, + { + type: 'Layout', + component: 'Row', + fields: [ + { + type: Types::FieldType::STRING, + label: 'sub_field_change', + is_required: true + }, + DynamicField.new( + type: Types::FieldType::STRING, + label: 'sub_field_to_change', + is_read_only: true, + value: proc do |context| + context.get_form_value('sub_field_change') if context.field_changed?('sub_field_change') + end + ) + ] } ] ) { |_context, result_builder| result_builder.error(message: 'meeh') } @@ -265,6 +358,15 @@ module Action have_attributes(label: 'to change', is_read_only: true, watch_changes: false) ) end + + it 'add watchChange property to sub fields of row layout that need to trigger a recompute on change' do + form = @decorated_book.get_form(caller, 'make photocopy', {}, Filter.new) + + expect(form.last.fields).to include( + have_attributes(label: 'sub_field_change', watch_changes: true), + have_attributes(label: 'sub_field_to_change', is_read_only: true, watch_changes: false) + ) + end end describe 'with single action with search hook' do @@ -308,6 +410,55 @@ module Action ) end end + + describe 'add_action' do + it 'raise an error if multiple fields with same id are provided' do + action = BaseAction.new( + scope: Types::ActionScope::GLOBAL, + form: [ + { id: 'id', label: 'amount', type: Types::FieldType::NUMBER }, + { id: 'id', label: 'cost', type: Types::FieldType::NUMBER } + ] + ) { |_context, result_builder| result_builder.error(message: 'foo') } + + expect do + @decorated_book.add_action('make photocopy', action) + end.to raise_error(Exceptions::ForestException, "🌳🌳🌳 All field must have different 'id'. Conflict come from field 'id'") + end + + it 'raise an error if multiple fields with same id are provided in row' do + action = BaseAction.new( + scope: Types::ActionScope::GLOBAL, + form: [ + { + type: 'Layout', + component: 'Row', + fields: [ + { id: 'id', label: 'amount', type: Types::FieldType::NUMBER }, + { id: 'id', label: 'cost', type: Types::FieldType::NUMBER } + ] + } + ] + ) { |_context, result_builder| result_builder.error(message: 'foo') } + + expect do + @decorated_book.add_action('make photocopy', action) + end.to raise_error(Exceptions::ForestException, "🌳🌳🌳 All field must have different 'id'. Conflict come from field 'id'") + end + + it 'raise an error if field (hash) is provided without id and label' do + action = BaseAction.new( + scope: Types::ActionScope::GLOBAL, + form: [ + { type: Types::FieldType::NUMBER } + ] + ) { |_context, result_builder| result_builder.error(message: 'foo') } + + expect do + @decorated_book.add_action('make photocopy', action) + end.to raise_error(Exceptions::ForestException, "🌳🌳🌳 A field must have an 'id' or a 'label' defined.") + end + end end end end diff --git a/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/action/dynamic_field_spec.rb b/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/action/dynamic_field_spec.rb new file mode 100644 index 000000000..6e927cbf6 --- /dev/null +++ b/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/action/dynamic_field_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +module ForestAdminDatasourceCustomizer + module Decorators + module Action + include ForestAdminDatasourceToolkit + + describe DynamicField do + let(:type) { Types::FieldType::STRING } + + it 'use label value if id is not provided' do + plain_field = { type: type, label: 'test' } + field = described_class.new(**plain_field) + + expect(field.id).to eq('test') + end + + it 'use id value if label is not provided' do + plain_field = { type: type, id: 'test' } + field = described_class.new(**plain_field) + + expect(field.label).to eq('test') + end + + it 'use id value and label value when both are provided' do + plain_field = { type: type, id: 'test_id', label: 'test' } + field = described_class.new(**plain_field) + + expect(field.label).to eq('test') + expect(field.id).to eq('test_id') + end + + it 'raise an error if id and label are not provided' do + plain_field = { type: type } + + expect do + described_class.new(**plain_field) + end.to raise_error(Exceptions::ForestException, "🌳🌳🌳 A field must have an 'id' or a 'label' defined.") + end + + it 'raise an error if id and label are nil' do + plain_field = { type: type, id: nil, label: nil } + + expect do + described_class.new(**plain_field) + end.to raise_error(Exceptions::ForestException, "🌳🌳🌳 A field must have an 'id' or a 'label' defined.") + end + end + end + end +end diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/actions/action_field.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/actions/action_field.rb index 909bcdb40..a226155bd 100644 --- a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/actions/action_field.rb +++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/actions/action_field.rb @@ -4,11 +4,12 @@ module Actions class ActionField attr_accessor :value, :watch_changes attr_reader :type, :label, :description, :is_required, :is_read_only, :enum_values, :collection_name, :widget, - :placeholder + :placeholder, :id def initialize( type:, - label:, + label: nil, + id: nil, description: nil, is_required: false, is_read_only: false, @@ -21,6 +22,7 @@ def initialize( ) @type = type @label = label + @id = id @description = description @is_required = is_required @is_read_only = is_read_only