linny-r 2.0.12 → 2.1.1

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.
@@ -115,13 +115,16 @@ class Repository {
115
115
  .catch((err) => UI.warn(UI.WARNING.NO_CONNECTION, err));
116
116
  }
117
117
 
118
- loadModule(n, include=false) {
119
- // Loads Linny-R model with index `n` from this repository
118
+ loadModule(n, include=false, update=false) {
119
+ // Load Linny-R model with index `n` from this repository.
120
120
  // NOTES:
121
- // (1) when `include` is FALSE, this function behaves as the `loadModel`
121
+ // (1) When `include` is FALSE, this function behaves as the `loadModel`
122
122
  // method of FileManager; when `include` is TRUE, the module is included
123
- // as a cluster (with parameterization via an IO context)
124
- // (2) loading a module requires no authentication
123
+ // as a cluster (with parameterization via an IO context).
124
+ // (2) When `update` is TRUE, all clusters that were previously included
125
+ // using the specified module are replaced by clusters for the (new)
126
+ // module using the bindings inferred from the existing cluster.
127
+ // (3) Loading a module requires no authentication.
125
128
  fetch('repo/', postData({
126
129
  action: 'load',
127
130
  repo: this.name,
@@ -135,12 +138,18 @@ class Repository {
135
138
  })
136
139
  .then((data) => {
137
140
  if(data !== '' && UI.postResponseOK(data)) {
138
- // Server returns Linny-R model file
141
+ // Server returns Linny-R model file.
139
142
  if(include) {
140
- // Include module into current model
141
- REPOSITORY_BROWSER.promptForInclusion(
142
- this.name, this.module_names[n],
143
- parseXML(data.replace(/%23/g, '#')));
143
+ const xml = parseXML(data.replace(/%23/g, '#'));
144
+ if(update) {
145
+ // Include module into current model.
146
+ REPOSITORY_BROWSER.promptForUpdate(
147
+ this.name, this.module_names[n], xml);
148
+ } else {
149
+ // Include module into current model.
150
+ REPOSITORY_BROWSER.promptForInclusion(
151
+ this.name, this.module_names[n], xml);
152
+ }
144
153
  } else {
145
154
  if(UI.loadModelFromXML(data)) {
146
155
  UI.notify(`Model <tt>${this.module_names[n]}</tt> ` +
@@ -153,8 +162,8 @@ class Repository {
153
162
  }
154
163
 
155
164
  storeModelAsModule(name, black_box=false) {
156
- // Stores the current model in this repository
157
- // NOTE: this requires authentication
165
+ // Store the current model in this repository.
166
+ // NOTE: This requires authentication.
158
167
  UI.waitingCursor();
159
168
  fetch('repo/', postData({
160
169
  action: 'store',
@@ -246,6 +255,8 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
246
255
  'click', () => REPOSITORY_BROWSER.includeModule());
247
256
  document.getElementById('repo-load-btn').addEventListener(
248
257
  'click', () => REPOSITORY_BROWSER.confirmLoadModuleAsModel());
258
+ document.getElementById('repo-update-btn').addEventListener(
259
+ 'click', () => REPOSITORY_BROWSER.includeModule(true));
249
260
  document.getElementById('repo-store-btn').addEventListener(
250
261
  'click', () => REPOSITORY_BROWSER.promptForStoring());
251
262
  document.getElementById('repo-black-box-btn').addEventListener(
@@ -294,6 +305,14 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
294
305
  this.include_modal.element('actor').addEventListener(
295
306
  'blur', () => REPOSITORY_BROWSER.updateActors());
296
307
 
308
+ this.update_modal = new ModalDialog('update');
309
+ this.update_modal.ok.addEventListener(
310
+ 'click', () => REPOSITORY_BROWSER.performUpdate());
311
+ this.update_modal.cancel.addEventListener(
312
+ 'click', () => REPOSITORY_BROWSER.cancelUpdate());
313
+ this.update_modal.element('module').addEventListener(
314
+ 'change', () => REPOSITORY_BROWSER.checkUpdateBindings());
315
+
297
316
  this.confirm_load_modal = new ModalDialog('confirm-load-from-repo');
298
317
  this.confirm_load_modal.ok.addEventListener(
299
318
  'click', () => REPOSITORY_BROWSER.loadModuleAsModel());
@@ -566,6 +585,13 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
566
585
  this.modules_count.innerHTML = pluralS(mcount, 'module');
567
586
  if(this.module_index >= 0) {
568
587
  UI.enableButtons('repo-load repo-include');
588
+ // Enable update button only when model contains included clusters.
589
+ const ninc = Object.keys(MODEL.includedModules).length;
590
+ if(ninc) {
591
+ UI.enableButtons(' repo-update');
592
+ } else {
593
+ UI.disableButtons(' repo-update');
594
+ }
569
595
  // NOTE: Only allow deletion from local host repository.
570
596
  if(this.repository_index === 0 && this.isLocalHost) {
571
597
  UI.enableButtons(' repo-delete');
@@ -573,7 +599,7 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
573
599
  UI.disableButtons(' repo-delete');
574
600
  }
575
601
  } else {
576
- UI.disableButtons('repo-load repo-include repo-delete');
602
+ UI.disableButtons('repo-load repo-include repo-update repo-delete');
577
603
  }
578
604
  }
579
605
 
@@ -604,6 +630,10 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
604
630
  }
605
631
  this.updateModulesTable();
606
632
  }
633
+
634
+ //
635
+ // Methods that implement the inclusion of the selected module.
636
+ //
607
637
 
608
638
  promptForInclusion(repo, file, node) {
609
639
  // Add entities defined in the parsed XML tree with root `node`.
@@ -631,7 +661,7 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
631
661
  if(ids.indexOf(oid) >= 0) sel.value = oid;
632
662
  }
633
663
  }
634
-
664
+
635
665
  updateActors() {
636
666
  // Add actor (if specified) to model, and then updates the selector options
637
667
  // for each actor binding selector.
@@ -709,8 +739,11 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
709
739
  `<strong>${IO_CONTEXT.repo_name}</strong>${counts}`);
710
740
  // Get the containing cluster.
711
741
  obj = MODEL.objectByName(IO_CONTEXT.clusterName);
712
- // Position it in the focal cluster.
713
742
  if(obj instanceof Cluster) {
743
+ // Record from which module it has been included with what bindings.
744
+ obj.module = {name: IO_CONTEXT.file_name,
745
+ bindings: IO_CONTEXT.copyOfBindings};
746
+ // Position it in the focal cluster.
714
747
  obj.x = IO_CONTEXT.centroid_x;
715
748
  obj.y = IO_CONTEXT.centroid_y;
716
749
  obj.clearAllProcesses();
@@ -734,6 +767,260 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
734
767
  IO_CONTEXT = null;
735
768
  this.include_modal.hide();
736
769
  }
770
+
771
+ //
772
+ // Methods that implement updating of perviously included clusters.
773
+ //
774
+
775
+ promptForUpdate(repo, file, node) {
776
+ // Add entities defined in the parsed XML tree with root `node`.
777
+ this.included_modules = MODEL.includedModules;
778
+ const
779
+ md = this.update_modal,
780
+ options = [],
781
+ keys = Object.keys(this.included_modules).sort(compareWithTailNumbers),
782
+ // Use file name without tail number as basis for comparison.
783
+ fwot = file.replace(/\-\d+$/, '');
784
+ // Do not prompt if no modules referenced by clusters.
785
+ if(!keys.length) return;
786
+ IO_CONTEXT = new IOContext(repo, file, node);
787
+ let max = 0,
788
+ index = -1,
789
+ mcnt = '';
790
+ for(const k of keys) {
791
+ const tn = endsWithDigits(k);
792
+ if(tn && k === `${fwot}-${tn}`) {
793
+ max = Math.max(max, parseInt(tn));
794
+ index = options.length;
795
+ mcnt = `(${pluralS(this.included_modules[k].length, 'cluster')})`;
796
+ }
797
+ options.push('<option value="', k, '">', k, '</option>');
798
+ }
799
+ if(index >= 0) options[index].replace('">', '" selected>');
800
+ md.element('name').innerHTML = IO_CONTEXT.file_name;
801
+ md.element('module').innerHTML = options.join('');
802
+ md.element('count').innerText = mcnt;
803
+ md.element('issues').style.display = 'none';
804
+ md.element('remove').style.display = 'none';
805
+ md.show('module');
806
+ this.checkUpdateBindings();
807
+ }
808
+
809
+ checkUpdateBindings() {
810
+ // Verify that all module parameters are bound by the previous
811
+ // bindings, or that such binding can be inferred.
812
+ const
813
+ md = this.update_modal,
814
+ mkey = md.element('module').value,
815
+ iml = this.included_modules[mkey],
816
+ mcnt = `(${pluralS(iml.length, 'cluster')})`,
817
+ missing_params = {},
818
+ cnlist = [];
819
+ for(const im of iml) cnlist.push(safeDoubleQuotes(im.displayName));
820
+ md.element('count').innerText = mcnt;
821
+ md.element('count').title = cnlist.sort().join('\n');
822
+ this.obsolete_items = [];
823
+ // Check bindings of the included clusters.
824
+ for(const im of iml) {
825
+ const
826
+ cn = im.name,
827
+ iob = im.module.bindings,
828
+ bk = Object.keys(iob),
829
+ ck = Object.keys(IO_CONTEXT.bindings),
830
+ missing = complement(ck, bk);
831
+ if(missing.length) {
832
+ // Try to match name in module with existing prefixed entity in model.
833
+ for(let mi = missing.length - 1; mi >= 0; mi++) {
834
+ const mb = IO_CONTEXT.bindings[missing[mi]];
835
+ if(mb.io_type === 2) {
836
+ // Actual name = formal name, so known.
837
+ missing.splice(mi, 1);
838
+ } else {
839
+ const
840
+ pent = cn + UI.PREFIXER + mb.name_in_module,
841
+ obj = MODEL.objectByName(pent);
842
+ if(obj && obj.type === mb.entity_type) {
843
+ mb.actual_name = pent;
844
+ console.log('HERE -- completed original bindings', mb);
845
+ } else {
846
+ console.log('HERE -- incomplete original bindings', obj, mb);
847
+ }
848
+ }
849
+ }
850
+ }
851
+ // If not resolved, add module to the missing record.
852
+ for(const m of missing) {
853
+ if(missing_params.hasOwnProperty(m)) {
854
+ missing_params[m].push(im);
855
+ } else {
856
+ missing_params[m] = [im];
857
+ }
858
+ }
859
+ for(const k of MODEL.datasetKeysByPrefix(cn)) {
860
+ this.obsolete_items.push(MODEL.datasets[k]);
861
+ }
862
+ for(const e of MODEL.equationsByPrefix(cn)) this.obsolete_items.push(e);
863
+ for(const c of MODEL.chartsByPrefix(cn)) this.obsolete_items.push(c);
864
+ }
865
+ const
866
+ remove_div = md.element('remove'),
867
+ remove_count = md.element('remove-count'),
868
+ remove_list = md.element('remove-area');
869
+ if(this.obsolete_items.length) {
870
+ remove_count.innerHTML = pluralS(this.obsolete_items.length,
871
+ 'obsolete item');
872
+ const html = [];
873
+ for(const item of this.obsolete_items.sort(
874
+ (a, b) => {
875
+ const
876
+ at = a.type,
877
+ bt = b.type,
878
+ order = ['Dataset', 'Equation', 'Chart'];
879
+ if(at === bt) return ciCompare(a.displayName, b.displayName);
880
+ return order.indexOf(at) - order.indexOf(bt);
881
+ })) {
882
+ html.push('<div><img src="images/', item.type.toLowerCase(),
883
+ '.png" class="sbtn">', item.displayName, '</div>');
884
+ }
885
+ remove_list.innerHTML = html.join('');
886
+ remove_div.style.display = 'block';
887
+ } else {
888
+ remove_count.innerHTML = '';
889
+ remove_list.innerHTML = '';
890
+ remove_div.style.display = 'none';
891
+ }
892
+ const
893
+ issues_div = md.element('issues'),
894
+ issues_header = md.element('issues-header'),
895
+ issues_list = md.element('issues-area'),
896
+ mpkeys = Object.keys(missing_params);
897
+ if(mpkeys.length) {
898
+ // When parameters are missing, report this...
899
+ issues_header.innerHTML = pluralS(mpkeys.length, 'unresolved parameter');
900
+ const html = [];
901
+ for(const k of mpkeys) {
902
+ const mb = IO_CONTEXT.bindings[k];
903
+ cnlist.length = 0;
904
+ for(const im of missing_params[k]) {
905
+ cnlist.push(safeDoubleQuotes(im.displayName));
906
+ }
907
+ html.push('<div>', mb.name_in_module,
908
+ '<span class="update-cc" title="', cnlist.sort().join('\n'),
909
+ '">(in ', pluralS(cnlist.length, 'cluster'), ')</span></div>');
910
+ }
911
+ issues_list.innerHTML = html.join('');
912
+ issues_div.style.display = 'block';
913
+ // ... and disable the OK button so that no update can be performed.
914
+ md.ok.classList.add('disab');
915
+ IO_CONTEXT = null;
916
+ } else {
917
+ // No issues report.
918
+ issues_header.innerHTML = '';
919
+ issues_list.innerHTML = '';
920
+ issues_div.style.display = 'none';
921
+ // Enable the OK button so that update can be performed.
922
+ md.ok.classList.remove('disab');
923
+ }
924
+ }
925
+
926
+ performUpdate() {
927
+ // Update all eligible previously included modules.
928
+ if(!IO_CONTEXT) {
929
+ UI.alert('Cannot update modules without context');
930
+ return;
931
+ }
932
+ const
933
+ md = this.update_modal,
934
+ mkey = md.element('module').value,
935
+ iml = this.included_modules[mkey];
936
+ MODEL.clearSelection();
937
+ // Delete obsolete items from the model.
938
+ for(const item of this.obsolete_items) {
939
+ if(item instanceof Dataset) {
940
+ delete MODEL.datasets[item.identifier];
941
+ } else if(item instanceof DatasetModifier) {
942
+ delete MODEL.equations_dataset.modifiers[UI.nameToID(item.selector)];
943
+ } else if(item instanceof Chart) {
944
+ MODEL.deleteChart(MODEL.charts.indexOf(item));
945
+ }
946
+ }
947
+ // NOTE: The included module list contains clusters.
948
+ const last = iml[iml.length - 1];
949
+ // Ensure that expressions are recompiled only after the last inclusion.
950
+ for(const c of iml) this.updateCluster(c, c === last);
951
+ // Notify modeler of the scope of the update.
952
+ let counts = `: ${pluralS(IO_CONTEXT.added_nodes.length, 'node')}, ` +
953
+ pluralS(IO_CONTEXT.added_links.length, 'link');
954
+ if(IO_CONTEXT.superseded.length > 0) {
955
+ counts += ` (superseded ${IO_CONTEXT.superseded.length})`;
956
+ console.log('SUPERSEDED:', IO_CONTEXT.superseded);
957
+ }
958
+ UI.notify(`Model updated using <tt>${IO_CONTEXT.file_name}</tt> from ` +
959
+ `<strong>${IO_CONTEXT.repo_name}</strong>${counts}`);
960
+ // Reset the IO context.
961
+ IO_CONTEXT = null;
962
+ this.update_modal.hide();
963
+ MODEL.cleanUpActors();
964
+ MODEL.focal_cluster.clearAllProcesses();
965
+ UI.drawDiagram(MODEL);
966
+ UI.updateControllerDialogs('CDEFJX');
967
+ }
968
+
969
+ updateCluster(c, last) {
970
+ // Update perviously included cluster `c`.
971
+ // Remember the XY-position of the cluster.
972
+ const
973
+ cx = c.x,
974
+ cy = c.y;
975
+ // The name of `c` will be used again as prefix.
976
+ IO_CONTEXT.prefix = c.name;
977
+ IO_CONTEXT.actor_name = UI.realActorName(c.actor.name);
978
+ // NOTE: `last` = TRUE indicates that expressions must be recompiled
979
+ // only after updating this cluster.
980
+ IO_CONTEXT.recompile = last;
981
+ // Copy the original bindings to the IO context.
982
+ const ob = c.module.bindings;
983
+ for(const k of Object.keys(ob)) {
984
+ // NOTE: Only copy bindings for parameters of the module used for updating.
985
+ if(IO_CONTEXT.bindings.hasOwnProperty(k)) {
986
+ const nb = IO_CONTEXT.bindings[k];
987
+ nb.actual_name = ob[k].actual_name;
988
+ nb.actual_id = UI.nameToID(nb.actual_name);
989
+ }
990
+ }
991
+ // Delete `c` from the model without adding its XML (UNDO to be implemented).
992
+ MODEL.deleteCluster(c, false);
993
+ // NOTE: Including may affect focal cluster, so store it...
994
+ const fc = MODEL.focal_cluster;
995
+ MODEL.initFromXML(IO_CONTEXT.xml);
996
+ // ... and restore it afterwards.
997
+ MODEL.focal_cluster = fc;
998
+ // Get the newly added cluster.
999
+ const nac = MODEL.objectByName(IO_CONTEXT.clusterName);
1000
+ if(nac instanceof Cluster) {
1001
+ // Record from which module it has been included with what bindings.
1002
+ nac.module = {name: IO_CONTEXT.file_name,
1003
+ bindings: IO_CONTEXT.copyOfBindings};
1004
+ // Give the updated cluster the same position as the original one.
1005
+ nac.x = cx;
1006
+ nac.y = cy;
1007
+ // Prepare for redraw.
1008
+ nac.clearAllProcesses();
1009
+ return true;
1010
+ }
1011
+ UI.alert('Update failed to create a cluster');
1012
+ return false;
1013
+ }
1014
+
1015
+ cancelUpdate() {
1016
+ // Clear the IO context and closes the update dialog.
1017
+ IO_CONTEXT = null;
1018
+ this.update_modal.hide();
1019
+ }
1020
+
1021
+ //
1022
+ // Methods for storing/loading a module in/from a repository
1023
+ //
737
1024
 
738
1025
  promptForStoring() {
739
1026
  if(this.repository_index >= 0) {
@@ -792,11 +1079,11 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
792
1079
  }
793
1080
  }
794
1081
 
795
- includeModule() {
1082
+ includeModule(update=false) {
796
1083
  // Include selected module into the current model.
797
1084
  if(this.repository_index >= 0 && this.module_index >= 0) {
798
1085
  const r = this.repositories[this.repository_index];
799
- r.loadModule(this.module_index, true);
1086
+ r.loadModule(this.module_index, true, update);
800
1087
  }
801
1088
  }
802
1089
 
@@ -255,14 +255,13 @@ class UndoStack {
255
255
  // (2) Set "selected" attribute of objects to FALSE, as the selection will
256
256
  // be restored from UndoEdit.
257
257
  const n = parseXML(MODEL.xml_header + `<edits>${xml}</edits>`);
258
- if(n && n.childNodes) {
258
+ if(n) {
259
259
  const
260
- li = [],
261
- ppi = [],
262
- ci = [];
263
- for(let i = 0; i < n.childNodes.length; i++) {
264
- const c = n.childNodes[i];
265
- // Immediately restore "independent" entities ...
260
+ ln = [],
261
+ ppn = [],
262
+ cn = [];
263
+ for(const c of n.childNodes) {
264
+ // Immediately restore "independent" entities ...
266
265
  if(c.nodeName === 'dataset') {
267
266
  MODEL.addDataset(xmlDecoded(nodeContentByTag(c, 'name')), c);
268
267
  } else if(c.nodeName === 'actor') {
@@ -280,37 +279,33 @@ class UndoStack {
280
279
  obj.selected = false;
281
280
  } else if(c.nodeName === 'chart') {
282
281
  MODEL.addChart(xmlDecoded(nodeContentByTag(c, 'title')), c);
283
- // ... but merely collect indices of other entities.
282
+ // ... but merely collect child nodes for other entities.
284
283
  } else if(c.nodeName === 'link' || c.nodeName === 'constraint') {
285
- li.push(i);
284
+ ln.push(c);
286
285
  } else if(c.nodeName === 'product-position') {
287
- ppi.push(i);
286
+ ppn.push(c);
288
287
  } else if(c.nodeName === 'cluster') {
289
- ci.push(i);
288
+ cn.push(c);
290
289
  }
291
290
  }
292
- // NOTE: Collecting the indices of links, product positions and clusters
291
+ // NOTE: Collecting the child nodes forlinks, product positions and clusters
293
292
  // saves the effort to iterate over ALL childnodes again.
294
293
  // First restore links and constraints.
295
- for(const i of li) {
296
- const c = n.childNodes[i];
297
- // Double-check that this node defines a link or a constraint.
298
- if(c.nodeName === 'link' || c.nodeName === 'constraint') {
299
- let name = xmlDecoded(nodeContentByTag(c, 'from-name'));
300
- let actor = xmlDecoded(nodeContentByTag(c, 'from-owner'));
294
+ for(const c of ln) {
295
+ let name = xmlDecoded(nodeContentByTag(c, 'from-name'));
296
+ let actor = xmlDecoded(nodeContentByTag(c, 'from-owner'));
297
+ if(actor != UI.NO_ACTOR) name += ` (${actor})`;
298
+ let fn = MODEL.nodeBoxByID(UI.nameToID(name));
299
+ if(fn) {
300
+ name = xmlDecoded(nodeContentByTag(c, 'to-name'));
301
+ actor = xmlDecoded(nodeContentByTag(c, 'to-owner'));
301
302
  if(actor != UI.NO_ACTOR) name += ` (${actor})`;
302
- let fn = MODEL.nodeBoxByID(UI.nameToID(name));
303
- if(fn) {
304
- name = xmlDecoded(nodeContentByTag(c, 'to-name'));
305
- actor = xmlDecoded(nodeContentByTag(c, 'to-owner'));
306
- if(actor != UI.NO_ACTOR) name += ` (${actor})`;
307
- let tn = MODEL.nodeBoxByID(UI.nameToID(name));
308
- if(tn) {
309
- if(c.nodeName === 'link') {
310
- MODEL.addLink(fn, tn, c).selected = false;
311
- } else {
312
- MODEL.addConstraint(fn, tn, c).selected = false;
313
- }
303
+ let tn = MODEL.nodeBoxByID(UI.nameToID(name));
304
+ if(tn) {
305
+ if(c.nodeName === 'link') {
306
+ MODEL.addLink(fn, tn, c).selected = false;
307
+ } else {
308
+ MODEL.addConstraint(fn, tn, c).selected = false;
314
309
  }
315
310
  }
316
311
  }
@@ -319,39 +314,33 @@ class UndoStack {
319
314
  // NOTE: These correspond to the products that were part of the
320
315
  // selection; all other product positions are restored as part of their
321
316
  // containing clusters.
322
- for(const i of ppi) {
323
- const c = n.childNodes[i];
324
- // Double-check that this node defines a product position.
325
- if(c.nodeName === 'product-position') {
326
- const obj = MODEL.nodeBoxByID(UI.nameToID(
327
- xmlDecoded(nodeContentByTag(c, 'product-name'))));
328
- if(obj) {
329
- obj.selected = false;
330
- MODEL.focal_cluster.addProductPosition(obj).initFromXML(c);
331
- }
317
+ for(const c of ppn) {
318
+ const obj = MODEL.nodeBoxByID(UI.nameToID(
319
+ xmlDecoded(nodeContentByTag(c, 'product-name'))));
320
+ if(obj) {
321
+ obj.selected = false;
322
+ MODEL.focal_cluster.addProductPosition(obj).initFromXML(c);
332
323
  }
333
324
  }
334
325
  // Lastly, restore clusters.
335
326
  // NOTE: Store focal cluster, because this may change while initializing
336
327
  // a cluster from XML.
337
328
  const fc = MODEL.focal_cluster;
338
- for(const i of ci) {
339
- const c = n.childNodes[i];
340
- if(c.nodeName === 'cluster') {
341
- const obj = MODEL.addCluster(xmlDecoded(nodeContentByTag(c, 'name')),
342
- xmlDecoded(nodeContentByTag(c, 'owner')), c);
343
- obj.selected = false;
344
-
329
+ for(const c of cn) {
330
+ const obj = MODEL.addCluster(xmlDecoded(nodeContentByTag(c, 'name')),
331
+ xmlDecoded(nodeContentByTag(c, 'owner')), c);
332
+ obj.selected = false;
333
+ /*
345
334
  // TEMPORARY trace (remove when done testing)
346
335
  if (MODEL.focal_cluster === fc) {
347
336
  console.log('NO refocus needed');
348
337
  } else {
349
338
  console.log('Refocusing from ... to ... : ', MODEL.focal_cluster, fc);
350
339
  }
351
- // Restore original focal cluster because addCluster may shift focus
352
- // to a sub-cluster.
353
- MODEL.focal_cluster = fc;
354
- }
340
+ */
341
+ // Restore original focal cluster because addCluster may shift focus
342
+ // to a sub-cluster.
343
+ MODEL.focal_cluster = fc;
355
344
  }
356
345
  }
357
346
  MODEL.clearSelection();
@@ -443,7 +432,7 @@ if (MODEL.focal_cluster === fc) {
443
432
  // First check whether product P needs to be restored.
444
433
  if(!p && ue.xml) {
445
434
  const n = parseXML(MODEL.xml_header + `<edits>${ue.xml}</edits>`);
446
- if(n && n.childNodes) {
435
+ if(n && n.childNodes.length) {
447
436
  let c = n.childNodes[0];
448
437
  if(c.nodeName === 'product') {
449
438
  p = MODEL.addProduct(