glib-web 6.7.0 → 6.8.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/channels/glib/channel/appearance_channel.rb +3 -3
- data/app/channels/glib/channel/is_typing_channel.rb +1 -1
- data/app/channels/glib/channel/online_channel.rb +1 -1
- data/app/controllers/concerns/glib/auth/response.rb +2 -2
- data/app/controllers/concerns/glib/json/libs.rb +9 -9
- data/app/controllers/concerns/glib/json/traversal.rb +1 -1
- data/app/controllers/glib/home_controller.rb +1 -1
- data/app/helpers/glib/json_ui/abstract_builder.rb +1 -1
- data/app/helpers/glib/json_ui/page_helper.rb +1 -1
- data/app/helpers/glib/json_ui/view_builder/panels.rb +111 -1
- data/app/views/json_ui/garage/test_page/panels_bulkEdit3.json.jbuilder +279 -0
- data/lib/glib/json_crawler/action_crawlers/action_http.rb +1 -1
- data/lib/glib/json_crawler/action_crawlers/forms_submit.rb +4 -4
- data/lib/glib/json_crawler/router.rb +6 -3
- data/lib/glib/json_ui/config.rb +40 -0
- data/lib/glib/json_ui/name.rb +53 -0
- data/lib/glib/test_helpers.rb +10 -8
- data/lib/glib-web.rb +5 -0
- metadata +11 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0f8366db6a89ce988b77fb0392bc24078686aabf55873f2d8fcd38a5f39950bf
|
|
4
|
+
data.tar.gz: b27ab6afcea23ef3ea93f9b1c8289cb0a905ff563fb768d537cb940afae50a2a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 13c54527c80a6b95961dddec9e419c50c36c6d628a2c47577216e144583b44879caa7308c1e5b924a63d29aad3f935e83de86dc72095c333cf5660b8e8f53958
|
|
7
|
+
data.tar.gz: 4b4037b3e40cd01c00086219414a846e10a978510ebd3084db7b614b5480b83577c326bea105a60f0871ecf7962ad3392cccb2d826fed58034eba32758fd6668
|
|
@@ -7,15 +7,15 @@ module Glib
|
|
|
7
7
|
|
|
8
8
|
def status(data)
|
|
9
9
|
action = {
|
|
10
|
-
action: 'labels/set',
|
|
10
|
+
action: Glib::JsonUi::Name.for('labels/set'),
|
|
11
11
|
targetId: 'label-1',
|
|
12
12
|
text: data['text'],
|
|
13
13
|
onSet: {
|
|
14
|
-
action: 'timeouts/set',
|
|
14
|
+
action: Glib::JsonUi::Name.for('timeouts/set'),
|
|
15
15
|
interval: 3000,
|
|
16
16
|
timerId: 'label-1-set',
|
|
17
17
|
onTimeout: {
|
|
18
|
-
action: 'labels/set',
|
|
18
|
+
action: Glib::JsonUi::Name.for('labels/set'),
|
|
19
19
|
targetId: 'label-1',
|
|
20
20
|
text: "Room ##{data['room'].upcase}"
|
|
21
21
|
}
|
|
@@ -23,7 +23,7 @@ module Glib
|
|
|
23
23
|
ActionCable.server.broadcast \
|
|
24
24
|
"#{self.class.channel_name}_#{params['conversation_id']}",
|
|
25
25
|
action: {
|
|
26
|
-
action: 'component/set',
|
|
26
|
+
action: Glib::JsonUi::Name.for('component/set'),
|
|
27
27
|
text: text,
|
|
28
28
|
status: data['status'],
|
|
29
29
|
user_id: data['user_id']
|
|
@@ -23,7 +23,7 @@ module Glib
|
|
|
23
23
|
ActionCable.server.broadcast \
|
|
24
24
|
"#{self.class.channel_name}_#{params['conversation_id']}",
|
|
25
25
|
action: {
|
|
26
|
-
action: 'component/set',
|
|
26
|
+
action: Glib::JsonUi::Name.for('component/set'),
|
|
27
27
|
text: text,
|
|
28
28
|
status: data['status'],
|
|
29
29
|
value: data['reset_value'],
|
|
@@ -21,9 +21,9 @@ module Glib::Auth
|
|
|
21
21
|
view: 'button',
|
|
22
22
|
text: 'Re-login',
|
|
23
23
|
onClick: {
|
|
24
|
-
action: 'dialogs/close',
|
|
24
|
+
action: Glib::JsonUi::Name.for('dialogs/close'),
|
|
25
25
|
onClose: {
|
|
26
|
-
action: allow_other_host ? 'windows/openWeb' : 'windows/open',
|
|
26
|
+
action: Glib::JsonUi::Name.for(allow_other_host ? 'windows/openWeb' : 'windows/open'),
|
|
27
27
|
url: sign_in_url,
|
|
28
28
|
targetWindow: allow_other_host ? '_self' : nil
|
|
29
29
|
}
|
|
@@ -172,7 +172,7 @@ module Glib::Json::Libs
|
|
|
172
172
|
else
|
|
173
173
|
{
|
|
174
174
|
onResponse: {
|
|
175
|
-
action: 'windows/open', url: user_default_url
|
|
175
|
+
action: Glib::JsonUi::Name.for('windows/open'), url: user_default_url
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
178
|
end
|
|
@@ -215,7 +215,7 @@ module Glib::Json::Libs
|
|
|
215
215
|
|
|
216
216
|
def __glib_error_dialog(title, message, style_classes, bottom_views = [])
|
|
217
217
|
{
|
|
218
|
-
action: 'dialogs/show',
|
|
218
|
+
action: Glib::JsonUi::Name.for('dialogs/show'),
|
|
219
219
|
# Don't show another dialog, just use the current one since it's a new dialog anyway, which means
|
|
220
220
|
# that it wouldn't contain existing user data.
|
|
221
221
|
updateExisting: glib_json_dialog_mode?,
|
|
@@ -223,7 +223,7 @@ module Glib::Json::Libs
|
|
|
223
223
|
body: {
|
|
224
224
|
childViews: [
|
|
225
225
|
{
|
|
226
|
-
view: 'panels/vertical',
|
|
226
|
+
view: Glib::JsonUi::Name.for('panels/vertical'),
|
|
227
227
|
styleClasses: ['glib-error-dialog'].concat(style_classes),
|
|
228
228
|
childViews: [
|
|
229
229
|
{
|
|
@@ -286,12 +286,12 @@ module Glib::Json::Libs
|
|
|
286
286
|
def json_ui_redirect_to(url, status: :ok)
|
|
287
287
|
# Use windows_reload instead of windows_open so clicking back will skip the bridging page.
|
|
288
288
|
# on_response = { action: 'windows/open', url: url }
|
|
289
|
-
on_response = { action: 'windows/reload', url: url }
|
|
289
|
+
on_response = { action: Glib::JsonUi::Name.for('windows/reload'), url: url }
|
|
290
290
|
|
|
291
291
|
if glib_json_dialog_mode?
|
|
292
292
|
# Only do this in dialog mode because this seems to add to the rendering time
|
|
293
293
|
# (i.e. longer flicker time), presumably due to the use of things like `nextTick()`.
|
|
294
|
-
on_response = { action: 'dialogs/close', onClose: on_response }
|
|
294
|
+
on_response = { action: Glib::JsonUi::Name.for('dialogs/close'), onClose: on_response }
|
|
295
295
|
end
|
|
296
296
|
|
|
297
297
|
# Use onResponse because it is harder to guarantee that `onLoad` executes in Vuejs because it relies
|
|
@@ -389,14 +389,14 @@ module Glib::Json::Libs
|
|
|
389
389
|
view: 'button',
|
|
390
390
|
text: 'Refresh',
|
|
391
391
|
onClick: {
|
|
392
|
-
action: 'auth/saveCsrfToken',
|
|
392
|
+
action: Glib::JsonUi::Name.for('auth/saveCsrfToken'),
|
|
393
393
|
token: form_authenticity_token,
|
|
394
394
|
onSave: {
|
|
395
|
-
action: 'dialogs/close',
|
|
395
|
+
action: Glib::JsonUi::Name.for('dialogs/close'),
|
|
396
396
|
onClose: {
|
|
397
|
-
action: 'windows/reload',
|
|
397
|
+
action: Glib::JsonUi::Name.for('windows/reload'),
|
|
398
398
|
onReload: {
|
|
399
|
-
action: 'snackbars/alert',
|
|
399
|
+
action: Glib::JsonUi::Name.for('snackbars/alert'),
|
|
400
400
|
message: 'Refreshed. You can try again now.'
|
|
401
401
|
}
|
|
402
402
|
}
|
|
@@ -15,7 +15,7 @@ module Glib
|
|
|
15
15
|
|
|
16
16
|
def add_singleton_element(type, name, *args)
|
|
17
17
|
name_components = name.to_s.split('_')
|
|
18
|
-
json.set! type,
|
|
18
|
+
json.set! type, Name.join(name_components) if type
|
|
19
19
|
args = [yield] if block_given?
|
|
20
20
|
|
|
21
21
|
# TODO: consider caching these instances
|
|
@@ -264,7 +264,7 @@ module Glib
|
|
|
264
264
|
panel_options = options.slice!(:url, :method, :local, :onSubmit, :paramNameForFormData, :model, :onChange, :onChangeAndLoad, :autoValidate, :disableDirtyCheck)
|
|
265
265
|
|
|
266
266
|
json.fullPageForm do
|
|
267
|
-
json.view 'panels/fullPageForm'
|
|
267
|
+
json.view Name.for('panels/fullPageForm')
|
|
268
268
|
form.props(options)
|
|
269
269
|
end
|
|
270
270
|
|
|
@@ -294,7 +294,7 @@ class Glib::JsonUi::ViewBuilder
|
|
|
294
294
|
json.childViews do
|
|
295
295
|
if @method != :get
|
|
296
296
|
json.child! do
|
|
297
|
-
json.view 'fields/hidden'
|
|
297
|
+
json.view Glib::JsonUi::Name.for('fields/hidden')
|
|
298
298
|
json.name 'authenticity_token'
|
|
299
299
|
json.value page.context.form_authenticity_token
|
|
300
300
|
end
|
|
@@ -606,6 +606,116 @@ class Glib::JsonUi::ViewBuilder
|
|
|
606
606
|
|
|
607
607
|
end
|
|
608
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
|
+
if page.current_form
|
|
620
|
+
raise 'panels_bulkEdit3 is not allowed inside a form; place it as a sibling and target it with forms_submit targetId'
|
|
621
|
+
end
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
# Maximum number of data rows an uploaded file may contain; files with
|
|
625
|
+
# more rows are rejected whole. No limit when unset.
|
|
626
|
+
int :maxRows
|
|
627
|
+
|
|
628
|
+
array :viewHeaders
|
|
629
|
+
|
|
630
|
+
string :paramNameForRowId
|
|
631
|
+
|
|
632
|
+
action :onCellChange
|
|
633
|
+
action :onRowsLoad
|
|
634
|
+
action :onRowsSelect
|
|
635
|
+
action :onRowsSucceed
|
|
636
|
+
# Fires when parked rows drain out of the table on save (they never
|
|
637
|
+
# POST), so the page can keep its Loaded/Uploaded/Skipped arithmetic
|
|
638
|
+
# honest.
|
|
639
|
+
action :onRowsSkip
|
|
640
|
+
action :onSubmitStart
|
|
641
|
+
action :onSubmitEnd
|
|
642
|
+
|
|
643
|
+
# The file shape. Without it the table takes plain CSVs (header row
|
|
644
|
+
# first, header names = field ids -- the bulkEdit2 shape):
|
|
645
|
+
# detectHeader: { allOf: [...], anyOf: [...], scanRows: N } -- find
|
|
646
|
+
# the header row BY CONTENT (report preambles survive);
|
|
647
|
+
# headerMap: { csvHeader => field } -- header-keyed, order-independent
|
|
648
|
+
# mapping (unknown columns ignored);
|
|
649
|
+
# requiredHeaders: [...] -- named rejection when missing;
|
|
650
|
+
# rejectIf: [{ when: <JsonLogic over {headers, rowCount}>, error: }]
|
|
651
|
+
# -- named wrong-file rejections, first match wins;
|
|
652
|
+
# transforms: { field => ['unguard', 'date', 'lowercase', 'trim',
|
|
653
|
+
# 'coalesce:<other_field>' (fills an empty self), 'prefer:<other_field>'
|
|
654
|
+
# (the other field wins when present)] };
|
|
655
|
+
# derived: { field => <JsonLogic over the mapped values> };
|
|
656
|
+
# uniqueBy: { field:, error: } -- duplicate-value rejection;
|
|
657
|
+
# errors: { headerNotFound:, missingHeaders:, tooManyRows:,
|
|
658
|
+
# noDataRows:, duplicates:, unreadable:, readFailure: } -- copy
|
|
659
|
+
# templates ({rowCount} {maxRows} {headers} {values});
|
|
660
|
+
# dropzone: { title:, subtitle: }; accept: '.csv'.
|
|
661
|
+
hash :parse
|
|
662
|
+
|
|
663
|
+
# Ordered row-eligibility rules: the first JsonLogic `when` that matches
|
|
664
|
+
# parks the row (unticked, disabled checkbox, note under the note
|
|
665
|
+
# column, warning-icon tooltip). note/noteDetail/checkboxTitle are
|
|
666
|
+
# strings or JsonLogic. Editing a cell re-evaluates the gates and can
|
|
667
|
+
# revive a parked row.
|
|
668
|
+
array :rowGates
|
|
669
|
+
|
|
670
|
+
# Newest-row-wins: marks superseded rows (older rows per groupBy) with a
|
|
671
|
+
# `superseded` var that rowGates can test and order against.
|
|
672
|
+
# dedup: { groupBy: <JsonLogic>, newestBy: [date fields in priority] }
|
|
673
|
+
hash :dedup
|
|
674
|
+
|
|
675
|
+
# Referenced rows submit last (one level): rows whose `reference` value
|
|
676
|
+
# (e.g. a guardian email) matches another selected row's `target` value
|
|
677
|
+
# (e.g. a member email) go after theirs.
|
|
678
|
+
# submitOrder: { reference: <JsonLogic>, target: <JsonLogic> }
|
|
679
|
+
hash :submitOrder
|
|
680
|
+
|
|
681
|
+
# File-level notes rendered after load: [{ when: <JsonLogic over
|
|
682
|
+
# {headers, rowCount, rows}>, text: string-or-JsonLogic, style:
|
|
683
|
+
# 'info'|'warning' }].
|
|
684
|
+
array :fileNotes
|
|
685
|
+
|
|
686
|
+
# Labels for the visibility chips (rendered when the file left rows
|
|
687
|
+
# out). Defaults: 'To import' / 'Left out'.
|
|
688
|
+
hash :viewChips
|
|
689
|
+
|
|
690
|
+
# Rendered when the included view is empty and rows were left out.
|
|
691
|
+
string :emptyNote
|
|
692
|
+
|
|
693
|
+
# The column whose cell carries a parked row's note (default: the first
|
|
694
|
+
# viewHeader's id).
|
|
695
|
+
string :noteColumn
|
|
696
|
+
|
|
697
|
+
# Constants merged into every rule context (row gates, derived fields,
|
|
698
|
+
# file notes) at the lowest precedence -- e.g. an award-spelling map.
|
|
699
|
+
hash :variables
|
|
700
|
+
|
|
701
|
+
def viewCells(values)
|
|
702
|
+
json.set! 'viewCells' do
|
|
703
|
+
values.each do |value|
|
|
704
|
+
value.call(page.view_builder)
|
|
705
|
+
end
|
|
706
|
+
end
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
def dataRows(value)
|
|
710
|
+
json.dataRows value
|
|
711
|
+
end
|
|
712
|
+
|
|
713
|
+
def viewRows(value)
|
|
714
|
+
json.viewRows value
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
end
|
|
718
|
+
|
|
609
719
|
# deprecated
|
|
610
720
|
class BulkEdit < View
|
|
611
721
|
hash :nextPage
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
json.title 'Test Page (Panels Bulk Edit 3)'
|
|
2
|
+
|
|
3
|
+
page = json_ui_page json
|
|
4
|
+
|
|
5
|
+
render 'json_ui/garage/test_page/header', json: json, page: page
|
|
6
|
+
|
|
7
|
+
# A report-shaped file: preamble rows before the header, apostrophe-guarded
|
|
8
|
+
# IDs, mixed date formats, a Status column and rows with no email -- enough to
|
|
9
|
+
# exercise header detection, headerMap, transforms, gates and the chips.
|
|
10
|
+
report_header_map = {
|
|
11
|
+
'Member ID' => :sg_id,
|
|
12
|
+
'First Name' => :given_name,
|
|
13
|
+
'Last Name' => :family_name,
|
|
14
|
+
'Email Address 1' => :email,
|
|
15
|
+
'Status' => :status,
|
|
16
|
+
'Member Since' => :member_since
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# Ordered eligibility rules: first match parks the row. Interpolated copy via
|
|
20
|
+
# printf, static copy as strings.
|
|
21
|
+
report_row_gates = [
|
|
22
|
+
{
|
|
23
|
+
when: { '!=' => [{ 'var' => 'status' }, 'Active'] },
|
|
24
|
+
note: { 'printf' => ['Status: {0}', { 'var' => 'status' }] },
|
|
25
|
+
checkboxTitle: { 'printf' => ['Status is {0}: only Active members are imported', { 'var' => 'status' }] }
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
when: { '!' => { 'or' => [{ 'isPresent' => [{ 'var' => 'email' }] }] } },
|
|
29
|
+
note: 'No email in this file',
|
|
30
|
+
noteDetail: 'This row has no email address, so there is no way to reach the member. Record one in the source system and re-export.'
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
plain_headers = [
|
|
35
|
+
{ text: 'Month', id: 'month', importable: true, tooltip: { text: 'Month', placement: 'top' } },
|
|
36
|
+
{ text: 'Electricity usage', id: 'electricity_usage', importable: true },
|
|
37
|
+
{ text: 'Gas usage', id: 'gas_usage', importable: true }
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
plain_cells = [
|
|
41
|
+
->(view) do
|
|
42
|
+
view.fields_text styleClass: 'compact', width: 'matchParent'
|
|
43
|
+
end,
|
|
44
|
+
->(view) do
|
|
45
|
+
view.fields_number styleClass: 'compact', width: 'matchParent'
|
|
46
|
+
end,
|
|
47
|
+
->(view) do
|
|
48
|
+
view.fields_number styleClass: 'compact', width: 'matchParent'
|
|
49
|
+
end
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
report_headers = [
|
|
53
|
+
{ text: 'Name', id: 'display_name', readOnly: true },
|
|
54
|
+
{ text: 'Source ID', id: 'sg_id', readOnly: true },
|
|
55
|
+
{ text: 'Email', id: 'email', importable: true },
|
|
56
|
+
{ text: 'Status', id: 'status', readOnly: true },
|
|
57
|
+
{ text: 'Member Since', id: 'member_since', readOnly: true }
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
report_cells = [
|
|
61
|
+
->(view) do
|
|
62
|
+
view.label
|
|
63
|
+
end,
|
|
64
|
+
->(view) do
|
|
65
|
+
view.label
|
|
66
|
+
end,
|
|
67
|
+
->(view) do
|
|
68
|
+
view.fields_email styleClass: 'compact', width: 'matchParent'
|
|
69
|
+
end,
|
|
70
|
+
->(view) do
|
|
71
|
+
view.label
|
|
72
|
+
end,
|
|
73
|
+
->(view) do
|
|
74
|
+
view.label
|
|
75
|
+
end
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
import_settings = { submitUrl: json_ui_garage_url(path: 'forms/bulk_edit_post2'), paramName: 'user' }
|
|
79
|
+
|
|
80
|
+
page.body(
|
|
81
|
+
childViews: ->(body) do
|
|
82
|
+
body.panels_responsive(
|
|
83
|
+
padding: glib_json_padding_body,
|
|
84
|
+
childViews: ->(res) do
|
|
85
|
+
res.h2 text: 'Overview'
|
|
86
|
+
res.p text: 'panels_bulkEdit3 is the generic bulk-edit table: bulkEdit2’s skeleton plus config-driven file parsing, row gates, read-only columns, view chips and submission extras.'
|
|
87
|
+
res.spacer height: 12
|
|
88
|
+
res.hr width: 'matchParent'
|
|
89
|
+
|
|
90
|
+
res.h2 text: 'Basic example (plain CSV)'
|
|
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 (the bulkEdit2 shape). maxRows 3 rejects oversized files whole.'
|
|
93
|
+
res.spacer height: 8
|
|
94
|
+
res.panels_horizontal(
|
|
95
|
+
align: 'middle',
|
|
96
|
+
childViews: ->(horizontal) do
|
|
97
|
+
horizontal.label text: 'Loaded: '
|
|
98
|
+
horizontal.label id: 'bulk_edit3_loaded_count', text: '0'
|
|
99
|
+
horizontal.spacer width: 12
|
|
100
|
+
horizontal.label text: 'Selected: '
|
|
101
|
+
horizontal.label id: 'bulk_edit3_selected_count', text: '0'
|
|
102
|
+
end
|
|
103
|
+
)
|
|
104
|
+
res.spacer height: 8
|
|
105
|
+
res.panels_bulkEdit3(
|
|
106
|
+
id: 'bulk_edit3_plain',
|
|
107
|
+
width: 'matchParent',
|
|
108
|
+
paramNameForRowId: 'row_id',
|
|
109
|
+
maxRows: 3,
|
|
110
|
+
import: import_settings,
|
|
111
|
+
viewHeaders: plain_headers,
|
|
112
|
+
viewCells: plain_cells,
|
|
113
|
+
onRowsLoad: ->(action) do
|
|
114
|
+
action.runMultiple(
|
|
115
|
+
fieldName: 'data',
|
|
116
|
+
childActions: ->(multiple) do
|
|
117
|
+
multiple.logics_set(
|
|
118
|
+
targetId: 'bulk_edit3_loaded_count',
|
|
119
|
+
paramName: 'variables',
|
|
120
|
+
fieldName: 'data',
|
|
121
|
+
conditionalData: {
|
|
122
|
+
text: { 'count': [{ 'var': 'data.loadedRows' }] }
|
|
123
|
+
}
|
|
124
|
+
)
|
|
125
|
+
end
|
|
126
|
+
)
|
|
127
|
+
end,
|
|
128
|
+
onRowsSelect: ->(action) do
|
|
129
|
+
action.logics_set(
|
|
130
|
+
targetId: 'bulk_edit3_selected_count',
|
|
131
|
+
paramName: 'variables',
|
|
132
|
+
fieldName: 'data',
|
|
133
|
+
conditionalData: {
|
|
134
|
+
text: { 'count': [{ 'var': 'data.selectedRows' }] }
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
)
|
|
139
|
+
res.spacer height: 12
|
|
140
|
+
res.hr width: 'matchParent'
|
|
141
|
+
|
|
142
|
+
res.h2 text: 'Report-shaped file (parse config + row gates)'
|
|
143
|
+
res.spacer height: 8
|
|
144
|
+
res.markdown text: 'The same component with `parse` config: the header row is found by content (preamble rows survive), CSV headers map to fields, IDs are apostrophe-unwrapped, dates normalize, and the first matching row gate parks the row (unticked, disabled checkbox, note under the first column). Parked rows drain into Skipped on save.'
|
|
145
|
+
res.spacer height: 8
|
|
146
|
+
res.panels_horizontal(
|
|
147
|
+
align: 'middle',
|
|
148
|
+
childViews: ->(horizontal) do
|
|
149
|
+
horizontal.label text: 'Loaded: '
|
|
150
|
+
horizontal.label id: 'bulk_edit3_report_loaded_count', text: '0'
|
|
151
|
+
horizontal.spacer width: 12
|
|
152
|
+
horizontal.label text: 'Selected: '
|
|
153
|
+
horizontal.label id: 'bulk_edit3_report_selected_count', text: '0'
|
|
154
|
+
horizontal.panels_horizontal id: 'bulk_edit3_skipped_counter', displayed: false, childViews: ->(counter) do
|
|
155
|
+
counter.label text: ' - '
|
|
156
|
+
counter.chip id: 'bulk_edit3_skipped_count', text: 'Skipped: 0'
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
)
|
|
160
|
+
res.spacer height: 8
|
|
161
|
+
res.panels_bulkEdit3(
|
|
162
|
+
id: 'bulk_edit3_report',
|
|
163
|
+
width: 'matchParent',
|
|
164
|
+
paramNameForRowId: 'row_id',
|
|
165
|
+
maxRows: 100,
|
|
166
|
+
parse: {
|
|
167
|
+
headerMap: report_header_map,
|
|
168
|
+
requiredHeaders: ['Member ID', 'First Name', 'Status'],
|
|
169
|
+
detectHeader: {
|
|
170
|
+
allOf: ['Member ID'],
|
|
171
|
+
anyOf: ['Status', 'Email Address 1'],
|
|
172
|
+
scanRows: 12
|
|
173
|
+
},
|
|
174
|
+
transforms: {
|
|
175
|
+
sg_id: ['unguard'],
|
|
176
|
+
member_since: ['date'],
|
|
177
|
+
email: ['lowercase']
|
|
178
|
+
},
|
|
179
|
+
derived: {
|
|
180
|
+
display_name: { 'joinNonEmpty': [' ', { 'var': 'given_name' }, { 'var': 'family_name' }] }
|
|
181
|
+
},
|
|
182
|
+
uniqueBy: {
|
|
183
|
+
field: 'sg_id',
|
|
184
|
+
error: 'This file contains duplicate Member IDs ({values}). Each member row must have a unique Member ID.'
|
|
185
|
+
},
|
|
186
|
+
errors: {
|
|
187
|
+
headerNotFound: "This doesn't look like the expected export. Couldn't find the column headers."
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
rowGates: report_row_gates,
|
|
191
|
+
fileNotes: [
|
|
192
|
+
{
|
|
193
|
+
when: { '<': [0, { 'count': [{ 'filter': [{ 'var': 'rows' }, { '!=': [{ 'var': 'status' }, 'Active'] }] }] }] },
|
|
194
|
+
text: { 'printf': ['Heads up: {0} member(s) in this file are not Active.', { 'count': [{ 'filter': [{ 'var': 'rows' }, { '!=': [{ 'var': 'status' }, 'Active'] }] }] }] },
|
|
195
|
+
style: 'info'
|
|
196
|
+
}
|
|
197
|
+
],
|
|
198
|
+
import: {
|
|
199
|
+
submitUrl: json_ui_garage_url(path: 'forms/bulk_edit_post2'),
|
|
200
|
+
paramName: 'member_import',
|
|
201
|
+
rowFields: ['sg_id', 'status']
|
|
202
|
+
},
|
|
203
|
+
viewHeaders: report_headers,
|
|
204
|
+
viewCells: report_cells,
|
|
205
|
+
onRowsLoad: ->(action) do
|
|
206
|
+
action.runMultiple(
|
|
207
|
+
fieldName: 'data',
|
|
208
|
+
childActions: ->(multiple) do
|
|
209
|
+
multiple.logics_set(
|
|
210
|
+
targetId: 'bulk_edit3_report_loaded_count',
|
|
211
|
+
paramName: 'variables',
|
|
212
|
+
fieldName: 'data',
|
|
213
|
+
conditionalData: {
|
|
214
|
+
text: { 'count': [{ 'var': 'data.loadedRows' }] }
|
|
215
|
+
}
|
|
216
|
+
)
|
|
217
|
+
end
|
|
218
|
+
)
|
|
219
|
+
end,
|
|
220
|
+
onRowsSelect: ->(action) do
|
|
221
|
+
action.runMultiple(
|
|
222
|
+
fieldName: 'data',
|
|
223
|
+
childActions: ->(multiple) do
|
|
224
|
+
multiple.logics_set(
|
|
225
|
+
targetId: 'bulk_edit3_report_selected_count',
|
|
226
|
+
paramName: 'variables',
|
|
227
|
+
fieldName: 'data',
|
|
228
|
+
conditionalData: {
|
|
229
|
+
text: { 'count': [{ 'var': 'data.selectedRows' }] }
|
|
230
|
+
}
|
|
231
|
+
)
|
|
232
|
+
end
|
|
233
|
+
)
|
|
234
|
+
end,
|
|
235
|
+
onRowsSkip: ->(action) do
|
|
236
|
+
action.runMultiple(
|
|
237
|
+
fieldName: 'data',
|
|
238
|
+
childActions: ->(multiple) do
|
|
239
|
+
multiple.components_set targetId: 'bulk_edit3_skipped_counter', data: { displayed: true }
|
|
240
|
+
multiple.logics_set(
|
|
241
|
+
targetId: 'bulk_edit3_skipped_count',
|
|
242
|
+
paramName: 'variables',
|
|
243
|
+
fieldName: 'data',
|
|
244
|
+
conditionalData: {
|
|
245
|
+
text: {
|
|
246
|
+
'printf': ['Skipped: {0}', { 'count': [{ 'var': 'data.skippedRows' }] }]
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
)
|
|
250
|
+
end
|
|
251
|
+
)
|
|
252
|
+
end
|
|
253
|
+
)
|
|
254
|
+
res.spacer height: 12
|
|
255
|
+
res.hr width: 'matchParent'
|
|
256
|
+
res.h2 text: 'Actions/events'
|
|
257
|
+
res.spacer height: 8
|
|
258
|
+
res.panels_flow(
|
|
259
|
+
innerPadding: { bottom: 0 },
|
|
260
|
+
childViews: ->(flow) do
|
|
261
|
+
flow.button(
|
|
262
|
+
text: 'Submit (forms_submit)',
|
|
263
|
+
onClick: ->(action) do
|
|
264
|
+
action.forms_submit targetId: 'bulk_edit3_plain'
|
|
265
|
+
end
|
|
266
|
+
)
|
|
267
|
+
flow.spacer width: 8
|
|
268
|
+
flow.button(
|
|
269
|
+
text: 'Start over (invoke)',
|
|
270
|
+
onClick: ->(action) do
|
|
271
|
+
action.components_invoke targetId: 'bulk_edit3_report', name: 'startOver'
|
|
272
|
+
end
|
|
273
|
+
)
|
|
274
|
+
end
|
|
275
|
+
)
|
|
276
|
+
end
|
|
277
|
+
)
|
|
278
|
+
end
|
|
279
|
+
)
|
|
@@ -4,7 +4,7 @@ module Glib
|
|
|
4
4
|
def initialize(method, http, args, controller)
|
|
5
5
|
@http = http
|
|
6
6
|
json = @http.send(method, args['url'], controller, args.fetch('formData', {}))
|
|
7
|
-
@http.router.http_actions.add([args['action'], args['url'], args.fetch('formData', {})])
|
|
7
|
+
@http.router.http_actions.add([Glib::JsonUi::Name.normalize(args['action']), args['url'], args.fetch('formData', {})])
|
|
8
8
|
perform(json['onResponse'])
|
|
9
9
|
end
|
|
10
10
|
end
|
|
@@ -15,8 +15,8 @@ module Glib
|
|
|
15
15
|
|
|
16
16
|
raise "Submit action needs to be inside a form: #{http.router.last_log}" unless form
|
|
17
17
|
|
|
18
|
-
case form['view']
|
|
19
|
-
when 'panels/bulkEdit2'
|
|
18
|
+
case Glib::JsonUi::Name.normalize(form['view'])
|
|
19
|
+
when 'panels/bulkEdit2', 'panels/bulkEdit3'
|
|
20
20
|
method = 'post'
|
|
21
21
|
action = "forms/#{method}"
|
|
22
22
|
url = form['import']['submitUrl']
|
|
@@ -43,7 +43,7 @@ module Glib
|
|
|
43
43
|
child_views = view['childViews'] || []
|
|
44
44
|
|
|
45
45
|
# If this form is page level, we need to check the child views of the entire page.
|
|
46
|
-
if view['view']
|
|
46
|
+
if Glib::JsonUi::Name.eq?(view['view'], 'panels/fullPageForm')
|
|
47
47
|
child_views += @http.router.page_spec['header']&.[]('childViews') || []
|
|
48
48
|
child_views += @http.router.page_spec['body']&.[]('childViews') || []
|
|
49
49
|
child_views += @http.router.page_spec['footer']&.[]('childViews') || []
|
|
@@ -52,7 +52,7 @@ module Glib
|
|
|
52
52
|
@http.router.crawl_multiple child_views, ->(child) do
|
|
53
53
|
next if child['template'].present?
|
|
54
54
|
|
|
55
|
-
name = child['view'] || ''
|
|
55
|
+
name = Glib::JsonUi::Name.normalize(child['view']) || ''
|
|
56
56
|
if name.start_with?('fields/')
|
|
57
57
|
fields << child
|
|
58
58
|
|
|
@@ -66,7 +66,7 @@ module Glib
|
|
|
66
66
|
|
|
67
67
|
def step(http, args)
|
|
68
68
|
# TODO: Refactor
|
|
69
|
-
case args['view']
|
|
69
|
+
case Glib::JsonUi::Name.normalize(args['view'])
|
|
70
70
|
when 'fields/submit'
|
|
71
71
|
@depth += 1
|
|
72
72
|
# forms = @visitor.forms
|
|
@@ -98,7 +98,10 @@ module Glib
|
|
|
98
98
|
end
|
|
99
99
|
|
|
100
100
|
def process_action(http, spec)
|
|
101
|
-
|
|
101
|
+
# Normalize to the canonical slash form so the crawler works against apps
|
|
102
|
+
# configured with either separator, and so recorded/logged action names
|
|
103
|
+
# stay stable (permission snapshots compare them).
|
|
104
|
+
action = Glib::JsonUi::Name.normalize(spec&.fetch('action'))
|
|
102
105
|
params = spec
|
|
103
106
|
|
|
104
107
|
assert_target_ids_exist(params)
|
|
@@ -304,7 +307,7 @@ module Glib
|
|
|
304
307
|
http_actions.each do |http_action|
|
|
305
308
|
next if http_action[1].blank?
|
|
306
309
|
distance = ::DamerauLevenshtein.distance(http_action[1], params['url'])
|
|
307
|
-
result = (distance <= 1) && (http_action[0] == params['action'])
|
|
310
|
+
result = (distance <= 1) && (http_action[0] == Glib::JsonUi::Name.normalize(params['action']))
|
|
308
311
|
|
|
309
312
|
break if result
|
|
310
313
|
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module Glib
|
|
2
|
+
module JsonUi
|
|
3
|
+
# Global JSON UI settings.
|
|
4
|
+
#
|
|
5
|
+
# `name_separator` controls the character that joins the namespace and the
|
|
6
|
+
# component/action name in the generated JSON, e.g. `panels/responsive`
|
|
7
|
+
# (default) vs `panels_responsive`.
|
|
8
|
+
#
|
|
9
|
+
# The default stays `/` for backwards compatibility; `_` becomes the default
|
|
10
|
+
# in the next major version. Apps opt in early via an initializer:
|
|
11
|
+
#
|
|
12
|
+
# # config/initializers/glib.rb
|
|
13
|
+
# Glib::JsonUi::Config.name_separator = '_'
|
|
14
|
+
#
|
|
15
|
+
# Frontends built on glib-web-npm accept either form, so flipping this is a
|
|
16
|
+
# server-side-only change.
|
|
17
|
+
class Config
|
|
18
|
+
SEPARATORS = ['/', '_'].freeze
|
|
19
|
+
|
|
20
|
+
# The shipped default. Stays '/' until the next major version, when it
|
|
21
|
+
# becomes '_'. Apps opt in early via `Config.name_separator = '_'`.
|
|
22
|
+
DEFAULT_NAME_SEPARATOR = '/'
|
|
23
|
+
|
|
24
|
+
@@name_separator = DEFAULT_NAME_SEPARATOR
|
|
25
|
+
|
|
26
|
+
def self.name_separator
|
|
27
|
+
@@name_separator
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.name_separator=(value)
|
|
31
|
+
value = value.to_s
|
|
32
|
+
unless SEPARATORS.include?(value)
|
|
33
|
+
raise ArgumentError, "Invalid name separator: #{value.inspect}. Allowed: #{SEPARATORS.map(&:inspect).join(', ')}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
@@name_separator = value
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module Glib
|
|
2
|
+
module JsonUi
|
|
3
|
+
# Helpers for the namespaced names that appear in the generated JSON as
|
|
4
|
+
# `view`, `action` and `template` values.
|
|
5
|
+
#
|
|
6
|
+
# Names are always written in canonical form in this codebase (`panels/form`)
|
|
7
|
+
# regardless of what `Config.name_separator` is set to:
|
|
8
|
+
#
|
|
9
|
+
# - Producing: run the canonical literal through `Name.for` (or build it
|
|
10
|
+
# from parts with `Name.join`) so it is emitted using the configured
|
|
11
|
+
# separator.
|
|
12
|
+
# - Consuming: compare with `Name.eq?` / `Name.start_with?`, which accept
|
|
13
|
+
# either separator. Reads are deliberately separator-agnostic so tools
|
|
14
|
+
# like the crawler work against any app, and against fixtures recorded
|
|
15
|
+
# before the switch.
|
|
16
|
+
module Name
|
|
17
|
+
class << self
|
|
18
|
+
# 'windows/reload' -> 'windows/reload' or 'windows_reload'
|
|
19
|
+
def for(canonical)
|
|
20
|
+
return canonical unless canonical.is_a?(String)
|
|
21
|
+
|
|
22
|
+
canonical.tr('/', Config.name_separator)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# ['windows', 'reload'] -> 'windows/reload' or 'windows_reload'
|
|
26
|
+
def join(components)
|
|
27
|
+
components.join(Config.name_separator)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Rewrites a name coming from JSON into canonical (slash) form. Only the
|
|
31
|
+
# first separator is rewritten: the second part may itself be camelCase
|
|
32
|
+
# but never contains a separator (e.g. `storageItems_set`).
|
|
33
|
+
def normalize(value)
|
|
34
|
+
return value unless value.is_a?(String)
|
|
35
|
+
|
|
36
|
+
value.sub('_', '/')
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Either side may use either separator.
|
|
40
|
+
def eq?(value, canonical)
|
|
41
|
+
normalize(value) == normalize(canonical)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def start_with?(value, *canonicals)
|
|
45
|
+
normalized = normalize(value)
|
|
46
|
+
return false unless normalized.is_a?(String)
|
|
47
|
+
|
|
48
|
+
canonicals.any? { |canonical| normalized.start_with?(normalize(canonical)) }
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
data/lib/glib/test_helpers.rb
CHANGED
|
@@ -229,12 +229,14 @@ module Glib
|
|
|
229
229
|
end
|
|
230
230
|
|
|
231
231
|
def __view_is_form?(view)
|
|
232
|
-
view.is_a?(Hash)
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
232
|
+
return false unless view.is_a?(Hash)
|
|
233
|
+
|
|
234
|
+
name = Glib::JsonUi::Name.normalize(view['view'])
|
|
235
|
+
name&.start_with?('panels/form') ||
|
|
236
|
+
name&.start_with?('panels/fullPageForm') ||
|
|
237
|
+
name == 'panels/bulkEdit2' ||
|
|
238
|
+
name == 'panels/bulkEdit3' ||
|
|
239
|
+
name == 'panels/splitForm'
|
|
238
240
|
end
|
|
239
241
|
|
|
240
242
|
def __build_form_params(form, data)
|
|
@@ -247,7 +249,7 @@ module Glib
|
|
|
247
249
|
data_prefixes = data.keys.map { |k| "#{k}[" }
|
|
248
250
|
|
|
249
251
|
__extract_form_fields(form).each do |field|
|
|
250
|
-
next unless field[:view]
|
|
252
|
+
next unless Glib::JsonUi::Name.eq?(field[:view], 'fields/hidden')
|
|
251
253
|
next if data_prefixes.any? { |prefix| field[:name]&.start_with?(prefix) }
|
|
252
254
|
|
|
253
255
|
params[field[:name]] = field[:value]
|
|
@@ -268,7 +270,7 @@ module Glib
|
|
|
268
270
|
def __extract_form_fields(form)
|
|
269
271
|
fields = []
|
|
270
272
|
__traverse_form_views(form['childViews'] || []) do |view|
|
|
271
|
-
if view.is_a?(Hash) && view['view']
|
|
273
|
+
if view.is_a?(Hash) && Glib::JsonUi::Name.start_with?(view['view'], 'fields/')
|
|
272
274
|
fields << { name: view['name'], value: view['value'], view: view['view'] }
|
|
273
275
|
end
|
|
274
276
|
end
|
data/lib/glib-web.rb
CHANGED
|
@@ -5,6 +5,11 @@ if defined?(::Rails)
|
|
|
5
5
|
require 'glib/snapshot'
|
|
6
6
|
end
|
|
7
7
|
require 'glib/value'
|
|
8
|
+
# Loaded eagerly (not autoloaded from app/) so an app initializer can set
|
|
9
|
+
# `Glib::JsonUi::Config.name_separator` without tripping Rails' ban on
|
|
10
|
+
# autoloading during initialization -- and so the setting survives a reload.
|
|
11
|
+
require 'glib/json_ui/config'
|
|
12
|
+
require 'glib/json_ui/name'
|
|
8
13
|
require 'glib/json_crawler'
|
|
9
14
|
|
|
10
15
|
require 'glib/dynamic_text'
|
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: 6.
|
|
4
|
+
version: 6.8.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-08-22 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: activestorage
|
|
@@ -177,6 +178,7 @@ dependencies:
|
|
|
177
178
|
- - ">="
|
|
178
179
|
- !ruby/object:Gem::Version
|
|
179
180
|
version: '0'
|
|
181
|
+
description:
|
|
180
182
|
email: ''
|
|
181
183
|
executables: []
|
|
182
184
|
extensions: []
|
|
@@ -476,6 +478,7 @@ files:
|
|
|
476
478
|
- app/views/json_ui/garage/test_page/pagination.json.jbuilder
|
|
477
479
|
- app/views/json_ui/garage/test_page/panels.json.jbuilder
|
|
478
480
|
- app/views/json_ui/garage/test_page/panels_bulkEdit2.json.jbuilder
|
|
481
|
+
- app/views/json_ui/garage/test_page/panels_bulkEdit3.json.jbuilder
|
|
479
482
|
- app/views/json_ui/garage/test_page/popovers.json.jbuilder
|
|
480
483
|
- app/views/json_ui/garage/test_page/progressCircle.json.jbuilder
|
|
481
484
|
- app/views/json_ui/garage/test_page/responsive.json.jbuilder
|
|
@@ -558,6 +561,8 @@ files:
|
|
|
558
561
|
- lib/glib/json_crawler/coverage.rb
|
|
559
562
|
- lib/glib/json_crawler/http.rb
|
|
560
563
|
- lib/glib/json_crawler/router.rb
|
|
564
|
+
- lib/glib/json_ui/config.rb
|
|
565
|
+
- lib/glib/json_ui/name.rb
|
|
561
566
|
- lib/glib/last_resort_unit_test.rb
|
|
562
567
|
- lib/glib/mailer_tester.rb
|
|
563
568
|
- lib/glib/non_http_integration_test.rb
|
|
@@ -575,8 +580,10 @@ files:
|
|
|
575
580
|
- lib/glib/time_returning_mailer.rb
|
|
576
581
|
- lib/glib/value.rb
|
|
577
582
|
- lib/tasks/db.rake
|
|
583
|
+
homepage:
|
|
578
584
|
licenses: []
|
|
579
585
|
metadata: {}
|
|
586
|
+
post_install_message:
|
|
580
587
|
rdoc_options: []
|
|
581
588
|
require_paths:
|
|
582
589
|
- lib
|
|
@@ -591,7 +598,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
591
598
|
- !ruby/object:Gem::Version
|
|
592
599
|
version: '0'
|
|
593
600
|
requirements: []
|
|
594
|
-
rubygems_version: 4.
|
|
601
|
+
rubygems_version: 3.4.6
|
|
602
|
+
signing_key:
|
|
595
603
|
specification_version: 4
|
|
596
604
|
summary: ''
|
|
597
605
|
test_files: []
|