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 +4 -4
- data/app/controllers/concerns/glib/form_stepper/controller.rb +73 -0
- data/app/controllers/concerns/glib/form_stepper/step.rb +12 -0
- 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/country_list.rb +33 -0
- data/app/models/glib/state_list.rb +49 -0
- data/app/views/json_ui/garage/actions/_dialogs_show.json.jbuilder +5 -1
- data/app/views/json_ui/garage/test_page/charts.json.jbuilder +3 -0
- 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-web.rb +2 -0
- metadata +35 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 66421bc5e4ae520c5229884ac1e89dc896990352772d0287413ba61e066e4015
|
|
4
|
+
data.tar.gz: '01995c09d93d6cd07e7cefab2ee5f566c41b83b723582d5434c571db1495e0ab'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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',
|
|
@@ -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 = {}
|
|
@@ -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
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.
|
|
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-
|
|
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
|