Skip to content

Commit

Permalink
feat(form): support layout elements in smart action form hooks (#691)
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolasalexandre9 authored Oct 17, 2024
1 parent af1bc99 commit d2a2d47
Show file tree
Hide file tree
Showing 8 changed files with 276 additions and 5 deletions.
13 changes: 9 additions & 4 deletions app/controllers/forest_liana/actions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,14 @@ def handle_result(result, action)
return render status: 500, json: { error: 'Error in smart action load hook: hook must return an array of fields' }
end

result = SmartActionFormParser.extract_fields_and_layout(result)

# Validate that the fields are well formed.
begin
# action.hooks[:change] is a hashmap here
# to do the validation, only the hook names are require
change_hooks_name = action.hooks[:change].nil? ? nil : action.hooks[:change].keys
ForestLiana::SmartActionFieldValidator.validate_smart_action_fields(result, action.name, change_hooks_name)
ForestLiana::SmartActionFieldValidator.validate_smart_action_fields(result[:fields], action.name, change_hooks_name)
rescue ForestLiana::Errors::SmartActionInvalidFieldError => invalid_field_error
FOREST_LOGGER.warn invalid_field_error.message
rescue ForestLiana::Errors::SmartActionInvalidFieldHookError => invalid_hook_error
Expand All @@ -67,8 +69,8 @@ def handle_result(result, action)
end

# Apply result on fields (transform the object back to an array), preserve order.
fields = result.map do |field|
updated_field = result.find{|f| f[:field] == field[:field]}
fields = result[:fields].map do |field|
updated_field = result[:fields].find{|f| f[:field] == field[:field]}

# Reset `value` when not present in `enums` (which means `enums` has changed).
if updated_field[:enums].is_a?(Array)
Expand All @@ -88,7 +90,10 @@ def handle_result(result, action)
updated_field.transform_keys { |key| key.to_s.camelize(:lower) }
end

render serializer: nil, json: { fields: fields }, status: :ok
response = { fields: fields }
response[:layout] = result[:layout] unless result[:layout].all? { |element| element[:component] == 'input' }

render serializer: nil, json: response, status: :ok
end

def load
Expand Down
4 changes: 3 additions & 1 deletion app/models/forest_liana/model/action.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ class ForestLiana::Model::Action
extend ActiveModel::Naming

attr_accessor :id, :name, :base_url, :endpoint, :http_method, :fields, :redirect,
:type, :download, :hooks
:type, :download, :hooks, :description, :submit_button_label

def initialize(attributes = {})
if attributes.key?(:global)
Expand Down Expand Up @@ -74,6 +74,8 @@ def initialize(attributes = {})
@type ||= "bulk"
@download ||= false
@hooks = !@hooks.nil? ? @hooks.symbolize_keys : nil
@description ||= nil
@submit_button_label ||= nil
end

def persisted?
Expand Down
2 changes: 2 additions & 0 deletions app/services/forest_liana/apimap_sorter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ class ApimapSorter
'download',
'fields',
'hooks',
'description',
'submit_button_label',
]
KEYS_ACTION_FIELD = [
'field',
Expand Down
69 changes: 69 additions & 0 deletions app/services/forest_liana/smart_action_form_parser.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
module ForestLiana
class SmartActionFormParser
def self.extract_fields_and_layout(form)
fields = []
layout = []
form&.each do |element|
if element[:type] == 'Layout'
validate_layout_element(element)
element[:component] = element[:component].camelize(:lower)
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
layout << { component: 'input', fieldId: element[:field] }
end
end

{ fields: fields, layout: layout }
end

def self.extract_fields_and_layout_for_component(element)
# 'page' is in camel case because at this step the 'component' attribute is already convert for the response
key = element[:component] == 'page' ? :elements : :fields
extract = extract_fields_and_layout(element[key])
element[key] = extract[:layout]

extract
end

def self.validate_layout_element(element)
valid_components = %w[Page Row Separator HtmlBlock]
unless valid_components.include?(element[:component])
raise ForestLiana::Errors::HTTP422Error.new(
"#{element[:component]} is not a valid component. Valid components are #{valid_components.join(' or ')}"
)
end

if element[:component] == 'Page'
unless element[:elements].is_a? Array
raise ForestLiana::Errors::HTTP422Error.new(
"Page components must contain an array of fields or layout elements in property 'elements'"
)
end

if element[:elements].any? { |element| element[:component] === 'Page' }
raise ForestLiana::Errors::HTTP422Error.new('Pages cannot contain other pages')
end
end

if element[:component] == 'Row'
unless element[:fields].is_a? Array
raise ForestLiana::Errors::HTTP422Error.new(
"Row components must contain an array of fields in property 'fields'"
)
end

if element[:fields].any? { |element| element[:type] === 'Layout' }
raise ForestLiana::Errors::HTTP422Error.new('Row components can only contain fields')
end
end
end
end
end
2 changes: 2 additions & 0 deletions lib/forest_liana/schema_file_updater.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ class SchemaFileUpdater
'download',
'fields',
'hooks',
'description',
'submit_button_label',
]
KEYS_ACTION_FIELD = [
'field',
Expand Down
83 changes: 83 additions & 0 deletions spec/dummy/lib/forest_liana/collections/island.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,89 @@ class Forest::Island
}
}

action 'my_action_with_layout',
fields: [foo],
hooks: {
:load => -> (context) {
[
{
type: 'Layout',
component: 'Page',
elements: [
{
type: 'Layout',
component: 'HtmlBlock',
content: '<p>test</p>',
},
{
type: 'Layout',
component: 'Separator',
},
foo,
{
field: 'field 1',
type: 'String',
},
{
type: 'Layout',
component: 'Separator',
},
{
field: 'field 2',
type: 'String',
}
]
},
]
},
:change => {
'on_foo_changed' => -> (context) {
[
{
type: 'Layout',
component: 'Page',
elements: [
{
type: 'Layout',
component: 'HtmlBlock',
content: '<div style="text-align:center;">
<p>
<strong>Hi #{ctx.form_values["firstName"]} #{ctx.form_values["lastName"]}</strong>,
<br/>here you can put
<strong style="color: red;">all the html</strong> you want.
</p>
</div>
<div style="display: flex; flex-flow: row wrap; justify-content: space-around;">
<a href="https://www.w3schools.com" target="_blank">
<img src="https://www.w3schools.com/html/w3schools.jpg">
</a>
<iframe src="https://www.youtube.com/embed/xHPKuu9-yyw?autoplay=1&mute=1"></iframe>
</div>',
},
{
type: 'Layout',
component: 'Separator',
},
foo,
{
field: 'field 1',
type: 'String',
},
{
type: 'Layout',
component: 'Separator',
},
{
field: 'field 2',
type: 'String',
}
]
},
]
}
}
}

action 'fail_action',
fields: [foo],
hooks: {
Expand Down
53 changes: 53 additions & 0 deletions spec/requests/actions_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,57 @@
describe 'hooks' do
island = ForestLiana.apimap.find {|collection| collection.name.to_s == ForestLiana.name_for(Island)}

describe 'call /load on layout form' do
params = {
data: {
attributes: { ids: [1], collection_name: 'Island' }
}
}

it 'should respond 200 with expected response on load' do
post '/forest/actions/my_action_with_layout/hooks/load', params: JSON.dump(params), headers: headers
result = JSON.parse(response.body)

expect(response.status).to eq(200)
expect(result).to eq(
{
"fields" => [
{
"field"=>"foo",
"type"=>"String",
"defaultValue"=>nil,
"enums"=>nil,
"isRequired"=>false,
"isReadOnly"=>false,
"reference"=>nil,
"description"=>nil,
"hook"=>"on_foo_changed",
"position"=>0,
"widgetEdit"=>nil,
"value"=>nil
},
{ "field"=>"field 1", "type"=>"String"},
{"field"=>"field 2", "type"=>"String" }
],
"layout"=>[
{
"type"=>"Layout",
"component"=>"page",
"elements"=>[
{"type"=>"Layout", "component"=>"htmlBlock", "content"=>"<p>test</p>"},
{"type"=>"Layout", "component"=>"separator"},
{"component"=>"input", "fieldId"=>"foo"},
{"component"=>"input", "fieldId"=>"field 1"},
{"type"=>"Layout", "component"=>"separator"},
{"component"=>"input", "fieldId"=>"field 2"}
]
}
]
}
)
end
end

describe 'call /load' do
params = {
data: {
Expand All @@ -54,6 +105,8 @@
foo = action.fields.select { |field| field[:field] == 'foo' }.first
expect(response.status).to eq(200)
expect(JSON.parse(response.body)).to eq({'fields' => [foo.merge({:value => nil}).transform_keys { |key| key.to_s.camelize(:lower) }.stringify_keys]})
# action form without layout elements should not have the key layout
expect(JSON.parse(response.body)).not_to have_key('layout')
end

it 'should respond 422 with bad params' do
Expand Down
55 changes: 55 additions & 0 deletions spec/services/forest_liana/smart_action_form_parser_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
module ForestLiana
describe SmartActionFormParser do
describe "self.validate_layout_element" do
it "raise an error with an invalid component" do
expect { SmartActionFormParser.validate_layout_element({ type: 'Layout', component: 'foo' }) }
.to raise_error(
ForestLiana::Errors::HTTP422Error,
'foo is not a valid component. Valid components are Page or Row or Separator or HtmlBlock'
)
end

it "raise an error with an invalid Page" do
expect do
SmartActionFormParser.validate_layout_element(
{ type: 'Layout', component: 'Page', elements: 'foo' }
)
end.to raise_error(
ForestLiana::Errors::HTTP422Error,
"Page components must contain an array of fields or layout elements in property 'elements'"
)
end

it "raise an error with a Page that contains page" do
expect do
SmartActionFormParser.validate_layout_element(
{ type: 'Layout', component: 'Page', elements: [{ type: 'Layout', component: 'Page', elements: [] }] }
)
end.to raise_error(ForestLiana::Errors::HTTP422Error, 'Pages cannot contain other pages')
end

it "should raise an error with an invalid Row" do
expect do
SmartActionFormParser.validate_layout_element(
{ type: 'Layout', component: 'Row', fields: 'foo' }
)
end.to raise_error(
ForestLiana::Errors::HTTP422Error,
"Row components must contain an array of fields in property 'fields'"
)
end

it "raise an error with a row that contains layout element" do
expect do
SmartActionFormParser.validate_layout_element(
{
type: 'Layout',
component: 'Row',
fields: [ { type: 'Layout', component: 'HtmlBlock', fields: 'Row components can only contain fields' }]
}
)
end.to raise_error(ForestLiana::Errors::HTTP422Error, 'Row components can only contain fields')
end
end
end
end

0 comments on commit d2a2d47

Please sign in to comment.