iknow_view_models 2.9.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/iknow_view_models.gemspec +1 -1
- data/lib/iknow_view_models/version.rb +1 -1
- data/lib/view_model/active_record/association_data.rb +206 -92
- data/lib/view_model/active_record/association_manipulation.rb +22 -12
- data/lib/view_model/active_record/cache/cacheable_view.rb +3 -13
- data/lib/view_model/active_record/cache.rb +2 -2
- data/lib/view_model/active_record/cloner.rb +11 -11
- data/lib/view_model/active_record/controller.rb +0 -2
- data/lib/view_model/active_record/update_context.rb +21 -3
- data/lib/view_model/active_record/update_data.rb +43 -45
- data/lib/view_model/active_record/update_operation.rb +265 -153
- data/lib/view_model/active_record/visitor.rb +9 -6
- data/lib/view_model/active_record.rb +94 -74
- data/lib/view_model/after_transaction_runner.rb +3 -18
- data/lib/view_model/changes.rb +24 -16
- data/lib/view_model/config.rb +6 -2
- data/lib/view_model/deserialization_error.rb +31 -0
- data/lib/view_model/deserialize_context.rb +2 -6
- data/lib/view_model/error_view.rb +6 -5
- data/lib/view_model/record/attribute_data.rb +11 -6
- data/lib/view_model/record.rb +44 -24
- data/lib/view_model/serialize_context.rb +2 -63
- data/lib/view_model.rb +17 -8
- data/shell.nix +1 -1
- data/test/helpers/arvm_test_utilities.rb +6 -0
- data/test/helpers/controller_test_helpers.rb +5 -3
- data/test/helpers/viewmodel_spec_helpers.rb +63 -52
- data/test/unit/view_model/access_control_test.rb +88 -37
- data/test/unit/view_model/active_record/belongs_to_test.rb +110 -178
- data/test/unit/view_model/active_record/cache_test.rb +3 -2
- data/test/unit/view_model/active_record/cloner_test.rb +1 -1
- data/test/unit/view_model/active_record/controller_test.rb +12 -20
- data/test/unit/view_model/active_record/has_many_test.rb +540 -316
- data/test/unit/view_model/active_record/has_many_through_poly_test.rb +12 -15
- data/test/unit/view_model/active_record/has_many_through_test.rb +15 -58
- data/test/unit/view_model/active_record/has_one_test.rb +288 -135
- data/test/unit/view_model/active_record/poly_test.rb +0 -1
- data/test/unit/view_model/active_record/shared_test.rb +21 -39
- data/test/unit/view_model/active_record/version_test.rb +3 -2
- data/test/unit/view_model/active_record_test.rb +5 -63
- data/test/unit/view_model/callbacks_test.rb +1 -0
- data/test/unit/view_model/record_test.rb +0 -32
- data/test/unit/view_model/traversal_context_test.rb +13 -12
- metadata +5 -8
- data/test/unit/view_model/active_record/optional_attribute_view_test.rb +0 -58
@@ -40,10 +40,6 @@ class ViewModel::ActiveRecord
|
|
40
40
|
end
|
41
41
|
end
|
42
42
|
|
43
|
-
def deferred?
|
44
|
-
viewmodel.nil?
|
45
|
-
end
|
46
|
-
|
47
43
|
def built?
|
48
44
|
@built
|
49
45
|
end
|
@@ -112,9 +108,8 @@ class ViewModel::ActiveRecord
|
|
112
108
|
if child_operation
|
113
109
|
child_ctx = viewmodel.context_for_child(association_data.association_name, context: deserialize_context)
|
114
110
|
child_viewmodel = child_operation.run!(deserialize_context: child_ctx)
|
115
|
-
|
116
|
-
|
117
|
-
end
|
111
|
+
propagate_tree_changes(association_data, child_viewmodel.previous_changes)
|
112
|
+
|
118
113
|
child_viewmodel.model
|
119
114
|
end
|
120
115
|
association.writer(new_target)
|
@@ -153,9 +148,8 @@ class ViewModel::ActiveRecord
|
|
153
148
|
if child_operation
|
154
149
|
ViewModel::Utils.map_one_or_many(child_operation) do |op|
|
155
150
|
child_viewmodel = op.run!(deserialize_context: child_ctx)
|
156
|
-
|
157
|
-
|
158
|
-
end
|
151
|
+
propagate_tree_changes(association_data, child_viewmodel.previous_changes)
|
152
|
+
|
159
153
|
child_viewmodel.model
|
160
154
|
end
|
161
155
|
end
|
@@ -183,7 +177,11 @@ class ViewModel::ActiveRecord
|
|
183
177
|
child_hook_control.record_changes(changes)
|
184
178
|
end
|
185
179
|
|
186
|
-
|
180
|
+
if child_association_data.nested?
|
181
|
+
viewmodel.nested_children_changed!
|
182
|
+
elsif child_association_data.owned?
|
183
|
+
viewmodel.referenced_children_changed!
|
184
|
+
end
|
187
185
|
end
|
188
186
|
debug "<- #{debug_name}: Finished checking released children permissions"
|
189
187
|
end
|
@@ -208,9 +206,18 @@ class ViewModel::ActiveRecord
|
|
208
206
|
raise ViewModel::DeserializationError::DatabaseConstraint.from_exception(ex, blame_reference)
|
209
207
|
end
|
210
208
|
|
209
|
+
def propagate_tree_changes(association_data, child_changes)
|
210
|
+
if association_data.nested?
|
211
|
+
viewmodel.nested_children_changed! if child_changes.changed_nested_tree?
|
212
|
+
viewmodel.referenced_children_changed! if child_changes.changed_referenced_children?
|
213
|
+
elsif association_data.owned?
|
214
|
+
viewmodel.referenced_children_changed! if child_changes.changed_owned_tree?
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
211
218
|
# Recursively builds UpdateOperations for the associations in our UpdateData
|
212
219
|
def build!(update_context)
|
213
|
-
raise ViewModel::DeserializationError::Internal.new("Internal error: UpdateOperation cannot build a deferred update") if
|
220
|
+
raise ViewModel::DeserializationError::Internal.new("Internal error: UpdateOperation cannot build a deferred update") if viewmodel.nil?
|
214
221
|
return self if built?
|
215
222
|
|
216
223
|
update_data.associations.each do |association_name, association_update_data|
|
@@ -231,8 +238,10 @@ class ViewModel::ActiveRecord
|
|
231
238
|
update =
|
232
239
|
if association_data.through?
|
233
240
|
build_updates_for_collection_referenced_association(association_data, reference_string, update_context)
|
241
|
+
elsif association_data.collection?
|
242
|
+
build_updates_for_collection_association(association_data, reference_string, update_context)
|
234
243
|
else
|
235
|
-
|
244
|
+
build_update_for_single_association(association_data, reference_string, update_context)
|
236
245
|
end
|
237
246
|
|
238
247
|
add_update(association_data, update)
|
@@ -254,38 +263,6 @@ class ViewModel::ActiveRecord
|
|
254
263
|
|
255
264
|
private
|
256
265
|
|
257
|
-
def build_update_for_single_referenced_association(association_data, reference_string, update_context)
|
258
|
-
# TODO intern loads for shared items so we only load them once
|
259
|
-
model = self.viewmodel.model
|
260
|
-
previous_child_viewmodel = model.public_send(association_data.direct_reflection.name).try do |previous_child_model|
|
261
|
-
vm_class = association_data.viewmodel_class_for_model!(previous_child_model.class)
|
262
|
-
vm_class.new(previous_child_model)
|
263
|
-
end
|
264
|
-
|
265
|
-
if reference_string.nil?
|
266
|
-
referred_update = nil
|
267
|
-
referred_viewmodel = nil
|
268
|
-
else
|
269
|
-
referred_update = update_context.resolve_reference(reference_string, blame_reference)
|
270
|
-
referred_viewmodel = referred_update.viewmodel
|
271
|
-
|
272
|
-
unless association_data.accepts?(referred_viewmodel.class)
|
273
|
-
raise ViewModel::DeserializationError::InvalidAssociationType.new(
|
274
|
-
association_data.association_name.to_s,
|
275
|
-
referred_viewmodel.class.view_name,
|
276
|
-
blame_reference)
|
277
|
-
end
|
278
|
-
|
279
|
-
referred_update.build!(update_context)
|
280
|
-
end
|
281
|
-
|
282
|
-
if previous_child_viewmodel != referred_viewmodel
|
283
|
-
viewmodel.association_changed!(association_data.association_name)
|
284
|
-
end
|
285
|
-
|
286
|
-
referred_update
|
287
|
-
end
|
288
|
-
|
289
266
|
# Resolve or construct viewmodels for incoming update data. Where a child
|
290
267
|
# hash references an existing model not currently attached to this parent,
|
291
268
|
# it must be found before recursing into that child. If the model is
|
@@ -321,6 +298,60 @@ class ViewModel::ActiveRecord
|
|
321
298
|
end
|
322
299
|
end
|
323
300
|
|
301
|
+
def resolve_referenced_viewmodels(association_data, update_datas, previous_child_viewmodels, update_context)
|
302
|
+
previous_child_viewmodels = Array.wrap(previous_child_viewmodels).index_by(&:to_reference)
|
303
|
+
|
304
|
+
ViewModel::Utils.map_one_or_many(update_datas) do |update_data|
|
305
|
+
if update_data.is_a?(UpdateData)
|
306
|
+
# Dummy child update data for an unmodified previous child of a
|
307
|
+
# functional update; create it an empty update operation.
|
308
|
+
viewmodel = previous_child_viewmodels.fetch(update_data.viewmodel_reference)
|
309
|
+
update = update_context.new_update(viewmodel, update_data)
|
310
|
+
next [update, viewmodel]
|
311
|
+
end
|
312
|
+
|
313
|
+
reference_string = update_data
|
314
|
+
child_update = update_context.resolve_reference(reference_string, blame_reference)
|
315
|
+
child_viewmodel = child_update.viewmodel
|
316
|
+
|
317
|
+
unless association_data.accepts?(child_viewmodel.class)
|
318
|
+
raise ViewModel::DeserializationError::InvalidAssociationType.new(
|
319
|
+
association_data.association_name.to_s,
|
320
|
+
child_viewmodel.class.view_name,
|
321
|
+
blame_reference)
|
322
|
+
end
|
323
|
+
|
324
|
+
child_ref = child_viewmodel.to_reference
|
325
|
+
|
326
|
+
# The case of two potential owners trying to claim a new referenced
|
327
|
+
# child is covered by set_reference_update_parent.
|
328
|
+
claimed = !association_data.owned? ||
|
329
|
+
child_update.update_data.new? ||
|
330
|
+
previous_child_viewmodels.has_key?(child_ref) ||
|
331
|
+
update_context.try_take_released_viewmodel(child_ref).present?
|
332
|
+
|
333
|
+
if claimed
|
334
|
+
[child_update, child_viewmodel]
|
335
|
+
else
|
336
|
+
# Return the reference to indicate a deferred update
|
337
|
+
[child_update, child_ref]
|
338
|
+
end
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
def set_reference_update_parent(association_data, update, parent_data)
|
343
|
+
if update.reparent_to
|
344
|
+
# Another parent has already tried to take this (probably new)
|
345
|
+
# owned referenced view. It can only be claimed by one of them.
|
346
|
+
other_parent = update.reparent_to.viewmodel.to_reference
|
347
|
+
raise ViewModel::DeserializationError::DuplicateOwner.new(
|
348
|
+
association_data.association_name,
|
349
|
+
[blame_reference, other_parent])
|
350
|
+
end
|
351
|
+
|
352
|
+
update.reparent_to = parent_data
|
353
|
+
end
|
354
|
+
|
324
355
|
def build_update_for_single_association(association_data, association_update_data, update_context)
|
325
356
|
model = self.viewmodel.model
|
326
357
|
|
@@ -329,25 +360,63 @@ class ViewModel::ActiveRecord
|
|
329
360
|
vm_class.new(previous_child_model)
|
330
361
|
end
|
331
362
|
|
332
|
-
if
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
363
|
+
if association_data.pointer_location == :remote
|
364
|
+
if previous_child_viewmodel.present?
|
365
|
+
# Clear the cached association so that AR's save behaviour doesn't
|
366
|
+
# conflict with our explicit parent updates. If we still have a child
|
367
|
+
# after the update, we'll either call `Association#writer` or manually
|
368
|
+
# fix the target cache after recursing in run!(). If we don't, we promise
|
369
|
+
# that the child will no longer be attached in the database, so the new
|
370
|
+
# cached data of nil will be correct.
|
371
|
+
clear_association_cache(model, association_data.direct_reflection)
|
372
|
+
end
|
373
|
+
|
374
|
+
reparent_data =
|
375
|
+
ParentData.new(association_data.direct_reflection.inverse_of, viewmodel)
|
340
376
|
end
|
341
377
|
|
342
|
-
|
343
|
-
if
|
344
|
-
|
378
|
+
if association_update_data.present?
|
379
|
+
if association_data.referenced?
|
380
|
+
# resolve reference string
|
381
|
+
reference_string = association_update_data
|
382
|
+
child_update, child_viewmodel = resolve_referenced_viewmodels(association_data, reference_string,
|
383
|
+
previous_child_viewmodel, update_context)
|
384
|
+
|
385
|
+
if reparent_data
|
386
|
+
set_reference_update_parent(association_data, child_update, reparent_data)
|
387
|
+
end
|
388
|
+
|
389
|
+
if child_viewmodel.is_a?(ViewModel::Reference)
|
390
|
+
update_context.defer_update(child_viewmodel, child_update)
|
391
|
+
end
|
392
|
+
else
|
393
|
+
# Resolve direct children
|
394
|
+
child_viewmodel =
|
395
|
+
resolve_child_viewmodels(association_data, association_update_data, previous_child_viewmodel, update_context)
|
396
|
+
|
397
|
+
child_update =
|
398
|
+
if child_viewmodel.is_a?(ViewModel::Reference)
|
399
|
+
update_context.new_deferred_update(child_viewmodel, association_update_data, reparent_to: reparent_data)
|
400
|
+
else
|
401
|
+
update_context.new_update(child_viewmodel, association_update_data, reparent_to: reparent_data)
|
402
|
+
end
|
345
403
|
end
|
346
404
|
|
405
|
+
# Build the update if we've claimed it
|
406
|
+
unless child_viewmodel.is_a?(ViewModel::Reference)
|
407
|
+
child_update.build!(update_context)
|
408
|
+
end
|
409
|
+
else
|
410
|
+
child_update = nil
|
411
|
+
child_viewmodel = nil
|
412
|
+
end
|
413
|
+
|
414
|
+
# Handle changes
|
347
415
|
if previous_child_viewmodel != child_viewmodel
|
348
416
|
viewmodel.association_changed!(association_data.association_name)
|
349
|
-
|
350
|
-
if
|
417
|
+
|
418
|
+
# free previous child if present and owned
|
419
|
+
if previous_child_viewmodel.present? && association_data.owned?
|
351
420
|
if association_data.pointer_location == :local
|
352
421
|
# When we free a child that's pointed to from its old parent, we need to
|
353
422
|
# clear the cached association to that old parent. If we don't do this,
|
@@ -358,12 +427,7 @@ class ViewModel::ActiveRecord
|
|
358
427
|
# model.association(...).inverse_reflection_for(previous_child_model), but
|
359
428
|
# that's private.
|
360
429
|
|
361
|
-
inverse_reflection =
|
362
|
-
if association_data.direct_reflection.polymorphic?
|
363
|
-
association_data.direct_reflection.polymorphic_inverse_of(previous_child_viewmodel.model.class)
|
364
|
-
else
|
365
|
-
association_data.direct_reflection.inverse_of
|
366
|
-
end
|
430
|
+
inverse_reflection = association_data.direct_reflection_inverse(previous_child_viewmodel.model.class)
|
367
431
|
|
368
432
|
if inverse_reflection.present?
|
369
433
|
clear_association_cache(previous_child_viewmodel.model, inverse_reflection)
|
@@ -374,25 +438,7 @@ class ViewModel::ActiveRecord
|
|
374
438
|
end
|
375
439
|
end
|
376
440
|
|
377
|
-
|
378
|
-
if child_viewmodel.present?
|
379
|
-
# If the association's pointer is in the child, need to provide it with a
|
380
|
-
# ParentData to update
|
381
|
-
parent_data =
|
382
|
-
if association_data.pointer_location == :remote
|
383
|
-
ParentData.new(association_data.direct_reflection.inverse_of, viewmodel)
|
384
|
-
else
|
385
|
-
nil
|
386
|
-
end
|
387
|
-
|
388
|
-
case child_viewmodel
|
389
|
-
when ViewModel::Reference # deferred
|
390
|
-
vm_ref = child_viewmodel
|
391
|
-
update_context.new_deferred_update(vm_ref, association_update_data, reparent_to: parent_data)
|
392
|
-
else
|
393
|
-
update_context.new_update(child_viewmodel, association_update_data, reparent_to: parent_data).build!(update_context)
|
394
|
-
end
|
395
|
-
end
|
441
|
+
child_update
|
396
442
|
end
|
397
443
|
|
398
444
|
def build_updates_for_collection_association(association_data, association_update, update_context)
|
@@ -402,11 +448,13 @@ class ViewModel::ActiveRecord
|
|
402
448
|
parent_data = ParentData.new(association_data.direct_reflection.inverse_of, viewmodel)
|
403
449
|
|
404
450
|
# load children already attached to this model
|
405
|
-
child_viewmodel_class
|
451
|
+
child_viewmodel_class = association_data.viewmodel_class
|
452
|
+
|
406
453
|
previous_child_viewmodels =
|
407
454
|
model.public_send(association_data.direct_reflection.name).map do |child_model|
|
408
455
|
child_viewmodel_class.new(child_model)
|
409
456
|
end
|
457
|
+
|
410
458
|
if child_viewmodel_class._list_member?
|
411
459
|
previous_child_viewmodels.sort_by!(&:_list_attribute)
|
412
460
|
end
|
@@ -421,114 +469,178 @@ class ViewModel::ActiveRecord
|
|
421
469
|
clear_association_cache(model, association_data.direct_reflection)
|
422
470
|
end
|
423
471
|
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
472
|
+
# Update contents are either UpdateData in the case of a nested
|
473
|
+
# association, or reference strings in the case of a reference association.
|
474
|
+
# The former are resolved with resolve_child_viewmodels, the latter with
|
475
|
+
# resolve_referenced_viewmodels.
|
476
|
+
resolve_child_data_reference = ->(child_data) do
|
477
|
+
case child_data
|
478
|
+
when UpdateData
|
479
|
+
child_data.viewmodel_reference if child_data.id
|
480
|
+
when String
|
481
|
+
update_context.resolve_reference(child_data, nil).viewmodel_reference
|
482
|
+
else
|
483
|
+
raise ViewModel::DeserializationError::Internal.new(
|
484
|
+
"Unexpected child data type in collection update: #{child_data.class.name}")
|
485
|
+
end
|
486
|
+
end
|
428
487
|
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
UpdateData.empty_update_for(previous_child_viewmodel)
|
433
|
-
end
|
488
|
+
case association_update
|
489
|
+
when AbstractCollectionUpdate::Replace
|
490
|
+
child_datas = association_update.contents
|
434
491
|
|
435
|
-
|
492
|
+
when AbstractCollectionUpdate::Functional
|
493
|
+
# A fupdate isn't permitted to edit the same model twice.
|
494
|
+
association_update.check_for_duplicates!(update_context, blame_reference)
|
436
495
|
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
child_datas = child_datas.reject { |child| moved_refs.include?(child.viewmodel_reference) }
|
496
|
+
# Construct empty updates for previous children
|
497
|
+
child_datas =
|
498
|
+
previous_child_viewmodels.map do |previous_child_viewmodel|
|
499
|
+
UpdateData.empty_update_for(previous_child_viewmodel)
|
500
|
+
end
|
443
501
|
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
502
|
+
# Insert or replace with either real UpdateData or reference strings
|
503
|
+
association_update.actions.each do |fupdate|
|
504
|
+
case fupdate
|
505
|
+
when FunctionalUpdate::Append
|
506
|
+
# If we're referring to existing members, ensure that they're removed before we append/insert
|
507
|
+
existing_refs = fupdate.contents
|
508
|
+
.map(&resolve_child_data_reference)
|
509
|
+
.to_set
|
510
|
+
|
511
|
+
child_datas.reject! do |child_data|
|
512
|
+
child_ref = resolve_child_data_reference.(child_data)
|
513
|
+
child_ref && existing_refs.include?(child_ref)
|
514
|
+
end
|
450
515
|
|
451
|
-
|
452
|
-
|
516
|
+
if fupdate.before || fupdate.after
|
517
|
+
rel_ref = fupdate.before || fupdate.after
|
453
518
|
|
454
|
-
|
455
|
-
|
519
|
+
# Find the relative insert location. This might be an empty
|
520
|
+
# UpdateData from a previous child or an already-fupdated
|
521
|
+
# reference string.
|
522
|
+
index = child_datas.find_index do |child_data|
|
523
|
+
rel_ref == resolve_child_data_reference.(child_data)
|
524
|
+
end
|
456
525
|
|
526
|
+
unless index
|
527
|
+
raise ViewModel::DeserializationError::AssociatedNotFound.new(
|
528
|
+
association_data.association_name.to_s, rel_ref, blame_reference)
|
457
529
|
end
|
458
530
|
|
459
|
-
|
460
|
-
|
461
|
-
|
531
|
+
index += 1 if fupdate.after
|
532
|
+
child_datas.insert(index, *fupdate.contents)
|
533
|
+
|
534
|
+
else
|
535
|
+
child_datas.concat(fupdate.contents)
|
536
|
+
end
|
462
537
|
|
463
|
-
|
464
|
-
|
465
|
-
|
538
|
+
when FunctionalUpdate::Remove
|
539
|
+
removed_refs = fupdate.removed_vm_refs.to_set
|
540
|
+
child_datas.reject! do |child_data|
|
541
|
+
child_ref = resolve_child_data_reference.(child_data)
|
542
|
+
removed_refs.include?(child_ref)
|
543
|
+
end
|
466
544
|
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
end
|
545
|
+
when FunctionalUpdate::Update
|
546
|
+
# Already guaranteed that each ref has a single existing child attached
|
547
|
+
new_child_datas = fupdate.contents.index_by(&resolve_child_data_reference)
|
471
548
|
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
end
|
477
|
-
else
|
478
|
-
raise ViewModel::DeserializationError::InvalidSyntax.new(
|
479
|
-
"Unknown functional update type: '#{fupdate.type}'",
|
480
|
-
blame_reference)
|
549
|
+
# Replace matched child_datas with the update contents.
|
550
|
+
child_datas.map! do |child_data|
|
551
|
+
child_ref = resolve_child_data_reference.(child_data)
|
552
|
+
new_child_datas.delete(child_ref) { child_data }
|
481
553
|
end
|
554
|
+
|
555
|
+
# Assertion that all values in the update were found in child_datas
|
556
|
+
unless new_child_datas.empty?
|
557
|
+
raise ViewModel::DeserializationError::AssociatedNotFound.new(
|
558
|
+
association_data.association_name.to_s, new_child_datas.keys, blame_reference)
|
559
|
+
end
|
560
|
+
else
|
561
|
+
raise ViewModel::DeserializationError::InvalidSyntax.new(
|
562
|
+
"Unknown functional update type: '#{fupdate.type}'",
|
563
|
+
blame_reference)
|
482
564
|
end
|
565
|
+
end
|
566
|
+
end
|
567
|
+
|
568
|
+
if association_data.referenced?
|
569
|
+
# child_datas are either pre-resolved UpdateData (for non-fupdated
|
570
|
+
# existing members) or reference strings. Resolve into pairs of
|
571
|
+
# [UpdateOperation, ViewModel] if we can create or claim the
|
572
|
+
# UpdateOperation or [UpdateOperation, ViewModelReference] otherwise.
|
573
|
+
resolved_children =
|
574
|
+
resolve_referenced_viewmodels(association_data, child_datas, previous_child_viewmodels, update_context)
|
483
575
|
|
484
|
-
|
576
|
+
resolved_children.each do |child_update, child_viewmodel|
|
577
|
+
set_reference_update_parent(association_data, child_update, parent_data)
|
578
|
+
|
579
|
+
if child_viewmodel.is_a?(ViewModel::Reference)
|
580
|
+
update_context.defer_update(child_viewmodel, child_update)
|
581
|
+
end
|
485
582
|
end
|
486
583
|
|
487
|
-
|
584
|
+
else
|
585
|
+
# child datas are all UpdateData
|
586
|
+
child_viewmodels = resolve_child_viewmodels(association_data, child_datas, previous_child_viewmodels, update_context)
|
488
587
|
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
588
|
+
resolved_children = child_datas.zip(child_viewmodels).map do |child_data, child_viewmodel|
|
589
|
+
child_update =
|
590
|
+
if child_viewmodel.is_a?(ViewModel::Reference)
|
591
|
+
update_context.new_deferred_update(child_viewmodel, child_data, reparent_to: parent_data)
|
592
|
+
else
|
593
|
+
update_context.new_update(child_viewmodel, child_data, reparent_to: parent_data)
|
594
|
+
end
|
595
|
+
|
596
|
+
[child_update, child_viewmodel]
|
496
597
|
end
|
497
598
|
end
|
498
599
|
|
499
600
|
# Calculate new positions for children if in a list. Ignore previous
|
500
|
-
# positions for unresolved references: they'll always
|
501
|
-
# anyway since their parent pointer will change.
|
502
|
-
new_positions = Array.new(
|
601
|
+
# positions (i.e. return nil) for unresolved references: they'll always
|
602
|
+
# need to be updated anyway since their parent pointer will change.
|
603
|
+
new_positions = Array.new(resolved_children.length)
|
503
604
|
|
504
605
|
if association_data.viewmodel_class._list_member?
|
505
606
|
set_position = ->(index, pos) { new_positions[index] = pos }
|
607
|
+
|
506
608
|
get_previous_position = ->(index) do
|
507
|
-
vm =
|
609
|
+
vm = resolved_children[index][1]
|
508
610
|
vm._list_attribute unless vm.is_a?(ViewModel::Reference)
|
509
611
|
end
|
510
612
|
|
511
613
|
ActsAsManualList.update_positions(
|
512
|
-
(0...
|
614
|
+
(0...resolved_children.size).to_a, # indexes
|
513
615
|
position_getter: get_previous_position,
|
514
616
|
position_setter: set_position)
|
515
617
|
end
|
516
618
|
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
619
|
+
resolved_children.zip(new_positions).each do |(child_update, child_viewmodel), new_position|
|
620
|
+
child_update.reposition_to = new_position
|
621
|
+
|
622
|
+
# Recurse into building child updates that we've claimed
|
623
|
+
unless child_viewmodel.is_a?(ViewModel::Reference)
|
624
|
+
child_update.build!(update_context)
|
625
|
+
end
|
626
|
+
end
|
627
|
+
|
628
|
+
child_updates, child_viewmodels = resolved_children.transpose.presence || [[], []]
|
629
|
+
|
630
|
+
# if the new children differ, including in order, mark that this
|
631
|
+
# association has changed and release any no-longer-attached children
|
632
|
+
if child_viewmodels != previous_child_viewmodels
|
633
|
+
viewmodel.association_changed!(association_data.association_name)
|
634
|
+
|
635
|
+
released_child_viewmodels = previous_child_viewmodels - child_viewmodels
|
636
|
+
released_child_viewmodels.each do |vm|
|
637
|
+
release_viewmodel(vm, association_data, update_context)
|
525
638
|
end
|
526
639
|
end
|
527
640
|
|
528
641
|
child_updates
|
529
642
|
end
|
530
643
|
|
531
|
-
|
532
644
|
class ReferencedCollectionMember
|
533
645
|
attr_reader :indirect_viewmodel_reference, :direct_viewmodel
|
534
646
|
attr_accessor :ref_string, :position
|
@@ -694,7 +806,7 @@ class ViewModel::ActiveRecord
|
|
694
806
|
target_collection = MutableReferencedCollection.new(
|
695
807
|
association_data, update_context, previous_members, blame_reference)
|
696
808
|
|
697
|
-
# All updates to
|
809
|
+
# All updates to referenced collections produce a complete target list of
|
698
810
|
# ReferencedCollectionMembers including a ViewModel::Reference to the
|
699
811
|
# indirect child, and an existing (from previous) or new ViewModel for the
|
700
812
|
# direct child.
|
@@ -1,11 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class ViewModel::ActiveRecord::Visitor
|
4
|
-
attr_reader :visit_shared, :for_edit
|
4
|
+
attr_reader :visit_referenced, :visit_shared, :for_edit
|
5
5
|
|
6
|
-
def initialize(visit_shared: true, for_edit: false)
|
7
|
-
@
|
8
|
-
@
|
6
|
+
def initialize(visit_referenced: true, visit_shared: true, for_edit: false)
|
7
|
+
@visit_referenced = visit_referenced
|
8
|
+
@visit_shared = visit_shared
|
9
|
+
@for_edit = for_edit
|
9
10
|
end
|
10
11
|
|
11
12
|
def visit(view, context: nil)
|
@@ -29,8 +30,10 @@ class ViewModel::ActiveRecord::Visitor
|
|
29
30
|
# visit the underlying viewmodel for each association, ignoring any
|
30
31
|
# customization
|
31
32
|
view.class._members.each do |name, member_data|
|
32
|
-
next unless member_data.
|
33
|
-
next if member_data.
|
33
|
+
next unless member_data.association?
|
34
|
+
next if member_data.referenced? && !visit_referenced
|
35
|
+
next if !member_data.owned? && !visit_shared
|
36
|
+
|
34
37
|
children = Array.wrap(view._read_association(name))
|
35
38
|
children.each do |child|
|
36
39
|
if context
|