diff --git a/.rubocop.yml b/.rubocop.yml index 513697910..70668f85b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -228,6 +228,7 @@ Metrics/MethodLength: - 'packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/action/base_action.rb' - 'packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/rename_field/rename_field_collection_decorator.rb' - 'packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/action/action_collection_decorator.rb' + - 'packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/action/form_factory.rb' Metrics/BlockLength: Exclude: @@ -285,6 +286,7 @@ Layout/LineLength: - 'packages/forest_admin_agent/lib/forest_admin_agent/http/forest_admin_api_requester.rb' - 'packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/list.rb' - 'packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb' + - 'packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/action/form_layout_element.rb' - 'packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/override/context/create_override_customization_context.rb' - 'packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/override/context/update_override_customization_context.rb' - 'packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/override/context/delete_override_customization_context.rb' 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 5bbe548f6..0836c5bc5 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 @@ -29,7 +29,6 @@ def self.build_schema(collection, name) action = collection.schema[:actions][name] action_index = collection.schema[:actions].keys.index(name) slug = get_action_slug(name) - form_elements = extract_fields_and_layout(collection.get_form(nil, name)) if action.static_form? fields = build_fields(collection, form_elements[:fields]) @@ -72,6 +71,14 @@ def self.build_layout_schema(field) component: field.component.camelize(:lower), fields: field.fields.map { |f| build_layout_schema(f) } } + elsif field.component == 'Page' + return { + **field.to_h, + component: field.component.camelize(:lower), + elements: field.elements.map do |f| + build_layout_schema(f) + end + } end { **field.to_h, component: field.component.camelize(:lower) } @@ -135,18 +142,15 @@ def self.build_layout(elements) def self.extract_fields_and_layout(form) fields = [] layout = [] - form&.each do |element| if element.type == Actions::FieldType::LAYOUT - if element.component == 'Row' - extract = extract_fields_and_layout(element.fields) - element.fields = extract[:layout] + if %w[Page Row].include?(element.component) + extract = extract_fields_and_layout_for_component(element) layout << element fields.concat(extract[:fields]) else layout << element end - else fields << element # frontend rule @@ -156,6 +160,13 @@ def self.extract_fields_and_layout(form) { fields: fields, layout: layout } end + + def self.extract_fields_and_layout_for_component(element) + key = element.component == 'Page' ? :elements : :fields + extract = extract_fields_and_layout(element.public_send(key)) + element.public_send(:"#{key}=", extract[:layout]) + extract + end end 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 639bd870c..50f2f488b 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 @@ -397,6 +397,80 @@ module Schema end end + context 'with page element' do + before do + @collection = collection_build( + schema: { + actions: { + 'Charge credit card' => BaseAction.new( + scope: Types::ActionScope::SINGLE + ) + } + }, + get_form: [ + ActionLayoutElement::PageElement.new( + next_button_label: 'Next', + previous_button_label: 'Previous', + elements: [ + ActionLayoutElement::HtmlBlockElement.new(content: '

Charge the credit card of the customer

'), + ActionLayoutElement::RowElement.new( + fields: [ + ActionField.new(id: 'label', label: 'label', type: 'String'), + ActionField.new(id: 'amount', label: 'amount', type: 'String') + ] + ) + ] + ) + ] + ) + end + + it 'generate schema correctly' do + schema = described_class.build_schema(@collection, 'Charge credit card') + + expect(schema).to eq( + { + id: 'collection-0-charge-credit-card', + name: 'Charge credit card', + submitButtonLabel: nil, + description: nil, + type: 'single', + baseUrl: nil, + endpoint: '/forest/_actions/collection/0/charge-credit-card', + httpMethod: 'POST', + redirect: nil, + download: false, + fields: [ + { + field: 'label', + label: 'label', + type: 'String', + description: nil, + isRequired: false, + isReadOnly: false, + widgetEdit: nil, + defaultValue: nil + }, + { + field: 'amount', + label: 'amount', + type: 'String', + description: nil, + isRequired: false, + isReadOnly: false, + widgetEdit: nil, + defaultValue: nil + } + ], + # layout: [ + # { component: 'page', type: 'Layout', elements: ['htmlBlock', 'row'] } + # ], + hooks: { load: false, change: ['changeHook'] } + } + ) + end + end + describe 'extract_fields_and_layout' do before do @collection = collection_build( 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 24a269fe0..13a894f35 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 @@ -10,6 +10,7 @@ def initialize(child_collection, datasource) end def add_action(name, action) + ensure_form_is_correct(action.form, name) action.build_elements action.validate_fields_ids @actions[name] = action @@ -41,15 +42,13 @@ def get_form(caller, name, data = nil, filter = nil, metas = {}) context = get_context(caller, action, form_values, filter, used, metas[:change_field]) dynamic_fields = action.form - 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.id == metas[:search_field] } - end + dynamic_fields = select_in_form_fields(dynamic_fields, metas[:search_field]) if metas[:search_field] 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 = drop_deferred(context, metas[:search_values], dynamic_fields) + + fields.compact set_watch_changes_on_fields(form_values, used, fields) @@ -64,6 +63,31 @@ def refine_schema(sub_schema) private + def ensure_form_is_correct(form, action_name) + return if form.nil? || form.empty? + + is_page_component = ->(element) { element[:type] == 'Layout' && element[:component] == 'Page' } + pages = is_page_component.call(form.first) + + form.each do |element| + if pages != is_page_component.call(element) + raise ForestAdminDatasourceToolkit::Exceptions::ForestException, + "You cannot mix pages and other form elements in smart action '#{action_name}' form" + end + end + end + + def select_in_form_fields(fields, search_field) + fields.select do |field| + if nested_layout_element?(field) + key = field.component == 'Page' ? :elements : :fields + select_in_form_fields(field.public_send(:"#{key}"), search_field) + else + field.id == search_field + end + end + end + def set_watch_changes_on_fields(form_values, used, fields) fields.each do |field| if field.type != 'Layout' @@ -77,14 +101,20 @@ def set_watch_changes_on_fields(form_values, used, fields) field.watch_changes = used.include?(field.id) elsif field.component == 'Row' set_watch_changes_on_fields(form_values, used, field.fields) + elsif field.component == 'Page' + set_watch_changes_on_fields(form_values, used, field.elements) end end end def execute_on_sub_fields(field) - return unless field.type == 'Layout' && field.component == 'Row' + return unless nested_layout_element?(field) - field.fields = yield(field.fields) + if field.component == 'Page' + field.elements = yield(field.elements) + elsif field.component == 'Row' + field.fields = yield(field.fields) + end end def drop_defaults(context, fields, data) @@ -110,10 +140,12 @@ def drop_ifs(context, fields) 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 || []) + elsif nested_layout_element?(field) + key = field.component == 'Page' ? :elements : :fields + field.public_send(:"#{key}=", drop_ifs(context, field.public_send(:"#{key}") || [])) + + true unless field.public_send(:"#{key}").empty? - true unless field.fields.empty? else true end @@ -170,6 +202,10 @@ def get_context(caller, action, form_values = [], filter = nil, used = [], chang Context::ActionContext.new(self, caller, form_values, filter, used, change_field) end + + def nested_layout_element?(field) + field.type == 'Layout' && %w[Page Row].include?(field.component) + end end end 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 4aed12ddd..6b056aea1 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 @@ -14,21 +14,22 @@ def initialize(scope:, form: nil, is_generate_file: false, description: nil, sub end def build_elements - @form = @form&.map do |field| - if field.key? :widget - build_widget(field) - elsif field[:type] == 'Layout' - build_layout_element(field) - else - DynamicField.new(**field) - end - end + @form = FormFactory.build_elements(form) + end + + def static_form? + return form&.all?(&:static?) && form&.none? { |field| field.type == 'Layout' } if form + + true 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) + if element.type == 'Layout' + if %w[Page Row].include?(element.component) + key = element.component == 'Page' ? :elements : :fields + validate_fields_ids(element.public_send(key), used) + end else if used.include?(element.id) raise ForestAdminDatasourceToolkit::Exceptions::ForestException, @@ -38,69 +39,6 @@ def validate_fields_ids(form = @form, used = []) end end end - - def build_widget(field) - case field[:widget] - when 'AddressAutocomplete' - WidgetField::AddressAutocompleteField.new(**field) - when 'Checkbox' - WidgetField::CheckboxField.new(**field) - when 'CheckboxGroup' - WidgetField::CheckboxGroupField.new(**field) - when 'ColorPicker' - WidgetField::ColorPickerField.new(**field) - when 'CurrencyInput' - WidgetField::CurrencyInputField.new(**field) - when 'DatePicker' - WidgetField::DatePickerField.new(**field) - when 'Dropdown' - WidgetField::DropdownField.new(**field) - when 'FilePicker' - WidgetField::FilePickerField.new(**field) - when 'JsonEditor' - WidgetField::JsonEditorField.new(**field) - when 'NumberInput' - WidgetField::NumberInputField.new(**field) - when 'NumberInputList' - WidgetField::NumberInputListField.new(**field) - when 'RadioGroup' - WidgetField::RadioGroupField.new(**field) - when 'RichText' - WidgetField::RichTextField.new(**field) - when 'TextArea' - WidgetField::TextAreaField.new(**field) - when 'TextInput' - WidgetField::TextInputField.new(**field) - when 'TextInputList' - WidgetField::TextInputListField.new(**field) - when 'TimePicker' - WidgetField::TimePickerField.new(**field) - when 'UserDropdown' - WidgetField::UserDropdownField.new(**field) - else - raise ForestAdminDatasourceToolkit::Exceptions::ForestException, "Unknow widget type: #{field[:widget]}" - end - end - - def build_layout_element(field) - case field[:component] - when 'Separator' - FormLayoutElement::SeparatorElement.new(**field) - when 'HtmlBlock' - FormLayoutElement::HtmlBlockElement.new(**field) - when 'Row' - FormLayoutElement::RowElement.new(**field) - else - raise ForestAdminDatasourceToolkit::Exceptions::ForestException, - "Unknow component type: #{field[:component]}" - end - end - - def static_form? - return form&.all?(&:static?) && form&.none? { |field| field.type == 'Layout' } if form - - true - end end end end diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/action/form_factory.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/action/form_factory.rb new file mode 100644 index 000000000..685d28a04 --- /dev/null +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/action/form_factory.rb @@ -0,0 +1,87 @@ +module ForestAdminDatasourceCustomizer + module Decorators + module Action + class FormFactory + def self.build_elements(form) + form&.map do |field| + case field + when Hash + if field.key?(:widget) + build_widget(field) + elsif field[:type] == 'Layout' + build_layout_element(field) + else + DynamicField.new(**field) + end + when FormLayoutElement::RowElement + build_elements(field.fields) + when FormLayoutElement::PageElement + build_elements(field.elements) + else + field + end + end + end + + def self.build_widget(field) + case field[:widget] + when 'AddressAutocomplete' + WidgetField::AddressAutocompleteField.new(**field) + when 'Checkbox' + WidgetField::CheckboxField.new(**field) + when 'CheckboxGroup' + WidgetField::CheckboxGroupField.new(**field) + when 'ColorPicker' + WidgetField::ColorPickerField.new(**field) + when 'CurrencyInput' + WidgetField::CurrencyInputField.new(**field) + when 'DatePicker' + WidgetField::DatePickerField.new(**field) + when 'Dropdown' + WidgetField::DropdownField.new(**field) + when 'FilePicker' + WidgetField::FilePickerField.new(**field) + when 'JsonEditor' + WidgetField::JsonEditorField.new(**field) + when 'NumberInput' + WidgetField::NumberInputField.new(**field) + when 'NumberInputList' + WidgetField::NumberInputListField.new(**field) + when 'RadioGroup' + WidgetField::RadioGroupField.new(**field) + when 'RichText' + WidgetField::RichTextField.new(**field) + when 'TextArea' + WidgetField::TextAreaField.new(**field) + when 'TextInput' + WidgetField::TextInputField.new(**field) + when 'TextInputList' + WidgetField::TextInputListField.new(**field) + when 'TimePicker' + WidgetField::TimePickerField.new(**field) + when 'UserDropdown' + WidgetField::UserDropdownField.new(**field) + else + raise ForestAdminDatasourceToolkit::Exceptions::ForestException, "Unknow widget type: #{field[:widget]}" + end + end + + def self.build_layout_element(field) + case field[:component] + when 'Separator' + FormLayoutElement::SeparatorElement.new(**field) + when 'HtmlBlock' + FormLayoutElement::HtmlBlockElement.new(**field) + when 'Row' + FormLayoutElement::RowElement.new(**field) + when 'Page' + FormLayoutElement::PageElement.new(**field) + else + raise ForestAdminDatasourceToolkit::Exceptions::ForestException, + "Unknow component type: #{field[:component]}" + end + end + end + end + end +end 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 d74be0d92..ab5b84ece 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 @@ -58,10 +58,44 @@ def validate_no_layout_subfields!(fields) end def instantiate_subfields(fields) - fields.map do |field| - DynamicField.new(**field.to_h) + FormFactory.build_elements(fields) + end + end + + class PageElement < LayoutElement + include ForestAdminDatasourceToolkit::Exceptions + + attr_accessor :elements, :next_button_label, :previous_button_label + + def initialize(options) + super(component: 'Page', **options) + + validate_elements_presence!(options) + validate_no_page_elements!(options[:elements]) + @next_button_label = options[:next_button_label] + @previous_button_label = options[:previous_button_label] + @elements = instantiate_elements(options[:elements] || []) + end + + private + + def validate_elements_presence!(options) + return if options.key?(:elements) + + raise ForestException, "Using 'elements' in a 'Page' configuration is mandatory" + end + + def validate_no_page_elements!(elements) + elements&.each do |element| + if element[:component] == 'Page' + raise ForestException, "'Page' component cannot be used within 'elements'" + end end end + + def instantiate_elements(elements) + FormFactory.build_elements(elements) + end end end end diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/action/types/field_type.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/action/types/field_type.rb index 2ae07310d..7a86c87b4 100644 --- a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/action/types/field_type.rb +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/decorators/action/types/field_type.rb @@ -21,6 +21,8 @@ module FieldType JSON = 'Json'.freeze + LAYOUT = 'Layout'.freeze + NUMBER = 'Number'.freeze NUMBER_LIST = 'NumberList'.freeze 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 7c62798c6..199eb4003 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 @@ -127,81 +127,119 @@ module Action end describe 'with a single action with a if_conditions' do - before do - @decorated_book.add_action( - 'make photocopy', - BaseAction.new( - scope: Types::ActionScope::GLOBAL, - form: [ - { label: 'noIf', type: Types::FieldType::STRING }, - { - label: 'dynamicIfFalse', - type: Types::FieldType::STRING, - if_condition: proc { false } - }, - { - 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') } - ) - end + context 'with row element' do + before do + @decorated_book.add_action( + 'make photocopy', + BaseAction.new( + scope: Types::ActionScope::GLOBAL, + form: [ + { label: 'noIf', type: Types::FieldType::STRING }, + { + label: 'dynamicIfFalse', + type: Types::FieldType::STRING, + if_condition: proc { false } + }, + { + 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') } + ) + end - it 'drop ifs which are false if required' do - form = @decorated_book.get_form(caller, 'make photocopy', {}, Filter.new, { include_hidden_fields: false }) + it 'drop ifs which are false if required' do + form = @decorated_book.get_form(caller, 'make photocopy', {}, Filter.new, { include_hidden_fields: false }) - expect(form).to include( - have_attributes(label: 'noIf', type: 'String'), - have_attributes(label: 'dynamicIfTrue', type: 'String') - ) - end + expect(form).to include( + have_attributes(label: 'noIf', type: 'String'), + have_attributes(label: 'dynamicIfTrue', type: 'String') + ) + 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 }) + 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') - ) + 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 }) + + expect(form).to include( + have_attributes(label: 'noIf', type: 'String'), + have_attributes(label: 'dynamicIfFalse', type: 'String'), + have_attributes(label: 'dynamicIfTrue', type: 'String') + ) + end end - it 'not dropIfs if required' do - form = @decorated_book.get_form(caller, 'make photocopy', {}, Filter.new, { include_hidden_fields: true }) + context 'with page element' do + before do + @decorated_book.add_action( + 'make photocopy', + BaseAction.new( + scope: Types::ActionScope::GLOBAL, + form: [ + { + type: 'Layout', + component: 'Page', + elements: [ + { label: 'field_1', type: Types::FieldType::STRING, if_condition: proc { false } } + ] + }, + { + type: 'Layout', + component: 'Page', + elements: [ + { label: 'field_2', type: Types::FieldType::STRING, if_condition: proc { true } } + ] + } + ] + ) { |_context, result_builder| result_builder.error(message: 'meeh') } + ) + end - expect(form).to include( - have_attributes(label: 'noIf', type: 'String'), - have_attributes(label: 'dynamicIfFalse', type: 'String'), - have_attributes(label: 'dynamicIfTrue', type: 'String') - ) + it 'drop page if element are empty' do + form = @decorated_book.get_form(caller, 'make photocopy', {}, Filter.new, { include_hidden_fields: false }) + + expect(form.size).to eq(1) + expect(form.first.elements).to include( + have_attributes(label: 'field_2', type: 'String') + ) + end end end @@ -212,15 +250,21 @@ module Action 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: [ + component: 'Page', + elements: [ + { label: 'field_1', type: Types::FieldType::STRING, default_value: proc { 'default value field_1' } }, { - type: Types::FieldType::STRING, - label: 'sub_field_1', - default_value: proc { 'default value sub_field_1' } + type: 'Layout', + component: 'Row', + fields: [ + { + type: Types::FieldType::STRING, + label: 'sub_field_1', + default_value: proc { 'default value sub_field_1' } + } + ] } ] } @@ -233,11 +277,17 @@ module Action 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')) + component: 'Page', + elements: 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 @@ -458,6 +508,26 @@ module Action @decorated_book.add_action('make photocopy', action) end.to raise_error(Exceptions::ForestException, "🌳🌳🌳 A field must have an 'id' or a 'label' defined.") end + + it 'raise an error if form mix form elements and pages' do + action = BaseAction.new( + scope: Types::ActionScope::GLOBAL, + form: [ + { label: 'amount', type: Types::FieldType::NUMBER }, + { + type: 'Layout', + component: 'Page', + elements: [ + { type: 'Layout', component: 'HtmlBlock', content: 'foo' } + ] + } + ] + ) { |_context, result_builder| result_builder.error(message: 'foo') } + + expect do + @decorated_book.add_action('make photocopy', action) + end.to raise_error(Exceptions::ForestException, "🌳🌳🌳 You cannot mix pages and other form elements in smart action 'make photocopy' form") + end end end end diff --git a/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/action/base_action_spec.rb b/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/action/base_action_spec.rb index 1caaf7f0e..ecf48224a 100644 --- a/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/action/base_action_spec.rb +++ b/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/action/base_action_spec.rb @@ -34,230 +34,6 @@ module Action end end - describe 'when build_widget' do - it 'returns a CheckboxField for widget type "Checkbox"' do - result = action.build_widget(field_send_notification) - expect(result).to be_a(WidgetField::CheckboxField) - end - - it 'raises an exception for an unknown widget type' do - field = { widget: 'UnknownWidget' } - expect { action.build_widget(field) }.to raise_error(ForestAdminDatasourceToolkit::Exceptions::ForestException) - end - - context 'when widget is AddressAutocomplete' do - let(:field) { { label: 'foo', widget: 'AddressAutocomplete', type: 'String' } } - - it 'returns an AddressAutocompleteField' do - result = action.build_widget(field) - expect(result).to be_a(ForestAdminDatasourceCustomizer::Decorators::Action::WidgetField::AddressAutocompleteField) - expect(result.widget).to eq('AddressAutocomplete') - end - end - - context 'when widget is CheckboxGroup' do - let(:field) { { label: 'foo', widget: 'CheckboxGroup', type: 'StringList', options: [] } } - - it 'returns a CheckboxGroupField' do - result = action.build_widget(field) - expect(result).to be_a(ForestAdminDatasourceCustomizer::Decorators::Action::WidgetField::CheckboxGroupField) - expect(result.widget).to eq('CheckboxGroup') - end - end - - context 'when widget is ColorPicker' do - let(:field) { { label: 'foo', widget: 'ColorPicker', type: 'base_', enable_opacity: true } } - - it 'returns a ColorPickerField' do - result = action.build_widget(field) - expect(result).to be_a(ForestAdminDatasourceCustomizer::Decorators::Action::WidgetField::ColorPickerField) - expect(result.widget).to eq('ColorPicker') - end - end - - context 'when widget is CurrencyInput' do - let(:field) { { label: 'foo', widget: 'CurrencyInput', type: 'Number', currency: 'USD' } } - - it 'returns a CurrencyInputField' do - result = action.build_widget(field) - expect(result).to be_a(ForestAdminDatasourceCustomizer::Decorators::Action::WidgetField::CurrencyInputField) - expect(result.widget).to eq('CurrencyInput') - end - end - - context 'when widget is DatePicker' do - let(:field) { { label: 'foo', widget: 'DatePicker', type: 'Date' } } - - it 'returns a DatePickerField' do - result = action.build_widget(field) - expect(result).to be_a(ForestAdminDatasourceCustomizer::Decorators::Action::WidgetField::DatePickerField) - expect(result.widget).to eq('DatePicker') - end - end - - context 'when widget is Dropdown' do - let(:field) { { label: 'foo', widget: 'Dropdown', type: 'String', options: [] } } - - it 'returns a DropdownField' do - result = action.build_widget(field) - expect(result).to be_a(ForestAdminDatasourceCustomizer::Decorators::Action::WidgetField::DropdownField) - expect(result.widget).to eq('Dropdown') - end - end - - context 'when widget is FilePicker' do - let(:field) { { label: 'foo', widget: 'FilePicker', type: 'File', options: [] } } - - it 'returns a FilePickerField' do - result = action.build_widget(field) - expect(result).to be_a(ForestAdminDatasourceCustomizer::Decorators::Action::WidgetField::FilePickerField) - expect(result.widget).to eq('FilePicker') - end - end - - context 'when widget is JsonEditor' do - let(:field) { { label: 'foo', widget: 'JsonEditor', type: 'String' } } - - it 'returns a JsonEditorField' do - result = action.build_widget(field) - expect(result).to be_a(ForestAdminDatasourceCustomizer::Decorators::Action::WidgetField::JsonEditorField) - expect(result.widget).to eq('JsonEditor') - end - end - - context 'when widget is NumberInput' do - let(:field) { { label: 'foo', widget: 'NumberInput', type: 'Number' } } - - it 'returns a NumberInputField' do - result = action.build_widget(field) - expect(result).to be_a(ForestAdminDatasourceCustomizer::Decorators::Action::WidgetField::NumberInputField) - expect(result.widget).to eq('NumberInput') - end - end - - context 'when widget is NumberInputList' do - let(:field) { { label: 'foo', widget: 'NumberInputList', type: 'NumberList', options: [] } } - - it 'returns a NumberInputListField' do - result = action.build_widget(field) - expect(result).to be_a(ForestAdminDatasourceCustomizer::Decorators::Action::WidgetField::NumberInputListField) - expect(result.widget).to eq('NumberInputList') - end - end - - context 'when widget is RadioGroup' do - let(:field) { { label: 'foo', widget: 'RadioGroup', type: 'Number', options: [] } } - - it 'returns a RadioGroupField' do - result = action.build_widget(field) - expect(result).to be_a(ForestAdminDatasourceCustomizer::Decorators::Action::WidgetField::RadioGroupField) - expect(result.widget).to eq('RadioGroup') - end - end - - context 'when widget is RichText' do - let(:field) { { label: 'foo', widget: 'RichText', type: 'String' } } - - it 'returns a RichTextField' do - result = action.build_widget(field) - expect(result).to be_a(ForestAdminDatasourceCustomizer::Decorators::Action::WidgetField::RichTextField) - expect(result.widget).to eq('RichText') - end - end - - context 'when widget is TextArea' do - let(:field) { { label: 'foo', widget: 'TextArea', type: 'String' } } - - it 'returns a TextAreaField' do - result = action.build_widget(field) - expect(result).to be_a(ForestAdminDatasourceCustomizer::Decorators::Action::WidgetField::TextAreaField) - expect(result.widget).to eq('TextArea') - end - end - - context 'when widget is TextInput' do - let(:field) { { label: 'foo', widget: 'TextInput', type: 'String' } } - - it 'returns a TextInputField' do - result = action.build_widget(field) - expect(result).to be_a(ForestAdminDatasourceCustomizer::Decorators::Action::WidgetField::TextInputField) - expect(result.widget).to eq('TextInput') - end - end - - context 'when widget is TextInputList' do - let(:field) { { label: 'foo', widget: 'TextInputList', type: 'StringList', options: [] } } - - it 'returns a TextInputListField' do - result = action.build_widget(field) - expect(result).to be_a(ForestAdminDatasourceCustomizer::Decorators::Action::WidgetField::TextInputListField) - expect(result.widget).to eq('TextInputList') - end - end - - context 'when widget is TimePicker' do - let(:field) { { label: 'foo', widget: 'TimePicker', type: 'Time' } } - - it 'returns a TimePickerField' do - result = action.build_widget(field) - expect(result).to be_a(ForestAdminDatasourceCustomizer::Decorators::Action::WidgetField::TimePickerField) - expect(result.widget).to eq('TimePicker') - end - end - - context 'when widget is UserDropdown' do - let(:field) { { label: 'foo', widget: 'UserDropdown', type: 'String', options: [] } } - - it 'returns a UserDropdownField' do - result = action.build_widget(field) - expect(result).to be_a(ForestAdminDatasourceCustomizer::Decorators::Action::WidgetField::UserDropdownField) - expect(result.widget).to eq('UserDropdown') - end - end - end - - describe 'when build_layout_element' do - context 'when element is a separator' do - let(:element) { { type: 'Layout', component: 'Separator' } } - - it 'returns a separator element' do - result = action.build_layout_element(element) - expect(result).to be_a(ForestAdminDatasourceCustomizer::Decorators::Action::FormLayoutElement::SeparatorElement) - end - end - - context 'when element is a HtmlBlock' do - let(:element) { { type: 'Layout', component: 'HtmlBlock', content: '

foo

' } } - - it 'returns a html block element' do - result = action.build_layout_element(element) - expect(result).to be_a(ForestAdminDatasourceCustomizer::Decorators::Action::FormLayoutElement::HtmlBlockElement) - expect(result.content).to eq('

foo

') - end - end - - context 'when element is a Row' do - let(:element) { { type: 'Layout', component: 'Row', fields: [field_send_notification, field_message] } } - - it 'returns a row element' do - result = action.build_layout_element(element) - expect(result).to be_a(ForestAdminDatasourceCustomizer::Decorators::Action::FormLayoutElement::RowElement) - expect(result.fields[0].label).to eq('Send a notification') - expect(result.fields[1].label).to eq('Notification message') - end - - it 'raises an exception when fields are missing' do - element.delete(:fields) - expect { action.build_layout_element(element) }.to raise_error(ForestAdminDatasourceToolkit::Exceptions::ForestException) - end - - it 'raises an exception when fields contain a layout element' do - element[:fields] << { type: 'Layout' } - expect { action.build_layout_element(element) }.to raise_error(ForestAdminDatasourceToolkit::Exceptions::ForestException) - end - end - end - describe 'when check form is static' do context 'when form is nil' do let(:action) { described_class.new(scope: :single) } diff --git a/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/action/form_factory_spec.rb b/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/action/form_factory_spec.rb new file mode 100644 index 000000000..3d451c8f8 --- /dev/null +++ b/packages/forest_admin_datasource_customizer/spec/lib/forest_admin_datasource_customizer/decorators/action/form_factory_spec.rb @@ -0,0 +1,267 @@ +require 'spec_helper' + +module ForestAdminDatasourceCustomizer + module Decorators + module Action + describe FormFactory do + let(:scope) { Types::ActionScope::SINGLE } + let(:field_send_notification) { { label: 'Send a notification', type: 'Boolean', widget: 'Checkbox', is_required: true, default_value: false } } + let(:field_message) { { label: 'Notification message', type: 'String', is_required: true, default_value: 'Hello' } } + let(:form) do + [ + field_send_notification, + field_message + ] + end + let(:action) { BaseAction.new(scope: scope, form: form) } + + describe 'when build_widget' do + it 'returns a CheckboxField for widget type "Checkbox"' do + result = described_class.build_widget(field_send_notification) + expect(result).to be_a(WidgetField::CheckboxField) + end + + it 'raises an exception for an unknown widget type' do + field = { widget: 'UnknownWidget' } + expect { described_class.build_widget(field) }.to raise_error(ForestAdminDatasourceToolkit::Exceptions::ForestException) + end + + context 'when widget is AddressAutocomplete' do + let(:field) { { label: 'foo', widget: 'AddressAutocomplete', type: 'String' } } + + it 'returns an AddressAutocompleteField' do + result = described_class.build_widget(field) + expect(result).to be_a(ForestAdminDatasourceCustomizer::Decorators::Action::WidgetField::AddressAutocompleteField) + expect(result.widget).to eq('AddressAutocomplete') + end + end + + context 'when widget is CheckboxGroup' do + let(:field) { { label: 'foo', widget: 'CheckboxGroup', type: 'StringList', options: [] } } + + it 'returns a CheckboxGroupField' do + result = described_class.build_widget(field) + expect(result).to be_a(ForestAdminDatasourceCustomizer::Decorators::Action::WidgetField::CheckboxGroupField) + expect(result.widget).to eq('CheckboxGroup') + end + end + + context 'when widget is ColorPicker' do + let(:field) { { label: 'foo', widget: 'ColorPicker', type: 'base_', enable_opacity: true } } + + it 'returns a ColorPickerField' do + result = described_class.build_widget(field) + expect(result).to be_a(ForestAdminDatasourceCustomizer::Decorators::Action::WidgetField::ColorPickerField) + expect(result.widget).to eq('ColorPicker') + end + end + + context 'when widget is CurrencyInput' do + let(:field) { { label: 'foo', widget: 'CurrencyInput', type: 'Number', currency: 'USD' } } + + it 'returns a CurrencyInputField' do + result = described_class.build_widget(field) + expect(result).to be_a(ForestAdminDatasourceCustomizer::Decorators::Action::WidgetField::CurrencyInputField) + expect(result.widget).to eq('CurrencyInput') + end + end + + context 'when widget is DatePicker' do + let(:field) { { label: 'foo', widget: 'DatePicker', type: 'Date' } } + + it 'returns a DatePickerField' do + result = described_class.build_widget(field) + expect(result).to be_a(ForestAdminDatasourceCustomizer::Decorators::Action::WidgetField::DatePickerField) + expect(result.widget).to eq('DatePicker') + end + end + + context 'when widget is Dropdown' do + let(:field) { { label: 'foo', widget: 'Dropdown', type: 'String', options: [] } } + + it 'returns a DropdownField' do + result = described_class.build_widget(field) + expect(result).to be_a(ForestAdminDatasourceCustomizer::Decorators::Action::WidgetField::DropdownField) + expect(result.widget).to eq('Dropdown') + end + end + + context 'when widget is FilePicker' do + let(:field) { { label: 'foo', widget: 'FilePicker', type: 'File', options: [] } } + + it 'returns a FilePickerField' do + result = described_class.build_widget(field) + expect(result).to be_a(ForestAdminDatasourceCustomizer::Decorators::Action::WidgetField::FilePickerField) + expect(result.widget).to eq('FilePicker') + end + end + + context 'when widget is JsonEditor' do + let(:field) { { label: 'foo', widget: 'JsonEditor', type: 'String' } } + + it 'returns a JsonEditorField' do + result = described_class.build_widget(field) + expect(result).to be_a(ForestAdminDatasourceCustomizer::Decorators::Action::WidgetField::JsonEditorField) + expect(result.widget).to eq('JsonEditor') + end + end + + context 'when widget is NumberInput' do + let(:field) { { label: 'foo', widget: 'NumberInput', type: 'Number' } } + + it 'returns a NumberInputField' do + result = described_class.build_widget(field) + expect(result).to be_a(ForestAdminDatasourceCustomizer::Decorators::Action::WidgetField::NumberInputField) + expect(result.widget).to eq('NumberInput') + end + end + + context 'when widget is NumberInputList' do + let(:field) { { label: 'foo', widget: 'NumberInputList', type: 'NumberList', options: [] } } + + it 'returns a NumberInputListField' do + result = described_class.build_widget(field) + expect(result).to be_a(ForestAdminDatasourceCustomizer::Decorators::Action::WidgetField::NumberInputListField) + expect(result.widget).to eq('NumberInputList') + end + end + + context 'when widget is RadioGroup' do + let(:field) { { label: 'foo', widget: 'RadioGroup', type: 'Number', options: [] } } + + it 'returns a RadioGroupField' do + result = described_class.build_widget(field) + expect(result).to be_a(ForestAdminDatasourceCustomizer::Decorators::Action::WidgetField::RadioGroupField) + expect(result.widget).to eq('RadioGroup') + end + end + + context 'when widget is RichText' do + let(:field) { { label: 'foo', widget: 'RichText', type: 'String' } } + + it 'returns a RichTextField' do + result = described_class.build_widget(field) + expect(result).to be_a(ForestAdminDatasourceCustomizer::Decorators::Action::WidgetField::RichTextField) + expect(result.widget).to eq('RichText') + end + end + + context 'when widget is TextArea' do + let(:field) { { label: 'foo', widget: 'TextArea', type: 'String' } } + + it 'returns a TextAreaField' do + result = described_class.build_widget(field) + expect(result).to be_a(ForestAdminDatasourceCustomizer::Decorators::Action::WidgetField::TextAreaField) + expect(result.widget).to eq('TextArea') + end + end + + context 'when widget is TextInput' do + let(:field) { { label: 'foo', widget: 'TextInput', type: 'String' } } + + it 'returns a TextInputField' do + result = described_class.build_widget(field) + expect(result).to be_a(ForestAdminDatasourceCustomizer::Decorators::Action::WidgetField::TextInputField) + expect(result.widget).to eq('TextInput') + end + end + + context 'when widget is TextInputList' do + let(:field) { { label: 'foo', widget: 'TextInputList', type: 'StringList', options: [] } } + + it 'returns a TextInputListField' do + result = described_class.build_widget(field) + expect(result).to be_a(ForestAdminDatasourceCustomizer::Decorators::Action::WidgetField::TextInputListField) + expect(result.widget).to eq('TextInputList') + end + end + + context 'when widget is TimePicker' do + let(:field) { { label: 'foo', widget: 'TimePicker', type: 'Time' } } + + it 'returns a TimePickerField' do + result = described_class.build_widget(field) + expect(result).to be_a(ForestAdminDatasourceCustomizer::Decorators::Action::WidgetField::TimePickerField) + expect(result.widget).to eq('TimePicker') + end + end + + context 'when widget is UserDropdown' do + let(:field) { { label: 'foo', widget: 'UserDropdown', type: 'String', options: [] } } + + it 'returns a UserDropdownField' do + result = described_class.build_widget(field) + expect(result).to be_a(ForestAdminDatasourceCustomizer::Decorators::Action::WidgetField::UserDropdownField) + expect(result.widget).to eq('UserDropdown') + end + end + end + + describe 'when build_layout_element' do + context 'when element is a separator' do + let(:element) { { type: 'Layout', component: 'Separator' } } + + it 'returns a separator element' do + result = described_class.build_layout_element(element) + expect(result).to be_a(ForestAdminDatasourceCustomizer::Decorators::Action::FormLayoutElement::SeparatorElement) + end + end + + context 'when element is a HtmlBlock' do + let(:element) { { type: 'Layout', component: 'HtmlBlock', content: '

foo

' } } + + it 'returns a html block element' do + result = described_class.build_layout_element(element) + expect(result).to be_a(ForestAdminDatasourceCustomizer::Decorators::Action::FormLayoutElement::HtmlBlockElement) + expect(result.content).to eq('

foo

') + end + end + + context 'when element is a Row' do + let(:element) { { type: 'Layout', component: 'Row', fields: [field_send_notification, field_message] } } + + it 'returns a row element' do + result = described_class.build_layout_element(element) + expect(result).to be_a(ForestAdminDatasourceCustomizer::Decorators::Action::FormLayoutElement::RowElement) + expect(result.fields[0].label).to eq('Send a notification') + expect(result.fields[1].label).to eq('Notification message') + end + + it 'raises an exception when fields are missing' do + element.delete(:fields) + expect { described_class.build_layout_element(element) }.to raise_error(ForestAdminDatasourceToolkit::Exceptions::ForestException) + end + + it 'raises an exception when fields contain a layout element' do + element[:fields] << { type: 'Layout' } + expect { described_class.build_layout_element(element) }.to raise_error(ForestAdminDatasourceToolkit::Exceptions::ForestException) + end + end + + context 'when element is a Page' do + let(:element) { { type: 'Layout', component: 'Page', elements: [field_send_notification, field_message], next_button_label: proc { 'Next' }, previous_button_label: proc { 'Previous' } } } + + it 'returns a page element' do + result = described_class.build_layout_element(element) + expect(result).to be_a(ForestAdminDatasourceCustomizer::Decorators::Action::FormLayoutElement::PageElement) + expect(result.elements[0].label).to eq('Send a notification') + expect(result.elements[1].label).to eq('Notification message') + expect(result.next_button_label).to be_a(Proc) + expect(result.previous_button_label).to be_a(Proc) + end + + it 'raises an exception when there is no elements' do + element.delete(:elements) + expect { described_class.build_layout_element(element) }.to raise_error(ForestAdminDatasourceToolkit::Exceptions::ForestException, "🌳🌳🌳 Using 'elements' in a 'Page' configuration is mandatory") + end + + it 'raises an error when element contains a Page' do + element[:elements] = [element] + expect { described_class.build_layout_element(element) }.to raise_error(ForestAdminDatasourceToolkit::Exceptions::ForestException, "🌳🌳🌳 'Page' component cannot be used within 'elements'") + end + end + 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 a226155bd..e1865e49f 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 @@ -37,6 +37,15 @@ def initialize( def watch_changes? @watch_changes end + + def to_h + result = {} + instance_variables.each do |attribute| + result[attribute.to_s.delete('@').to_sym] = instance_variable_get(attribute) + end + + result + end end end end diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/actions/action_field_factory.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/actions/action_field_factory.rb index 0a7fd1183..074e785bc 100644 --- a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/actions/action_field_factory.rb +++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/actions/action_field_factory.rb @@ -3,7 +3,7 @@ module Components module Actions class ActionFieldFactory def self.build(field) - if field.key? :widget + if field.key?(:widget) && !field[:widget].nil? build_widget(field) elsif field[:type] == 'Layout' build_layout_element(field) @@ -21,6 +21,10 @@ def self.build_layout_element(field) when 'Row' return ActionLayoutElement::RowElement.new(**field) unless field[:fields].empty? + nil + when 'Page' + return ActionLayoutElement::PageElement.new(**field) unless field[:elements].empty? + nil end end diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/actions/action_layout_element.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/actions/action_layout_element.rb index 3ab0569b3..410fb41a4 100644 --- a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/actions/action_layout_element.rb +++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/actions/action_layout_element.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/string/inflections' + module ForestAdminDatasourceToolkit module Components module Actions @@ -49,7 +51,31 @@ class RowElement < BaseLayoutElement def initialize(fields:, **options) super(component: 'Row', **options) - @fields = fields + @fields = instantiate_subfields(fields) + end + + def instantiate_subfields(fields) + fields.map do |field| + ActionFieldFactory.build(field.to_h) + end + end + end + + class PageElement < BaseLayoutElement + attr_accessor :elements, :next_button_label, :previous_button_label + + def initialize(elements:, previous_button_label:, next_button_label:, **options) + super(component: 'Page', **options) + @elements = elements + @next_button_label = next_button_label + @previous_button_label = previous_button_label + @elements = instantiate_elements(elements) + end + + def instantiate_elements(elements) + elements.map do |element| + ActionFieldFactory.build(element.to_h) + end end end end