formalist 0.6.0 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/Gemfile +1 -1
- data/lib/formalist/child_forms/builder.rb +46 -0
- data/lib/formalist/child_forms/child_form.rb +48 -0
- data/lib/formalist/child_forms/params_processor.rb +30 -0
- data/lib/formalist/child_forms/validity_check.rb +30 -0
- data/lib/formalist/elements/form_field.rb +25 -0
- data/lib/formalist/elements/many.rb +6 -12
- data/lib/formalist/elements/many_forms.rb +115 -0
- data/lib/formalist/elements.rb +4 -0
- data/lib/formalist/form/validity_check.rb +11 -2
- data/lib/formalist/rich_text/embedded_form_compiler.rb +1 -1
- data/lib/formalist/rich_text/embedded_forms_container/registration.rb +4 -1
- data/lib/formalist/rich_text/rendering/embedded_form_renderer.rb +22 -3
- data/lib/formalist/version.rb +1 -1
- metadata +13 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e0d3e4752a943653fc73963ad57981a923b541b23e4d621fbe938bdd07bb42ff
|
4
|
+
data.tar.gz: 20ec653404675c1ac5dfae830151a383450c908d4e46a8f4edf32dc0bf989ae2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: da69af398ecc1393915e7f92d86402f16ed0af4be7b60fe99cabb4229f1f44947fb6bca4ce869911e7643fb6188df4d0c3f3f4a8903311195adaafd4f0f35009
|
7
|
+
data.tar.gz: 4e54bf6266a30d01cbd6cc037871325dc9c503c55252c4e53ab062f93b113976ebc14a02e9b16e8b2b385608f1aeac7d6472d042fcf94e13b73adc0ab2bffeb0
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,11 @@
|
|
1
|
+
# 0.7.0 / 2022-04-07
|
2
|
+
|
3
|
+
### Added
|
4
|
+
|
5
|
+
- Add support a field with an arbitrary list of forms. Supporting updates in formalist-standard-react@^4.2.0
|
6
|
+
- Add namespace and paths options to embedded form renderer
|
7
|
+
- Support dry-schema/dry-validation 1.0
|
8
|
+
|
1
9
|
# 0.6.0 / 2020-05-06
|
2
10
|
|
3
11
|
### Changed
|
data/Gemfile
CHANGED
@@ -0,0 +1,46 @@
|
|
1
|
+
require "json"
|
2
|
+
require_relative "child_form"
|
3
|
+
|
4
|
+
module Formalist
|
5
|
+
module ChildForms
|
6
|
+
class Builder
|
7
|
+
attr_reader :embedded_forms
|
8
|
+
MissingFormDefinitionError = Class.new(StandardError)
|
9
|
+
|
10
|
+
def initialize(embedded_form_collection)
|
11
|
+
@embedded_forms = embedded_form_collection
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(input)
|
15
|
+
return input if input.nil?
|
16
|
+
input.map { |node| visit(node) }
|
17
|
+
end
|
18
|
+
alias_method :[], :call
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def visit(node)
|
23
|
+
name, data = node.values_at(:name, :data)
|
24
|
+
|
25
|
+
embedded_form = embedded_forms[name]
|
26
|
+
if embedded_form.nil?
|
27
|
+
raise MissingFormDefinitionError, "Form +#{embedded_forms[name]}+ is missing from the embeddable forms collection"
|
28
|
+
end
|
29
|
+
child_form(name, embedded_form).fill(input: data)
|
30
|
+
end
|
31
|
+
|
32
|
+
def child_form(name, embedded_form)
|
33
|
+
ChildForm.build(
|
34
|
+
name: name,
|
35
|
+
attributes: {
|
36
|
+
label: embedded_form.label,
|
37
|
+
form: embedded_form.form,
|
38
|
+
schema: embedded_form.schema,
|
39
|
+
input_processor: embedded_form.input_processor,
|
40
|
+
preview_image_url: embedded_form.preview_image_url
|
41
|
+
}
|
42
|
+
)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require "formalist/element"
|
2
|
+
|
3
|
+
module Formalist
|
4
|
+
module ChildForms
|
5
|
+
class ChildForm < Element
|
6
|
+
DEFAULT_INPUT_PROCESSOR = -> input { input }.freeze
|
7
|
+
|
8
|
+
attribute :label
|
9
|
+
attribute :form
|
10
|
+
attribute :schema
|
11
|
+
attribute :input_processor, default: DEFAULT_INPUT_PROCESSOR
|
12
|
+
attribute :preview_image_url
|
13
|
+
|
14
|
+
def fill(input: {}, errors: {})
|
15
|
+
super(input: form_input_ast(input), errors: errors.to_a)
|
16
|
+
end
|
17
|
+
|
18
|
+
def attributes
|
19
|
+
super.merge(form: form_attribute_ast)
|
20
|
+
end
|
21
|
+
|
22
|
+
def form_attribute_ast
|
23
|
+
@attributes[:form].to_ast
|
24
|
+
end
|
25
|
+
|
26
|
+
def form_input_ast(data)
|
27
|
+
# Run the raw data through the validation schema
|
28
|
+
validation = @attributes[:schema].(data)
|
29
|
+
|
30
|
+
# And then through the embedded form's input processor (which may add
|
31
|
+
# extra system-generated information necessary for the form to render
|
32
|
+
# fully)
|
33
|
+
input = @attributes[:input_processor].(validation.to_h)
|
34
|
+
|
35
|
+
@attributes[:form].fill(input: input, errors: validation.errors.to_h).to_ast
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_ast
|
39
|
+
[:child_form, [
|
40
|
+
name,
|
41
|
+
type,
|
42
|
+
input,
|
43
|
+
Element::Attributes.new(attributes).to_ast,
|
44
|
+
]]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require "json"
|
2
|
+
require_relative "builder"
|
3
|
+
|
4
|
+
module Formalist
|
5
|
+
module ChildForms
|
6
|
+
class ParamsProcessor
|
7
|
+
attr_reader :embedded_forms
|
8
|
+
|
9
|
+
def initialize(embedded_form_collection)
|
10
|
+
@embedded_forms = embedded_form_collection
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(input)
|
14
|
+
return input if input.nil?
|
15
|
+
input.inject([]) { |output, node| output.push(process(node)) }
|
16
|
+
end
|
17
|
+
alias_method :[], :call
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def process(node)
|
22
|
+
name, data = node.values_at(:name, :data)
|
23
|
+
|
24
|
+
validation = embedded_forms[name].schema.(data)
|
25
|
+
node.merge(data: validation.to_h)
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require "json"
|
2
|
+
require_relative "builder"
|
3
|
+
|
4
|
+
module Formalist
|
5
|
+
module ChildForms
|
6
|
+
class ValidityCheck
|
7
|
+
attr_reader :embedded_forms
|
8
|
+
|
9
|
+
def initialize(embedded_form_collection)
|
10
|
+
@embedded_forms = embedded_form_collection
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(input)
|
14
|
+
return input if input.nil?
|
15
|
+
input.map { |node| valid?(node) }.all?
|
16
|
+
end
|
17
|
+
alias_method :[], :call
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def valid?(node)
|
22
|
+
name, data = node.values_at(:name, :data)
|
23
|
+
|
24
|
+
validation = embedded_forms[name].schema
|
25
|
+
validation.(data).success?
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require "formalist/child_forms/child_form"
|
2
|
+
|
3
|
+
module Formalist
|
4
|
+
class Elements
|
5
|
+
class FormField < ChildForms::ChildForm
|
6
|
+
attribute :hint
|
7
|
+
|
8
|
+
def fill(input: {}, errors: {})
|
9
|
+
input = input[name]
|
10
|
+
errors = errors[name].to_a
|
11
|
+
|
12
|
+
super(input: input, errors: errors)
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_ast
|
16
|
+
[:form_field, [
|
17
|
+
name,
|
18
|
+
type,
|
19
|
+
input,
|
20
|
+
Element::Attributes.new(attributes).to_ast,
|
21
|
+
]]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -5,10 +5,15 @@ module Formalist
|
|
5
5
|
class Many < Element
|
6
6
|
attribute :action_label
|
7
7
|
attribute :sortable
|
8
|
+
attribute :moveable
|
8
9
|
attribute :label
|
9
10
|
attribute :max_height
|
10
11
|
attribute :placeholder
|
11
12
|
attribute :validation
|
13
|
+
attribute :allow_create, default: true
|
14
|
+
attribute :allow_update, default: true
|
15
|
+
attribute :allow_destroy, default: true
|
16
|
+
attribute :allow_reorder, default: true
|
12
17
|
|
13
18
|
# @api private
|
14
19
|
attr_reader :child_template
|
@@ -28,7 +33,7 @@ module Formalist
|
|
28
33
|
# @api private
|
29
34
|
def fill(input: {}, errors: {})
|
30
35
|
input = input.fetch(name) { [] }
|
31
|
-
errors = errors
|
36
|
+
errors = errors[name] || {}
|
32
37
|
|
33
38
|
# Errors look like this when they are on the array itself: ["size cannot be greater than 2"]
|
34
39
|
# Errors look like this when they are on children: {0=>{:summary=>["must be filled"]}
|
@@ -47,17 +52,6 @@ module Formalist
|
|
47
52
|
)
|
48
53
|
end
|
49
54
|
|
50
|
-
# Until we can put defaults on `Types::Bool`, supply them here
|
51
|
-
# @api private
|
52
|
-
def attributes
|
53
|
-
{
|
54
|
-
allow_create: true,
|
55
|
-
allow_update: true,
|
56
|
-
allow_destroy: true,
|
57
|
-
allow_reorder: true,
|
58
|
-
}.merge(super)
|
59
|
-
end
|
60
|
-
|
61
55
|
# Converts a collection of "many" repeating elements into an abstract
|
62
56
|
# syntax tree.
|
63
57
|
#
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require "formalist/element"
|
2
|
+
require "formalist/child_forms/builder"
|
3
|
+
|
4
|
+
module Formalist
|
5
|
+
class Elements
|
6
|
+
class ManyForms < Element
|
7
|
+
attribute :action_label
|
8
|
+
attribute :sortable
|
9
|
+
attribute :moveable
|
10
|
+
attribute :label
|
11
|
+
attribute :max_height
|
12
|
+
attribute :placeholder
|
13
|
+
attribute :embeddable_forms
|
14
|
+
attribute :validation
|
15
|
+
attribute :allow_create, default: true
|
16
|
+
attribute :allow_update, default: true
|
17
|
+
attribute :allow_destroy, default: true
|
18
|
+
attribute :allow_reorder, default: true
|
19
|
+
|
20
|
+
# FIXME: it would be tidier to have a reader method for each attribute
|
21
|
+
def attributes
|
22
|
+
super.merge(embeddable_forms: embeddable_forms_ast)
|
23
|
+
end
|
24
|
+
|
25
|
+
# @api private
|
26
|
+
def fill(input: {}, errors: {})
|
27
|
+
input = input[name] || []
|
28
|
+
errors = errors[name].to_a
|
29
|
+
|
30
|
+
children = child_form_builder.(input)
|
31
|
+
|
32
|
+
super(
|
33
|
+
input: input,
|
34
|
+
errors: errors,
|
35
|
+
children: children,
|
36
|
+
)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Replace the form objects with their AST
|
40
|
+
def embeddable_forms_ast
|
41
|
+
@attributes[:embeddable_forms].to_h.map { |key, attrs|
|
42
|
+
template_attrs = attrs.slice(:label, :preview_image_url)
|
43
|
+
|
44
|
+
[
|
45
|
+
key,
|
46
|
+
attrs.merge(
|
47
|
+
form: attrs[:form].to_ast,
|
48
|
+
attributes_template: Element::Attributes.new(template_attrs).to_ast
|
49
|
+
)
|
50
|
+
]
|
51
|
+
}.to_h
|
52
|
+
end
|
53
|
+
|
54
|
+
def child_form_builder
|
55
|
+
ChildForms::Builder.new(@attributes[:embeddable_forms])
|
56
|
+
end
|
57
|
+
|
58
|
+
# Converts a collection of "many" repeating elements into an abstract
|
59
|
+
# syntax tree.
|
60
|
+
#
|
61
|
+
# It takes the following format:
|
62
|
+
#
|
63
|
+
# ```
|
64
|
+
# [:many_forms, [params]]
|
65
|
+
# ```
|
66
|
+
#
|
67
|
+
# With the following parameters:
|
68
|
+
#
|
69
|
+
# 1. Collection name
|
70
|
+
# 2. Custom form element type (or `:many_forms` otherwise)
|
71
|
+
# 3. Collection-level error messages
|
72
|
+
# 4. Form element attributes
|
73
|
+
# 6. Child elements, one for each of the entries in the input data (or
|
74
|
+
# none, if there is no or empty input data)
|
75
|
+
#
|
76
|
+
# @see Formalist::Element::Attributes#to_ast "Form element attributes" structure
|
77
|
+
#
|
78
|
+
# @example "components" collection
|
79
|
+
# many_forms.to_ast
|
80
|
+
# # => [:many_forms, [
|
81
|
+
# :components,
|
82
|
+
# :many_forms,
|
83
|
+
# ["components size cannot be less than 3"],
|
84
|
+
# [:object, [
|
85
|
+
# [:allow_create, [:value, [true]]],
|
86
|
+
# [:allow_update, [:value, [true]]],
|
87
|
+
# [:allow_destroy, [:value, [true]]],
|
88
|
+
# [:allow_reorder, [:value, [true]]]
|
89
|
+
# ]],
|
90
|
+
# [
|
91
|
+
# [
|
92
|
+
# [:child_form,
|
93
|
+
# [:image_with_captions,
|
94
|
+
# :child_form,
|
95
|
+
# [[:field, [:image_id, :text_field, "", ["must be filled"], [:object, []]]], [:field, [:caption, :text_field, "Large panda", [], [:object, []]]]],
|
96
|
+
# [:object, []]
|
97
|
+
# ]
|
98
|
+
# ]]
|
99
|
+
#
|
100
|
+
# @return [Array] the collection as an abstract syntax tree.
|
101
|
+
def to_ast
|
102
|
+
local_errors = errors.is_a?(Array) ? errors : []
|
103
|
+
|
104
|
+
[:many_forms, [
|
105
|
+
name,
|
106
|
+
type,
|
107
|
+
local_errors,
|
108
|
+
Element::Attributes.new(attributes).to_ast,
|
109
|
+
children.map(&:to_ast)
|
110
|
+
]]
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
data/lib/formalist/elements.rb
CHANGED
@@ -4,6 +4,8 @@ require "formalist/elements/compound_field"
|
|
4
4
|
require "formalist/elements/field"
|
5
5
|
require "formalist/elements/group"
|
6
6
|
require "formalist/elements/many"
|
7
|
+
require "formalist/elements/many_forms"
|
8
|
+
require "formalist/elements/form_field"
|
7
9
|
require "formalist/elements/section"
|
8
10
|
|
9
11
|
module Formalist
|
@@ -15,6 +17,8 @@ module Formalist
|
|
15
17
|
register :field, Field
|
16
18
|
register :group, Group
|
17
19
|
register :many, Many
|
20
|
+
register :many_forms, ManyForms
|
21
|
+
register :form_field, FormField
|
18
22
|
register :section, Section
|
19
23
|
end
|
20
24
|
end
|
@@ -41,11 +41,20 @@ module Formalist
|
|
41
41
|
def visit_many(node)
|
42
42
|
_name, _type, errors, _attributes, _child_template, children = node
|
43
43
|
|
44
|
-
# The `children
|
45
|
-
#
|
44
|
+
# The `children parameter for `many` elements is nested since there are
|
45
|
+
# many groups of elements, we need to flatten to traverse them all
|
46
46
|
errors.empty? && children.flatten(1).map { |child| visit(child) }.all?
|
47
47
|
end
|
48
48
|
|
49
|
+
# TODO work out what to do with this.
|
50
|
+
# I think it's only relevant to many_forms
|
51
|
+
# nested in rich text ast
|
52
|
+
def visit_many_forms(node)
|
53
|
+
_name, _type, errors, _attributes, children = node
|
54
|
+
|
55
|
+
errors.empty? && children.map { |child| visit(child[:form]) }.all?
|
56
|
+
end
|
57
|
+
|
49
58
|
def visit_section(node)
|
50
59
|
_name, _type, _attributes, children = node
|
51
60
|
|
@@ -79,7 +79,7 @@ module Formalist
|
|
79
79
|
# fully)
|
80
80
|
input = embedded_form.input_processor.(validation.to_h)
|
81
81
|
|
82
|
-
embedded_form.form.fill(input: input, errors: validation.
|
82
|
+
embedded_form.form.fill(input: input, errors: validation.errors.to_h).to_ast
|
83
83
|
end
|
84
84
|
end
|
85
85
|
end
|
@@ -8,12 +8,14 @@ module Formalist
|
|
8
8
|
attr_reader :form
|
9
9
|
attr_reader :schema
|
10
10
|
attr_reader :input_processor
|
11
|
+
attr_reader :preview_image_url
|
11
12
|
|
12
|
-
def initialize(label:, form:, schema:, input_processor: DEFAULT_INPUT_PROCESSOR)
|
13
|
+
def initialize(label:, form:, schema:, preview_image_url: nil, input_processor: DEFAULT_INPUT_PROCESSOR)
|
13
14
|
@label = label
|
14
15
|
@form = form
|
15
16
|
@schema = schema
|
16
17
|
@input_processor = input_processor
|
18
|
+
@preview_image_url = preview_image_url
|
17
19
|
end
|
18
20
|
|
19
21
|
def to_h
|
@@ -22,6 +24,7 @@ module Formalist
|
|
22
24
|
form: form,
|
23
25
|
schema: schema,
|
24
26
|
input_processor: input_processor,
|
27
|
+
preview_image_url: preview_image_url
|
25
28
|
}
|
26
29
|
end
|
27
30
|
end
|
@@ -3,22 +3,41 @@ module Formalist
|
|
3
3
|
module Rendering
|
4
4
|
class EmbeddedFormRenderer
|
5
5
|
attr_reader :container
|
6
|
+
attr_reader :namespace
|
7
|
+
attr_reader :paths
|
6
8
|
attr_reader :options
|
7
9
|
|
8
|
-
def initialize(container = {}, **options)
|
10
|
+
def initialize(container = {}, namespace: nil, paths: [], **options)
|
9
11
|
@container = container
|
12
|
+
@namespace = namespace
|
13
|
+
@paths = paths
|
10
14
|
@options = options
|
11
15
|
end
|
12
16
|
|
13
17
|
def call(form_data)
|
14
18
|
type, data = form_data.values_at(:name, :data)
|
15
19
|
|
16
|
-
|
17
|
-
|
20
|
+
key = resolve_key(type)
|
21
|
+
|
22
|
+
if key
|
23
|
+
container[key].(data, options)
|
18
24
|
else
|
19
25
|
""
|
20
26
|
end
|
21
27
|
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def resolve_key(type)
|
32
|
+
paths.each do |path|
|
33
|
+
path_key = path.tr("/", ".")
|
34
|
+
key = [namespace, path_key, type].compact.join(".")
|
35
|
+
return key if container.key?(key)
|
36
|
+
end
|
37
|
+
|
38
|
+
key = [namespace, type].compact.join(".")
|
39
|
+
return key if container.key?(key)
|
40
|
+
end
|
22
41
|
end
|
23
42
|
end
|
24
43
|
end
|
data/lib/formalist/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: formalist
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tim Riley
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-04-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: dry-configurable
|
@@ -136,7 +136,7 @@ dependencies:
|
|
136
136
|
- - ">="
|
137
137
|
- !ruby/object:Gem::Version
|
138
138
|
version: '0'
|
139
|
-
description:
|
139
|
+
description:
|
140
140
|
email:
|
141
141
|
- tim@icelab.com.au
|
142
142
|
executables: []
|
@@ -155,6 +155,10 @@ files:
|
|
155
155
|
- bin/console
|
156
156
|
- formalist.gemspec
|
157
157
|
- lib/formalist.rb
|
158
|
+
- lib/formalist/child_forms/builder.rb
|
159
|
+
- lib/formalist/child_forms/child_form.rb
|
160
|
+
- lib/formalist/child_forms/params_processor.rb
|
161
|
+
- lib/formalist/child_forms/validity_check.rb
|
158
162
|
- lib/formalist/definition.rb
|
159
163
|
- lib/formalist/element.rb
|
160
164
|
- lib/formalist/element/attributes.rb
|
@@ -163,8 +167,10 @@ files:
|
|
163
167
|
- lib/formalist/elements/attr.rb
|
164
168
|
- lib/formalist/elements/compound_field.rb
|
165
169
|
- lib/formalist/elements/field.rb
|
170
|
+
- lib/formalist/elements/form_field.rb
|
166
171
|
- lib/formalist/elements/group.rb
|
167
172
|
- lib/formalist/elements/many.rb
|
173
|
+
- lib/formalist/elements/many_forms.rb
|
168
174
|
- lib/formalist/elements/section.rb
|
169
175
|
- lib/formalist/elements/standard.rb
|
170
176
|
- lib/formalist/elements/standard/check_box.rb
|
@@ -199,7 +205,7 @@ homepage: https://github.com/icelab/formalist
|
|
199
205
|
licenses:
|
200
206
|
- MIT
|
201
207
|
metadata: {}
|
202
|
-
post_install_message:
|
208
|
+
post_install_message:
|
203
209
|
rdoc_options: []
|
204
210
|
require_paths:
|
205
211
|
- lib
|
@@ -214,8 +220,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
214
220
|
- !ruby/object:Gem::Version
|
215
221
|
version: '0'
|
216
222
|
requirements: []
|
217
|
-
rubygems_version: 3.
|
218
|
-
signing_key:
|
223
|
+
rubygems_version: 3.1.6
|
224
|
+
signing_key:
|
219
225
|
specification_version: 4
|
220
226
|
summary: Flexible form builder
|
221
227
|
test_files: []
|