pakyow-presenter 1.0.0.rc2 → 1.0.0.rc3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/lib/pakyow/{presenter/actions → actions/presenter}/auto_render.rb +2 -2
  3. data/lib/pakyow/plugin/helpers/rendering.rb +15 -2
  4. data/lib/pakyow/presenter.rb +1 -1
  5. data/lib/pakyow/presenter/attributes.rb +8 -0
  6. data/lib/pakyow/presenter/attributes/attribute.rb +0 -1
  7. data/lib/pakyow/presenter/attributes/boolean.rb +0 -1
  8. data/lib/pakyow/presenter/behavior/error_rendering.rb +1 -0
  9. data/lib/pakyow/presenter/behavior/initializing.rb +1 -1
  10. data/lib/pakyow/presenter/behavior/modes.rb +1 -0
  11. data/lib/pakyow/presenter/binder.rb +2 -0
  12. data/lib/pakyow/presenter/binding_parts.rb +1 -0
  13. data/lib/pakyow/presenter/component.rb +1 -4
  14. data/lib/pakyow/presenter/composers/component.rb +1 -0
  15. data/lib/pakyow/presenter/composers/view.rb +1 -0
  16. data/lib/pakyow/presenter/errors.rb +2 -2
  17. data/lib/pakyow/presenter/framework.rb +22 -25
  18. data/lib/pakyow/presenter/presenter.rb +5 -0
  19. data/lib/pakyow/presenter/presenters/endpoint.rb +3 -3
  20. data/lib/pakyow/presenter/presenters/form.rb +5 -5
  21. data/lib/pakyow/presenter/processor.rb +42 -38
  22. data/lib/pakyow/presenter/renderer.rb +6 -1
  23. data/lib/pakyow/presenter/renderer/behavior/cleanup_prototype_nodes.rb +23 -0
  24. data/lib/pakyow/presenter/renderer/behavior/cleanup_unbound_bindings.rb +37 -0
  25. data/lib/pakyow/presenter/renderer/behavior/create_template_nodes.rb +29 -0
  26. data/lib/pakyow/presenter/renderer/behavior/insert_prototype_bar.rb +103 -0
  27. data/lib/pakyow/presenter/renderer/behavior/install_authenticity.rb +44 -0
  28. data/lib/pakyow/presenter/renderer/behavior/place_in_mode.rb +58 -0
  29. data/lib/pakyow/presenter/renderer/behavior/render_components.rb +281 -0
  30. data/lib/pakyow/presenter/renderer/behavior/set_page_title.rb +37 -0
  31. data/lib/pakyow/presenter/renderer/behavior/setup_endpoints.rb +64 -0
  32. data/lib/pakyow/presenter/renderer/behavior/setup_forms.rb +176 -0
  33. data/lib/pakyow/presenter/significant_nodes.rb +2 -2
  34. data/lib/pakyow/presenter/templates.rb +24 -15
  35. data/lib/pakyow/presenter/versioned_view.rb +1 -0
  36. data/lib/pakyow/presenter/view.rb +11 -9
  37. data/lib/pakyow/presenter/views/form.rb +39 -35
  38. data/lib/pakyow/presenter/views/layout.rb +20 -18
  39. data/lib/pakyow/presenter/views/page.rb +47 -45
  40. data/lib/pakyow/presenter/views/partial.rb +17 -15
  41. data/lib/string_doc.rb +3 -1
  42. data/lib/string_doc/attributes.rb +1 -0
  43. data/lib/string_doc/meta_attributes.rb +1 -0
  44. data/lib/string_doc/meta_node.rb +1 -0
  45. data/lib/string_doc/node.rb +1 -0
  46. metadata +19 -20
  47. data/lib/pakyow/presenter/presentable_error.rb +0 -19
  48. data/lib/pakyow/presenter/rendering/actions/cleanup_prototype_nodes.rb +0 -21
  49. data/lib/pakyow/presenter/rendering/actions/cleanup_unbound_bindings.rb +0 -35
  50. data/lib/pakyow/presenter/rendering/actions/create_template_nodes.rb +0 -27
  51. data/lib/pakyow/presenter/rendering/actions/insert_prototype_bar.rb +0 -101
  52. data/lib/pakyow/presenter/rendering/actions/install_authenticity.rb +0 -42
  53. data/lib/pakyow/presenter/rendering/actions/place_in_mode.rb +0 -56
  54. data/lib/pakyow/presenter/rendering/actions/render_components.rb +0 -279
  55. data/lib/pakyow/presenter/rendering/actions/set_page_title.rb +0 -35
  56. data/lib/pakyow/presenter/rendering/actions/setup_endpoints.rb +0 -62
  57. data/lib/pakyow/presenter/rendering/actions/setup_forms.rb +0 -174
@@ -7,7 +7,7 @@ require "pakyow/support/hookable"
7
7
  require "pakyow/support/core_refinements/proc/introspection"
8
8
  require "pakyow/support/core_refinements/string/normalization"
9
9
 
10
- require "pakyow/presenter/rendering/actions/render_components"
10
+ require "pakyow/presenter/renderer/behavior/render_components"
11
11
 
12
12
  require "pakyow/presenter/composers/view"
13
13
 
@@ -155,6 +155,7 @@ module Pakyow
155
155
  connection.rendered
156
156
  end
157
157
 
158
+ # @api private
158
159
  def render_implicitly(connection)
159
160
  view_path = connection.get(:__endpoint_path) || connection.path
160
161
 
@@ -179,6 +180,7 @@ module Pakyow
179
180
  end
180
181
  end
181
182
 
183
+ # @api private
182
184
  def build!(view, app:, modes:, composer:)
183
185
  @__build_fns.each do |fn|
184
186
  options = {}
@@ -199,6 +201,7 @@ module Pakyow
199
201
  end
200
202
  end
201
203
 
204
+ # @api private
202
205
  def attach!(presenter, app:)
203
206
  @__attach_fns.each do |fn|
204
207
  options = {}
@@ -211,12 +214,14 @@ module Pakyow
211
214
  end
212
215
  end
213
216
 
217
+ # @api private
214
218
  def expose!(connection)
215
219
  @__expose_fns.each do |fn|
216
220
  fn.call(connection)
217
221
  end
218
222
  end
219
223
 
224
+ # @api private
220
225
  def find_presenter(app, path)
221
226
  path = String.normalize_path(path)
222
227
  unless presenter = @__presenters_by_path[path]
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pakyow/support/extension"
4
+
5
+ module Pakyow
6
+ module Presenter
7
+ class Renderer
8
+ module Behavior
9
+ module CleanupPrototypeNodes
10
+ extend Support::Extension
11
+
12
+ apply_extension do
13
+ build do |view|
14
+ unless Pakyow.env?(:prototype)
15
+ view.object.each_significant_node(:prototype, descend: true).map(&:itself).each(&:remove)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pakyow/support/extension"
4
+
5
+ module Pakyow
6
+ module Presenter
7
+ class Renderer
8
+ module Behavior
9
+ module CleanupUnboundBindings
10
+ extend Support::Extension
11
+
12
+ apply_extension do
13
+ attach do |presenter|
14
+ unless Pakyow.env?(:prototype)
15
+ # Cleanup unbound bindings. We don't do this in prototype mode because it's important
16
+ # for the prototype to be complete, showing everything to the designer.
17
+ #
18
+ presenter.render node: -> {
19
+ object.find_significant_nodes(:binding, descend: true).map { |node|
20
+ View.from_object(node)
21
+ }
22
+ }, priority: :low do
23
+ # We check that the node is still labeled as a binding in case the node was replaced
24
+ # during a previous transformation with a node that isn't a binding.
25
+ #
26
+ unless !view.object.labeled?(:binding) || view.object.labeled?(:bound) || view.object.labeled?(:failed) || view.object.label(:version) == :empty
27
+ remove
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pakyow/support/extension"
4
+
5
+ module Pakyow
6
+ module Presenter
7
+ class Renderer
8
+ module Behavior
9
+ module CreateTemplateNodes
10
+ extend Support::Extension
11
+
12
+ apply_extension do
13
+ build do |view|
14
+ unless Pakyow.env?(:prototype)
15
+ view.each_binding_scope(descend: true) do |node_with_binding|
16
+ attributes = node_with_binding.attributes.attributes_hash.each_with_object({}) do |(attribute, value), acc|
17
+ acc[attribute] = value if attribute.to_s.start_with?("data")
18
+ end
19
+
20
+ node_with_binding.after("<script type=\"text/template\"#{StringDoc::Attributes.new(attributes).to_s}>#{node_with_binding.render}</script>")
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pakyow/support/extension"
4
+
5
+ module Pakyow
6
+ module Presenter
7
+ class Renderer
8
+ module Behavior
9
+ module InsertPrototypeBar
10
+ extend Support::Extension
11
+
12
+ apply_extension do
13
+ attach do |presenter|
14
+ if Pakyow.env?(:prototype)
15
+ presenter.render node: -> {
16
+ if body = object.find_first_significant_node(:body)
17
+ View.from_object(body)
18
+ end
19
+ } do
20
+ view.object.append_html <<~HTML
21
+ <style>
22
+ .pw-prototype {
23
+ font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;
24
+ display:flex;
25
+ align-items: center;
26
+ position: fixed;
27
+ z-index: 1000;
28
+ right: 0;
29
+ bottom: 0;
30
+ background: #156eed;
31
+ color: #fff;
32
+ font-size: 11px;
33
+ line-height: 11px;
34
+ font-weight: 500;
35
+ border-top-left-radius: 1px;
36
+ padding-left: 5px;
37
+ }
38
+
39
+ .pw-prototype-tag {
40
+ background: #ff8b6c;
41
+ color: #fff;
42
+ text-transform: uppercase;
43
+ font-size: 10px;
44
+ line-height: 12px;
45
+ font-weight: 600;
46
+ padding: 5px 5px 4px 5px;
47
+ margin-left: 10px;
48
+ }
49
+ </style>
50
+
51
+ <div class="pw-prototype">
52
+ #{InsertPrototypeBar.ui_modes_html(view, __modes || [:default])}
53
+
54
+ <div class="pw-prototype-tag">
55
+ Prototype
56
+ </div>
57
+ </div>
58
+ HTML
59
+ end
60
+ end
61
+ end
62
+
63
+ expose do |connection|
64
+ if Pakyow.env?(:prototype)
65
+ connection.set(:__modes, connection.params[:modes])
66
+ end
67
+ end
68
+ end
69
+
70
+ # @api private
71
+ def self.ui_modes_html(view, current_modes)
72
+ current_modes = current_modes.map(&:to_sym)
73
+
74
+ modes = view.object.each_significant_node(:mode).map { |node|
75
+ node.label(:mode)
76
+ }
77
+
78
+ modes.unshift(
79
+ (view.info(:mode) || :default).to_sym
80
+ ).uniq!
81
+
82
+ options = modes.map { |each_mode|
83
+ selected = if current_modes.include?(each_mode)
84
+ " selected=\"selected\""
85
+ else
86
+ ""
87
+ end
88
+
89
+ nice_mode = Support.inflector.humanize(Support.inflector.underscore(each_mode))
90
+ "<option value=\"#{each_mode}\"#{selected}>#{nice_mode}</option>"
91
+ }.join
92
+
93
+ <<~HTML
94
+ UI Mode: <select onchange="document.location = window.location.pathname + '?modes[]=' + this.value " style="-webkit-appearance: none; -moz-appearance: none; -ms-appearance: none; -o-appearance: none; appearance: none; font-size: 11px; font-weight: 500; line-height: 20px; background: none; border: none; color: #fff; outline: none; margin: 0; margin-left: 5px;">
95
+ #{options}
96
+ </select>
97
+ HTML
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pakyow/support/extension"
4
+ require "pakyow/support/message_verifier"
5
+ require "pakyow/support/safe_string"
6
+
7
+ module Pakyow
8
+ module Presenter
9
+ class Renderer
10
+ module Behavior
11
+ module InstallAuthenticity
12
+ extend Support::Extension
13
+
14
+ apply_extension do
15
+ build do |view, app:|
16
+ if app.config.presenter.embed_authenticity_token && head = view.head
17
+ head.append(Support::SafeStringHelpers.html_safe("<meta name=\"pw-authenticity-token\">"))
18
+ head.append(Support::SafeStringHelpers.html_safe("<meta name=\"pw-authenticity-param\" content=\"#{app.config.security.csrf.param}\">"))
19
+ end
20
+ end
21
+
22
+ attach do |presenter|
23
+ presenter.render node: -> {
24
+ node = object.each_significant_node(:meta).find { |meta_node|
25
+ meta_node.attributes[:name] == "pw-authenticity-token"
26
+ }
27
+
28
+ unless node.nil?
29
+ View.from_object(node)
30
+ end
31
+ } do
32
+ attributes[:content] = @presentables[:__verifier].sign(Support::MessageVerifier.key)
33
+ end
34
+ end
35
+
36
+ expose do |connection|
37
+ connection.set(:__verifier, connection.verifier)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pakyow/support/extension"
4
+
5
+ module Pakyow
6
+ module Presenter
7
+ class Renderer
8
+ module Behavior
9
+ module PlaceInMode
10
+ extend Support::Extension
11
+
12
+ apply_extension do
13
+ build do |view, modes:|
14
+ unless Pakyow.env?(:prototype)
15
+ PlaceInMode.perform(view, modes)
16
+ end
17
+ end
18
+
19
+ attach do |presenter|
20
+ if Pakyow.env?(:prototype)
21
+ presenter.render node: -> {
22
+ object.find_significant_nodes(:mode, descend: true).map { |node|
23
+ View.from_object(node)
24
+ }
25
+ } do
26
+ PlaceInMode.perform(view, __modes)
27
+ end
28
+ end
29
+ end
30
+
31
+ expose do |connection|
32
+ if Pakyow.env?(:prototype)
33
+ connection.set(:__modes, connection.params[:modes] || [:default])
34
+ end
35
+ end
36
+ end
37
+
38
+ # @api private
39
+ def self.perform(view, modes)
40
+ if modes.length == 1 && modes.first.to_sym == :default
41
+ modes = view.info(:modes) || modes
42
+ end
43
+
44
+ modes.map!(&:to_sym)
45
+
46
+ if view.object.is_a?(StringDoc::Node) && view.object.significant?(:mode) && !modes.include?(view.object.label(:mode))
47
+ view.remove
48
+ else
49
+ view.object.each_significant_node(:mode, descend: true).select { |node|
50
+ !modes.include?(node.label(:mode))
51
+ }.each(&:remove)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,281 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pakyow/support/extension"
4
+
5
+ require "pakyow/presenter/composers/component"
6
+
7
+ module Pakyow
8
+ module Presenter
9
+ class Renderer
10
+ module Behavior
11
+ module RenderComponents
12
+ extend Support::Extension
13
+
14
+ apply_extension do
15
+ build do |view, app:, composer:, modes:|
16
+ unless Pakyow.env?(:prototype)
17
+ initial_path = case composer
18
+ when Composers::Component
19
+ composer.component_path
20
+ else
21
+ []
22
+ end
23
+
24
+ component_view = case composer
25
+ when Composers::Component
26
+ composer.class.follow_path(composer.component_path, view)
27
+ else
28
+ view
29
+ end
30
+
31
+ RenderComponents.initialize_renderable_components(
32
+ component_view, app: app, composer: composer, modes: modes, path: initial_path
33
+ )
34
+ end
35
+ end
36
+
37
+ expose do |connection|
38
+ # Prevent state from leaking from the component to the rest of the app.
39
+ #
40
+ component_connection = connection.dup
41
+
42
+ # Expose the component connection for performing from each component.
43
+ #
44
+ connection.set(:__component_connection, component_connection)
45
+ end
46
+ end
47
+
48
+ # @api private
49
+ def self.initialize_renderable_components(view, app:, composer:, modes:, path: [])
50
+ view.components.each_with_index do |component_view, i|
51
+ current_path = path.dup
52
+ current_path << i
53
+
54
+ # If view will be rendered from the app, look for the component on the app.
55
+ #
56
+ component_state = if app.is_a?(Plugin) && app.parent.view?(composer.view_path)
57
+ app.parent.state(:component)
58
+ else
59
+ app.state(:component)
60
+ end
61
+
62
+ components = component_view.object.label(:components).each_with_object([]) { |component_label, arr|
63
+ component_class = component_state.find { |component|
64
+ component.__object_name.name == component_label[:name]
65
+ }
66
+
67
+ if component_class
68
+ # Turn the component into a renderable component. Once an instance is attached on the
69
+ # backend, the component will not be traversed by renders from its parent instead being
70
+ # rendered by its own renderer instance.
71
+ #
72
+ # We don't want the same restriction for non-renderable components because a change to
73
+ # the view should not affect how things work on the backend.
74
+ #
75
+ component_label[:renderable] = true
76
+
77
+ arr << {
78
+ class: component_class,
79
+ config: component_label[:config]
80
+ }
81
+ end
82
+ }
83
+
84
+ if components.any?
85
+ # Since one or more attached components is renderable, we no longer want to descend.
86
+ #
87
+ component_view.object.set_label(:descend, false)
88
+
89
+ # Define the render function that calls the component and renders it at render time.
90
+ #
91
+ component_render = app.isolated(:Presenter).send(:render_proc, component_view) { |node, _context, string|
92
+ presentable_component_connection = presentables[:__component_connection]
93
+ component_connection = presentable_component_connection.dup
94
+
95
+ components.each do |component|
96
+ presentables.each do |key, value|
97
+ if key.to_s.start_with?("__")
98
+ component_connection.set(key, value)
99
+ end
100
+ end
101
+
102
+ # If the component was defined in an app but being called inside a plugin, set the app to the app instead of the plugin.
103
+ #
104
+ if component_connection.app.is_a?(Plugin) && component[:class].ancestors.include?(component_connection.app.parent.isolated(:Component))
105
+ component_connection = component_connection.class.from_connection(component_connection, :@app => component_connection.app.parent)
106
+ end
107
+
108
+ component_instance = component[:class].new(
109
+ connection: component_connection,
110
+ config: component[:config]
111
+ )
112
+
113
+ # Call the component.
114
+ #
115
+ component_instance.perform
116
+ end
117
+
118
+ # Build a compound component presenter.
119
+ #
120
+ component_presenter = if components.length > 1
121
+ RenderComponents.find_compound_presenter(
122
+ app, components.map { |c| c[:class] }
123
+ )
124
+ else
125
+ components.first[:class].__presenter_class
126
+ end
127
+
128
+ # Setup the renderer for the component.
129
+ #
130
+ renderer = app.isolated(:Renderer).new(
131
+ app: app,
132
+ presentables: component_connection.values,
133
+ presenter_class: component_presenter,
134
+ composer: Composers::Component.new(
135
+ composer.view_path, current_path, app: app, labels: node.labels
136
+ ),
137
+ modes: modes
138
+ )
139
+
140
+ # Render to the main buffer.
141
+ #
142
+ renderer.perform(string)
143
+
144
+ # Return nil so nothing else gets written.
145
+ #
146
+ nil
147
+ }
148
+
149
+ # Attach the above render function to the render node.
150
+ #
151
+ component_view.object.transform do |node, context, string|
152
+ component_render.call(node, context, string); nil
153
+ end
154
+ else
155
+ initialize_renderable_components(
156
+ component_view, app: app, composer: composer, modes: modes, path: current_path
157
+ )
158
+ end
159
+ end
160
+ end
161
+
162
+ # @api private
163
+ def self.find_renderable_components(view, components = [])
164
+ view.components.each do |component_view|
165
+ find_renderable_components(component_view, components)
166
+
167
+ if component_view.object.label(:components).any? { |c| c[:renderable] }
168
+ components << component_view
169
+ end
170
+ end
171
+
172
+ components
173
+ end
174
+
175
+ # @api private
176
+ def self.wrap_block(block, context_class)
177
+ Proc.new do
178
+ @app.presenter_for_context(
179
+ context_class.__presenter_class, self
180
+ ).instance_eval(&block)
181
+ end
182
+ end
183
+
184
+ # @api private
185
+ def self.find_compound_presenter(app, component_classes)
186
+ compound_name = component_classes.map { |component_class|
187
+ component_class.__object_name.name.to_s
188
+ }.join("_")
189
+
190
+ object_name = Support::ObjectName.namespace(
191
+ app.class.__object_name.namespace.parts[0], :components, compound_name, :presenter
192
+ )
193
+
194
+ if const_defined?(object_name.constant)
195
+ const_get(object_name.constant)
196
+ else
197
+ nil
198
+ end
199
+ end
200
+
201
+ # @api private
202
+ #
203
+ def self.find_or_build_compound_presenter(app, component_classes)
204
+ compound_name = component_classes.map { |component_class|
205
+ component_class.__object_name.name.to_s
206
+ }.join("_")
207
+
208
+ object_name = Support::ObjectName.namespace(
209
+ app.class.__object_name.namespace.parts[0], :components, compound_name, :presenter
210
+ )
211
+
212
+ if const_defined?(object_name.constant)
213
+ const_get(object_name.constant)
214
+ else
215
+ component_presenter = Class.new(app.isolated(:Presenter))
216
+ Support::ObjectMaker.define_const_for_object_with_name(component_presenter, object_name)
217
+
218
+ component_classes.each do |component_class|
219
+ # Copy unique attached renders.
220
+ #
221
+ component_class.__presenter_class.__attached_renders.each_with_index do |attached_render, i|
222
+ component_presenter.__attached_renders.insert(i, {
223
+ binding_path: attached_render[:binding_path],
224
+ channel: attached_render[:channel],
225
+ node: attached_render[:node],
226
+ priority: attached_render[:priority],
227
+ block: wrap_block(attached_render[:block], component_class),
228
+ })
229
+ end
230
+
231
+ # Copy unique global options.
232
+ #
233
+ component_class.__presenter_class.__global_options.each do |form_binding, field_binding_values|
234
+ field_binding_values.each do |field_binding, field_binding_value|
235
+ component_presenter.options_for(
236
+ form_binding,
237
+ field_binding,
238
+ field_binding_value[:options],
239
+ &wrap_block(field_binding_value[:block], component_class)
240
+ )
241
+ end
242
+ end
243
+
244
+ # Copy unique presentation logic.
245
+ #
246
+ component_class.__presenter_class.__presentation_logic.each do |binding_name, logic_arr|
247
+ unless component_presenter.__presentation_logic.include?(binding_name)
248
+ component_presenter.__presentation_logic[binding_name] = []
249
+ end
250
+
251
+ logic_arr.each_with_index do |logic, i|
252
+ component_presenter.__presentation_logic[binding_name].insert(i, {
253
+ block: wrap_block(logic[:block], component_class),
254
+ channel: logic[:channel]
255
+ })
256
+ end
257
+ end
258
+
259
+ # Copy unique versioning logic.
260
+ #
261
+ component_class.__presenter_class.__versioning_logic.each do |binding_name, logic_arr|
262
+ unless component_presenter.__versioning_logic.include?(binding_name)
263
+ component_presenter.__versioning_logic[binding_name] = []
264
+ end
265
+
266
+ logic_arr.each_with_index do |logic, i|
267
+ component_presenter.__versioning_logic[binding_name].insert(i, {
268
+ block: wrap_block(logic[:block], component_class)
269
+ })
270
+ end
271
+ end
272
+ end
273
+
274
+ component_presenter
275
+ end
276
+ end
277
+ end
278
+ end
279
+ end
280
+ end
281
+ end