glib-web 0.5.14 → 0.5.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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