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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7a88117fbaf57f748a21abf1ea95fc18fdec09171c7b93e87ac8ef5e02850f93
4
- data.tar.gz: d37adcb5e04c61400b5f073ada5a65d0cd5e5f39b62df49c3a28c93c93b06a41
3
+ metadata.gz: e178f9febdf54e054c6415f98e2dba1168d13ca1eea53c3a2e4c7aba62a434e9
4
+ data.tar.gz: f882a75e2b6c4c56b09b02419eb67b08e3d771958f5d3445f51ebbd93ffba806
5
5
  SHA512:
6
- metadata.gz: a47b8e5826f8ac505d0e38b4530a9294140dcd8e5280178c636e2abe78d38d4c323a04d02e0915aec9bcaf73441fecb90d5d0f89650059e7cdc9a7658f737b56
7
- data.tar.gz: 7c8ce8df263b855e5a09cd4e42d5e0bf039c0b24d9d08b1f6f6767e5d8857f4cd33d1f9b5a272f3a4c8b8aa16291ff26c57f4dfa350d8881d5b79129dd77f2be
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
- # browser-side reports are distinguishable from server exceptions;
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
- "JS: #{params[:message]}",
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 "Multiple actions called on a singleton ActionBuilder. " \
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
- "chain actions using `onClose` callbacks."
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
- # `components_set`/`logics_set` with `width`/`height`.
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
- class Table2 < View
456
- attr_reader :template
457
-
458
- hash :nextPage
459
- action :onScrollToTop
460
- action :onScrollToBottom
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
- # bulkEdit2 is a form PEER, not a form child: it answers the same
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 -- the bulkEdit2 shape):
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.
@@ -382,12 +382,6 @@ module Glib
382
382
  end
383
383
  end
384
384
 
385
- class Datetime < View
386
- date_time :value
387
- string :format
388
- bool :tick
389
- end
390
-
391
385
  class ProgressBar < View
392
386
  float :value
393
387
  color :color
@@ -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, but they are no longer consulted for the skip decision.
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
- self.attributes_before_save = attributes_in_database
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
- unless same_as_before?(calculated_diff)
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), never from the instance-local `snapshot_changed`
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). For a first snapshot
194
- # (no previous version) the diff runs current state against `attributes_before_save || {}`.
195
- def same_as_before?(computed_diff = diff)
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.panels_bulkEdit2 \
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', importable: true, tooltip: { text: 'Month', placement: 'top' } },
47
- { text: 'Electricity usage', id: 'electricity_usage', importable: true },
48
- { text: 'Gas usage', id: 'gas_usage', importable: true },
49
- { text: 'Sources', id: 'sources', importable: true },
50
- { text: 'Compliant', id: 'compliant', importable: true }
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
@@ -56,7 +56,6 @@ nav_groups = {
56
56
  'flow',
57
57
  'grid',
58
58
  'horizontal',
59
- 'panels_bulkEdit2',
60
59
  'pagination',
61
60
  'responsive',
62
61
  'scroll',
@@ -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: nil }
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: bulkEdit2’s skeleton plus config-driven file parsing, row gates, read-only columns, view chips and submission extras.'
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 (the bulkEdit2 shape). maxRows 3 rejects oversized files whole.'
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
- url_primary = 'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf'
14
- url_alt = 'https://example.com'
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: url_primary
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: 'PDF', onClick: ->(action) do
30
- action.logics_set targetId: 'web_main', data: { url: url_primary }
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: 'Example.com', onClick: ->(action) do
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: url_primary
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/bulkEdit2', 'panels/bulkEdit3'
19
+ when 'panels/bulkEdit3'
20
20
  method = 'post'
21
21
  action = "forms/#{method}"
22
22
  url = form['import']['submitUrl']
@@ -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. `panels/responsive`
7
- # (default) vs `panels_responsive`.
6
+ # component/action name in the generated JSON, e.g. `panels_responsive`
7
+ # (default since 7.0) vs `panels/responsive`.
8
8
  #
9
- # The default stays `/` for backwards compatibility; `_` becomes the default
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 flipping this is a
16
- # server-side-only change.
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. Stays '/' until the next major version, when it
21
- # becomes '_'. Apps opt in early via `Config.name_separator = '_'`.
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
@@ -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.0.1.beta1
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: 1980-01-02 00:00:00.000000000 Z
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.0.16
627
+ rubygems_version: 3.4.6
628
+ signing_key:
625
629
  specification_version: 4
626
630
  summary: ''
627
631
  test_files: []
@@ -1,14 +0,0 @@
1
- class Glib::JsonUi::ActionBuilder
2
- module GlobalStates
3
- # Experimental
4
- class Watch < Action
5
- string :state
6
- action :onChange
7
- end
8
-
9
- class Set < Action
10
- string :state
11
- hash :data
12
- end
13
- end
14
- end
@@ -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
- )