@abi-software/mapintegratedvuer 1.21.1 → 1.22.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.
Files changed (68) hide show
  1. package/README.md +23 -5
  2. package/assets/location-icon.html +33 -1
  3. package/assets/styleguide.css +18 -18
  4. package/cypress.config.js +7 -9
  5. package/dist/{ConnectivityGraph-CZMROk9r.js → ConnectivityGraph-BBZ3Dgat.js} +3 -3
  6. package/dist/{ContentMixin-Vn34n5t2.js → ContentMixin-B-O_IwPg.js} +16 -18
  7. package/dist/{DynamicMarkerMixin-BS6t7w_i.js → DynamicMarkerMixin-B9iqtP_f.js} +1 -1
  8. package/dist/{Flatmap-BejxA0uz.js → Flatmap-BoEFI50b.js} +4 -4
  9. package/dist/{Iframe-BWbhTjCR.js → Iframe-Slj-LQTF.js} +4 -4
  10. package/dist/{MultiFlatmap-nlmsH-Ib.js → MultiFlatmap-l6aBn6x3.js} +17 -5
  11. package/dist/{Plot-ChbPsDZX.js → Plot-CIhEip5b.js} +2 -2
  12. package/dist/{Scaffold-C6f7c7Ha.js → Scaffold-D5ZGCDvt.js} +4 -4
  13. package/dist/{Simulation-DcJScRYo.js → Simulation-C5o5DXRb.js} +2 -2
  14. package/dist/{connectivities-UXTqf7_0.js → connectivities-Br3MpoAv.js} +154 -95
  15. package/dist/index.html +9 -6
  16. package/dist/mapintegratedvuer.js +5506 -5582
  17. package/dist/mapintegratedvuer.umd.cjs +196 -196
  18. package/dist/style.css +1 -1
  19. package/eslint.config.js +39 -15
  20. package/package.json +12 -6
  21. package/public/index.html +9 -6
  22. package/src/App.vue +224 -255
  23. package/src/assets/_variables.scss +12 -10
  24. package/src/assets/header-icon.scss +8 -9
  25. package/src/assets/mapicon-species-style.css +10 -8
  26. package/src/assets/styles.scss +1 -1
  27. package/src/components/ContentBar.vue +99 -106
  28. package/src/components/ContentVuer.vue +36 -39
  29. package/src/components/ContextCard.vue +238 -199
  30. package/src/components/ContextHelp.vue +22 -26
  31. package/src/components/CustomSplitter.vue +32 -43
  32. package/src/components/DatasetHeader.vue +37 -41
  33. package/src/components/DialogToolbarContent.vue +292 -256
  34. package/src/components/FlatmapContextCard.vue +52 -57
  35. package/src/components/MapContent.vue +111 -87
  36. package/src/components/ResizeSensor.vue +8 -13
  37. package/src/components/SearchControls.vue +26 -35
  38. package/src/components/SimulatedData.js +196 -219
  39. package/src/components/SplitDialog.vue +139 -129
  40. package/src/components/SplitFlow.vue +433 -239
  41. package/src/components/index.js +1 -3
  42. package/src/components/scripts/simulation_data.js +393 -397
  43. package/src/components/scripts/utilities.js +108 -78
  44. package/src/components/viewers/CellDLViewer.vue +9 -9
  45. package/src/components/viewers/ConnectivityGraph.vue +9 -12
  46. package/src/components/viewers/Flatmap.vue +23 -25
  47. package/src/components/viewers/Iframe.vue +8 -10
  48. package/src/components/viewers/MultiFlatmap.vue +73 -69
  49. package/src/components/viewers/Plot.vue +6 -7
  50. package/src/components/viewers/Scaffold.vue +73 -63
  51. package/src/components/viewers/Simulation.vue +10 -15
  52. package/src/main.js +16 -21
  53. package/src/mixins/ContentMixin.js +208 -178
  54. package/src/mixins/DynamicMarkerMixin.js +14 -18
  55. package/src/mixins/RetrieveContextCardMixin.js +35 -30
  56. package/src/mixins/S3Bucket.vue +10 -13
  57. package/src/services/knowledgeMapping.js +53 -53
  58. package/src/services/tagging.js +4 -4
  59. package/src/stores/connectivities.js +46 -8
  60. package/src/stores/entries.js +7 -9
  61. package/src/stores/index.js +7 -7
  62. package/src/stores/settings.js +23 -31
  63. package/src/stores/splitFlow.js +395 -298
  64. package/vite.bundle-build.js +24 -26
  65. package/vite.config.js +22 -21
  66. package/vite.static-build.js +4 -4
  67. package/vitest.workspace.js +1 -3
  68. package/vuese-generator.js +47 -55
@@ -15,9 +15,7 @@
15
15
  />
16
16
  </el-header>
17
17
  <el-main class="dialog-main">
18
- <div
19
- style="width: 100%; height: 100%; position: relative; overflow: hidden"
20
- >
18
+ <div style="width: 100%; height: 100%; position: relative; overflow: hidden">
21
19
  <SideBar
22
20
  ref="sideBar"
23
21
  :envVars="envVars"
@@ -66,36 +64,28 @@
66
64
  </template>
67
65
 
68
66
  <script>
69
- /* eslint-disable no-alert, no-console */
70
67
  import { provide, markRaw, computed } from 'vue';
71
68
  import Tagging from '../services/tagging.js';
72
- import DialogToolbarContent from "./DialogToolbarContent.vue";
73
- import EventBus from "./EventBus";
74
- import SplitDialog from "./SplitDialog.vue";
69
+ import DialogToolbarContent from './DialogToolbarContent.vue';
70
+ import EventBus from './EventBus';
71
+ import SplitDialog from './SplitDialog.vue';
75
72
  // import contextCards from './context-cards'
76
- import { SideBar } from "@abi-software/map-side-bar";
77
- import "@abi-software/map-side-bar/dist/style.css";
78
- import {
79
- queryForwardBackwardConnections
80
- } from '@abi-software/map-utilities';
73
+ import { SideBar } from '@abi-software/map-side-bar';
74
+ import '@abi-software/map-side-bar/dist/style.css';
75
+ import { queryForwardBackwardConnections } from '@abi-software/map-utilities';
81
76
  import {
82
77
  capitalise,
83
78
  getNewMapEntry,
84
79
  initialDefaultState,
85
80
  intersectArrays,
86
- } from "./scripts/utilities.js";
87
- import { AnnotationService } from '@abi-software/sparc-annotation'
81
+ } from './scripts/utilities.js';
82
+ import { AnnotationService } from '@abi-software/sparc-annotation';
88
83
  import { mapStores } from 'pinia';
89
84
  import { useEntriesStore } from '../stores/entries';
90
- import { useMainStore } from '../stores/index'
85
+ import { useMainStore } from '../stores/index';
91
86
  import { useSettingsStore } from '../stores/settings';
92
87
  import { useSplitFlowStore } from '../stores/splitFlow';
93
88
  import { useConnectivitiesStore } from '../stores/connectivities';
94
- import {
95
- ElContainer as Container,
96
- ElHeader as Header,
97
- ElMain as Main,
98
- } from "element-plus";
99
89
 
100
90
  const getAllFacetLabels = (children) => {
101
91
  const labels = [];
@@ -108,12 +98,12 @@ const getAllFacetLabels = (children) => {
108
98
  });
109
99
  }
110
100
  return labels;
111
- }
101
+ };
112
102
 
113
103
  const getFacetsFromAction = (action) => {
114
104
  const actionFacets = action.facets ? action.facets : action.labels ? action.labels : [];
115
105
  return actionFacets;
116
- }
106
+ };
117
107
 
118
108
  const getAnatomyTermsForFilters = (action, availableNameCurieMapping) => {
119
109
  const facets = [];
@@ -126,17 +116,14 @@ const getAnatomyTermsForFilters = (action, availableNameCurieMapping) => {
126
116
  }
127
117
  }
128
118
  return facets;
129
- }
119
+ };
130
120
 
131
121
  /**
132
122
  * Component of the floating dialogs.
133
123
  */
134
124
  export default {
135
- name: "SplitFlow",
125
+ name: 'SplitFlow',
136
126
  components: {
137
- Container,
138
- Header,
139
- Main,
140
127
  DialogToolbarContent,
141
128
  SplitDialog,
142
129
  SideBar,
@@ -147,8 +134,11 @@ export default {
147
134
  let annotator = markRaw(new AnnotationService(`${settings.flatmapAPI}annotator`));
148
135
  provide('userApiKey', mainStore.userToken);
149
136
  provide('$annotator', annotator);
150
- provide('showLongLabel', computed(() => props.showLongLabel));
151
- return { annotator }
137
+ provide(
138
+ 'showLongLabel',
139
+ computed(() => props.showLongLabel),
140
+ );
141
+ return { annotator };
152
142
  },
153
143
  props: {
154
144
  state: {
@@ -168,6 +158,11 @@ export default {
168
158
  startUp: true,
169
159
  sidebarStateRestored: false,
170
160
  sidebarAnnotationState: false,
161
+ // true while a permalink's sidebar.connectivityEntries are being resolved across the active viewers,
162
+ // used to avoid flashing the full/unfiltered connectivity list
163
+ // or dropping entries resolved by other viewers.
164
+ restoringConnectivityState: false,
165
+ restoreAttempts: 0,
171
166
  search: '',
172
167
  expanded: '',
173
168
  filterTriggered: false,
@@ -187,7 +182,7 @@ export default {
187
182
  filterVisibility: true,
188
183
  filterOptions: [],
189
184
  annotationHighlight: [],
190
- }
185
+ };
191
186
  },
192
187
  watch: {
193
188
  state: {
@@ -218,29 +213,27 @@ export default {
218
213
  const filterExpression = {
219
214
  OR: [
220
215
  { NOT: { 'tile-layer': 'pathways' } },
221
- { NOT: { 'HAS': 'nerves' } },
216
+ { NOT: { HAS: 'nerves' } },
222
217
  {
223
- AND: [
224
- { 'tile-layer': 'pathways' },
225
- { 'models': this.connectivityHighlight }
226
- ]
227
- }
228
- ]
218
+ AND: [{ 'tile-layer': 'pathways' }, { models: this.connectivityHighlight }],
219
+ },
220
+ ],
229
221
  };
230
222
  const validFilter = this.filterVisibility && this.connectivityProcessed;
231
223
  const payload = validFilter ? filterExpression : undefined;
232
224
  EventBus.emit('filter-visibility', payload);
233
225
  },
234
226
  onConnectivityCollapseChange: function (payload) {
235
- this.expanded = payload.id
227
+ this.expanded = payload.id;
236
228
  const splitdialog = this.$refs.splitdialog;
237
229
  if (splitdialog) {
238
230
  const activeContents = splitdialog.getActiveContents();
239
- const hasFlatmap = activeContents.find(c => c.viewerType.includes('Flatmap'));
240
- const hasHumanMaleFlatmap = activeContents.find(c => c.activeSpecies === "Human Male");
241
- let nonFlatmapLoad = false
242
- activeContents.forEach(content => {
243
- const isFlatmap = content.viewerType === 'Flatmap' || content.viewerType === 'MultiFlatmap';
231
+ const hasFlatmap = activeContents.find((c) => c.viewerType.includes('Flatmap'));
232
+ const hasHumanMaleFlatmap = activeContents.find((c) => c.activeSpecies === 'Human Male');
233
+ let nonFlatmapLoad = false;
234
+ activeContents.forEach((content) => {
235
+ const isFlatmap =
236
+ content.viewerType === 'Flatmap' || content.viewerType === 'MultiFlatmap';
244
237
  // minimise connectivity detail fetch
245
238
  const shouldLoad =
246
239
  (hasFlatmap && isFlatmap) ||
@@ -265,7 +258,7 @@ export default {
265
258
  if (splitdialog) {
266
259
  const activeContents = splitdialog.getActiveContents();
267
260
 
268
- activeContents.forEach(content => {
261
+ activeContents.forEach((content) => {
269
262
  if (content?.$refs['viewer']) {
270
263
  const contentViewer = content.$refs['viewer'];
271
264
  const flatmapRef = contentViewer.flatmapRef;
@@ -273,7 +266,8 @@ export default {
273
266
  let flatmap = null;
274
267
 
275
268
  if (flatmapRef) flatmap = flatmapRef;
276
- if (multiflatmapRef) flatmap = multiflatmapRef.getCurrentFlatmap();
269
+ if (multiflatmapRef && contentViewer.flatmapIsReady())
270
+ flatmap = multiflatmapRef.getCurrentFlatmap();
277
271
 
278
272
  if (flatmap) {
279
273
  activeFlatmaps.push(flatmap);
@@ -283,12 +277,31 @@ export default {
283
277
  }
284
278
  return activeFlatmaps;
285
279
  },
280
+ // Scaffold viewers are not Flatmap/MultiFlatmap instances so they're not returned by getActiveFlatmaps,
281
+ // but their own nervesKnowledge (populated once loadConnectivityExplorerConfig resolves)
282
+ // carries the nerve-label data needed to restore their connectivity entries.
283
+ getActiveScaffolds: function () {
284
+ const activeScaffolds = [];
285
+ let splitdialog = this.$refs.splitdialog;
286
+
287
+ if (splitdialog) {
288
+ const activeContents = splitdialog.getActiveContents();
289
+
290
+ activeContents.forEach((content) => {
291
+ const contentViewer = content?.$refs['viewer'];
292
+ if (contentViewer?.scaffoldRef) {
293
+ activeScaffolds.push(contentViewer);
294
+ }
295
+ });
296
+ }
297
+ return activeScaffolds;
298
+ },
286
299
  /**
287
300
  * Callback when an action is performed (open new dialogs).
288
301
  */
289
302
  actionClick: function (action) {
290
303
  if (action) {
291
- if (!this.availableFacets || (this.availableFacets.length === 0)) {
304
+ if (!this.availableFacets || this.availableFacets.length === 0) {
292
305
  const availableFacetsRaw = localStorage.getItem('available-anatomy-facets');
293
306
  const availableFacetsAll = availableFacetsRaw ? JSON.parse(availableFacetsRaw) : [];
294
307
 
@@ -296,7 +309,10 @@ export default {
296
309
  this.availableFacets = markRaw([...new Set(getAllFacetLabels(availableFacetsAll))]);
297
310
  }
298
311
 
299
- if (!this.availableNameCurieMapping || (Object.keys(this.availableNameCurieMapping).length === 0)) {
312
+ if (
313
+ !this.availableNameCurieMapping ||
314
+ Object.keys(this.availableNameCurieMapping).length === 0
315
+ ) {
300
316
  const availableDataRaw = localStorage.getItem('available-name-curie-mapping');
301
317
  const availableData = availableDataRaw ? JSON.parse(availableDataRaw) : {};
302
318
 
@@ -304,7 +320,7 @@ export default {
304
320
  this.availableNameCurieMapping = markRaw(availableData);
305
321
  }
306
322
 
307
- if (action.type == "Search") {
323
+ if (action.type == 'Search') {
308
324
  if (action.nervePath) {
309
325
  this.openSearch([action.filter], action.label);
310
326
  } else {
@@ -315,67 +331,67 @@ export default {
315
331
  ? 'portal_maps_featured_dataset_search'
316
332
  : 'portal_maps_action_search';
317
333
  Tagging.sendEvent({
318
- 'event': 'interaction_event',
319
- 'event_name': eventName,
320
- 'category': action.term || 'filter',
321
- 'location': 'map_location_pin'
334
+ event: 'interaction_event',
335
+ event_name: eventName,
336
+ category: action.term || 'filter',
337
+ location: 'map_location_pin',
322
338
  });
323
339
  this.filterTriggered = true;
324
340
  }
325
- } else if (action.type == "URL") {
326
- window.open(action.resource, "_blank");
327
- } else if (action.type == "Facet") {
341
+ } else if (action.type == 'URL') {
342
+ window.open(action.resource, '_blank');
343
+ } else if (action.type == 'Facet') {
328
344
  if (this.$refs.sideBar) {
329
345
  const sendAction = {
330
- facetPropPath: "anatomy.organ.category.name",
331
- facetSubPropPath: "anatomy.organ.name",
332
- term: "Anatomical structure",
346
+ facetPropPath: 'anatomy.organ.category.name',
347
+ facetSubPropPath: 'anatomy.organ.name',
348
+ term: 'Anatomical structure',
333
349
  };
334
350
  const filters = [];
335
351
  const facets = getAnatomyTermsForFilters(action, this.availableNameCurieMapping);
336
352
  const actionFacets = getFacetsFromAction(action);
337
353
  const facetString = actionFacets.join(', ');
338
- facets.forEach(facet => filters.push({...sendAction, facet}));
354
+ facets.forEach((facet) => filters.push({ ...sendAction, facet }));
339
355
  this.$refs.sideBar.addFilter(filters);
340
356
  // GA Tagging
341
357
  // Event tracking for map action search/filter data
342
358
  Tagging.sendEvent({
343
- 'event': 'interaction_event',
344
- 'event_name': 'portal_maps_action_filter',
345
- 'category': facetString || 'filter_reset',
346
- 'location': 'map_location_pin'
359
+ event: 'interaction_event',
360
+ event_name: 'portal_maps_action_filter',
361
+ category: facetString || 'filter_reset',
362
+ location: 'map_location_pin',
347
363
  });
348
364
  this.filterTriggered = true;
349
365
  }
350
- } else if (action.type == "Facets") {
366
+ } else if (action.type == 'Facets') {
351
367
  const facets = [];
352
368
  const facetsArray = getAnatomyTermsForFilters(action, this.availableNameCurieMapping);
353
369
  const filterValuesArray = intersectArrays(this.availableFacets, facetsArray);
354
370
  const filterValues = filterValuesArray.join(', ');
355
371
 
356
- this.settingsStore.facets.species.forEach(e => {
372
+ this.settingsStore.facets.species.forEach((e) => {
357
373
  facets.push({
358
374
  facet: capitalise(e),
359
- term: "Species",
360
- facetPropPath: "organisms.primary.species.name",
375
+ term: 'Species',
376
+ facetPropPath: 'organisms.primary.species.name',
361
377
  });
362
378
  });
363
379
  facets.push(
364
- ...filterValuesArray.map(val => ({
380
+ ...filterValuesArray.map((val) => ({
365
381
  facet: capitalise(val),
366
- term: "Anatomical structure",
367
- facetPropPath: "anatomy.organ.category.name",
368
- facetSubPropPath: "anatomy.organ.name",
369
- }))
382
+ term: 'Anatomical structure',
383
+ facetPropPath: 'anatomy.organ.category.name',
384
+ facetSubPropPath: 'anatomy.organ.name',
385
+ })),
370
386
  );
371
- this.openSearch(facets, "")
387
+ this.openSearch(facets, '');
372
388
  // GA Tagging
373
389
  // Event tracking for map action search/filter data
374
390
  Tagging.sendEvent({
375
- 'event': 'interaction_event',
376
- 'event_name': 'portal_maps_action_filter',
377
- 'category': filterValues || 'filter_reset',
378
- 'location': 'map_popup_button'
391
+ event: 'interaction_event',
392
+ event_name: 'portal_maps_action_filter',
393
+ category: filterValues || 'filter_reset',
394
+ location: 'map_popup_button',
379
395
  });
380
396
  this.filterTriggered = true;
381
397
  } else {
@@ -393,7 +409,7 @@ export default {
393
409
  if (type) categoryValues.push(type);
394
410
  if (datasetId) categoryValues.push('(' + id + ')');
395
411
  if (resource) {
396
- if (type === "Plot") {
412
+ if (type === 'Plot') {
397
413
  filePath = resource.dataSource.url;
398
414
  } else {
399
415
  filePath = typeof resource === 'string' ? resource : resource.share_link;
@@ -403,12 +419,12 @@ export default {
403
419
  // GA Tagging
404
420
  // Event tracking for map sidebar gallery click
405
421
  Tagging.sendEvent({
406
- 'event': 'interaction_event',
407
- 'event_name': 'portal_maps_gallery_click',
408
- 'category': categoryValues.join(' '),
409
- 'location': 'map_sidebar_gallery',
410
- 'dataset_id': id ? id + '' : '', // change to string format
411
- 'file_path': filePath,
422
+ event: 'interaction_event',
423
+ event_name: 'portal_maps_gallery_click',
424
+ category: categoryValues.join(' '),
425
+ location: 'map_sidebar_gallery',
426
+ dataset_id: id ? id + '' : '', // change to string format
427
+ file_path: filePath,
412
428
  });
413
429
  },
414
430
  onDisplaySearch: function (payload, tracking = true) {
@@ -417,7 +433,7 @@ export default {
417
433
  let splitdialog = this.$refs.splitdialog;
418
434
  if (splitdialog) {
419
435
  const activeContents = splitdialog.getActiveContents();
420
- activeContents.forEach(content => {
436
+ activeContents.forEach((content) => {
421
437
  if (content.search(payload.term)) {
422
438
  searchFound = true;
423
439
  }
@@ -429,29 +445,31 @@ export default {
429
445
  // GA Tagging
430
446
  // Event tracking for map on display search
431
447
  Tagging.sendEvent({
432
- 'event': 'interaction_event',
433
- 'event_name': 'portal_maps_display_search',
434
- 'category': payload.term,
435
- 'location': 'map_toolbar'
448
+ event: 'interaction_event',
449
+ event_name: 'portal_maps_display_search',
450
+ category: payload.term,
451
+ location: 'map_toolbar',
436
452
  });
437
453
  }
438
454
  },
439
- fetchSuggestions: function(payload) {
455
+ fetchSuggestions: function (payload) {
440
456
  const suggestions = [];
441
457
  //Search all active viewers when global callback is on
442
458
  let splitdialog = this.$refs.splitdialog;
443
459
  const activeContents = splitdialog.getActiveContents();
444
460
  //Push new suggestions into the pre-existing suggestions array
445
- activeContents.forEach(content => content.searchSuggestions(payload.data.term, suggestions));
461
+ activeContents.forEach((content) =>
462
+ content.searchSuggestions(payload.data.term, suggestions),
463
+ );
446
464
  const parsed = [];
447
465
  //Remove double quote as it is used as a speical character
448
- suggestions.forEach(suggestion => {
449
- parsed.push(suggestion.replaceAll("\"", ""));
466
+ suggestions.forEach((suggestion) => {
467
+ parsed.push(suggestion.replaceAll('"', ''));
450
468
  });
451
469
  const unique = new Set(parsed);
452
470
  suggestions.length = 0;
453
471
  for (const item of unique) {
454
- suggestions.push({"value": "\"" + item +"\""});
472
+ suggestions.push({ value: '"' + item + '"' });
455
473
  }
456
474
  payload.data.cb(suggestions);
457
475
  },
@@ -467,7 +485,7 @@ export default {
467
485
  // offset sidebar only on singlepanel and 2horpanel views
468
486
  EventBus.emit('show-connectivity', {
469
487
  featureIds: featureIds,
470
- offset: activeView === 'singlepanel' || activeView === '2horpanel'
488
+ offset: activeView === 'singlepanel' || activeView === '2horpanel',
471
489
  });
472
490
  }
473
491
  },
@@ -475,23 +493,25 @@ export default {
475
493
  // expand connectivity card and show connectivity info
476
494
  // if expanded exist, payload should be an array of one element
477
495
  // skip payload not match the expanded in multiple views
478
- const isMatched = payload.some(entry => entry.featureId[0] === this.expanded);
496
+ const isMatched = payload.some((entry) => entry.featureId[0] === this.expanded);
479
497
  if (this.expanded && this.connectivityExplorerClicked.length && !isMatched) {
480
498
  this.connectivityExplorerClicked.pop();
481
499
  return;
482
500
  }
483
501
 
484
502
  // Remove duplicate items from payload
485
- const uniquePayload = [...new Map(payload.map((entry) => [entry.featureId[0], entry])).values()];
486
- this.connectivityEntry = uniquePayload.map((entry) => {
503
+ const uniquePayload = [
504
+ ...new Map(payload.map((entry) => [entry.featureId[0], entry])).values(),
505
+ ];
506
+ const mappedPayload = uniquePayload.map((entry) => {
487
507
  let result = {
488
508
  ...entry,
489
509
  label: entry.title,
490
510
  id: entry.featureId[0],
491
- }
492
- const ck = this.connectivityKnowledge.find(ck => ck.id === result.id);
511
+ };
512
+ const ck = this.connectivityKnowledge.find((ck) => ck.id === result.id);
493
513
  if (entry.ready) {
494
- result['nerve-label'] = entry['nerve-label'] || ck['nerve-label'];
514
+ result['nerve-label'] = entry['nerve-label'] || (ck && ck['nerve-label']);
495
515
  }
496
516
  if (ck && ck['long-label']) {
497
517
  result['long-label'] = ck['long-label'];
@@ -502,13 +522,56 @@ export default {
502
522
  return result;
503
523
  });
504
524
 
525
+ // While restoring a permalink's sidebar connectivityEntries,
526
+ // popups from multiple active viewers (e.g. two MultiFlatmaps + a Scaffold)
527
+ // resolve concurrently and each only reports the subset it recognises.
528
+ // Merge by id (keeping richer fields like nerve-label/long-label, e.g. from a Scaffold viewer,
529
+ // if a later duplicate for the same id lacks them) instead of replacing,
530
+ // so entries resolved by one viewer are not dropped or downgraded when another viewer's payload arrives.
531
+ if (this.restoringConnectivityState) {
532
+ const merged = new Map(this.connectivityEntry.map((entry) => [entry.id, entry]));
533
+ mappedPayload.forEach((entry) => {
534
+ const existing = merged.get(entry.id);
535
+ merged.set(
536
+ entry.id,
537
+ existing
538
+ ? {
539
+ ...existing,
540
+ ...entry,
541
+ // Viewers emit placeholders (ready: false) before their resolved entries (ready: true),
542
+ // so a late placeholder from one viewer must not downgrade an entry another viewer already resolved.
543
+ ready: existing.ready || entry.ready,
544
+ 'nerve-label': entry['nerve-label'] || existing['nerve-label'],
545
+ 'long-label': entry['long-label'] || existing['long-label'],
546
+ 'expert-consultants':
547
+ entry['expert-consultants'] || existing['expert-consultants'],
548
+ }
549
+ : entry,
550
+ );
551
+ });
552
+ this.connectivityEntry = Array.from(merged.values());
553
+ // Entries resolve asynchronously via connectivity-info-open,
554
+ // which is not tied to connectivity-knowledge ticks, so re-run the restore completion check here.
555
+ // Otherwise the last entries becoming ready never re-evaluates allEntriesReady
556
+ // and the restore never finalises.
557
+ this.checkConnectivityRestoreComplete(this.state?.sidebar);
558
+ } else {
559
+ this.connectivityEntry = mappedPayload;
560
+ }
561
+
505
562
  // Fetch long-label from global connectivities if not exist in the payload,
506
563
  // this is for the case when user click on the flatmap paths/features directly without going through sidebar list,
507
- // which will only have id and label in the payload
508
- if (!this.connectivityEntry[0]['long-label'] && this.connectivityEntry[0].mapuuid) {
509
- const connectivityData = this.connectivitiesStore.globalConnectivities[this.connectivityEntry[0].mapuuid] || [];
564
+ // which will only have id and label in the payload.
565
+ // Skipped while restoring since connectivityEntry[0] may not relate to the entries just merged in above.
566
+ if (
567
+ !this.restoringConnectivityState &&
568
+ !this.connectivityEntry[0]['long-label'] &&
569
+ this.connectivityEntry[0].mapuuid
570
+ ) {
571
+ const connectivityData =
572
+ this.connectivitiesStore.globalConnectivities[this.connectivityEntry[0].mapuuid] || [];
510
573
  if (connectivityData.length) {
511
- const ck = connectivityData.find(ck => ck.id === this.connectivityEntry[0].id);
574
+ const ck = connectivityData.find((ck) => ck.id === this.connectivityEntry[0].id);
512
575
  if (ck && ck['long-label']) {
513
576
  this.connectivityEntry[0]['long-label'] = ck['long-label'];
514
577
  }
@@ -517,13 +580,13 @@ export default {
517
580
 
518
581
  if (this.connectivityExplorerClicked.length) {
519
582
  // only remove clicked if not placeholder entry
520
- if (this.connectivityEntry.every(entry => entry.ready)) {
583
+ if (this.connectivityEntry.every((entry) => entry.ready)) {
521
584
  this.connectivityExplorerClicked.pop();
522
585
  }
523
586
  } else {
524
587
  // click on the flatmap paths/features directly
525
588
  // or onDisplaySearch is performed
526
- const connectivityEntries = this.connectivityEntry.map(entry => entry.id);
589
+ const connectivityEntries = this.connectivityEntry.map((entry) => entry.id);
527
590
  const flatmapAPI = this.settingsStore.flatmapAPI;
528
591
  const { viewingMode } = this.settingsStore.globalSettings;
529
592
  const knowledgeSource = this.connectivityEntry[0].mapuuid || '';
@@ -532,11 +595,12 @@ export default {
532
595
 
533
596
  // fetch forward/backward connections on Neuron Connection mode
534
597
  if (viewingMode === 'Neuron Connection' && connectivityEntries.length && knowledgeSource) {
535
- forwardBackwardConnections = await queryForwardBackwardConnections(flatmapAPI, knowledgeSource, connectivityEntries);
536
- const allConnections = [
537
- ...connectivityEntries,
538
- ...forwardBackwardConnections,
539
- ];
598
+ forwardBackwardConnections = await queryForwardBackwardConnections(
599
+ flatmapAPI,
600
+ knowledgeSource,
601
+ connectivityEntries,
602
+ );
603
+ const allConnections = [...connectivityEntries, ...forwardBackwardConnections];
540
604
  const availableConnectivities = this.connectivitiesStore.getUniqueConnectivitiesByKeys;
541
605
  mappedConnections = allConnections.map((connId) => {
542
606
  return availableConnectivities.find((ac) => ac.id === connId);
@@ -547,12 +611,12 @@ export default {
547
611
  if (forwardBackwardConnections.length) {
548
612
  this.connectivityEntry = [];
549
613
  this.connectivityKnowledge = mappedConnections;
550
- this.connectivityHighlight = this.connectivityKnowledge.map(ck => ck.id);
614
+ this.connectivityHighlight = this.connectivityKnowledge.map((ck) => ck.id);
551
615
  this.connectivityProcessed = true;
552
616
  } else {
553
617
  this.connectivityKnowledge = this.connectivityEntry;
554
- if (this.connectivityKnowledge.every(ck => ck.ready)) {
555
- this.connectivityHighlight = this.connectivityKnowledge.map(ck => ck.id);
618
+ if (this.connectivityKnowledge.every((ck) => ck.ready)) {
619
+ this.connectivityHighlight = this.connectivityKnowledge.map((ck) => ck.id);
556
620
  this.connectivityProcessed = true;
557
621
  }
558
622
  }
@@ -567,7 +631,7 @@ export default {
567
631
  this.annotationEntry = payload.annotationEntry;
568
632
  // If drawing, `entry.models` may be undefined; use an empty array instead of [undefined]
569
633
  // to prevent errors on highlight
570
- this.annotationHighlight = this.annotationEntry.map(entry => entry.models).filter(Boolean);
634
+ this.annotationHighlight = this.annotationEntry.map((entry) => entry.models).filter(Boolean);
571
635
  if (payload.commitCallback) {
572
636
  this.annotationCallback = markRaw(payload.commitCallback);
573
637
  }
@@ -589,7 +653,7 @@ export default {
589
653
  this.confirmCommentCallback = markRaw(payload.confirmComment);
590
654
  }
591
655
  if (this.$refs.sideBar) {
592
- this.$refs.sideBar.tabClicked({id: 3, type: 'annotation'});
656
+ this.$refs.sideBar.tabClicked({ id: 3, type: 'annotation' });
593
657
  this.$refs.sideBar.setDrawerOpen(true);
594
658
  }
595
659
  },
@@ -609,74 +673,95 @@ export default {
609
673
  connectivityInfo: data.connectivityInfo,
610
674
  resource: data.entry,
611
675
  type: 'ConnectivityGraph',
612
- label: data.title || data.label || data.entry ||'Connectivity Graph',
676
+ label: data.title || data.label || data.entry || 'Connectivity Graph',
613
677
  graphPayload: { ...data },
614
678
  mapServer: this.settingsStore.flatmapAPI,
615
679
  sckanVersion: data.sckanVersion,
616
680
  });
617
681
 
618
- this.splitFlowStore.updateActiveView({
619
- view: '2vertpanel',
620
- entries: this.entries,
621
- }, false);
682
+ this.splitFlowStore.updateActiveView(
683
+ {
684
+ view: '2vertpanel',
685
+ entries: this.entries,
686
+ },
687
+ false,
688
+ );
622
689
 
623
690
  if (previousPrimaryId && previousPrimaryId !== connectivityGraphId) {
624
- this.splitFlowStore.assignOrSwapPaneWithIds({
625
- source: connectivityGraphId,
626
- target: previousPrimaryId,
627
- }, false);
691
+ this.splitFlowStore.assignOrSwapPaneWithIds(
692
+ {
693
+ source: connectivityGraphId,
694
+ target: previousPrimaryId,
695
+ },
696
+ false,
697
+ );
628
698
  }
629
699
 
630
700
  const secondPaneId = this.splitFlowStore.customLayout?.['pane-2']?.id;
631
701
  if (secondPaneId && secondPaneId !== connectivityGraphId) {
632
- this.splitFlowStore.assignOrSwapPaneWithIds({
633
- source: connectivityGraphId,
634
- target: secondPaneId,
635
- }, false);
702
+ this.splitFlowStore.assignOrSwapPaneWithIds(
703
+ {
704
+ source: connectivityGraphId,
705
+ target: secondPaneId,
706
+ },
707
+ false,
708
+ );
636
709
  }
637
710
  this.splitFlowStore.updateSplitPanels();
638
711
  },
639
712
  hoverChanged: function (data) {
640
- let hoverAnatomies = [], hoverOrgans = [], hoverDOI = '', hoverConnectivity = [];
713
+ let hoverAnatomies = [],
714
+ hoverOrgans = [],
715
+ hoverDOI = '',
716
+ hoverConnectivity = [];
641
717
  if (data.tabType === 'dataset') {
642
718
  hoverAnatomies = data.anatomy ? data.anatomy : [];
643
719
  hoverOrgans = data.organs ? data.organs : [];
644
720
  hoverDOI = data.doi ? data.doi : '';
645
721
  } else if (data.tabType === 'connectivity') {
646
722
  hoverConnectivity = data.id ? [data.id] : this.connectivityHighlight;
647
- hoverOrgans = data['nerve-label'] ? data['nerve-label'].flatMap(nerve => nerve.subNerves) : [];
723
+ hoverOrgans = data['nerve-label']
724
+ ? data['nerve-label'].flatMap((nerve) => nerve.subNerves)
725
+ : [];
648
726
  } else if (data.tabType === 'annotation') {
649
727
  hoverConnectivity = data.models ? [data.models] : this.annotationHighlight;
650
728
  }
651
- this.settingsStore.updateHoverFeatures(hoverAnatomies, hoverOrgans, hoverDOI, hoverConnectivity);
652
- EventBus.emit("hoverUpdate", { connectivityProcessed: this.connectivityProcessed });
729
+ this.settingsStore.updateHoverFeatures(
730
+ hoverAnatomies,
731
+ hoverOrgans,
732
+ hoverDOI,
733
+ hoverConnectivity,
734
+ );
735
+ EventBus.emit('hoverUpdate', { connectivityProcessed: this.connectivityProcessed });
653
736
  },
654
737
  searchChanged: function (data) {
655
738
  if (data.tabType === 'dataset') {
656
- if (data && data.type == "reset-update") {
739
+ if (data && data.type == 'reset-update') {
657
740
  this.settingsStore.updateAppliedFacets([]);
658
741
  }
659
- if (data && data.type == "query-update") {
742
+ if (data && data.type == 'query-update') {
660
743
  this.search = data.value;
661
744
  if (this.search && !this.filterTriggered) {
662
745
  // GA Tagging
663
746
  // Event tracking for map action search/filter data
664
747
  Tagging.sendEvent({
665
- 'event': 'interaction_event',
666
- 'event_name': 'portal_maps_action_search',
667
- 'category': this.search,
668
- 'location': 'map_sidebar_dataset_search'
748
+ event: 'interaction_event',
749
+ event_name: 'portal_maps_action_search',
750
+ category: this.search,
751
+ location: 'map_sidebar_dataset_search',
669
752
  });
670
753
  }
671
754
  this.filterTriggered = false; // reset for next action
672
755
  }
673
- if (data && data.type == "filter-update") {
756
+ if (data && data.type == 'filter-update') {
674
757
  this.settingsStore.updateFacets(data.value);
675
758
  // Remove filter event from maps' popup
676
759
  if (!this.filterTriggered) {
677
- const filterValuesArray = data.value.filter((val) => {
678
- return val.facet && val.facet.toLowerCase() !== 'show all';
679
- }).map((val) => val.facet);
760
+ const filterValuesArray = data.value
761
+ .filter((val) => {
762
+ return val.facet && val.facet.toLowerCase() !== 'show all';
763
+ })
764
+ .map((val) => val.facet);
680
765
  const labels = filterValuesArray.map((val) => val.toLowerCase());
681
766
  const newFacets = [...new Set([...labels])];
682
767
  this.settingsStore.updateAppliedFacets(newFacets);
@@ -684,16 +769,16 @@ export default {
684
769
  // GA Tagging
685
770
  // Event tracking for map action search/filter data
686
771
  Tagging.sendEvent({
687
- 'event': 'interaction_event',
688
- 'event_name': 'portal_maps_action_filter',
689
- 'category': filterValues || 'filter_reset',
690
- 'location': 'map_sidebar_dataset_filter'
772
+ event: 'interaction_event',
773
+ event_name: 'portal_maps_action_filter',
774
+ category: filterValues || 'filter_reset',
775
+ location: 'map_sidebar_dataset_filter',
691
776
  });
692
777
  }
693
778
  this.filterTriggered = false; // reset for next action
694
779
  }
695
780
  } else if (data.tabType === 'connectivity') {
696
- if (data && data.type == "reset-update") {
781
+ if (data && data.type == 'reset-update') {
697
782
  const activeFlatmaps = this.getActiveFlatmaps();
698
783
  activeFlatmaps.forEach((activeFlatmap) => {
699
784
  activeFlatmap.resetConnectivityfilters(data.value);
@@ -706,28 +791,29 @@ export default {
706
791
  activeFlatmaps.forEach((activeFlatmap) => {
707
792
  activeFlatmap.updateConnectivityFilters(data.filter);
708
793
  });
709
- EventBus.emit("connectivity-query-filter", data);
794
+ EventBus.emit('connectivity-query-filter', data);
710
795
 
711
- const filterValues = data.filter.filter(f => (f.facet && f.facet.toLowerCase() !== 'show all'))
796
+ const filterValues = data.filter
797
+ .filter((f) => f.facet && f.facet.toLowerCase() !== 'show all')
712
798
  .map((f) => f.tagLabel)
713
799
  .join(', ');
714
800
  const searchValue = data.query;
715
801
 
716
802
  if (filterValues) {
717
803
  Tagging.sendEvent({
718
- 'event': 'interaction_event',
719
- 'event_name': 'portal_maps_action_filter',
720
- 'category': filterValues,
721
- 'location': 'map_sidebar_connectivity_filter'
804
+ event: 'interaction_event',
805
+ event_name: 'portal_maps_action_filter',
806
+ category: filterValues,
807
+ location: 'map_sidebar_connectivity_filter',
722
808
  });
723
809
  }
724
810
 
725
811
  if (searchValue) {
726
812
  Tagging.sendEvent({
727
- 'event': 'interaction_event',
728
- 'event_name': 'portal_maps_action_search',
729
- 'category': searchValue,
730
- 'location': 'map_sidebar_connectivity_search'
813
+ event: 'interaction_event',
814
+ event_name: 'portal_maps_action_search',
815
+ category: searchValue,
816
+ location: 'map_sidebar_connectivity_search',
731
817
  });
732
818
  }
733
819
  }
@@ -735,31 +821,41 @@ export default {
735
821
  },
736
822
  updateMarkers: function (data) {
737
823
  this.settingsStore.updateMarkers(data);
738
- EventBus.emit("markerUpdate");
824
+ EventBus.emit('markerUpdate');
739
825
  },
740
826
  updateScaffoldMarkers: function (data) {
741
827
  this.settingsStore.updateNumberOfDatasetsForFacets(data);
742
828
  },
743
- getNewEntryId: function() {
829
+ getNewEntryId: function () {
744
830
  if (this.entries.length) {
745
- return (this.entries[this.entries.length - 1]).id + 1;
831
+ return this.entries[this.entries.length - 1].id + 1;
746
832
  }
747
833
  return 1;
748
834
  },
749
835
  /**
750
836
  * Add new entry which will sequentially create a
751
837
  * new dialog.
838
+ * Set `options.openInSplitView` with `options.sourceId` to open the
839
+ * new entry next to the source entry instead of the primary pane.
752
840
  */
753
- createNewEntry: function (data) {
841
+ createNewEntry: function (data, options = {}) {
754
842
  let newEntry = {};
755
843
  newEntry.viewUrl = undefined;
756
844
  newEntry.state = undefined;
757
845
  Object.assign(newEntry, data);
758
- newEntry.mode = "normal";
846
+ newEntry.mode = 'normal';
759
847
  newEntry.id = this.getNewEntryId();
760
848
  newEntry.discoverId = data.discoverId;
761
849
  this.entriesStore.addNewEntry(newEntry);
762
- this.splitFlowStore.setIdToPrimaryPane(newEntry.id);
850
+ if (options.openInSplitView) {
851
+ this.splitFlowStore.placeEntryInSplitView({
852
+ id: newEntry.id,
853
+ sourceId: options.sourceId,
854
+ entries: this.entries,
855
+ });
856
+ } else {
857
+ this.splitFlowStore.setIdToPrimaryPane(newEntry.id);
858
+ }
763
859
  //close sidebar on entry creation to see the context card
764
860
  if (this.$refs.sideBar) {
765
861
  this.$refs.sideBar.setDrawerOpen(false);
@@ -771,7 +867,7 @@ export default {
771
867
  const entry = await getNewMapEntry(type, this.settingsStore.sparcApi);
772
868
  this.createNewEntry(entry);
773
869
  if (entry.contextCard) {
774
- EventBus.emit("contextUpdate", entry.contextCard);
870
+ EventBus.emit('contextUpdate', entry.contextCard);
775
871
  }
776
872
  },
777
873
  openSearch: function (facets, query) {
@@ -784,12 +880,12 @@ export default {
784
880
  this._facets = facets;
785
881
  if (this.$refs && this.$refs.sideBar) {
786
882
  this.$refs.sideBar.openSearch(facets, query);
787
- this.$refs.sideBar.tabClicked({id: 1, type: 'datasetExplorer'});
883
+ this.$refs.sideBar.tabClicked({ id: 1, type: 'datasetExplorer' });
788
884
  }
789
885
  this.startUp = false;
790
886
  },
791
887
  onFullscreen: function (val) {
792
- this.$emit("onFullscreen", val);
888
+ this.$emit('onFullscreen', val);
793
889
  },
794
890
  resetApp: function () {
795
891
  this.setState(initialDefaultState());
@@ -800,39 +896,118 @@ export default {
800
896
  restoreConnectivityEntries: function (connectivityEntries) {
801
897
  const activeFlatmaps = this.getActiveFlatmaps();
802
898
  activeFlatmaps.forEach((activeFlatmap) => {
803
- const featureIds = connectivityEntries.map((entry) => {
804
- const featureId = activeFlatmap.mapImp.modelFeatureIds(entry)[0];
805
- const feature = activeFlatmap.mapImp.featureProperties(featureId);
806
- const data = {
807
- resource: [feature.models],
808
- feature: feature,
809
- label: feature.label,
810
- provenanceTaxonomy: feature.taxons,
811
- alert: feature.alert,
812
- };
813
- return data;
814
- });
815
- activeFlatmap.checkAndCreatePopups(featureIds, true)
899
+ if (!activeFlatmap.mapImp) return;
900
+ const featureIds = connectivityEntries.reduce((acc, entry) => {
901
+ // A given flatmap may not contain every restored connectivity id
902
+ // (e.g. it belongs to another species/map); skip those instead of
903
+ // throwing, which would otherwise abort processing for this map
904
+ // entirely and leave its other entries stuck loading forever.
905
+ try {
906
+ const featureId = activeFlatmap.mapImp.modelFeatureIds(entry)[0];
907
+ if (!featureId) return acc;
908
+ const feature = activeFlatmap.mapImp.featureProperties(featureId);
909
+ if (!feature) return acc;
910
+ acc.push({
911
+ resource: [feature.models],
912
+ feature: feature,
913
+ label: feature.label,
914
+ provenanceTaxonomy: feature.taxons,
915
+ alert: feature.alert,
916
+ });
917
+ } catch (_error) {
918
+ // ignore entries that cannot be resolved in this map
919
+ }
920
+ return acc;
921
+ }, []);
922
+ if (featureIds.length) {
923
+ activeFlatmap.checkAndCreatePopups(featureIds, true);
924
+ }
925
+ });
926
+
927
+ // Restore Scaffold-only connectivity entries (e.g. carrying nerve-label)
928
+ // via each Scaffold's own resolved knowledge, since Scaffolds aren't
929
+ // flatmaps and are otherwise never asked to restore their entries.
930
+ const activeScaffolds = this.getActiveScaffolds();
931
+ activeScaffolds.forEach((scaffold) => {
932
+ const matched = (scaffold.nervesKnowledge || []).filter((knowledge) =>
933
+ connectivityEntries.includes(knowledge.id),
934
+ );
935
+ if (matched.length) {
936
+ scaffold.getKnowledgeTooltip({ data: matched, type: scaffold.entry });
937
+ }
816
938
  });
817
939
  },
940
+ // Maps a sidebar activeTabId to the tab 'type' expected by tabClicked.
941
+ getSidebarTabType: function (tabId) {
942
+ if (tabId === 1) return 'datasetExplorer';
943
+ if (tabId === 3) return 'annotation';
944
+ return 'connectivityExplorer';
945
+ },
946
+ // Called once every restored connectivityEntry has resolved (or we've
947
+ // given up retrying): shows only the restored/filtered connectivities
948
+ // (not the full knowledge list) and opens the sidebar on the tab stored
949
+ // in the permalink state.
950
+ finalizeConnectivityRestore: function (sidebarState) {
951
+ this.restoringConnectivityState = false;
952
+ this.connectivityKnowledge = this.connectivityEntry;
953
+ if (this.connectivityEntry.every((entry) => entry.ready)) {
954
+ this.connectivityHighlight = this.connectivityEntry.map((entry) => entry.id);
955
+ this.connectivityProcessed = true;
956
+ }
957
+ if (this.$refs.sideBar) {
958
+ const tabId = sidebarState?.activeTabId ?? 2;
959
+ this.$refs.sideBar.tabClicked({ id: tabId, type: this.getSidebarTabType(tabId) });
960
+ this.$refs.sideBar.setDrawerOpen(true);
961
+ }
962
+ },
963
+ // Shared completion check for the connectivity restore loop:
964
+ // finalises the restore once every restored entry has resolved,
965
+ // or gives up after enough retry attempts (an id may never be resolvable by any active viewer).
966
+ // Called both from the connectivity-knowledge retry ticks and from openConnectivityInfo,
967
+ // since entries resolve asynchronously through connectivity-info-open rather than the knowledge events.
968
+ checkConnectivityRestoreComplete: function (sidebarState) {
969
+ if (this.sidebarStateRestored || !sidebarState?.connectivityEntries?.length) {
970
+ return;
971
+ }
972
+ // Multiple viewers resolve connectivity knowledge independently,
973
+ // so keep retrying until all restored entries have resolved.
974
+ // A becoming-active flatmap alone is not enough (its knowledge may still be loading),
975
+ // so only finalise on that basis after enough attempts
976
+ // to avoid getting stuck if an id can never be resolved by any viewer.
977
+ const allEntriesReady =
978
+ this.connectivityEntry.length >= sidebarState.connectivityEntries.length &&
979
+ this.connectivityEntry.every((entry) => entry.ready);
980
+ const giveUp = this.restoreAttempts >= 30;
981
+ if (allEntriesReady || giveUp) {
982
+ this.sidebarStateRestored = true;
983
+ this.finalizeConnectivityRestore(sidebarState);
984
+ }
985
+ },
818
986
  restoreSidebarState: function (state) {
819
987
  // Restore sidebar state only if
820
988
  // - there is sidebar state
821
989
  // - sidebar component is loaded
822
- // - connectivity knowledge is loaded
823
990
  // - if sidebar state is not restored yet
824
991
  const sidebarState = state?.sidebar;
825
- if (!this.sidebarStateRestored && sidebarState && this.$refs.sideBar && this.connectivityKnowledge?.length) {
992
+ if (
993
+ !this.sidebarStateRestored &&
994
+ sidebarState &&
995
+ this.$refs.sideBar &&
996
+ this.connectivityKnowledge?.length
997
+ ) {
826
998
  if (sidebarState.connectivityEntries?.length) {
827
999
  this.restoreConnectivityEntries(sidebarState.connectivityEntries);
1000
+ this.restoreAttempts += 1;
1001
+ this.checkConnectivityRestoreComplete(sidebarState);
828
1002
  } else if (sidebarState.annotationEntries?.length && state.annotationId) {
829
1003
  // Restore annotation state only if the state has annotationId
830
1004
  this.restoreConnectivityEntries(sidebarState.annotationEntries);
831
1005
  this.sidebarAnnotationState = true;
1006
+ this.sidebarStateRestored = true;
832
1007
  } else {
833
1008
  this.$refs.sideBar.setState(sidebarState);
1009
+ this.sidebarStateRestored = true;
834
1010
  }
835
- this.sidebarStateRestored = true;
836
1011
  }
837
1012
  },
838
1013
  setState: function (state) {
@@ -840,9 +1015,23 @@ export default {
840
1015
  //Support both old and new permalink.
841
1016
  if (state.splitFlow) {
842
1017
  this.splitFlowStore.setState(state.splitFlow);
1018
+ } else {
1019
+ this.entries.forEach((entry) => this.splitFlowStore.setIdToPrimaryPane(entry.id));
843
1020
  }
844
- else {
845
- this.entries.forEach(entry => this.splitFlowStore.setIdToPrimaryPane(entry.id));
1021
+
1022
+ // Seed placeholder (not-ready) connectivity entries immediately so the
1023
+ // sidebar shows a loading count right away, instead of either an empty
1024
+ // list or a flash of the full/unfiltered connectivity knowledge while
1025
+ // the active viewers and their connectivity knowledge are still loading.
1026
+ if (state?.sidebar?.connectivityEntries?.length && !this.sidebarStateRestored) {
1027
+ this.restoringConnectivityState = true;
1028
+ this.connectivityEntry = state.sidebar.connectivityEntries.map((id) => ({
1029
+ id,
1030
+ label: '',
1031
+ featureId: [id],
1032
+ ready: false,
1033
+ }));
1034
+ this.connectivityKnowledge = this.connectivityEntry;
846
1035
  }
847
1036
 
848
1037
  this.restoreSidebarState(state);
@@ -857,12 +1046,10 @@ export default {
857
1046
  const entry = state.entries[i];
858
1047
  entry.state = dialogStates[i];
859
1048
  //We do not want to serialise the following properties
860
- if (entry.type === "Scaffold" && "viewUrl" in entry)
861
- delete entry.viewUrl;
862
- if (entry.type === "MultiFlatmap" && "uberonId" in entry)
863
- delete entry.uberonId;
1049
+ if (entry.type === 'Scaffold' && 'viewUrl' in entry) delete entry.viewUrl;
1050
+ if (entry.type === 'MultiFlatmap' && 'uberonId' in entry) delete entry.uberonId;
864
1051
  if (anonymousAnnotations === false) {
865
- if (entry.type === "Scaffold" && entry?.state?.offlineAnnotations) {
1052
+ if (entry.type === 'Scaffold' && entry?.state?.offlineAnnotations) {
866
1053
  delete entry.state.offlineAnnotations;
867
1054
  } else if (entry?.state?.state?.offlineAnnotations) {
868
1055
  delete entry.state.state.offlineAnnotations;
@@ -882,20 +1069,20 @@ export default {
882
1069
  this.entriesStore.destroyEntry(index);
883
1070
  },
884
1071
  resourceSelected: function (result) {
885
- this.$emit("resource-selected", result);
1072
+ this.$emit('resource-selected', result);
886
1073
  },
887
- speciesChanged: function (species) {
1074
+ speciesChanged: function (_species) {
888
1075
  if (this.$refs.sideBar) {
889
1076
  // Use to update the connectivity when switch species
890
1077
  // Wait for provenance info with uuid update
891
1078
  this.$nextTick(() => {
892
1079
  EventBus.emit('species-layout-connectivity-update');
893
1080
  this.$refs.sideBar.close();
894
- })
1081
+ });
895
1082
  }
896
1083
  },
897
1084
  contextUpdate: function (payload) {
898
- EventBus.emit("contextUpdate", payload);
1085
+ EventBus.emit('contextUpdate', payload);
899
1086
  },
900
1087
  datalinkClicked: function (payload) {
901
1088
  // payload is dataset URL
@@ -904,37 +1091,37 @@ export default {
904
1091
  const substringB = '?type=dataset';
905
1092
  const datasetId = datasetURL.substring(
906
1093
  datasetURL.indexOf(substringA) + substringA.length,
907
- datasetURL.indexOf(substringB)
1094
+ datasetURL.indexOf(substringB),
908
1095
  );
909
1096
 
910
1097
  // GA Tagging
911
1098
  // Event tracking for map sidebar gallery dataset click
912
1099
  Tagging.sendEvent({
913
- 'event': 'interaction_event',
914
- 'event_name': 'portal_maps_gallery_click',
915
- 'category': datasetURL,
916
- 'location': 'map_sidebar_gallery',
917
- 'dataset_id': datasetId || ''
1100
+ event: 'interaction_event',
1101
+ event_name: 'portal_maps_gallery_click',
1102
+ category: datasetURL,
1103
+ location: 'map_sidebar_gallery',
1104
+ dataset_id: datasetId || '',
918
1105
  });
919
1106
  },
920
- onAnnotationSubmitted: function(annotation) {
1107
+ onAnnotationSubmitted: function (annotation) {
921
1108
  if (this.annotationCallback) {
922
1109
  this.annotationCallback(annotation);
923
1110
  } else if (this.confirmCommentCallback) {
924
- this.confirmCommentCallback(annotation)
1111
+ this.confirmCommentCallback(annotation);
925
1112
  }
926
1113
  },
927
- onConfirmCreate: function(payload) {
1114
+ onConfirmCreate: function (payload) {
928
1115
  if (this.confirmCreateCallback) {
929
1116
  this.confirmCreateCallback(payload);
930
1117
  }
931
1118
  },
932
- onCancelCreate: function() {
1119
+ onCancelCreate: function () {
933
1120
  if (this.cancelCreateCallback) {
934
1121
  this.cancelCreateCallback();
935
1122
  }
936
1123
  },
937
- onConfirmDelete: function(payload) {
1124
+ onConfirmDelete: function (payload) {
938
1125
  if (this.confirmDeleteCallback) {
939
1126
  this.confirmDeleteCallback(payload);
940
1127
  }
@@ -954,7 +1141,7 @@ export default {
954
1141
  this.$refs.dialogToolbar.loadGlobalSettings();
955
1142
  },
956
1143
  onSidebarTabClosed: function (tab) {
957
- if (tab.id === 3 && tab.type === "annotation") {
1144
+ if (tab.id === 3 && tab.type === 'annotation') {
958
1145
  EventBus.emit('sidebar-annotation-close');
959
1146
  }
960
1147
  },
@@ -979,16 +1166,16 @@ export default {
979
1166
  this._externalStateSet = false;
980
1167
  },
981
1168
  mounted: function () {
982
- EventBus.on("CreateNewEntry", newView => {
1169
+ EventBus.on('CreateNewEntry', (newView) => {
983
1170
  this.createNewEntry(newView);
984
1171
  });
985
- EventBus.on("RemoveEntryRequest", id => {
1172
+ EventBus.on('RemoveEntryRequest', (id) => {
986
1173
  this.removeEntry(id);
987
1174
  });
988
- EventBus.on("PopoverActionClick", payload => {
1175
+ EventBus.on('PopoverActionClick', (payload) => {
989
1176
  this.actionClick(payload);
990
1177
  });
991
- EventBus.on('annotation-open', payload => {
1178
+ EventBus.on('annotation-open', (payload) => {
992
1179
  this.openAnnotation(payload);
993
1180
  });
994
1181
  EventBus.on('sidebar-annotation-close', () => {
@@ -1000,7 +1187,7 @@ export default {
1000
1187
  // open the anootation tab and return.
1001
1188
  if (this.sidebarAnnotationState && viewingMode === 'Annotation') {
1002
1189
  this.sidebarAnnotationState = false;
1003
- this.$refs.sideBar.tabClicked({id: 3, type: 'annotation'});
1190
+ this.$refs.sideBar.tabClicked({ id: 3, type: 'annotation' });
1004
1191
  return;
1005
1192
  }
1006
1193
 
@@ -1008,10 +1195,10 @@ export default {
1008
1195
  this.createData = {};
1009
1196
 
1010
1197
  if (this.$refs.sideBar) {
1011
- if (interactiveMode === "dataset") {
1012
- this.$refs.sideBar.tabClicked({id: 1, type: 'datasetExplorer'});
1013
- } else if (interactiveMode === "connectivity") {
1014
- this.$refs.sideBar.tabClicked({id: 2, type: 'connectivityExplorer'});
1198
+ if (interactiveMode === 'dataset') {
1199
+ this.$refs.sideBar.tabClicked({ id: 1, type: 'datasetExplorer' });
1200
+ } else if (interactiveMode === 'connectivity') {
1201
+ this.$refs.sideBar.tabClicked({ id: 2, type: 'connectivityExplorer' });
1015
1202
  }
1016
1203
 
1017
1204
  this.$refs.sideBar.closeConnectivity();
@@ -1021,21 +1208,21 @@ export default {
1021
1208
  EventBus.on('update-offline-annotation-enabled', (payload) => {
1022
1209
  this.settingsStore.updateOfflineAnnotationEnabled(payload);
1023
1210
  });
1024
- EventBus.on('connectivity-info-open', payload => {
1211
+ EventBus.on('connectivity-info-open', (payload) => {
1025
1212
  this.openConnectivityInfo(payload);
1026
1213
  });
1027
- EventBus.on('connectivity-info-close', payload => {
1214
+ EventBus.on('connectivity-info-close', (_payload) => {
1028
1215
  if (this.$refs.sideBar) {
1029
1216
  this.connectivityProcessed = false;
1030
1217
  this.$refs.sideBar.resetConnectivitySearch();
1031
1218
  }
1032
1219
  });
1033
- EventBus.on('connectivity-error', payload => {
1220
+ EventBus.on('connectivity-error', (payload) => {
1034
1221
  if (this.$refs.sideBar) {
1035
1222
  this.$refs.sideBar.updateConnectivityError(payload.data);
1036
1223
  }
1037
1224
  });
1038
- EventBus.on('neuron-connection-feature-click', payload => {
1225
+ EventBus.on('neuron-connection-feature-click', (payload) => {
1039
1226
  if (this.$refs.sideBar) {
1040
1227
  const { filters, search } = payload;
1041
1228
  this.$refs.sideBar.openConnectivitySearch(filters, search);
@@ -1043,36 +1230,44 @@ export default {
1043
1230
  this.$refs.sideBar.setDrawerOpen(true);
1044
1231
  }
1045
1232
  });
1046
- EventBus.on("OpenNewMap", type => {
1233
+ EventBus.on('OpenNewMap', (type) => {
1047
1234
  this.updateFlatmapMinimap();
1048
1235
  this.openNewMap(type);
1049
1236
  });
1050
- EventBus.on("startHelp", () => {
1237
+ EventBus.on('startHelp', () => {
1051
1238
  if (this.$refs.sideBar) {
1052
1239
  this.$refs.sideBar.close();
1053
1240
  }
1054
1241
  });
1055
- EventBus.on("connectivity-knowledge", payload => {
1056
- this.connectivityKnowledge = payload.data;
1057
- this.connectivityHighlight = payload.highlight;
1058
- this.connectivityProcessed = payload.processed;
1242
+ EventBus.on('connectivity-knowledge', (payload) => {
1243
+ // Ignore the default/global connectivity dump (emitted when no active
1244
+ // viewer's map/species keys are resolved yet) while restoring a
1245
+ // permalink's sidebar connectivityEntries, otherwise the sidebar would
1246
+ // briefly flash the entire, unfiltered connectivity list.
1247
+ if (!(payload.isDefaultKnowledge && this.restoringConnectivityState)) {
1248
+ this.connectivityKnowledge = payload.data;
1249
+ this.connectivityHighlight = payload.highlight;
1250
+ this.connectivityProcessed = payload.processed;
1251
+ }
1059
1252
 
1060
1253
  // Restore sidebar state if it exists and not restored yet
1061
1254
  // after loading connectivity knowledge
1062
- this.restoreSidebarState(this.state);
1063
- })
1064
- EventBus.on("modeUpdate", payload => {
1065
- if (payload === "dataset") {
1066
- this.$refs.sideBar.tabClicked({id: 1, type: 'datasetExplorer'});
1067
- } else if (payload === "connectivity") {
1068
- this.$refs.sideBar.tabClicked({id: 2, type: 'connectivityExplorer'});
1255
+ if (!this.sidebarStateRestored) {
1256
+ this.restoreSidebarState(this.state);
1257
+ }
1258
+ });
1259
+ EventBus.on('modeUpdate', (payload) => {
1260
+ if (payload === 'dataset') {
1261
+ this.$refs.sideBar.tabClicked({ id: 1, type: 'datasetExplorer' });
1262
+ } else if (payload === 'connectivity') {
1263
+ this.$refs.sideBar.tabClicked({ id: 2, type: 'connectivityExplorer' });
1069
1264
  }
1070
- })
1071
- EventBus.on("connectivity-filter-options", payload => {
1265
+ });
1266
+ EventBus.on('connectivity-filter-options', (payload) => {
1072
1267
  this.filterOptions = payload;
1073
- })
1268
+ });
1074
1269
  this.$nextTick(() => {
1075
- if (this.search === "" && this._facets.length === 0) {
1270
+ if (this.search === '' && this._facets.length === 0) {
1076
1271
  if (this.$refs.sideBar) {
1077
1272
  this.$refs.sideBar.close();
1078
1273
  }
@@ -1095,7 +1290,7 @@ export default {
1095
1290
  FLATMAPAPI_LOCATION: this.settingsStore.flatmapAPI,
1096
1291
  };
1097
1292
  },
1098
- entries: function() {
1293
+ entries: function () {
1099
1294
  return this.entriesStore.entries;
1100
1295
  },
1101
1296
  },
@@ -1139,5 +1334,4 @@ export default {
1139
1334
  }
1140
1335
  }
1141
1336
  }
1142
-
1143
1337
  </style>