diff --git a/common/constants.js b/common/constants.js index 3930701456f635903a155fc824992572478ba505..284345dec61ce5f62edaa1770fbb7b799abec7d6 100644 --- a/common/constants.js +++ b/common/constants.js @@ -8,6 +8,7 @@ COLLAPSE: 'Collapse', // dataset specific + EXPLORE_DATASET_IN_KG: `Explore dataset in Knowledge`, SHOW_DATASET_PREVIEW: 'Show dataset preview', TOGGLE_EXPLORE_PANEL: `Toggle explore panel`, MODALITY_FILTER: `Toggle dataset modality filter`, @@ -36,6 +37,7 @@ SHARE_CUSTOM_URL_DIALOG: 'Dialog for creating a custom URL', // parcellation region specific + GO_TO_REGION_CENTROID: 'Navigate to region centroid', SHOW_ORIGIN_DATASET: `Show probabilistic map`, SHOW_CONNECTIVITY_DATA: `Show connectivity data`, SHOW_IN_OTHER_REF_SPACE: `Show in other reference space`, diff --git a/src/glue.ts b/src/glue.ts index 1cb4534213bd9a051e9110782cd4856ed3344fda..1e2cc043ca988e2e2196ed6cbd6084e856ea40d8 100644 --- a/src/glue.ts +++ b/src/glue.ts @@ -23,8 +23,6 @@ const PREVIEW_FILE_TYPES_NO_UI = [ const DATASET_PREVIEW_ANNOTATION = `DATASET_PREVIEW_ANNOTATION` -export const GET_KGDS_PREVIEW_INFO_FROM_ID_FILENAME: InjectionToken<({ datasetId, filename }) => Observable<any>> = new InjectionToken('GET_KGDS_PREVIEW_INFO_FROM_ID_FILENAME') - export const glueActionToggleDatasetPreview = createAction( '[glue] toggleDatasetPreview', props<{ datasetPreviewFile: IDatasetPreviewData }>() diff --git a/src/main.module.ts b/src/main.module.ts index 5c8684a3d60cfb763777fedae3928b08f080e815..be401e8923810618d16b6bae7e6a5413e241b61d 100644 --- a/src/main.module.ts +++ b/src/main.module.ts @@ -26,7 +26,7 @@ import { LocalFileService } from "./services/localFile.service"; import { NgViewerUseEffect } from "./services/state/ngViewerState.store"; import { ViewerStateUseEffect } from "./services/state/viewerState.store"; import { UIService } from "./services/uiService.service"; -import { DatabrowserModule, OVERRIDE_IAV_DATASET_PREVIEW_DATASET_FN, DataBrowserFeatureStore } from "src/ui/databrowserModule"; +import { DatabrowserModule, OVERRIDE_IAV_DATASET_PREVIEW_DATASET_FN, DataBrowserFeatureStore, GET_KGDS_PREVIEW_INFO_FROM_ID_FILENAME } from "src/ui/databrowserModule"; import { DatabrowserService } from "./ui/databrowserModule/databrowser.service"; import { ViewerStateControllerUseEffect } from "./ui/viewerStateController/viewerState.useEffect"; import { DockedContainerDirective } from "./util/directives/dockedContainer.directive"; @@ -54,7 +54,7 @@ import 'hammerjs' import 'src/res/css/extra_styles.css' import 'src/res/css/version.css' import 'src/theme.scss' -import { DatasetPreviewGlue, datasetPreviewMetaReducer, IDatasetPreviewGlue, GlueEffects, GET_KGDS_PREVIEW_INFO_FROM_ID_FILENAME } from './glue'; +import { DatasetPreviewGlue, datasetPreviewMetaReducer, IDatasetPreviewGlue, GlueEffects } from './glue'; import { viewerStateHelperReducer, viewerStateFleshOutDetail, viewerStateMetaReducers, ViewerStateHelperEffect } from './services/state/viewerState.store.helper'; export function debug(reducer: ActionReducer<any>): ActionReducer<any> { diff --git a/src/res/css/extra_styles.css b/src/res/css/extra_styles.css index 77eb5642a313267cfb3ed3584a255b8956d559b7..d9e557e5055d7b8469eeac8d7c8942224dc1cab6 100644 --- a/src/res/css/extra_styles.css +++ b/src/res/css/extra_styles.css @@ -372,6 +372,11 @@ markdown-dom pre code pointer-events: none; } +.h-2rem +{ + height: 2rem!important; +} + .h-5em { height: 5em!important; @@ -751,3 +756,8 @@ mat-list.sm mat-list-item height:2rem!important; font-size: 0.8rem!important; } + +.color-inherit +{ + color: inherit!important; +} diff --git a/src/res/ext/bigbrain.json b/src/res/ext/bigbrain.json index cd704abd59eb5939c71d7490530ad9f7879e8dd7..4af8e77006c9a108bebfed0e9d74ad39230a0efa 100644 --- a/src/res/ext/bigbrain.json +++ b/src/res/ext/bigbrain.json @@ -659,6 +659,12 @@ "@id": "juelich/iav/atlas/v1.0.0/3", "name": "Cortical Layers Segmentation", "ngId": "cortical layers", + "originDatasets": [ + { + "kgSchema": "minds/core/dataset/v1.0.0", + "kgId": "13e6ec48-4fec-4e9a-9bcf-45036d3c8ef9" + } + ], "properties": { "name": "Cortical Layers Segmentation", "description": "The cerebral isocortex has six cytoarchitectonic layers that vary depending on cortical area and local morphology. This datasets provides a 3D segmentation of all cortical and laminar surfaces in the BigBrain, a high-resolution, 3D histological model of the human brain. The segmentation has been computed automatically based on histological intensities along 3D cortical profiles which were sampled between the pial and white matter throughout the dataset. These cortical profiles were segmented into layers using a convolutional neural network. Training profiles were generated from examples of manually segmented layers on cortical regions from 2D histological sections of the BigBrain. From the segmented intensity profiles, surface meshes and voxel masks of all six cortical layers in the space of the Big Brain have been computed.", diff --git a/src/services/state/dataState/actions.ts b/src/services/state/dataState/actions.ts new file mode 100644 index 0000000000000000000000000000000000000000..22560c293cd4134629a85406686158c8873e63ac --- /dev/null +++ b/src/services/state/dataState/actions.ts @@ -0,0 +1,21 @@ +import { createAction, props } from "@ngrx/store"; + +export const datastateActionToggleFav = createAction( + `[datastate] toggleFav`, + props<{payload: { fullId: string }}>() +) + +export const datastateActionUpdateFavDataset = createAction( + `[datastate] updateFav`, + props<{ favDataEntries: any[] }>() +) + +export const datastateActionUnfavDataset = createAction( + `[datastate] unFav`, + props<{ payload: { fullId: string } }>() +) + +export const datastateActionFavDataset = createAction( + `[datastate] fav`, + props<{ payload: { fullId: string } }>() +) diff --git a/src/services/state/dataStore.store.ts b/src/services/state/dataStore.store.ts index 9acdec3622e4939ee278a8556c7630b2bad628dd..aae960f5f7a6dcb032793dfdb80c1033f6e860d1 100644 --- a/src/services/state/dataStore.store.ts +++ b/src/services/state/dataStore.store.ts @@ -5,6 +5,7 @@ import { Action } from '@ngrx/store' import { GENERAL_ACTION_TYPES } from '../stateStore.service' import { LOCAL_STORAGE_CONST } from 'src/util/constants' +import { datastateActionUpdateFavDataset } from './dataState/actions' /** * TODO merge with databrowser.usereffect.ts @@ -48,13 +49,13 @@ export const getStateStore = ({ state: state = defaultState } = {}) => (prevStat fetchedDataEntries : action.fetchedDataEntries, } } - case FETCHED_SPATIAL_DATA : { + case FETCHED_SPATIAL_DATA: { return { ...prevState, fetchedSpatialData : action.fetchedDataEntries, } } - case ACTION_TYPES.UPDATE_FAV_DATASETS: { + case datastateActionUpdateFavDataset.type: { const { favDataEntries = [] } = action return { ...prevState, @@ -211,10 +212,6 @@ export interface IFileSupplementData { } const ACTION_TYPES = { - FAV_DATASET: `FAV_DATASET`, - UPDATE_FAV_DATASETS: `UPDATE_FAV_DATASETS`, - UNFAV_DATASET: 'UNFAV_DATASET', - TOGGLE_FAV_DATASET: 'TOGGLE_FAV_DATASET', PREVIEW_DATASET: 'PREVIEW_DATASET', CLEAR_PREVIEW_DATASET: 'CLEAR_PREVIEW_DATASET', CLEAR_PREVIEW_DATASETS: 'CLEAR_PREVIEW_DATASETS' diff --git a/src/ui/databrowserModule/constants.ts b/src/ui/databrowserModule/constants.ts index 36163582e6666bd05746f97a7d495a62a35183e9..617d395988ea28e763b1b071c22a492acb37a011 100644 --- a/src/ui/databrowserModule/constants.ts +++ b/src/ui/databrowserModule/constants.ts @@ -1,5 +1,6 @@ import { InjectionToken } from "@angular/core"; import { LOCAL_STORAGE_CONST } from "src/util/constants"; +import { Observable } from "rxjs"; export const OVERRIDE_IAV_DATASET_PREVIEW_DATASET_FN = new InjectionToken<(file: any, dataset: any) => void>('OVERRIDE_IAV_DATASET_PREVIEW_DATASET_FN') export const DATASTORE_DEFAULT_STATE = { @@ -93,3 +94,5 @@ export interface DatasetPreview { datasetId: string filename: string } + +export const GET_KGDS_PREVIEW_INFO_FROM_ID_FILENAME: InjectionToken<({ datasetId, filename }) => Observable<any>> = new InjectionToken('GET_KGDS_PREVIEW_INFO_FROM_ID_FILENAME') diff --git a/src/ui/databrowserModule/databrowser.module.ts b/src/ui/databrowserModule/databrowser.module.ts index 2e06dc0806a3ea8f2f65e69c0c88efbd9debb896..059ea3107f95dced726fca438c90dbec1346051f 100644 --- a/src/ui/databrowserModule/databrowser.module.ts +++ b/src/ui/databrowserModule/databrowser.module.ts @@ -42,6 +42,7 @@ import { ShownPreviewsDirective } from "./preview/shownPreviews.directive"; import { FilterPreviewByType } from "./preview/filterPreview.pipe"; import { PreviewCardComponent } from "./preview/previewCard/previewCard.component"; import { LayerBrowserModule } from "../layerbrowser"; +import { DatabrowserDirective } from "./databrowser/databrowser.directive"; const previewEmitFactory = ( overrideFn: (file: any, dataset: any) => void) => { @@ -75,6 +76,7 @@ const previewEmitFactory = ( overrideFn: (file: any, dataset: any) => void) => { ShowDatasetDialogDirective, PreviewDatasetFile, ShownPreviewsDirective, + DatabrowserDirective, /** * pipes @@ -114,6 +116,7 @@ const previewEmitFactory = ( overrideFn: (file: any, dataset: any) => void) => { ShownPreviewsDirective, FilterPreviewByType, PreviewCardComponent, + DatabrowserDirective, ], entryComponents: [ DataBrowser, diff --git a/src/ui/databrowserModule/databrowser.service.ts b/src/ui/databrowserModule/databrowser.service.ts index 57ca8c1f2bb68852912e718a0404f31e220772cd..3f9c3245c99328afaca798d939e68c67e2850265 100644 --- a/src/ui/databrowserModule/databrowser.service.ts +++ b/src/ui/databrowserModule/databrowser.service.ts @@ -10,13 +10,13 @@ import { AtlasWorkerService } from "src/atlasViewer/atlasViewer.workerService.se import { WidgetUnit } from "src/widget"; import { LoggingService } from "src/logging"; -import { DATASETS_ACTIONS_TYPES } from "src/services/state/dataStore.store"; import { SHOW_KG_TOS } from "src/services/state/uiState.store"; import { FETCHED_DATAENTRIES, FETCHED_SPATIAL_DATA, IavRootStoreInterface, IDataEntry, safeFilter } from "src/services/stateStore.service"; import { regionFlattener } from "src/util/regionFlattener"; import { DataBrowser } from "./databrowser/databrowser.component"; import { NO_METHODS } from "./util/filterDataEntriesByMethods.pipe"; import { FilterDataEntriesByRegion } from "./util/filterDataEntriesByRegion.pipe"; +import { datastateActionToggleFav, datastateActionUnfavDataset, datastateActionFavDataset } from "src/services/state/dataState/actions"; const noMethodDisplayName = 'No methods described' @@ -206,24 +206,33 @@ export class DatabrowserService implements OnDestroy { } public toggleFav(dataentry: Partial<IDataEntry>) { - this.store.dispatch({ - type: DATASETS_ACTIONS_TYPES.TOGGLE_FAV_DATASET, - payload: dataentry, - }) + this.store.dispatch( + datastateActionToggleFav({ + payload: { + fullId: dataentry.fullId || null + } + }) + ) } public saveToFav(dataentry: Partial<IDataEntry>) { - this.store.dispatch({ - type: DATASETS_ACTIONS_TYPES.FAV_DATASET, - payload: dataentry, - }) + this.store.dispatch( + datastateActionFavDataset({ + payload: { + fullId: dataentry?.fullId || null + } + }) + ) } public removeFromFav(dataentry: Partial<IDataEntry>) { - this.store.dispatch({ - type: DATASETS_ACTIONS_TYPES.UNFAV_DATASET, - payload: dataentry, - }) + this.store.dispatch( + datastateActionUnfavDataset({ + payload: { + fullId: dataentry.fullId || null + } + }) + ) } // TODO deprecate diff --git a/src/ui/databrowserModule/databrowser.useEffect.spec.ts b/src/ui/databrowserModule/databrowser.useEffect.spec.ts index 3ff661da4d435d28d5b99481c215b91836f590df..c6e3fda8053396e6196c3e1902349085be437917 100644 --- a/src/ui/databrowserModule/databrowser.useEffect.spec.ts +++ b/src/ui/databrowserModule/databrowser.useEffect.spec.ts @@ -1,62 +1,39 @@ -// TODO reenable test -// when injecting DataBrowserUseEffect, referenceError: defaultState is not defined error was thrown +const defaultState = { + fetchedDataEntries: [], + favDataEntries: [], + fetchedSpatialData: [], +} -// import { DataBrowserUseEffect } from './databrowser.useEffect' -// import { TestBed, async } from '@angular/core/testing' -// import { AngularMaterialModule } from '../sharedModules/angularMaterial.module' -// import { Observable } from 'rxjs' -// import { provideMockActions } from '@ngrx/effects/testing' -// import { provideMockStore } from '@ngrx/store/testing' -// import { defaultRootState } from 'src/services/stateStore.service' -// import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing' -// import { DatabrowserModule } from './databrowser.module' -// import { hot } from 'jasmine-marbles' -// import { KgSingleDatasetService } from './singleDataset/singleDataset.base' +import { DataBrowserUseEffect } from './databrowser.useEffect' +import { TestBed } from '@angular/core/testing' +import { Observable } from 'rxjs' +import { provideMockActions } from '@ngrx/effects/testing' +import { provideMockStore } from '@ngrx/store/testing' +import { hot } from 'jasmine-marbles' -// describe('databrowser.useEffect.spec.ts', () => { - -// it("fails", () => { -// expect(true).toEqual(true) -// }) -// describe('DataBrowserUseEffect', () => { -// let actions$: Observable<any> -// beforeEach(async(() => { -// TestBed.configureTestingModule({ -// providers: [ -// DataBrowserUseEffect, -// provideMockActions(() => actions$), -// provideMockStore({ initialState: defaultRootState }) -// ] -// }).compileComponents() -// })) - -// afterEach(() => { -// const ctrl = TestBed.inject(HttpTestingController) -// ctrl.verify() -// }) - -// describe('storePreviewDatasetFile$', () => { -// it('on init, emit []', () => { -// // const effect = TestBed.inject(DataBrowserUseEffect) -// // expect( -// // (effect as any).storePreviewDatasetFile$ as Observable<any> -// // ).toBeObservable( -// // hot( -// // 'a', -// // { -// // a: [] -// // } -// // ) -// // ) - -// }) -// // it('on datasetPreviews change, kgSingleDatasetService.getInfoFromKg gets called', () => { -// // const store = TestBed.get(Store) -// // const copiedState = JSON.parse(JSON.stringify( defaultRootState )) -// // }) -// }) -// describe('previewRegisteredVolumes$', () => { - -// }) -// }) -// }) \ No newline at end of file +let actions$: Observable<any> +describe('> databrowser.useEffect.ts', () => { + describe('> DataBrowserUseEffect', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + provideMockStore({ + initialState: { + dataStore: defaultState + } + }), + provideMockActions(() => actions$), + DataBrowserUseEffect, + ] + }) + }) + it('> should instantiate properly', () => { + const useEffect = TestBed.inject(DataBrowserUseEffect) + expect(useEffect.favDataEntries$).toBeObservable( + hot('a', { + a: [] + }) + ) + }) + }) +}) \ No newline at end of file diff --git a/src/ui/databrowserModule/databrowser.useEffect.ts b/src/ui/databrowserModule/databrowser.useEffect.ts index b1ea697bd800382d3b718b580400fbdc03c64609..88205d2485845f7d869e8f0809219900d57ad010 100644 --- a/src/ui/databrowserModule/databrowser.useEffect.ts +++ b/src/ui/databrowserModule/databrowser.useEffect.ts @@ -3,9 +3,9 @@ import { Actions, Effect, ofType } from "@ngrx/effects"; import { select, Store } from "@ngrx/store"; import { Observable, Subscription } from "rxjs"; import { filter, map, withLatestFrom } from "rxjs/operators"; -import { DATASETS_ACTIONS_TYPES, IDataEntry } from "src/services/state/dataStore.store"; import { LOCAL_STORAGE_CONST } from "src/util/constants"; import { getKgSchemaIdFromFullId } from "./util/getKgSchemaIdFromFullId.pipe"; +import { datastateActionToggleFav, datastateActionUpdateFavDataset, datastateActionUnfavDataset, datastateActionFavDataset } from "src/services/state/dataState/actions"; @Injectable({ providedIn: 'root', @@ -26,7 +26,7 @@ export class DataBrowserUseEffect { ) this.toggleDataset$ = this.actions$.pipe( - ofType(DATASETS_ACTIONS_TYPES.TOGGLE_FAV_DATASET), + ofType(datastateActionToggleFav.type), withLatestFrom(this.favDataEntries$), map(([action, prevFavDataEntries]) => { const { payload = {} } = action as any @@ -35,27 +35,25 @@ export class DataBrowserUseEffect { const re1 = getKgSchemaIdFromFullId(fullId) if (!re1) { - return { - type: DATASETS_ACTIONS_TYPES.UPDATE_FAV_DATASETS, - favDataEntries: DATASETS_ACTIONS_TYPES.UPDATE_FAV_DATASETS, - } + return datastateActionUpdateFavDataset({ + favDataEntries: prevFavDataEntries + }) } 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, + return datastateActionUpdateFavDataset({ favDataEntries: favIdx >= 0 ? prevFavDataEntries.filter((_, idx) => idx !== favIdx) : prevFavDataEntries.concat(payload), - } + }) }), ) this.unfavDataset$ = this.actions$.pipe( - ofType(DATASETS_ACTIONS_TYPES.UNFAV_DATASET), + ofType(datastateActionUnfavDataset.type), withLatestFrom(this.favDataEntries$), map(([action, prevFavDataEntries]) => { @@ -64,20 +62,19 @@ export class DataBrowserUseEffect { const re1 = getKgSchemaIdFromFullId(fullId) - return { - type: DATASETS_ACTIONS_TYPES.UPDATE_FAV_DATASETS, + return datastateActionUpdateFavDataset({ favDataEntries: prevFavDataEntries.filter(ds => { const re2 = getKgSchemaIdFromFullId(ds.fullId) if (!re2) return false if (!re1) return true return re2[1] !== re1[1] - }), - } + }) + }) }), ) this.favDataset$ = this.actions$.pipe( - ofType(DATASETS_ACTIONS_TYPES.FAV_DATASET), + ofType(datastateActionFavDataset.type), withLatestFrom(this.favDataEntries$), map(([ action, prevFavDataEntries ]) => { const { payload } = action as any @@ -88,10 +85,9 @@ export class DataBrowserUseEffect { const { fullId } = payload const re1 = getKgSchemaIdFromFullId(fullId) if (!re1) { - return { - type: DATASETS_ACTIONS_TYPES.UPDATE_FAV_DATASETS, - favDataEntries: prevFavDataEntries, - } + return datastateActionUpdateFavDataset({ + favDataEntries: prevFavDataEntries + }) } const isDuplicate = prevFavDataEntries.some(favDe => { @@ -103,10 +99,9 @@ export class DataBrowserUseEffect { ? prevFavDataEntries : prevFavDataEntries.concat(payload) - return { - type: DATASETS_ACTIONS_TYPES.UPDATE_FAV_DATASETS, - favDataEntries, - } + return datastateActionUpdateFavDataset({ + favDataEntries + }) }), ) @@ -136,7 +131,7 @@ export class DataBrowserUseEffect { private savedFav$: Observable<Array<{id: string, name: string}> | null> - private favDataEntries$: Observable<Partial<IDataEntry>[]> + public favDataEntries$: Observable<Partial<any>[]> @Effect() public favDataset$: Observable<any> diff --git a/src/ui/databrowserModule/databrowser/databrowser.base.ts b/src/ui/databrowserModule/databrowser/databrowser.base.ts new file mode 100644 index 0000000000000000000000000000000000000000..ce800d094acbb676deaddd5f55fa2eda71f36366 --- /dev/null +++ b/src/ui/databrowserModule/databrowser/databrowser.base.ts @@ -0,0 +1,92 @@ +import { Input, Output, EventEmitter } from "@angular/core" +import { LoggingService } from "src/logging" +import { DatabrowserService } from "../singleDataset/singleDataset.base" +import { Observable } from "rxjs" +import { IDataEntry } from "src/services/stateStore.service" + +export class DatabrowserBase{ + + @Output() + public dataentriesUpdated: EventEmitter<IDataEntry[]> = new EventEmitter() + + @Input() + regions: any[] = [] + + @Input() + public template: any + + @Input() + public parcellation: any + + public fetchError: boolean = false + public fetchingFlag = false + + public favDataentries$: Observable<Partial<IDataEntry>[]> + + public dataentries: IDataEntry[] = [] + + constructor( + private dbService: DatabrowserService, + private log: LoggingService, + ){ + + this.favDataentries$ = this.dbService.favedDataentries$ + } + + ngOnChanges(){ + + + this.regions = this.regions.map(r => { + /** + * TODO to be replaced with properly region UUIDs from KG + */ + return { + id: `${this.parcellation?.name || 'untitled_parcellation'}/${r.name}`, + ...r, + } + }) + const { regions, parcellation, template } = this + this.fetchingFlag = true + + // input may be undefined/null + if (!parcellation) { return } + + /** + * reconstructing parcellation region is async (done on worker thread) + * if parcellation region is not yet defined, return. + * parccellation will eventually be updated with the correct region + */ + if (!parcellation.regions) { return } + + this.dbService.getDataByRegion({ regions, parcellation, template }) + .then(de => { + this.dataentries = de + return de + }) + .catch(e => { + this.log.error(e) + this.fetchError = true + }) + .finally(() => { + this.fetchingFlag = false + this.dataentriesUpdated.emit(this.dataentries) + }) + } + + public retryFetchData(event: MouseEvent) { + event.preventDefault() + this.dbService.manualFetchDataset$.next(null) + } + + public toggleFavourite(dataset: IDataEntry) { + this.dbService.toggleFav(dataset) + } + + public saveToFavourite(dataset: IDataEntry) { + this.dbService.saveToFav(dataset) + } + + public removeFromFavourite(dataset: IDataEntry) { + this.dbService.removeFromFav(dataset) + } +} diff --git a/src/ui/databrowserModule/databrowser/databrowser.component.ts b/src/ui/databrowserModule/databrowser/databrowser.component.ts index 7a257e749fcb84cc625161b6bcbf0267864981a5..4ed2d667cd80fe7a4922171e902e0c69d862cbb6 100644 --- a/src/ui/databrowserModule/databrowser/databrowser.component.ts +++ b/src/ui/databrowserModule/databrowser/databrowser.component.ts @@ -5,6 +5,8 @@ import { IDataEntry } from "src/services/state/dataStore.store"; import { CountedDataModality, DatabrowserService } from "../databrowser.service"; import { ModalityPicker } from "../modalityPicker/modalityPicker.component"; import { ARIA_LABELS } from 'common/constants.js' +import { DatabrowserBase } from "./databrowser.base"; +import { debounceTime } from "rxjs/operators"; const { MODALITY_FILTER, LIST_OF_DATASETS } = ARIA_LABELS @@ -18,7 +20,7 @@ const { MODALITY_FILTER, LIST_OF_DATASETS } = ARIA_LABELS changeDetection: ChangeDetectionStrategy.OnPush, }) -export class DataBrowser implements OnChanges, OnDestroy, OnInit { +export class DataBrowser extends DatabrowserBase implements OnChanges, OnDestroy, OnInit { @Input() disableVirtualScroll: boolean = false @@ -28,22 +30,8 @@ export class DataBrowser implements OnChanges, OnDestroy, OnInit { public MODALITY_FILTER_ARIA_LABEL = MODALITY_FILTER public LIST_OF_DATASETS_ARIA_LABEL = LIST_OF_DATASETS - @Input() - public regions: any[] = [] - - @Input() - public template: any - - @Input() - public parcellation: any - @Output() - public dataentriesUpdated: EventEmitter<IDataEntry[]> = new EventEmitter() - public dataentries: IDataEntry[] = [] - - public fetchingFlag: boolean = false - public fetchError: boolean = false /** * TODO filter types */ @@ -54,7 +42,6 @@ export class DataBrowser implements OnChanges, OnDestroy, OnInit { @ViewChild(ModalityPicker) public modalityPicker: ModalityPicker - public favDataentries$: Observable<Partial<IDataEntry>[]> /** * TODO @@ -65,67 +52,36 @@ export class DataBrowser implements OnChanges, OnDestroy, OnInit { public gemoetryFilter: any constructor( - private dbService: DatabrowserService, + private dataService: DatabrowserService, private cdr: ChangeDetectorRef, - private log: LoggingService, + log: LoggingService, ) { - this.favDataentries$ = this.dbService.favedDataentries$ + super(dataService, log) } public ngOnChanges() { + super.ngOnChanges() + } - this.regions = this.regions.map(r => { - /** - * TODO to be replaced with properly region UUIDs from KG - */ - return { - id: `${this.parcellation?.name || 'untitled_parcellation'}/${r.name}`, - ...r, - } - }) - const { regions, parcellation, template } = this - this.fetchingFlag = true - - // input may be undefined/null - if (!parcellation) { return } - - /** - * reconstructing parcellation region is async (done on worker thread) - * if parcellation region is not yet defined, return. - * parccellation will eventually be updated with the correct region - */ - if (!parcellation.regions) { return } + public ngOnInit() { - this.dbService.getDataByRegion({ regions, parcellation, template }) - .then(de => { - this.dataentries = de - return de - }) - .then(this.dbService.getModalityFromDE) - .then(modalities => { - this.countedDataM = modalities - }) - .catch(e => { - this.log.error(e) - this.fetchError = true - }) - .finally(() => { - this.fetchingFlag = false - this.dataentriesUpdated.emit(this.dataentries) + this.subscriptions.push( + this.dataentriesUpdated.pipe( + debounceTime(60) + ).subscribe(() => { + this.countedDataM = this.dataService.getModalityFromDE(this.dataentries) this.cdr.markForCheck() }) - - } - - public ngOnInit() { + ) + /** * TODO gets init'ed everytime when appends to ngtemplateoutlet */ - this.dbService.dbComponentInit(this) + this.dataService.dbComponentInit(this) this.subscriptions.push( merge( - // this.dbService.selectedRegions$, - this.dbService.fetchDataObservable$, + // this.dataService.selectedRegions$, + this.dataService.fetchDataObservable$, ).subscribe(() => { /** * Only reset modality picker @@ -163,23 +119,6 @@ export class DataBrowser implements OnChanges, OnDestroy, OnInit { this.cdr.markForCheck() } - public retryFetchData(event: MouseEvent) { - event.preventDefault() - this.dbService.manualFetchDataset$.next(null) - } - - public toggleFavourite(dataset: IDataEntry) { - this.dbService.toggleFav(dataset) - } - - public saveToFavourite(dataset: IDataEntry) { - this.dbService.saveToFav(dataset) - } - - public removeFromFavourite(dataset: IDataEntry) { - this.dbService.removeFromFav(dataset) - } - public showParcellationList: boolean = false public filePreviewName: string diff --git a/src/ui/databrowserModule/databrowser/databrowser.directive.ts b/src/ui/databrowserModule/databrowser/databrowser.directive.ts new file mode 100644 index 0000000000000000000000000000000000000000..5eeeacf5f26907fc47b3d9eb6d39ec160d7c72a4 --- /dev/null +++ b/src/ui/databrowserModule/databrowser/databrowser.directive.ts @@ -0,0 +1,18 @@ +import { Directive } from "@angular/core"; +import { DatabrowserBase } from "./databrowser.base"; +import { DatabrowserService } from "../singleDataset/singleDataset.base"; +import { LoggingService } from "src/logging"; + +@Directive({ + selector: '[iav-databrowser-directive]', + exportAs: 'iavDatabrowserDirective' +}) + +export class DatabrowserDirective extends DatabrowserBase{ + constructor( + dataService: DatabrowserService, + log: LoggingService, + ){ + super(dataService, log) + } +} diff --git a/src/ui/databrowserModule/databrowser/databrowser.template.html b/src/ui/databrowserModule/databrowser/databrowser.template.html index f6982a35af3b68fe4de74affdcae1afe20572a7e..27e4efbc014223045688118abe5981214a85cae8 100644 --- a/src/ui/databrowserModule/databrowser/databrowser.template.html +++ b/src/ui/databrowserModule/databrowser/databrowser.template.html @@ -99,7 +99,7 @@ <mat-card-content class="w-100"> <!-- TODO export aria labels to common/constants --> <div *ngIf="showList"> - <div *ngFor="let dataset of filteredDataEntry; trackBy: trackByFn; templateCacheSize: 20; let index = index" + <div *ngFor="let dataset of filteredDataEntry; trackBy: trackByFn; let index = index" class="scroll-element overflow-hidden"> <mat-divider *ngIf="index !== 0"></mat-divider> diff --git a/src/ui/databrowserModule/index.ts b/src/ui/databrowserModule/index.ts index 72cea1ddf7bbb1dd4145f0a60386f414e5b4fc25..a82fb38598b0d164ae5a105e0b99c1ce115218b9 100644 --- a/src/ui/databrowserModule/index.ts +++ b/src/ui/databrowserModule/index.ts @@ -20,4 +20,5 @@ export { PreviewComponentWrapper, getKgSchemaIdFromFullId, PreviewDatasetFile, + GET_KGDS_PREVIEW_INFO_FROM_ID_FILENAME, } from './pure' diff --git a/src/ui/databrowserModule/preview/previewCard/previewCard.component.ts b/src/ui/databrowserModule/preview/previewCard/previewCard.component.ts index 91f44051cbf1f4ac36b2bfaeebfe30a1b5e50ae5..ffba858ee0dc3d617bae212869ad8f33a0742924 100644 --- a/src/ui/databrowserModule/preview/previewCard/previewCard.component.ts +++ b/src/ui/databrowserModule/preview/previewCard/previewCard.component.ts @@ -1,6 +1,6 @@ import { Component, Optional, Inject } from "@angular/core"; import { PreviewBase } from "../preview.base"; -import { GET_KGDS_PREVIEW_INFO_FROM_ID_FILENAME } from "src/glue"; +import { GET_KGDS_PREVIEW_INFO_FROM_ID_FILENAME } from "../../pure"; @Component({ selector: 'preview-card', diff --git a/src/ui/databrowserModule/preview/previewCard/previewCard.template.html b/src/ui/databrowserModule/preview/previewCard/previewCard.template.html index 82a521b2cef663f118915b321d46acbc26483981..51c3b7a79e00f0dbe0dd127b9d36b842ca19d1d1 100644 --- a/src/ui/databrowserModule/preview/previewCard/previewCard.template.html +++ b/src/ui/databrowserModule/preview/previewCard/previewCard.template.html @@ -4,11 +4,41 @@ {{ singleDsView?.name || file.name || filename }} </mat-card-title> - <mat-card-subtitle> + <mat-card-subtitle class="d-inline-flex align-items-center"> <mat-icon fontSet="fas" fontIcon="fa-database"></mat-icon> <span> Dataset preview </span> + + <mat-divider [vertical]="true" class="ml-2 h-2rem"></mat-divider> + + <!-- explore btn --> + <a *ngFor="let kgRef of singleDsView.kgReference" + [href]="kgRef | doiParserPipe" + class="color-inherit" + target="_blank"> + <button mat-icon-button + [matTooltip]="singleDsView.EXPLORE_DATASET_IN_KG_ARIA_LABEL"> + <i class="fas fa-external-link-alt"></i> + </button> + </a> + + <!-- pin dataset btn --> + <ng-container *ngTemplateOutlet="favDatasetBtn; context: { singleDataset: singleDsView }"> + </ng-container> + + <!-- download zip btn --> + <a *ngIf="singleDsView.files && singleDsView.files.length > 0" + [href]="singleDsView.dlFromKgHref" + class="color-inherit" + target="_blank"> + <button mat-icon-button + [matTooltip]="singleDsView.tooltipText" + [disabled]="singleDsView.downloadInProgress"> + <i class="ml-1 fas" [ngClass]="!singleDsView.downloadInProgress? 'fa-download' :'fa-spinner fa-pulse'"></i> + </button> + </a> + </mat-card-subtitle> </div> </mat-card> @@ -17,11 +47,32 @@ <single-dataset-view [fullId]="datasetId" [hideTitle]="true" [hidePreview]="true" + [hideExplore]="true" + [hidePinBtn]="true" + [hideDownloadBtn]="true" #singleDsView="singleDatasetView"> </single-dataset-view> </mat-card> +<!-- TODO --> +<!-- this is not exactly right --> <div class="mt-4"> <layer-browser></layer-browser> </div> + +<!-- templates --> +<ng-template #favDatasetBtn let-singleDataset="singleDataset"> + <ng-container *ngTemplateOutlet="isFavCtxTmpl; context: { isFav: (singleDataset.favedDataentries$ | async | datasetIsFaved : singleDataset.dataset) }"> + </ng-container> + + <ng-template #isFavCtxTmpl let-isFav="isFav"> + <button mat-icon-button + (click)="isFav ? singleDataset.undoableRemoveFav() : singleDataset.undoableAddFav()" + [attr.aria-label]="singleDataset.PIN_DATASET_ARIA_LABEL" + [matTooltip]="singleDataset.PIN_DATASET_ARIA_LABEL" + [color]="isFav ? 'primary' : 'basic'"> + <i class="fas fa-thumbtack"></i> + </button> + </ng-template> +</ng-template> diff --git a/src/ui/databrowserModule/preview/previewDatasetFile.directive.ts b/src/ui/databrowserModule/preview/previewDatasetFile.directive.ts index 7aecf85f50567e00549b936e37e489294ab6bd81..f3f649ddfa66216a57173cfdeb0ffa32bbf3f91c 100644 --- a/src/ui/databrowserModule/preview/previewDatasetFile.directive.ts +++ b/src/ui/databrowserModule/preview/previewDatasetFile.directive.ts @@ -4,7 +4,7 @@ import { ViewerPreviewFile, IDataEntry } from 'src/services/state/dataStore.stor import { Observable, Subscription } from "rxjs"; import { distinctUntilChanged } from "rxjs/operators"; import { PreviewBase } from "./preview.base"; -import { GET_KGDS_PREVIEW_INFO_FROM_ID_FILENAME } from "src/glue"; +import { GET_KGDS_PREVIEW_INFO_FROM_ID_FILENAME } from "../pure"; export const IAV_DATASET_PREVIEW_DATASET_FN = 'IAV_DATASET_PREVIEW_DATASET_FN' diff --git a/src/ui/databrowserModule/preview/shownPreviews.directive.ts b/src/ui/databrowserModule/preview/shownPreviews.directive.ts index 8307bcd5eac4ade95087d2e250b38ecf58fb9479..bdebaf94796d878e4cfa2974b7b2820f3613a7f9 100644 --- a/src/ui/databrowserModule/preview/shownPreviews.directive.ts +++ b/src/ui/databrowserModule/preview/shownPreviews.directive.ts @@ -4,7 +4,7 @@ import { uiStatePreviewingDatasetFilesSelector } from "src/services/state/uiStat import { EnumPreviewFileTypes } from "../pure"; import { switchMap, map, startWith } from "rxjs/operators"; import { forkJoin, of, Subscription } from "rxjs"; -import { GET_KGDS_PREVIEW_INFO_FROM_ID_FILENAME } from "src/glue"; +import { GET_KGDS_PREVIEW_INFO_FROM_ID_FILENAME } from "../pure"; @Directive({ selector: '[iav-shown-previews]', diff --git a/src/ui/databrowserModule/pure.ts b/src/ui/databrowserModule/pure.ts index e9bfb417c7f3dff8b2913f5bda208621623bdda9..9fdf87408120e554508fc1e8e12e2d2f45af79b1 100644 --- a/src/ui/databrowserModule/pure.ts +++ b/src/ui/databrowserModule/pure.ts @@ -10,7 +10,8 @@ export { IKgParcellationRegion, IKgPublication, IKgReferenceSpace, - DatasetPreview + DatasetPreview, + GET_KGDS_PREVIEW_INFO_FROM_ID_FILENAME, } from './constants' export { PreviewFileTypePipe } from './preview/previewFileType.pipe' diff --git a/src/ui/databrowserModule/singleDataset/detailedView/singleDataset.component.ts b/src/ui/databrowserModule/singleDataset/detailedView/singleDataset.component.ts index 338eca13297d2f673ca9935a6b46e0584c945ddc..2d34d3971dfb4205ec513ef4f64757f0a986fa04 100644 --- a/src/ui/databrowserModule/singleDataset/detailedView/singleDataset.component.ts +++ b/src/ui/databrowserModule/singleDataset/detailedView/singleDataset.component.ts @@ -33,4 +33,13 @@ export class SingleDatasetView extends SingleDatasetBase { @Input() hidePreview = false + + @Input() + hideExplore = false + + @Input() + hidePinBtn = false + + @Input() + hideDownloadBtn = false } diff --git a/src/ui/databrowserModule/singleDataset/detailedView/singleDataset.template.html b/src/ui/databrowserModule/singleDataset/detailedView/singleDataset.template.html index 5113d07d490cc84c02f756b4aeb60e3e83e65157..cf98554f9ee83564167e026962b09c6b756674c7 100644 --- a/src/ui/databrowserModule/singleDataset/detailedView/singleDataset.template.html +++ b/src/ui/databrowserModule/singleDataset/detailedView/singleDataset.template.html @@ -70,7 +70,7 @@ <ng-template #actionBtns let-mediaBreakPoint> <!-- explore --> - <ng-container *ngIf="!strictLocal"> + <ng-container *ngIf="!strictLocal && !hideExplore"> <a *ngFor="let kgRef of kgReference" [href]="kgRef | doiParserPipe" @@ -95,6 +95,7 @@ <ng-template #favDatasetBtn let-isFav> <iav-dynamic-mat-button + *ngIf="!hidePinBtn" (click)="isFav ? undoableRemoveFav() : undoableAddFav()" iav-stop="click mousedown" [iav-dynamic-mat-button-aria-label]="PIN_DATASET_ARIA_LABEL" @@ -110,7 +111,7 @@ </ng-container> <!-- download --> - <ng-container *ngIf="!strictLocal"> + <ng-container *ngIf="!strictLocal && !hideDownloadBtn"> <a *ngIf="files && files.length > 0" [href]="dlFromKgHref" diff --git a/src/ui/databrowserModule/singleDataset/singleDataset.base.ts b/src/ui/databrowserModule/singleDataset/singleDataset.base.ts index 12d3bfd05b27f8a465a17e861273c05050ee38da..2847d14289ea3b636297d17662ab91be10f4973a 100644 --- a/src/ui/databrowserModule/singleDataset/singleDataset.base.ts +++ b/src/ui/databrowserModule/singleDataset/singleDataset.base.ts @@ -22,6 +22,7 @@ export class SingleDatasetBase implements OnChanges, OnDestroy { public SHOW_DATASET_PREVIEW_ARIA_LABEL = ARIA_LABELS.SHOW_DATASET_PREVIEW public PIN_DATASET_ARIA_LABEL = ARIA_LABELS.PIN_DATASET + public EXPLORE_DATASET_IN_KG_ARIA_LABEL = ARIA_LABELS.EXPLORE_DATASET_IN_KG @Input() public ripple: boolean = false diff --git a/src/ui/databrowserModule/store.module.ts b/src/ui/databrowserModule/store.module.ts index eab27f3e99edb2b1b2ca8f1d0800b04bc48c7125..c8e0cf510b5c83aafa5b94d96c3d78dc7087753c 100644 --- a/src/ui/databrowserModule/store.module.ts +++ b/src/ui/databrowserModule/store.module.ts @@ -1,5 +1,5 @@ import { NgModule } from "@angular/core"; -import { stateStore } from "src/services/state/uiState.store"; +import { stateStore } from "src/services/state/dataStore.store"; import { DataBrowserUseEffect } from "./databrowser.useEffect"; import { StoreModule } from "@ngrx/store"; import { EffectsModule } from "@ngrx/effects"; diff --git a/src/ui/layerbrowser/layerBrowserComponent/layerbrowser.component.ts b/src/ui/layerbrowser/layerBrowserComponent/layerbrowser.component.ts index 3225c8935ef9a4850ad1c0f79b61ba3b3a91c603..8c70e75bd16c65c68c325e81459304b5d3408416 100644 --- a/src/ui/layerbrowser/layerBrowserComponent/layerbrowser.component.ts +++ b/src/ui/layerbrowser/layerBrowserComponent/layerbrowser.component.ts @@ -56,7 +56,6 @@ export class LayerBrowser implements OnInit, OnDestroy { private constantsService: PureContantService, private log: LoggingService, ) { - console.log('init') this.ngLayers$ = store.pipe( select('viewerState'), select('templateSelected'), diff --git a/src/ui/nehubaContainer/nehubaContainer.component.ts b/src/ui/nehubaContainer/nehubaContainer.component.ts index 54b01efda3f2d1ce8d3d4b97bce4a2d1b20910e2..64ab6f2c942dcd68be5d1a8549e1978e803582e4 100644 --- a/src/ui/nehubaContainer/nehubaContainer.component.ts +++ b/src/ui/nehubaContainer/nehubaContainer.component.ts @@ -132,7 +132,6 @@ const { export class NehubaContainer implements OnInit, OnChanges, OnDestroy { - public _tmp: number = 0 public ARIA_LABEL_ZOOM_IN = ZOOM_IN public ARIA_LABEL_ZOOM_OUT = ZOOM_OUT public ARIA_LABEL_TOGGLE_SIDE_PANEL = TOGGLE_SIDE_PANEL diff --git a/src/ui/nehubaContainer/nehubaContainer.template.html b/src/ui/nehubaContainer/nehubaContainer.template.html index 6e46e1ee9bfeb3f841b9e1233c704cd850b8d540..7f2195f4a9982d5f4f9ca991860b763a4c15db2e 100644 --- a/src/ui/nehubaContainer/nehubaContainer.template.html +++ b/src/ui/nehubaContainer/nehubaContainer.template.html @@ -114,7 +114,6 @@ (openedChange)="$event && sideNavSwitch.open()" [disableClose]="true" [@openClose]="sideNavMasterSwitch.switchState && sideNavSwitch.switchState ? 'open' : 'closed'" - (@openClose.start)="_tmp=1" (@openClose.done)="$event.toState === 'closed' && matDrawerMinor.close()"> <div class="position-relative d-flex flex-column h-100"> @@ -134,7 +133,7 @@ </div> <ng-container *ngIf="previews.iavAdditionalLayers$ | async | filterPreviewByType : [previews.FILETYPES.VOLUMES] as volumePreviews"> - <ng-template [ngIf]="volumePreviews.length > 0" [ngIfElse]="sidenavTmpl"> + <ng-template [ngIf]="volumePreviews.length > 0" [ngIfElse]="sidenavRegionTmpl"> <ng-container *ngFor="let vPreview of volumePreviews"> <ng-container *ngTemplateOutlet="sidenavDsPreviewTmpl; context: vPreview"> @@ -167,7 +166,7 @@ <button mat-raised-button class="mat-elevation-z8" [attr.aria-label]="ARIA_LABEL_COLLAPSE" (click)="sideNavSwitch.close()" - color="accent"> + color="basic"> <i class="fas fa-chevron-up"></i> <span> collapse @@ -177,7 +176,7 @@ </ng-template> <!-- region sidenav tmpl --> -<ng-template #sidenavTmpl> +<ng-template #sidenavRegionTmpl> <div class="flex-shrink-1 flex-grow-1 d-flex flex-column" [ngClass]="{'region-populated': (selectedRegions$ | async).length > 0 }"> @@ -358,9 +357,16 @@ </data-browser> </ng-template> + <div class="hidden" iav-databrowser-directive + [template]="templateSelected$ | async" + [parcellation]="selectedParcellation" + [regions]="[region]" + #iavDbDirective="iavDatabrowserDirective"> + </div> + <ng-container *ngTemplateOutlet="ngMatAccordionTmpl; context: { title: 'Regional features', - desc: 123, + desc: iavDbDirective?.dataentries?.length, iconClass: 'fas fa-database', iavNgIf: true, content: regionalFeaturesTmpl diff --git a/src/ui/parcellationRegion/region.base.ts b/src/ui/parcellationRegion/region.base.ts index 6f4eac0d406e244bb75342346d3a0ba272fb3d41..17b3833ca2e0e4e990a18be9a26e1080e74e20c4 100644 --- a/src/ui/parcellationRegion/region.base.ts +++ b/src/ui/parcellationRegion/region.base.ts @@ -12,9 +12,6 @@ export class RegionBase { public rgbString: string public rgbDarkmode: boolean - @Input() - showRegionInOtherTmpl: boolean = true - private _region: any @Input() @@ -121,6 +118,7 @@ export class RegionBase { }) } + public GO_TO_REGION_CENTROID = ARIA_LABELS.GO_TO_REGION_CENTROID public SHOW_CONNECTIVITY_DATA = ARIA_LABELS.SHOW_CONNECTIVITY_DATA public SHOW_IN_OTHER_REF_SPACE = ARIA_LABELS.SHOW_IN_OTHER_REF_SPACE public SHOW_ORIGIN_DATASET = ARIA_LABELS.SHOW_ORIGIN_DATASET diff --git a/src/ui/parcellationRegion/regionMenu/regionMenu.component.spec.ts b/src/ui/parcellationRegion/regionMenu/regionMenu.component.spec.ts index 9567a7337b3f5f64ed69138e941eb5e0d094ee5a..3cf92bedded94d7222bbdf48813fb34f4d14d9ef 100644 --- a/src/ui/parcellationRegion/regionMenu/regionMenu.component.spec.ts +++ b/src/ui/parcellationRegion/regionMenu/regionMenu.component.spec.ts @@ -3,11 +3,12 @@ import { RegionMenuComponent } from "./regionMenu.component" import { AngularMaterialModule } from "src/ui/sharedModules/angularMaterial.module" import { UtilModule } from "src/util/util.module" import { CommonModule } from "@angular/common" -import { PreviewDatasetFile } from "src/ui/databrowserModule/pure" import { provideMockStore, MockStore } from "@ngrx/store/testing" import { regionInOtherTemplateSelector, RenderViewOriginDatasetLabelPipe } from '../region.base' import { ARIA_LABELS } from 'common/constants' import { By } from "@angular/platform-browser" +import { Directive, Input } from "@angular/core" +import { NoopAnimationsModule } from "@angular/platform-browser/animations" const mt0 = { name: 'mt0' @@ -55,6 +56,31 @@ const hemisphereMrms = [ { const nohemisphereHrms = [mrm0, mrm1] +@Directive({ + selector: '[iav-dataset-preview-dataset-file]', + exportAs: 'iavDatasetPreviewDatasetFile' +}) +class MockPrvDsFileDirective { + @Input('iav-dataset-preview-dataset-file') + file + + @Input('iav-dataset-preview-dataset-file-filename') + filefilename + + @Input('iav-dataset-preview-dataset-file-dataset') + filedataset + + @Input('iav-dataset-preview-dataset-file-kgid') + filekgid + + @Input('iav-dataset-preview-dataset-file-kgschema') + filekgschema + + @Input('iav-dataset-preview-dataset-file-fullid') + filefullid + +} + describe('> regionMenu.component.ts', () => { describe('> RegionMenuComponent', () => { beforeEach(async(() => { @@ -64,6 +90,7 @@ describe('> regionMenu.component.ts', () => { UtilModule, AngularMaterialModule, CommonModule, + NoopAnimationsModule, ], declarations: [ RegionMenuComponent, @@ -71,7 +98,7 @@ describe('> regionMenu.component.ts', () => { /** * Used by regionMenu.template.html to show region preview */ - PreviewDatasetFile, + MockPrvDsFileDirective, ], providers: [ provideMockStore({ initialState: {} }) @@ -87,7 +114,7 @@ describe('> regionMenu.component.ts', () => { describe('> regionInOtherTemplatesTmpl', () => { - it('> toggleBtn does not exists if selector returns empty array', () => { + it('> if selector returns empty array, data-available-in-tmpl-count == 0', () => { const mockStore = TestBed.inject(MockStore) mockStore.overrideSelector( @@ -99,11 +126,11 @@ describe('> regionMenu.component.ts', () => { fixture.componentInstance.region = mr1 fixture.detectChanges() - const toggleBtn = fixture.debugElement.query( By.css(`[aria-label="${ARIA_LABELS.SHOW_IN_OTHER_REF_SPACE}"]`) ) - expect(toggleBtn).toBeFalsy() + const toggleBtn = fixture.debugElement.query( By.css(`[aria-label="${ARIA_LABELS.AVAILABILITY_IN_OTHER_REF_SPACE}"]`) ) + expect(toggleBtn.attributes['data-available-in-tmpl-count']).toEqual('0') }) - it('> toggleBtn exists if selector returns non empty array', () => { + it('> if selector returns non empty array, data-available-in-tmpl-count == array.length', () => { const mockStore = TestBed.inject(MockStore) mockStore.overrideSelector( @@ -115,8 +142,8 @@ describe('> regionMenu.component.ts', () => { fixture.componentInstance.region = mr1 fixture.detectChanges() - const toggleBtn = fixture.debugElement.query( By.css(`[aria-label="${ARIA_LABELS.SHOW_IN_OTHER_REF_SPACE}"]`) ) - expect(toggleBtn).toBeTruthy() + const toggleBtn = fixture.debugElement.query( By.css(`[aria-label="${ARIA_LABELS.AVAILABILITY_IN_OTHER_REF_SPACE}"]`) ) + expect(toggleBtn.attributes['data-available-in-tmpl-count']).toEqual('2') }) it('> if showRegionInOtherTmpl is set to false, toggle btn will not be shown', () => { @@ -148,7 +175,7 @@ describe('> regionMenu.component.ts', () => { fixture.componentInstance.region = mr1 fixture.detectChanges() - const listContainer = fixture.debugElement.query( By.css(`[aria-label="${ARIA_LABELS.AVAILABILITY_IN_OTHER_REF_SPACE}"]`) ) + const listContainer = fixture.debugElement.query( By.css(`[aria-label="${ARIA_LABELS.SHOW_IN_OTHER_REF_SPACE}"]`) ) expect(listContainer).toBeFalsy() }) @@ -163,10 +190,10 @@ describe('> regionMenu.component.ts', () => { const fixture = TestBed.createComponent(RegionMenuComponent) fixture.componentInstance.region = mr1 fixture.detectChanges() - const toggleBtn = fixture.debugElement.query( By.css(`[aria-label="${ARIA_LABELS.SHOW_IN_OTHER_REF_SPACE}"]`) ) + const toggleBtn = fixture.debugElement.query( By.css(`[aria-label="${ARIA_LABELS.AVAILABILITY_IN_OTHER_REF_SPACE}"]`) ) toggleBtn.triggerEventHandler('click', null) fixture.detectChanges() - const listContainer = fixture.debugElement.query( By.css(`[aria-label="${ARIA_LABELS.AVAILABILITY_IN_OTHER_REF_SPACE}"]`) ) + const listContainer = fixture.debugElement.query( By.css(`[aria-label="${ARIA_LABELS.SHOW_IN_OTHER_REF_SPACE}"]`) ) expect(listContainer).toBeTruthy() }) @@ -181,11 +208,11 @@ describe('> regionMenu.component.ts', () => { const fixture = TestBed.createComponent(RegionMenuComponent) fixture.componentInstance.region = mr1 fixture.detectChanges() - const toggleBtn = fixture.debugElement.query( By.css(`[aria-label="${ARIA_LABELS.SHOW_IN_OTHER_REF_SPACE}"]`) ) + const toggleBtn = fixture.debugElement.query( By.css(`[aria-label="${ARIA_LABELS.AVAILABILITY_IN_OTHER_REF_SPACE}"]`) ) toggleBtn.triggerEventHandler('click', null) fixture.detectChanges() - const listContainer = fixture.debugElement.query( By.css(`[aria-label="${ARIA_LABELS.AVAILABILITY_IN_OTHER_REF_SPACE}"]`) ) - expect(listContainer.nativeElement.children.length).toEqual(2) + const listContainer = fixture.debugElement.queryAll( By.css(`[aria-label="${ARIA_LABELS.SHOW_IN_OTHER_REF_SPACE}"] [role="button"]`) ) + expect(listContainer.length).toEqual(2) }) it('> the text (no hemisphere metadata) on the list is as expected', () => { @@ -199,13 +226,14 @@ describe('> regionMenu.component.ts', () => { const fixture = TestBed.createComponent(RegionMenuComponent) fixture.componentInstance.region = mr1 fixture.detectChanges() - const toggleBtn = fixture.debugElement.query( By.css(`[aria-label="${ARIA_LABELS.SHOW_IN_OTHER_REF_SPACE}"]`) ) + const toggleBtn = fixture.debugElement.query( By.css(`[aria-label="${ARIA_LABELS.AVAILABILITY_IN_OTHER_REF_SPACE}"]`) ) toggleBtn.triggerEventHandler('click', null) fixture.detectChanges() - const listContainer = fixture.debugElement.query( By.css(`[aria-label="${ARIA_LABELS.AVAILABILITY_IN_OTHER_REF_SPACE}"]`) ) + const listContainer = fixture.debugElement.queryAll( By.css(`[aria-label="${ARIA_LABELS.SHOW_IN_OTHER_REF_SPACE}"] [role="button"]`) ) // trim white spaces before and after - const texts = Array.from(listContainer.nativeElement.children).map((c: HTMLElement) => c.textContent.replace(/^\s+/, '').replace(/\s+$/, '')) + + const texts = listContainer.map(c => c.nativeElement.textContent.replace(/^\s+/, '').replace(/\s+$/, '')) expect(texts).toContain(mt0.name) expect(texts).toContain(mt1.name) }) @@ -221,13 +249,13 @@ describe('> regionMenu.component.ts', () => { const fixture = TestBed.createComponent(RegionMenuComponent) fixture.componentInstance.region = mr1 fixture.detectChanges() - const toggleBtn = fixture.debugElement.query( By.css(`[aria-label="${ARIA_LABELS.SHOW_IN_OTHER_REF_SPACE}"]`) ) + const toggleBtn = fixture.debugElement.query( By.css(`[aria-label="${ARIA_LABELS.AVAILABILITY_IN_OTHER_REF_SPACE}"]`) ) toggleBtn.triggerEventHandler('click', null) fixture.detectChanges() - const listContainer = fixture.debugElement.query( By.css(`[aria-label="${ARIA_LABELS.AVAILABILITY_IN_OTHER_REF_SPACE}"]`) ) + const listContainer = fixture.debugElement.queryAll( By.css(`[aria-label="${ARIA_LABELS.SHOW_IN_OTHER_REF_SPACE}"] [role="button"]`) ) // trim white spaces before and after, and middle white spaces into a single white space - const texts = Array.from(listContainer.nativeElement.children).map((c: HTMLElement) => c.textContent.replace(/^\s+/, '').replace(/\s+$/, '').replace(/\s+/g, ' ')) + const texts = listContainer.map(c => c.nativeElement.textContent.replace(/^\s+/, '').replace(/\s+$/, '').replace(/\s+/g, ' ')) expect(texts).toContain(`${mt0.name} (left hemisphere)`) expect(texts).toContain(`${mt1.name} (left hemisphere)`) }) diff --git a/src/ui/parcellationRegion/regionMenu/regionMenu.component.ts b/src/ui/parcellationRegion/regionMenu/regionMenu.component.ts index 8d121014612f34788c6b252540ff557dedb5f3c3..8d9f0854522ede479b6ab101d2e535d1504932fa 100644 --- a/src/ui/parcellationRegion/regionMenu/regionMenu.component.ts +++ b/src/ui/parcellationRegion/regionMenu/regionMenu.component.ts @@ -1,7 +1,8 @@ -import { Component, OnDestroy } from "@angular/core"; +import { Component, OnDestroy, Input } from "@angular/core"; import { Store } from "@ngrx/store"; import { Subscription } from "rxjs"; import { RegionBase } from '../region.base' +import { ARIA_LABELS } from 'common/constants' @Component({ selector: 'region-menu', @@ -22,4 +23,8 @@ export class RegionMenuComponent extends RegionBase implements OnDestroy { this.subscriptions.forEach(s => s.unsubscribe()) } + @Input() + showRegionInOtherTmpl: boolean = true + + SHOW_IN_OTHER_REF_SPACE = ARIA_LABELS.SHOW_IN_OTHER_REF_SPACE } diff --git a/src/ui/parcellationRegion/regionMenu/regionMenu.template.html b/src/ui/parcellationRegion/regionMenu/regionMenu.template.html index 7cbfc972b91bbdca805c887c44a948de715cddc9..75d64eafecbc999c64a33ad85dabdf933a3bbbc4 100644 --- a/src/ui/parcellationRegion/regionMenu/regionMenu.template.html +++ b/src/ui/parcellationRegion/regionMenu/regionMenu.template.html @@ -13,20 +13,16 @@ </mat-card-title> <!-- subtitle on what it is --> - <mat-card-subtitle> + <mat-card-subtitle class="d-inline-flex align-items-center"> <mat-icon fontSet="fas" fontIcon="fa-brain"></mat-icon> <span> Brain region </span> - </mat-card-subtitle> - - <mat-divider></mat-divider> - <!-- other info about brain region --> - <mat-card-subtitle class="mt-4 mb-0 ml-5-n mr-5-n"> + <mat-divider vertical="true" class="ml-2 h-2rem"></mat-divider> <!-- origin datas --> - <button mat-button + <button mat-icon-button [color]="previewDirective.active ? 'primary' : 'basic'" *ngFor="let originDataset of (region.originDatasets || []); let index = index" iav-dataset-preview-dataset-file @@ -37,21 +33,35 @@ [attr.primary]="previewDirective.active || null" role="switch" [attr.aria-checked]="previewDirective.active" + [matTooltip]="SHOW_ORIGIN_DATASET" [attr.aria-label]="SHOW_ORIGIN_DATASET"> <mat-icon fontSet="fas" fontIcon="fa-eye"></mat-icon> - <span> + <!-- <span> View {{ regionOriginDatasetLabels$ | async | renderViewOriginDatasetlabel : index }} - </span> + </span> --> </button> + <!-- position --> <button mat-icon-button *ngIf="region?.position" (click)="navigateToRegion()" - [matTooltip]="region.position | nmToMm | addUnitAndJoin : 'mm'"> + [matTooltip]="GO_TO_REGION_CENTROID + ': ' + (region.position | nmToMm | addUnitAndJoin : 'mm')"> <mat-icon fontSet="fas" fontIcon="fa-map-marked-alt"> </mat-icon> </button> + + <!-- region in other templates --> + <button mat-icon-button + *ngIf="showRegionInOtherTmpl" + [attr.data-available-in-tmpl-count]="(regionInOtherTemplates$ | async).length" + [attr.aria-label]="AVAILABILITY_IN_OTHER_REF_SPACE" + [matMenuTriggerFor]="regionInOtherTemplatesMenu" + [matMenuTriggerData]="{ regionInOtherTemplates: regionInOtherTemplates$ | async }"> + <i class="fas fa-globe"></i> + </button> + </mat-card-subtitle> + </div> </mat-card> @@ -68,43 +78,24 @@ </mat-menu> <!-- template for switching template --> -<ng-template #regionInOtherTemplatesTmpl let-regionInOtherTemplates="regionInOtherTemplates"> - - <div iav-switch #changeTmplSwitch="iavSwitch"> +<mat-menu #regionInOtherTemplatesMenu="matMenu" + [aria-label]="SHOW_IN_OTHER_REF_SPACE"> + hello world + <ng-template matMenuContent let-regionInOtherTemplates="regionInOtherTemplates"> - <mat-list-item *ngIf="regionInOtherTemplates.length" + <mat-list-item *ngFor="let sameRegion of regionInOtherTemplates; let i = index" + [attr.aria-label]="SHOW_IN_OTHER_REF_SPACE + ': ' + sameRegion.template.name + (sameRegion.hemisphere ? (' - ' + sameRegion.hemisphere) : '') " + (click)="changeView(sameRegion)" mat-ripple - [attr.aria-label]="SHOW_IN_OTHER_REF_SPACE" - (click)="changeTmplSwitch && changeTmplSwitch.toggle()"> - <mat-icon fontSet="fas" fontIcon="fa-brain" mat-list-icon></mat-icon> + [attr.role]="'button'"> + <mat-icon fontSet="fas" fontIcon="fa-none" mat-list-icon></mat-icon> <div mat-line> - <span> - Explore in other templates - </span> - <span class="muted"> - ({{ regionInOtherTemplates.length }}) - </span> + <ng-container *ngTemplateOutlet="regionInOtherTemplate; context: sameRegion"> + </ng-container> </div> - <mat-icon fontSet="fas" [fontIcon]="changeTmplSwitch.switchState ? 'fa-chevron-up' : 'fa-chevron-down'"></mat-icon> </mat-list-item> - - <!-- change template items --> - <div *ngIf="changeTmplSwitch.switchState" - [attr.aria-label]="AVAILABILITY_IN_OTHER_REF_SPACE"> - <mat-list-item *ngFor="let sameRegion of regionInOtherTemplates; let i = index" - [attr.aria-label]="SHOW_IN_OTHER_REF_SPACE + ': ' + sameRegion.template.name + (sameRegion.hemisphere ? (' - ' + sameRegion.hemisphere) : '') " - (click)="changeView(sameRegion)" - mat-ripple - [attr.role]="'button'"> - <mat-icon fontSet="fas" fontIcon="fa-none" mat-list-icon></mat-icon> - <div mat-line> - <ng-container *ngTemplateOutlet="regionInOtherTemplate; context: sameRegion"> - </ng-container> - </div> - </mat-list-item> - </div> - </div> -</ng-template> + </ng-template> +</mat-menu> <!-- template for rendering template name and template hemisphere --> <ng-template #regionInOtherTemplate let-template="template" let-hemisphere="hemisphere">