@abi-software/map-side-bar 2.19.0 → 2.19.1-demo.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.
@@ -33,6 +33,7 @@
33
33
 
34
34
  <script>
35
35
  import EventBus from './EventBus';
36
+ import { capitalise as capitaliseText } from '../utils/common.js';
36
37
 
37
38
  export default {
38
39
  name: 'ConnectivityCard',
@@ -81,8 +82,7 @@ export default {
81
82
  },
82
83
  methods: {
83
84
  capitalise: function (text) {
84
- if (text) return text.charAt(0).toUpperCase() + text.slice(1);
85
- return '';
85
+ return capitaliseText(text);
86
86
  },
87
87
  cardClicked: function (data) {
88
88
  if (!this.loading) {
@@ -343,6 +343,11 @@ import {
343
343
  ExternalResourceCard,
344
344
  } from '@abi-software/map-utilities';
345
345
  import '@abi-software/map-utilities/dist/style.css';
346
+ import {
347
+ capitalise,
348
+ formatAlertText as formatAlertTextUtil,
349
+ scrollToRef,
350
+ } from '../utils/common.js';
346
351
 
347
352
  const titleCase = (str) => {
348
353
  return str.replace(/\w\S*/g, (t) => {
@@ -350,11 +355,6 @@ const titleCase = (str) => {
350
355
  });
351
356
  };
352
357
 
353
- const capitalise = function (str) {
354
- if (str) return str.charAt(0).toUpperCase() + str.slice(1);
355
- return '';
356
- };
357
-
358
358
  export default {
359
359
  name: 'ConnectivityInfo',
360
360
  components: {
@@ -935,6 +935,9 @@ export default {
935
935
  return null;
936
936
  }
937
937
 
938
+ //A scicrunch entry typically looks like https://scicrunch.org/resolver/RRID:XXXXXX
939
+ //but due to CORS issue, we need to replace it with sparc-api interface,
940
+ //Replacing the domain while leaving the subpath the same.
938
941
  const APIURL = isScicrunchURL
939
942
  ? url.replace(scicrunchBase, `${apiLocationBase}scicrunch`)
940
943
  : url.replace(orcidBase, orcidAPIBase);
@@ -1159,40 +1162,10 @@ export default {
1159
1162
  EventBus.emit('trackEvent', data);
1160
1163
  },
1161
1164
  showAlertMessage: function () {
1162
- // scroll to alert message
1163
- this.$nextTick(() => {
1164
- const alertElement = this.$refs.alertElement;
1165
- if (alertElement) {
1166
- alertElement.scrollIntoView({
1167
- behavior: 'smooth',
1168
- block: 'start',
1169
- inline: 'nearest',
1170
- });
1171
- }
1172
- });
1165
+ scrollToRef(this, 'alertElement');
1173
1166
  },
1174
1167
  formatAlertText: function (text) {
1175
- if (!text) return '';
1176
- const escaped = text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1177
- const linkified = escaped.replace(/(https?:\/\/[^\s"<>[]+)/g, (url) => {
1178
- const parts = url.match(/^(.*?)([\].,;:!?]*)$/);
1179
- const cleanUrl = parts ? parts[1] : url;
1180
- const suffix = parts ? parts[2] : '';
1181
- return `<a href="${cleanUrl}" target="_blank" rel="noopener noreferrer">${cleanUrl}</a>${suffix}`;
1182
- });
1183
-
1184
- const normalised = linkified
1185
- .replace(/\\n/g, '\n')
1186
- .replace(/\r\n/g, '\n')
1187
- .replace(/\r/g, '\n');
1188
-
1189
- return normalised
1190
- .split('\n')
1191
- .map((line) => {
1192
- const withBoldLabel = line.replace(/^\s*([A-Za-z][^:<]{0,120}:)/, '<strong>$1</strong>');
1193
- return `<div class="alert-line">${withBoldLabel}</div>`;
1194
- })
1195
- .join('\n');
1168
+ return formatAlertTextUtil(text, { formatLines: true });
1196
1169
  },
1197
1170
  },
1198
1171
  mounted: function () {
@@ -143,10 +143,7 @@ import '@abi-software/svg-sprite/dist/style.css';
143
143
  import { AlgoliaClient } from '../algolia/algolia.js';
144
144
  import { facetPropPathMapping } from '../algolia/utils.js';
145
145
  import EventBus from './EventBus.js';
146
-
147
- const capitalise = function (txt) {
148
- return txt.charAt(0).toUpperCase() + txt.slice(1);
149
- };
146
+ import { capitalise } from '../utils/common.js';
150
147
 
151
148
  const convertReadableLabel = function (original) {
152
149
  const name = original.toLowerCase();
@@ -273,16 +270,22 @@ export default {
273
270
  return value;
274
271
  },
275
272
  createChildrenCascaderValue: function (children, facet, facets) {
273
+ const parentKey = facet.key;
274
+
276
275
  if (children?.length) {
277
276
  for (let i = 0; i < children.length; i++) {
278
277
  const facetItem = children[i];
279
278
  //copy the facets into
280
279
  if (children[i].facetPropPath !== 'supportingAwards.consortium.name') {
281
- children[i].label = convertReadableLabel(facetItem.label);
280
+ // `sourceNomenclatureLabel` is from cell card explorer filters
281
+ if (parentKey === 'sourceNomenclatureLabel') {
282
+ children[i].label = facetItem.label;
283
+ } else {
284
+ children[i].label = convertReadableLabel(facetItem.label);
285
+ }
282
286
  }
283
- if (facetItem.key && facet.key.includes('flatmap.connectivity.source.')) {
287
+ if (facetItem.key && parentKey.includes('flatmap.connectivity.source.')) {
284
288
  const childKey = facetItem.key;
285
- const parentKey = facet.key;
286
289
  const key = childKey.replace(`${parentKey}.`, '');
287
290
  children[i].value = this.createCascaderItemValue([facet.label, key]);
288
291
  } else {
@@ -125,23 +125,10 @@
125
125
 
126
126
  <script>
127
127
  import EventBus from './EventBus.js';
128
+ import { generateUUID } from '../utils/common.js';
128
129
 
129
130
  const MAX_SEARCH_HISTORY = 12;
130
131
 
131
- function generateUUID() {
132
- const arr = new Uint8Array(16);
133
- window.crypto.getRandomValues(arr);
134
-
135
- arr[6] = (arr[6] & 0x0f) | 0x40;
136
- arr[8] = (arr[8] & 0x3f) | 0x80;
137
-
138
- const hex = Array.from(arr)
139
- .map((byte) => byte.toString(16).padStart(2, '0'))
140
- .join('');
141
-
142
- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
143
- }
144
-
145
132
  export default {
146
133
  name: 'SearchHistory',
147
134
  props: {
@@ -317,7 +304,14 @@ export default {
317
304
  getParentComponentName: function () {
318
305
  const isConnectivity = this.localStorageKey?.indexOf('connectivity') !== -1;
319
306
  const isDataset = this.localStorageKey?.indexOf('dataset') !== -1;
320
- const location = isConnectivity ? 'connectivity' : isDataset ? 'dataset' : '';
307
+ const isCellCard = this.localStorageKey?.indexOf('cell-card') !== -1;
308
+ const location = isConnectivity
309
+ ? 'connectivity'
310
+ : isDataset
311
+ ? 'dataset'
312
+ : isCellCard
313
+ ? 'cell_card'
314
+ : '';
321
315
  return location;
322
316
  },
323
317
  search: function (item) {
@@ -65,6 +65,20 @@
65
65
  @connectivity-item-close="onConnectivityItemClose"
66
66
  />
67
67
  </template>
68
+ <template v-else-if="tab.type === 'cellCardExplorer' && showCellCards">
69
+ <CellCardExplorer
70
+ :ref="'cellCardExplorerTab_' + tab.id"
71
+ class="sidebar-content-container"
72
+ v-show="tab.id === activeTabId"
73
+ :envVars="envVars"
74
+ :activeSpecies="activeSpeciesForEntries"
75
+ @search-changed="searchChanged(tab.id, $event)"
76
+ @dataset-search="openDatasetSearchFromCellCard($event)"
77
+ @connectivity-search="openConnectivitySearch($event.facets, $event.query)"
78
+ @soma-location-hovered="showSomaLocation"
79
+ @soma-locations-ready="onSomaLocationsReady"
80
+ />
81
+ </template>
68
82
  <template v-else>
69
83
  <DatasetExplorer
70
84
  class="sidebar-content-container"
@@ -93,6 +107,7 @@ import EventBus from './EventBus.js';
93
107
  import Tabs from './Tabs.vue';
94
108
  import AnnotationTool from './AnnotationTool.vue';
95
109
  import ConnectivityExplorer from './ConnectivityExplorer.vue';
110
+ import CellCardExplorer from './CellCardExplorer.vue';
96
111
  import { removeShowAllFacets } from '../algolia/utils.js';
97
112
 
98
113
  /**
@@ -106,6 +121,7 @@ export default {
106
121
  ElIconArrowRight,
107
122
  AnnotationTool,
108
123
  ConnectivityExplorer,
124
+ CellCardExplorer,
109
125
  },
110
126
  name: 'SideBar',
111
127
  props: {
@@ -115,6 +131,7 @@ export default {
115
131
  { title: 'Dataset Explorer', id: 1, type: 'datasetExplorer', closable: false },
116
132
  { title: 'Connectivity Explorer', id: 2, type: 'connectivityExplorer', closable: false },
117
133
  { title: 'Annotation', id: 3, type: 'annotation', closable: true },
134
+ { title: 'Cell Card Explorer', id: 4, type: 'cellCardExplorer', closable: false },
118
135
  ],
119
136
  },
120
137
  /**
@@ -180,6 +197,10 @@ export default {
180
197
  type: Boolean,
181
198
  default: false,
182
199
  },
200
+ showCellCards: {
201
+ type: Boolean,
202
+ default: false,
203
+ },
183
204
  },
184
205
  data: function () {
185
206
  return {
@@ -188,6 +209,7 @@ export default {
188
209
  activeTabId: 1,
189
210
  activeAnnotationData: { tabType: 'annotation' },
190
211
  activeConnectivityData: { tabType: 'connectivity' },
212
+ activeSpeciesForEntries: [],
191
213
  state: {
192
214
  dataset: {
193
215
  search: '',
@@ -197,6 +219,10 @@ export default {
197
219
  search: '',
198
220
  filters: [],
199
221
  },
222
+ cellCards: {
223
+ search: '',
224
+ filters: [],
225
+ },
200
226
  connectivityEntries: [],
201
227
  annotationEntries: [],
202
228
  activeTabId: this.activeTabId,
@@ -231,6 +257,16 @@ export default {
231
257
  this.activeAnnotationData = data;
232
258
  }
233
259
  },
260
+ /**
261
+ * This event is emitted when the mouse hover on or off a soma location in cell card explorer.
262
+ * @param name Soma location
263
+ */
264
+ showSomaLocation: function (name) {
265
+ this.$emit('soma-location-hovered', name);
266
+ },
267
+ onSomaLocationsReady: function (somaLocations) {
268
+ this.$emit('soma-locations-ready', somaLocations);
269
+ },
234
270
  /**
235
271
  * This event is emitted when the show connectivity button is clicked.
236
272
  * @arg featureIds
@@ -295,6 +331,42 @@ export default {
295
331
  datasetExplorerTabRef.openSearch(facets, query);
296
332
  });
297
333
  },
334
+ openCellCardExplorerSearch: function (filters, query) {
335
+ this.drawerOpen = true;
336
+ // Because refs are in v-for, nextTick is needed here
337
+ this.$nextTick(() => {
338
+ const cellCardExplorerTabRef = this.getTabRef(undefined, 'cellCardExplorer', true);
339
+ if (cellCardExplorerTabRef && typeof cellCardExplorerTabRef.openSearch === 'function') {
340
+ cellCardExplorerTabRef.openSearch(filters, query);
341
+ }
342
+ });
343
+ },
344
+ openDatasetSearchFromCellCard: function (payload) {
345
+ if (!payload || typeof payload !== 'object') {
346
+ this.openSearch([], payload);
347
+ return;
348
+ }
349
+
350
+ const facets = [];
351
+ if (payload.species) {
352
+ facets.push({
353
+ facet: payload.species,
354
+ term: 'Species',
355
+ facetPropPath: 'organisms.primary.species.name',
356
+ });
357
+ }
358
+
359
+ if (payload.location) {
360
+ facets.push({
361
+ facet: payload.location,
362
+ term: 'Anatomical structure',
363
+ facetPropPath: 'anatomy.organ.category.name',
364
+ AND: true,
365
+ });
366
+ }
367
+
368
+ this.openSearch(facets, '');
369
+ },
298
370
  /**
299
371
  * Get the ref id of the tab by id and type.
300
372
  */
@@ -432,6 +504,13 @@ export default {
432
504
  this.state.connectivity.filters = connectivityExplorerTabRef.getFilters();
433
505
  }
434
506
 
507
+ // Update cell card explorer state if available
508
+ const cellCardExplorerTabRef = this.getTabRef(undefined, 'cellCardExplorer');
509
+ if (cellCardExplorerTabRef) {
510
+ this.state.cellCards.search = cellCardExplorerTabRef.getSearch();
511
+ this.state.cellCards.filters = cellCardExplorerTabRef.getFilters();
512
+ }
513
+
435
514
  // Update connectivity entries if available
436
515
  if (this.connectivityEntry && this.connectivityEntry.length > 0) {
437
516
  this.state.connectivityEntries = this.connectivityEntry.map((entry) => entry.id);
@@ -461,11 +540,15 @@ export default {
461
540
  setState: function (state) {
462
541
  // if state is not provided or formatted incorrectly, do nothing
463
542
  if (!state || !state.dataset || !state.connectivity) return;
464
- this.state = JSON.parse(JSON.stringify(state)); // deep copy to avoid reference issues
465
- const datasetFilters = state.dataset.filters;
466
- const connectivityFilters = state.connectivity.filters;
467
- const datasetSearch = state.dataset.search;
468
- const connectivitySearch = state.connectivity.search;
543
+ // Create a deep copy while preserving the original state structure for future extensibility
544
+ const newState = JSON.parse(JSON.stringify(state));
545
+ this.state = { ...this.state, ...newState };
546
+ const datasetFilters = this.state.dataset.filters;
547
+ const connectivityFilters = this.state.connectivity.filters;
548
+ const datasetSearch = this.state.dataset.search;
549
+ const connectivitySearch = this.state.connectivity.search;
550
+ const cellCardFilters = this.state.cellCards.filters || [];
551
+ const cellCardSearch = this.state.cellCards.search || '';
469
552
 
470
553
  if (datasetFilters.length || datasetSearch) {
471
554
  this.openSearch(datasetFilters, datasetSearch);
@@ -475,6 +558,10 @@ export default {
475
558
  this.openConnectivitySearch(connectivityFilters, connectivitySearch);
476
559
  }
477
560
 
561
+ if (cellCardFilters.length || cellCardSearch) {
562
+ this.openCellCardExplorerSearch(cellCardFilters, cellCardSearch);
563
+ }
564
+
478
565
  if (state.activeTabId) {
479
566
  this.$nextTick(() => {
480
567
  const hasTab = this.tabEntries.find((t) => t.id === state.activeTabId);
@@ -497,6 +584,16 @@ export default {
497
584
  };
498
585
  this.$emit('trackEvent', taggingData);
499
586
  },
587
+ /**
588
+ * @public
589
+ * Update the active species to use in filters.
590
+ * Used by SplitFlow component.
591
+ * @param activeSpecies
592
+ */
593
+ updateActiveSpeciesForEntries: function (activeSpecies) {
594
+ const speciesEntries = Array.isArray(activeSpecies) ? activeSpecies : [activeSpecies];
595
+ this.activeSpeciesForEntries = [...new Set(speciesEntries.filter(Boolean))];
596
+ },
500
597
  },
501
598
  computed: {
502
599
  // This should respect the information provided by the property
@@ -505,7 +602,8 @@ export default {
505
602
  (tab) =>
506
603
  tab.type === 'datasetExplorer' ||
507
604
  tab.type === 'connectivityExplorer' ||
508
- (tab.type === 'annotation' && this.annotationEntry && this.annotationEntry.length > 0),
605
+ (tab.type === 'annotation' && this.annotationEntry && this.annotationEntry.length > 0) ||
606
+ (tab.type === 'cellCardExplorer' && this.showCellCards),
509
607
  );
510
608
  },
511
609
  },
@@ -4,7 +4,7 @@
4
4
  class="tab"
5
5
  v-for="tab in tabs"
6
6
  :key="tab.id"
7
- :class="{ 'active-tab': tab.id == activeId }"
7
+ :class="{ 'active-tab': tab.id == activeId, 'closable-tab': tab.closable }"
8
8
  @click="tabClicked(tab)"
9
9
  >
10
10
  <span class="tab-title">{{ tab.title }}</span>
@@ -98,14 +98,23 @@ export default {
98
98
  .tab-title {
99
99
  text-align: center;
100
100
  font-size: 14px;
101
- padding: 0 1rem;
101
+ padding: 0 0.75rem;
102
+
103
+ .closable-tab & {
104
+ padding-right: 0.25rem;
105
+ }
102
106
  }
103
107
 
104
108
  .tab-close-icon {
105
- width: 20px !important;
106
- height: 20px !important;
107
- font-size: 20px !important;
108
- padding-right: 4px !important;
109
+ width: 18px !important;
110
+ height: 18px !important;
111
+ font-size: 18px !important;
112
+ margin-right: 4px !important;
109
113
  color: $app-primary-color !important;
114
+ border-radius: 2px;
115
+
116
+ &:hover {
117
+ background-color: rgba($app-primary-color, 0.15) !important;
118
+ }
110
119
  }
111
120
  </style>
@@ -0,0 +1,23 @@
1
+ <template>
2
+ <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
3
+ <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
4
+ <polygon
5
+ fill="currentColor"
6
+ fill-rule="nonzero"
7
+ transform="translate(15.500000, 8.878680) rotate(225.000000) translate(-15.500000, -8.878680)"
8
+ points="16.25 4.12867966 16.25 13.6286797 14.75 13.6286797 14.75 4.12867966"
9
+ />
10
+ <polygon
11
+ fill="currentColor"
12
+ fill-rule="nonzero"
13
+ transform="translate(17.500000, 6.918996) rotate(225.000000) translate(-17.500000, -6.918996)"
14
+ points="20.9294459 3.82036868 22.058311 4.80812559 17.5 10.0176239 12.941689 4.80812559 14.0705541 3.82036868 17.5 7.739"
15
+ />
16
+ <polygon
17
+ fill="currentColor"
18
+ fill-rule="nonzero"
19
+ points="11.6570511 7.25 11.6570511 8.75 5.75 8.75 5.75 16.25 15.25 16.25 15.25 11.2267933 16.75 11.2267933 16.75 17.75 4.25 17.75 4.25 7.25"
20
+ />
21
+ </g>
22
+ </svg>
23
+ </template>
@@ -13,6 +13,8 @@ declare module 'vue' {
13
13
  export interface GlobalComponents {
14
14
  AnnotationTool: typeof import('./components/AnnotationTool.vue')['default']
15
15
  BadgesGroup: typeof import('./components/BadgesGroup.vue')['default']
16
+ CellCard: typeof import('./components/CellCard.vue')['default']
17
+ CellCardExplorer: typeof import('./components/CellCardExplorer.vue')['default']
16
18
  ConnectivityCard: typeof import('./components/ConnectivityCard.vue')['default']
17
19
  ConnectivityExplorer: typeof import('./components/ConnectivityExplorer.vue')['default']
18
20
  ConnectivityInfo: typeof import('./components/ConnectivityInfo.vue')['default']
@@ -48,6 +50,7 @@ declare module 'vue' {
48
50
  ElRow: typeof import('element-plus/es')['ElRow']
49
51
  ElSelect: typeof import('element-plus/es')['ElSelect']
50
52
  ElTag: typeof import('element-plus/es')['ElTag']
53
+ IconOpenExternal: typeof import('./components/icons/IconOpenExternal.vue')['default']
51
54
  ImageGallery: typeof import('./components/ImageGallery.vue')['default']
52
55
  SearchFilters: typeof import('./components/SearchFilters.vue')['default']
53
56
  SearchHistory: typeof import('./components/SearchHistory.vue')['default']
@@ -0,0 +1,59 @@
1
+ export function generateUUID() {
2
+ const arr = new Uint8Array(16);
3
+ window.crypto.getRandomValues(arr);
4
+
5
+ arr[6] = (arr[6] & 0x0f) | 0x40;
6
+ arr[8] = (arr[8] & 0x3f) | 0x80;
7
+
8
+ const hex = Array.from(arr)
9
+ .map((byte) => byte.toString(16).padStart(2, '0'))
10
+ .join('');
11
+
12
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
13
+ }
14
+
15
+ export function capitalise(text) {
16
+ if (!text) return '';
17
+ const value = String(text);
18
+ return value.charAt(0).toUpperCase() + value.slice(1);
19
+ }
20
+
21
+ export function formatAlertText(text, { formatLines = false } = {}) {
22
+ if (!text) return '';
23
+
24
+ const escaped = String(text).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
25
+
26
+ const linkified = escaped.replace(/(https?:\/\/[^\s"<>[]+)/g, (url) => {
27
+ const parts = url.match(/^(.*?)([\].,;:!?]*)$/);
28
+ const cleanUrl = parts ? parts[1] : url;
29
+ const suffix = parts ? parts[2] : '';
30
+ return `<a href="${cleanUrl}" target="_blank" rel="noopener noreferrer">${cleanUrl}</a>${suffix}`;
31
+ });
32
+
33
+ const normalised = linkified.replace(/\\n/g, '\n').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
34
+
35
+ if (!formatLines) {
36
+ return normalised;
37
+ }
38
+
39
+ return normalised
40
+ .split('\n')
41
+ .map((line) => {
42
+ const withBoldLabel = line.replace(/^\s*([A-Za-z][^:<]{0,120}:)/, '<strong>$1</strong>');
43
+ return `<div class="alert-line">${withBoldLabel}</div>`;
44
+ })
45
+ .join('\n');
46
+ }
47
+
48
+ export function scrollToRef(vm, refName) {
49
+ vm?.$nextTick(() => {
50
+ const element = vm?.$refs?.[refName];
51
+ if (element) {
52
+ element.scrollIntoView({
53
+ behavior: 'smooth',
54
+ block: 'start',
55
+ inline: 'nearest',
56
+ });
57
+ }
58
+ });
59
+ }