Skip to content

Commit d2a2d47

Browse files
feat(form): support layout elements in smart action form hooks (#691)
1 parent af1bc99 commit d2a2d47

File tree

8 files changed

+276
-5
lines changed

8 files changed

+276
-5
lines changed

app/controllers/forest_liana/actions_controller.rb

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,14 @@ def handle_result(result, action)
5252
return render status: 500, json: { error: 'Error in smart action load hook: hook must return an array of fields' }
5353
end
5454

55+
result = SmartActionFormParser.extract_fields_and_layout(result)
56+
5557
# Validate that the fields are well formed.
5658
begin
5759
# action.hooks[:change] is a hashmap here
5860
# to do the validation, only the hook names are require
5961
change_hooks_name = action.hooks[:change].nil? ? nil : action.hooks[:change].keys
60-
ForestLiana::SmartActionFieldValidator.validate_smart_action_fields(result, action.name, change_hooks_name)
62+
ForestLiana::SmartActionFieldValidator.validate_smart_action_fields(result[:fields], action.name, change_hooks_name)
6163
rescue ForestLiana::Errors::SmartActionInvalidFieldError => invalid_field_error
6264
FOREST_LOGGER.warn invalid_field_error.message
6365
rescue ForestLiana::Errors::SmartActionInvalidFieldHookError => invalid_hook_error
@@ -67,8 +69,8 @@ def handle_result(result, action)
6769
end
6870

6971
# Apply result on fields (transform the object back to an array), preserve order.
70-
fields = result.map do |field|
71-
updated_field = result.find{|f| f[:field] == field[:field]}
72+
fields = result[:fields].map do |field|
73+
updated_field = result[:fields].find{|f| f[:field] == field[:field]}
7274

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

91-
render serializer: nil, json: { fields: fields }, status: :ok
93+
response = { fields: fields }
94+
response[:layout] = result[:layout] unless result[:layout].all? { |element| element[:component] == 'input' }
95+
96+
render serializer: nil, json: response, status: :ok
9297
end
9398

9499
def load

app/models/forest_liana/model/action.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ class ForestLiana::Model::Action
55
extend ActiveModel::Naming
66

77
attr_accessor :id, :name, :base_url, :endpoint, :http_method, :fields, :redirect,
8-
:type, :download, :hooks
8+
:type, :download, :hooks, :description, :submit_button_label
99

1010
def initialize(attributes = {})
1111
if attributes.key?(:global)
@@ -74,6 +74,8 @@ def initialize(attributes = {})
7474
@type ||= "bulk"
7575
@download ||= false
7676
@hooks = !@hooks.nil? ? @hooks.symbolize_keys : nil
77+
@description ||= nil
78+
@submit_button_label ||= nil
7779
end
7880

7981
def persisted?

app/services/forest_liana/apimap_sorter.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ class ApimapSorter
4141
'download',
4242
'fields',
4343
'hooks',
44+
'description',
45+
'submit_button_label',
4446
]
4547
KEYS_ACTION_FIELD = [
4648
'field',
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
module ForestLiana
2+
class SmartActionFormParser
3+
def self.extract_fields_and_layout(form)
4+
fields = []
5+
layout = []
6+
form&.each do |element|
7+
if element[:type] == 'Layout'
8+
validate_layout_element(element)
9+
element[:component] = element[:component].camelize(:lower)
10+
if %w[page row].include?(element[:component])
11+
extract = extract_fields_and_layout_for_component(element)
12+
layout << element
13+
fields.concat(extract[:fields])
14+
else
15+
layout << element
16+
end
17+
else
18+
fields << element
19+
# frontend rule
20+
layout << { component: 'input', fieldId: element[:field] }
21+
end
22+
end
23+
24+
{ fields: fields, layout: layout }
25+
end
26+
27+
def self.extract_fields_and_layout_for_component(element)
28+
# 'page' is in camel case because at this step the 'component' attribute is already convert for the response
29+
key = element[:component] == 'page' ? :elements : :fields
30+
extract = extract_fields_and_layout(element[key])
31+
element[key] = extract[:layout]
32+
33+
extract
34+
end
35+
36+
def self.validate_layout_element(element)
37+
valid_components = %w[Page Row Separator HtmlBlock]
38+
unless valid_components.include?(element[:component])
39+
raise ForestLiana::Errors::HTTP422Error.new(
40+
"#{element[:component]} is not a valid component. Valid components are #{valid_components.join(' or ')}"
41+
)
42+
end
43+
44+
if element[:component] == 'Page'
45+
unless element[:elements].is_a? Array
46+
raise ForestLiana::Errors::HTTP422Error.new(
47+
"Page components must contain an array of fields or layout elements in property 'elements'"
48+
)
49+
end
50+
51+
if element[:elements].any? { |element| element[:component] === 'Page' }
52+
raise ForestLiana::Errors::HTTP422Error.new('Pages cannot contain other pages')
53+
end
54+
end
55+
56+
if element[:component] == 'Row'
57+
unless element[:fields].is_a? Array
58+
raise ForestLiana::Errors::HTTP422Error.new(
59+
"Row components must contain an array of fields in property 'fields'"
60+
)
61+
end
62+
63+
if element[:fields].any? { |element| element[:type] === 'Layout' }
64+
raise ForestLiana::Errors::HTTP422Error.new('Row components can only contain fields')
65+
end
66+
end
67+
end
68+
end
69+
end

lib/forest_liana/schema_file_updater.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ class SchemaFileUpdater
5252
'download',
5353
'fields',
5454
'hooks',
55+
'description',
56+
'submit_button_label',
5557
]
5658
KEYS_ACTION_FIELD = [
5759
'field',

spec/dummy/lib/forest_liana/collections/island.rb

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,89 @@ class Forest::Island
4343
}
4444
}
4545

46+
action 'my_action_with_layout',
47+
fields: [foo],
48+
hooks: {
49+
:load => -> (context) {
50+
[
51+
{
52+
type: 'Layout',
53+
component: 'Page',
54+
elements: [
55+
{
56+
type: 'Layout',
57+
component: 'HtmlBlock',
58+
content: '<p>test</p>',
59+
},
60+
{
61+
type: 'Layout',
62+
component: 'Separator',
63+
},
64+
foo,
65+
{
66+
field: 'field 1',
67+
type: 'String',
68+
},
69+
{
70+
type: 'Layout',
71+
component: 'Separator',
72+
},
73+
{
74+
field: 'field 2',
75+
type: 'String',
76+
}
77+
]
78+
},
79+
]
80+
},
81+
:change => {
82+
'on_foo_changed' => -> (context) {
83+
[
84+
{
85+
type: 'Layout',
86+
component: 'Page',
87+
elements: [
88+
{
89+
type: 'Layout',
90+
component: 'HtmlBlock',
91+
content: '<div style="text-align:center;">
92+
<p>
93+
<strong>Hi #{ctx.form_values["firstName"]} #{ctx.form_values["lastName"]}</strong>,
94+
<br/>here you can put
95+
<strong style="color: red;">all the html</strong> you want.
96+
</p>
97+
</div>
98+
<div style="display: flex; flex-flow: row wrap; justify-content: space-around;">
99+
<a href="https://www.w3schools.com" target="_blank">
100+
<img src="https://www.w3schools.com/html/w3schools.jpg">
101+
</a>
102+
<iframe src="https://www.youtube.com/embed/xHPKuu9-yyw?autoplay=1&mute=1"></iframe>
103+
</div>',
104+
},
105+
{
106+
type: 'Layout',
107+
component: 'Separator',
108+
},
109+
foo,
110+
{
111+
field: 'field 1',
112+
type: 'String',
113+
},
114+
{
115+
type: 'Layout',
116+
component: 'Separator',
117+
},
118+
{
119+
field: 'field 2',
120+
type: 'String',
121+
}
122+
]
123+
},
124+
]
125+
}
126+
}
127+
}
128+
46129
action 'fail_action',
47130
fields: [foo],
48131
hooks: {

spec/requests/actions_controller_spec.rb

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,57 @@
4141
describe 'hooks' do
4242
island = ForestLiana.apimap.find {|collection| collection.name.to_s == ForestLiana.name_for(Island)}
4343

44+
describe 'call /load on layout form' do
45+
params = {
46+
data: {
47+
attributes: { ids: [1], collection_name: 'Island' }
48+
}
49+
}
50+
51+
it 'should respond 200 with expected response on load' do
52+
post '/forest/actions/my_action_with_layout/hooks/load', params: JSON.dump(params), headers: headers
53+
result = JSON.parse(response.body)
54+
55+
expect(response.status).to eq(200)
56+
expect(result).to eq(
57+
{
58+
"fields" => [
59+
{
60+
"field"=>"foo",
61+
"type"=>"String",
62+
"defaultValue"=>nil,
63+
"enums"=>nil,
64+
"isRequired"=>false,
65+
"isReadOnly"=>false,
66+
"reference"=>nil,
67+
"description"=>nil,
68+
"hook"=>"on_foo_changed",
69+
"position"=>0,
70+
"widgetEdit"=>nil,
71+
"value"=>nil
72+
},
73+
{ "field"=>"field 1", "type"=>"String"},
74+
{"field"=>"field 2", "type"=>"String" }
75+
],
76+
"layout"=>[
77+
{
78+
"type"=>"Layout",
79+
"component"=>"page",
80+
"elements"=>[
81+
{"type"=>"Layout", "component"=>"htmlBlock", "content"=>"<p>test</p>"},
82+
{"type"=>"Layout", "component"=>"separator"},
83+
{"component"=>"input", "fieldId"=>"foo"},
84+
{"component"=>"input", "fieldId"=>"field 1"},
85+
{"type"=>"Layout", "component"=>"separator"},
86+
{"component"=>"input", "fieldId"=>"field 2"}
87+
]
88+
}
89+
]
90+
}
91+
)
92+
end
93+
end
94+
4495
describe 'call /load' do
4596
params = {
4697
data: {
@@ -54,6 +105,8 @@
54105
foo = action.fields.select { |field| field[:field] == 'foo' }.first
55106
expect(response.status).to eq(200)
56107
expect(JSON.parse(response.body)).to eq({'fields' => [foo.merge({:value => nil}).transform_keys { |key| key.to_s.camelize(:lower) }.stringify_keys]})
108+
# action form without layout elements should not have the key layout
109+
expect(JSON.parse(response.body)).not_to have_key('layout')
57110
end
58111

59112
it 'should respond 422 with bad params' do
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
module ForestLiana
2+
describe SmartActionFormParser do
3+
describe "self.validate_layout_element" do
4+
it "raise an error with an invalid component" do
5+
expect { SmartActionFormParser.validate_layout_element({ type: 'Layout', component: 'foo' }) }
6+
.to raise_error(
7+
ForestLiana::Errors::HTTP422Error,
8+
'foo is not a valid component. Valid components are Page or Row or Separator or HtmlBlock'
9+
)
10+
end
11+
12+
it "raise an error with an invalid Page" do
13+
expect do
14+
SmartActionFormParser.validate_layout_element(
15+
{ type: 'Layout', component: 'Page', elements: 'foo' }
16+
)
17+
end.to raise_error(
18+
ForestLiana::Errors::HTTP422Error,
19+
"Page components must contain an array of fields or layout elements in property 'elements'"
20+
)
21+
end
22+
23+
it "raise an error with a Page that contains page" do
24+
expect do
25+
SmartActionFormParser.validate_layout_element(
26+
{ type: 'Layout', component: 'Page', elements: [{ type: 'Layout', component: 'Page', elements: [] }] }
27+
)
28+
end.to raise_error(ForestLiana::Errors::HTTP422Error, 'Pages cannot contain other pages')
29+
end
30+
31+
it "should raise an error with an invalid Row" do
32+
expect do
33+
SmartActionFormParser.validate_layout_element(
34+
{ type: 'Layout', component: 'Row', fields: 'foo' }
35+
)
36+
end.to raise_error(
37+
ForestLiana::Errors::HTTP422Error,
38+
"Row components must contain an array of fields in property 'fields'"
39+
)
40+
end
41+
42+
it "raise an error with a row that contains layout element" do
43+
expect do
44+
SmartActionFormParser.validate_layout_element(
45+
{
46+
type: 'Layout',
47+
component: 'Row',
48+
fields: [ { type: 'Layout', component: 'HtmlBlock', fields: 'Row components can only contain fields' }]
49+
}
50+
)
51+
end.to raise_error(ForestLiana::Errors::HTTP422Error, 'Row components can only contain fields')
52+
end
53+
end
54+
end
55+
end

0 commit comments

Comments
 (0)