diff --git a/deploy/datasets/index.js b/deploy/datasets/index.js index 1ab3efac04ce2a1bb9d4b7e12317830fe61e9905..13dc40ccd9ffbdfa7d4f9f2b8004d1ec1c968960 100644 --- a/deploy/datasets/index.js +++ b/deploy/datasets/index.js @@ -6,6 +6,7 @@ const { init, getDatasets, getPreview, getDatasetFromId, getDatasetFileAsZip, ge const { retry } = require('./util') const url = require('url') const qs = require('querystring') +const archiver = require('archiver') const bodyParser = require('body-parser') @@ -181,4 +182,34 @@ datasetsRouter.get('/downloadKgFiles', checkKgQuery, async (req, res) => { } }) +datasetsRouter.post('/bulkDownloadKgFiles', bodyParser.urlencoded({ extended: false }), async (req, res) => { + try{ + const { body = {}, user } = req + const { kgIds } = body + if (!kgIds) throw new Error(`kgIds needs to be populated`) + const arrKgIds = JSON.parse(kgIds) + if (!Array.isArray(arrKgIds)) { + throw new Error(`kgIds needs to be an array`) + } + if (arrKgIds.length === 0) { + throw new Error(`There needs to be at least 1 kgId in kgIds`) + } + const zip = archiver('zip') + for (const kgId of arrKgIds) { + zip.append( + await getDatasetFileAsZip({ user, kgId }), + { + name: `${kgId}.zip` + } + ) + } + zip.finalize() + res.setHeader('Content-disposition', `attachment; filename="bulkDsDownload.zip"`) + res.setHeader('Content-Type', 'application/zip') + zip.pipe(res) + }catch(e){ + res.status(400).send(e.toString()) + } +}) + module.exports = datasetsRouter \ No newline at end of file diff --git a/e2e/src/advanced/favDatasets.e2e-spec.js b/e2e/src/advanced/favDatasets.e2e-spec.js new file mode 100644 index 0000000000000000000000000000000000000000..0a90936c3bd505f68ed4a34c11da39c4b7a00416 --- /dev/null +++ b/e2e/src/advanced/favDatasets.e2e-spec.js @@ -0,0 +1,205 @@ +const { AtlasPage } = require('../util') + +const template = 'ICBM 2009c Nonlinear Asymmetric' +const area = 'Area hOc1 (V1, 17, CalcS)' + +const receptorName = `Density measurements of different receptors for Area hOc1` +const pmapName = `Probabilistic cytoarchitectonic map of Area hOc1 (V1, 17, CalcS) (v2.4)` + +// TODO finish writing tests +describe(`fav'ing dataset`, () => { + let iavPage + beforeEach(async () => { + iavPage = new AtlasPage() + await iavPage.init() + await iavPage.goto() + await iavPage.selectTitleCard(template) + await iavPage.wait(500) + await iavPage.waitUntilAllChunksLoaded() + }) + + afterEach(async () => { + await iavPage.clearAlerts() + await iavPage.wait(500) + + try { + await iavPage.clearSearchRegionWithText() + await iavPage.wait(500) + await iavPage.clearAllSelectedRegions() + }catch(e) { + + } + + try { + await iavPage.showPinnedDatasetPanel() + const textsArr = await iavPage.getPinnedDatasetsFromOpenedPanel() + let length = textsArr.length + while(length > 0){ + await iavPage.unpinNthDatasetFromOpenedPanel(0) + length -- + } + }catch(e){ + + } + + await iavPage.wait(500) + }) + + it('> dataset can be fav ed from result panel', async () => { + + await iavPage.searchRegionWithText(area) + await iavPage.wait(2000) + await iavPage.selectSearchRegionAutocompleteWithText() + await iavPage.dismissModal() + await iavPage.searchRegionWithText('') + + const datasets = await iavPage.getVisibleDatasets() + + const receptorIndex = datasets.indexOf(receptorName) + const probMap = datasets.indexOf(pmapName) + expect(receptorIndex).toBeGreaterThanOrEqual(0) + expect(probMap).toBeGreaterThanOrEqual(0) + + await iavPage.togglePinNthDataset(receptorIndex) + await iavPage.wait(500) + const txt = await iavPage.getSnackbarMessage() + expect(txt).toEqual(`Pinned dataset: ${receptorName}`) + + await iavPage.togglePinNthDataset(probMap) + await iavPage.wait(500) + const txt2 = await iavPage.getSnackbarMessage() + expect(txt2).toEqual(`Pinned dataset: ${pmapName}`) + }) + + + describe('> fav dataset list', () => { + beforeEach(async () => { + + await iavPage.searchRegionWithText(area) + await iavPage.wait(2000) + await iavPage.selectSearchRegionAutocompleteWithText() + await iavPage.dismissModal() + await iavPage.searchRegionWithText('') + + const datasets = await iavPage.getVisibleDatasets() + + const receptorIndex = datasets.indexOf(receptorName) + const probMap = datasets.indexOf(pmapName) + + await iavPage.togglePinNthDataset(receptorIndex) + await iavPage.togglePinNthDataset(probMap) + }) + + it('> fav ed dataset is visible on UI', async () => { + const number = await iavPage.getNumberOfFavDataset() + expect(number).toEqual(2) + }) + + it('> clicking pin shows pinned datasets', async () => { + await iavPage.showPinnedDatasetPanel() + await iavPage.wait(500) + const textsArr = await iavPage.getPinnedDatasetsFromOpenedPanel() + + expect(textsArr.length).toEqual(2) + expect(textsArr).toContain(receptorName) + expect(textsArr).toContain(pmapName) + }) + + it('> click unpin in fav data panel unpins, but also allow user to undo', async () => { + await iavPage.showPinnedDatasetPanel() + await iavPage.wait(1000) + const textsArr = await iavPage.getPinnedDatasetsFromOpenedPanel() + const idx = textsArr.indexOf(receptorName) + if (idx < 0) throw new Error(`index of receptor name not found: ${receptorName}: ${textsArr}`) + await iavPage.unpinNthDatasetFromOpenedPanel(idx) + await iavPage.wait(500) + + const txt = await iavPage.getSnackbarMessage() + expect(txt).toEqual(`Unpinned dataset: ${receptorName}`) + + const number = await iavPage.getNumberOfFavDataset() + expect(number).toEqual(1) + + await iavPage.clickSnackbarAction() + await iavPage.wait(500) + + const textsArr3 = await iavPage.getPinnedDatasetsFromOpenedPanel() + const number2 = await iavPage.getNumberOfFavDataset() + expect(number2).toEqual(2) + expect( + textsArr3.indexOf(receptorName) + ).toBeGreaterThanOrEqual(0) + + }) + + // TODO effectively test the bulk dl button + // it('> if fav dataset >=1, bulk dl btn is visible', async () => { + + // }) + }) + + describe('> fav functionality in detailed dataset panel', () => { + beforeEach(async () => { + + await iavPage.searchRegionWithText(area) + await iavPage.wait(2000) + await iavPage.selectSearchRegionAutocompleteWithText() + await iavPage.dismissModal() + await iavPage.searchRegionWithText('') + + + }) + it('> click pin in dataset detail sheet pins fav, but also allows user to undo', async () => { + + const datasets = await iavPage.getVisibleDatasets() + + const receptorIndex = datasets.indexOf(receptorName) + await iavPage.clickNthDataset(receptorIndex) + await iavPage.wait(500) + await iavPage.clickModalBtnByText(/pin\ this\ dataset/i) + await iavPage.wait(500) + + const txt = await iavPage.getSnackbarMessage() + expect(txt).toEqual(`Pinned dataset: ${receptorName}`) + + const number = await iavPage.getNumberOfFavDataset() + expect(number).toEqual(1) + + await iavPage.clickSnackbarAction() + await iavPage.wait(500) + + const number2 = await iavPage.getNumberOfFavDataset() + expect(number2).toEqual(0) + }) + + it('click unpin in dataset detail sheet unpins fav, but also allows user to undo', async () => { + + const datasets = await iavPage.getVisibleDatasets() + + const receptorIndex = datasets.indexOf(receptorName) + await iavPage.clickNthDataset(receptorIndex) + await iavPage.wait(500) + await iavPage.clickModalBtnByText(/pin\ this\ dataset/i) + await iavPage.wait(500) + + const numberOfFav = await iavPage.getNumberOfFavDataset() + expect(numberOfFav).toEqual(1) + + await iavPage.clickModalBtnByText(/unpin\ this\ dataset/i) + await iavPage.wait(500) + + const txt = await iavPage.getSnackbarMessage() + expect(txt).toEqual(`Unpinned dataset: ${receptorName}`) + + const numberOfFav1 = await iavPage.getNumberOfFavDataset() + expect(numberOfFav1).toEqual(0) + + await iavPage.clickSnackbarAction() + await iavPage.wait(500) + + + const numberOfFav2 = await iavPage.getNumberOfFavDataset() + expect(numberOfFav2).toEqual(1) + }) + }) +}) diff --git a/e2e/src/util.js b/e2e/src/util.js index 281cdcfe5101dc744c1f2a4e1761024f9ad8a40e..d1128db02fe640a53ed542fcb44f20adc0bf3aca 100644 --- a/e2e/src/util.js +++ b/e2e/src/util.js @@ -127,6 +127,33 @@ class WdBase{ if (!ms) throw new Error(`wait duration must be specified!`) await this._browser.sleep(ms) } + + async getSnackbarMessage(){ + const txt = await this._driver + .findElement( By.tagName('simple-snack-bar') ) + .findElement( By.tagName('span') ) + .getText() + return txt + } + + async clickSnackbarAction(){ + await this._driver + .findElement( By.tagName('simple-snack-bar') ) + .findElement( By.tagName('button') ) + .click() + } + + async clearAlerts() { + await this._driver + .actions() + .sendKeys( + Key.ESCAPE, + Key.ESCAPE, + Key.ESCAPE, + Key.ESCAPE + ) + .perform() + } } class WdLayoutPage extends WdBase{ @@ -135,10 +162,13 @@ class WdLayoutPage extends WdBase{ super() } + _getModal(){ + return this._browser.findElement( By.tagName('mat-dialog-container') ) + } + async dismissModal() { try { - const okBtn = await this._browser - .findElement( By.tagName('mat-dialog-container') ) + const okBtn = await this._getModal() .findElement( By.tagName('mat-dialog-actions') ) .findElement( By.css('button[color="primary"]') ) await okBtn.click() @@ -147,6 +177,42 @@ class WdLayoutPage extends WdBase{ } } + _getModalBtns(){ + return this._getModal() + .findElement( By.tagName('mat-card-actions') ) + .findElements( By.tagName('button') ) + } + + async getModalActions(){ + const btns = await this._getModalBtns() + + const arr = [] + for (const btn of btns){ + arr.push(await _getTextFromWebElement(btn)) + } + return arr + } + + // text can be instance of regex or string + async clickModalBtnByText(text){ + const btns = await this._getModalBtns() + const arr = await this.getModalActions() + if (typeof text === 'string') { + const idx = arr.indexOf(text) + if (idx < 0) throw new Error(`clickModalBtnByText: ${text} not found.`) + await btns[idx].click() + return + } + if (text instanceof RegExp) { + const idx = arr.findIndex(item => text.test(item)) + if (idx < 0) throw new Error(`clickModalBtnByText: regexp ${text.toString()} not found`) + await btns[idx].click() + return + } + + throw new Error(`clickModalBtnByText arg must be instance of string or regexp`) + } + async _findTitleCard(title) { const titleCards = await this._browser .findElement( By.tagName('ui-splashscreen') ) @@ -273,6 +339,55 @@ class WdLayoutPage extends WdBase{ ) .click() } + + // Signin banner + _getFavDatasetIcon(){ + return this._driver + .findElement( By.css('[aria-label="Show pinned datasets"]') ) + } + + async getNumberOfFavDataset(){ + const attr = await this._getFavDatasetIcon().getAttribute('pinned-datasets-length') + return Number(attr) + } + + async showPinnedDatasetPanel(){ + await this._getFavDatasetIcon().click() + await this.wait(500) + } + + _getPinnedDatasetPanel(){ + return this._driver + .findElement( + By.css('[aria-label="Pinned datasets panel"]') + ) + } + + async getPinnedDatasetsFromOpenedPanel(){ + const list = await this._getPinnedDatasetPanel() + .findElements( + By.tagName('mat-list-item') + ) + + const returnArr = [] + for (const el of list) { + const text = await _getTextFromWebElement(el) + returnArr.push(text) + } + return returnArr + } + + async unpinNthDatasetFromOpenedPanel(index){ + const list = await this._getPinnedDatasetPanel() + .findElements( + By.tagName('mat-list-item') + ) + + if (!list[index]) throw new Error(`index out of bound: ${index} in list with size ${list.length}`) + await list[index] + .findElement( By.css('[aria-label="Toggle pinning this dataset"]') ) + .click() + } } class WdIavPage extends WdLayoutPage{ @@ -379,11 +494,15 @@ class WdIavPage extends WdLayoutPage{ } } - async getVisibleDatasets() { - const singleDatasetListView = await this._browser + _getSingleDatasetListView(){ + return this._browser .findElement( By.tagName('data-browser') ) .findElement( By.css('div.cdk-virtual-scroll-content-wrapper') ) .findElements( By.tagName('single-dataset-list-view') ) + } + + async getVisibleDatasets() { + const singleDatasetListView = await this._getSingleDatasetListView() const returnArr = [] for (const item of singleDatasetListView) { @@ -392,6 +511,21 @@ class WdIavPage extends WdLayoutPage{ return returnArr } + async clickNthDataset(index){ + if (!Number.isInteger(index)) throw new Error(`index needs to be an integer`) + const list = await this._getSingleDatasetListView() + await list[index].click() + } + + async togglePinNthDataset(index) { + if (!Number.isInteger(index)) throw new Error(`index needs to be an integer`) + const list = await this._getSingleDatasetListView() + if (!list[index]) throw new Error(`out of bound ${index} in list with length ${list.length}`) + await list[index] + .findElement( By.css('[aria-label="Toggle pinning this dataset"]') ) + .click() + } + async viewerIsPopulated() { const ngContainer = await this._browser.findElement( By.id('neuroglancer-container') diff --git a/src/services/state/dataStore.store.ts b/src/services/state/dataStore.store.ts index e9a9e301375cc1923fe5833bb3001e79821276f3..5d16c11a604d0d01b6db3913c05c998840dad3bd 100644 --- a/src/services/state/dataStore.store.ts +++ b/src/services/state/dataStore.store.ts @@ -1,5 +1,6 @@ import { Action } from '@ngrx/store' import { GENERAL_ACTION_TYPES } from '../stateStore.service' +import { LOCAL_STORAGE_CONST } from 'src/util/constants' /** * TODO merge with databrowser.usereffect.ts @@ -12,14 +13,27 @@ export interface DatasetPreview { export interface IStateInterface { fetchedDataEntries: IDataEntry[] - favDataEntries: IDataEntry[] + favDataEntries: Partial<IDataEntry>[] fetchedSpatialData: IDataEntry[] datasetPreviews: DatasetPreview[] } + + export const defaultState = { fetchedDataEntries: [], - favDataEntries: [], + favDataEntries: (() => { + try { + const saved = localStorage.getItem(LOCAL_STORAGE_CONST.FAV_DATASET) + const arr = JSON.parse(saved) as any[] + return arr.every(item => item && !!item.fullId) + ? arr + : [] + } catch (e) { + // TODO propagate error + return [] + } + })(), fetchedSpatialData: [], datasetPreviews: [], } diff --git a/src/ui/databrowserModule/bulkDownload/bulkDownloadBtn.component.ts b/src/ui/databrowserModule/bulkDownload/bulkDownloadBtn.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..28dbf168600ae2902fba295f54b8d0672319237d --- /dev/null +++ b/src/ui/databrowserModule/bulkDownload/bulkDownloadBtn.component.ts @@ -0,0 +1,53 @@ +import { Component, Input, OnChanges, Pipe, PipeTransform, ChangeDetectionStrategy } from "@angular/core"; +import { AtlasViewerConstantsServices } from "../singleDataset/singleDataset.base"; +import { IDataEntry } from "src/services/stateStore.service"; +import { getKgSchemaIdFromFullId } from "../util/getKgSchemaIdFromFullId.pipe"; + +const ARIA_LABEL_HAS_DOWNLOAD = `Bulk download all favourited datasets` +const ARIA_LABEL_HAS_NO_DOWNLOAD = `No favourite datasets to download` + +@Component({ + selector: 'iav-datamodule-bulkdownload-cmp', + templateUrl: './bulkDownloadBtn.template.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) + +export class BulkDownloadBtn implements OnChanges{ + @Input() + kgSchema = 'minds/core/dataset/v1.0.0' + + @Input() + kgIds: string[] = [] + + public postUrl: string + public stringifiedKgIds: string = `[]` + public ariaLabel = ARIA_LABEL_HAS_DOWNLOAD + + constructor( + constantService: AtlasViewerConstantsServices + ){ + const _url = new URL(`datasets/bulkDownloadKgFiles`, constantService.backendUrl) + this.postUrl = _url.toString() + } + + ngOnChanges(){ + this.stringifiedKgIds = JSON.stringify(this.kgIds) + this.ariaLabel = this.kgIds.length === 0 + ? ARIA_LABEL_HAS_NO_DOWNLOAD + : ARIA_LABEL_HAS_DOWNLOAD + } +} + +@Pipe({ + name: 'iavDatamoduleTransformDsToIdPipe' +}) + +export class TransformDatasetToIdPipe implements PipeTransform{ + public transform(datasets: IDataEntry[]): string[]{ + return datasets.map(({ fullId }) => { + const re = getKgSchemaIdFromFullId(fullId) + if (re) return re[1] + else return null + }) + } +} diff --git a/src/ui/databrowserModule/bulkDownload/bulkDownloadBtn.template.html b/src/ui/databrowserModule/bulkDownload/bulkDownloadBtn.template.html new file mode 100644 index 0000000000000000000000000000000000000000..6e2543715588361e3a65bc857c037edce88bdbf4 --- /dev/null +++ b/src/ui/databrowserModule/bulkDownload/bulkDownloadBtn.template.html @@ -0,0 +1,20 @@ +<form [action]="postUrl" + method="POST" + target="_blank" + #bulkDlForm> + <input + hidden + [value]="stringifiedKgIds" + type="text" + name="kgIds" + id="kgIds" + readonly="readonly"> + + <button mat-icon-button + [attr.aria-label]="ariaLabel" + [ngClass]="{'text-muted': kgIds.length === 0}" + [matTooltip]="kgIds.length === 0 ? 'No favourite datasets to download' : 'Bulk download all favourited datasets'" + (click)="kgIds.length === 0 ? null : bulkDlForm.submit()"> + <i class="fas fa-download"></i> + </button> +</form> \ No newline at end of file diff --git a/src/ui/databrowserModule/databrowser.module.ts b/src/ui/databrowserModule/databrowser.module.ts index e2691ed9f9b9088343111934b521167d2a3857b4..c0dd1fd10c3ba6bb94dd8bb61426c9fc09472407 100644 --- a/src/ui/databrowserModule/databrowser.module.ts +++ b/src/ui/databrowserModule/databrowser.module.ts @@ -27,6 +27,7 @@ import { ResetCounterModalityPipe } from "./util/resetCounterModality.pipe"; import { PreviewFileVisibleInSelectedReferenceTemplatePipe } from "./util/previewFileDisabledByReferenceSpace.pipe"; import { DatasetPreviewList, UnavailableTooltip } from "./singleDataset/datasetPreviews/datasetPreviewsList/datasetPreviewList.component"; import { PreviewComponentWrapper } from "./preview/previewComponentWrapper/previewCW.component"; +import { BulkDownloadBtn, TransformDatasetToIdPipe } from "./bulkDownload/bulkDownloadBtn.component"; @NgModule({ imports: [ @@ -44,6 +45,7 @@ import { PreviewComponentWrapper } from "./preview/previewComponentWrapper/previ SingleDatasetListView, DatasetPreviewList, PreviewComponentWrapper, + BulkDownloadBtn, /** * pipes @@ -63,6 +65,7 @@ import { PreviewComponentWrapper } from "./preview/previewComponentWrapper/previ ResetCounterModalityPipe, PreviewFileVisibleInSelectedReferenceTemplatePipe, UnavailableTooltip, + TransformDatasetToIdPipe, ], exports: [ DataBrowser, @@ -71,6 +74,8 @@ import { PreviewComponentWrapper } from "./preview/previewComponentWrapper/previ ModalityPicker, FilterDataEntriesbyMethods, GetKgSchemaIdFromFullIdPipe, + BulkDownloadBtn, + TransformDatasetToIdPipe, ], entryComponents: [ DataBrowser, diff --git a/src/ui/databrowserModule/databrowser.service.ts b/src/ui/databrowserModule/databrowser.service.ts index b635bd463ad63894356af48e3af1ed94bf72d898..de480e44d03abed477e4df8a4699fd2de6f9a34a 100644 --- a/src/ui/databrowserModule/databrowser.service.ts +++ b/src/ui/databrowserModule/databrowser.service.ts @@ -202,21 +202,21 @@ export class DatabrowserService implements OnDestroy { this.subscriptions.forEach(s => s.unsubscribe()) } - public toggleFav(dataentry: IDataEntry) { + public toggleFav(dataentry: Partial<IDataEntry>) { this.store.dispatch({ type: DATASETS_ACTIONS_TYPES.TOGGLE_FAV_DATASET, payload: dataentry, }) } - public saveToFav(dataentry: IDataEntry) { + public saveToFav(dataentry: Partial<IDataEntry>) { this.store.dispatch({ type: DATASETS_ACTIONS_TYPES.FAV_DATASET, payload: dataentry, }) } - public removeFromFav(dataentry: IDataEntry) { + public removeFromFav(dataentry: Partial<IDataEntry>) { this.store.dispatch({ type: DATASETS_ACTIONS_TYPES.UNFAV_DATASET, payload: dataentry, diff --git a/src/ui/databrowserModule/databrowser.useEffect.ts b/src/ui/databrowserModule/databrowser.useEffect.ts index 05a4c8d5ba6b58266d7b956ec45ced17189db2ff..f202ab20cd8015d4351e994a5869a7a24f281d18 100644 --- a/src/ui/databrowserModule/databrowser.useEffect.ts +++ b/src/ui/databrowserModule/databrowser.useEffect.ts @@ -2,12 +2,11 @@ import { Injectable, OnDestroy } from "@angular/core"; import { Actions, Effect, ofType } from "@ngrx/effects"; import { select, Store } from "@ngrx/store"; import { from, merge, Observable, of, Subscription, forkJoin, combineLatest } from "rxjs"; -import { catchError, filter, map, scan, switchMap, withLatestFrom, mapTo, shareReplay, startWith, distinctUntilChanged } from "rxjs/operators"; +import { filter, map, scan, switchMap, withLatestFrom, mapTo, shareReplay, startWith, distinctUntilChanged } from "rxjs/operators"; import { LoggingService } from "src/services/logging.service"; import { DATASETS_ACTIONS_TYPES, IDataEntry, ViewerPreviewFile } from "src/services/state/dataStore.store"; import { IavRootStoreInterface, ADD_NG_LAYER, CHANGE_NAVIGATION } from "src/services/stateStore.service"; import { LOCAL_STORAGE_CONST, DS_PREVIEW_URL } from "src/util/constants"; -import { getIdFromDataEntry } from "./databrowser.service"; import { KgSingleDatasetService } from "./kgSingleDatasetService.service"; import { determinePreviewFileType, PREVIEW_FILE_TYPES, PREVIEW_FILE_TYPES_NO_UI } from "./preview/previewFileIcon.pipe"; import { GLSL_COLORMAP_JET } from "src/atlasViewer/atlasViewer.constantService.service"; @@ -15,26 +14,9 @@ import { SHOW_BOTTOM_SHEET } from "src/services/state/uiState.store"; import { MatSnackBar } from "@angular/material/snack-bar"; import { MatDialog } from "@angular/material/dialog"; import { PreviewComponentWrapper } from "./preview/previewComponentWrapper/previewCW.component"; -import { GetKgSchemaIdFromFullIdPipe } from "./util/getKgSchemaIdFromFullId.pipe"; +import { getKgSchemaIdFromFullId } from "./util/getKgSchemaIdFromFullId.pipe"; import { HttpClient } from "@angular/common/http"; -const savedFav$ = of(window.localStorage.getItem(LOCAL_STORAGE_CONST.FAV_DATASET)).pipe( - map(string => JSON.parse(string)), - map(arr => { - if (arr.every(item => item.id )) { return arr } - throw new Error('Not every item has id and/or name defined') - }), - catchError(err => { - /** - * TODO emit proper error - * possibly wipe corrupted local stoage here? - */ - return of(null) - }), -) - -const getSchemaIdFromPipe = new GetKgSchemaIdFromFullIdPipe() - @Injectable({ providedIn: 'root', }) @@ -80,7 +62,7 @@ export class DataBrowserUseEffect implements OnDestroy { filter(datasetPreviews => datasetPreviews.length > 0), map((datasetPreviews) => datasetPreviews[datasetPreviews.length - 1]), switchMap(({ datasetId, filename }) =>{ - const re = getSchemaIdFromPipe.transform(datasetId) + const re = getKgSchemaIdFromFullId(datasetId) return this.http.get(`${DATASET_PREVIEW_URL}/${re[1]}/${filename}`).pipe( filter((file: any) => PREVIEW_FILE_TYPES_NO_UI.indexOf( determinePreviewFileType(file) ) < 0), mapTo({ @@ -93,7 +75,7 @@ export class DataBrowserUseEffect implements OnDestroy { // TODO replace with common/util/getIdFromFullId - const re = getSchemaIdFromPipe.transform(datasetId) + const re = getKgSchemaIdFromFullId(datasetId) this.dialog.open( PreviewComponentWrapper, { @@ -123,7 +105,7 @@ export class DataBrowserUseEffect implements OnDestroy { switchMap((arr: any[]) => { return merge( ... (arr.map(({ datasetId, filename }) => { - const re = getSchemaIdFromPipe.transform(datasetId) + const re = getKgSchemaIdFromFullId(datasetId) if (!re) throw new Error(`datasetId ${datasetId} does not follow organisation/domain/schema/version/uuid rule`) return forkJoin( @@ -184,7 +166,7 @@ export class DataBrowserUseEffect implements OnDestroy { ) ).pipe( map(([templateSelected, arr]) => { - const re = getSchemaIdFromPipe.transform( + const re = getKgSchemaIdFromFullId( (templateSelected && templateSelected.fullId) || '' ) const templateId = re && re[1] @@ -192,7 +174,7 @@ export class DataBrowserUseEffect implements OnDestroy { return determinePreviewFileType(file) === PREVIEW_FILE_TYPES.VOLUMES && file.referenceSpaces.findIndex(({ fullId }) => { if (fullId === '*') return true - const regex = getSchemaIdFromPipe.transform(fullId) + const regex = getKgSchemaIdFromFullId(fullId) const fileReferenceTemplateId = regex && regex[1] if (!fileReferenceTemplateId) return false return fileReferenceTemplateId === templateId @@ -244,13 +226,25 @@ export class DataBrowserUseEffect implements OnDestroy { withLatestFrom(this.favDataEntries$), map(([action, prevFavDataEntries]) => { const { payload = {} } = action as any - const { id } = payload + const { fullId } = payload + + const re1 = getKgSchemaIdFromFullId(fullId) - const wasFav = prevFavDataEntries.findIndex(ds => ds.id === id) >= 0 + if (!re1) { + return { + type: DATASETS_ACTIONS_TYPES.UPDATE_FAV_DATASETS, + favDataEntries: DATASETS_ACTIONS_TYPES.UPDATE_FAV_DATASETS, + } + } + const favIdx = prevFavDataEntries.findIndex(ds => { + const re2 = getKgSchemaIdFromFullId(ds.fullId) + if (!re2) return false + return re2[1] === re1[1] + }) return { type: DATASETS_ACTIONS_TYPES.UPDATE_FAV_DATASETS, - favDataEntries: wasFav - ? prevFavDataEntries.filter(ds => ds.id !== id) + favDataEntries: favIdx >= 0 + ? prevFavDataEntries.filter((_, idx) => idx !== favIdx) : prevFavDataEntries.concat(payload), } }), @@ -262,10 +256,18 @@ export class DataBrowserUseEffect implements OnDestroy { map(([action, prevFavDataEntries]) => { const { payload = {} } = action as any - const { id } = payload + const { fullId } = payload + + const re1 = getKgSchemaIdFromFullId(fullId) + return { type: DATASETS_ACTIONS_TYPES.UPDATE_FAV_DATASETS, - favDataEntries: prevFavDataEntries.filter(ds => ds.id !== id), + favDataEntries: prevFavDataEntries.filter(ds => { + const re2 = getKgSchemaIdFromFullId(ds.fullId) + if (!re2) return false + if (!re1) return true + return re2[1] !== re1[1] + }), } }), ) @@ -279,7 +281,21 @@ export class DataBrowserUseEffect implements OnDestroy { /** * check duplicate */ - const favDataEntries = prevFavDataEntries.find(favDEs => favDEs.id === payload.id) + const { fullId } = payload + const re1 = getKgSchemaIdFromFullId(fullId) + if (!re1) { + return { + type: DATASETS_ACTIONS_TYPES.UPDATE_FAV_DATASETS, + favDataEntries: prevFavDataEntries, + } + } + + const isDuplicate = prevFavDataEntries.some(favDe => { + const re2 = getKgSchemaIdFromFullId(favDe.fullId) + if (!re2) return false + return re1[1] === re2[1] + }) + const favDataEntries = isDuplicate ? prevFavDataEntries : prevFavDataEntries.concat(payload) @@ -300,54 +316,12 @@ export class DataBrowserUseEffect implements OnDestroy { * * do not save anything else on localstorage. This could potentially be leaking sensitive information */ - const serialisedFavDataentries = favDataEntries.map(dataentry => { - const id = getIdFromDataEntry(dataentry) - return { id } + const serialisedFavDataentries = favDataEntries.map(({ fullId }) => { + return { fullId } }) window.localStorage.setItem(LOCAL_STORAGE_CONST.FAV_DATASET, JSON.stringify(serialisedFavDataentries)) }), ) - - this.savedFav$ = savedFav$ - - this.onInitGetFav$ = this.savedFav$.pipe( - filter(v => !!v), - switchMap(arr => - merge( - ...arr.map(({ id: kgId }) => - from( this.kgSingleDatasetService.getInfoFromKg({ kgId })).pipe( - catchError(err => { - this.log.log(`fetchInfoFromKg error`, err) - return of(null) - }), - switchMap(dataset => - this.kgSingleDatasetService.datasetHasPreview(dataset).pipe( - catchError(err => { - this.log.log(`fetching hasPreview error`, err) - return of({}) - }), - map(resp => { - return { - ...dataset, - ...resp, - } - }), - ), - ), - ), - ), - ).pipe( - filter(v => !!v), - scan((acc, curr) => acc.concat(curr), []), - ), - ), - map(favDataEntries => { - return { - type: DATASETS_ACTIONS_TYPES.UPDATE_FAV_DATASETS, - favDataEntries, - } - }), - ) } public ngOnDestroy() { @@ -358,9 +332,6 @@ export class DataBrowserUseEffect implements OnDestroy { private savedFav$: Observable<Array<{id: string, name: string}> | null> - @Effect() - public onInitGetFav$: Observable<any> - private favDataEntries$: Observable<IDataEntry[]> @Effect() diff --git a/src/ui/databrowserModule/databrowser/databrowser.style.css b/src/ui/databrowserModule/databrowser/databrowser.style.css index 0a854b80f0892e5c1f9fd387efb5d16652d140f7..1180d17e0281ed33a5cb1b5ba71240216e38207e 100644 --- a/src/ui/databrowserModule/databrowser/databrowser.style.css +++ b/src/ui/databrowserModule/databrowser/databrowser.style.css @@ -1,7 +1,6 @@ modality-picker { font-size: 90%; - display:inline-block; } radio-list diff --git a/src/ui/databrowserModule/kgSingleDatasetService.service.ts b/src/ui/databrowserModule/kgSingleDatasetService.service.ts index 9544ef15f53013ffac9e3d238bb7b5ca2678ef74..0ad31491acbe3797dc07d376842a028381b304ec 100644 --- a/src/ui/databrowserModule/kgSingleDatasetService.service.ts +++ b/src/ui/databrowserModule/kgSingleDatasetService.service.ts @@ -15,8 +15,6 @@ export class KgSingleDatasetService implements OnDestroy { private subscriptions: Subscription[] = [] public ngLayers: Set<string> = new Set() - private getKgSchemaIdFromFullIdPipe: GetKgSchemaIdFromFullIdPipe = new GetKgSchemaIdFromFullIdPipe() - constructor( private constantService: AtlasViewerConstantsServices, private store$: Store<IavRootStoreInterface>, @@ -93,14 +91,6 @@ export class KgSingleDatasetService implements OnDestroy { }, }) } - - public getKgSchemaKgIdFromFullId(fullId: string) { - const match = this.getKgSchemaIdFromFullIdPipe.transform(fullId) - return match && { - kgSchema: match[0], - kgId: match[1], - } - } } interface KgQueryInterface { diff --git a/src/ui/databrowserModule/modalityPicker/modalityPicker.style.css b/src/ui/databrowserModule/modalityPicker/modalityPicker.style.css index df41aab07fc807183ce87f68846d37ea65578ec1..85feb59e81b8a38ede569688b605d82e39932cca 100644 --- a/src/ui/databrowserModule/modalityPicker/modalityPicker.style.css +++ b/src/ui/databrowserModule/modalityPicker/modalityPicker.style.css @@ -1,3 +1,9 @@ +:host +{ + display: flex; + flex-direction: column; +} + div { white-space: nowrap; diff --git a/src/ui/databrowserModule/singleDataset/detailedView/singleDataset.component.ts b/src/ui/databrowserModule/singleDataset/detailedView/singleDataset.component.ts index 6659e3791e4fed9af7cddfc986efb86b75e239db..e76e30ae9ce96d2575ea0013be013a9aa4d5900a 100644 --- a/src/ui/databrowserModule/singleDataset/detailedView/singleDataset.component.ts +++ b/src/ui/databrowserModule/singleDataset/detailedView/singleDataset.component.ts @@ -5,6 +5,7 @@ import { SingleDatasetBase, } from "../singleDataset.base"; import {MAT_DIALOG_DATA} from "@angular/material/dialog"; +import { MatSnackBar } from "@angular/material/snack-bar"; @Component({ selector: 'single-dataset-view', @@ -21,10 +22,10 @@ export class SingleDatasetView extends SingleDatasetBase { dbService: DatabrowserService, singleDatasetService: KgSingleDatasetService, cdr: ChangeDetectorRef, - + snackbar: MatSnackBar, @Optional() @Inject(MAT_DIALOG_DATA) data: any, ) { - super(dbService, singleDatasetService, cdr, data) + super(dbService, singleDatasetService, cdr,snackbar, data) } } diff --git a/src/ui/databrowserModule/singleDataset/detailedView/singleDataset.template.html b/src/ui/databrowserModule/singleDataset/detailedView/singleDataset.template.html index c473ac7849449ff477d1fc641056f402ef84dff8..993958bec3737b6311782c62075a22da6c962afc 100644 --- a/src/ui/databrowserModule/singleDataset/detailedView/singleDataset.template.html +++ b/src/ui/databrowserModule/singleDataset/detailedView/singleDataset.template.html @@ -1,16 +1,28 @@ <!-- title --> <mat-card-subtitle> - {{ name }} + <span *ngIf="name; else nameLoading"> + {{ name }} + </span> + <ng-template #nameLoading> + <div class="spinnerAnimationCircle"></div> + </ng-template> </mat-card-subtitle> <mat-card-content mat-dialog-content> <!-- description --> <small> - <markdown-dom class="d-block" [markdown]="description"> + <markdown-dom + *ngIf="description; else descriptionLoading" + class="d-block" + [markdown]="description"> </markdown-dom> + + <ng-template #descriptionLoading> + <div class="d-inline-block spinnerAnimationCircle"></div> + </ng-template> </small> <!-- publications --> @@ -46,15 +58,32 @@ </a> <!-- pin data --> - <button mat-button - *ngIf="downloadEnabled" - iav-stop="click mousedown" - (click)="toggleFav()" - color="primary" - [color]="(favedDataentries$ | async | datasetIsFaved : dataset) ? 'primary' : 'basic'"> - {{ (favedDataentries$ | async | datasetIsFaved : dataset) ? 'Unpin' : 'Pin' }} this dataset - <i class="fas fa-thumbtack"></i> - </button> + <ng-container *ngIf="downloadEnabled && kgId"> + + <!-- Is currently fav'ed --> + <ng-container *ngIf="favedDataentries$ | async | datasetIsFaved : dataset; else notCurrentlyFav"> + + <button mat-button + iav-stop="click mousedown" + (click)="undoableRemoveFav()" + color="primary"> + Unpin this dataset + <i class="fas fa-thumbtack"></i> + </button> + </ng-container> + + <!-- Is NOT currently fav'ed --> + <ng-template #notCurrentlyFav> + + <button mat-button + iav-stop="click mousedown" + (click)="undoableAddFav()" + color="default"> + Pin this dataset + <i class="fas fa-thumbtack"></i> + </button> + </ng-template> + </ng-container> <!-- download --> diff --git a/src/ui/databrowserModule/singleDataset/listView/singleDatasetListView.component.ts b/src/ui/databrowserModule/singleDataset/listView/singleDatasetListView.component.ts index e30630467500748ffd1075ee20066794eeb28542..be8e778626e3f6d81aaa408e0a6c1c4b0a1e94e8 100644 --- a/src/ui/databrowserModule/singleDataset/listView/singleDatasetListView.component.ts +++ b/src/ui/databrowserModule/singleDataset/listView/singleDatasetListView.component.ts @@ -19,44 +19,21 @@ import {MatSnackBar} from "@angular/material/snack-bar"; export class SingleDatasetListView extends SingleDatasetBase { constructor( - private _dbService: DatabrowserService, + _dbService: DatabrowserService, singleDatasetService: KgSingleDatasetService, cdr: ChangeDetectorRef, private dialog: MatDialog, - private snackBar: MatSnackBar, + snackBar: MatSnackBar, ) { - super(_dbService, singleDatasetService, cdr) + super(_dbService, singleDatasetService, cdr, snackBar) } public showDetailInfo() { this.dialog.open(SingleDatasetView, { - data: this.dataset, + autoFocus: false, + data: { + fullId: this.fullId + }, }) } - - public undoableRemoveFav() { - this.snackBar.open(`Unpinned dataset: ${this.dataset.name}`, 'Undo', { - duration: 5000, - }) - .afterDismissed() - .subscribe(({ dismissedByAction }) => { - if (dismissedByAction) { - this._dbService.saveToFav(this.dataset) - } - }) - this._dbService.removeFromFav(this.dataset) - } - - public undoableAddFav() { - this.snackBar.open(`Pin dataset: ${this.dataset.name}`, 'Undo', { - duration: 5000, - }) - .afterDismissed() - .subscribe(({ dismissedByAction }) => { - if (dismissedByAction) { - this._dbService.removeFromFav(this.dataset) - } - }) - this._dbService.saveToFav(this.dataset) - } } diff --git a/src/ui/databrowserModule/singleDataset/listView/singleDatasetListView.template.html b/src/ui/databrowserModule/singleDataset/listView/singleDatasetListView.template.html index ac442f6602363777ea6a667217a66f2da9f82f46..b8256849438468a91c3b92ff37ffaca0a81faae0 100644 --- a/src/ui/databrowserModule/singleDataset/listView/singleDatasetListView.template.html +++ b/src/ui/databrowserModule/singleDataset/listView/singleDatasetListView.template.html @@ -6,9 +6,12 @@ <!-- title --> <div class="flex-grow-1 name-container d-flex align-items-start"> - <small class="flex-grow-1 flex-shrink-1"> + <small *ngIf="name else loadingName" class="flex-grow-1 flex-shrink-1"> {{ name }} </small> + <ng-template #loadingName> + <div class="spinnerAnimationCircle"></div> + </ng-template> </div> @@ -19,6 +22,7 @@ <!-- unpin --> <button mat-icon-button *ngIf="favedDataentries$ | async | datasetIsFaved : dataset; else pinTmpl" + aria-label="Toggle pinning this dataset" (click)="undoableRemoveFav()" class="no-focus flex-grow-0 flex-shrink-0" color="primary"> @@ -28,6 +32,7 @@ <!-- pin --> <ng-template #pinTmpl> <button mat-icon-button + aria-label="Toggle pinning this dataset" (click)="undoableAddFav()" class="no-focus flex-grow-0 flex-shrink-0" color="basic"> @@ -122,6 +127,7 @@ <!-- pin dataset --> <button mat-icon-button + aria-label="Toggle pinning this dataset" *ngIf="downloadEnabled" iav-stop="click mousedown" (click)="toggleFav()" diff --git a/src/ui/databrowserModule/singleDataset/singleDataset.base.ts b/src/ui/databrowserModule/singleDataset/singleDataset.base.ts index 039a834d70ae4ae0fd78dcee07da90d45c750a71..dfb7f94a34a6bb746d7049611aae6bdd31c67e92 100644 --- a/src/ui/databrowserModule/singleDataset/singleDataset.base.ts +++ b/src/ui/databrowserModule/singleDataset/singleDataset.base.ts @@ -1,4 +1,4 @@ -import { ChangeDetectorRef, Input, OnInit, TemplateRef } from "@angular/core"; +import { ChangeDetectorRef, Input, OnInit, TemplateRef, OnChanges } from "@angular/core"; import { Observable } from "rxjs"; import { AtlasViewerConstantsServices } from "src/atlasViewer/atlasViewer.constantService.service"; import { IDataEntry, IFile, IPublication, ViewerPreviewFile } from 'src/services/state/dataStore.store' @@ -7,6 +7,8 @@ import { DatabrowserService } from "../databrowser.service"; import { KgSingleDatasetService } from "../kgSingleDatasetService.service"; import { DS_PREVIEW_URL } from 'src/util/constants' +import { getKgSchemaIdFromFullId } from "../util/getKgSchemaIdFromFullId.pipe"; +import { MatSnackBar } from "@angular/material/snack-bar"; export { DatabrowserService, @@ -15,7 +17,7 @@ export { AtlasViewerConstantsServices } -export class SingleDatasetBase implements OnInit { +export class SingleDatasetBase implements OnInit, OnChanges { @Input() public ripple: boolean = false @@ -27,7 +29,18 @@ export class SingleDatasetBase implements OnInit { @Input() public description?: string @Input() public publications?: IPublication[] - @Input() public kgSchema?: string + private _fullId: string + + @Input() + set fullId(val){ + this._fullId = val + } + + get fullId(){ + return this._fullId || (this.kgSchema && this.kgId && `${this.kgSchema}/${this.kgId}`) || null + } + + @Input() public kgSchema?: string = 'minds/core/dataset/v1.0.0' @Input() public kgId?: string @Input() public dataset: any = null @@ -59,16 +72,16 @@ export class SingleDatasetBase implements OnInit { private dbService: DatabrowserService, private singleDatasetService: KgSingleDatasetService, private cdr: ChangeDetectorRef, + private snackBar: MatSnackBar, dataset?: any, ) { - this.favedDataentries$ = this.dbService.favedDataentries$ + if (dataset) { - this.dataset = dataset const { fullId } = dataset - const obj = this.singleDatasetService.getKgSchemaKgIdFromFullId(fullId) + const obj = getKgSchemaIdFromFullId(fullId) if (obj) { - const { kgSchema, kgId } = obj + const [ kgSchema, kgId ] = obj this.kgSchema = kgSchema this.kgId = kgId } @@ -89,6 +102,38 @@ export class SingleDatasetBase implements OnInit { } } + public async fetchDatasetDetail(){ + try { + const { kgId } = this + if (!kgId) return + const dataset = await this.singleDatasetService.getInfoFromKg({ kgId }) + + const { name, description, publications, fullId } = dataset + this.name = name + this.description = description + this.publications = publications + this.fullId = fullId + + this.cdr.detectChanges() + } catch (e) { + // catch error + } + } + + public ngOnChanges(){ + if (!this.kgId) { + const fullId = this.fullId || this.dataset?.fullId + + const re = getKgSchemaIdFromFullId(fullId) + if (re) { + this.kgSchema = re[0] + this.kgId = re[1] + } + } + + this.fetchDatasetDetail() + } + public ngOnInit() { const { kgId, kgSchema, dataset } = this this.dlFromKgHref = this.singleDatasetService.getDownloadZipFromKgHref({ kgSchema, kgId }) @@ -160,7 +205,7 @@ export class SingleDatasetBase implements OnInit { } public toggleFav() { - this.dbService.toggleFav(this.dataset) + this.dbService.toggleFav({ fullId: this.fullId }) } public showPreviewList(templateRef: TemplateRef<any>) { @@ -170,4 +215,32 @@ export class SingleDatasetBase implements OnInit { public handlePreviewFile(file: ViewerPreviewFile) { this.singleDatasetService.previewFile(file, this.dataset) } + + public undoableRemoveFav() { + this.snackBar.open(`Unpinned dataset: ${this.name}`, 'Undo', { + duration: 5000, + politeness: "polite" + }) + .afterDismissed() + .subscribe(({ dismissedByAction }) => { + if (dismissedByAction) { + this.dbService.saveToFav({ fullId: this.fullId}) + } + }) + this.dbService.removeFromFav({ fullId: this.fullId}) + } + + public undoableAddFav() { + this.snackBar.open(`Pinned dataset: ${this.name}`, 'Undo', { + duration: 5000, + politeness: "polite" + }) + .afterDismissed() + .subscribe(({ dismissedByAction }) => { + if (dismissedByAction) { + this.dbService.removeFromFav({ fullId: this.fullId}) + } + }) + this.dbService.saveToFav({ fullId: this.fullId}) + } } diff --git a/src/ui/databrowserModule/util/datasetIsFaved.pipe.ts b/src/ui/databrowserModule/util/datasetIsFaved.pipe.ts index a7ca19ef1c0e712e46a895ff73ea06952ab3ff25..dcb578417f01260b7cd00fbcbd6f6930e6ebc247 100644 --- a/src/ui/databrowserModule/util/datasetIsFaved.pipe.ts +++ b/src/ui/databrowserModule/util/datasetIsFaved.pipe.ts @@ -1,5 +1,6 @@ import { Pipe, PipeTransform } from "@angular/core"; import { IDataEntry } from "src/services/stateStore.service"; +import { getKgSchemaIdFromFullId } from "./getKgSchemaIdFromFullId.pipe"; @Pipe({ name: 'datasetIsFaved', @@ -7,6 +8,12 @@ import { IDataEntry } from "src/services/stateStore.service"; export class DatasetIsFavedPipe implements PipeTransform { public transform(favedDataEntry: IDataEntry[], dataentry: IDataEntry): boolean { if (!dataentry) { return false } - return favedDataEntry.findIndex(ds => ds.id === dataentry.id) >= 0 + const re2 = getKgSchemaIdFromFullId(dataentry.fullId) + if (!re2) return false + return favedDataEntry.findIndex(ds => { + const re1 = getKgSchemaIdFromFullId(ds.fullId) + if (!re1) return false + return re1[1] === re2[1] + }) >= 0 } } diff --git a/src/ui/databrowserModule/util/getKgSchemaIdFromFullId.pipe.ts b/src/ui/databrowserModule/util/getKgSchemaIdFromFullId.pipe.ts index 9c95af439bb65496d8d9cc5956b1839b8edf4fa9..2d9854711410aab6a8cc4959dac6e324bd8f4391 100644 --- a/src/ui/databrowserModule/util/getKgSchemaIdFromFullId.pipe.ts +++ b/src/ui/databrowserModule/util/getKgSchemaIdFromFullId.pipe.ts @@ -6,9 +6,13 @@ import { Pipe, PipeTransform } from "@angular/core"; export class GetKgSchemaIdFromFullIdPipe implements PipeTransform { public transform(fullId: string): [string, string] { - if (!fullId) { return [null, null] } - const match = /([\w\-.]*\/[\w\-.]*\/[\w\-.]*\/[\w\-.]*)\/([\w\-.]*)$/.exec(fullId) - if (!match) { return [null, null] } - return [match[1], match[2]] + return getKgSchemaIdFromFullId(fullId) } } + +export function getKgSchemaIdFromFullId(fullId: string): [string, string]{ + if (!fullId) { return [null, null] } + const match = /([\w\-.]*\/[\w\-.]*\/[\w\-.]*\/[\w\-.]*)\/([\w\-.]*)$/.exec(fullId) + if (!match) { return [null, null] } + return [match[1], match[2]] +} \ No newline at end of file diff --git a/src/ui/signinBanner/signinBanner.template.html b/src/ui/signinBanner/signinBanner.template.html index 18254bba0fdc1b8d1145e3ad8b46100639d0a806..32deee57d34cd6be014c47040a9471fd681fe133 100644 --- a/src/ui/signinBanner/signinBanner.template.html +++ b/src/ui/signinBanner/signinBanner.template.html @@ -6,6 +6,8 @@ <div class="btnWrapper"> <button mat-icon-button + aria-label="Show pinned datasets" + [attr.pinned-datasets-length]="(favDataEntries$ | async)?.length" (click)="bottomSheet.open(savedDatasets)" [matBadge]="(favDataEntries$ | async)?.length > 0 ? (favDataEntries$ | async)?.length : null " matBadgeColor="accent" @@ -127,8 +129,17 @@ <!-- saved dataset tmpl --> <ng-template #savedDatasets> - <mat-list rol="list"> - <h3 mat-subheader>Pinned Datasets</h3> + <mat-list rol="list" + aria-label="Pinned datasets panel"> + <h3 mat-subheader> + <span> + Pinned Datasets + </span> + <iav-datamodule-bulkdownload-cmp + [kgIds]="favDataEntries$ | async | iavDatamoduleTransformDsToIdPipe"> + + </iav-datamodule-bulkdownload-cmp> + </h3> <!-- place holder when no fav data is available --> <mat-card *ngIf="(!(favDataEntries$ | async)) || (favDataEntries$ | async).length === 0"> diff --git a/src/util/constants.ts b/src/util/constants.ts index e9d16f04d6a432b054b425702525bfbcf6304342..b6f7198ec9bc85453c6a60d4b642ef670564f38a 100644 --- a/src/util/constants.ts +++ b/src/util/constants.ts @@ -6,7 +6,7 @@ export const LOCAL_STORAGE_CONST = { AGREE_COOKIE: 'fzj.xg.iv.AGREE_COOKIE', AGREE_KG_TOS: 'fzj.xg.iv.AGREE_KG_TOS', - FAV_DATASET: 'fzj.xg.iv.FAV_DATASET', + FAV_DATASET: 'fzj.xg.iv.FAV_DATASET_V2', } export const COOKIE_VERSION = '0.3.0'