glib-web 0.5.14 → 0.5.20

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +5 -5
  2. data/app/controllers/concerns/glib/json/traversal.rb +2 -1
  3. data/app/controllers/concerns/glib/json/ui.rb +5 -8
  4. data/app/controllers/glib/home_controller.rb +0 -0
  5. data/app/helpers/glib/json_ui/action_builder.rb +10 -0
  6. data/app/helpers/glib/json_ui/menu_builder.rb +11 -6
  7. data/app/helpers/glib/json_ui/page_helper.rb +10 -0
  8. data/app/helpers/glib/json_ui/response_helper.rb +0 -0
  9. data/app/helpers/glib/json_ui/view_builder/fields.rb +2 -0
  10. data/app/helpers/glib/json_ui/view_builder/panels.rb +14 -1
  11. data/app/views/json_ui/garage/_nav_menu.json.jbuilder +0 -1
  12. data/app/views/json_ui/garage/actions/_http.json.jbuilder +9 -3
  13. data/app/views/json_ui/garage/actions/index.json.jbuilder +0 -0
  14. data/app/views/json_ui/garage/forms/basic.json.jbuilder +2 -14
  15. data/app/views/json_ui/garage/forms/dynamic_group.json.jbuilder +31 -16
  16. data/app/views/json_ui/garage/forms/pickers.json.jbuilder +0 -0
  17. data/app/views/json_ui/garage/home/index.json.jbuilder +0 -0
  18. data/app/views/json_ui/garage/lists/{_infinite_scroll_section.json.jbuilder → _autoload_section.json.jbuilder} +4 -2
  19. data/app/views/json_ui/garage/lists/autoload_all.json.jbuilder +32 -0
  20. data/app/views/json_ui/garage/lists/{infinite_scroll.json.jbuilder → autoload_as_needed.json.jbuilder} +9 -12
  21. data/app/views/json_ui/garage/lists/chat_ui.json.jbuilder +112 -0
  22. data/app/views/json_ui/garage/lists/fab.json.jbuilder +6 -8
  23. data/app/views/json_ui/garage/lists/index.json.jbuilder +8 -5
  24. data/app/views/json_ui/garage/notifications/android_post.json.jbuilder +48 -0
  25. data/app/views/json_ui/garage/notifications/index.json.jbuilder +14 -0
  26. data/app/views/json_ui/garage/pages/nav_buttons.json.jbuilder +4 -16
  27. data/app/views/json_ui/garage/tables/_autoload_section.json.jbuilder +6 -2
  28. data/app/views/json_ui/garage/tables/autoload_all.json.jbuilder +15 -10
  29. data/app/views/json_ui/garage/tables/autoload_as_needed.json.jbuilder +13 -2
  30. data/app/views/json_ui/garage/tables/index.json.jbuilder +19 -20
  31. data/app/views/json_ui/garage/tables/layout.json.jbuilder +26 -28
  32. data/app/views/json_ui/garage/views/banners.json.jbuilder +18 -6
  33. data/app/views/json_ui/garage/views/icons.json.jbuilder +1439 -11
  34. data/app/views/json_ui/garage/views/index.json.jbuilder +6 -3
  35. data/app/views/layouts/json_ui/renderer.html.erb +7 -4
  36. data/lib/generators/templates/20191024063257_add_scope_to_texts.rb +1 -1
  37. data/lib/generators/templates/20191126071051_create_active_storage_tables.active_storage.rb +1 -1
  38. data/lib/generators/templates/dynamic_text.rb +1 -1
  39. data/lib/glib-web.rb +5 -5
  40. data/lib/glib/engine.rb +1 -1
  41. data/lib/glib/json_crawler/action_crawlers/forms_submit.rb +4 -4
  42. data/lib/glib/json_crawler/http.rb +1 -1
  43. data/lib/glib/test_helpers.rb +3 -3
  44. data/lib/glib/value.rb +1 -1
  45. data/lib/glib/version.rb +2 -2
  46. data/lib/tasks/db.rake +11 -11
  47. metadata +7 -5
  48. data/app/views/json_ui/garage/lists/chat.json.jbuilder +0 -112
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: f440c189aedc472b187e045fc6a7f860450f8301
4
- data.tar.gz: 395891b80b5ca90022b42f5216003d57006a5ebc
2
+ SHA256:
3
+ metadata.gz: d1d6528e82e0ce768820820d544d382db8f412e2e0ea2d5e572ab16065f7b173
4
+ data.tar.gz: 2c8d52f0b60d4ac9b8253bbc1f7cc67c7172647981b5e036d4bedc306827f97a
5
5
  SHA512:
6
- metadata.gz: 29f95c4110b05050e3d5a28035c12c62c1bae2448798882f629fb1e2c0fd5a9b9e40c9df149f6ec90233dbce339d1a83830c99245cee5a33559c7fe7c3d283e7
7
- data.tar.gz: 01fee1f715657b457dfd1f4e2026db4e014c5912c446c12e6042b8b416e0119e9da985491d37fee39af24c3576a40d3da41d2c3916820b4b13b4da71cdc1a6fd
6
+ metadata.gz: 591bb9bb58b8034ee825c49f27483f8b1a756efc586de444c49b9bce35727fac47eb7852511b81dd0762736cdad7fd5972172c74d73d3b8f69218beaead56611
7
+ data.tar.gz: b7a9552472085631f0123187103b1667652428f58a45863539e1ca2d8a2ef219b59357655f88abe4c9a2291fda805627ddc9a385b51f69d4bdd04d37aac80c95
@@ -76,8 +76,9 @@ module Glib::Json::Traversal
76
76
  # Table/List
77
77
  if (sections = view['sections']).is_a? Array
78
78
  sections.each do |section|
79
- # Table
79
+ traverse_vertical_content section['header'], block
80
80
  traverse_multiple section['rows'], block
81
+ traverse_vertical_content section['footer'], block
81
82
  end
82
83
  end
83
84
  end
@@ -66,12 +66,9 @@ module Glib::Json::Ui
66
66
  end
67
67
 
68
68
  private
69
-
70
- def __json_ui_vue(hash, options)
71
- renderer_path = options[:renderer_path]
72
- @__json_ui_orig_page = response.body
73
- response.body = render_to_string(template: renderer_path, layout: 'json_ui/renderer', content_type: 'text/html', locals: { page: hash, options: options })
74
-
75
- # response.body = render_to_string(template: renderer_path, layout: false, content_type: 'text/html', locals: { page: hash, options: options })
76
- end
69
+ def __json_ui_vue(hash, options)
70
+ renderer_path = options[:renderer_path]
71
+ @__json_ui_orig_page = response.body
72
+ response.body = render_to_string(template: renderer_path, layout: 'json_ui/renderer', content_type: 'text/html', locals: { page: hash, options: options })
73
+ end
77
74
  end
File without changes
@@ -71,6 +71,16 @@ module Glib
71
71
  end
72
72
  end
73
73
 
74
+ module Devices
75
+ class GetPushToken < Action
76
+ string :postUrl
77
+ string :paramNameForToken
78
+
79
+ # Use postUrl instead
80
+ # action :onGet
81
+ end
82
+ end
83
+
74
84
  module Analytics
75
85
  class LogEvent < Action
76
86
  string :name
@@ -15,8 +15,8 @@ module Glib
15
15
  bool :disabled
16
16
  singleton_array :styleClass, :styleClasses
17
17
 
18
- def childItems(block)
19
- json.childItems do
18
+ def childButtons(block)
19
+ json.childButtons do
20
20
  block.call page.menu_builder
21
21
  end
22
22
  end
@@ -43,10 +43,15 @@ module Glib
43
43
  end
44
44
  end
45
45
 
46
- class MenuLeftBottom < Button
47
- icon :icon
48
- array :buttons
49
- end
46
+ # class MenuLeftBottom < Button
47
+ # icon :icon
48
+ # array :buttons
49
+ # end
50
+
51
+ # class Select < Button
52
+ # icon :icon
53
+ # array :buttons
54
+ # end
50
55
  end
51
56
  end
52
57
  end
@@ -10,6 +10,10 @@ module Glib
10
10
  )
11
11
  end
12
12
 
13
+ def json_ui_garage_current_url(options = {})
14
+ json_ui_garage_url(options.merge(path: params[:path]))
15
+ end
16
+
13
17
  # TODO: Remove the block
14
18
  def json_ui_page(json, &block)
15
19
  @__json_ui_page ||= Page.new(json, self)
@@ -45,6 +49,12 @@ module Glib
45
49
  @__json_ui_section.action_builder
46
50
  end
47
51
 
52
+ def json_ui_action_payload(&block)
53
+ dataJson = Jbuilder.new
54
+ block&.call Page.new(dataJson, self).action_builder
55
+ dataJson.attributes!
56
+ end
57
+
48
58
  class Page
49
59
  attr_reader :json, :context, :view_builder, :action_builder, :menu_builder
50
60
  attr_reader :list_section_builder, :table_section_builder, :drawer_content_builder, :split_content_builder
@@ -95,6 +95,7 @@ class Glib::JsonUi::ViewBuilder
95
95
  # - It has a default onClick so no need to specify `onClick: ->(action) { action.forms_submit }`
96
96
  class Submit < AbstractField
97
97
  string :text
98
+ color :color
98
99
  end
99
100
 
100
101
  class CheckGroup < AbstractField
@@ -157,6 +158,7 @@ class Glib::JsonUi::ViewBuilder
157
158
  class DynamicGroup < AbstractField
158
159
  string :titlePrefix
159
160
  panels_builder :content, :template
161
+ hash :groupFieldProperties
160
162
 
161
163
  # NOTE: Consider using sub-panel instead (e.g. groupTemplate)
162
164
  # views :groupTemplateViews
@@ -89,7 +89,6 @@ class Glib::JsonUi::ViewBuilder
89
89
  @childViewsBlock.call(page.view_builder)
90
90
  page.current_form = nil
91
91
  end
92
-
93
92
  end
94
93
 
95
94
  def childViews(block)
@@ -98,6 +97,7 @@ class Glib::JsonUi::ViewBuilder
98
97
  end
99
98
 
100
99
  class List < View
100
+ hash :ws
101
101
  hash :nextPage
102
102
  action :onScrollToTop
103
103
  action :onScrollToBottom
@@ -164,6 +164,19 @@ class Glib::JsonUi::ViewBuilder
164
164
  hash :md
165
165
  hash :sm
166
166
  hash :xs
167
+
168
+ hash :xlOnly
169
+ hash :lgOnly
170
+ hash :mdOnly
171
+ hash :smOnly
172
+ hash :xsOnly
173
+
174
+ hash :xlAndDown
175
+ hash :lgAndDown
176
+ hash :mdAndDown
177
+ hash :smAndDown
178
+ hash :xsAndDown
179
+
167
180
  views :childViews
168
181
  end
169
182
 
@@ -1,6 +1,5 @@
1
1
 
2
2
  if local_assigns[:top_nav] || json_ui_app_is_web?
3
-
4
3
  page.leftDrawer content: ->(drawer) do
5
4
  drawer.header childViews: ->(header) do
6
5
  header.button text: 'App', styleClasses: ['link', 'logo'], onClick: ->(action) do
@@ -5,14 +5,20 @@ end
5
5
 
6
6
  section.rows builder: ->(template) do
7
7
  template.thumbnail title: 'http/post', onClick: ->(action) do
8
- action.http_post url: json_ui_garage_url(path: 'forms/basic_post'), formData: { 'user[name]' => 'New Joe' }
8
+ action.auth_saveCsrfToken token: form_authenticity_token, onSave: ->(subaction) do
9
+ subaction.http_post url: json_ui_garage_url(path: 'forms/basic_post'), formData: { 'user[name]' => 'New Joe' }
10
+ end
9
11
  end
10
12
 
11
13
  template.thumbnail title: 'http/patch', onClick: ->(action) do
12
- action.http_patch url: json_ui_garage_url(path: 'forms/basic_post'), formData: { 'user[name]' => 'Edit Joe' }
14
+ action.auth_saveCsrfToken token: form_authenticity_token, onSave: ->(subaction) do
15
+ subaction.http_patch url: json_ui_garage_url(path: 'forms/basic_post'), formData: { 'user[name]' => 'Edit Joe' }
16
+ end
13
17
  end
14
18
 
15
19
  template.thumbnail title: 'http/delete', onClick: ->(action) do
16
- action.http_delete url: json_ui_garage_url(path: 'forms/basic_post'), formData: { 'user[name]' => 'Delete Joe' }
20
+ action.auth_saveCsrfToken token: form_authenticity_token, onSave: ->(subaction) do
21
+ subaction.http_delete url: json_ui_garage_url(path: 'forms/basic_post'), formData: { 'user[name]' => 'Delete Joe' }
22
+ end
17
23
  end
18
24
  end
@@ -7,14 +7,6 @@ json_ui_page json do |page|
7
7
  form.fields_text name: 'user[name]', width: 'matchParent', label: 'Name'
8
8
  form.fields_password name: 'user[password]', width: 'matchParent', label: 'Password'
9
9
 
10
- # form.panels_split width: 'matchParent', leftViews: ->(split) do
11
- # if params[:mode] == 'dialog'
12
- # split.button styleClass: 'link', text: 'cancel', onClick: ->(action) { action.dialogs_close }
13
- # end
14
- # end, rightViews: ->(split) do
15
- # split.button text: 'Submit', onClick: ->(action) { action.forms_submit }
16
- # end
17
-
18
10
  form.panels_split width: 'matchParent', content: ->(split) do
19
11
  split.left childViews: ->(left) do
20
12
  if params[:mode] == 'dialog'
@@ -22,13 +14,9 @@ json_ui_page json do |page|
22
14
  end
23
15
  end
24
16
  split.right childViews: ->(right) do
25
- right.button text: 'Submit', onClick: ->(action) { action.forms_submit }
17
+ # right.button text: 'Submit', onClick: ->(action) { action.forms_submit }
18
+ right.fields_submit text: 'Submit'
26
19
  end
27
20
  end
28
-
29
21
  end
30
- # , paramNameForFormData: 'formData', onSubmit: ->(action) do
31
- # action.http_post url: json_ui_garage_url(path: 'forms/generic_post')
32
- # end
33
-
34
22
  end
@@ -7,23 +7,38 @@ json_ui_page json do |page|
7
7
  form.h2 text: 'Dynamic Group'
8
8
  form.spacer height: 6
9
9
 
10
- value = [
11
- {
12
- 'question': 'Punctuality',
13
- 'type': 'rating',
14
- 'enabled': '1'
15
- },
16
- {
17
- 'question': 'Quality of work',
18
- 'type': 'rating'
19
- },
20
- {
21
- 'question': 'Satisfied?',
22
- 'type': 'yes_no'
23
- }
24
- ]
10
+ # value = [
11
+ # {
12
+ # 'question': 'Punctuality',
13
+ # 'type': 'rating'
14
+ # },
15
+ # {
16
+ # 'question': 'Quality of work',
17
+ # 'type': 'rating',
18
+ # 'enabled': '1'
19
+ # },
20
+ # {
21
+ # 'question': 'Satisfied?',
22
+ # 'type': 'yes_no'
23
+ # }
24
+ # ]
25
25
 
26
- form.fields_dynamicGroup width: 'matchParent', name: 'user[evaluation]', value: value, titlePrefix: 'Entry', content: ->(group) do
26
+ properties = [
27
+ [
28
+ { name: 'question', value: 'Punctuality' },
29
+ { name: 'type', value: 'rating' },
30
+ ],
31
+ [
32
+ { name: 'question', value: 'Quality of work' },
33
+ { name: 'type', value: 'rating' },
34
+ { name: 'enabled', value: '1', styleClasses: ['success'] },
35
+ ],
36
+ [
37
+ { name: 'question', value: 'Satisfied?' },
38
+ { name: 'type', value: 'yes_no' },
39
+ ]
40
+ ]
41
+ form.fields_dynamicGroup width: 'matchParent', name: 'user[evaluation]', groupFieldProperties: properties, titlePrefix: 'Entry', content: ->(group) do
27
42
  group.template padding: { left: 32 }, childViews: ->(template) do
28
43
  template.spacer height: 10
29
44
  template.fields_text width: 'matchParent', name: 'question', label: 'Question', placeholder: 'Question'
@@ -10,11 +10,13 @@
10
10
  # end
11
11
  # end
12
12
 
13
- section = json_ui_section json
13
+ # section = json_ui_section json
14
+
15
+ section = page.list_section_builder
14
16
  section.rows builder: ->(row) do
15
17
  batch_count = 30
16
18
  batch_count.times do |i|
17
- index = page * batch_count + i
19
+ index = page_index * batch_count + i
18
20
  row.thumbnail title: "Item #{index}"
19
21
  end
20
22
  end
@@ -0,0 +1,32 @@
1
+
2
+ page_index = params[:page].to_i
3
+ next_page = {
4
+ url: json_ui_garage_url(path: 'lists/autoload_all', page: page_index + 1, section_only: 'v1'),
5
+ autoload: 'all'
6
+ }
7
+
8
+ page = json_ui_page json
9
+
10
+ if params[:section_only].present?
11
+ sleep 1
12
+
13
+ json.nextPage next_page if page_index < 3
14
+ json.sections do
15
+ json.child! do
16
+ render 'json_ui/garage/lists/autoload_section', page: page, page_index: page_index
17
+ end
18
+ end
19
+ else
20
+ json.title 'Lists'
21
+
22
+ render "#{@path_prefix}/nav_menu", json: json, page: page
23
+
24
+ page.list nextPage: next_page, firstSection: ->(section) do
25
+ render 'json_ui/garage/lists/autoload_section', page: page, page_index: page_index
26
+ end, onScrollToBottom: ->(action) do
27
+ action.snackbars_alert message: 'Scrolled to Bottom'
28
+ end, onScrollToTop: ->(action) do
29
+ action.snackbars_alert message: 'Scrolled to Top'
30
+ end
31
+
32
+ end
@@ -1,34 +1,31 @@
1
1
 
2
2
  page_index = params[:page].to_i
3
3
  next_page = {
4
- url: json_ui_garage_url(path: 'lists/infinite_scroll', page: page_index + 1, section_only: 'v1'),
5
- # TODO: rename, e.g. autoloadAsNeeded vs autoloadAll
6
- autoLoad: true
4
+ url: json_ui_garage_url(path: 'lists/autoload_as_needed', page: page_index + 1, section_only: 'v1'),
5
+ autoload: 'asNeeded'
7
6
  }
8
7
 
8
+ page = json_ui_page json
9
+
9
10
  # TODO: Cater
10
11
  # - for SEO: one URL for a standalone page and one URL for pagination only
11
12
  # - for generic approach, e.g. excluding nav_menu when there is no change
12
13
  if params[:section_only].present?
13
- json.nextPage next_page
14
+ sleep 1
15
+
16
+ json.nextPage next_page if page_index < 3
14
17
  json.sections do
15
18
  json.child! do
16
- render 'json_ui/garage/lists/infinite_scroll_section', json: json, page: page_index
19
+ render 'json_ui/garage/lists/autoload_section', page: page, page_index: page_index
17
20
  end
18
21
  end
19
22
  else
20
23
  json.title 'Lists'
21
24
 
22
- # options = { nextPage: nextPage }
23
- # json_body_with_list json, nil, nil, options do
24
- # render 'json_ui/garage/lists/infinite_scroll_section', json: json, page: page
25
- # end
26
-
27
- page = json_ui_page json
28
25
  render "#{@path_prefix}/nav_menu", json: json, page: page
29
26
 
30
27
  page.list nextPage: next_page, firstSection: ->(section) do
31
- render 'json_ui/garage/lists/infinite_scroll_section', json: json, page: page_index
28
+ render 'json_ui/garage/lists/autoload_section', page: page, page_index: page_index
32
29
  end, onScrollToBottom: ->(action) do
33
30
  action.snackbars_alert message: 'Scrolled to Bottom'
34
31
  end, onScrollToTop: ->(action) do
@@ -0,0 +1,112 @@
1
+ json.title 'Lists'
2
+
3
+ liked = params[:liked] == 'true'
4
+
5
+ page = json_ui_page json
6
+ render "#{@path_prefix}/nav_menu", json: json, page: page
7
+
8
+ json.ws({
9
+ "socket" => {
10
+ "endpoint" => "/socket/websocket",
11
+ "params" => {
12
+ vsn: '2.0.0',
13
+ token: 'TOKEN'
14
+ }
15
+ },
16
+ "topic" => "rooms",
17
+ "events" => [],
18
+ # "events" => ["new_link_added"],
19
+ # "header" => {
20
+ # "user_id" => 2,
21
+ # "prev_item_id" => nil,
22
+ # "last_item_id" => nil
23
+ # }
24
+ })
25
+
26
+ list_ws = { topic: 'rooms', events: ['comments_updated'] }
27
+ page.list ws: list_ws, firstSection: ->(section) do
28
+ section.header padding: { top: 12, left: 16, right: 16, bottom: 12 }, childViews: ->(header) do
29
+ header.h3 text: 'Chat with John Doe'
30
+ end
31
+
32
+ section.rows builder: ->(template) do
33
+ # template.thumbnail title: "windows/reload (timestamp: #{DateTime.current.to_i}) -- #{liked}", onClick: ->(action) do
34
+ # action.windows_reload
35
+ # end, onLongPress: ->(action) do
36
+ # action.sheets_select message: "Context Menu (#{DateTime.current.to_i})", buttons: ->(menu) do
37
+ # if liked
38
+ # menu.button text: 'Cancel 👍', onClick: ->(subaction) do
39
+ # subaction.windows_reload url: json_ui_garage_url(path: 'lists/chat', liked: false)
40
+ # end
41
+ # else
42
+ # menu.button text: 'Give 👍', onClick: ->(subaction) do
43
+ # subaction.windows_reload url: json_ui_garage_url(path: 'lists/chat', liked: true)
44
+ # end
45
+ # end
46
+ # end
47
+ # end
48
+
49
+ template.commentOutgoing subtitle: 'Hey!', subsubtitle: l(10.minutes.ago, format: :short), imageUrl: glib_json_image_standard_url, chips: ->(menu) do
50
+ menu.button text: '😊 2', styleClass: 'info'
51
+ end
52
+
53
+ template.commentOutgoing subtitle: 'How are you?', subsubtitle: l(DateTime.current, format: :short)
54
+
55
+ template.commentIncoming title: 'John Doe', subtitle: 'Very well', subsubtitle: l(7.minutes.ago, format: :short), imageUrl: glib_json_image_standard_url, chips: ->(menu) do
56
+ menu.button text: "👍 #{liked ? 2 : 1}"
57
+ end, onLongPress: ->(action) do
58
+ action.sheets_select message: 'Context Menu', buttons: ->(menu) do
59
+ if liked
60
+ menu.button text: 'Cancel 👍', onClick: ->(subaction) do
61
+ subaction.windows_reload url: json_ui_garage_url(path: 'lists/chat_ui', liked: false)
62
+ end
63
+ else
64
+ menu.button text: 'Give 👍', onClick: ->(subaction) do
65
+ subaction.windows_reload url: json_ui_garage_url(path: 'lists/chat_ui', liked: true)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+ page.footer padding: { top: 12, left: 16, right: 16, bottom: 12 }, childViews: ->(footer) do
74
+ # json.ws({
75
+ # "socket" => {
76
+ # "endpoint" => "/socket/websocket",
77
+ # "params" => {
78
+ # vsn: '2.0.0',
79
+ # token: 'TOKEN'
80
+ # }
81
+ # },
82
+ # # "topic" => "room:30",
83
+ # # "event" => "comments_updated",
84
+ # "topic" => "links",
85
+ # "events" => ["new_link_added"],
86
+ # "header" => {
87
+ # "user_id" => 2,
88
+ # "prev_item_id" => nil,
89
+ # "last_item_id" => nil
90
+ # }
91
+ # })
92
+
93
+ footer.panels_form width: 'matchParent', url: json_ui_garage_url(path: 'forms/basic_post'), method: 'post', padding: glib_json_padding_body, paramNameForFormData: 'formData', onSubmit: ->(action) do
94
+ json.action "ws/push"
95
+ json.topic "rooms"
96
+ json.event "create_comment"
97
+ json.payload({
98
+ "room_id": "30",
99
+ "user_id": "2"
100
+ })
101
+
102
+ end, childViews: ->(form) do
103
+ form.fields_text name: 'user[message]', width: 'matchParent', label: 'Message'
104
+
105
+ form.panels_split width: 'matchParent', content: ->(split) do
106
+ split.right childViews: ->(right) do
107
+ right.button text: 'Submit', onClick: ->(action) { action.forms_submit }
108
+ end
109
+ end
110
+ end
111
+
112
+ end