@abi-software/map-side-bar 2.3.0 → 2.4.0-alpha-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.
Files changed (43) hide show
  1. package/.eslintrc.js +12 -12
  2. package/.postcssrc.json +5 -5
  3. package/LICENSE +201 -201
  4. package/README.md +168 -168
  5. package/cypress.config.js +23 -23
  6. package/dist/data/pmr-sample.json +3181 -0
  7. package/dist/map-side-bar.js +15142 -9024
  8. package/dist/map-side-bar.umd.cjs +50 -103
  9. package/dist/style.css +1 -1
  10. package/package.json +77 -77
  11. package/public/data/pmr-sample.json +3181 -0
  12. package/reporter-config.json +9 -9
  13. package/src/App.vue +266 -265
  14. package/src/algolia/algolia.js +255 -242
  15. package/src/algolia/utils.js +100 -100
  16. package/src/assets/_variables.scss +43 -43
  17. package/src/assets/styles.scss +6 -6
  18. package/src/components/BadgesGroup.vue +124 -124
  19. package/src/components/ConnectivityInfo.vue +619 -619
  20. package/src/components/DatasetCard.vue +367 -357
  21. package/src/components/EventBus.js +3 -3
  22. package/src/components/ExternalResourceCard.vue +113 -113
  23. package/src/components/FlatmapDatasetCard.vue +171 -0
  24. package/src/components/ImageGallery.vue +542 -542
  25. package/src/components/PMRDatasetCard.vue +237 -0
  26. package/src/components/SearchFilters.vue +1023 -1006
  27. package/src/components/SearchHistory.vue +175 -175
  28. package/src/components/SideBar.vue +436 -436
  29. package/src/components/SidebarContent.vue +730 -603
  30. package/src/components/Tabs.vue +145 -145
  31. package/src/components/allPaths.js +5928 -0
  32. package/src/components/index.js +8 -8
  33. package/src/components/pmrTest.js +4 -0
  34. package/src/components/species-map.js +8 -8
  35. package/src/components.d.ts +2 -0
  36. package/src/exampleConnectivityInput.js +291 -291
  37. package/src/flatmapQueries/flatmapQueries.js +169 -0
  38. package/src/main.js +9 -9
  39. package/src/mixins/S3Bucket.vue +37 -37
  40. package/src/mixins/mixedPageCalculation.vue +78 -0
  41. package/static.json +6 -6
  42. package/vite.config.js +55 -55
  43. package/vuese-generator.js +65 -65
@@ -1,603 +1,730 @@
1
- <template>
2
- <el-card :body-style="bodyStyle" class="content-card">
3
- <template #header>
4
- <div class="header">
5
- <el-input
6
- class="search-input"
7
- placeholder="Search"
8
- v-model="searchInput"
9
- @keyup="searchEvent"
10
- clearable
11
- @clear="clearSearchClicked"
12
- ></el-input>
13
- <el-button
14
- type="primary"
15
- class="button"
16
- @click="searchEvent"
17
- size="large"
18
- >
19
- Search
20
- </el-button>
21
- </div>
22
- </template>
23
- <SearchFilters
24
- class="filters"
25
- ref="filtersRef"
26
- :entry="filterEntry"
27
- :envVars="envVars"
28
- @filterResults="filterUpdate"
29
- @numberPerPage="numberPerPageUpdate"
30
- @loading="filtersLoading"
31
- @cascaderReady="cascaderReady"
32
- ></SearchFilters>
33
- <SearchHistory
34
- ref="searchHistory"
35
- @search="searchHistorySearch"
36
- ></SearchHistory>
37
- <div class="content scrollbar" v-loading="loadingCards" ref="content">
38
- <div class="error-feedback" v-if="results.length === 0 && !loadingCards">
39
- No results found - Please change your search / filter criteria.
40
- </div>
41
- <div v-for="result in results" :key="result.doi" class="step-item">
42
- <DatasetCard
43
- class="dataset-card"
44
- :entry="result"
45
- :envVars="envVars"
46
- @mouseenter="hoverChanged(result)"
47
- @mouseleave="hoverChanged(undefined)"
48
- />
49
- </div>
50
- <el-pagination
51
- class="pagination"
52
- v-model:current-page="page"
53
- hide-on-single-page
54
- large
55
- layout="prev, pager, next"
56
- :page-size="numberPerPage"
57
- :total="numberOfHits"
58
- @current-change="pageChange"
59
- ></el-pagination>
60
- </div>
61
- </el-card>
62
- </template>
63
-
64
- <script>
65
- /* eslint-disable no-alert, no-console */
66
- import {
67
- ElButton as Button,
68
- ElCard as Card,
69
- ElDrawer as Drawer,
70
- ElIcon as Icon,
71
- ElInput as Input,
72
- ElPagination as Pagination,
73
- } from 'element-plus'
74
- import SearchFilters from './SearchFilters.vue'
75
- import SearchHistory from './SearchHistory.vue'
76
- import DatasetCard from './DatasetCard.vue'
77
- import EventBus from './EventBus.js'
78
-
79
- import { AlgoliaClient } from '../algolia/algolia.js'
80
- import { getFilters, facetPropPathMapping } from '../algolia/utils.js'
81
-
82
- // handleErrors: A custom fetch error handler to recieve messages from the server
83
- // even when an error is found
84
- var handleErrors = async function (response) {
85
- if (!response.ok) {
86
- let parse = await response.json()
87
- if (parse) {
88
- throw new Error(parse.message)
89
- } else {
90
- throw new Error(response)
91
- }
92
- }
93
- return response
94
- }
95
-
96
- var initial_state = {
97
- searchInput: '',
98
- lastSearch: '',
99
- results: [],
100
- numberOfHits: 0,
101
- filter: [],
102
- loadingCards: false,
103
- numberPerPage: 10,
104
- page: 1,
105
- pageModel: 1,
106
- start: 0,
107
- hasSearched: false,
108
- contextCardEnabled: false,
109
- }
110
-
111
- export default {
112
- components: {
113
- SearchFilters,
114
- DatasetCard,
115
- SearchHistory,
116
- Button,
117
- Card,
118
- Drawer,
119
- Icon,
120
- Input,
121
- Pagination
122
- },
123
- name: 'SideBarContent',
124
- props: {
125
- visible: {
126
- type: Boolean,
127
- default: false,
128
- },
129
- isDrawer: {
130
- type: Boolean,
131
- default: true,
132
- },
133
- entry: {
134
- type: Object,
135
- default: () => initial_state,
136
- },
137
- envVars: {
138
- type: Object,
139
- default: () => {},
140
- },
141
- },
142
- data: function () {
143
- return {
144
- ...this.entry,
145
- bodyStyle: {
146
- flex: '1 1 auto',
147
- 'flex-flow': 'column',
148
- display: 'flex',
149
- },
150
- cascaderIsReady: false,
151
- }
152
- },
153
- computed: {
154
- // This computed property populates filter data's entry object with $data from this sidebar
155
- filterEntry: function () {
156
- return {
157
- numberOfHits: this.numberOfHits,
158
- filterFacets: this.filter,
159
- }
160
- },
161
- },
162
- methods: {
163
- hoverChanged: function (data) {
164
- this.$emit('hover-changed', data)
165
- },
166
- resetSearch: function () {
167
- this.numberOfHits = 0
168
- this.discoverIds = []
169
- this._dois = []
170
- this.results = []
171
- this.loadingCards = false
172
- },
173
- openSearch: function (filter, search = '') {
174
- this.searchInput = search
175
- this.resetPageNavigation()
176
- //Proceed normally if cascader is ready
177
- if (this.cascaderIsReady) {
178
- this.filter =
179
- this.$refs.filtersRef.getHierarchicalValidatedFilters(filter)
180
- //Facets provided but cannot find at least one valid
181
- //facet. Tell the users the search is invalid and reset
182
- //facets check boxes.
183
- if (
184
- filter &&
185
- filter.length > 0 &&
186
- this.filter &&
187
- this.filter.length === 0
188
- ) {
189
- this.$refs.filtersRef.checkShowAllBoxes()
190
- this.resetSearch()
191
- } else if (this.filter) {
192
- this.searchAlgolia(this.filter, search)
193
- this.$refs.filtersRef.setCascader(this.filter)
194
- }
195
- } else {
196
- //cascader is not ready, perform search if no filter is set,
197
- //otherwise waith for cascader to be ready
198
- this.filter = filter
199
- if (!filter || filter.length == 0) {
200
- this.searchAlgolia(this.filter, search)
201
- }
202
- }
203
- },
204
- addFilter: function (filter) {
205
- if (this.cascaderIsReady) {
206
- this.resetPageNavigation()
207
- if (filter) {
208
- if (this.$refs.filtersRef.addFilter(filter))
209
- this.$refs.filtersRef.initiateSearch()
210
- }
211
- } else {
212
- if (Array.isArray(this.filter)) {
213
- this.filter.push(filter)
214
- } else {
215
- this.filter = [filter]
216
- }
217
- }
218
- },
219
- cascaderReady: function () {
220
- this.cascaderIsReady = true
221
- this.openSearch(this.filter, this.searchInput)
222
- },
223
- clearSearchClicked: function () {
224
- this.searchInput = ''
225
- this.resetPageNavigation()
226
- this.searchAlgolia(this.filters, this.searchInput)
227
- this.$refs.searchHistory.selectValue = 'Full search history'
228
- },
229
- searchEvent: function (event = false) {
230
- if (event.keyCode === 13 || event instanceof MouseEvent) {
231
- this.resetPageNavigation()
232
- this.searchAlgolia(this.filters, this.searchInput)
233
- this.$refs.searchHistory.selectValue = 'Full search history'
234
- this.$refs.searchHistory.addSearchToHistory(
235
- this.filters,
236
- this.searchInput
237
- )
238
- }
239
- },
240
- filterUpdate: function (filters) {
241
- this.filters = [...filters]
242
- this.resetPageNavigation()
243
- this.searchAlgolia(filters, this.searchInput)
244
- this.$emit('search-changed', {
245
- value: filters,
246
- type: 'filter-update',
247
- })
248
- },
249
- searchAlgolia(filters, query = '') {
250
- // Algolia search
251
-
252
- this.loadingCards = true
253
- this.algoliaClient
254
- .anatomyInSearch(getFilters(filters), query)
255
- .then((r) => {
256
- // Send result anatomy to the scaffold and flatmap
257
- EventBus.emit('anatomy-in-datasets', r.forFlatmap)
258
- EventBus.emit('number-of-datasets-for-anatomies', r.forScaffold)
259
- })
260
- this.algoliaClient
261
- .search(getFilters(filters), query, this.numberPerPage, this.page)
262
- .then((searchData) => {
263
- this.numberOfHits = searchData.total
264
- this.discoverIds = searchData.discoverIds
265
- this._dois = searchData.dois
266
- this.results = searchData.items
267
- this.loadingCards = false
268
- this.scrollToTop()
269
- this.$emit('search-changed', {
270
- value: this.searchInput,
271
- type: 'query-update',
272
- })
273
- if (this._abortController) this._abortController.abort()
274
- this._abortController = new AbortController()
275
- const signal = this._abortController.signal
276
- //Search ongoing, let the current flow progress
277
- this.perItemSearch(signal, { count: 0 })
278
- })
279
- },
280
- filtersLoading: function (val) {
281
- this.loadingCards = val
282
- },
283
- numberPerPageUpdate: function (val) {
284
- this.numberPerPage = val
285
- this.pageChange(1)
286
- },
287
- pageChange: function (page) {
288
- this.start = (page - 1) * this.numberPerPage
289
- this.page = page
290
- this.searchAlgolia(
291
- this.filters,
292
- this.searchInput,
293
- this.numberPerPage,
294
- this.page
295
- )
296
- },
297
- handleMissingData: function (doi) {
298
- let i = this.results.findIndex((res) => res.doi === doi)
299
- if (this.results[i]) this.results[i].detailsReady = true
300
- },
301
- perItemSearch: function (signal, data) {
302
- //Maximum 10 downloads at once to prevent long waiting time
303
- //between unfinished search and new search
304
- const maxDownloads = 10
305
- if (maxDownloads > data.count) {
306
- const doi = this._dois.shift()
307
- if (doi) {
308
- data.count++
309
- this.callSciCrunch(this.envVars.API_LOCATION, { dois: [doi] }, signal)
310
- .then((result) => {
311
- if (result.numberOfHits === 0) this.handleMissingData(doi)
312
- else this.resultsProcessing(result)
313
- this.$refs.content.style['overflow-y'] = 'scroll'
314
- data.count--
315
- //Async::Download finished, get the next one
316
- this.perItemSearch(signal, data)
317
- })
318
- .catch((result) => {
319
- if (result.name !== 'AbortError') {
320
- this.handleMissingData(doi)
321
- data.count--
322
- //Async::Download not aborted, get the next one
323
- this.perItemSearch(signal, data)
324
- }
325
- })
326
- //Check and make another request until it gets to max downloads
327
- this.perItemSearch(signal, data)
328
- }
329
- }
330
- },
331
- scrollToTop: function () {
332
- if (this.$refs.content) {
333
- this.$refs.content.scroll({ top: 0, behavior: 'smooth' })
334
- }
335
- },
336
- resetPageNavigation: function () {
337
- this.start = 0
338
- this.page = 1
339
- },
340
- resultsProcessing: function (data) {
341
- this.lastSearch = this.searchInput
342
-
343
- if (data.results.length === 0) {
344
- return
345
- }
346
- data.results.forEach((element) => {
347
- // match the scicrunch result with algolia result
348
- let i = this.results.findIndex((res) =>
349
- element.doi ? element.doi.includes(res.doi) : false
350
- )
351
- // Assign scicrunch results to the object
352
- Object.assign(this.results[i], element)
353
- // Assign the attributes that need some processing
354
- Object.assign(this.results[i], {
355
- numberSamples: element.sampleSize ? parseInt(element.sampleSize) : 0,
356
- numberSubjects: element.subjectSize
357
- ? parseInt(element.subjectSize)
358
- : 0,
359
- updated:
360
- (element.updated && element.updated.length) > 0
361
- ? element.updated[0].timestamp.split('T')[0]
362
- : '',
363
- url: element.uri[0],
364
- datasetId: element.dataset_identifier,
365
- datasetRevision: element.dataset_revision,
366
- datasetVersion: element.dataset_version,
367
- organs:
368
- element.organs && element.organs.length > 0
369
- ? [...new Set(element.organs.map((v) => v.name))]
370
- : undefined,
371
- species: element.organisms
372
- ? element.organisms[0].species
373
- ? [
374
- ...new Set(
375
- element.organisms.map((v) =>
376
- v.species ? v.species.name : null
377
- )
378
- ),
379
- ]
380
- : undefined
381
- : undefined, // This processing only includes each gender once into 'sexes'
382
- scaffolds: element['abi-scaffold-metadata-file'],
383
- thumbnails: element['abi-thumbnail']
384
- ? element['abi-thumbnail']
385
- : element['abi-scaffold-thumbnail'],
386
- scaffoldViews: element['abi-scaffold-view-file'],
387
- videos: element.video,
388
- plots: element['abi-plot'],
389
- images: element['common-images'],
390
- contextualInformation:
391
- element['abi-contextual-information'].length > 0
392
- ? element['abi-contextual-information']
393
- : undefined,
394
- segmentation: element['mbf-segmentation'],
395
- simulation: element['abi-simulation-file'],
396
- additionalLinks: element.additionalLinks,
397
- detailsReady: true,
398
- })
399
- this.results[i] = this.results[i]
400
- })
401
- },
402
- createfilterParams: function (params) {
403
- let p = new URLSearchParams()
404
- //Check if field is array or value
405
- for (const key in params) {
406
- if (Array.isArray(params[key])) {
407
- params[key].forEach((e) => {
408
- p.append(key, e)
409
- })
410
- } else {
411
- p.append(key, params[key])
412
- }
413
- }
414
- return p.toString()
415
- },
416
- callSciCrunch: function (apiLocation, params = {}, signal) {
417
- return new Promise((resolve, reject) => {
418
- // Add parameters if we are sent them
419
- let fullEndpoint =
420
- this.envVars.API_LOCATION +
421
- this.searchEndpoint +
422
- '?' +
423
- this.createfilterParams(params)
424
- fetch(fullEndpoint, { signal })
425
- .then(handleErrors)
426
- .then((response) => response.json())
427
- .then((data) => resolve(data))
428
- .catch((data) => reject(data))
429
- })
430
- },
431
- getAlgoliaFacets: async function () {
432
- let facets = await this.algoliaClient.getAlgoliaFacets(
433
- facetPropPathMapping
434
- )
435
- return facets
436
- },
437
- searchHistorySearch: function (item) {
438
- this.searchInput = item.search
439
- this.filters = item.filters
440
- this.openSearch(item.filters, item.search)
441
- },
442
- },
443
- mounted: function () {
444
- // initialise algolia
445
- this.algoliaClient = new AlgoliaClient(
446
- this.envVars.ALGOLIA_ID,
447
- this.envVars.ALGOLIA_KEY,
448
- this.envVars.PENNSIEVE_API_LOCATION
449
- )
450
- this.algoliaClient.initIndex(this.envVars.ALGOLIA_INDEX)
451
- this.openSearch(this.filter, this.searchInput)
452
- },
453
- created: function () {
454
- //Create non-reactive local variables
455
- this.searchEndpoint = 'dataset_info/using_multiple_dois/'
456
- },
457
- }
458
- </script>
459
-
460
- <style lang="scss" scoped>
461
- .dataset-card {
462
- position: relative;
463
-
464
- &::before {
465
- content: "";
466
- display: block;
467
- width: calc(100% - 15px);
468
- height: 100%;
469
- position: absolute;
470
- top: 7px;
471
- left: 7px;
472
- border-style: solid;
473
- border-radius: 5px;
474
- border-color: transparent;
475
- }
476
-
477
- &:hover {
478
- &::before {
479
- border-color: var(--el-color-primary);
480
- }
481
- }
482
- }
483
-
484
- .content-card {
485
- height: 100%;
486
- flex-flow: column;
487
- display: flex;
488
- }
489
-
490
- .step-item {
491
- font-size: 14px;
492
- margin-bottom: 18px;
493
- text-align: left;
494
- }
495
-
496
- .search-input {
497
- width: 298px !important;
498
- height: 40px;
499
- padding-right: 14px;
500
- align-items: left;
501
- }
502
-
503
- .header {
504
- border: solid 1px #292b66;
505
- background-color: #292b66;
506
- text-align: left;
507
-
508
- .el-button {
509
- &:hover,
510
- &:focus {
511
- background: $app-primary-color;
512
- box-shadow: -3px 2px 4px #00000040;
513
- color: #fff;
514
- }
515
- }
516
- }
517
-
518
- .pagination {
519
- padding-bottom: 16px;
520
- background-color: white;
521
- padding-left: 95px;
522
- font-weight: bold;
523
- }
524
-
525
- .pagination :deep(button) {
526
- background-color: white !important;
527
- }
528
- .pagination :deep(li) {
529
- background-color: white !important;
530
- }
531
- .pagination :deep(li.is-active) {
532
- color: $app-primary-color;
533
- }
534
-
535
- .error-feedback {
536
- font-family: Asap;
537
- font-size: 14px;
538
- font-style: italic;
539
- padding-top: 15px;
540
- }
541
-
542
- .content-card :deep(.el-card__header) {
543
- background-color: #292b66;
544
- padding: 1rem;
545
- }
546
-
547
- .content-card :deep(.el-card__body) {
548
- background-color: #f7faff;
549
- overflow-y: hidden;
550
- padding: 1rem;
551
- }
552
-
553
- .content {
554
- // width: 515px;
555
- flex: 1 1 auto;
556
- box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.06);
557
- border: solid 1px #e4e7ed;
558
- background-color: #ffffff;
559
- overflow-y: scroll;
560
- scrollbar-width: thin;
561
- }
562
-
563
- .content :deep(.el-loading-spinner .path) {
564
- stroke: $app-primary-color;
565
- }
566
-
567
- .content :deep(.step-item:first-child .seperator-path) {
568
- display: none;
569
- }
570
-
571
- .content :deep(.step-item:not(:first-child) .seperator-path) {
572
- width: 455px;
573
- height: 0px;
574
- border: solid 1px #e4e7ed;
575
- background-color: #e4e7ed;
576
- }
577
-
578
- .scrollbar::-webkit-scrollbar-track {
579
- border-radius: 10px;
580
- background-color: #f5f5f5;
581
- }
582
-
583
- .scrollbar::-webkit-scrollbar {
584
- width: 12px;
585
- right: -12px;
586
- background-color: #f5f5f5;
587
- }
588
-
589
- .scrollbar::-webkit-scrollbar-thumb {
590
- border-radius: 4px;
591
- box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.06);
592
- background-color: #979797;
593
- }
594
-
595
- :deep(.el-input__suffix) {
596
- padding-right: 0px;
597
- }
598
-
599
- :deep(.my-drawer) {
600
- background: rgba(0, 0, 0, 0);
601
- box-shadow: none;
602
- }
603
- </style>
1
+ <template>
2
+ <el-card :body-style="bodyStyle" class="content-card">
3
+ <template #header>
4
+ <div class="header">
5
+ <el-input
6
+ class="search-input"
7
+ placeholder="Search"
8
+ v-model="searchInput"
9
+ @keyup="searchEvent"
10
+ clearable
11
+ @clear="clearSearchClicked"
12
+ ></el-input>
13
+ <el-select
14
+ v-model="mode"
15
+ class="data-type-select"
16
+ placeholder="Search over..."
17
+ @change="onSelectValueChange"
18
+ >
19
+ <el-option
20
+ v-for="item in selectOptions"
21
+ :key="item.value"
22
+ :label="item.label"
23
+ :value="item.value"
24
+ />
25
+ </el-select>
26
+ </div>
27
+ </template>
28
+ <SearchFilters
29
+ class="filters"
30
+ ref="filtersRef"
31
+ :entry="filterEntry"
32
+ :envVars="envVars"
33
+ @filterResults="filterUpdate"
34
+ @numberPerPage="numberPerPageUpdate"
35
+ @loading="filtersLoading"
36
+ @cascaderReady="cascaderReady"
37
+ ></SearchFilters>
38
+ <SearchHistory
39
+ ref="searchHistory"
40
+ @search="searchHistorySearch"
41
+ ></SearchHistory>
42
+ pmr hits: {{ pmrNumberOfHits }}
43
+ <div class="content scrollbar" v-loading="loadingCards" ref="content">
44
+ <div class="error-feedback" v-if="results.length === 0 && !loadingCards">
45
+ No results found - Please change your search / filter criteria.
46
+ </div>
47
+ <div v-for="(result, i) in results" :key="i" class="step-item">
48
+ <DatasetCard
49
+ v-if="result.dataSource === 'SPARC'"
50
+ class="dataset-card"
51
+ :entry="result"
52
+ :envVars="envVars"
53
+ @mouseenter="hoverChanged(result)"
54
+ @mouseleave="hoverChanged(undefined)"
55
+ ></DatasetCard>
56
+ <PMRDatasetCard
57
+ v-else-if="result.dataSource === 'PMR'"
58
+ class="dataset-card"
59
+ :entry="result"
60
+ :envVars="envVars"
61
+ @mouseenter="hoverChanged(result)"
62
+ @mouseleave="hoverChanged(undefined)"
63
+ ></PMRDatasetCard>
64
+ <flatmap-dataset-card
65
+ v-else-if="result.dataSource === 'Flatmap'"
66
+ class="dataset-card"
67
+ :entry="result"
68
+ :envVars="envVars"
69
+ @mouseenter="hoverChanged(result)"
70
+ @mouseleave="hoverChanged(undefined)"
71
+ ></flatmap-dataset-card>
72
+ </div>
73
+ <el-pagination
74
+ class="pagination"
75
+ v-model:current-page="page"
76
+ hide-on-single-page
77
+ large
78
+ layout="prev, pager, next"
79
+ :page-size="numberPerPage"
80
+ :total="numberOfHits"
81
+ @current-change="pageChange"
82
+ ></el-pagination>
83
+ </div>
84
+ </el-card>
85
+ </template>
86
+
87
+ <script>
88
+ /* eslint-disable no-alert, no-console */
89
+ import {
90
+ ElButton as Button,
91
+ ElCard as Card,
92
+ ElDrawer as Drawer,
93
+ ElIcon as Icon,
94
+ ElInput as Input,
95
+ ElPagination as Pagination,
96
+ } from 'element-plus'
97
+ import SearchFilters from './SearchFilters.vue'
98
+ import SearchHistory from './SearchHistory.vue'
99
+ import EventBus from './EventBus.js'
100
+
101
+ import DatasetCard from "./DatasetCard.vue";
102
+ import PMRDatasetCard from "./PMRDatasetCard.vue";
103
+ import FlatmapDatasetCard from './FlatmapDatasetCard.vue';
104
+
105
+ import allPaths from './allPaths.js'
106
+
107
+ import { AlgoliaClient } from '../algolia/algolia.js'
108
+ import { getFilters, facetPropPathMapping } from '../algolia/utils.js'
109
+ import FlatmapQueries from '../flatmapQueries/flatmapQueries.js'
110
+ import mixedPageCalculation from '../mixins/mixedPageCalculation.vue'
111
+
112
+ // TODO: to update API URL
113
+ const API_URL = "/data/pmr-sample.json";
114
+ const RatioOfPMRResults = 0.2; // Ratio of PMR results to Sparc results
115
+
116
+ // handleErrors: A custom fetch error handler to recieve messages from the server
117
+ // even when an error is found
118
+ var handleErrors = async function (response) {
119
+ if (!response.ok) {
120
+ let parse = await response.json()
121
+ if (parse) {
122
+ throw new Error(parse.message)
123
+ } else {
124
+ throw new Error(response)
125
+ }
126
+ }
127
+ return response
128
+ }
129
+
130
+ var initial_state = {
131
+ searchInput: '',
132
+ lastSearch: '',
133
+ results: [],
134
+ pmrNumberOfHits: 0,
135
+ sparcNumberOfHits: 0,
136
+ variableRatio: RatioOfPMRResults,
137
+ filter: [],
138
+ loadingCards: false,
139
+ numberPerPage: 10,
140
+ page: 1,
141
+ pmrResultsOnlyFlag: false,
142
+ hasSearched: false,
143
+ contextCardEnabled: false,
144
+ pmrResults: [],
145
+ mode: 'Sparc Datasets',
146
+ allPaths: allPaths.values,
147
+ selectOptions: [
148
+ { value: "Sparc Datasets", label: "Sparc Datasets" },
149
+ { value: "PMR", label: "PMR" },
150
+ { value: "Flatmap", label: "Flatmap"}
151
+ ],
152
+ };
153
+
154
+
155
+
156
+ export default {
157
+ components: {
158
+ SearchFilters,
159
+ DatasetCard,
160
+ PMRDatasetCard,
161
+ FlatmapDatasetCard,
162
+ SearchHistory,
163
+ Button,
164
+ Card,
165
+ Drawer,
166
+ Icon,
167
+ Input,
168
+ Pagination
169
+ },
170
+ name: 'SideBarContent',
171
+ mixins: [mixedPageCalculation],
172
+ props: {
173
+ visible: {
174
+ type: Boolean,
175
+ default: false,
176
+ },
177
+ isDrawer: {
178
+ type: Boolean,
179
+ default: true,
180
+ },
181
+ entry: {
182
+ type: Object,
183
+ default: () => initial_state,
184
+ },
185
+ envVars: {
186
+ type: Object,
187
+ default: () => {},
188
+ },
189
+ },
190
+ data: function () {
191
+ return {
192
+ ...this.entry,
193
+ bodyStyle: {
194
+ flex: '1 1 auto',
195
+ 'flex-flow': 'column',
196
+ display: 'flex',
197
+ },
198
+ cascaderIsReady: false,
199
+ }
200
+ },
201
+ computed: {
202
+ // This computed property populates filter data's entry object with $data from this sidebar
203
+ filterEntry: function () {
204
+ return {
205
+ numberOfHits: this.numberOfHits,
206
+ filterFacets: this.filter,
207
+ }
208
+ },
209
+ // npp_SPARC: Number per page for SPARC datasets
210
+ npp_SPARC: function () {
211
+ return Math.round(this.numberPerPage * (1 - RatioOfPMRResults))
212
+ },
213
+ // npp_PMR: Number per page for PMR datasets
214
+ npp_PMR: function () {
215
+ return Math.round(this.numberPerPage * RatioOfPMRResults)
216
+ },
217
+ numberOfHits: function () {
218
+ return this.sparcNumberOfHits + this.pmrNumberOfHits
219
+ },
220
+
221
+ },
222
+ methods: {
223
+ hoverChanged: function (data) {
224
+ this.$emit('hover-changed', data)
225
+ },
226
+ resetSearch: function () {
227
+ this.pmrNumberOfHits = 0
228
+ this.sparcNumberOfHits = 0
229
+ this.page = 1
230
+ this.calculateVariableRatio()
231
+ this.discoverIds = []
232
+ this._dois = []
233
+ this.results = []
234
+ this.loadingCards = false
235
+ },
236
+ // openSearch: Resets the results, populates dataset cards and filters. Will use Algolia and SciCrunch data uness pmr mode is set
237
+ openSearch: function(filter, search = '', resetSearch = true) {
238
+ if (resetSearch) {
239
+ this.resetSearch();
240
+ this.openAlgoliaSearch(filter, search);
241
+ } else {
242
+ this.searchAlgolia(filter, search);
243
+ }
244
+ this.openPMRSearch(filter, search)
245
+ },
246
+
247
+ // openPMRSearch: Resets the results, populates dataset cards and filters with PMR data.
248
+ openPMRSearch: function (filter, search = '') {
249
+ this.flatmapQueries.updateOffset(this.calculatePMROffest())
250
+ this.flatmapQueries.updateLimit(this.PMRLimit(this.pmrResultsOnlyFlag))
251
+ this.flatmapQueries.pmrSearch(filter, search).then((data) => {
252
+ data.forEach((result) => {
253
+ this.results.push(result)
254
+ })
255
+ this.pmrNumberOfHits = this.flatmapQueries.numberOfHits
256
+ })
257
+ },
258
+
259
+ // openAlgoliaSearch: Resets the results, populates dataset cards and filters with Algloia and SciCrunch data.
260
+ openAlgoliaSearch: function (filter, search = '') {
261
+ this.searchInput = search
262
+ //Proceed normally if cascader is ready
263
+ if (this.cascaderIsReady) {
264
+ this.filter =
265
+ this.$refs.filtersRef.getHierarchicalValidatedFilters(filter)
266
+ //Facets provided but cannot find at least one valid
267
+ //facet. Tell the users the search is invalid and reset
268
+ //facets check boxes.
269
+ if (
270
+ filter &&
271
+ filter.length > 0 &&
272
+ this.filter &&
273
+ this.filter.length === 0
274
+ ) {
275
+ this.$refs.filtersRef.checkShowAllBoxes()
276
+ this.resetSearch()
277
+ } else if (this.filter) {
278
+ this.searchAlgolia(this.filter, search)
279
+ this.$refs.filtersRef.setCascader(this.filter)
280
+ }
281
+ } else {
282
+ //cascader is not ready, perform search if no filter is set,
283
+ //otherwise waith for cascader to be ready
284
+ this.filter = filter
285
+ if (!filter || filter.length == 0) {
286
+ this.searchAlgolia(this.filter, search)
287
+ }
288
+ }
289
+ },
290
+ addFilter: function (filter) {
291
+ if (this.cascaderIsReady) {
292
+ this.resetSearch()
293
+ if (filter) {
294
+ if (this.$refs.filtersRef.addFilter(filter))
295
+ this.$refs.filtersRef.initiateSearch()
296
+ }
297
+ } else {
298
+ if (Array.isArray(this.filter)) {
299
+ this.filter.push(filter)
300
+ } else {
301
+ this.filter = [filter]
302
+ }
303
+ }
304
+ },
305
+ cascaderReady: function () {
306
+ this.cascaderIsReady = true
307
+ this.openSearch(this.filter, this.searchInput)
308
+ },
309
+ clearSearchClicked: function () {
310
+ this.searchInput = ''
311
+ this.resetSearch()
312
+ this.openSearch(this.filter, this.searchInput)
313
+ this.$refs.searchHistory.selectValue = 'Full search history'
314
+ },
315
+ searchEvent: function (event = false) {
316
+ if (event.keyCode === 13 || event instanceof MouseEvent) {
317
+ this.openSearch(this.filter, this.searchInput)
318
+ this.$refs.searchHistory.selectValue = 'Full search history'
319
+ this.$refs.searchHistory.addSearchToHistory(
320
+ this.filters,
321
+ this.searchInput
322
+ )
323
+ }
324
+ },
325
+ updatePMROnlyFlag: function (filters) {
326
+ const pmrSearchObject = filters.find((tmp) => tmp.facet === 'PMR');
327
+ if (pmrSearchObject) {
328
+ this.pmrResultsOnlyFlag = true
329
+ } else {
330
+ this.pmrResultsOnlyFlag = false
331
+ }
332
+ },
333
+ filterUpdate: function (filters) {
334
+ this.filters = [...filters]
335
+
336
+ // Check if PMR is in the filters
337
+ this.updatePMROnlyFlag(filters)
338
+
339
+ // Note that we cannot use the openSearch function as that modifies filters
340
+ this.resetSearch()
341
+ this.searchAlgolia(filters, this.searchInput)
342
+ this.openPMRSearch(filters, this.searchInput)
343
+ this.$emit('search-changed', {
344
+ value: filters,
345
+ type: 'filter-update',
346
+ })
347
+ },
348
+ searchAlgolia(filters, query = '') {
349
+
350
+ // Remove loading if we dont expect any results
351
+ if (this.SPARCLimit() === 0) {
352
+ this.loadingCards = false
353
+ return
354
+ }
355
+
356
+ // Algolia search
357
+
358
+ this.loadingCards = true
359
+ this.algoliaClient
360
+ .anatomyInSearch(getFilters(filters), query)
361
+ .then((r) => {
362
+ // Send result anatomy to the scaffold and flatmap
363
+ EventBus.emit('anatomy-in-datasets', r.forFlatmap)
364
+ EventBus.emit('number-of-datasets-for-anatomies', r.forScaffold)
365
+ })
366
+ this.algoliaClient
367
+ .search(getFilters(filters), query, this.calculateSPARCOffest(), this.SPARCLimit(this.pmrResultsOnlyFlag) )
368
+ .then((searchData) => {
369
+ this.sparcNumberOfHits = searchData.total
370
+ this.discoverIds = searchData.discoverIds
371
+ this._dois = searchData.dois
372
+ searchData.items.forEach((item) => {
373
+ item.detailsReady = false
374
+ this.results.push(item)
375
+ })
376
+ // add the items to the results
377
+ this.results.concat(searchData.items)
378
+ this.loadingCards = false
379
+ this.scrollToTop()
380
+ this.$emit('search-changed', {
381
+ value: this.searchInput,
382
+ type: 'query-update',
383
+ })
384
+ if (this._abortController) this._abortController.abort()
385
+ this._abortController = new AbortController()
386
+ const signal = this._abortController.signal
387
+ //Search ongoing, let the current flow progress
388
+ this.perItemSearch(signal, { count: 0 })
389
+ })
390
+ },
391
+ filtersLoading: function (val) {
392
+ this.loadingCards = val
393
+ },
394
+ numberPerPageUpdate: function (val) {
395
+ this.numberPerPage = val
396
+ this.pageChange(1)
397
+ },
398
+ pageChange: function (page) {
399
+ this.page = page
400
+ this.results = []
401
+ this.calculateVariableRatio()
402
+ this.openSearch(this.filter, this.searchInput, false)
403
+ },
404
+ handleMissingData: function (doi) {
405
+ let i = this.results.findIndex((res) => res.doi === doi)
406
+ if (this.results[i]) this.results[i].detailsReady = true
407
+ },
408
+ perItemSearch: function (signal, data) {
409
+ //Maximum 10 downloads at once to prevent long waiting time
410
+ //between unfinished search and new search
411
+ const maxDownloads = 10
412
+ if (maxDownloads > data.count) {
413
+ const doi = this._dois.shift()
414
+ if (doi) {
415
+ data.count++
416
+ this.callSciCrunch(this.envVars.API_LOCATION, { dois: [doi] }, signal)
417
+ .then((result) => {
418
+ if (result.numberOfHits === 0) this.handleMissingData(doi)
419
+ else this.resultsProcessing(result)
420
+ this.$refs.content.style['overflow-y'] = 'scroll'
421
+ data.count--
422
+ //Async::Download finished, get the next one
423
+ this.perItemSearch(signal, data)
424
+ })
425
+ .catch((result) => {
426
+ if (result.name !== 'AbortError') {
427
+ this.handleMissingData(doi)
428
+ data.count--
429
+ //Async::Download not aborted, get the next one
430
+ this.perItemSearch(signal, data)
431
+ }
432
+ })
433
+ //Check and make another request until it gets to max downloads
434
+ this.perItemSearch(signal, data)
435
+ }
436
+ }
437
+ },
438
+ scrollToTop: function () {
439
+ if (this.$refs.content) {
440
+ this.$refs.content.scroll({ top: 0, behavior: 'smooth' })
441
+ }
442
+ },
443
+ resetPageNavigation: function () {
444
+ this.page = 1
445
+ },
446
+ resultsProcessing: function (data) {
447
+ this.lastSearch = this.searchInput
448
+
449
+ if (data.results.length === 0) {
450
+ return
451
+ }
452
+ data.results.forEach((element) => {
453
+ // match the scicrunch result with algolia result
454
+ let i = this.results.findIndex((res) =>
455
+ element.doi ? element.doi.includes(res.doi) : false
456
+ )
457
+ // Assign scicrunch results to the object
458
+ Object.assign(this.results[i], element)
459
+ // Assign the attributes that need some processing
460
+ Object.assign(this.results[i], {
461
+ numberSamples: element.sampleSize ? parseInt(element.sampleSize) : 0,
462
+ numberSubjects: element.subjectSize
463
+ ? parseInt(element.subjectSize)
464
+ : 0,
465
+ updated:
466
+ (element.updated && element.updated.length) > 0
467
+ ? element.updated[0].timestamp.split('T')[0]
468
+ : '',
469
+ url: element.uri[0],
470
+ datasetId: element.dataset_identifier,
471
+ datasetRevision: element.dataset_revision,
472
+ datasetVersion: element.dataset_version,
473
+ organs:
474
+ element.organs && element.organs.length > 0
475
+ ? [...new Set(element.organs.map((v) => v.name))]
476
+ : undefined,
477
+ species: element.organisms
478
+ ? element.organisms[0].species
479
+ ? [
480
+ ...new Set(
481
+ element.organisms.map((v) =>
482
+ v.species ? v.species.name : null
483
+ )
484
+ ),
485
+ ]
486
+ : undefined
487
+ : undefined, // This processing only includes each gender once into 'sexes'
488
+ scaffolds: element['abi-scaffold-metadata-file'],
489
+ thumbnails: element['abi-thumbnail']
490
+ ? element['abi-thumbnail']
491
+ : element['abi-scaffold-thumbnail'],
492
+ scaffoldViews: element['abi-scaffold-view-file'],
493
+ videos: element.video,
494
+ plots: element['abi-plot'],
495
+ images: element['common-images'],
496
+ contextualInformation:
497
+ element['abi-contextual-information'].length > 0
498
+ ? element['abi-contextual-information']
499
+ : undefined,
500
+ segmentation: element['mbf-segmentation'],
501
+ simulation: element['abi-simulation-file'],
502
+ additionalLinks: element.additionalLinks,
503
+ detailsReady: true,
504
+ })
505
+ this.results[i] = this.results[i]
506
+ })
507
+ },
508
+ createfilterParams: function (params) {
509
+ let p = new URLSearchParams()
510
+ //Check if field is array or value
511
+ for (const key in params) {
512
+ if (Array.isArray(params[key])) {
513
+ params[key].forEach((e) => {
514
+ p.append(key, e)
515
+ })
516
+ } else {
517
+ p.append(key, params[key])
518
+ }
519
+ }
520
+ return p.toString()
521
+ },
522
+ callSciCrunch: function (apiLocation, params = {}, signal) {
523
+ return new Promise((resolve, reject) => {
524
+ // Add parameters if we are sent them
525
+ let fullEndpoint =
526
+ this.envVars.API_LOCATION +
527
+ this.searchEndpoint +
528
+ '?' +
529
+ this.createfilterParams(params)
530
+ fetch(fullEndpoint, { signal })
531
+ .then(handleErrors)
532
+ .then((response) => response.json())
533
+ .then((data) => resolve(data))
534
+ .catch((data) => reject(data))
535
+ })
536
+ },
537
+ getAlgoliaFacets: async function () {
538
+ let facets = await this.algoliaClient.getAlgoliaFacets(
539
+ facetPropPathMapping
540
+ )
541
+ return facets
542
+ },
543
+ searchHistorySearch: function (item) {
544
+ this.searchInput = item.search
545
+ this.filters = item.filters
546
+ this.openSearch(item.filters, item.search)
547
+ },
548
+ onSelectValueChange: function (val) {
549
+ this.mode = val
550
+ this.openSearch(this.filter, this.searchInput, val)
551
+ },
552
+ },
553
+ mounted: function () {
554
+ // initialise algolia
555
+ this.algoliaClient = new AlgoliaClient(
556
+ this.envVars.ALGOLIA_ID,
557
+ this.envVars.ALGOLIA_KEY,
558
+ this.envVars.PENNSIEVE_API_LOCATION
559
+ )
560
+ this.algoliaClient.initIndex(this.envVars.ALGOLIA_INDEX)
561
+
562
+ // initialise flatmap queries
563
+ this.flatmapQueries = new FlatmapQueries()
564
+ this.flatmapQueries.initialise(this.envVars.FLATMAP_API_LOCATION)
565
+
566
+ // open search
567
+ this.openSearch(this.filter, this.searchInput, this.mode )
568
+ },
569
+ created: function () {
570
+ //Create non-reactive local variables
571
+ this.searchEndpoint = 'dataset_info/using_multiple_dois/'
572
+ },
573
+ }
574
+ </script>
575
+
576
+ <style lang="scss" scoped>
577
+ .dataset-card {
578
+ position: relative;
579
+
580
+ &::before {
581
+ content: "";
582
+ display: block;
583
+ width: calc(100% - 15px);
584
+ height: 100%;
585
+ position: absolute;
586
+ top: 7px;
587
+ left: 7px;
588
+ border-style: solid;
589
+ border-radius: 5px;
590
+ border-color: transparent;
591
+ }
592
+
593
+ &:hover {
594
+ &::before {
595
+ border-color: var(--el-color-primary);
596
+ }
597
+ }
598
+ }
599
+
600
+ .content-card {
601
+ height: 100%;
602
+ flex-flow: column;
603
+ display: flex;
604
+ }
605
+
606
+ .data-type-select {
607
+ width: 90px;
608
+ margin-left: 10px;
609
+ }
610
+
611
+ .button {
612
+ background-color: $app-primary-color;
613
+ border: $app-primary-color;
614
+ color: white;
615
+ }
616
+
617
+ .step-item {
618
+ font-size: 14px;
619
+ margin-bottom: 18px;
620
+ text-align: left;
621
+ }
622
+
623
+ .search-input {
624
+ width: 298px !important;
625
+ height: 40px;
626
+ padding-right: 14px;
627
+ align-items: left;
628
+ }
629
+
630
+ .header {
631
+ border: solid 1px #292b66;
632
+ background-color: #292b66;
633
+ text-align: left;
634
+
635
+ .el-button {
636
+ &:hover,
637
+ &:focus {
638
+ background: $app-primary-color;
639
+ box-shadow: -3px 2px 4px #00000040;
640
+ color: #fff;
641
+ }
642
+ }
643
+ }
644
+
645
+ .pagination {
646
+ padding-bottom: 16px;
647
+ background-color: white;
648
+ padding-left: 95px;
649
+ font-weight: bold;
650
+ }
651
+
652
+ .pagination :deep(button) {
653
+ background-color: white !important;
654
+ }
655
+ .pagination :deep(li) {
656
+ background-color: white !important;
657
+ }
658
+ .pagination :deep(li.is-active) {
659
+ color: $app-primary-color;
660
+ }
661
+
662
+ .error-feedback {
663
+ font-family: Asap;
664
+ font-size: 14px;
665
+ font-style: italic;
666
+ padding-top: 15px;
667
+ }
668
+
669
+ .content-card :deep(.el-card__header) {
670
+ background-color: #292b66;
671
+ padding: 1rem;
672
+ }
673
+
674
+ .content-card :deep(.el-card__body) {
675
+ background-color: #f7faff;
676
+ overflow-y: hidden;
677
+ padding: 1rem;
678
+ }
679
+
680
+ .content {
681
+ // width: 515px;
682
+ flex: 1 1 auto;
683
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.06);
684
+ border: solid 1px #e4e7ed;
685
+ background-color: #ffffff;
686
+ overflow-y: scroll;
687
+ scrollbar-width: thin;
688
+ }
689
+
690
+ .content :deep(.el-loading-spinner .path) {
691
+ stroke: $app-primary-color;
692
+ }
693
+
694
+ .content :deep(.step-item:first-child .seperator-path) {
695
+ display: none;
696
+ }
697
+
698
+ .content :deep(.step-item:not(:first-child) .seperator-path) {
699
+ width: 455px;
700
+ height: 0px;
701
+ border: solid 1px #e4e7ed;
702
+ background-color: #e4e7ed;
703
+ }
704
+
705
+ .scrollbar::-webkit-scrollbar-track {
706
+ border-radius: 10px;
707
+ background-color: #f5f5f5;
708
+ }
709
+
710
+ .scrollbar::-webkit-scrollbar {
711
+ width: 12px;
712
+ right: -12px;
713
+ background-color: #f5f5f5;
714
+ }
715
+
716
+ .scrollbar::-webkit-scrollbar-thumb {
717
+ border-radius: 4px;
718
+ box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.06);
719
+ background-color: #979797;
720
+ }
721
+
722
+ :deep(.el-input__suffix) {
723
+ padding-right: 0px;
724
+ }
725
+
726
+ :deep(.my-drawer) {
727
+ background: rgba(0, 0, 0, 0);
728
+ box-shadow: none;
729
+ }
730
+ </style>