glib-web 5.0.0 → 5.0.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7f82cbe6244860401184fb219b90a22f66839abbb8c0348fe324d5662491ff53
4
- data.tar.gz: 94f1678aaf84f214ab0fa9d4f913a3e2df19a6b0eacb54c8967201a26abe2569
3
+ metadata.gz: e50fd58d6fa3b0ee4d505c72f4bc49adf0809e208486eeb5c0aaeada10d39dfa
4
+ data.tar.gz: f32fb5c6985560a68a6240586e2a6411ffaec55dab2a30c12eecc177a002efaa
5
5
  SHA512:
6
- metadata.gz: 5c097c0f55e226e9960f13da37912392729a8f7acbfc2b314b18b146e340be7747aa61d7a58fdc13b1dcd77e00e1f157e0fee8f6dea6f1265d8bfe4389939b27
7
- data.tar.gz: 3442a41bf7b197ce9c6a0bebd2cbe95bff08d0e97f3db65e70e5e0264bcdf6056b6942bbbdc531e4c028a6a93cdce724cfc69d1590fcb54e5ae9e973bad3f2ba
6
+ metadata.gz: 0ddfb285130294449bbb45b56458dedfaa5cdc7ad791877b8e7e722e4da26bf0cfa2c11b5b24a56031d2da80bcdac40a6065bee6c453add16891f91956f81cca
7
+ data.tar.gz: 3f5ed1fc1745800c005db0356d123b6e334d6333262e99943933ee528861227cb31174618bd9d400c73eb8b849906a534bc47018bbe0eeb5472cc5efc323325f
@@ -1,6 +1,6 @@
1
1
  module Glib
2
2
  module FormatHelper
3
- require 'redcarpet/render_strip'
3
+ require 'redcarpet'
4
4
 
5
5
  class CustomRenderer < Redcarpet::Render::XHTML
6
6
  def strikethrough(text)
@@ -286,7 +286,16 @@ module Glib
286
286
  def self.action(propName)
287
287
  define_method(propName) do |value|
288
288
  if value # Allow nil value
289
- json.set!(propName) { value.call(page.action_builder) }
289
+ json.set!(propName) do
290
+ # Save/restore singleton tracking around nested action callbacks (e.g. onClick)
291
+ # to prevent the inner callback from polluting the outer context's state.
292
+ builder = page.action_builder
293
+ saved_singleton_set = builder.singleton_action_set
294
+ saved_singleton_name = builder.singleton_action_name
295
+ builder.reset_singleton_tracking!
296
+ value.call(builder)
297
+ builder.restore_singleton_tracking!(saved_singleton_set, saved_singleton_name)
298
+ end
290
299
  end
291
300
  end
292
301
  end
@@ -7,10 +7,30 @@ module Glib
7
7
  @multiple = multiple
8
8
  end
9
9
 
10
+ attr_reader :singleton_action_set, :singleton_action_name
11
+
12
+ def reset_singleton_tracking!
13
+ @singleton_action_set = false
14
+ @singleton_action_name = nil
15
+ end
16
+
17
+ def restore_singleton_tracking!(action_set, action_name)
18
+ @singleton_action_set = action_set
19
+ @singleton_action_name = action_name
20
+ end
21
+
10
22
  def method_missing(m, *args)
11
23
  if @multiple
12
24
  add_element_to_array_v1 'action', m, *args
13
25
  else
26
+ if !Rails.env.production? && @singleton_action_set
27
+ raise "Multiple actions called on a singleton ActionBuilder. " \
28
+ "Only one action is allowed. Previous action: '#{@singleton_action_name}', " \
29
+ "current action: '#{m}'. Use `run_multiple` with `childActions` or " \
30
+ "chain actions using `onClose` callbacks."
31
+ end
32
+ @singleton_action_set = true
33
+ @singleton_action_name = m
14
34
  add_singleton_element_v1 'action', m, *args
15
35
  end
16
36
  end
@@ -527,6 +527,9 @@ class Glib::JsonUi::ViewBuilder
527
527
  #
528
528
  # @see app/views/json_ui/garage/forms/text_validation.json.jbuilder Garage example
529
529
  class Textarea < Text
530
+ # Number of visible text lines.
531
+ # @example rows: 3
532
+ int :rows
530
533
  end
531
534
 
532
535
  # Rich text editor field with WYSIWYG editing.
@@ -59,17 +59,6 @@ module Glib
59
59
  end
60
60
  end
61
61
 
62
- if @onMaxFileLengthError.blank?
63
- @onMaxFileLengthError = ->(action) do
64
- action.snackbars_alert \
65
- message: I18n.t(
66
- 'glib.fields.upload.on_max_file_length_error',
67
- max: @accepts['maxFileLength'],
68
- default: "You can't upload more than the maximum number of files allowed. The limit is #{@accepts['maxFileLength']} files."),
69
- styleClass: 'error'
70
- end
71
- end
72
-
73
62
  if @onFileTypeError.blank?
74
63
  @onFileTypeError = ->(action) do
75
64
  action.snackbars_alert \
@@ -81,9 +70,12 @@ module Glib
81
70
  end
82
71
 
83
72
  json.accepts @accepts
84
- json.set!('onMaxFileSizeError') { @onMaxFileSizeError.call(page.action_builder) }
85
- json.set!('onMaxFileLengthError') { @onMaxFileLengthError.call(page.action_builder) }
86
- json.set!('onFileTypeError') { @onFileTypeError.call(page.action_builder) }
73
+ # Each handler needs its own singleton ActionBuilder — sharing
74
+ # `page.action_builder` across all three trips the "only one action
75
+ # per singleton" guard because the builder accumulates state.
76
+ json.set!('onMaxFileSizeError') { @onMaxFileSizeError.call(ActionBuilder.new(json, page, false)) }
77
+ json.set!('onMaxFileLengthError') { @onMaxFileLengthError.call(ActionBuilder.new(json, page, false)) }
78
+ json.set!('onFileTypeError') { @onFileTypeError.call(ActionBuilder.new(json, page, false)) }
87
79
  end
88
80
  end
89
81
  end
@@ -130,7 +122,7 @@ module Glib
130
122
  hash :padding, optional: [:top, :right, :bottom, :left, :x, :y, :all]
131
123
  singleton_array :styleClass, :styleClasses
132
124
 
133
- hash :showIf
125
+ hash :showIf # Deprecated: use logics_set instead
134
126
  hash :loadIf
135
127
  action :onIfTrue
136
128
  action :onIfFalse
@@ -247,6 +239,7 @@ module Glib
247
239
  string :base64Data
248
240
  action :onClick
249
241
  string :fit
242
+ string :alt
250
243
  end
251
244
 
252
245
  class Avatar < View
@@ -2,55 +2,63 @@ json.title 'Test Page (Custom)'
2
2
 
3
3
  page = json_ui_page json
4
4
 
5
- page.body childViews: ->(body) do
6
- render 'json_ui/garage/test_page/header', view: body
7
-
8
- body.panels_responsive padding: glib_json_padding_body, childViews: ->(res) do
9
- res.h2 text: 'Overview'
10
- res.p text: 'Custom panels render a predefined template with custom data payloads.'
11
-
12
- data_primary = { imageUrl: glib_json_image_standard_url, title: 'Primary', subtitle: 'Thumbnail template' }
13
- data_alt = { imageUrl: glib_json_image_standard_url, title: 'Alternate', subtitle: 'Alternate data payload' }
14
-
15
- res.spacer height: 12
16
- res.hr width: 'matchParent'
17
-
18
- res.h2 text: 'Basic'
19
- res.spacer height: 8
20
- res.panels_custom id: 'custom_main', template: 'thumbnail', width: 'matchParent', backgroundColor: '#fafafa', data: data_primary
21
-
22
- res.spacer height: 12
23
- res.hr width: 'matchParent'
24
-
25
- res.h2 text: 'Variants and Props'
26
- res.spacer height: 8
27
- res.panels_flow innerPadding: { bottom: 0 }, childViews: ->(flow) do
28
- flow.button text: 'Primary data', onClick: ->(action) do
29
- action.components_set targetId: 'custom_main', data: { data: data_primary }
5
+ page.body(
6
+ childViews: ->(body) do
7
+ render 'json_ui/garage/test_page/header', view: body
8
+ body.panels_responsive(
9
+ padding: glib_json_padding_body,
10
+ childViews: ->(res) do
11
+ res.h2 text: 'Overview'
12
+ res.p text: 'Custom panels render a predefined template with custom data payloads.'
13
+ data_primary = { imageUrl: glib_json_image_standard_url, title: 'Primary', subtitle: 'Thumbnail template' }
14
+ data_alt = { imageUrl: glib_json_image_standard_url, title: 'Alternate', subtitle: 'Alternate data payload' }
15
+ res.spacer height: 12
16
+ res.hr width: 'matchParent'
17
+ res.h2 text: 'Basic'
18
+ res.spacer height: 8
19
+ res.panels_custom id: 'custom_main', template: 'thumbnail', width: 'matchParent', backgroundColor: '#fafafa', data: data_primary
20
+ res.spacer height: 12
21
+ res.hr width: 'matchParent'
22
+ res.h2 text: 'Variants and Props'
23
+ res.spacer height: 8
24
+ res.panels_flow(
25
+ innerPadding: { bottom: 0 },
26
+ childViews: ->(flow) do
27
+ flow.button(
28
+ text: 'Primary data',
29
+ onClick: ->(action) do
30
+ action.components_set targetId: 'custom_main', data: { data: data_primary }
31
+ end
32
+ )
33
+ flow.spacer width: 4
34
+ flow.button(
35
+ text: 'Alternate data',
36
+ onClick: ->(action) do
37
+ action.components_set targetId: 'custom_main', data: { data: data_alt }
38
+ end
39
+ )
40
+ end
41
+ )
42
+ res.spacer height: 12
43
+ res.hr width: 'matchParent'
44
+ res.h2 text: 'Actions and Events'
45
+ res.spacer height: 8
46
+ res.label id: 'custom_action_status', text: 'Action status: idle'
47
+ res.spacer height: 6
48
+ res.panels_custom(
49
+ template: 'thumbnail',
50
+ data: data_primary,
51
+ onClick: ->(action) do
52
+ action.components_set targetId: 'custom_action_status', data: { text: 'Action status: clicked' }
53
+ end
54
+ )
55
+ res.spacer height: 12
56
+ res.hr width: 'matchParent'
57
+ res.h2 text: 'Edge and Advanced'
58
+ res.spacer height: 8
59
+ res.label text: 'Non-existent template'
60
+ res.panels_custom template: 'nonExistentTemplate'
30
61
  end
31
- flow.spacer width: 4
32
- flow.button text: 'Alternate data', onClick: ->(action) do
33
- action.components_set targetId: 'custom_main', data: { data: data_alt }
34
- end
35
- end
36
-
37
- res.spacer height: 12
38
- res.hr width: 'matchParent'
39
-
40
- res.h2 text: 'Actions and Events'
41
- res.spacer height: 8
42
- res.label id: 'custom_action_status', text: 'Action status: idle'
43
- res.spacer height: 6
44
- res.panels_custom template: 'thumbnail', data: data_primary, onClick: ->(action) do
45
- action.components_set targetId: 'custom_action_status', data: { text: 'Action status: clicked' }
46
- end
47
-
48
- res.spacer height: 12
49
- res.hr width: 'matchParent'
50
-
51
- res.h2 text: 'Edge and Advanced'
52
- res.spacer height: 8
53
- res.label text: 'Non-existent template'
54
- res.panels_custom template: 'nonExistentTemplate'
62
+ )
55
63
  end
56
- end
64
+ )
@@ -19,6 +19,59 @@ page.body(
19
19
  res.spacer height: 12
20
20
  res.hr width: 'matchParent'
21
21
  res.spacer height: 12
22
+ res.h2 text: 'Inside dialog'
23
+ res.spacer height: 8
24
+ res.button(
25
+ text: 'open dialog with popover',
26
+ onClick: ->(action) do
27
+ action.dialogs_show(
28
+ content: ->(dialog) do
29
+ dialog.body(
30
+ padding: glib_json_padding_body,
31
+ childViews: ->(dbody) do
32
+ dbody.label id: 'dialog_popover_target', text: 'Dialog anchor'
33
+ dbody.spacer height: 8
34
+ dbody.button(
35
+ text: 'show popover',
36
+ onClick: ->(daction) do
37
+ daction.popovers_show(
38
+ key: 'popover_in_dialog',
39
+ placement: 'top-start',
40
+ targetId: 'dialog_popover_target',
41
+ content: ->(pdialog) do
42
+ pdialog.body(
43
+ padding: glib_json_padding_body,
44
+ childViews: ->(pbody) do
45
+ pbody.label text: 'Popover inside dialog'
46
+ pbody.spacer height: 8
47
+ pbody.button(
48
+ text: 'close popover',
49
+ onClick: ->(paction) do
50
+ paction.popovers_close key: 'popover_in_dialog'
51
+ end
52
+ )
53
+ end
54
+ )
55
+ end
56
+ )
57
+ end
58
+ )
59
+ dbody.spacer height: 8
60
+ dbody.button(
61
+ text: 'close dialog',
62
+ onClick: ->(daction) do
63
+ daction.dialogs_close
64
+ end
65
+ )
66
+ end
67
+ )
68
+ end
69
+ )
70
+ end
71
+ )
72
+ res.spacer height: 16
73
+ res.hr width: 'matchParent'
74
+ res.spacer height: 16
22
75
 
23
76
  res.h2 text: 'Basic example'
24
77
  res.spacer height: 8
@@ -27,23 +80,40 @@ page.body(
27
80
  res.button(
28
81
  text: 'popovers/show menu',
29
82
  onClick: ->(action) do
30
- action.popovers_show \
83
+ action.popovers_show(
31
84
  key: 'popover_menu',
32
85
  placement: 'bottom-start',
33
86
  targetId: 'popover_target',
34
87
  content: ->(dialog) do
35
- dialog.body styleClass: 'popover-menu', padding: glib_json_padding_body, childViews: ->(menu) do
36
- menu.label text: 'Item 1', styleClass: 'popover-menu-item', onClick: ->(subaction) do
37
- subaction.popovers_close key: 'popover_menu'
38
- end
39
- menu.label text: 'Item 2', styleClass: 'popover-menu-item', onClick: ->(subaction) do
40
- subaction.popovers_close key: 'popover_menu'
41
- end
42
- menu.label text: 'Item 3', styleClass: 'popover-menu-item', onClick: ->(subaction) do
43
- subaction.popovers_close key: 'popover_menu'
88
+ dialog.body(
89
+ styleClass: 'popover-menu',
90
+ padding: glib_json_padding_body,
91
+ childViews: ->(menu) do
92
+ menu.label(
93
+ text: 'Item 1',
94
+ styleClass: 'popover-menu-item',
95
+ onClick: ->(subaction) do
96
+ subaction.popovers_close key: 'popover_menu'
97
+ end
98
+ )
99
+ menu.label(
100
+ text: 'Item 2',
101
+ styleClass: 'popover-menu-item',
102
+ onClick: ->(subaction) do
103
+ subaction.popovers_close key: 'popover_menu'
104
+ end
105
+ )
106
+ menu.label(
107
+ text: 'Item 3',
108
+ styleClass: 'popover-menu-item',
109
+ onClick: ->(subaction) do
110
+ subaction.popovers_close key: 'popover_menu'
111
+ end
112
+ )
44
113
  end
45
- end
114
+ )
46
115
  end
116
+ )
47
117
  end
48
118
  )
49
119
 
@@ -63,10 +133,11 @@ page.body(
63
133
  res.button(
64
134
  text: 'popovers/open (updateExisting)',
65
135
  onClick: ->(action) do
66
- action.popovers_open \
136
+ action.popovers_open(
67
137
  url: json_ui_garage_url(path: 'views/popovers_open', key: 'server_popover'),
68
138
  key: 'server_popover',
69
139
  updateExisting: true
140
+ )
70
141
  end
71
142
  )
72
143
 
@@ -99,15 +170,19 @@ page.body(
99
170
  res.button(
100
171
  text: 'popovers/show (top-end)',
101
172
  onClick: ->(action) do
102
- action.popovers_show \
173
+ action.popovers_show(
103
174
  key: 'popover_note',
104
175
  placement: 'top-end',
105
176
  targetId: 'popover_target',
106
177
  content: ->(dialog) do
107
- dialog.body padding: glib_json_padding_body, childViews: ->(menu) do
108
- menu.label text: 'Pinned note'
109
- end
178
+ dialog.body(
179
+ padding: glib_json_padding_body,
180
+ childViews: ->(menu) do
181
+ menu.label text: 'Pinned note'
182
+ end
183
+ )
110
184
  end
185
+ )
111
186
  end
112
187
  )
113
188
  end
data/config/routes.rb CHANGED
@@ -6,10 +6,6 @@ Glib::Web::Engine.routes.draw do
6
6
  delete 'json_ui_garage', to: 'home#json_ui_garage'
7
7
  post 'chat', to: 'home#chat'
8
8
 
9
- get 'json_ui_api', to: 'api_docs#index'
10
- get 'json_ui_api/:category', to: 'api_docs#show', as: :json_ui_api_category
11
- get 'json_ui_api/:category/:component', to: 'api_docs#component', as: :json_ui_api_component
12
-
13
9
  resources :glib_direct_uploads, only: [:create]
14
10
  resources :blob_url_generators, only: [:create]
15
11
  resources :errors, only: [:create]
@@ -83,6 +83,58 @@ module Glib
83
83
  "http://#{HOST}/users/sign_out.json"
84
84
  end
85
85
 
86
+ # Submits a form by parsing the page payload and extracting form data.
87
+ # This ensures the view renders successfully before submitting.
88
+ #
89
+ # @param page_payload [String] The JSON response body containing the form
90
+ # @param data [Hash] The form data to submit (e.g., { chat: { message_body: 'hello' } })
91
+ # @param url [String, nil] Optional URL override (if form URL is nil)
92
+ # @param method [Symbol, nil] Optional HTTP method override
93
+ #
94
+ # @example
95
+ # get new_chat_url(checklist_id: @checklist.id), params: json_params
96
+ # submit_form(response.body, { chat: { message_body: 'Hello' } })
97
+ #
98
+ def submit_form(page_payload, data, url: nil, method: nil)
99
+ json = JSON.parse(page_payload)
100
+ form = __find_form(json)
101
+
102
+ raise 'No form found in page. Ensure the form is rendered properly.' unless form
103
+
104
+ # Get URL from form, or use provided URL
105
+ form_url = form['url']
106
+ if form_url.blank? && form['onSubmit']
107
+ on_submit = form['onSubmit']
108
+ form_url = on_submit['httpPost']&.[]('url') || on_submit['http']&.[]('url')
109
+ end
110
+ url ||= form_url
111
+
112
+ raise "Form missing 'url'. Please provide url: parameter. Form keys: #{form.keys.join(', ')}" if url.blank?
113
+
114
+ # Get method from parameter, form, or default to post
115
+ method ||= form['method']
116
+ method = 'post' if method.blank?
117
+
118
+ # Build params: hidden fields from form + provided data
119
+ params = __build_form_params(form, data)
120
+
121
+ # Submit using the appropriate HTTP method
122
+ case method.to_sym
123
+ when :get
124
+ get url, params: params
125
+ when :post
126
+ post url, params: params
127
+ when :patch
128
+ patch url, params: params
129
+ when :put
130
+ put url, params: params
131
+ else
132
+ send(method, url, params: params)
133
+ end
134
+
135
+ assert_response :success
136
+ end
137
+
86
138
  def glib_travel(*args, &block)
87
139
  Timecop.travel(*args, &block)
88
140
  end
@@ -96,6 +148,81 @@ module Glib
96
148
  end
97
149
 
98
150
  private
151
+ # --- submit_form helpers ---
152
+
153
+ def __find_form(json)
154
+ forms = []
155
+ __traverse_form_view(json) do |view|
156
+ forms << view if __view_is_form?(view)
157
+ end
158
+ forms.max_by { |f| f['childViews']&.length || 0 }
159
+ end
160
+
161
+ def __view_is_form?(view)
162
+ view.is_a?(Hash) && (
163
+ view['view']&.start_with?('panels/form') ||
164
+ view['view']&.start_with?('panels/fullPageForm') ||
165
+ view['view'] == 'panels/bulkEdit2-v1' ||
166
+ view['view'] == 'panels/splitForm'
167
+ )
168
+ end
169
+
170
+ def __build_form_params(form, data)
171
+ params = {}
172
+ data_prefixes = data.keys.map { |k| "#{k}[" }
173
+
174
+ __extract_form_fields(form).each do |field|
175
+ next unless field[:view] == 'fields/hidden' || field[:view] == 'fields/hidden-v1'
176
+ next if data_prefixes.any? { |prefix| field[:name]&.start_with?(prefix) }
177
+
178
+ params[field[:name]] = field[:value]
179
+ end
180
+
181
+ data.each do |key, value|
182
+ if value.is_a?(Hash)
183
+ params[key] ||= {}
184
+ params[key].merge!(value)
185
+ else
186
+ params[key] = value
187
+ end
188
+ end
189
+
190
+ params
191
+ end
192
+
193
+ def __extract_form_fields(form)
194
+ fields = []
195
+ __traverse_form_views(form['childViews'] || []) do |view|
196
+ if view.is_a?(Hash) && view['view']&.start_with?('fields/')
197
+ fields << { name: view['name'], value: view['value'], view: view['view'] }
198
+ end
199
+ end
200
+ fields
201
+ end
202
+
203
+ def __traverse_form_views(views, &block)
204
+ return unless views.is_a?(Array)
205
+
206
+ views.each { |view| __traverse_form_view(view, &block) }
207
+ end
208
+
209
+ def __traverse_form_view(view, &block)
210
+ return unless view.is_a?(Hash)
211
+
212
+ block.call(view)
213
+
214
+ ['childViews', 'header', 'body', 'footer', 'left', 'right', 'panels'].each do |key|
215
+ child = view[key]
216
+ if child.is_a?(Array)
217
+ __traverse_form_views(child, &block)
218
+ elsif child.is_a?(Hash)
219
+ __traverse_form_view(child, &block)
220
+ end
221
+ end
222
+ end
223
+
224
+ # --- Crawler helpers ---
225
+
99
226
  def __execute_crawler(user, inspect_http:, log_file: nil, &block)
100
227
  auth_token = login user
101
228
  user[:token] = auth_token
data/lib/tasks/db.rake CHANGED
@@ -64,6 +64,7 @@ namespace :db do
64
64
  entries.each do |a|
65
65
  attrs = Hash[a.attributes.sort]
66
66
  attrs.delete_if { |k, v| v.nil? }
67
+ # attrs.delete_if { |k, v| v.nil? }
67
68
 
68
69
  id = if attrs.include?('id')
69
70
  [a.id]
metadata CHANGED
@@ -1,11 +1,11 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: glib-web
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.0.0
4
+ version: 5.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - ''
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
  date: 2019-10-04 00:00:00.000000000 Z
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 8.0.0
19
+ version: '8.0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 8.0.0
26
+ version: '8.0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: pundit
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -58,14 +58,14 @@ dependencies:
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: 8.0.0
61
+ version: '8.0'
62
62
  type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: 8.0.0
68
+ version: '8.0'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: js_regex
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -108,6 +108,20 @@ dependencies:
108
108
  - - ">="
109
109
  - !ruby/object:Gem::Version
110
110
  version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: redcarpet
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
111
125
  - !ruby/object:Gem::Dependency
112
126
  name: rubocop
113
127
  requirement: !ruby/object:Gem::Requirement
@@ -136,7 +150,7 @@ dependencies:
136
150
  - - ">="
137
151
  - !ruby/object:Gem::Version
138
152
  version: '0'
139
- description:
153
+ description:
140
154
  email: ''
141
155
  executables: []
142
156
  extensions: []
@@ -156,7 +170,6 @@ files:
156
170
  - app/controllers/concerns/glib/json/traversal.rb
157
171
  - app/controllers/concerns/glib/json/ui.rb
158
172
  - app/controllers/concerns/glib/json/validation.rb
159
- - app/controllers/glib/api_docs_controller.rb
160
173
  - app/controllers/glib/blob_url_generators_controller.rb
161
174
  - app/controllers/glib/errors_controller.rb
162
175
  - app/controllers/glib/glib_direct_uploads_controller.rb
@@ -221,9 +234,6 @@ files:
221
234
  - app/validators/mutually_exclusive_with_validator.rb
222
235
  - app/validators/presence_only_if_validator.rb
223
236
  - app/validators/url_validator.rb
224
- - app/views/glib/api_docs/component.json.jbuilder
225
- - app/views/glib/api_docs/index.json.jbuilder
226
- - app/views/glib/api_docs/show.json.jbuilder
227
237
  - app/views/json_ui/garage/_nav_menu.json.jbuilder
228
238
  - app/views/json_ui/garage/actions/_commands.json.jbuilder
229
239
  - app/views/json_ui/garage/actions/_components.json.jbuilder
@@ -515,11 +525,10 @@ files:
515
525
  - lib/glib/value.rb
516
526
  - lib/glib/version.rb
517
527
  - lib/tasks/db.rake
518
- - lib/tasks/docs.rake
519
- homepage:
528
+ homepage:
520
529
  licenses: []
521
530
  metadata: {}
522
- post_install_message:
531
+ post_install_message:
523
532
  rdoc_options: []
524
533
  require_paths:
525
534
  - lib
@@ -535,7 +544,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
535
544
  version: '0'
536
545
  requirements: []
537
546
  rubygems_version: 3.4.6
538
- signing_key:
547
+ signing_key:
539
548
  specification_version: 4
540
549
  summary: ''
541
550
  test_files: []
@@ -1,145 +0,0 @@
1
- module Glib
2
- class ApiDocsController < ApplicationController
3
- prepend_before_action do
4
- params[:_skip_custom_render] = 'true' if params[:custom_render] != 'true'
5
- end
6
-
7
- if try(:glib_auth_inited?)
8
- skip_before_action :glib_load_resource
9
- skip_before_action :glib_authorize_resource
10
- end
11
-
12
- # Skip JSON injections from the host app
13
- skip_after_action :json_inject_left_menu, raise: false
14
- skip_after_action :json_inject_feature_usage_analytics, raise: false
15
- skip_after_action :json_inject_flash_actions, raise: false
16
-
17
- before_action :set_path_prefix
18
- layout false
19
-
20
- # Override injection methods to do nothing
21
- def json_inject_left_menu; end
22
- def json_inject_feature_usage_analytics; end
23
- def json_inject_flash_actions; end
24
-
25
- def set_path_prefix
26
- @path_prefix = 'glib/api_docs'
27
- end
28
-
29
- # GET /glib/json_ui_api
30
- def index
31
- @categories = load_all_categories
32
- end
33
-
34
- # GET /glib/json_ui_api/:category
35
- def show
36
- @category = params[:category]
37
- @doc = load_documentation(@category)
38
-
39
- unless @doc
40
- render plain: "Category '#{@category}' not found", status: :not_found
41
- return
42
- end
43
-
44
- @components = @doc['components'] || {}
45
- end
46
-
47
- # GET /glib/json_ui_api/:category/:component
48
- def component
49
- @category = params[:category]
50
- @component_key = params[:component]
51
- @doc = load_documentation(@category)
52
-
53
- unless @doc
54
- render plain: "Category '#{@category}' not found", status: :not_found
55
- return
56
- end
57
-
58
- @component = @doc['components'][@component_key]
59
-
60
- unless @component
61
- render plain: "Component '#{@component_key}' not found in category '#{@category}'", status: :not_found
62
- return
63
- end
64
- end
65
-
66
- private
67
-
68
- def load_all_categories
69
- # Try to find the doc/components directory
70
- # First try the gem's path
71
- doc_path = Glib::Web::Engine.root.join('doc', 'components')
72
-
73
- # If not found, try relative to Rails root (for development in host app)
74
- unless Dir.exist?(doc_path)
75
- doc_path = Rails.root.join('..', 'glib-web', 'doc', 'components')
76
- end
77
-
78
- return [] unless Dir.exist?(doc_path)
79
-
80
- categories = []
81
- Dir.glob(doc_path.join('*.yml')).each do |file|
82
- next if File.basename(file) == 'README.md'
83
-
84
- basename = File.basename(file, '.yml')
85
- doc = YAML.load_file(file)
86
-
87
- categories << {
88
- 'key' => basename,
89
- 'name' => basename.titleize,
90
- 'file' => file,
91
- 'component_count' => doc['components']&.size || 0,
92
- 'source_file' => doc.dig('meta', 'source_file')
93
- }
94
- end
95
-
96
- categories.sort_by { |c| c['name'] }
97
- end
98
-
99
- def load_documentation(category)
100
- # Try to find the YAML file
101
- # First try the gem's path
102
- doc_path = Glib::Web::Engine.root.join('doc', 'components', "#{category}.yml")
103
-
104
- # If not found, try relative to Rails root (for development in host app)
105
- unless File.exist?(doc_path)
106
- doc_path = Rails.root.join('..', 'glib-web', 'doc', 'components', "#{category}.yml")
107
- end
108
-
109
- return nil unless File.exist?(doc_path)
110
-
111
- YAML.load_file(doc_path)
112
- end
113
-
114
- helper_method :json_ui_api_url
115
- def json_ui_api_url(category: nil, component: nil)
116
- if component && category
117
- "/glib/json_ui_api/#{category}/#{component}"
118
- elsif category
119
- "/glib/json_ui_api/#{category}"
120
- else
121
- "/glib/json_ui_api"
122
- end
123
- end
124
-
125
- helper_method :property_type_badge_class
126
- def property_type_badge_class(type)
127
- case type
128
- when 'string', 'text'
129
- 'badge-primary'
130
- when 'int', 'float'
131
- 'badge-success'
132
- when 'bool'
133
- 'badge-info'
134
- when 'action'
135
- 'badge-warning'
136
- when 'views', 'panels_builder'
137
- 'badge-secondary'
138
- when 'hash', 'array'
139
- 'badge-dark'
140
- else
141
- 'badge-light'
142
- end
143
- end
144
- end
145
- end
@@ -1,215 +0,0 @@
1
- json.title @component['class_name']
2
-
3
- page = json_ui_page json
4
-
5
- page.scroll padding: glib_json_padding_body, childViews: ->(scroll) do
6
- # Back button
7
- scroll.button \
8
- text: "← Back to #{@category.titleize}",
9
- styleClasses: ['btn-outline-secondary', 'btn-sm'],
10
- onClick: ->(action) do
11
- action.windows_open url: json_ui_api_url(category: @category)
12
- end
13
-
14
- scroll.spacer height: 16
15
-
16
- # Title
17
- scroll.panels_horizontal align: 'middle', childViews: ->(title_row) do
18
- title_row.h1 text: @component['class_name']
19
-
20
- if @component['extends']
21
- title_row.spacer width: 12
22
- title_row.h5 text: "extends #{@component['extends']}", styleClass: 'text-muted'
23
- end
24
- end
25
-
26
- # Deprecated warning
27
- if @component['deprecated']
28
- scroll.spacer height: 16
29
- scroll.panels_vertical \
30
- padding: { all: 16 },
31
- backgroundColor: '#fff3cd',
32
- styleClasses: ['rounded'],
33
- childViews: ->(warning) do
34
- warning.h5 text: '⚠️ Deprecated', color: '#856404'
35
- deprecation_text = @component['deprecated'] == true ? 'This component is deprecated and should not be used in new code.' : @component['deprecated']
36
- warning.p text: deprecation_text, color: '#856404'
37
- end
38
- end
39
-
40
- scroll.spacer height: 24
41
-
42
- # Description
43
- if @component['description']
44
- scroll.h3 text: 'Description'
45
- @component['description'].split("\n\n").each do |paragraph|
46
- scroll.p text: paragraph.strip, styleClass: 'lead'
47
- end
48
- scroll.spacer height: 32
49
- end
50
-
51
- # Properties
52
- if @component['properties'] && @component['properties'].any?
53
- scroll.h2 text: 'Properties'
54
- scroll.spacer height: 16
55
-
56
- @component['properties'].each do |prop_name, prop|
57
- scroll.panels_vertical \
58
- padding: { all: 16 },
59
- styleClasses: ['card', 'rounded'],
60
- childViews: ->(prop_card) do
61
- # Property name and badges
62
- prop_card.panels_horizontal align: 'middle', childViews: ->(header) do
63
- header.h5 text: prop_name, styleClass: 'font-monospace'
64
- header.spacer width: 8
65
-
66
- # Type badge
67
- type_color = case prop['type']
68
- when 'string', 'text' then '#0d6efd'
69
- when 'int', 'float' then '#198754'
70
- when 'bool' then '#0dcaf0'
71
- when 'action' then '#ffc107'
72
- when 'views', 'panels_builder' then '#6c757d'
73
- when 'hash', 'array' then '#212529'
74
- else '#f8f9fa'
75
- end
76
-
77
- header.label text: prop['type'], backgroundColor: type_color, styleClasses: ['badge'], color: '#ffffff'
78
-
79
- if prop['required']
80
- header.spacer width: 4
81
- header.label text: 'required', backgroundColor: '#dc3545', styleClasses: ['badge'], color: '#ffffff'
82
- end
83
-
84
- if prop['deprecated']
85
- header.spacer width: 4
86
- header.label text: 'deprecated', backgroundColor: '#ffc107', styleClasses: ['badge']
87
- end
88
- end
89
-
90
- # Description
91
- if prop['description']
92
- prop_card.spacer height: 8
93
- prop['description'].split("\n").each do |line|
94
- prop_card.p text: line.strip
95
- end
96
- end
97
-
98
- # Hash options
99
- if prop['options'] && prop['options']['optional']
100
- prop_card.spacer height: 8
101
- prop_card.label text: 'Optional hash keys:', styleClass: 'text-muted'
102
- prop_card.label text: prop['options']['optional'].join(', '), styleClass: 'font-monospace'
103
- end
104
-
105
- # Examples
106
- if prop['examples'] && prop['examples'].any?
107
- prop_card.spacer height: 8
108
- prop_card.label text: 'Example:', styleClasses: ['text-muted', 'font-weight-bold']
109
- prop['examples'].each do |example|
110
- prop_card.panels_vertical \
111
- padding: { all: 12 },
112
- backgroundColor: '#f6f8fa',
113
- styleClasses: ['rounded'],
114
- childViews: ->(code_block) do
115
- code_block.label text: example, styleClass: 'font-monospace'
116
- end
117
- end
118
- end
119
-
120
- # Notes
121
- if prop['notes'] && prop['notes'].any?
122
- prop_card.spacer height: 8
123
- prop_card.panels_vertical \
124
- padding: { all: 12 },
125
- backgroundColor: '#cfe2ff',
126
- styleClasses: ['rounded'],
127
- childViews: ->(note_block) do
128
- note_block.label text: 'Note:', styleClasses: ['font-weight-bold'], color: '#084298'
129
- prop['notes'].each do |note|
130
- note_block.label text: note, color: '#084298'
131
- end
132
- end
133
- end
134
-
135
- # Deprecation
136
- if prop['deprecated']
137
- prop_card.spacer height: 8
138
- prop_card.panels_vertical \
139
- padding: { all: 12 },
140
- backgroundColor: '#fff3cd',
141
- styleClasses: ['rounded'],
142
- childViews: ->(dep_block) do
143
- dep_block.label text: 'Deprecated:', styleClasses: ['font-weight-bold'], color: '#856404'
144
- dep_block.label text: prop['deprecated'], color: '#856404'
145
- end
146
- end
147
- end
148
-
149
- scroll.spacer height: 12
150
- end
151
-
152
- scroll.spacer height: 32
153
- end
154
-
155
- # Examples
156
- if @component['examples'] && @component['examples'].any?
157
- scroll.h2 text: 'Examples'
158
- scroll.spacer height: 16
159
-
160
- @component['examples'].each_with_index do |example, index|
161
- scroll.panels_vertical \
162
- styleClasses: ['card'],
163
- childViews: ->(example_card) do
164
- # Header
165
- example_card.panels_vertical \
166
- padding: { all: 12 },
167
- backgroundColor: '#f8f9fa',
168
- childViews: ->(header) do
169
- header.h5 text: example['label'] || "Example #{index + 1}"
170
- end
171
-
172
- # Code
173
- example_card.panels_vertical \
174
- padding: { all: 16 },
175
- backgroundColor: '#f6f8fa',
176
- childViews: ->(code_block) do
177
- example['code'].split("\n").each do |line|
178
- code_block.label text: line, styleClass: 'font-monospace'
179
- end
180
- end
181
- end
182
-
183
- scroll.spacer height: 12
184
- end
185
-
186
- scroll.spacer height: 32
187
- end
188
-
189
- # References
190
- if @component['references'] && @component['references'].any?
191
- scroll.h2 text: 'References'
192
- scroll.spacer height: 16
193
-
194
- scroll.panels_vertical styleClasses: ['card'], childViews: ->(refs) do
195
- @component['references'].each do |ref|
196
- refs.panels_vertical padding: { all: 12 }, childViews: ->(ref_item) do
197
- if ref['url']
198
- ref_item.button \
199
- text: ref['url'],
200
- styleClasses: ['btn-link', 'text-start'],
201
- onClick: ->(action) do
202
- action.windows_open url: ref['url']
203
- end
204
-
205
- if ref['description']
206
- ref_item.label text: ref['description'], styleClasses: ['text-muted', 'text-small']
207
- end
208
- else
209
- ref_item.label text: ref['text'], styleClass: 'font-monospace'
210
- end
211
- end
212
- end
213
- end
214
- end
215
- end
@@ -1,103 +0,0 @@
1
- json.title 'Glib Component API Documentation'
2
-
3
- page = json_ui_page json
4
-
5
- page.scroll padding: glib_json_padding_body, childViews: ->(scroll) do
6
- # Header
7
- scroll.panels_vertical \
8
- padding: { top: 32, bottom: 32 },
9
- backgroundColor: '#667eea',
10
- align: 'center',
11
- childViews: ->(header) do
12
- header.h1 text: 'Glib Component API', color: '#ffffff'
13
- header.p text: 'Interactive documentation for all Glib UI components', color: '#ffffff'
14
- header.label text: 'Auto-generated from YARD comments in source code', color: '#e0e0e0', styleClass: 'text-small'
15
- end
16
-
17
- scroll.spacer height: 32
18
-
19
- # Title
20
- scroll.h2 text: 'Component Categories'
21
- scroll.p text: 'Browse components by category', styleClass: 'text-muted'
22
-
23
- scroll.spacer height: 16
24
-
25
- if @categories.empty?
26
- # Warning message
27
- scroll.panels_vertical \
28
- padding: { all: 16 },
29
- backgroundColor: '#fff3cd',
30
- styleClasses: ['rounded'],
31
- childViews: ->(warning) do
32
- warning.h4 text: 'No Documentation Found', color: '#856404'
33
- warning.p text: 'Component documentation has not been generated yet.'
34
- warning.p text: 'Run bin/generate_component_docs to generate documentation.', styleClass: 'code'
35
- end
36
- else
37
- # Categories grid
38
- scroll.panels_responsive width: 'matchParent', childViews: ->(responsive) do
39
- @categories.each do |category|
40
- responsive.panels_column \
41
- lg: { cols: 4 },
42
- md: { cols: 6 },
43
- xs: { cols: 12 },
44
- childViews: ->(col) do
45
- col.panels_vertical \
46
- padding: { all: 16 },
47
- styleClasses: ['card', 'rounded'],
48
- onClick: ->(action) do
49
- action.windows_open url: json_ui_api_url(category: category['key'])
50
- end,
51
- childViews: ->(card) do
52
- # Title with badge
53
- card.panels_horizontal \
54
- align: 'middle',
55
- childViews: ->(title_row) do
56
- title_row.h5 text: category['name']
57
- title_row.spacer width: 8
58
- title_row.label \
59
- text: "#{category['component_count']} components",
60
- styleClasses: ['badge', 'bg-primary']
61
- end
62
-
63
- card.spacer height: 8
64
-
65
- # Source file
66
- if category['source_file']
67
- card.label \
68
- text: File.basename(category['source_file']),
69
- styleClasses: ['text-muted', 'text-small', 'font-monospace']
70
- end
71
-
72
- card.spacer height: 12
73
-
74
- # Button
75
- card.button \
76
- text: 'View Components →',
77
- styleClasses: ['btn-outline-primary', 'btn-sm']
78
- end
79
- end
80
- end
81
- end
82
- end
83
-
84
- scroll.spacer height: 48
85
-
86
- # About section
87
- scroll.h3 text: 'About This Documentation'
88
- scroll.panels_vertical styleClasses: ['card'], padding: { all: 16 }, childViews: ->(about) do
89
- about.p text: 'This documentation is automatically generated from YARD comments in the Glib source code. Each component includes:'
90
-
91
- about.panels_ul childViews: ->(ul) do
92
- ul.label text: 'Description - What the component does and when to use it'
93
- ul.label text: 'Properties - All available properties with types and descriptions'
94
- ul.label text: 'Examples - Real code examples showing how to use the component'
95
- ul.label text: 'References - Links to external documentation and garage examples'
96
- end
97
-
98
- about.spacer height: 8
99
- about.label \
100
- text: 'For more information, see doc/components/README.md and doc/common/component_documentation_guidelines.md',
101
- styleClasses: ['text-muted', 'text-small']
102
- end
103
- end
@@ -1,111 +0,0 @@
1
- json.title "#{@category.titleize} Components"
2
-
3
- page = json_ui_page json
4
-
5
- page.scroll padding: glib_json_padding_body, childViews: ->(scroll) do
6
- # Back button
7
- scroll.button \
8
- text: '← Back to Categories',
9
- styleClasses: ['btn-outline-secondary', 'btn-sm'],
10
- onClick: ->(action) do
11
- action.windows_open url: json_ui_api_url
12
- end
13
-
14
- scroll.spacer height: 16
15
-
16
- # Title
17
- scroll.h2 text: "#{@category.titleize} Components"
18
-
19
- # Metadata
20
- if @doc['meta']
21
- scroll.panels_vertical \
22
- padding: { all: 12 },
23
- backgroundColor: '#f8f9fa',
24
- styleClasses: ['rounded'],
25
- childViews: ->(meta) do
26
- meta.label \
27
- text: "Source: #{@doc['meta']['source_file']}",
28
- styleClasses: ['text-muted', 'text-small', 'font-monospace']
29
-
30
- if @doc['meta']['generated_at']
31
- generated_time = Time.parse(@doc['meta']['generated_at']).strftime('%Y-%m-%d %H:%M') rescue @doc['meta']['generated_at']
32
- meta.label \
33
- text: "Generated: #{generated_time}",
34
- styleClasses: ['text-muted', 'text-small']
35
- end
36
- end
37
- end
38
-
39
- scroll.spacer height: 24
40
-
41
- # Component cards
42
- @components.sort.each do |key, component|
43
- scroll.panels_vertical \
44
- id: key,
45
- padding: { all: 16 },
46
- styleClasses: ['card', 'rounded'],
47
- childViews: ->(card) do
48
- # Title
49
- card.panels_horizontal align: 'middle', childViews: ->(title_row) do
50
- title_row.h4 text: component['class_name']
51
-
52
- if component['extends']
53
- title_row.spacer width: 8
54
- title_row.label \
55
- text: "extends #{component['extends']}",
56
- styleClasses: ['text-muted']
57
- end
58
- end
59
-
60
- # Deprecated warning
61
- if component['deprecated']
62
- card.spacer height: 8
63
- card.panels_vertical \
64
- padding: { all: 12 },
65
- backgroundColor: '#fff3cd',
66
- styleClasses: ['rounded'],
67
- childViews: ->(warning) do
68
- warning.label text: '⚠️ Deprecated', color: '#856404', styleClass: 'font-weight-bold'
69
- deprecation_text = component['deprecated'] == true ? 'This component is deprecated' : component['deprecated']
70
- warning.label text: deprecation_text, color: '#856404'
71
- end
72
- end
73
-
74
- # Description
75
- if component['description']
76
- card.spacer height: 12
77
- component['description'].split("\n\n").each do |paragraph|
78
- card.p text: paragraph.strip
79
- end
80
- end
81
-
82
- card.spacer height: 12
83
-
84
- # Metadata and View Details button
85
- card.panels_horizontal align: 'middle', childViews: ->(actions) do
86
- actions.button \
87
- text: 'View Details →',
88
- styleClasses: ['btn-outline-primary', 'btn-sm'],
89
- onClick: ->(action) do
90
- action.windows_open url: json_ui_api_url(category: @category, component: key)
91
- end
92
-
93
- if component['properties']
94
- actions.spacer width: 12
95
- actions.label \
96
- text: "#{component['properties'].size} #{'property'.pluralize(component['properties'].size)}",
97
- styleClasses: ['text-muted']
98
- end
99
-
100
- if component['examples']
101
- actions.spacer width: 12
102
- actions.label \
103
- text: "#{component['examples'].size} #{'example'.pluralize(component['examples'].size)}",
104
- styleClasses: ['text-muted']
105
- end
106
- end
107
- end
108
-
109
- scroll.spacer height: 16
110
- end
111
- end
data/lib/tasks/docs.rake DELETED
@@ -1,59 +0,0 @@
1
- namespace :glib do
2
- namespace :docs do
3
- desc 'Generate component documentation in YAML format'
4
- task :generate do
5
- require_relative '../glib/doc_generator'
6
-
7
- generator = Glib::DocGenerator.new
8
-
9
- # Generate documentation for different component categories
10
- categories = [
11
- {
12
- name: 'panels',
13
- file: 'app/helpers/glib/json_ui/view_builder/panels.rb',
14
- output: 'doc/components/panels.yml'
15
- },
16
- {
17
- name: 'fields',
18
- file: 'app/helpers/glib/json_ui/view_builder/fields.rb',
19
- output: 'doc/components/fields.yml'
20
- },
21
- {
22
- name: 'views',
23
- file: 'app/helpers/glib/json_ui/view_builder.rb',
24
- output: 'doc/components/views.yml'
25
- },
26
- {
27
- name: 'actions',
28
- file: 'app/helpers/glib/json_ui/action_builder.rb',
29
- output: 'doc/components/actions.yml'
30
- }
31
- ]
32
-
33
- categories.each do |category|
34
- puts "Generating documentation for #{category[:name]}..."
35
- generator.generate_for_file(category[:file], category[:output])
36
- puts " -> #{category[:output]}"
37
- end
38
-
39
- # Also scan action subdirectories
40
- Dir.glob('app/helpers/glib/json_ui/action_builder/*.rb').each do |file|
41
- basename = File.basename(file, '.rb')
42
- output = "doc/components/actions_#{basename}.yml"
43
- puts "Generating documentation for actions/#{basename}..."
44
- generator.generate_for_file(file, output)
45
- puts " -> #{output}"
46
- end
47
-
48
- puts "\nDocumentation generation complete!"
49
- end
50
-
51
- desc 'Validate YARD documentation'
52
- task :validate do
53
- puts "Validating YARD documentation..."
54
- system('yard stats --list-undoc')
55
- system('yard doc --fail-on-warning --no-output')
56
- puts "\nValidation complete!"
57
- end
58
- end
59
- end