scrivito_sdk 1.16.0 → 1.17.0.rc1
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/README.md +1 -1
- data/app/cms/scrivito/attribute_serializer.rb +2 -0
- data/app/cms/scrivito/basic_obj.rb +58 -24
- data/app/cms/scrivito/basic_widget.rb +36 -14
- data/app/cms/scrivito/cms_backend.rb +1 -1
- data/app/cms/scrivito/cms_field_tag.rb +2 -2
- data/app/cms/scrivito/cms_rest_api.rb +7 -4
- data/app/cms/scrivito/cms_rest_api/widget_extractor.rb +42 -27
- data/app/cms/scrivito/controller_actions.rb +1 -1
- data/app/cms/scrivito/editing_context_middleware.rb +2 -4
- data/app/cms/scrivito/layout_tags.rb +3 -3
- data/app/cms/scrivito/migrations/migration_store.rb +2 -2
- data/app/cms/scrivito/obj_collection.rb +5 -5
- data/app/cms/scrivito/obj_search_enumerator.rb +47 -0
- data/app/cms/scrivito/page_config.rb +2 -2
- data/app/cms/scrivito/sdk_engine.rb +1 -1
- data/app/cms/scrivito/ui_config.rb +3 -2
- data/app/controllers/scrivito/objs_controller.rb +2 -2
- data/app/controllers/scrivito/ui_controller.rb +2 -2
- data/config/ca-bundle.crt +143 -78
- data/lib/assets/javascripts/scrivito.js +1 -1
- data/lib/assets/javascripts/scrivito_ui_redirect.js +1 -1
- data/lib/assets/javascripts/scrivito_with_js_sdk.js +58 -107
- data/lib/assets/stylesheets/scrivito_editing.css +1 -1
- metadata +8 -50
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 51526b37bad3d471777ad7fffc844651cb89374e653ef1db39f13e01629373ed
|
4
|
+
data.tar.gz: 8e0e6d162678e41110d374798dc7f1c0cbb46bf8bdd47d2596cf3b51fdb94b33
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3738673cb784d53bdf76e76d82b211a326ed0857a4c72cb4a6f94edd5679238a6b7d594d85039989322e28cdbf15034754fdb1acd26416f9f9d3e886b9bcf909
|
7
|
+
data.tar.gz: 574ab479d5a0f01797f85c3be7571ae061dad11d634cd2e2d0d555ebc49fa03cf832dbde5e3cd8a3f6c8d3ec7b97a19308e2ded17366336364bb925cf45f308a
|
data/README.md
CHANGED
@@ -12,7 +12,7 @@ of concepts and APIs, tutorials, and release notes.
|
|
12
12
|
|
13
13
|
## License
|
14
14
|
|
15
|
-
Copyright (c) 2009 -
|
15
|
+
Copyright (c) 2009 - 2020 Infopark Group GmbH (http://www.infopark.com)
|
16
16
|
|
17
17
|
This software can be used and modified under the LGPL-3.0. Please refer to
|
18
18
|
http://www.gnu.org/licenses/lgpl-3.0.html for the license text.
|
@@ -29,6 +29,8 @@ class AttributeSerializer < Struct.new(:obj_class, :obj_id)
|
|
29
29
|
_permalink
|
30
30
|
].include?(attribute_name)
|
31
31
|
[attribute_name, attribute_value.to_s]
|
32
|
+
elsif attribute_name == '_restriction'
|
33
|
+
[attribute_name, attribute_value]
|
32
34
|
else
|
33
35
|
raise_unknown_attribute(attribute_name)
|
34
36
|
end
|
@@ -1,4 +1,3 @@
|
|
1
|
-
require 'json'
|
2
1
|
require 'ostruct'
|
3
2
|
require 'active_model/naming'
|
4
3
|
|
@@ -25,15 +24,18 @@ module Scrivito
|
|
25
24
|
PublicSystemAttributeDefinition = Class.new(AttributeDefinition)
|
26
25
|
|
27
26
|
SYSTEM_ATTRIBUTES = AttributeDefinitionCollection.new(
|
28
|
-
'_id'
|
29
|
-
'
|
30
|
-
'
|
31
|
-
'
|
32
|
-
'
|
33
|
-
|
34
|
-
'
|
35
|
-
'
|
36
|
-
|
27
|
+
'_id' => PublicSystemAttributeDefinition.new(:_id, :string),
|
28
|
+
'_created_at' => PublicSystemAttributeDefinition.new(:_created_at, :date),
|
29
|
+
'_last_changed' => PublicSystemAttributeDefinition.new(:_last_changed, :date),
|
30
|
+
'_first_published_at' => PublicSystemAttributeDefinition.new(:_first_published_at, :date),
|
31
|
+
'_published_at' => PublicSystemAttributeDefinition.new(:_published_at, :date),
|
32
|
+
'_obj_class' => PublicSystemAttributeDefinition.new(:_obj_class, :string),
|
33
|
+
'_path' => PublicSystemAttributeDefinition.new(:_path, :string),
|
34
|
+
'_permalink' => PublicSystemAttributeDefinition.new(:_permalink, :string),
|
35
|
+
|
36
|
+
'_conflicts' => AttributeDefinition.new(:_conflicts, nil),
|
37
|
+
'_modification' => AttributeDefinition.new(:_modification, nil),
|
38
|
+
'_widget_pool' => AttributeDefinition.new(:_widget_pool, nil),
|
37
39
|
)
|
38
40
|
|
39
41
|
UNIQ_ATTRIBUTES = %w[
|
@@ -44,7 +46,10 @@ module Scrivito
|
|
44
46
|
|
45
47
|
GENERATED_ATTRIBUTES = %w[
|
46
48
|
_conflicts
|
49
|
+
_created_at
|
47
50
|
_last_changed
|
51
|
+
_first_published_at
|
52
|
+
_published_at
|
48
53
|
_modification
|
49
54
|
_obj_type
|
50
55
|
_text_links
|
@@ -138,14 +143,14 @@ module Scrivito
|
|
138
143
|
else
|
139
144
|
attributes = build_attributes_with_defaults(attributes, context)
|
140
145
|
attributes = prepare_attributes_for_instantiation(attributes)
|
141
|
-
|
146
|
+
extraction, api_attributes = prepare_attributes_for_rest_api(attributes)
|
142
147
|
|
143
148
|
workspace = Workspace.current
|
144
149
|
obj_data = workspace.create_obj(obj: api_attributes)
|
145
150
|
obj = BasicObj.instantiate(obj_data)
|
146
151
|
obj.revision = workspace.revision
|
147
152
|
|
148
|
-
|
153
|
+
extraction.notify_persisted(obj)
|
149
154
|
obj
|
150
155
|
end
|
151
156
|
end
|
@@ -301,7 +306,7 @@ module Scrivito
|
|
301
306
|
# @api public
|
302
307
|
def self.find_by_path(path)
|
303
308
|
Workspace.current.objs.find_one_by(:path, path,
|
304
|
-
|
309
|
+
type_computer.obj_class_name_for_type(self))
|
305
310
|
end
|
306
311
|
|
307
312
|
# Find an {Scrivito::BasicObj Obj} with the given name.
|
@@ -329,7 +334,7 @@ module Scrivito
|
|
329
334
|
# @api public
|
330
335
|
def self.find_by_permalink(permalink)
|
331
336
|
Workspace.current.objs.find_one_by(:permalink, permalink,
|
332
|
-
|
337
|
+
type_computer.obj_class_name_for_type(self))
|
333
338
|
end
|
334
339
|
|
335
340
|
# Returns the {Scrivito::BasicObj Obj} with the given permalink, or raises
|
@@ -414,9 +419,9 @@ module Scrivito
|
|
414
419
|
# }
|
415
420
|
# )
|
416
421
|
def update(attributes)
|
417
|
-
|
422
|
+
extraction, api_attributes = prepare_attributes_for_rest_api(attributes)
|
418
423
|
update_data(workspace.update_obj(id, obj: api_attributes))
|
419
|
-
|
424
|
+
extraction.notify_persisted(self)
|
420
425
|
self
|
421
426
|
end
|
422
427
|
|
@@ -633,7 +638,7 @@ module Scrivito
|
|
633
638
|
return [] if binary?
|
634
639
|
toclist = children
|
635
640
|
toclist = toclist.reject { |toc| toc.binary? } unless args.include?(:all)
|
636
|
-
toclist
|
641
|
+
toclist.sort_by(&:id)
|
637
642
|
end
|
638
643
|
|
639
644
|
# @api public
|
@@ -659,13 +664,23 @@ module Scrivito
|
|
659
664
|
SYSTEM_KEYS = Set.new(%w[
|
660
665
|
body
|
661
666
|
_id
|
667
|
+
_created_at
|
662
668
|
_last_changed
|
669
|
+
_first_published_at
|
670
|
+
_published_at
|
663
671
|
_path
|
664
672
|
_permalink
|
665
673
|
_obj_class
|
666
674
|
title
|
667
675
|
])
|
668
676
|
|
677
|
+
DATE_KEYS = Set.new(%w[
|
678
|
+
_created_at
|
679
|
+
_last_changed
|
680
|
+
_first_published_at
|
681
|
+
_published_at
|
682
|
+
])
|
683
|
+
|
669
684
|
# Returns the value of a system or custom attribute specified by its name.
|
670
685
|
# Passing an invalid key will not raise an error but return +nil+.
|
671
686
|
# @api public
|
@@ -704,11 +719,30 @@ module Scrivito
|
|
704
719
|
}
|
705
720
|
end
|
706
721
|
|
722
|
+
# @return [nil, ActiveSupport::TimeWithZone]
|
723
|
+
# @api public
|
724
|
+
def created_at
|
725
|
+
read_attribute('_created_at')
|
726
|
+
end
|
727
|
+
|
728
|
+
# @return [ActiveSupport::TimeWithZone]
|
707
729
|
# @api public
|
708
730
|
def last_changed
|
709
731
|
read_attribute('_last_changed')
|
710
732
|
end
|
711
733
|
|
734
|
+
# @return [nil, ActiveSupport::TimeWithZone]
|
735
|
+
# @api public
|
736
|
+
def first_published_at
|
737
|
+
read_attribute('_first_published_at')
|
738
|
+
end
|
739
|
+
|
740
|
+
# @return [nil, ActiveSupport::TimeWithZone]
|
741
|
+
# @api public
|
742
|
+
def published_at
|
743
|
+
read_attribute('_published_at')
|
744
|
+
end
|
745
|
+
|
712
746
|
def new?(revision=workspace.base_revision)
|
713
747
|
quick_modification(revision) == "new"
|
714
748
|
end
|
@@ -991,15 +1025,16 @@ module Scrivito
|
|
991
1025
|
end
|
992
1026
|
|
993
1027
|
def self.prepare_attributes_for_rest_api(attributes, obj = nil)
|
994
|
-
|
1028
|
+
extraction = CmsRestApi::WidgetExtractor.call(attributes, obj)
|
1029
|
+
obj_attributes = extraction.attributes
|
995
1030
|
obj_id = obj ? obj.id : obj_attributes.fetch(:_id)
|
996
1031
|
workspace = obj ? obj.revision.workspace : Workspace.current
|
997
1032
|
|
998
1033
|
api_attributes = serialize_attributes(
|
999
|
-
obj_attributes, widget_pool_attributes, workspace, obj_id
|
1034
|
+
obj_attributes, extraction.widget_pool_attributes, workspace, obj_id
|
1000
1035
|
)
|
1001
1036
|
|
1002
|
-
[
|
1037
|
+
[extraction, api_attributes]
|
1003
1038
|
end
|
1004
1039
|
|
1005
1040
|
def self.find_attribute_definitions(obj_class, basic_class = self)
|
@@ -1025,7 +1060,7 @@ module Scrivito
|
|
1025
1060
|
|
1026
1061
|
private_class_method def self.serialize_widget_pool_attributes(serializer, widget_pool_attributes)
|
1027
1062
|
{}.tap do |serialized_attributes|
|
1028
|
-
widget_pool_attributes.
|
1063
|
+
widget_pool_attributes.each_value do |(widget, widget_attributes)|
|
1029
1064
|
obj_class = widget_attributes['_obj_class']
|
1030
1065
|
widget_serializer = AttributeSerializer.new(obj_class, serializer.obj_id)
|
1031
1066
|
|
@@ -1066,8 +1101,7 @@ module Scrivito
|
|
1066
1101
|
|
1067
1102
|
def instantiate_widget(widget_id, widget_data)
|
1068
1103
|
BasicWidget.instantiate(widget_data).tap do |widget|
|
1069
|
-
widget.
|
1070
|
-
widget.obj = self
|
1104
|
+
widget.attach_to_obj(self, widget_id)
|
1071
1105
|
end
|
1072
1106
|
end
|
1073
1107
|
|
@@ -1151,7 +1185,7 @@ module Scrivito
|
|
1151
1185
|
|
1152
1186
|
def value_of_system_attribute(attribute_name)
|
1153
1187
|
attribute_value = data_from_cms.value_of(attribute_name)
|
1154
|
-
if attribute_name
|
1188
|
+
if DATE_KEYS.include?(attribute_name)
|
1155
1189
|
DateConversion.deserialize_from_backend(attribute_value)
|
1156
1190
|
else
|
1157
1191
|
attribute_value
|
@@ -95,8 +95,6 @@ class BasicWidget
|
|
95
95
|
|
96
96
|
attr_accessor :container, :container_attribute_name
|
97
97
|
|
98
|
-
attr_writer :obj, :id
|
99
|
-
|
100
98
|
attr_reader :attributes_to_be_saved
|
101
99
|
|
102
100
|
delegate :widget_from_pool, to: :obj
|
@@ -221,12 +219,29 @@ class BasicWidget
|
|
221
219
|
@attributes_to_be_saved.nil?
|
222
220
|
end
|
223
221
|
|
222
|
+
def attach_to_obj(an_obj, widget_id = compute_widget_id(an_obj))
|
223
|
+
@obj = an_obj
|
224
|
+
@id = widget_id
|
225
|
+
|
226
|
+
attributes_to_be_saved
|
227
|
+
end
|
228
|
+
|
224
229
|
def ==(other)
|
225
|
-
|
230
|
+
eql?(other)
|
226
231
|
end
|
227
232
|
|
228
233
|
def eql?(other)
|
229
|
-
|
234
|
+
other.respond_to?(:equality_identifier, true) &&
|
235
|
+
equality_identifier.eql?(other.equality_identifier)
|
236
|
+
end
|
237
|
+
|
238
|
+
def hash
|
239
|
+
unless persisted?
|
240
|
+
# An unbound widget will likely be attached soon, which mutates its #hash value. Evil.
|
241
|
+
raise ScrivitoError, 'a new widget is not allowed to be used as a hash key'
|
242
|
+
end
|
243
|
+
|
244
|
+
equality_identifier.hash
|
230
245
|
end
|
231
246
|
|
232
247
|
#
|
@@ -262,7 +277,7 @@ class BasicWidget
|
|
262
277
|
{ obj: {_widget_pool: {id => previous_widget_content}} })
|
263
278
|
reload
|
264
279
|
else
|
265
|
-
raise ScrivitoError, "cannot revert changes, since widget is #{modification}
|
280
|
+
raise ScrivitoError, "cannot revert changes, since widget is #{modification}"
|
266
281
|
end
|
267
282
|
end
|
268
283
|
|
@@ -297,14 +312,6 @@ class BasicWidget
|
|
297
312
|
end
|
298
313
|
end
|
299
314
|
|
300
|
-
def hash
|
301
|
-
if @obj && @id
|
302
|
-
(id + obj.id).hash
|
303
|
-
else
|
304
|
-
super
|
305
|
-
end
|
306
|
-
end
|
307
|
-
|
308
315
|
# Returns the entity ({Scrivito::BasicObj} or {Scrivito::BasicWidget})
|
309
316
|
# that references this widget.
|
310
317
|
# @api public
|
@@ -360,7 +367,8 @@ class BasicWidget
|
|
360
367
|
end
|
361
368
|
end
|
362
369
|
|
363
|
-
def
|
370
|
+
def notify_persisted(obj)
|
371
|
+
@obj = obj
|
364
372
|
@attributes_to_be_saved = nil
|
365
373
|
reload_data
|
366
374
|
end
|
@@ -398,6 +406,12 @@ class BasicWidget
|
|
398
406
|
referenced + referenced.map { |widget| widget.contained_widgets }.flatten
|
399
407
|
end
|
400
408
|
|
409
|
+
protected
|
410
|
+
|
411
|
+
def equality_identifier
|
412
|
+
[id, obj.id]
|
413
|
+
end
|
414
|
+
|
401
415
|
private
|
402
416
|
|
403
417
|
def workspace
|
@@ -433,6 +447,14 @@ class BasicWidget
|
|
433
447
|
def value_of_system_attribute(attribute_name)
|
434
448
|
data_from_cms.value_of(attribute_name)
|
435
449
|
end
|
450
|
+
|
451
|
+
def compute_widget_id(obj)
|
452
|
+
@attributes_to_be_saved.delete('_id') || generate_widget_id(obj)
|
453
|
+
end
|
454
|
+
|
455
|
+
def generate_widget_id(obj)
|
456
|
+
obj.present? ? obj.generate_widget_pool_id : BasicObj.generate_widget_pool_id
|
457
|
+
end
|
436
458
|
end
|
437
459
|
|
438
460
|
end
|
@@ -206,7 +206,7 @@ module Scrivito
|
|
206
206
|
private_class_method :blob_data_cache_key
|
207
207
|
|
208
208
|
def self.search_params_cache_key(params)
|
209
|
-
|
209
|
+
JSON.generate(normalize_search_param(params))
|
210
210
|
end
|
211
211
|
private_class_method :search_params_cache_key
|
212
212
|
|
@@ -67,7 +67,7 @@ class CmsFieldTag < Struct.new(:view, :tag_name, :obj_or_widget, :editing_option
|
|
67
67
|
if FIELD_TYPES_WITH_ORIGINAL_CONTENT.include?(field_type)
|
68
68
|
original_value = view.scrivito_value(current_value)
|
69
69
|
original_content = original_content(field_type, original_value, current_value)
|
70
|
-
encoded_content = Base64.strict_encode64(
|
70
|
+
encoded_content = Base64.strict_encode64(JSON.generate(original_content))
|
71
71
|
options['private-field-original-content'] = encoded_content
|
72
72
|
end
|
73
73
|
|
@@ -161,7 +161,7 @@ class CmsFieldTag < Struct.new(:view, :tag_name, :obj_or_widget, :editing_option
|
|
161
161
|
end
|
162
162
|
|
163
163
|
def valid_values
|
164
|
-
|
164
|
+
JSON.generate(obj_or_widget.attribute_definitions[field_name].try(:values) || [])
|
165
165
|
end
|
166
166
|
|
167
167
|
def assert_valid_attribute
|
@@ -123,7 +123,7 @@ module Scrivito
|
|
123
123
|
timer)
|
124
124
|
request = method_to_net_http_class(method).new(path(resource_path))
|
125
125
|
set_headers(request)
|
126
|
-
request.body =
|
126
|
+
request.body = JSON.generate(payload) if payload.present?
|
127
127
|
|
128
128
|
response = retry_once_on_network_error(method, timer) do
|
129
129
|
CmsRestApi::RateLimit.retry_on_rate_limit(timer) do
|
@@ -138,12 +138,12 @@ module Scrivito
|
|
138
138
|
private_class_method def self.handle_response(resource_path, response)
|
139
139
|
http_code = response.code.to_i
|
140
140
|
if response.code.start_with?('2')
|
141
|
-
|
141
|
+
JSON.parse(response.body)
|
142
142
|
elsif response.code == '403'
|
143
143
|
raise AccessDenied.new(response.body)
|
144
144
|
else
|
145
145
|
begin
|
146
|
-
error_body =
|
146
|
+
error_body = JSON.parse(response.body)
|
147
147
|
specific_output = error_body['error']
|
148
148
|
|
149
149
|
if response.code.start_with?('4')
|
@@ -155,7 +155,7 @@ module Scrivito
|
|
155
155
|
else # 3xx and >500 are treated as NetworkErrors
|
156
156
|
raise NetworkError.new(response.body, http_code)
|
157
157
|
end
|
158
|
-
rescue
|
158
|
+
rescue JSON::ParserError, TypeError
|
159
159
|
raise NetworkError.new(response.body, http_code)
|
160
160
|
end
|
161
161
|
end
|
@@ -170,6 +170,9 @@ module Scrivito
|
|
170
170
|
message = task_data["message"] ||
|
171
171
|
"Missing error message in task response #{task_data}"
|
172
172
|
|
173
|
+
if task_data['status'] == 'exception'
|
174
|
+
raise BackendError.new(message, 500)
|
175
|
+
end
|
173
176
|
backend_code = task_data['code']
|
174
177
|
raise ClientError.new(message, backend_code: backend_code)
|
175
178
|
end
|
@@ -1,53 +1,68 @@
|
|
1
1
|
module Scrivito
|
2
2
|
class CmsRestApi
|
3
3
|
class WidgetExtractor
|
4
|
-
|
5
4
|
# Extract widgets from a given attribute tree
|
6
5
|
# first return param: a flat hash of widget_like => updated widget attributes
|
7
6
|
# second return param: the updated attributes
|
8
7
|
def self.call(attributes, obj=nil)
|
8
|
+
new(obj).extract_widgets(attributes)
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(obj)
|
12
|
+
@obj = obj
|
13
|
+
end
|
14
|
+
|
15
|
+
def extract_widgets(attributes)
|
16
|
+
@extracted_widgets = {}
|
9
17
|
updated_attributes = attributes.dup
|
10
|
-
|
18
|
+
widget_pool = updated_attributes.delete(:_widget_pool)
|
19
|
+
extract_widgets_from_pool(widget_pool) if widget_pool
|
20
|
+
extract_widgets_from_attributes(updated_attributes)
|
11
21
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
22
|
+
Extraction.new(extracted_widgets, updated_attributes)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
attr_reader :obj
|
28
|
+
attr_reader :extracted_widgets
|
29
|
+
|
30
|
+
def extract_widgets_from_pool(widget_pool)
|
31
|
+
widget_pool.each do |widget, widget_properties|
|
32
|
+
extracted_widgets[widget.id] = [widget, widget_properties]
|
33
|
+
extract_widgets_from_attributes(widget_properties)
|
18
34
|
end
|
35
|
+
end
|
19
36
|
|
20
|
-
|
37
|
+
def extract_widgets_from_attributes(attributes)
|
38
|
+
attributes.each do |attribute_name, value|
|
21
39
|
if value.is_a?(Array) && value.all? { |w| w.is_a?(BasicWidget) }
|
22
40
|
value.each do |widget|
|
23
41
|
unless widget.persisted?
|
24
|
-
|
25
|
-
extracted_widgets[widget] = widget
|
26
|
-
|
42
|
+
widget_attributes = widget.attach_to_obj(obj)
|
43
|
+
extracted_widgets[widget.id] = [widget, widget_attributes]
|
44
|
+
extract_widgets_from_attributes(widget_attributes)
|
27
45
|
end
|
28
46
|
end
|
29
47
|
end
|
30
48
|
end
|
31
|
-
|
32
|
-
return extracted_widgets, updated_attributes
|
33
49
|
end
|
34
50
|
|
35
|
-
|
36
|
-
|
37
|
-
widget.obj = obj
|
38
|
-
end
|
51
|
+
class Extraction
|
52
|
+
attr_reader :widget_pool_attributes, :attributes
|
39
53
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
widget.obj = obj
|
44
|
-
widget.forget_unsaved_attributes
|
45
|
-
end
|
54
|
+
def initialize(widget_pool_attributes, attributes)
|
55
|
+
@widget_pool_attributes = widget_pool_attributes
|
56
|
+
@attributes = attributes
|
46
57
|
end
|
47
|
-
end
|
48
58
|
|
49
|
-
|
50
|
-
|
59
|
+
def notify_persisted(obj)
|
60
|
+
widget_pool_attributes.each_value do |(widget, attributes)|
|
61
|
+
if attributes.present?
|
62
|
+
widget.notify_persisted(obj)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
51
66
|
end
|
52
67
|
end
|
53
68
|
end
|