glib-web 6.10.1 → 6.10.3
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/helpers/glib/json_ui/view_builder/fields.rb +74 -9
- data/app/jobs/glib/purge_unattached_job.rb +6 -2
- data/app/models/concerns/glib/snapshot_v2.rb +29 -6
- data/app/views/json_ui/garage/test_page/_header.json.jbuilder +1 -0
- data/app/views/json_ui/garage/test_page/fields_change_trigger.json.jbuilder +128 -0
- data/app/views/json_ui/garage/test_page/fields_sign.json.jbuilder +47 -1
- data/lib/glib/purge_unattached/config.rb +38 -0
- data/lib/glib-web.rb +4 -2
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4466ade33c7e7109a75befa19d22951379a4b57f0c20608b26ba80a9e539901b
|
|
4
|
+
data.tar.gz: 9d05610c2feda72d878a64a44dec1f3cf8aa48c7f9237d2b3ce769d89beb9c8a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b2bbafdaaa9ddc994cafe76cc6520a3b8b7dd07e0ae456d666fe62ec061efd8f854f3e719d297d39146e6c0f259c80074f5b28655e2f898b5dbdc2ae243eb4aa
|
|
7
|
+
data.tar.gz: aed913d8a8aeb19d81e0b01bde95480efe12ddbec54acd2095aed02fcb05e2753d57068c586faba341a2e6f282be41ec9b0e353951e881b853130e292d5a4ddd
|
|
@@ -211,6 +211,51 @@ class Glib::JsonUi::ViewBuilder
|
|
|
211
211
|
end
|
|
212
212
|
end
|
|
213
213
|
|
|
214
|
+
# Timing options for the `onChange` / `onChangeAndLoad` actions of fields
|
|
215
|
+
# whose value changes while the user is still typing.
|
|
216
|
+
#
|
|
217
|
+
# Honoured by the single-line text family (text, number, email, url,
|
|
218
|
+
# password -- all one Vue component) and by textarea. Other fields ignore
|
|
219
|
+
# them: the discrete ones (check, radio, select, chip group, date pickers)
|
|
220
|
+
# commit a value the moment it is picked, and richText/phone/file keep
|
|
221
|
+
# their own reporting. The options are declared on `Text`, so its other
|
|
222
|
+
# subclasses accept them without effect -- same as `leftIcon` and
|
|
223
|
+
# `onTypeStart` there.
|
|
224
|
+
#
|
|
225
|
+
# In both modes the change is also reported when focus leaves the field.
|
|
226
|
+
# Under `input` that only flushes a change already pending in the debounce
|
|
227
|
+
# window -- it cannot produce a second run, because a value that was
|
|
228
|
+
# already reported is not reported again.
|
|
229
|
+
module ChangeTrigger
|
|
230
|
+
def self.included(base)
|
|
231
|
+
# When the `onChange` / `onChangeAndLoad` actions fire.
|
|
232
|
+
#
|
|
233
|
+
# - `input` (default): after the user pauses typing for `changeDelay`.
|
|
234
|
+
# - `blur`: only once focus leaves the field. Use for actions too
|
|
235
|
+
# expensive to run mid-word -- a server round trip, a recalculation
|
|
236
|
+
# that rewrites other fields under the user's cursor.
|
|
237
|
+
#
|
|
238
|
+
# @example Round-trip only once the user is done with the field
|
|
239
|
+
# form.fields_text \
|
|
240
|
+
# name: 'user[promo_code]',
|
|
241
|
+
# label: 'Promo code',
|
|
242
|
+
# changeOn: 'blur',
|
|
243
|
+
# onChange: ->(action) { action.http_get url: verify_promo_path }
|
|
244
|
+
base.enum :changeOn, options: %w[input blur]
|
|
245
|
+
|
|
246
|
+
# Milliseconds of no typing before `changeOn: 'input'` fires.
|
|
247
|
+
# Defaults to 300. Ignored under `changeOn: 'blur'`.
|
|
248
|
+
#
|
|
249
|
+
# @example Slow down a search-as-you-type round trip
|
|
250
|
+
# form.fields_text \
|
|
251
|
+
# name: 'q',
|
|
252
|
+
# label: 'Search',
|
|
253
|
+
# changeDelay: 800,
|
|
254
|
+
# onChange: ->(action) { action.http_get url: search_path }
|
|
255
|
+
base.int :changeDelay
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
214
259
|
# Basic text input field for single-line text entry.
|
|
215
260
|
#
|
|
216
261
|
# The most common field type, supporting various customizations like
|
|
@@ -228,6 +273,8 @@ class Glib::JsonUi::ViewBuilder
|
|
|
228
273
|
#
|
|
229
274
|
# @see app/views/json_ui/garage/forms/basic.json.jbuilder Garage example
|
|
230
275
|
class Text < AbstractField
|
|
276
|
+
include ChangeTrigger
|
|
277
|
+
|
|
231
278
|
# Maximum number of characters allowed.
|
|
232
279
|
int :maxLength
|
|
233
280
|
|
|
@@ -909,23 +956,41 @@ class Glib::JsonUi::ViewBuilder
|
|
|
909
956
|
# width: 220,
|
|
910
957
|
# height: 100
|
|
911
958
|
#
|
|
912
|
-
# @note
|
|
913
|
-
#
|
|
914
|
-
#
|
|
959
|
+
# @note A saved signature is a signed id that cannot be redrawn from the
|
|
960
|
+
# canvas: when a record is re-edited the field pre-populates `value`
|
|
961
|
+
# with the existing attachment's signed id (the component renders a
|
|
962
|
+
# read-only preview instead of ink). An untouched field therefore
|
|
963
|
+
# re-submits that same signed id, so consumers should treat a
|
|
964
|
+
# same-signed-id re-attach as "no change". Resize it via
|
|
965
|
+
# `components_set`/`logics_set` with `width`/`height`.
|
|
915
966
|
#
|
|
916
967
|
# @see app/views/json_ui/garage/test_page/fields_sign.json.jbuilder Garage examples
|
|
917
968
|
class Sign < AbstractField
|
|
918
969
|
string :directUploadUrl
|
|
919
970
|
required :directUploadUrl
|
|
920
971
|
|
|
921
|
-
#
|
|
922
|
-
#
|
|
923
|
-
#
|
|
924
|
-
|
|
972
|
+
# URL of an already-attached signature image to display as a read-only
|
|
973
|
+
# preview (typically `file_url(attachment)` when re-editing a record
|
|
974
|
+
# that has a drawn signature). Pair with `prop:` so the field also
|
|
975
|
+
# submits the existing signed id when left untouched.
|
|
976
|
+
string :fileUrl
|
|
977
|
+
|
|
978
|
+
# Enables the Type tab. `value` pre-populates the previously typed
|
|
979
|
+
# name; `label`/`placeholder` decorate the text input in the dialog.
|
|
980
|
+
# `name` is still accepted but unused: the typed name is submitted
|
|
981
|
+
# through the field's own param (single-param contract), and the
|
|
982
|
+
# server routes it to the name attribute.
|
|
983
|
+
hash :typedName, optional: [:name, :value, :label, :placeholder]
|
|
925
984
|
|
|
926
985
|
# Override
|
|
927
|
-
#
|
|
928
|
-
|
|
986
|
+
# A stored signature is an attachment -- submit its signed id (mirrors
|
|
987
|
+
# File#determine_value) so re-editing a signed submission pre-fills the
|
|
988
|
+
# field with the existing blob's signed id instead of serializing the
|
|
989
|
+
# whole attachment proxy.
|
|
990
|
+
def determine_value(context, prop)
|
|
991
|
+
if (value = context.field_value(prop)).attached?
|
|
992
|
+
value.signed_id || ''
|
|
993
|
+
end
|
|
929
994
|
end
|
|
930
995
|
end
|
|
931
996
|
|
|
@@ -6,17 +6,21 @@ module Glib
|
|
|
6
6
|
# Opt-in: if the host app has not run the gem's `create_snapshot_blob_references` migration,
|
|
7
7
|
# `SnapshotBlobReference.table_exists?` is false and the exclusion subquery short-circuits —
|
|
8
8
|
# the job then behaves identically to Rails' built-in unattached-blob cleanup, just delayed
|
|
9
|
-
# by 8 hours to give upload flows time
|
|
9
|
+
# by `Glib::PurgeUnattached::Config.min_age` (8 hours by default) to give upload flows time
|
|
10
|
+
# to attach.
|
|
10
11
|
#
|
|
11
12
|
# Schedule from the host app (e.g. via sidekiq-cron):
|
|
12
13
|
# Glib::PurgeUnattachedJob.perform_later
|
|
14
|
+
#
|
|
15
|
+
# Adjust the grace window from the host app (e.g. for a slower upload flow):
|
|
16
|
+
# Glib::PurgeUnattached::Config.min_age = 10.hours
|
|
13
17
|
class PurgeUnattachedJob < ApplicationJob
|
|
14
18
|
queue_as :cleanup
|
|
15
19
|
|
|
16
20
|
def perform(*_args)
|
|
17
21
|
scope = ActiveStorage::Blob
|
|
18
22
|
.unattached
|
|
19
|
-
.where('active_storage_blobs.created_at < ?',
|
|
23
|
+
.where('active_storage_blobs.created_at < ?', Glib::PurgeUnattached::Config.min_age.ago)
|
|
20
24
|
|
|
21
25
|
if SnapshotBlobReference.table_exists?
|
|
22
26
|
scope = scope.where.not(id: SnapshotBlobReference.select(:blob_id))
|
|
@@ -10,6 +10,24 @@ require 'hashdiff'
|
|
|
10
10
|
# 2. Nil has_one safe-nav in check_snapshot_changed.
|
|
11
11
|
# 3. has_one wrapping in diff_associations (V2 only tested with has_many in mapping-web).
|
|
12
12
|
# 4. ActiveStorage in diff_associations (convert Attached::One/Many to attachment records).
|
|
13
|
+
# 5. same_as_before? derives from the computed diff instead of the instance-local
|
|
14
|
+
# `snapshot_changed` flag (mapping-web's flag-gating silently dropped a target's
|
|
15
|
+
# snapshot when taken after the owner's -- see "Snapshot ordering" below, issue #481).
|
|
16
|
+
#
|
|
17
|
+
# ## Snapshot ordering: owner vs association targets
|
|
18
|
+
#
|
|
19
|
+
# Owner and association-target snapshots may be taken in ANY order within a request.
|
|
20
|
+
# `same_as_before?` decides from persisted state, so a target snapshotted after its owner
|
|
21
|
+
# (through the replaced association target) still records its version.
|
|
22
|
+
#
|
|
23
|
+
# The one residual ordering caveat: `diff` compares against the instance's IN-MEMORY
|
|
24
|
+
# attributes, so unsaved changes live only on the instance that holds them. If a target is
|
|
25
|
+
# modified in memory without saving, snapshot it via a local captured BEFORE the owner's
|
|
26
|
+
# `glib_create_snapshot!` (whose `with_lock` reload replaces the association targets) --
|
|
27
|
+
# reading the target back through the association snapshots the persisted state instead.
|
|
28
|
+
#
|
|
29
|
+
# `snapshot_changed` / `attributes_before_save` remain writable instance accessors for
|
|
30
|
+
# call-site use, but they are no longer consulted for the skip decision.
|
|
13
31
|
#
|
|
14
32
|
# To get all records of a model, you can use the following query:
|
|
15
33
|
# MyModel.joins(:snapshots).distinct
|
|
@@ -106,8 +124,9 @@ module Glib
|
|
|
106
124
|
metadata: {} # Keep for potential future use, but main data in columns
|
|
107
125
|
}
|
|
108
126
|
|
|
109
|
-
# Don't create version if same as before
|
|
110
|
-
|
|
127
|
+
# Don't create version if same as before. Reuses `calculated_diff` instead of
|
|
128
|
+
# recomputing it inside `same_as_before?`.
|
|
129
|
+
unless same_as_before?(calculated_diff)
|
|
111
130
|
# No nil guard: active_snapshot's `create_snapshot!` raises on failure and
|
|
112
131
|
# always returns the snapshot.
|
|
113
132
|
snapshot = create_snapshot!(**snapshot_obj)
|
|
@@ -166,10 +185,14 @@ module Glib
|
|
|
166
185
|
snapshots.order(version: :desc).first
|
|
167
186
|
end
|
|
168
187
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
188
|
+
# Decides whether `glib_create_snapshot!` should skip the version write. Derived from
|
|
189
|
+
# the computed diff (persisted state), never from the instance-local `snapshot_changed`
|
|
190
|
+
# flag: the flag dies with its instance, and an owner's snapshot replaces association
|
|
191
|
+
# targets with fresh instances (`with_lock`'s reload clears the association cache, then
|
|
192
|
+
# the diff walk re-reads the targets), so flag-gating used to silently drop a target's
|
|
193
|
+
# follow-up snapshot taken through the association (issue #481). For a first snapshot
|
|
194
|
+
# (no previous version) the diff runs current state against `attributes_before_save || {}`.
|
|
195
|
+
def same_as_before?(computed_diff = diff)
|
|
173
196
|
item_unchanged = computed_diff['item'].blank?
|
|
174
197
|
assoc_diff = computed_diff['associations']
|
|
175
198
|
associations_unchanged = assoc_diff.nil? || assoc_diff.values.all?(&:blank?)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
json.title 'Test Page (Fields Change Trigger)'
|
|
2
|
+
|
|
3
|
+
page = json_ui_page json
|
|
4
|
+
|
|
5
|
+
render 'json_ui/garage/test_page/header', json: json, page: page
|
|
6
|
+
|
|
7
|
+
page.body(
|
|
8
|
+
childViews: ->(body) do
|
|
9
|
+
|
|
10
|
+
body.panels_responsive(
|
|
11
|
+
padding: glib_json_padding_body,
|
|
12
|
+
childViews: ->(res) do
|
|
13
|
+
res.h2 text: 'Fields Change Trigger'
|
|
14
|
+
res.spacer height: 8
|
|
15
|
+
res.label text: 'changeOn / changeDelay control WHEN a typed-input field reports its change to onChange.'
|
|
16
|
+
res.spacer height: 12
|
|
17
|
+
|
|
18
|
+
res.panels_form(
|
|
19
|
+
url: json_ui_garage_url(path: 'forms/generic_post'),
|
|
20
|
+
method: 'post',
|
|
21
|
+
childViews: ->(form) do
|
|
22
|
+
|
|
23
|
+
# ── Default: changeOn 'input', 300ms ──────────────────────────────
|
|
24
|
+
form.h4 text: 'Default (changeOn: input)'
|
|
25
|
+
form.spacer height: 8
|
|
26
|
+
form.label text: 'No options set: reports once the user pauses for the default 300ms.'
|
|
27
|
+
form.spacer height: 8
|
|
28
|
+
|
|
29
|
+
form.fields_text(
|
|
30
|
+
name: 'user[default_value]',
|
|
31
|
+
label: 'Default',
|
|
32
|
+
placeholder: 'default',
|
|
33
|
+
width: 'matchParent',
|
|
34
|
+
onChange: ->(action) do
|
|
35
|
+
action.logics_set(
|
|
36
|
+
targetId: 'status_default',
|
|
37
|
+
conditionalData: { text: { 'printf': ['Default: {0}', { 'var': 'user[default_value]' }] } }
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
)
|
|
41
|
+
form.spacer height: 8
|
|
42
|
+
form.label id: 'status_default', text: 'Default: idle'
|
|
43
|
+
|
|
44
|
+
form.spacer height: 12
|
|
45
|
+
form.hr width: 'matchParent'
|
|
46
|
+
form.spacer height: 12
|
|
47
|
+
|
|
48
|
+
# ── changeDelay ───────────────────────────────────────────────────
|
|
49
|
+
form.h4 text: 'changeDelay: 1500'
|
|
50
|
+
form.spacer height: 8
|
|
51
|
+
form.label text: 'Same input mode, longer pause. Leaving the field still reports right away, rather than waiting out the remaining delay.'
|
|
52
|
+
form.spacer height: 8
|
|
53
|
+
|
|
54
|
+
form.fields_text(
|
|
55
|
+
name: 'user[slow]',
|
|
56
|
+
label: 'Slow',
|
|
57
|
+
placeholder: 'slow',
|
|
58
|
+
width: 'matchParent',
|
|
59
|
+
changeDelay: 1500,
|
|
60
|
+
onChange: ->(action) do
|
|
61
|
+
action.logics_set(
|
|
62
|
+
targetId: 'status_slow',
|
|
63
|
+
conditionalData: { text: { 'printf': ['Slow: {0}', { 'var': 'user[slow]' }] } }
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
)
|
|
67
|
+
form.spacer height: 8
|
|
68
|
+
form.label id: 'status_slow', text: 'Slow: idle'
|
|
69
|
+
|
|
70
|
+
form.spacer height: 12
|
|
71
|
+
form.hr width: 'matchParent'
|
|
72
|
+
form.spacer height: 12
|
|
73
|
+
|
|
74
|
+
# ── changeOn: 'blur' ──────────────────────────────────────────────
|
|
75
|
+
form.h4 text: "changeOn: 'blur'"
|
|
76
|
+
form.spacer height: 8
|
|
77
|
+
form.label text: 'Stays idle no matter how long the user pauses; reports only once focus leaves.'
|
|
78
|
+
form.spacer height: 8
|
|
79
|
+
|
|
80
|
+
form.fields_text(
|
|
81
|
+
name: 'user[on_blur]',
|
|
82
|
+
label: 'Reports on blur',
|
|
83
|
+
placeholder: 'on blur',
|
|
84
|
+
width: 'matchParent',
|
|
85
|
+
changeOn: 'blur',
|
|
86
|
+
onChange: ->(action) do
|
|
87
|
+
action.logics_set(
|
|
88
|
+
targetId: 'status_on_blur',
|
|
89
|
+
conditionalData: { text: { 'printf': ['Blur: {0}', { 'var': 'user[on_blur]' }] } }
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
)
|
|
93
|
+
form.spacer height: 8
|
|
94
|
+
form.label id: 'status_on_blur', text: 'Blur: idle'
|
|
95
|
+
|
|
96
|
+
form.spacer height: 12
|
|
97
|
+
form.hr width: 'matchParent'
|
|
98
|
+
form.spacer height: 12
|
|
99
|
+
|
|
100
|
+
# ── Textarea on blur ──────────────────────────────────────────────
|
|
101
|
+
form.h4 text: "fields_textarea with changeOn: 'blur'"
|
|
102
|
+
form.spacer height: 8
|
|
103
|
+
|
|
104
|
+
form.fields_textarea(
|
|
105
|
+
name: 'user[notes]',
|
|
106
|
+
label: 'Notes',
|
|
107
|
+
placeholder: 'notes',
|
|
108
|
+
width: 'matchParent',
|
|
109
|
+
rows: 3,
|
|
110
|
+
changeOn: 'blur',
|
|
111
|
+
onChange: ->(action) do
|
|
112
|
+
action.logics_set(
|
|
113
|
+
targetId: 'status_notes',
|
|
114
|
+
conditionalData: { text: { 'printf': ['Notes: {0}', { 'var': 'user[notes]' }] } }
|
|
115
|
+
)
|
|
116
|
+
end
|
|
117
|
+
)
|
|
118
|
+
form.spacer height: 8
|
|
119
|
+
form.label id: 'status_notes', text: 'Notes: idle'
|
|
120
|
+
|
|
121
|
+
form.spacer height: 16
|
|
122
|
+
form.fields_submit text: 'Submit'
|
|
123
|
+
end
|
|
124
|
+
)
|
|
125
|
+
end
|
|
126
|
+
)
|
|
127
|
+
end
|
|
128
|
+
)
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
json.title 'Test Page (Fields Sign)'
|
|
2
2
|
|
|
3
|
+
# 60x20 ink stand-in used by the pre-filled examples.
|
|
4
|
+
preview_data_uri = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAAUCAYAAADRA14pAAAAoklEQVR4nO2XUQrAIAxDcwjvf9WNfQzG1NFqUt1mwL8a8hBtxfYzYXSAaC1glVJK2RohOXAJdCS4FNgCGw0tA7YAjQCXAHsBoqAPbzpwa3A19OkLpjkjsAL66gmWOTOo0gtvOJXe/VcP9JpH3DvmewBLkdeUKfaLD09xqSaid7bkqilrS57pKHJCYmWq9uFZQD2ZLLkeB49ZQO/qybX+w1/XDujJvn2bufeCAAAAAElFTkSuQmCC'
|
|
5
|
+
|
|
3
6
|
page = json_ui_page json
|
|
4
7
|
|
|
5
8
|
render 'json_ui/garage/test_page/header', json: json, page: page
|
|
@@ -26,7 +29,10 @@ page.body(
|
|
|
26
29
|
id: 'signature_main',
|
|
27
30
|
name: 'user[signature]',
|
|
28
31
|
label: 'Your signature',
|
|
29
|
-
|
|
32
|
+
# Single-param contract: the typed name is submitted through the
|
|
33
|
+
# field's own param, so typedName carries only the input's
|
|
34
|
+
# decoration and pre-filled value.
|
|
35
|
+
typedName: { label: 'Type your full name', placeholder: 'Full name' },
|
|
30
36
|
directUploadUrl: glib_direct_uploads_url,
|
|
31
37
|
width: 320,
|
|
32
38
|
height: 160,
|
|
@@ -148,6 +154,46 @@ page.body(
|
|
|
148
154
|
width: 220,
|
|
149
155
|
height: 100
|
|
150
156
|
)
|
|
157
|
+
|
|
158
|
+
form.spacer height: 12
|
|
159
|
+
form.hr width: 'matchParent'
|
|
160
|
+
form.spacer height: 12
|
|
161
|
+
|
|
162
|
+
form.h2 text: 'Pre-filled (re-edit)'
|
|
163
|
+
form.spacer height: 8
|
|
164
|
+
form.label text: 'A saved signature: value carries the signed id, fileUrl renders the preview.'
|
|
165
|
+
form.spacer height: 8
|
|
166
|
+
form.fields_sign(
|
|
167
|
+
id: 'signature_prefilled',
|
|
168
|
+
name: 'user[signature_prefilled]',
|
|
169
|
+
# A stand-in for the signed id the Sign builder derives from
|
|
170
|
+
# `prop:` on a re-edit (see Sign#determine_value).
|
|
171
|
+
value: 'prefilled-signed-id',
|
|
172
|
+
fileUrl: preview_data_uri,
|
|
173
|
+
typedName: { label: 'Type your full name', placeholder: 'Full name' },
|
|
174
|
+
directUploadUrl: glib_direct_uploads_url,
|
|
175
|
+
width: 320,
|
|
176
|
+
height: 160
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
form.spacer height: 12
|
|
180
|
+
form.hr width: 'matchParent'
|
|
181
|
+
form.spacer height: 12
|
|
182
|
+
|
|
183
|
+
form.h2 text: 'Read-only (pre-filled)'
|
|
184
|
+
form.spacer height: 8
|
|
185
|
+
form.label text: 'A saved signature on a read-only field: the preview shows, the edit overlay does not.'
|
|
186
|
+
form.spacer height: 8
|
|
187
|
+
form.fields_sign(
|
|
188
|
+
id: 'signature_readonly',
|
|
189
|
+
name: 'user[signature_readonly]',
|
|
190
|
+
value: 'readonly-signed-id',
|
|
191
|
+
fileUrl: preview_data_uri,
|
|
192
|
+
directUploadUrl: glib_direct_uploads_url,
|
|
193
|
+
width: 320,
|
|
194
|
+
height: 160,
|
|
195
|
+
readOnly: true
|
|
196
|
+
)
|
|
151
197
|
end
|
|
152
198
|
)
|
|
153
199
|
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
require 'active_support/core_ext/numeric/time'
|
|
2
|
+
|
|
3
|
+
module Glib
|
|
4
|
+
module PurgeUnattached
|
|
5
|
+
# Global settings for Glib::PurgeUnattachedJob.
|
|
6
|
+
#
|
|
7
|
+
# `min_age` is how long an unattached ActiveStorage blob must exist before
|
|
8
|
+
# the purge may remove it. The 8-hour default leaves room for multi-step
|
|
9
|
+
# upload flows (direct upload -> form submit -> attach) to finish before
|
|
10
|
+
# their blob becomes eligible; apps with slower flows raise it:
|
|
11
|
+
#
|
|
12
|
+
# # config/initializers/glib.rb
|
|
13
|
+
# Glib::PurgeUnattached::Config.min_age = 10.hours
|
|
14
|
+
#
|
|
15
|
+
# Deliberately NOT a setting on the job class: the job is autoloaded from
|
|
16
|
+
# app/, so an initializer-assigned value would be wiped the first time the
|
|
17
|
+
# class is reloaded. This file lives in lib/ and is required eagerly by
|
|
18
|
+
# glib-web.rb, which is what makes the setting survive reloads.
|
|
19
|
+
class Config
|
|
20
|
+
# The shipped default. Apps opt into a different window via the setter.
|
|
21
|
+
DEFAULT_MIN_AGE = 8.hours
|
|
22
|
+
|
|
23
|
+
@@min_age = DEFAULT_MIN_AGE
|
|
24
|
+
|
|
25
|
+
def self.min_age
|
|
26
|
+
@@min_age
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.min_age=(value)
|
|
30
|
+
unless value.is_a?(ActiveSupport::Duration) && value.positive?
|
|
31
|
+
raise ArgumentError, "Invalid min_age: #{value.inspect}. Expected a positive ActiveSupport::Duration (e.g. 10.hours)."
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
@@min_age = value
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
data/lib/glib-web.rb
CHANGED
|
@@ -6,10 +6,12 @@ if defined?(::Rails)
|
|
|
6
6
|
end
|
|
7
7
|
require 'glib/value'
|
|
8
8
|
# Loaded eagerly (not autoloaded from app/) so an app initializer can set
|
|
9
|
-
# `Glib::JsonUi::Config.name_separator`
|
|
10
|
-
# autoloading during initialization -- and so the
|
|
9
|
+
# `Glib::JsonUi::Config.name_separator` or `Glib::PurgeUnattached::Config.min_age`
|
|
10
|
+
# without tripping Rails' ban on autoloading during initialization -- and so the
|
|
11
|
+
# setting survives a reload.
|
|
11
12
|
require 'glib/json_ui/config'
|
|
12
13
|
require 'glib/json_ui/name'
|
|
14
|
+
require 'glib/purge_unattached/config'
|
|
13
15
|
require 'glib/json_crawler'
|
|
14
16
|
|
|
15
17
|
require 'glib/dynamic_text'
|
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.10.
|
|
4
|
+
version: 6.10.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ''
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-16 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activestorage
|
|
@@ -456,6 +456,7 @@ files:
|
|
|
456
456
|
- app/views/json_ui/garage/test_page/duplicate_names.json.jbuilder
|
|
457
457
|
- app/views/json_ui/garage/test_page/fields.json.jbuilder
|
|
458
458
|
- app/views/json_ui/garage/test_page/fields_captcha.json.jbuilder
|
|
459
|
+
- app/views/json_ui/garage/test_page/fields_change_trigger.json.jbuilder
|
|
459
460
|
- app/views/json_ui/garage/test_page/fields_creditCard.json.jbuilder
|
|
460
461
|
- app/views/json_ui/garage/test_page/fields_date_time.json.jbuilder
|
|
461
462
|
- app/views/json_ui/garage/test_page/fields_dynamicSelect.json.jbuilder
|
|
@@ -590,6 +591,7 @@ files:
|
|
|
590
591
|
- lib/glib/last_resort_unit_test.rb
|
|
591
592
|
- lib/glib/mailer_tester.rb
|
|
592
593
|
- lib/glib/non_http_integration_test.rb
|
|
594
|
+
- lib/glib/purge_unattached/config.rb
|
|
593
595
|
- lib/glib/rubocop.rb
|
|
594
596
|
- lib/glib/rubocop/cops/json_ui/base_nested_parameter.rb
|
|
595
597
|
- lib/glib/rubocop/cops/json_ui/nested_action_parameter.rb
|