glib-web 7.0.0.beta1 → 7.0.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/auth/policy.rb +5 -1
- data/app/controllers/concerns/glib/auth/token_authenticatable.rb +37 -0
- data/app/controllers/concerns/glib/json/libs.rb +33 -0
- data/app/controllers/glib/api_controller.rb +52 -0
- data/app/helpers/glib/json_ui/view_builder/fields.rb +133 -4
- data/app/helpers/glib/json_ui/view_builder/panels.rb +8 -144
- data/app/jobs/glib/purge_unattached_job.rb +32 -0
- data/app/models/concerns/glib/snapshot_v2.rb +514 -0
- data/app/models/glib/model.rb +40 -0
- data/app/models/glib/plain_model.rb +23 -0
- data/app/models/snapshot_blob_reference.rb +20 -0
- data/app/views/json_ui/garage/tables/bulk_edit.json.jbuilder +6 -6
- data/app/views/json_ui/garage/test_page/_header.json.jbuilder +1 -1
- data/app/views/json_ui/garage/test_page/dialog.json.jbuilder +21 -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 +49 -1
- data/app/views/json_ui/garage/test_page/panels_bulkEdit3.json.jbuilder +2 -2
- data/lib/generators/glib/snapshot_v2_generator.rb +54 -0
- data/lib/generators/templates/snapshot_v2/add_snapshot_v2_columns.rb +18 -0
- data/lib/generators/templates/snapshot_v2/create_snapshot_blob_references.rb +10 -0
- data/lib/glib/engine.rb +32 -0
- data/lib/glib/json_crawler/action_crawlers/forms_submit.rb +1 -1
- data/lib/glib/purge_unattached/config.rb +38 -0
- data/lib/glib/test/full_suite_rubocop.rb +87 -0
- data/lib/glib/test/rubocop_guards.rb +42 -0
- data/lib/glib/test_helpers.rb +9 -7
- data/lib/glib-web.rb +4 -2
- metadata +29 -3
- data/app/views/json_ui/garage/test_page/panels_bulkEdit2.json.jbuilder +0 -244
- data/lib/glib/test/changed_files_rubocop.rb +0 -55
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8ea8c829dd6dc5164f98cf51bfed43321128774d9ac1bbc36e5dc317be1dbee5
|
|
4
|
+
data.tar.gz: 8a4667dcde7336f585c98759b52995cd97f8c125855858edd36bd129aa987d01
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 367ea48dd6c7f0b8168d0fb097d504f756fad8b92834d3e5d285d05c01d6f9dcdac00478b964b83c7105359b2450638a000ab5adb36848864d7997f1bb07bb2d
|
|
7
|
+
data.tar.gz: 401c7ebfc8b3e0dc38cac9bc4630d2eb14764130305592ca3b8cebac26532a980f0d67e28af094c02971a8e90f776ac25282bd4d53c147904e24ee8d9699784f
|
|
@@ -22,7 +22,11 @@ module Glib::Auth
|
|
|
22
22
|
# - Need to find a solution where we can reuse a single public policy
|
|
23
23
|
# after_action :verify_authorized
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
# Guarded because not every includer has `helper_method`: ActionController::Base
|
|
26
|
+
# does, but bare ActionController::API does not (gems like jbuilder may include
|
|
27
|
+
# ActionController::Helpers into it in a booted app, and a plain non-controller
|
|
28
|
+
# class never has it). Same guard Pundit's own included block uses.
|
|
29
|
+
helper_method :policy, :can?, :cannot? if respond_to?(:helper_method)
|
|
26
30
|
end
|
|
27
31
|
|
|
28
32
|
def assert_current_user_present
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Glib::Auth
|
|
2
|
+
# Bearer-token authentication for machine-facing APIs. The token's storage column and
|
|
3
|
+
# the user model are app-specific, so the lookup is a template method the app's base
|
|
4
|
+
# controller implements (return the user for a valid token, nil otherwise).
|
|
5
|
+
module TokenAuthenticatable
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
# Public to mirror the browser side, where Devise's current_user is public: glib
|
|
9
|
+
# hands policies the controller (Glib::ApplicationPolicy#controller), and host-app
|
|
10
|
+
# policies call these with an EXPLICIT receiver (`controller.current_user`,
|
|
11
|
+
# `delegate :user_signed_in?, to: :controller`). A private current_user would raise
|
|
12
|
+
# NoMethodError on the API line only -- the two surfaces must behave the same.
|
|
13
|
+
def current_user
|
|
14
|
+
@current_user
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Provided by Devise's helpers on the browser side; defined here so glib policies
|
|
18
|
+
# that delegate it to the controller work unchanged on API controllers.
|
|
19
|
+
def user_signed_in?
|
|
20
|
+
current_user.present?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
def authenticate_api_token!
|
|
25
|
+
token = request.headers['Authorization'].to_s.sub(/\ABearer\s+/i, '')
|
|
26
|
+
@current_user = glib_find_user_by_api_token(token) if token.present?
|
|
27
|
+
return if @current_user
|
|
28
|
+
|
|
29
|
+
render json: { error: 'unauthorized' }, status: :unauthorized
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# App implements: return the user for a valid token, nil otherwise.
|
|
33
|
+
def glib_find_user_by_api_token(_token)
|
|
34
|
+
raise NotImplementedError, "#{self.class} must define glib_find_user_by_api_token(token)"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -96,6 +96,18 @@ module Glib::Json::Libs
|
|
|
96
96
|
|
|
97
97
|
json_ui_redirect_to url, status: status
|
|
98
98
|
end
|
|
99
|
+
# Any other request format (e.g. a browser tab opening a protected file link
|
|
100
|
+
# `/temp_blobs/:id/report.pdf`, where the filename sets the format) used to raise
|
|
101
|
+
# ActionController::UnknownFormat -> a bare 406 with an empty body. Mirror the html
|
|
102
|
+
# branch instead: whatever opened the URL (browser tab, iframe, img) is redirected
|
|
103
|
+
# to the target, which the browser follows natively.
|
|
104
|
+
#
|
|
105
|
+
# MUST stay the LAST block. `negotiate_mime` answers `Accept: */*` requests with
|
|
106
|
+
# the FIRST declared format, so an `any` block placed before html would capture
|
|
107
|
+
# ordinary browser page loads and leave request.format as `*/*` for the action.
|
|
108
|
+
format.any do
|
|
109
|
+
redirect_to url
|
|
110
|
+
end
|
|
99
111
|
end
|
|
100
112
|
end
|
|
101
113
|
|
|
@@ -142,6 +154,17 @@ module Glib::Json::Libs
|
|
|
142
154
|
redirect_to sign_in_url, **redirect_options
|
|
143
155
|
end
|
|
144
156
|
end
|
|
157
|
+
# Other non-HTML/JSON formats (a protected file link whose filename sets the
|
|
158
|
+
# request format, an XML export, ...) used to raise UnknownFormat -> a bare 406.
|
|
159
|
+
# Mirrors the csv branch: a clean gate status in test, a redirect in production.
|
|
160
|
+
# MUST stay the LAST block -- see glib_redirect_to.
|
|
161
|
+
format.any do
|
|
162
|
+
if Rails.env.test?
|
|
163
|
+
head :unauthorized
|
|
164
|
+
else
|
|
165
|
+
redirect_to sign_in_url, **redirect_options
|
|
166
|
+
end
|
|
167
|
+
end
|
|
145
168
|
end
|
|
146
169
|
end
|
|
147
170
|
|
|
@@ -185,6 +208,16 @@ module Glib::Json::Libs
|
|
|
185
208
|
redirect_to user_default_url
|
|
186
209
|
end
|
|
187
210
|
end
|
|
211
|
+
# Other non-HTML/JSON formats -- mirrors the csv branch (clean gate status in
|
|
212
|
+
# test, redirect in production); previously raised UnknownFormat -> a bare 406.
|
|
213
|
+
# MUST stay the LAST block -- see glib_redirect_to.
|
|
214
|
+
format.any do
|
|
215
|
+
if Rails.env.test?
|
|
216
|
+
head :forbidden
|
|
217
|
+
else
|
|
218
|
+
redirect_to user_default_url
|
|
219
|
+
end
|
|
220
|
+
end
|
|
188
221
|
end
|
|
189
222
|
end
|
|
190
223
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# The ApplicationController analog for agent- or machine-facing JSON APIs.
|
|
2
|
+
#
|
|
3
|
+
# Posture (do not re-derive it from a sibling app):
|
|
4
|
+
# - The Bearer token IS the credential; possession decides authentication. There is
|
|
5
|
+
# deliberately no cookie/session path in: ActionController::API has no CSRF protection,
|
|
6
|
+
# so a cookie-authenticated write endpoint here would be a CSRF target. Do not add one.
|
|
7
|
+
# - Authorization works exactly as on the browser side: policies decide, one per resource,
|
|
8
|
+
# derived by `glib_authorize_resource` from the controller name (override with
|
|
9
|
+
# `class:` when the derived name is wrong, as on any glib controller).
|
|
10
|
+
# - Every subclass defines `glib_load_resource` (glib has no default) and is covered by a
|
|
11
|
+
# policy, including index-only controllers (define it as a no-op there).
|
|
12
|
+
# - Opt OUT of authentication with `skip_before_action :authenticate_api_token!` for a
|
|
13
|
+
# public endpoint; never opt in ad hoc.
|
|
14
|
+
#
|
|
15
|
+
# Callback order is declaration order and is load-bearing:
|
|
16
|
+
# authenticate_api_token! -> glib_load_resource -> glib_authorize_resource
|
|
17
|
+
# Token auth is declared before glib_auth_init so no request can reach record loading or
|
|
18
|
+
# authorization unauthenticated. Keep the includes in that order in subclasses too.
|
|
19
|
+
#
|
|
20
|
+
# Unhandled exceptions keep Rails' default error body, which is HTML in development
|
|
21
|
+
# unless the app is api-only. Consumers wanting JSON there should set
|
|
22
|
+
# `config.debug_exception_response_format = :api` or add their own
|
|
23
|
+
# `rescue_from` (the browser side's json_libs_rescue_500 does this for the same reason).
|
|
24
|
+
class Glib::ApiController < ActionController::API
|
|
25
|
+
include Glib::Auth::TokenAuthenticatable
|
|
26
|
+
before_action :authenticate_api_token!
|
|
27
|
+
|
|
28
|
+
include Glib::Auth::Policy
|
|
29
|
+
glib_auth_init
|
|
30
|
+
|
|
31
|
+
rescue_from ActiveRecord::RecordNotFound do
|
|
32
|
+
render json: { error: 'not found' }, status: :not_found
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
rescue_from ActiveRecord::RecordInvalid do |e|
|
|
36
|
+
render json: { errors: e.record.errors }, status: :unprocessable_entity
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
rescue_from ActionController::ParameterMissing do
|
|
40
|
+
render json: { error: 'bad request' }, status: :bad_request
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# The parent class, not Glib::Auth::Policy::UnauthorizedError: this covers both glib's
|
|
44
|
+
# raise sites (the subclass) and Pundit's own authorize(), which raises the parent --
|
|
45
|
+
# otherwise a consumer calling authorize() would get a 500 instead of JSON.
|
|
46
|
+
rescue_from Pundit::NotAuthorizedError do
|
|
47
|
+
# Token auth halts unauthenticated requests earlier, so this is normally 403;
|
|
48
|
+
# keep the 401 branch so the class is safe if a subclass skips token auth.
|
|
49
|
+
status = current_user ? :forbidden : :unauthorized
|
|
50
|
+
render json: { error: status.to_s }, status: status
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -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
|
|
|
@@ -659,7 +706,10 @@ class Glib::JsonUi::ViewBuilder
|
|
|
659
706
|
class ChipGroup < AbstractField
|
|
660
707
|
color :color
|
|
661
708
|
array :options
|
|
662
|
-
|
|
709
|
+
# cache: true for the same reason as Upload's — without it a
|
|
710
|
+
# multiple-selection ChipGroup submits under a single key and only the
|
|
711
|
+
# last selected value survives server-side parsing.
|
|
712
|
+
bool :multiple, cache: true
|
|
663
713
|
end
|
|
664
714
|
|
|
665
715
|
class TimeZone < AbstractField
|
|
@@ -797,7 +847,11 @@ class Glib::JsonUi::ViewBuilder
|
|
|
797
847
|
include Glib::JsonUi::FileUploadErrorHandler
|
|
798
848
|
|
|
799
849
|
action :onFinishUpload
|
|
800
|
-
|
|
850
|
+
# cache: true so `@multiple` is set before AbstractField#created computes
|
|
851
|
+
# the submit key — without it the `[]` array suffix is dropped and a
|
|
852
|
+
# multi-file submit collapses to the last file server-side (silently
|
|
853
|
+
# saves one Document per drop instead of one per file).
|
|
854
|
+
bool :multiple, cache: true
|
|
801
855
|
hash :placeholderView, required: [:type, :width, :height], optional: [:url]
|
|
802
856
|
hash :inputView, optional: [:files, :variant]
|
|
803
857
|
hash :clipboardView, optional: [:icon]
|
|
@@ -855,13 +909,88 @@ class Glib::JsonUi::ViewBuilder
|
|
|
855
909
|
end
|
|
856
910
|
end
|
|
857
911
|
|
|
912
|
+
# Signature field that lets the user draw a signature on a canvas and
|
|
913
|
+
# upload it as an image via ActiveStorage direct upload.
|
|
914
|
+
#
|
|
915
|
+
# The form shows a placeholder sized by `width`/`height`; tapping it opens
|
|
916
|
+
# a dialog with the drawing canvas, and Confirm uploads the drawing as a
|
|
917
|
+
# PNG to `directUploadUrl` and submits the resulting signed id under
|
|
918
|
+
# `name`. The placeholder then shows the signature, and reopening the
|
|
919
|
+
# dialog lets the user edit it. Because the canvas is not in the form,
|
|
920
|
+
# the placeholder can be given the same height as a neighbouring input
|
|
921
|
+
# (e.g. a typed-name alternative) without losing drawing space.
|
|
922
|
+
#
|
|
923
|
+
# `label` titles the dialog; `placeholder` replaces the "Tap to sign"
|
|
924
|
+
# prompt.
|
|
925
|
+
#
|
|
926
|
+
# With `typedName` the dialog gains Draw/Type tabs: the user can type
|
|
927
|
+
# their name instead of drawing, and the placeholder then shows the name
|
|
928
|
+
# in a handwriting style (the `glib-sign-typed` hook carries the font).
|
|
929
|
+
# The typed name is submitted under `typedName[:name]`; exactly one of the
|
|
930
|
+
# signed id and the typed name is ever non-blank. `typedName[:value]`
|
|
931
|
+
# pre-populates an existing name.
|
|
932
|
+
#
|
|
933
|
+
# @example Basic signature field
|
|
934
|
+
# form.fields_sign \
|
|
935
|
+
# name: 'user[signature]',
|
|
936
|
+
# label: 'Your signature',
|
|
937
|
+
# directUploadUrl: glib_direct_uploads_url,
|
|
938
|
+
# width: 320,
|
|
939
|
+
# height: 160,
|
|
940
|
+
# validation: { required: { message: 'add your signature!' } }
|
|
941
|
+
#
|
|
942
|
+
# @example Drawn or typed
|
|
943
|
+
# form.fields_sign \
|
|
944
|
+
# name: 'user[signature]',
|
|
945
|
+
# label: 'Your signature',
|
|
946
|
+
# typedName: { name: 'user[signature_name]', label: 'Type your full name', placeholder: 'Full name' },
|
|
947
|
+
# directUploadUrl: glib_direct_uploads_url,
|
|
948
|
+
# width: 320,
|
|
949
|
+
# height: 160
|
|
950
|
+
#
|
|
951
|
+
# @example Compact signature without validation
|
|
952
|
+
# form.fields_sign \
|
|
953
|
+
# id: 'signature_compact',
|
|
954
|
+
# name: 'user[signature_compact]',
|
|
955
|
+
# directUploadUrl: glib_direct_uploads_url,
|
|
956
|
+
# width: 220,
|
|
957
|
+
# height: 100
|
|
958
|
+
#
|
|
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`.
|
|
966
|
+
#
|
|
967
|
+
# @see app/views/json_ui/garage/test_page/fields_sign.json.jbuilder Garage examples
|
|
858
968
|
class Sign < AbstractField
|
|
859
969
|
string :directUploadUrl
|
|
860
970
|
required :directUploadUrl
|
|
861
971
|
|
|
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]
|
|
984
|
+
|
|
862
985
|
# Override
|
|
863
|
-
#
|
|
864
|
-
|
|
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
|
|
865
994
|
end
|
|
866
995
|
end
|
|
867
996
|
|
|
@@ -493,129 +493,23 @@ class Glib::JsonUi::ViewBuilder
|
|
|
493
493
|
end
|
|
494
494
|
end
|
|
495
495
|
|
|
496
|
-
|
|
496
|
+
# The generic bulk-edit table: form-peer submission, per-row POSTs,
|
|
497
|
+
# viewCells/viewHeaders, plus the full parse/gate/dedup config so any
|
|
498
|
+
# consumer can compose specialized import behavior (it replaced the
|
|
499
|
+
# retired bulkEdit2 and bulkEditSg -- see the component's header comment
|
|
500
|
+
# for the full mapping).
|
|
501
|
+
class BulkEdit3 < View
|
|
497
502
|
hash :import, required: [:submitUrl, :paramName]
|
|
498
503
|
|
|
499
504
|
# Override
|
|
500
505
|
def created
|
|
501
|
-
#
|
|
506
|
+
# bulkEdit3 is a form PEER, not a form child: it answers the same
|
|
502
507
|
# forms_submit action a form does (forms/directSubmit) and submits its
|
|
503
508
|
# rows itself to `import.submitUrl`. Inside a form, its repeated
|
|
504
509
|
# per-row input names would collide with the form's FormData scans
|
|
505
510
|
# (duplicate-name warnings, dirty-baseline pollution) while adding
|
|
506
511
|
# nothing to the form's submission -- so, like a nested form, reject
|
|
507
512
|
# the arrangement at render time.
|
|
508
|
-
if page.current_form
|
|
509
|
-
raise 'panels_bulkEdit2 is not allowed inside a form; place it as a sibling and target it with forms_submit targetId'
|
|
510
|
-
end
|
|
511
|
-
end
|
|
512
|
-
|
|
513
|
-
# Maximum number of data rows an uploaded file may contain; files with
|
|
514
|
-
# more rows are rejected with an error dialog. No limit when unset.
|
|
515
|
-
int :maxRows
|
|
516
|
-
|
|
517
|
-
# string :statusViewIdPrefix
|
|
518
|
-
array :viewHeaders
|
|
519
|
-
|
|
520
|
-
# string :paramNameForFormData
|
|
521
|
-
string :paramNameForRowId
|
|
522
|
-
action :onCellChange
|
|
523
|
-
action :onRowsLoad
|
|
524
|
-
action :onRowsSelect
|
|
525
|
-
action :onRowsSucceed
|
|
526
|
-
action :onSubmitStart
|
|
527
|
-
action :onSubmitEnd
|
|
528
|
-
|
|
529
|
-
# Experimental
|
|
530
|
-
# panels_builder :accessory, :header, :footer
|
|
531
|
-
|
|
532
|
-
def viewCells(values)
|
|
533
|
-
json.set! 'viewCells' do
|
|
534
|
-
values.each do |value|
|
|
535
|
-
value.call(page.view_builder)
|
|
536
|
-
end
|
|
537
|
-
end
|
|
538
|
-
end
|
|
539
|
-
|
|
540
|
-
def dataRows(value)
|
|
541
|
-
json.dataRows value
|
|
542
|
-
end
|
|
543
|
-
|
|
544
|
-
def viewRows(value)
|
|
545
|
-
json.viewRows value
|
|
546
|
-
end
|
|
547
|
-
|
|
548
|
-
end
|
|
549
|
-
|
|
550
|
-
# The Surfguard members-import table: a fork of bulkEdit2's builder for
|
|
551
|
-
# glib-web-npm's bulkEditSg component (same skeleton — form-peer
|
|
552
|
-
# submission, per-row POSTs, viewCells/viewHeaders — plus the Surfguard
|
|
553
|
-
# parse inputs). Deliberately SG-specific; consolidate with BulkEdit2 when
|
|
554
|
-
# a second consumer proves which props generalize.
|
|
555
|
-
class BulkEditSg < View
|
|
556
|
-
hash :import, required: [:submitUrl, :paramName]
|
|
557
|
-
|
|
558
|
-
# Override
|
|
559
|
-
def created
|
|
560
|
-
# Same form-peer rule as bulkEdit2 (see its comment).
|
|
561
|
-
if page.current_form
|
|
562
|
-
raise 'panels_bulkEditSg is not allowed inside a form; place it as a sibling and target it with forms_submit targetId'
|
|
563
|
-
end
|
|
564
|
-
end
|
|
565
|
-
|
|
566
|
-
# Maximum number of data rows an uploaded file may contain; files with
|
|
567
|
-
# more rows are rejected with an error dialog. No limit when unset.
|
|
568
|
-
int :maxRows
|
|
569
|
-
|
|
570
|
-
array :viewHeaders
|
|
571
|
-
|
|
572
|
-
# string :paramNameForFormData
|
|
573
|
-
string :paramNameForRowId
|
|
574
|
-
action :onCellChange
|
|
575
|
-
action :onRowsLoad
|
|
576
|
-
action :onRowsSelect
|
|
577
|
-
action :onRowsSucceed
|
|
578
|
-
# Fires when non-importable rows drain out of the table on save (they
|
|
579
|
-
# never POST), so the page can keep its Loaded/Uploaded/Skipped
|
|
580
|
-
# arithmetic honest.
|
|
581
|
-
action :onRowsSkip
|
|
582
|
-
action :onSubmitStart
|
|
583
|
-
action :onSubmitEnd
|
|
584
|
-
|
|
585
|
-
# Surfguard file shape: CSV header => form field. The parser follows
|
|
586
|
-
# this map, so a header-spelling fix is a one-line server change.
|
|
587
|
-
hash :headerMap
|
|
588
|
-
array :requiredHeaders
|
|
589
|
-
string :statusFilter
|
|
590
|
-
|
|
591
|
-
def viewCells(values)
|
|
592
|
-
json.set! 'viewCells' do
|
|
593
|
-
values.each do |value|
|
|
594
|
-
value.call(page.view_builder)
|
|
595
|
-
end
|
|
596
|
-
end
|
|
597
|
-
end
|
|
598
|
-
|
|
599
|
-
def dataRows(value)
|
|
600
|
-
json.dataRows value
|
|
601
|
-
end
|
|
602
|
-
|
|
603
|
-
def viewRows(value)
|
|
604
|
-
json.viewRows value
|
|
605
|
-
end
|
|
606
|
-
|
|
607
|
-
end
|
|
608
|
-
|
|
609
|
-
# The generic bulk-edit table: bulkEdit2's skeleton plus everything
|
|
610
|
-
# bulkEditSg hard-coded for the Surfguard imports, expressed as config so
|
|
611
|
-
# any consumer can compose the same behavior (bulkEditSg's replacement --
|
|
612
|
-
# see the component's header comment for the full mapping).
|
|
613
|
-
class BulkEdit3 < View
|
|
614
|
-
hash :import, required: [:submitUrl, :paramName]
|
|
615
|
-
|
|
616
|
-
# Override
|
|
617
|
-
def created
|
|
618
|
-
# Same form-peer rule as bulkEdit2 (see its comment).
|
|
619
513
|
if page.current_form
|
|
620
514
|
raise 'panels_bulkEdit3 is not allowed inside a form; place it as a sibling and target it with forms_submit targetId'
|
|
621
515
|
end
|
|
@@ -641,7 +535,7 @@ class Glib::JsonUi::ViewBuilder
|
|
|
641
535
|
action :onSubmitEnd
|
|
642
536
|
|
|
643
537
|
# The file shape. Without it the table takes plain CSVs (header row
|
|
644
|
-
# first, header names = field ids
|
|
538
|
+
# first, header names = field ids):
|
|
645
539
|
# detectHeader: { allOf: [...], anyOf: [...], scanRows: N } -- find
|
|
646
540
|
# the header row BY CONTENT (report preambles survive);
|
|
647
541
|
# headerMap: { csvHeader => field } -- header-keyed, order-independent
|
|
@@ -716,36 +610,6 @@ class Glib::JsonUi::ViewBuilder
|
|
|
716
610
|
|
|
717
611
|
end
|
|
718
612
|
|
|
719
|
-
# deprecated
|
|
720
|
-
class BulkEdit < View
|
|
721
|
-
hash :nextPage
|
|
722
|
-
hash :export, required: [:label, :fileName]
|
|
723
|
-
hash :import, required: [:submitUrl, :paramName]
|
|
724
|
-
action :onScrollToTop
|
|
725
|
-
action :onScrollToBottom
|
|
726
|
-
string :statusViewIdPrefix
|
|
727
|
-
action :onLoadRows
|
|
728
|
-
|
|
729
|
-
# Experimental
|
|
730
|
-
panels_builder :accessory, :header, :footer
|
|
731
|
-
|
|
732
|
-
def firstSection(block)
|
|
733
|
-
json.sections [1] do
|
|
734
|
-
block.call page.table_section_builder
|
|
735
|
-
end
|
|
736
|
-
end
|
|
737
|
-
|
|
738
|
-
def sections(blocks)
|
|
739
|
-
json.sections do
|
|
740
|
-
blocks.each do |block|
|
|
741
|
-
json.child! do
|
|
742
|
-
block.call page.table_section_builder
|
|
743
|
-
end
|
|
744
|
-
end
|
|
745
|
-
end
|
|
746
|
-
end
|
|
747
|
-
end
|
|
748
|
-
|
|
749
613
|
# Scrollable container panel for content that exceeds viewport height.
|
|
750
614
|
#
|
|
751
615
|
# Scroll panels automatically add scrollbars when content overflows.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module Glib
|
|
2
|
+
# Daily cleanup for ActiveStorage blobs that have no attachments. Excludes blobs referenced by
|
|
3
|
+
# any `SnapshotBlobReference` row, so old file versions that still appear in some snapshot's
|
|
4
|
+
# timeline (i.e. a previous version of an attached file) survive the purge.
|
|
5
|
+
#
|
|
6
|
+
# Opt-in: if the host app has not run the gem's `create_snapshot_blob_references` migration,
|
|
7
|
+
# `SnapshotBlobReference.table_exists?` is false and the exclusion subquery short-circuits —
|
|
8
|
+
# the job then behaves identically to Rails' built-in unattached-blob cleanup, just delayed
|
|
9
|
+
# by `Glib::PurgeUnattached::Config.min_age` (8 hours by default) to give upload flows time
|
|
10
|
+
# to attach.
|
|
11
|
+
#
|
|
12
|
+
# Schedule from the host app (e.g. via sidekiq-cron):
|
|
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
|
|
17
|
+
class PurgeUnattachedJob < ApplicationJob
|
|
18
|
+
queue_as :cleanup
|
|
19
|
+
|
|
20
|
+
def perform(*_args)
|
|
21
|
+
scope = ActiveStorage::Blob
|
|
22
|
+
.unattached
|
|
23
|
+
.where('active_storage_blobs.created_at < ?', Glib::PurgeUnattached::Config.min_age.ago)
|
|
24
|
+
|
|
25
|
+
if SnapshotBlobReference.table_exists?
|
|
26
|
+
scope = scope.where.not(id: SnapshotBlobReference.select(:blob_id))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
scope.find_each(&:purge_later)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|