glib-web 7.0.1.beta1 → 7.1.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/json/libs.rb +33 -0
- data/app/controllers/concerns/glib/params/allowlist.rb +11 -3
- data/app/controllers/glib/errors_controller.rb +8 -2
- data/app/helpers/glib/json_ui/action_builder.rb +2 -9
- data/app/helpers/glib/json_ui/view_builder/fields.rb +1 -1
- data/app/helpers/glib/json_ui/view_builder/panels.rb +8 -185
- data/app/helpers/glib/json_ui/view_builder.rb +0 -6
- data/app/models/concerns/glib/snapshot_v2.rb +38 -9
- data/app/views/json_ui/garage/tables/bulk_edit.json.jbuilder +6 -6
- data/app/views/json_ui/garage/test_page/_header.json.jbuilder +0 -1
- data/app/views/json_ui/garage/test_page/bottom_banners.json.jbuilder +69 -0
- data/app/views/json_ui/garage/test_page/fields_sign.json.jbuilder +1 -1
- data/app/views/json_ui/garage/test_page/panels_bulkEdit3.json.jbuilder +2 -2
- data/app/views/json_ui/garage/test_page/web.json.jbuilder +12 -7
- data/app/views/json_ui/garage/views/misc.json.jbuilder +10 -6
- data/lib/glib/json_crawler/action_crawlers/forms_submit.rb +1 -1
- data/lib/glib/json_ui/config.rb +8 -9
- data/lib/glib/test_helpers.rb +0 -1
- metadata +9 -5
- data/app/helpers/glib/json_ui/action_builder/global_states.rb +0 -14
- data/app/views/json_ui/garage/test_page/panels_bulkEdit2.json.jbuilder +0 -244
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e178f9febdf54e054c6415f98e2dba1168d13ca1eea53c3a2e4c7aba62a434e9
|
|
4
|
+
data.tar.gz: f882a75e2b6c4c56b09b02419eb67b08e3d771958f5d3445f51ebbd93ffba806
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5ad0e0bbe190e4e3c908561a379513b88d134cf2fafc454d8a843b0467a90d51577f9d1d40ab553774588ce23c454bccac19ba6608239fe5503ae2a5cf4ff4cd
|
|
7
|
+
data.tar.gz: 02f96c88f95bde1860894cb3e84d849b1a62bf35285178997ead4992ee4f9a8572ea9b48e7a5c73e0bf573cc3be56ee2dc056f2bce008792d180a286b8940813
|
|
@@ -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
|
|
|
@@ -11,7 +11,8 @@ module Glib::Params
|
|
|
11
11
|
|
|
12
12
|
# Resolve a stringly-typed request param (or any boundary string) to one
|
|
13
13
|
# symbol drawn from a closed allow-list (+allowed_symbols+), returning
|
|
14
|
-
# +default+ when the value is missing, blank, or not recognised
|
|
14
|
+
# +default+ when the value is missing, blank, or not recognised — with
|
|
15
|
+
# one default-config exception on mutating requests, spelled out below.
|
|
15
16
|
#
|
|
16
17
|
# This is the symbol-DoS-safe replacement for `params[:x].to_sym`. A bare
|
|
17
18
|
# `.to_sym` on a raw value mints a symbol for every distinct string a client
|
|
@@ -30,16 +31,23 @@ module Glib::Params
|
|
|
30
31
|
# Blank/missing values fall through to +default+ quietly on EVERY method
|
|
31
32
|
# (forms legitimately post no value; no symbol's string form is blank).
|
|
32
33
|
#
|
|
34
|
+
# +raise_on_unrecognized+ (default +true+) keeps that raise. Pass +false+
|
|
35
|
+
# for endpoints anyone can POST to — anonymous public endpoints where junk
|
|
36
|
+
# values are expected traffic: the present-but-unrecognised case then
|
|
37
|
+
# degrades to +default+ on every method instead of raising, so bot probes
|
|
38
|
+
# cannot 500 the endpoint. Endpoints whose only writers are the app's own
|
|
39
|
+
# UI keep the default — there the raise is the forgotten-entry detector.
|
|
40
|
+
#
|
|
33
41
|
# +allowed_symbols+ may also contain strings (e.g. a Rails enum's `.keys`)
|
|
34
42
|
# — the matched entry is symbolized on the way out, so the return value is
|
|
35
43
|
# ALWAYS a symbol (or +default+). That `to_sym` runs only on the
|
|
36
44
|
# developer-controlled list entry, never on +value+, so the DoS-safety
|
|
37
45
|
# above is unaffected.
|
|
38
|
-
def glib_allowlist_symbol_param(value, allowed_symbols, default:)
|
|
46
|
+
def glib_allowlist_symbol_param(value, allowed_symbols, default:, raise_on_unrecognized: true)
|
|
39
47
|
match = allowed_symbols.find { |symbol| symbol.to_s == value.to_s }&.to_sym
|
|
40
48
|
return match if match
|
|
41
49
|
|
|
42
|
-
if value.present? && !request.get? && !request.head?
|
|
50
|
+
if raise_on_unrecognized && value.present? && !request.get? && !request.head?
|
|
43
51
|
raise UnrecognizedValue,
|
|
44
52
|
"Unrecognised #{request.request_method} param value #{value.to_s.truncate(80).inspect} " \
|
|
45
53
|
"(allowed: #{allowed_symbols.map(&:to_sym).inspect})"
|
|
@@ -29,12 +29,18 @@ module Glib
|
|
|
29
29
|
if defined?(Rollbar)
|
|
30
30
|
# `GLib.log.warn` reports arrive with level=warning; everything else
|
|
31
31
|
# (uncaught errors, `GLib.log.error`) is an error. Prefixed so
|
|
32
|
-
#
|
|
32
|
+
# client-side reports are distinguishable from server exceptions;
|
|
33
33
|
# the details ride along as custom data for grouping.
|
|
34
34
|
rollbar_level = params[:level] == 'warning' ? :warning : :error
|
|
35
|
+
# The prefix names the reporting runtime: the npm client sends no
|
|
36
|
+
# platform and stays "JS"; the Android renderer sends "ANDROID"
|
|
37
|
+
# (spec: thread 174). A platform TAG, not free text -- truncated so
|
|
38
|
+
# a crafted value cannot smuggle a payload into the message, and
|
|
39
|
+
# absent/blank degrades to the historical JS label.
|
|
40
|
+
platform = params[:platform].to_s[0, 16].presence || 'JS'
|
|
35
41
|
Rollbar.public_send(
|
|
36
42
|
rollbar_level,
|
|
37
|
-
"
|
|
43
|
+
"#{platform}: #{params[:message]}",
|
|
38
44
|
stack: params[:stack],
|
|
39
45
|
url: params[:url],
|
|
40
46
|
context: params[:context],
|
|
@@ -39,10 +39,10 @@ module Glib
|
|
|
39
39
|
add_element_to_array 'action', m, *args
|
|
40
40
|
else
|
|
41
41
|
if !Rails.env.production? && @singleton_action_set
|
|
42
|
-
raise
|
|
42
|
+
raise 'Multiple actions called on a singleton ActionBuilder. ' \
|
|
43
43
|
"Only one action is allowed. Previous action: '#{@singleton_action_name}', " \
|
|
44
44
|
"current action: '#{m}'. Use `run_multiple` with `childActions` or " \
|
|
45
|
-
|
|
45
|
+
'chain actions using `onClose` callbacks.'
|
|
46
46
|
end
|
|
47
47
|
@singleton_action_set = true
|
|
48
48
|
@singleton_action_name = m
|
|
@@ -170,13 +170,6 @@ module Glib
|
|
|
170
170
|
end
|
|
171
171
|
end
|
|
172
172
|
|
|
173
|
-
module Labels
|
|
174
|
-
class Set < Action
|
|
175
|
-
string :text
|
|
176
|
-
action :onSet
|
|
177
|
-
end
|
|
178
|
-
end
|
|
179
|
-
|
|
180
173
|
module Lists
|
|
181
174
|
class Append < Action
|
|
182
175
|
hash :row
|
|
@@ -962,7 +962,7 @@ class Glib::JsonUi::ViewBuilder
|
|
|
962
962
|
# read-only preview instead of ink). An untouched field therefore
|
|
963
963
|
# re-submits that same signed id, so consumers should treat a
|
|
964
964
|
# same-signed-id re-attach as "no change". Resize it via
|
|
965
|
-
# `
|
|
965
|
+
# `logics_set` with `width`/`height`.
|
|
966
966
|
#
|
|
967
967
|
# @see app/views/json_ui/garage/test_page/fields_sign.json.jbuilder Garage examples
|
|
968
968
|
class Sign < AbstractField
|
|
@@ -452,170 +452,23 @@ class Glib::JsonUi::ViewBuilder
|
|
|
452
452
|
end
|
|
453
453
|
end
|
|
454
454
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
def initialize(json, page)
|
|
463
|
-
super
|
|
464
|
-
@template = Glib::JsonUi::TableBuilders::Template.new(json, self)
|
|
465
|
-
end
|
|
466
|
-
|
|
467
|
-
def header(options = {})
|
|
468
|
-
json.header do
|
|
469
|
-
json.backgroundColor options.delete(:backgroundColor)
|
|
470
|
-
json.cellViews do
|
|
471
|
-
options.delete(:cellViews)&.call page.view_builder
|
|
472
|
-
end
|
|
473
|
-
|
|
474
|
-
json.colSpans options.delete(:colSpans)
|
|
475
|
-
end
|
|
476
|
-
|
|
477
|
-
raise "Invalid properties: #{options.keys}" if options.size > 0
|
|
478
|
-
end
|
|
479
|
-
|
|
480
|
-
def rows(options = {})
|
|
481
|
-
block = options.delete(:builder)
|
|
482
|
-
json.rows do
|
|
483
|
-
if (objects = options.delete(:objects))
|
|
484
|
-
objects.each_with_index do |object, index|
|
|
485
|
-
block&.call template, object, index
|
|
486
|
-
end
|
|
487
|
-
else
|
|
488
|
-
block&.call template
|
|
489
|
-
end
|
|
490
|
-
end
|
|
491
|
-
|
|
492
|
-
raise "Invalid properties: #{options.keys}" if options.size > 0
|
|
493
|
-
end
|
|
494
|
-
end
|
|
495
|
-
|
|
496
|
-
class BulkEdit2 < View
|
|
455
|
+
# The generic bulk-edit table: form-peer submission, per-row POSTs,
|
|
456
|
+
# viewCells/viewHeaders, plus the full parse/gate/dedup config so any
|
|
457
|
+
# consumer can compose specialized import behavior (it replaced the
|
|
458
|
+
# retired bulkEdit2 and bulkEditSg -- see the component's header comment
|
|
459
|
+
# for the full mapping).
|
|
460
|
+
class BulkEdit3 < View
|
|
497
461
|
hash :import, required: [:submitUrl, :paramName]
|
|
498
462
|
|
|
499
463
|
# Override
|
|
500
464
|
def created
|
|
501
|
-
#
|
|
465
|
+
# bulkEdit3 is a form PEER, not a form child: it answers the same
|
|
502
466
|
# forms_submit action a form does (forms/directSubmit) and submits its
|
|
503
467
|
# rows itself to `import.submitUrl`. Inside a form, its repeated
|
|
504
468
|
# per-row input names would collide with the form's FormData scans
|
|
505
469
|
# (duplicate-name warnings, dirty-baseline pollution) while adding
|
|
506
470
|
# nothing to the form's submission -- so, like a nested form, reject
|
|
507
471
|
# 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
472
|
if page.current_form
|
|
620
473
|
raise 'panels_bulkEdit3 is not allowed inside a form; place it as a sibling and target it with forms_submit targetId'
|
|
621
474
|
end
|
|
@@ -641,7 +494,7 @@ class Glib::JsonUi::ViewBuilder
|
|
|
641
494
|
action :onSubmitEnd
|
|
642
495
|
|
|
643
496
|
# The file shape. Without it the table takes plain CSVs (header row
|
|
644
|
-
# first, header names = field ids
|
|
497
|
+
# first, header names = field ids):
|
|
645
498
|
# detectHeader: { allOf: [...], anyOf: [...], scanRows: N } -- find
|
|
646
499
|
# the header row BY CONTENT (report preambles survive);
|
|
647
500
|
# headerMap: { csvHeader => field } -- header-keyed, order-independent
|
|
@@ -716,36 +569,6 @@ class Glib::JsonUi::ViewBuilder
|
|
|
716
569
|
|
|
717
570
|
end
|
|
718
571
|
|
|
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
572
|
# Scrollable container panel for content that exceeds viewport height.
|
|
750
573
|
#
|
|
751
574
|
# Scroll panels automatically add scrollbars when content overflows.
|
|
@@ -12,7 +12,9 @@ require 'hashdiff'
|
|
|
12
12
|
# 4. ActiveStorage in diff_associations (convert Attached::One/Many to attachment records).
|
|
13
13
|
# 5. same_as_before? derives from the computed diff instead of the instance-local
|
|
14
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)
|
|
15
|
+
# snapshot when taken after the owner's -- see "Snapshot ordering" below, issue #481),
|
|
16
|
+
# with the flag re-entering as a NEGATIVE signal for first snapshots only (see
|
|
17
|
+
# `same_as_before?` for why an empty association baseline cannot decide those).
|
|
16
18
|
#
|
|
17
19
|
# ## Snapshot ordering: owner vs association targets
|
|
18
20
|
#
|
|
@@ -27,7 +29,8 @@ require 'hashdiff'
|
|
|
27
29
|
# reading the target back through the association snapshots the persisted state instead.
|
|
28
30
|
#
|
|
29
31
|
# `snapshot_changed` / `attributes_before_save` remain writable instance accessors for
|
|
30
|
-
# call-site use
|
|
32
|
+
# call-site use; `snapshot_changed` is consulted by the skip decision only as a NEGATIVE
|
|
33
|
+
# signal on first snapshots (see `same_as_before?`).
|
|
31
34
|
#
|
|
32
35
|
# To get all records of a model, you can use the following query:
|
|
33
36
|
# MyModel.joins(:snapshots).distinct
|
|
@@ -60,7 +63,16 @@ module Glib
|
|
|
60
63
|
# Only set snapshot_changed if not already set (handles case where before_save
|
|
61
64
|
# is called multiple times - e.g., when associated records are saved before parent)
|
|
62
65
|
if snapshot_changed.nil?
|
|
63
|
-
|
|
66
|
+
# For a save with no pending change `attributes_in_database` is empty -- an
|
|
67
|
+
# unchanged persisted record has no database-side delta -- which would make a
|
|
68
|
+
# first snapshot diff every attribute as an addition. Capture the full
|
|
69
|
+
# persisted state instead, so an unchanged save yields an empty item diff and
|
|
70
|
+
# the nil-guard keeps that baseline stable for later changes on the same
|
|
71
|
+
# instance. New records keep `attributes_in_database` (their all-nil
|
|
72
|
+
# originals): that non-empty baseline is exactly why a create's version 1
|
|
73
|
+
# records the full initial state. `unless` keeps the accessor nil, never
|
|
74
|
+
# `false`, for call-site nil-safety.
|
|
75
|
+
self.attributes_before_save = attributes_in_database.presence || (attributes unless new_record?)
|
|
64
76
|
self.snapshot_changed = check_snapshot_changed
|
|
65
77
|
end
|
|
66
78
|
end
|
|
@@ -125,8 +137,10 @@ module Glib
|
|
|
125
137
|
}
|
|
126
138
|
|
|
127
139
|
# Don't create version if same as before. Reuses `calculated_diff` instead of
|
|
128
|
-
# recomputing it inside `same_as_before?`.
|
|
129
|
-
|
|
140
|
+
# recomputing it inside `same_as_before?`. `version == 1` (no previous
|
|
141
|
+
# version exists) tells `same_as_before?` this is a FIRST snapshot, where the
|
|
142
|
+
# association baseline is empty and the flag carries decision weight.
|
|
143
|
+
unless same_as_before?(calculated_diff, first_snapshot: version == 1)
|
|
130
144
|
# No nil guard: active_snapshot's `create_snapshot!` raises on failure and
|
|
131
145
|
# always returns the snapshot.
|
|
132
146
|
snapshot = create_snapshot!(**snapshot_obj)
|
|
@@ -186,13 +200,28 @@ module Glib
|
|
|
186
200
|
end
|
|
187
201
|
|
|
188
202
|
# Decides whether `glib_create_snapshot!` should skip the version write. Derived from
|
|
189
|
-
# the computed diff (persisted state),
|
|
203
|
+
# the computed diff (persisted state), not from the instance-local `snapshot_changed`
|
|
190
204
|
# flag: the flag dies with its instance, and an owner's snapshot replaces association
|
|
191
205
|
# targets with fresh instances (`with_lock`'s reload clears the association cache, then
|
|
192
206
|
# 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).
|
|
194
|
-
#
|
|
195
|
-
|
|
207
|
+
# follow-up snapshot taken through the association (issue #481).
|
|
208
|
+
#
|
|
209
|
+
# The one place the flag still decides: a FIRST snapshot whose association diff is
|
|
210
|
+
# meaningless. With no previous version, `fetch_snapshot_items(nil)` returns `{}`
|
|
211
|
+
# for associations, so every pre-existing child row reads as `+` and the diff alone
|
|
212
|
+
# can never prove a no-op — any never-snapshotted record with children would mint a
|
|
213
|
+
# version on every snapshot call (observed as spurious "Edited" audit entries on
|
|
214
|
+
# routine re-saves). There the flag is consulted as a NEGATIVE signal:
|
|
215
|
+
# - `false` — this instance crossed the before_save boundary and
|
|
216
|
+
# `check_snapshot_changed` found no item or child change, and the item diff is
|
|
217
|
+
# net-zero against `attributes_before_save` => skip;
|
|
218
|
+
# - `nil` — no save boundary on this instance (an owner-triggered snapshot of a
|
|
219
|
+
# replaced association target, issue #481) => mint;
|
|
220
|
+
# - `true` — a change was detected at the save boundary; falls through to the
|
|
221
|
+
# diff-based decision (a reverted change still no-ops).
|
|
222
|
+
def same_as_before?(computed_diff = diff, first_snapshot: false)
|
|
223
|
+
return true if first_snapshot && snapshot_changed == false && computed_diff['item'].blank?
|
|
224
|
+
|
|
196
225
|
item_unchanged = computed_diff['item'].blank?
|
|
197
226
|
assoc_diff = computed_diff['associations']
|
|
198
227
|
associations_unchanged = assoc_diff.nil? || assoc_diff.values.all?(&:blank?)
|
|
@@ -37,17 +37,17 @@ page.scroll childViews: ->(scroll) do
|
|
|
37
37
|
displayed: false
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
wres.
|
|
40
|
+
wres.panels_bulkEdit3 \
|
|
41
41
|
id: 'import_table',
|
|
42
42
|
width: 'matchParent',
|
|
43
43
|
paramNameForRowId: 'row_id',
|
|
44
44
|
import: { submitUrl: json_ui_garage_url(path: 'forms/bulk_edit_post2'), paramName: 'user' },
|
|
45
45
|
viewHeaders: [
|
|
46
|
-
{ text: 'Month', id: 'month',
|
|
47
|
-
{ text: 'Electricity usage', id: 'electricity_usage'
|
|
48
|
-
{ text: 'Gas usage', id: 'gas_usage'
|
|
49
|
-
{ text: 'Sources', id: 'sources'
|
|
50
|
-
{ text: 'Compliant', id: 'compliant'
|
|
46
|
+
{ text: 'Month', id: 'month', tooltip: { text: 'Month', placement: 'top' } },
|
|
47
|
+
{ text: 'Electricity usage', id: 'electricity_usage' },
|
|
48
|
+
{ text: 'Gas usage', id: 'gas_usage' },
|
|
49
|
+
{ text: 'Sources', id: 'sources' },
|
|
50
|
+
{ text: 'Compliant', id: 'compliant' }
|
|
51
51
|
],
|
|
52
52
|
viewCells: [
|
|
53
53
|
->(view) do
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
json.title 'Test Page (Bottom Banners)'
|
|
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
|
+
body.panels_responsive(
|
|
10
|
+
padding: glib_json_padding_body,
|
|
11
|
+
childViews: ->(res) do
|
|
12
|
+
res.h2 text: 'Bottom Banners'
|
|
13
|
+
res.spacer height: 8
|
|
14
|
+
res.button text: "bottomBanners/open targetId: 'dock1'", onClick: ->(action) do
|
|
15
|
+
action.bottomBanners_open targetId: 'dock1', newView: ->(view) do
|
|
16
|
+
view.panels_responsive(
|
|
17
|
+
styleClass: ['frameless'],
|
|
18
|
+
childViews: ->(panel) do
|
|
19
|
+
panel.label text: 'Upload in progress'
|
|
20
|
+
panel.spacer height: 4
|
|
21
|
+
panel.button text: 'Details', onClick: ->(subaction) do
|
|
22
|
+
subaction.dialogs_alert message: 'Upload details'
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
res.spacer height: 4
|
|
29
|
+
res.button text: "bottomBanners/open targetId: 'dock2'", onClick: ->(action) do
|
|
30
|
+
action.bottomBanners_open targetId: 'dock2', newView: ->(view) do
|
|
31
|
+
view.panels_responsive(
|
|
32
|
+
styleClass: ['frameless'],
|
|
33
|
+
childViews: ->(panel) do
|
|
34
|
+
panel.banners_alert title: 'Background task', message: 'A long-running task is still active.'
|
|
35
|
+
end
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
res.spacer height: 4
|
|
40
|
+
res.button text: "bottomBanners/open replace 'dock1'", onClick: ->(action) do
|
|
41
|
+
action.bottomBanners_open targetId: 'dock1', newView: ->(view) do
|
|
42
|
+
view.panels_responsive(
|
|
43
|
+
styleClass: ['frameless'],
|
|
44
|
+
childViews: ->(panel) do
|
|
45
|
+
panel.label text: 'Upload complete'
|
|
46
|
+
panel.spacer height: 4
|
|
47
|
+
panel.button text: 'Dismiss', onClick: ->(subaction) do
|
|
48
|
+
subaction.bottomBanners_close targetId: 'dock1'
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
res.spacer height: 4
|
|
55
|
+
res.button text: "bottomBanners/close targetId: 'dock1'", onClick: ->(action) do
|
|
56
|
+
action.bottomBanners_close targetId: 'dock1'
|
|
57
|
+
end
|
|
58
|
+
res.spacer height: 4
|
|
59
|
+
res.button text: "bottomBanners/close targetId: 'dock2'", onClick: ->(action) do
|
|
60
|
+
action.bottomBanners_close targetId: 'dock2'
|
|
61
|
+
end
|
|
62
|
+
res.spacer height: 4
|
|
63
|
+
res.button text: 'bottomBanners/close (no targetId)', onClick: ->(action) do
|
|
64
|
+
action.bottomBanners_close
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
)
|
|
@@ -121,7 +121,7 @@ page.body(
|
|
|
121
121
|
text: 'Clear value',
|
|
122
122
|
onClick: ->(action) do
|
|
123
123
|
action.runMultiple childActions: ->(multiple) do
|
|
124
|
-
multiple.logics_set targetId: 'signature_main', data: { value:
|
|
124
|
+
multiple.logics_set targetId: 'signature_main', data: { value: '' }
|
|
125
125
|
multiple.logics_set targetId: 'signature_status', data: { text: 'Status: cleared' }
|
|
126
126
|
end
|
|
127
127
|
end
|
|
@@ -83,13 +83,13 @@ page.body(
|
|
|
83
83
|
padding: glib_json_padding_body,
|
|
84
84
|
childViews: ->(res) do
|
|
85
85
|
res.h2 text: 'Overview'
|
|
86
|
-
res.p text: 'panels_bulkEdit3 is the generic bulk-edit table:
|
|
86
|
+
res.p text: 'panels_bulkEdit3 is the generic bulk-edit table: cells, pagination and one-POST-per-row submission, plus config-driven file parsing, row gates, read-only columns, view chips and submission extras.'
|
|
87
87
|
res.spacer height: 12
|
|
88
88
|
res.hr width: 'matchParent'
|
|
89
89
|
|
|
90
90
|
res.h2 text: 'Basic example (plain CSV)'
|
|
91
91
|
res.spacer height: 8
|
|
92
|
-
res.p text: 'Without parse config the table takes plain CSVs: header row first, header names as field ids
|
|
92
|
+
res.p text: 'Without parse config the table takes plain CSVs: header row first, header names as field ids. maxRows 3 rejects oversized files whole.'
|
|
93
93
|
res.spacer height: 8
|
|
94
94
|
res.panels_horizontal(
|
|
95
95
|
align: 'middle',
|
|
@@ -10,15 +10,20 @@ page.body childViews: ->(body) do
|
|
|
10
10
|
res.h2 text: 'Overview'
|
|
11
11
|
res.p text: 'Web panels embed external web content within a panel container.'
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
# Hermetic content on the serving host itself (spec 169, Decision 5):
|
|
14
|
+
# the recording host's public/ carries the two deterministic pages, so
|
|
15
|
+
# the emitted URLs bake an origin the cassette replay can rewrite and
|
|
16
|
+
# serve — an external URL would leave the emulator for the real network
|
|
17
|
+
# and no golden could depend on it.
|
|
18
|
+
url_content = "#{request.base_url}/g-web-panel.html"
|
|
19
|
+
url_alt = "#{request.base_url}/g-web-panel-alt.html"
|
|
15
20
|
|
|
16
21
|
res.spacer height: 12
|
|
17
22
|
res.hr width: 'matchParent'
|
|
18
23
|
|
|
19
24
|
res.h2 text: 'Basic'
|
|
20
25
|
res.spacer height: 8
|
|
21
|
-
res.panels_web id: 'web_main', width: 'matchParent', height: 300, url:
|
|
26
|
+
res.panels_web id: 'web_main', width: 'matchParent', height: 300, url: url_content
|
|
22
27
|
|
|
23
28
|
res.spacer height: 12
|
|
24
29
|
res.hr width: 'matchParent'
|
|
@@ -26,11 +31,11 @@ page.body childViews: ->(body) do
|
|
|
26
31
|
res.h2 text: 'Variants and Props'
|
|
27
32
|
res.spacer height: 8
|
|
28
33
|
res.panels_flow innerPadding: { bottom: 0 }, childViews: ->(flow) do
|
|
29
|
-
flow.button text: '
|
|
30
|
-
action.logics_set targetId: 'web_main', data: { url:
|
|
34
|
+
flow.button text: 'Content A', onClick: ->(action) do
|
|
35
|
+
action.logics_set targetId: 'web_main', data: { url: url_content }
|
|
31
36
|
end
|
|
32
37
|
flow.spacer width: 4
|
|
33
|
-
flow.button text: '
|
|
38
|
+
flow.button text: 'Content B', onClick: ->(action) do
|
|
34
39
|
action.logics_set targetId: 'web_main', data: { url: url_alt }
|
|
35
40
|
end
|
|
36
41
|
flow.spacer width: 4
|
|
@@ -60,6 +65,6 @@ page.body childViews: ->(body) do
|
|
|
60
65
|
res.h2 text: 'Edge and Advanced'
|
|
61
66
|
res.spacer height: 8
|
|
62
67
|
res.label text: 'Small viewport'
|
|
63
|
-
res.panels_web width: 'matchParent', height: 120, url:
|
|
68
|
+
res.panels_web width: 'matchParent', height: 120, url: url_content
|
|
64
69
|
end
|
|
65
70
|
end
|
|
@@ -14,12 +14,6 @@ json_ui_page json do |page|
|
|
|
14
14
|
# ul.label text: 'Label'
|
|
15
15
|
# end
|
|
16
16
|
|
|
17
|
-
scroll.spacer height: 20
|
|
18
|
-
scroll.h2 text: 'Time'
|
|
19
|
-
scroll.spacer height: 6
|
|
20
|
-
scroll.datetime value: 1.week.ago, format: 'YYYY-MM-DD'
|
|
21
|
-
scroll.datetime value: DateTime.current, format: 'hh:mm:ss', tick: true
|
|
22
|
-
|
|
23
17
|
# scroll.spacer height: 20
|
|
24
18
|
# scroll.h2 text: 'Map'
|
|
25
19
|
# scroll.spacer height: 6
|
|
@@ -30,5 +24,15 @@ json_ui_page json do |page|
|
|
|
30
24
|
scroll.spacer height: 6
|
|
31
25
|
scroll.calendar width: 'matchParent', height: 500, dataUrl: json_ui_garage_url(path: 'views/calendar_data')
|
|
32
26
|
|
|
27
|
+
# h5/h6 exercised here rather than views/texts: the texts page is pinned
|
|
28
|
+
# by the client suite's golden, and a non-corpus page covers the headings
|
|
29
|
+
# without shifting it (component inventory, thread 195).
|
|
30
|
+
scroll.spacer height: 20
|
|
31
|
+
scroll.h2 text: 'Sub-headings'
|
|
32
|
+
scroll.spacer height: 6
|
|
33
|
+
scroll.h5 text: 'Heading 5'
|
|
34
|
+
scroll.spacer height: 6
|
|
35
|
+
scroll.h6 text: 'Heading 6'
|
|
36
|
+
|
|
33
37
|
end
|
|
34
38
|
end
|
|
@@ -16,7 +16,7 @@ module Glib
|
|
|
16
16
|
raise "Submit action needs to be inside a form: #{http.router.last_log}" unless form
|
|
17
17
|
|
|
18
18
|
case Glib::JsonUi::Name.normalize(form['view'])
|
|
19
|
-
when 'panels/
|
|
19
|
+
when 'panels/bulkEdit3'
|
|
20
20
|
method = 'post'
|
|
21
21
|
action = "forms/#{method}"
|
|
22
22
|
url = form['import']['submitUrl']
|
data/lib/glib/json_ui/config.rb
CHANGED
|
@@ -3,22 +3,21 @@ module Glib
|
|
|
3
3
|
# Global JSON UI settings.
|
|
4
4
|
#
|
|
5
5
|
# `name_separator` controls the character that joins the namespace and the
|
|
6
|
-
# component/action name in the generated JSON, e.g. `
|
|
7
|
-
# (default) vs `
|
|
6
|
+
# component/action name in the generated JSON, e.g. `panels_responsive`
|
|
7
|
+
# (default since 7.0) vs `panels/responsive`.
|
|
8
8
|
#
|
|
9
|
-
#
|
|
10
|
-
# in the next major version. Apps opt in early via an initializer:
|
|
9
|
+
# Apps that need the legacy slash names opt back in via an initializer:
|
|
11
10
|
#
|
|
12
11
|
# # config/initializers/glib.rb
|
|
13
|
-
# Glib::JsonUi::Config.name_separator = '
|
|
12
|
+
# Glib::JsonUi::Config.name_separator = '/'
|
|
14
13
|
#
|
|
15
|
-
# Frontends built on glib-web-npm accept either form, so
|
|
16
|
-
# server-side-only
|
|
14
|
+
# Frontends built on glib-web-npm accept either form, so this is a
|
|
15
|
+
# server-side-only setting.
|
|
17
16
|
class Config
|
|
18
17
|
SEPARATORS = ['/', '_'].freeze
|
|
19
18
|
|
|
20
|
-
# The shipped default
|
|
21
|
-
#
|
|
19
|
+
# The shipped default since the 7.0 major version. Apps that need the
|
|
20
|
+
# legacy names opt back in via `Config.name_separator = '/'`.
|
|
22
21
|
DEFAULT_NAME_SEPARATOR = '_'
|
|
23
22
|
|
|
24
23
|
@@name_separator = DEFAULT_NAME_SEPARATOR
|
data/lib/glib/test_helpers.rb
CHANGED
|
@@ -239,7 +239,6 @@ module Glib
|
|
|
239
239
|
name = Glib::JsonUi::Name.normalize(view['view'])
|
|
240
240
|
name&.start_with?('panels/form') ||
|
|
241
241
|
name&.start_with?('panels/fullPageForm') ||
|
|
242
|
-
name == 'panels/bulkEdit2' ||
|
|
243
242
|
name == 'panels/bulkEdit3' ||
|
|
244
243
|
name == 'panels/splitForm'
|
|
245
244
|
end
|
metadata
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: glib-web
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 7.
|
|
4
|
+
version: 7.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ''
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: bin
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2026-10-12 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: activestorage
|
|
@@ -191,6 +192,7 @@ dependencies:
|
|
|
191
192
|
- - ">="
|
|
192
193
|
- !ruby/object:Gem::Version
|
|
193
194
|
version: '0'
|
|
195
|
+
description:
|
|
194
196
|
email: ''
|
|
195
197
|
executables: []
|
|
196
198
|
extensions: []
|
|
@@ -234,7 +236,6 @@ files:
|
|
|
234
236
|
- app/helpers/glib/json_ui/action_builder/dialogs.rb
|
|
235
237
|
- app/helpers/glib/json_ui/action_builder/fields.rb
|
|
236
238
|
- app/helpers/glib/json_ui/action_builder/files.rb
|
|
237
|
-
- app/helpers/glib/json_ui/action_builder/global_states.rb
|
|
238
239
|
- app/helpers/glib/json_ui/action_builder/http.rb
|
|
239
240
|
- app/helpers/glib/json_ui/action_builder/iap.rb
|
|
240
241
|
- app/helpers/glib/json_ui/action_builder/logics.rb
|
|
@@ -438,6 +439,7 @@ files:
|
|
|
438
439
|
- app/views/json_ui/garage/test_page/_header.json.jbuilder
|
|
439
440
|
- app/views/json_ui/garage/test_page/auth.json.jbuilder
|
|
440
441
|
- app/views/json_ui/garage/test_page/auto_validate.json.jbuilder
|
|
442
|
+
- app/views/json_ui/garage/test_page/bottom_banners.json.jbuilder
|
|
441
443
|
- app/views/json_ui/garage/test_page/browsers.json.jbuilder
|
|
442
444
|
- app/views/json_ui/garage/test_page/calendar.json.jbuilder
|
|
443
445
|
- app/views/json_ui/garage/test_page/calendar_empty_data.json.jbuilder
|
|
@@ -497,7 +499,6 @@ files:
|
|
|
497
499
|
- app/views/json_ui/garage/test_page/multimedia_video.json.jbuilder
|
|
498
500
|
- app/views/json_ui/garage/test_page/pagination.json.jbuilder
|
|
499
501
|
- app/views/json_ui/garage/test_page/panels.json.jbuilder
|
|
500
|
-
- app/views/json_ui/garage/test_page/panels_bulkEdit2.json.jbuilder
|
|
501
502
|
- app/views/json_ui/garage/test_page/panels_bulkEdit3.json.jbuilder
|
|
502
503
|
- app/views/json_ui/garage/test_page/popovers.json.jbuilder
|
|
503
504
|
- app/views/json_ui/garage/test_page/progressCircle.json.jbuilder
|
|
@@ -605,8 +606,10 @@ files:
|
|
|
605
606
|
- lib/glib/time_returning_mailer.rb
|
|
606
607
|
- lib/glib/value.rb
|
|
607
608
|
- lib/tasks/db.rake
|
|
609
|
+
homepage:
|
|
608
610
|
licenses: []
|
|
609
611
|
metadata: {}
|
|
612
|
+
post_install_message:
|
|
610
613
|
rdoc_options: []
|
|
611
614
|
require_paths:
|
|
612
615
|
- lib
|
|
@@ -621,7 +624,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
621
624
|
- !ruby/object:Gem::Version
|
|
622
625
|
version: '0'
|
|
623
626
|
requirements: []
|
|
624
|
-
rubygems_version: 4.
|
|
627
|
+
rubygems_version: 3.4.6
|
|
628
|
+
signing_key:
|
|
625
629
|
specification_version: 4
|
|
626
630
|
summary: ''
|
|
627
631
|
test_files: []
|
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
json.title 'Test Page (Panels Bulk Edit 2)'
|
|
2
|
-
|
|
3
|
-
page = json_ui_page json
|
|
4
|
-
|
|
5
|
-
options = [{ text: 'yes', value: 'yes' }, { text: 'no', value: 'no' }]
|
|
6
|
-
source_options = ['Generators', 'Power Station', 'Solar Panels'].map do |source|
|
|
7
|
-
{ text: source, value: source }
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
default_headers = [
|
|
11
|
-
{ text: 'Month', id: 'month', importable: true, tooltip: { text: 'Month', placement: 'top' } },
|
|
12
|
-
{ text: 'Electricity usage', id: 'electricity_usage', importable: true },
|
|
13
|
-
{ text: 'Gas usage', id: 'gas_usage', importable: true },
|
|
14
|
-
{ text: 'Sources', id: 'sources', importable: true },
|
|
15
|
-
{ text: 'Compliant', id: 'compliant', importable: true }
|
|
16
|
-
]
|
|
17
|
-
|
|
18
|
-
default_cells = [
|
|
19
|
-
->(view) do
|
|
20
|
-
view.fields_number width: 'matchParent'
|
|
21
|
-
end,
|
|
22
|
-
->(view) do
|
|
23
|
-
view.fields_number width: 'matchParent'
|
|
24
|
-
end,
|
|
25
|
-
->(view) do
|
|
26
|
-
view.fields_number width: 'matchParent'
|
|
27
|
-
end,
|
|
28
|
-
->(view) do
|
|
29
|
-
view.fields_select width: 'matchParent', options: source_options, multiple: true
|
|
30
|
-
end,
|
|
31
|
-
->(view) do
|
|
32
|
-
view.fields_select width: 'matchParent', options: options
|
|
33
|
-
end
|
|
34
|
-
]
|
|
35
|
-
|
|
36
|
-
[
|
|
37
|
-
{ text: 'Month', id: 'month', importable: true },
|
|
38
|
-
{ text: 'Usage (kWh)', id: 'electricity_usage', importable: true },
|
|
39
|
-
{ text: 'Usage (Therms)', id: 'gas_usage', importable: true }
|
|
40
|
-
]
|
|
41
|
-
|
|
42
|
-
[
|
|
43
|
-
->(view) do
|
|
44
|
-
view.fields_number width: 120
|
|
45
|
-
end,
|
|
46
|
-
->(view) do
|
|
47
|
-
view.fields_number width: 140
|
|
48
|
-
end,
|
|
49
|
-
->(view) do
|
|
50
|
-
view.fields_number width: 140
|
|
51
|
-
end
|
|
52
|
-
]
|
|
53
|
-
|
|
54
|
-
import_settings = { submitUrl: json_ui_garage_url(path: 'forms/bulk_edit_post2'), paramName: 'user' }
|
|
55
|
-
|
|
56
|
-
render 'json_ui/garage/test_page/header', json: json, page: page
|
|
57
|
-
|
|
58
|
-
page.body(
|
|
59
|
-
childViews: ->(body) do
|
|
60
|
-
body.panels_responsive(
|
|
61
|
-
padding: glib_json_padding_body,
|
|
62
|
-
childViews: ->(res) do
|
|
63
|
-
res.h2 text: 'Overview'
|
|
64
|
-
res.p text: 'panels_bulkEdit2 renders an editable grid with import-backed rows and bulk submission events.'
|
|
65
|
-
res.spacer height: 12
|
|
66
|
-
res.hr width: 'matchParent'
|
|
67
|
-
res.h2 text: 'Basic example'
|
|
68
|
-
res.spacer height: 8
|
|
69
|
-
res.panels_horizontal(
|
|
70
|
-
align: 'middle',
|
|
71
|
-
childViews: ->(horizontal) do
|
|
72
|
-
horizontal.label text: 'Loaded: '
|
|
73
|
-
horizontal.label id: 'bulk_edit_loaded_count', text: '0'
|
|
74
|
-
horizontal.spacer width: 12
|
|
75
|
-
horizontal.label text: 'Selected: '
|
|
76
|
-
horizontal.label id: 'bulk_edit_selected_count', text: '0'
|
|
77
|
-
horizontal.spacer width: 12
|
|
78
|
-
horizontal.chip id: 'bulk_edit_succeeded_count', styleClass: 'success', text: 'Uploaded: 0', displayed: false
|
|
79
|
-
end
|
|
80
|
-
)
|
|
81
|
-
res.spacer height: 8
|
|
82
|
-
res.panels_bulkEdit2(
|
|
83
|
-
id: 'bulk_edit_main',
|
|
84
|
-
width: 'matchParent',
|
|
85
|
-
paramNameForRowId: 'row_id',
|
|
86
|
-
# Files with more data rows than this are rejected wholesale with an
|
|
87
|
-
# error dialog (the header row doesn't count).
|
|
88
|
-
maxRows: 3,
|
|
89
|
-
import: import_settings,
|
|
90
|
-
viewHeaders: default_headers,
|
|
91
|
-
viewCells: default_cells,
|
|
92
|
-
onCellChange: ->(action) do
|
|
93
|
-
action.snackbars_alert debug: true, paramName: 'message', fieldName: 'row'
|
|
94
|
-
end,
|
|
95
|
-
onSubmitStart: ->(action) do
|
|
96
|
-
action.runMultiple(
|
|
97
|
-
childActions: ->(multiple) do
|
|
98
|
-
multiple.logics_set targetId: 'bulk_edit_submit_top', data: { disabled: true }
|
|
99
|
-
multiple.logics_set targetId: 'bulk_edit_submit_bottom', data: { disabled: true }
|
|
100
|
-
multiple.logics_set targetId: 'bulk_edit_cancel', data: { displayed: true }
|
|
101
|
-
end
|
|
102
|
-
)
|
|
103
|
-
end,
|
|
104
|
-
onSubmitEnd: ->(action) do
|
|
105
|
-
action.runMultiple(
|
|
106
|
-
childActions: ->(multiple) do
|
|
107
|
-
multiple.logics_set targetId: 'bulk_edit_submit_top', data: { disabled: false }
|
|
108
|
-
multiple.logics_set targetId: 'bulk_edit_submit_bottom', data: { disabled: false }
|
|
109
|
-
multiple.logics_set targetId: 'bulk_edit_cancel', data: { displayed: false }
|
|
110
|
-
end
|
|
111
|
-
)
|
|
112
|
-
end,
|
|
113
|
-
onRowsLoad: ->(action) do
|
|
114
|
-
action.runMultiple(
|
|
115
|
-
debug: true,
|
|
116
|
-
fieldName: 'data',
|
|
117
|
-
childActions: ->(multiple) do
|
|
118
|
-
multiple.logics_set(
|
|
119
|
-
targetId: 'bulk_edit_loaded_count',
|
|
120
|
-
debug: true,
|
|
121
|
-
paramName: 'variables',
|
|
122
|
-
fieldName: 'data',
|
|
123
|
-
conditionalData: {
|
|
124
|
-
text: { "count": [{ "var": 'data.loadedRows' }] }
|
|
125
|
-
}
|
|
126
|
-
)
|
|
127
|
-
multiple.logics_set targetId: 'bulk_edit_submit_top', data: { displayed: true }
|
|
128
|
-
multiple.logics_set targetId: 'bulk_edit_submit_bottom', data: { displayed: true }
|
|
129
|
-
end
|
|
130
|
-
)
|
|
131
|
-
end,
|
|
132
|
-
onRowsSelect: ->(action) do
|
|
133
|
-
action.logics_set(
|
|
134
|
-
targetId: 'bulk_edit_selected_count',
|
|
135
|
-
debug: true,
|
|
136
|
-
paramName: 'variables',
|
|
137
|
-
fieldName: 'data',
|
|
138
|
-
conditionalData: {
|
|
139
|
-
text: { "count": [{ "var": 'data.selectedRows' }] }
|
|
140
|
-
}
|
|
141
|
-
)
|
|
142
|
-
end,
|
|
143
|
-
onRowsSucceed: ->(action) do
|
|
144
|
-
action.logics_set(
|
|
145
|
-
targetId: 'bulk_edit_succeeded_count',
|
|
146
|
-
debug: true,
|
|
147
|
-
paramName: 'variables',
|
|
148
|
-
fieldName: 'data',
|
|
149
|
-
conditionalData: {
|
|
150
|
-
text: {
|
|
151
|
-
"printf": [
|
|
152
|
-
'Uploaded: {0}',
|
|
153
|
-
{ "count": [{ "var": 'data.succeededRows' }] }
|
|
154
|
-
]
|
|
155
|
-
},
|
|
156
|
-
displayed: true
|
|
157
|
-
}
|
|
158
|
-
)
|
|
159
|
-
end
|
|
160
|
-
)
|
|
161
|
-
res.spacer height: 12
|
|
162
|
-
res.panels_flow(
|
|
163
|
-
innerPadding: { bottom: 0 },
|
|
164
|
-
childViews: ->(flow) do
|
|
165
|
-
flow.button(
|
|
166
|
-
id: 'bulk_edit_submit_top',
|
|
167
|
-
text: 'Submit (top)',
|
|
168
|
-
displayed: false,
|
|
169
|
-
onClick: ->(action) do
|
|
170
|
-
action.forms_submit targetId: 'bulk_edit_main'
|
|
171
|
-
end
|
|
172
|
-
)
|
|
173
|
-
flow.spacer width: 8
|
|
174
|
-
flow.button(
|
|
175
|
-
id: 'bulk_edit_submit_bottom',
|
|
176
|
-
text: 'Submit (bottom)',
|
|
177
|
-
displayed: false,
|
|
178
|
-
onClick: ->(action) do
|
|
179
|
-
action.forms_submit targetId: 'bulk_edit_main'
|
|
180
|
-
end
|
|
181
|
-
)
|
|
182
|
-
flow.spacer width: 8
|
|
183
|
-
flow.button(
|
|
184
|
-
id: 'bulk_edit_cancel',
|
|
185
|
-
text: 'Cancel import',
|
|
186
|
-
displayed: false,
|
|
187
|
-
onClick: ->(action) do
|
|
188
|
-
action.components_invoke targetId: 'bulk_edit_main', name: 'cancelImport'
|
|
189
|
-
end
|
|
190
|
-
)
|
|
191
|
-
end
|
|
192
|
-
)
|
|
193
|
-
res.spacer height: 12
|
|
194
|
-
res.hr width: 'matchParent'
|
|
195
|
-
# res.h2 text: 'Variants/props'
|
|
196
|
-
# res.spacer height: 8
|
|
197
|
-
# res.panels_flow innerPadding: { bottom: 0 }, childViews: ->(flow) do
|
|
198
|
-
# flow.button text: 'Default columns', onClick: ->(action) do
|
|
199
|
-
# action.logics_set targetId: 'bulk_edit_main', data: { viewHeaders: default_headers, viewCells: default_cells }
|
|
200
|
-
# end
|
|
201
|
-
# flow.spacer width: 8
|
|
202
|
-
# flow.button text: 'Compact columns', onClick: ->(action) do
|
|
203
|
-
# action.logics_set targetId: 'bulk_edit_main', data: { viewHeaders: compact_headers, viewCells: compact_cells }
|
|
204
|
-
# end
|
|
205
|
-
# flow.spacer width: 8
|
|
206
|
-
# flow.button text: 'Reset counts', onClick: ->(action) do
|
|
207
|
-
# action.runMultiple childActions: ->(multiple) do
|
|
208
|
-
# multiple.logics_set targetId: 'bulk_edit_loaded_count', data: { text: '0' }
|
|
209
|
-
# multiple.logics_set targetId: 'bulk_edit_selected_count', data: { text: '0' }
|
|
210
|
-
# multiple.logics_set targetId: 'bulk_edit_succeeded_count', data: { text: 'Uploaded: 0', displayed: false }
|
|
211
|
-
# end
|
|
212
|
-
# end
|
|
213
|
-
# end
|
|
214
|
-
# res.spacer height: 12
|
|
215
|
-
# res.hr width: 'matchParent'
|
|
216
|
-
res.h2 text: 'Actions/events'
|
|
217
|
-
res.spacer height: 8
|
|
218
|
-
res.panels_flow(
|
|
219
|
-
innerPadding: { bottom: 0 },
|
|
220
|
-
childViews: ->(flow) do
|
|
221
|
-
flow.button(
|
|
222
|
-
text: 'Submit (forms_submit)',
|
|
223
|
-
onClick: ->(action) do
|
|
224
|
-
action.forms_submit targetId: 'bulk_edit_main'
|
|
225
|
-
end
|
|
226
|
-
)
|
|
227
|
-
flow.spacer width: 8
|
|
228
|
-
flow.button(
|
|
229
|
-
text: 'Cancel import (invoke)',
|
|
230
|
-
onClick: ->(action) do
|
|
231
|
-
action.components_invoke targetId: 'bulk_edit_main', name: 'cancelImport'
|
|
232
|
-
end
|
|
233
|
-
)
|
|
234
|
-
end
|
|
235
|
-
)
|
|
236
|
-
res.spacer height: 12
|
|
237
|
-
res.hr width: 'matchParent'
|
|
238
|
-
res.h2 text: 'Edge/advanced'
|
|
239
|
-
res.spacer height: 8
|
|
240
|
-
res.label text: 'Swap to compact headers then try editing rows before submitting.'
|
|
241
|
-
end
|
|
242
|
-
)
|
|
243
|
-
end
|
|
244
|
-
)
|