glib-web 6.3.1 → 6.4.0

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: 8378cb302a462be1ed8ee965c9ace54b77c4eb29f7d1cc9d83a469c89d0f6d93
4
- data.tar.gz: 91f29270361f13067e3bc513cd01091e82b09d7a89a40eadb4dab893b59f7a66
3
+ metadata.gz: 66421bc5e4ae520c5229884ac1e89dc896990352772d0287413ba61e066e4015
4
+ data.tar.gz: '01995c09d93d6cd07e7cefab2ee5f566c41b83b723582d5434c571db1495e0ab'
5
5
  SHA512:
6
- metadata.gz: 62c1600d052a52e8fb020662ca4db7455518945e298c6e5ea5663e4960955d9d29129252b0780107b138bd4d585a4463f4bef253dc26460203acee362b083af8
7
- data.tar.gz: 4d816f8451f71efbbb96fbcbe64fe1586211b3e328c42ec89580d711abe9f5d62033ff645f1622b855fa31e86eee5a383ed2ff6732d0a817ba6462bea6e23821
6
+ metadata.gz: 99ed682bf5451d7a44b4f0e80f0ca77fbe9d7748c00a9bd784adc9ec6350a727d554371036e5d499f53a275f2594a34287784f6d4d2200471664fd425799788f
7
+ data.tar.gz: 00b5c68ba50339394fe5935622b7db12acfc2ca714fa68cb9224ada024293678296b4bb5e541e3a3fe90a6430497f7351d51fc9e33fbf2ace9fd1bdaafb49e7d
@@ -0,0 +1,73 @@
1
+ module Glib
2
+ module FormStepper
3
+ # A session-backed multi-step wizard: per-step answers accumulate in the
4
+ # session and nothing is persisted until the final step commits. Fits
5
+ # pre-account flows and any wizard where an abandoned run should simply
6
+ # disappear.
7
+ #
8
+ # The including controller declares the flow and its session bucket:
9
+ #
10
+ # class ApplyController < ApplicationController
11
+ # include Glib::FormStepper::Controller
12
+ #
13
+ # STEPS = [
14
+ # Glib::FormStepper::Step.new(key: :program, title: "Program", position: 1),
15
+ # Glib::FormStepper::Step.new(key: :auditor, title: "Auditor", position: 2)
16
+ # ].freeze
17
+ # SESSION_KEY = "apply_application".freeze
18
+ # end
19
+ #
20
+ # The current step is resolved from `params[:id]`, defaulting to the first step.
21
+ module Controller
22
+ extend ActiveSupport::Concern
23
+
24
+ included do
25
+ helper_method :current_step, :next_step, :prev_step, :step_data, :steps
26
+ end
27
+
28
+ def steps
29
+ self.class::STEPS
30
+ end
31
+
32
+ def current_step
33
+ @current_step ||= find_step(params[:id] || steps.first.key)
34
+ end
35
+
36
+ def next_step
37
+ idx = steps.index(current_step)
38
+ idx && steps[idx + 1]
39
+ end
40
+
41
+ def prev_step
42
+ idx = steps.index(current_step)
43
+ idx&.positive? ? steps[idx - 1] : nil
44
+ end
45
+
46
+ # Every answer gathered so far, one hash per step key.
47
+ def session_data
48
+ session[self.class::SESSION_KEY] ||= {}
49
+ end
50
+
51
+ # The current step's answers — what its form reads to prefill itself.
52
+ def step_data
53
+ session_data[current_step.key.to_s] || {}
54
+ end
55
+
56
+ # Merge (not replace) so re-submitting a step updates rather than clobbers.
57
+ def save_step_data(data)
58
+ key = current_step.key.to_s
59
+ session_data[key] = (session_data[key] || {}).merge(data)
60
+ end
61
+
62
+ def clear_session_data
63
+ session.delete(self.class::SESSION_KEY)
64
+ end
65
+
66
+ private
67
+ def find_step(key)
68
+ steps.find { |step| step.key == key.to_sym } ||
69
+ raise(ArgumentError, "Unknown step: #{key}")
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,12 @@
1
+ module Glib
2
+ module FormStepper
3
+ # One step in a session-backed wizard: just its identity and label. `key` is
4
+ # coerced to a symbol so a step declared with either a symbol or string, and a
5
+ # `params[:id]` that always arrives as a string, resolve to the same step.
6
+ Step = Data.define(:key, :title, :position) do
7
+ def initialize(key:, title:, position:)
8
+ super(key: key.to_sym, title: title, position: position)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -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 url_for(value)
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
- @multi_progress = values
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 { |file| { name: file.blob&.filename, signed_id: file.signed_id, url: url_for(file) } }
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
- @multi_progress['responseMessages'] ||= {}
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
- @multi_progress['responseMessages'][status] = I18n.t(key) if I18n.exists?(key)
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
- (@multi_progress['responseMessages'] || {}).reverse_merge(
842
+ messages.reverse_merge(
794
843
  '200' => 'Completed',
795
844
  '403' => 'Forbidden',
796
845
  '401' => 'Session expired',
@@ -0,0 +1,33 @@
1
+ require 'countries' # ISO 3166 data (ISO3166::Country)
2
+
3
+ module Glib
4
+ # ISO 3166 country data for country dropdowns.
5
+ #
6
+ # Backed entirely by the `countries` gem, so there is no table to migrate or
7
+ # seed, and renames (Türkiye, Czechia) arrive with a `bundle update`. Store
8
+ # the alpha-2 code rather than the name, so a rename never invalidates
9
+ # existing data.
10
+ module CountryList
11
+ class << self
12
+ # `common_name` is what people actually call the place ("South Korea",
13
+ # "United Kingdom") rather than the formal ISO name ("Korea (Republic of)").
14
+ def options
15
+ @options ||= ISO3166::Country.all
16
+ .map { |country| { value: country.alpha2, text: country.common_name || country.iso_short_name } }
17
+ .sort_by { |option| option[:text] }
18
+ .freeze
19
+ end
20
+
21
+ def codes
22
+ @codes ||= options.pluck(:value).freeze
23
+ end
24
+
25
+ def name_for(code)
26
+ return if code.blank?
27
+
28
+ country = ISO3166::Country[code]
29
+ country && (country.common_name || country.iso_short_name)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,49 @@
1
+ require 'countries' # ISO 3166-2 subdivision data (ISO3166::Country)
2
+
3
+ module Glib
4
+ # ISO 3166-2 subdivision (state/province/territory) data for state dropdowns.
5
+ #
6
+ # The companion to Glib::CountryList: backed entirely by the `countries` gem,
7
+ # so there is no table to migrate or seed, and renames arrive with a
8
+ # `bundle update`. Store the subdivision code (e.g. "NSW") rather than the
9
+ # name, so a rename never invalidates existing data. The code is only unique
10
+ # within a country, so always store it alongside the country code.
11
+ #
12
+ # Not every country has subdivisions in the ISO data (e.g. Vatican City); for
13
+ # those `options_for` returns `[]` and the UI should fall back to a plain text
14
+ # field or omit the state input entirely.
15
+ module StateList
16
+ class << self
17
+ # Options for a single country's state dropdown, sorted by display name.
18
+ # `country_code` is an alpha-2 code, matching what Glib::CountryList stores.
19
+ def options_for(country_code)
20
+ return [] if country_code.blank?
21
+
22
+ (@options_by_country ||= {})[country_code.to_s.upcase] ||= build_options(country_code)
23
+ end
24
+
25
+ def codes_for(country_code)
26
+ options_for(country_code).pluck(:value)
27
+ end
28
+
29
+ def name_for(country_code, state_code)
30
+ return if country_code.blank? || state_code.blank?
31
+
32
+ country = ISO3166::Country[country_code]
33
+ subdivision = country&.subdivisions&.dig(state_code.to_s.upcase)
34
+ subdivision&.name
35
+ end
36
+
37
+ private
38
+ def build_options(country_code)
39
+ country = ISO3166::Country[country_code]
40
+ return [] if country.nil?
41
+
42
+ country.subdivisions
43
+ .map { |code, subdivision| { value: code, text: subdivision.name } }
44
+ .sort_by { |option| option[:text] }
45
+ .freeze
46
+ end
47
+ end
48
+ end
49
+ end
@@ -24,7 +24,11 @@ markdown = '## Emphasis' + "\n" +
24
24
  It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged.
25
25
  It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages,
26
26
  and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
27
- "
27
+ ".gsub("\n", " \n")
28
+ # The gsub turns each bare newline into a markdown hard break: the renderer
29
+ # uses standard (breaks: false) semantics, under which these hand-wrapped
30
+ # lines would otherwise reflow into one block — and this dialog exists to
31
+ # demonstrate scrolling, so it needs its ~20 visible lines.
28
32
 
29
33
 
30
34
  options = {}
@@ -367,5 +367,8 @@ page.body childViews: ->(body) do
367
367
  # end
368
368
  # )
369
369
  # end
370
+ #
371
+
372
+ res.button text: 'windows/reload', onClick: ->(action) { action.windows_reload }
370
373
  end
371
374
  end
@@ -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-web.rb CHANGED
@@ -1,3 +1,5 @@
1
+ require 'ostruct'
2
+
1
3
  if defined?(::Rails)
2
4
  require 'glib/engine'
3
5
  require 'glib/snapshot'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: glib-web
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.3.1
4
+ version: 6.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ''
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-07-24 00:00:00.000000000 Z
11
+ date: 2026-08-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activestorage
@@ -122,6 +122,34 @@ dependencies:
122
122
  - - ">="
123
123
  - !ruby/object:Gem::Version
124
124
  version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: ostruct
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: countries
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '7.1'
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '7.1'
125
153
  - !ruby/object:Gem::Dependency
126
154
  name: rubocop
127
155
  requirement: !ruby/object:Gem::Requirement
@@ -163,6 +191,8 @@ files:
163
191
  - app/controllers/concerns/glib/analytics/funnel.rb
164
192
  - app/controllers/concerns/glib/auth/policy.rb
165
193
  - app/controllers/concerns/glib/auth/response.rb
194
+ - app/controllers/concerns/glib/form_stepper/controller.rb
195
+ - app/controllers/concerns/glib/form_stepper/step.rb
166
196
  - app/controllers/concerns/glib/json/dynamic_text.rb
167
197
  - app/controllers/concerns/glib/json/libs.rb
168
198
  - app/controllers/concerns/glib/json/new_dynamic_text.rb
@@ -228,8 +258,10 @@ files:
228
258
  - app/models/glib/active_storage/attachment.rb
229
259
  - app/models/glib/active_storage/blob.rb
230
260
  - app/models/glib/application_record.rb
261
+ - app/models/glib/country_list.rb
231
262
  - app/models/glib/dummy_job_application.rb
232
263
  - app/models/glib/dynamic_text_record.rb
264
+ - app/models/glib/state_list.rb
233
265
  - app/models/glib/text.rb
234
266
  - app/policies/glib/application_policy.rb
235
267
  - app/validators/email_typo_validator.rb
@@ -421,6 +453,7 @@ files:
421
453
  - app/views/json_ui/garage/test_page/fields_url_fragment.json.jbuilder
422
454
  - app/views/json_ui/garage/test_page/flow.json.jbuilder
423
455
  - app/views/json_ui/garage/test_page/form.json.jbuilder
456
+ - app/views/json_ui/garage/test_page/form_dirty_fields.json.jbuilder
424
457
  - app/views/json_ui/garage/test_page/form_dirty_hooks.json.jbuilder
425
458
  - app/views/json_ui/garage/test_page/form_dynamic.json.jbuilder
426
459
  - app/views/json_ui/garage/test_page/form_dynamic_dirty.json.jbuilder