glib-web 6.3.2 → 6.4.1
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 +4 -4
- data/app/controllers/glib/blob_url_generators_controller.rb +12 -0
- data/app/helpers/glib/json_ui/view_builder/fields.rb +55 -6
- data/app/models/glib/application_record.rb +8 -4
- data/app/views/json_ui/garage/test_page/form_dirty_fields.json.jbuilder +152 -0
- data/app/views/json_ui/garage/test_page/form_dirty_hooks.json.jbuilder +11 -0
- data/lib/glib/engine.rb +3 -2
- metadata +9 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d0c733a1c0dbc779a2bd72cbcd304074238f10a2efde2d74ee4c75f50bf861d4
|
|
4
|
+
data.tar.gz: 0ea1e58d18cea237c50fccaafb56a165abaa104caecd2011cbbeb7e5bdf7a97d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ff566900716099396b7817beca6b934ad2d18f8338c2dfa6743c9b3ed851927becc596d75d5bd71c9fc90ff3e9bbba572f665228755cc4cac60d6389b50103ff
|
|
7
|
+
data.tar.gz: 93e89c3f04019cf9517412416b4c8bb917b3171677f67a3214d8e12d705bddafb9f1744522452b93a1fa77d2278ed14499ee41d9097fc4a482db6b6ec40ac186
|
|
@@ -16,6 +16,18 @@
|
|
|
16
16
|
module Glib
|
|
17
17
|
class BlobUrlGeneratorsController < ::ActionController::Base
|
|
18
18
|
def create
|
|
19
|
+
# Same failure class as the upload fields' `url_for(attachment)` (see
|
|
20
|
+
# AbstractField#existing_attachment_url): `rails_blob_path` resolves
|
|
21
|
+
# through ActiveStorage's default routes, so in apps that disable those
|
|
22
|
+
# this endpoint would 500 in exactly and only the environments with the
|
|
23
|
+
# routes off. Fail with a teaching error instead.
|
|
24
|
+
unless ::ActiveStorage.draw_routes
|
|
25
|
+
raise 'the default blob URL generator returns `rails_blob_path`, which resolves through ' \
|
|
26
|
+
"ActiveStorage's default routes, and this app does not draw them " \
|
|
27
|
+
'(config.active_storage.draw_routes = false). Point `blobUrlGenerator:` at an app ' \
|
|
28
|
+
'endpoint that returns an authorized URL instead.'
|
|
29
|
+
end
|
|
30
|
+
|
|
19
31
|
blob = ::ActiveStorage::Blob.find_signed(params[:signed_id])
|
|
20
32
|
url = blob.present? ? rails_blob_path(blob) : nil
|
|
21
33
|
|
|
@@ -181,6 +181,34 @@ class Glib::JsonUi::ViewBuilder
|
|
|
181
181
|
association = form.nested_associations.last
|
|
182
182
|
@context ||= association || form
|
|
183
183
|
end
|
|
184
|
+
|
|
185
|
+
# Builds the URL of a file that is ALREADY attached to the model (e.g.
|
|
186
|
+
# the pre-population list an upload field shows when re-editing a
|
|
187
|
+
# record). An app-defined `glib_attachment_url_for(attachment)` view
|
|
188
|
+
# helper takes precedence; otherwise this falls back to
|
|
189
|
+
# `url_for(attachment)`, which resolves through ActiveStorage's default
|
|
190
|
+
# routes. Apps that disable those routes
|
|
191
|
+
# (`config.active_storage.draw_routes = false`, commonly a
|
|
192
|
+
# production-only hardening) MUST define the helper: without it,
|
|
193
|
+
# url_for raises in exactly and only the environments with the routes
|
|
194
|
+
# off, invisible to tests that run with them on. The draw_routes check
|
|
195
|
+
# below turns that into the same loud, teaching error in every
|
|
196
|
+
# environment.
|
|
197
|
+
def existing_attachment_url(attachment)
|
|
198
|
+
view = page.context
|
|
199
|
+
return view.glib_attachment_url_for(attachment) if view.respond_to?(:glib_attachment_url_for)
|
|
200
|
+
|
|
201
|
+
unless ::ActiveStorage.draw_routes
|
|
202
|
+
raise 'cannot build the URL of an existing attachment: `url_for(attachment)` resolves ' \
|
|
203
|
+
"through ActiveStorage's default routes, and this app does not draw them " \
|
|
204
|
+
'(config.active_storage.draw_routes = false). Define a ' \
|
|
205
|
+
'`glib_attachment_url_for(attachment)` view helper that returns the app\'s own ' \
|
|
206
|
+
'authorized file URL; for `fields_upload` you can instead supply the list ' \
|
|
207
|
+
'yourself via `multiProgressView: { files: [...] }`.'
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
url_for(attachment)
|
|
211
|
+
end
|
|
184
212
|
end
|
|
185
213
|
|
|
186
214
|
# Basic text input field for single-line text entry.
|
|
@@ -740,7 +768,7 @@ class Glib::JsonUi::ViewBuilder
|
|
|
740
768
|
|
|
741
769
|
if (value = context.field_value(@prop)).attached?
|
|
742
770
|
json.fileTitle value.blob.filename
|
|
743
|
-
json.fileUrl
|
|
771
|
+
json.fileUrl existing_attachment_url(value.attachment)
|
|
744
772
|
end
|
|
745
773
|
end
|
|
746
774
|
|
|
@@ -755,6 +783,15 @@ class Glib::JsonUi::ViewBuilder
|
|
|
755
783
|
end
|
|
756
784
|
end
|
|
757
785
|
|
|
786
|
+
# File upload field with direct-upload support and a progress list of
|
|
787
|
+
# already-attached files.
|
|
788
|
+
#
|
|
789
|
+
# When re-editing a record, the field pre-populates
|
|
790
|
+
# `multiProgressView.files` from the model's attachments. Apps that
|
|
791
|
+
# disable ActiveStorage's default routes
|
|
792
|
+
# (`config.active_storage.draw_routes = false`) must define a
|
|
793
|
+
# `glib_attachment_url_for(attachment)` view helper returning their own
|
|
794
|
+
# authorized file URL -- see AbstractField#existing_attachment_url.
|
|
758
795
|
class Upload < AbstractField
|
|
759
796
|
include Glib::JsonUi::Upload
|
|
760
797
|
include Glib::JsonUi::FileUploadErrorHandler
|
|
@@ -766,7 +803,12 @@ class Glib::JsonUi::ViewBuilder
|
|
|
766
803
|
hash :clipboardView, optional: [:icon]
|
|
767
804
|
|
|
768
805
|
def multiProgressView(values)
|
|
769
|
-
|
|
806
|
+
# Consumers naturally write jbuilder hashes with symbol keys, but the
|
|
807
|
+
# reads in `created` use string keys. Normalize so a symbol-keyed
|
|
808
|
+
# `files:`/`responseMessages:` is honored instead of silently ignored
|
|
809
|
+
# (an ignored `files:` used to send the field down the url_for
|
|
810
|
+
# pre-population path even when the app supplied its own list).
|
|
811
|
+
@multi_progress = values&.stringify_keys
|
|
770
812
|
end
|
|
771
813
|
|
|
772
814
|
def created
|
|
@@ -779,18 +821,25 @@ class Glib::JsonUi::ViewBuilder
|
|
|
779
821
|
if @prop && context && @multi_progress
|
|
780
822
|
value = context.field_value(@prop, collect_ids: false)
|
|
781
823
|
attachments = value.is_a?(::ActiveStorage::Attached::One) ? Array.wrap(value.attachment) : value.to_a
|
|
782
|
-
@multi_progress['files'] ||= attachments.map
|
|
824
|
+
@multi_progress['files'] ||= attachments.map do |file|
|
|
825
|
+
{ name: file.blob&.filename, signed_id: file.signed_id, url: existing_attachment_url(file) }
|
|
826
|
+
end
|
|
783
827
|
end
|
|
784
828
|
|
|
785
829
|
if @multi_progress
|
|
786
|
-
|
|
830
|
+
# Copy before writing: `stringify_keys` is shallow, so the nested
|
|
831
|
+
# hash may still be the caller's own object, and the I18n merge
|
|
832
|
+
# below must not mutate (or accumulate state in) a hash the
|
|
833
|
+
# consumer may freeze or reuse across renders.
|
|
834
|
+
messages = (@multi_progress['responseMessages'] || {}).dup
|
|
787
835
|
['200', '403', '401', 'else'].each do |status|
|
|
788
836
|
key = "glib.multi_upload.responseMessages.#{status}"
|
|
789
|
-
|
|
837
|
+
messages[status] = I18n.t(key) if I18n.exists?(key)
|
|
790
838
|
end
|
|
839
|
+
@multi_progress['responseMessages'] = messages
|
|
791
840
|
|
|
792
841
|
json.responseMessages(
|
|
793
|
-
|
|
842
|
+
messages.reverse_merge(
|
|
794
843
|
'200' => 'Completed',
|
|
795
844
|
'403' => 'Forbidden',
|
|
796
845
|
'401' => 'Session expired',
|
|
@@ -5,11 +5,15 @@ module Glib
|
|
|
5
5
|
|
|
6
6
|
self.abstract_class = true
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
# The id tiebreakers keep the order deterministic when timestamps tie
|
|
9
|
+
# (rows created in one transaction, dumped fixtures): a bare timestamp
|
|
10
|
+
# ORDER BY returns tied rows in unspecified database order, which flips
|
|
11
|
+
# listings -- and their test snapshots -- between runs.
|
|
12
|
+
scope :created_asc, -> { order(created_at: :asc, id: :asc) }
|
|
13
|
+
scope :created_desc, -> { order(created_at: :desc, id: :desc) }
|
|
10
14
|
|
|
11
|
-
scope :updated_asc, -> { order(updated_at: :asc) }
|
|
12
|
-
scope :updated_desc, -> { order(updated_at: :desc) }
|
|
15
|
+
scope :updated_asc, -> { order(updated_at: :asc, id: :asc) }
|
|
16
|
+
scope :updated_desc, -> { order(updated_at: :desc, id: :desc) }
|
|
13
17
|
|
|
14
18
|
def self.glib_has_one_attached(field_name)
|
|
15
19
|
has_one_attached field_name
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
json.title 'Test Page (Form dirty fields)'
|
|
2
|
+
|
|
3
|
+
page = json_ui_page json
|
|
4
|
+
|
|
5
|
+
render 'json_ui/garage/test_page/header', json: json, page: page
|
|
6
|
+
|
|
7
|
+
# A battery of field types wired to the same dirty-hooks contract as
|
|
8
|
+
# `form_dirty_hooks`: the submit enables only while the form is dirty. Each
|
|
9
|
+
# field here has at least one value-changing gesture that must flip the
|
|
10
|
+
# dirty state (pinned by formDirtyFields.cy.js).
|
|
11
|
+
page.body childViews: ->(body) do
|
|
12
|
+
body.panels_form \
|
|
13
|
+
padding: glib_json_padding_body,
|
|
14
|
+
onDirty: ->(action) do
|
|
15
|
+
action.logics_set targetId: 'dirty_fields_submit', data: { disabled: false }
|
|
16
|
+
end,
|
|
17
|
+
onPristine: ->(action) do
|
|
18
|
+
action.logics_set targetId: 'dirty_fields_submit', data: { disabled: true }
|
|
19
|
+
end,
|
|
20
|
+
# Pins that programmatic value changes reach the form-level onChange too
|
|
21
|
+
# (they fire the same synthetic `change` that native edits do).
|
|
22
|
+
onChange: ->(action) do
|
|
23
|
+
action.logics_set targetId: 'change_marker', data: { text: 'Changed' }
|
|
24
|
+
end,
|
|
25
|
+
childViews: ->(form) do
|
|
26
|
+
form.h2 text: 'Form dirty fields'
|
|
27
|
+
form.spacer height: 8
|
|
28
|
+
|
|
29
|
+
form.label id: 'change_marker', text: 'Unchanged'
|
|
30
|
+
form.spacer height: 8
|
|
31
|
+
|
|
32
|
+
form.fields_text name: 'user[name]', width: 'matchParent', label: 'Name', value: 'Initial', clearable: true
|
|
33
|
+
form.spacer height: 14
|
|
34
|
+
|
|
35
|
+
form.fields_date id: 'dirty_date', name: 'user[date]', width: 'matchParent', label: 'Date', value: '2020-01-10', clearable: true
|
|
36
|
+
form.spacer height: 14
|
|
37
|
+
|
|
38
|
+
form.fields_rating id: 'dirty_rating', name: 'user[rating]', value: 2
|
|
39
|
+
form.spacer height: 14
|
|
40
|
+
|
|
41
|
+
form.fields_otp id: 'dirty_otp', name: 'user[otp]', width: 'matchParent', length: 4
|
|
42
|
+
form.spacer height: 14
|
|
43
|
+
|
|
44
|
+
form.fields_dynamicSelect id: 'dirty_dynamic_select', name: 'user[language]', width: 'matchParent', label: 'Language',
|
|
45
|
+
selectedOptions: [{ value: 'id3', text: 'Item 3' }],
|
|
46
|
+
url: json_ui_garage_url(path: 'forms/dynamic_select_data'),
|
|
47
|
+
placeholder: 'Start typing'
|
|
48
|
+
form.spacer height: 14
|
|
49
|
+
|
|
50
|
+
form.button text: 'Check all', onClick: ->(action) do
|
|
51
|
+
action.components_invoke targetId: 'dirty_check_group', name: 'checkAll'
|
|
52
|
+
end
|
|
53
|
+
form.spacer height: 4
|
|
54
|
+
form.fields_checkGroup id: 'dirty_check_group', name: 'user[hobbies][]', childViews: ->(group) do
|
|
55
|
+
group.fields_check checkValue: 'grape', label: 'Grape'
|
|
56
|
+
group.fields_check checkValue: 'banana', label: 'Banana'
|
|
57
|
+
end
|
|
58
|
+
form.spacer height: 14
|
|
59
|
+
|
|
60
|
+
properties = [
|
|
61
|
+
[{ name: 'question', value: 'Punctuality' }]
|
|
62
|
+
]
|
|
63
|
+
form.fields_dynamicGroup id: 'dirty_dynamic_group', width: 'matchParent', name: 'user[evaluation]', groupFieldProperties: properties, titlePrefix: 'Entry', content: ->(group) do
|
|
64
|
+
group.template childViews: ->(template) do
|
|
65
|
+
template.fields_text width: 'matchParent', name: 'question', label: 'Question', styleClass: 'question'
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
form.spacer height: 14
|
|
69
|
+
|
|
70
|
+
form.fields_upload \
|
|
71
|
+
id: 'dirty_upload',
|
|
72
|
+
name: 'user[attachments][]',
|
|
73
|
+
label: 'Attachments',
|
|
74
|
+
width: 320,
|
|
75
|
+
directUploadUrl: glib_direct_uploads_url,
|
|
76
|
+
multiProgressView: { files: [] }
|
|
77
|
+
form.spacer height: 14
|
|
78
|
+
|
|
79
|
+
# Avatar-style upload prepopulated with a saved value; resetting it
|
|
80
|
+
# changes the submitted value, so it must count as dirty.
|
|
81
|
+
form.fields_upload \
|
|
82
|
+
id: 'dirty_avatar',
|
|
83
|
+
name: 'user[avatar]',
|
|
84
|
+
width: 320,
|
|
85
|
+
directUploadUrl: glib_direct_uploads_url,
|
|
86
|
+
value: 'saved-avatar-signed-id',
|
|
87
|
+
placeholderView: { type: 'avatar', url: 'https://picsum.photos/id/11/100/60', width: 64, height: 64 }
|
|
88
|
+
form.button text: 'Reset avatar', onClick: ->(action) do
|
|
89
|
+
action.components_invoke targetId: 'dirty_avatar', name: 'reset'
|
|
90
|
+
end
|
|
91
|
+
form.spacer height: 14
|
|
92
|
+
|
|
93
|
+
form.fields_sign id: 'dirty_sign', name: 'user[signature]', directUploadUrl: glib_direct_uploads_url, width: 320, height: 160
|
|
94
|
+
form.spacer height: 14
|
|
95
|
+
|
|
96
|
+
# Prepopulated phone: must stay pristine at load (the lib normalises the
|
|
97
|
+
# value at mount) and a type/backspace round trip must return pristine.
|
|
98
|
+
form.fields_phone id: 'dirty_phone', name: 'user[phone]', width: 'matchParent', label: 'Phone',
|
|
99
|
+
value: '+61212345678', defaultCountry: 'AU', disableAutoDetect: true
|
|
100
|
+
form.spacer height: 14
|
|
101
|
+
|
|
102
|
+
# Input-view upload; reset must count as a change.
|
|
103
|
+
form.fields_upload \
|
|
104
|
+
id: 'dirty_document',
|
|
105
|
+
name: 'user[document]',
|
|
106
|
+
label: 'Document',
|
|
107
|
+
width: 320,
|
|
108
|
+
directUploadUrl: glib_direct_uploads_url,
|
|
109
|
+
inputView: {}
|
|
110
|
+
form.button text: 'Reset document', onClick: ->(action) do
|
|
111
|
+
action.components_invoke targetId: 'dirty_document', name: 'reset'
|
|
112
|
+
end
|
|
113
|
+
form.spacer height: 14
|
|
114
|
+
|
|
115
|
+
# Clipboard uploads: direct-upload mode and plain (no-upload) mode.
|
|
116
|
+
form.fields_upload \
|
|
117
|
+
id: 'dirty_clipboard',
|
|
118
|
+
name: 'user[snippet]',
|
|
119
|
+
label: 'Snippet',
|
|
120
|
+
width: 320,
|
|
121
|
+
directUploadUrl: glib_direct_uploads_url,
|
|
122
|
+
clipboardView: {}
|
|
123
|
+
form.spacer height: 14
|
|
124
|
+
form.fields_upload \
|
|
125
|
+
id: 'dirty_clipboard_plain',
|
|
126
|
+
name: 'user[snippet_plain]',
|
|
127
|
+
label: 'Snippet (plain)',
|
|
128
|
+
width: 320,
|
|
129
|
+
clipboardView: {}
|
|
130
|
+
form.spacer height: 14
|
|
131
|
+
|
|
132
|
+
# Single-file upload with a saved value; replacing it must count.
|
|
133
|
+
form.fields_upload \
|
|
134
|
+
id: 'dirty_cover',
|
|
135
|
+
name: 'user[cover]',
|
|
136
|
+
label: 'Cover',
|
|
137
|
+
width: 320,
|
|
138
|
+
directUploadUrl: glib_direct_uploads_url,
|
|
139
|
+
multiple: false,
|
|
140
|
+
multiProgressView: { files: [{ name: 'cover.png', signed_id: 'saved-cover', url: 'https://picsum.photos/id/12/100/60' }] }
|
|
141
|
+
form.spacer height: 14
|
|
142
|
+
|
|
143
|
+
form.fields_richText id: 'dirty_rich_text', name: 'user[bio]', width: 'matchParent', label: 'Bio', value: '<p>hello</p>'
|
|
144
|
+
form.spacer height: 14
|
|
145
|
+
|
|
146
|
+
# Populated from the URL hash at mount; must be absorbed into the
|
|
147
|
+
# pristine baseline, not read as an edit.
|
|
148
|
+
form.fields_urlFragment name: 'user[fragment]'
|
|
149
|
+
|
|
150
|
+
form.fields_submit id: 'dirty_fields_submit', text: 'Save'
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -26,6 +26,17 @@ page.body childViews: ->(body) do
|
|
|
26
26
|
form.fields_text name: 'user[ignored]', width: 'matchParent', label: 'Ignored', disableDirtyCheck: true
|
|
27
27
|
form.spacer height: 14
|
|
28
28
|
|
|
29
|
+
# Direct-upload field -- a completed upload (and a removal) must count
|
|
30
|
+
# towards dirtiness just like a typed edit.
|
|
31
|
+
form.fields_upload \
|
|
32
|
+
name: 'user[attachments][]',
|
|
33
|
+
id: 'dirty_hooks_upload',
|
|
34
|
+
label: 'Attachments',
|
|
35
|
+
width: 320,
|
|
36
|
+
directUploadUrl: glib_direct_uploads_url,
|
|
37
|
+
multiProgressView: { files: [] }
|
|
38
|
+
form.spacer height: 14
|
|
39
|
+
|
|
29
40
|
form.fields_submit id: 'dirty_hooks_submit', text: 'Save'
|
|
30
41
|
end
|
|
31
42
|
end
|
data/lib/glib/engine.rb
CHANGED
|
@@ -16,9 +16,10 @@ module Glib
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
# Mirror `Glib::ApplicationRecord.created_desc` so call sites can use the codebase-wide
|
|
19
|
-
# convention instead of `order(created_at: :desc)`.
|
|
19
|
+
# convention instead of `order(created_at: :desc)`. Includes the same id
|
|
20
|
+
# tiebreaker: tied timestamps otherwise return in unspecified order.
|
|
20
21
|
unless respond_to?(:created_desc)
|
|
21
|
-
scope :created_desc, -> { order(created_at: :desc) }
|
|
22
|
+
scope :created_desc, -> { order(created_at: :desc, id: :desc) }
|
|
22
23
|
end
|
|
23
24
|
|
|
24
25
|
# Parses the stored diff into a flat hash of Change structs:
|
metadata
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: glib-web
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 6.
|
|
4
|
+
version: 6.4.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ''
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: bin
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
11
|
+
date: 2026-08-12 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: activestorage
|
|
@@ -177,6 +178,7 @@ dependencies:
|
|
|
177
178
|
- - ">="
|
|
178
179
|
- !ruby/object:Gem::Version
|
|
179
180
|
version: '0'
|
|
181
|
+
description:
|
|
180
182
|
email: ''
|
|
181
183
|
executables: []
|
|
182
184
|
extensions: []
|
|
@@ -451,6 +453,7 @@ files:
|
|
|
451
453
|
- app/views/json_ui/garage/test_page/fields_url_fragment.json.jbuilder
|
|
452
454
|
- app/views/json_ui/garage/test_page/flow.json.jbuilder
|
|
453
455
|
- app/views/json_ui/garage/test_page/form.json.jbuilder
|
|
456
|
+
- app/views/json_ui/garage/test_page/form_dirty_fields.json.jbuilder
|
|
454
457
|
- app/views/json_ui/garage/test_page/form_dirty_hooks.json.jbuilder
|
|
455
458
|
- app/views/json_ui/garage/test_page/form_dynamic.json.jbuilder
|
|
456
459
|
- app/views/json_ui/garage/test_page/form_dynamic_dirty.json.jbuilder
|
|
@@ -571,8 +574,10 @@ files:
|
|
|
571
574
|
- lib/glib/time_returning_mailer.rb
|
|
572
575
|
- lib/glib/value.rb
|
|
573
576
|
- lib/tasks/db.rake
|
|
577
|
+
homepage:
|
|
574
578
|
licenses: []
|
|
575
579
|
metadata: {}
|
|
580
|
+
post_install_message:
|
|
576
581
|
rdoc_options: []
|
|
577
582
|
require_paths:
|
|
578
583
|
- lib
|
|
@@ -587,7 +592,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
587
592
|
- !ruby/object:Gem::Version
|
|
588
593
|
version: '0'
|
|
589
594
|
requirements: []
|
|
590
|
-
rubygems_version: 4.
|
|
595
|
+
rubygems_version: 3.4.6
|
|
596
|
+
signing_key:
|
|
591
597
|
specification_version: 4
|
|
592
598
|
summary: ''
|
|
593
599
|
test_files: []
|