diff --git a/common/constants.js b/common/constants.js index f90d749f15be80159e09d5f65c9b2af2a08f78d0..dc425aa2e0f37b9ed4ff0353e137d7fb3335b3d8 100644 --- a/common/constants.js +++ b/common/constants.js @@ -58,6 +58,7 @@ } exports.CONST = { + MULTI_REGION_SELECTION: `Multi region selection`, REGIONAL_FEATURES: 'Regional features', NO_ADDIONTAL_INFO_AVAIL: `Currently, no additional information is linked to this region.` } diff --git a/docs/releases/v2.3.0.md b/docs/releases/v2.3.0.md index 85b6cf282d9496216f29716c1cccb2af796945b4..a0c965335ab9379b2a8ebfd3f29282255fd9a360 100644 --- a/docs/releases/v2.3.0.md +++ b/docs/releases/v2.3.0.md @@ -22,11 +22,3 @@ ## Under the hood stuff - Updated how dataset retrieval work. It will now query on a region basis - -## Breaking changes - -- Temporarily disabled multiregion selection - -> as a result of the UI overhaul, multi-region selection as it existed before will break the UI in several ways. As a result, it will be disabled temporarily until higher hierarchy region selection can be implemented properly. -> -> Any existing URL which points to a multi-region selection state will be shown a message `Selecting multiple regions has been temporarily disabled in v2.3.0` diff --git a/e2e/src/selecting/region.prod.e2e-spec.js b/e2e/src/selecting/region.prod.e2e-spec.js index 14a10f94c8e59b86b5170d8d6bc0249ad65a819e..45acdadc56b60629de39a547e20b57ba8070670d 100644 --- a/e2e/src/selecting/region.prod.e2e-spec.js +++ b/e2e/src/selecting/region.prod.e2e-spec.js @@ -1,5 +1,6 @@ const { AtlasPage } = require('../../src/util') const { height, width } = require('../../opts') +const { CONST } = require('../../../common/constants') describe('> selecting regions', () => { @@ -36,4 +37,16 @@ describe('> selecting regions', () => { }) }) + + describe('> [bkwdCompat] multi region select is handled gracefully', () => { + const url = `?templateSelected=Waxholm+Space+rat+brain+MRI%2FDTI&parcellationSelected=Waxholm+Space+rat+brain+atlas+v2&cRegionsSelected=%7B%22v2%22%3A%2213.a.b.19.6.c.q.x.1.1L.Y.1K.r.s.y.z._.1G.-.Z.18.v.f.g.1J.1C.k.14.15.7.1E.1F.10.11.12.1D.1S.A.1V.1W.1X.1Y.1Z.1a.1i.1j.1k.1m.1n.1o.1p.U.V.W.3.1I.e.d.1T.1H.m.h.n.1U.o.t.2.17.p.w.4.5.1A.1B.u.l.j.16%22%7D&cNavigation=0.0.0.-W000..2-8Bnd.2_tvb9._yymE._tYzz..1Sjt..9Hnn%7E.Lqll%7E.Vcf..9fo` + it('> handles waxholm v2 whole brains election', async () => { + const newPage = new AtlasPage() + await newPage.init() + await newPage.goto(url) + const texts = await newPage.getAllChipsText() + expect(texts.length).toEqual(2) + expect(texts).toContain(CONST.MULTI_REGION_SELECTION) + }) + }) }) diff --git a/e2e/util/selenium/layout.js b/e2e/util/selenium/layout.js index 286504b7f7b888c614c7fff373dfbf97943cbe86..ceaae248dee3ff0ea7a175d180520897437affed 100644 --- a/e2e/util/selenium/layout.js +++ b/e2e/util/selenium/layout.js @@ -190,6 +190,26 @@ class WdLayoutPage extends WdBase{ } } + /** + * Chips + */ + async _getChips(){ + return await this._browser.findElements( + By.css('mat-chip') + ) + } + + async getAllChipsText(){ + const texts = [] + const webEls = await this._getChips() + for (const el of webEls) { + texts.push( + await _getTextFromWebElement(el) + ) + } + return texts + } + /** * Other diff --git a/src/atlasViewer/atlasViewer.component.ts b/src/atlasViewer/atlasViewer.component.ts index 2cb42747ee6e835178689992567c8411acf22b57..fb2fca548322d3b0a91cdfdde93e7916f3481fcb 100644 --- a/src/atlasViewer/atlasViewer.component.ts +++ b/src/atlasViewer/atlasViewer.component.ts @@ -33,7 +33,7 @@ import { colorAnimation } from "./atlasViewer.animation" import { MouseHoverDirective } from "src/atlasViewer/mouseOver.directive"; import {MatSnackBar, MatSnackBarRef} from "@angular/material/snack-bar"; import {MatDialog, MatDialogRef} from "@angular/material/dialog"; -import { ARIA_LABELS } from 'common/constants' +import { ARIA_LABELS, CONST } from 'common/constants' export const NEHUBA_CLICK_OVERRIDE: InjectionToken<(next: () => void) => void> = new InjectionToken('NEHUBA_CLICK_OVERRIDE') @@ -66,6 +66,7 @@ const compareFn = (it, item) => it.name === item.name export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { + public CONST = CONST public CONTEXT_MENU_ARIA_LABEL = ARIA_LABELS.CONTEXT_MENU public compareFn = compareFn diff --git a/src/atlasViewer/atlasViewer.history.service.spec.ts b/src/atlasViewer/atlasViewer.history.service.spec.ts index aec52f695ecce935610571f746a9da7f5e642fb6..3e4bfb14670d9414ed0c5fc6102672cedad37326 100644 --- a/src/atlasViewer/atlasViewer.history.service.spec.ts +++ b/src/atlasViewer/atlasViewer.history.service.spec.ts @@ -7,7 +7,6 @@ import { Action, Store } from '@ngrx/store' import { defaultRootState } from '../services/stateStore.service' import { cold } from 'jasmine-marbles' import { HttpClientTestingModule } from '@angular/common/http/testing' -import { AngularMaterialModule } from 'src/ui/sharedModules/angularMaterial.module' const bigbrainJson = require('!json-loader!src/res/ext/bigbrain.json') @@ -17,8 +16,7 @@ describe('atlasviewer.history.service.ts', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [ - HttpClientTestingModule, - AngularMaterialModule, + HttpClientTestingModule ], providers: [ AtlasViewerHistoryUseEffect, diff --git a/src/atlasViewer/atlasViewer.history.service.ts b/src/atlasViewer/atlasViewer.history.service.ts index 59fe3cd5abd584c324dcbc541bd072700df4f827..c58aa5a5f32d4081e5e922cc0d1fa31188aa915b 100644 --- a/src/atlasViewer/atlasViewer.history.service.ts +++ b/src/atlasViewer/atlasViewer.history.service.ts @@ -8,7 +8,6 @@ import { AtlasViewerConstantsServices } from "src/atlasViewer/atlasViewer.consta import { cvtSearchParamToState, cvtStateToSearchParam } from "./atlasViewer.urlUtil"; import { viewerStateHelperStoreName } from '../services/state/viewerState.store.helper' import { PureContantService } from "src/util"; -import { MatSnackBar } from "@angular/material/snack-bar"; const getSearchParamStringFromState = state => { try { return cvtStateToSearchParam(state).toString() @@ -59,9 +58,7 @@ export class AtlasViewerHistoryUseEffect implements OnDestroy { } } else { // if non empty search param - const newState = cvtSearchParamToState(search, storeState, error => { - this.snackbar.open(`${error.message}`, 'Dismiss') - }) + const newState = cvtSearchParamToState(search, storeState) return { type: GENERAL_ACTION_TYPES.APPLY_STATE, state: newState, @@ -130,8 +127,7 @@ export class AtlasViewerHistoryUseEffect implements OnDestroy { private store$: Store<IavRootStoreInterface>, private actions$: Actions, private constantService: AtlasViewerConstantsServices, - private pureConstantSErvice: PureContantService, - private snackbar: MatSnackBar + private pureConstantSErvice: PureContantService ) { this.setNewSearchString$.subscribe(newSearchString => { diff --git a/src/atlasViewer/atlasViewer.template.html b/src/atlasViewer/atlasViewer.template.html index d09b5dfa6b0a0097677655890d531a69eed68aab..a45ee29bc896c9957348da909d03410244d579b8 100644 --- a/src/atlasViewer/atlasViewer.template.html +++ b/src/atlasViewer/atlasViewer.template.html @@ -218,69 +218,98 @@ </mat-menu> <ng-template #selectedRegionTmpl> - <ng-container *ngFor="let r of (selectedRegions$ | async)"> - - <!-- region chip for discrete map --> - <mat-chip - iav-region - (click)="uiNehubaContainer.matDrawerMinor.open() && uiNehubaContainer.navSideDrawerMainSwitch.open()" - [region]="r" - class="pe-all position-relative z-index-1 ml-8-n" - [ngClass]="{ - 'darktheme':regionDirective.rgbDarkmode === true, - 'lighttheme': regionDirective.rgbDarkmode === false - }" - [style.backgroundColor]="regionDirective.rgbString" - #regionDirective="iavRegion"> - <span class="iv-custom-comp text text-truncate d-inline pl-4"> - {{ r.name }} - </span> - <mat-icon - class="iv-custom-comp text" - (click)="clearSelectedRegions()" - fontSet="fas" - iav-stop="click" - fontIcon="fa-times"> - </mat-icon> - </mat-chip> - <!-- chips for previewing origin datasets/continous map --> - <ng-container *ngFor="let originDataset of (r.originDatasets || []); let index = index"> - <div class="hidden" - iav-dataset-preview-dataset-file - [iav-dataset-preview-dataset-file-kgid]="originDataset.kgId" - [iav-dataset-preview-dataset-file-filename]="originDataset.filename" - #previewDirective="iavDatasetPreviewDatasetFile"> - </div> - <mat-chip *ngIf="previewDirective.active" + <!-- regions chip --> + <ng-template [ngIf]="selectedRegions$ | async" let-selectedRegions="ngIf"> + <!-- if regions.length > 1 --> + <!-- use group chip --> + <ng-template [ngIf]="selectedRegions.length > 1" [ngIfElse]="singleRegionTmpl"> + <mat-chip + color="primary" + selected (click)="uiNehubaContainer.matDrawerMinor.open() && uiNehubaContainer.navSideDrawerMainSwitch.open()" - class="pe-all position-relative ml-8-n"> - <span class="pl-4"> - {{ regionDirective.regionOriginDatasetLabels$ | async | renderViewOriginDatasetlabel : index }} + class="pe-all position-relative z-index-1 ml-8-n"> + <span class="iv-custom-comp text text-truncate d-inline pl-4"> + {{ CONST.MULTI_REGION_SELECTION }} </span> - <mat-icon (click)="previewDirective.onClick()" + <mat-icon + (click)="clearSelectedRegions()" fontSet="fas" iav-stop="click" fontIcon="fa-times"> </mat-icon> </mat-chip> + </ng-template> - <mat-chip *ngFor="let key of clearViewKeys$ | async" - (click)="uiNehubaContainer.matDrawerMinor.open() && uiNehubaContainer.navSideDrawerMainSwitch.open()" - class="pe-all position-relative ml-8-n"> - <span class="pl-4"> - {{ key }} - </span> - <mat-icon (click)="unsetClearViewByKey(key)" - fontSet="fas" - iav-stop="click" - fontIcon="fa-times"> - - </mat-icon> - </mat-chip> - </ng-container> + <!-- if reginos.lengt === 1 --> + <!-- use single region chip --> + <ng-template #singleRegionTmpl> + <ng-container *ngFor="let r of selectedRegions"> + + <!-- region chip for discrete map --> + <mat-chip + iav-region + (click)="uiNehubaContainer.matDrawerMinor.open() && uiNehubaContainer.navSideDrawerMainSwitch.open()" + [region]="r" + class="pe-all position-relative z-index-1 ml-8-n" + [ngClass]="{ + 'darktheme':regionDirective.rgbDarkmode === true, + 'lighttheme': regionDirective.rgbDarkmode === false + }" + [style.backgroundColor]="regionDirective.rgbString" + #regionDirective="iavRegion"> + <span class="iv-custom-comp text text-truncate d-inline pl-4"> + {{ r.name }} + </span> + <mat-icon + class="iv-custom-comp text" + (click)="clearSelectedRegions()" + fontSet="fas" + iav-stop="click" + fontIcon="fa-times"> + </mat-icon> + </mat-chip> + + <!-- chips for previewing origin datasets/continous map --> + <ng-container *ngFor="let originDataset of (r.originDatasets || []); let index = index"> + <div class="hidden" + iav-dataset-preview-dataset-file + [iav-dataset-preview-dataset-file-kgid]="originDataset.kgId" + [iav-dataset-preview-dataset-file-filename]="originDataset.filename" + #previewDirective="iavDatasetPreviewDatasetFile"> + </div> + <mat-chip *ngIf="previewDirective.active" + (click)="uiNehubaContainer.matDrawerMinor.open() && uiNehubaContainer.navSideDrawerMainSwitch.open()" + class="pe-all position-relative ml-8-n"> + <span class="pl-4"> + {{ regionDirective.regionOriginDatasetLabels$ | async | renderViewOriginDatasetlabel : index }} + </span> + <mat-icon (click)="previewDirective.onClick()" + fontSet="fas" + iav-stop="click" + fontIcon="fa-times"> + </mat-icon> + </mat-chip> + + <mat-chip *ngFor="let key of clearViewKeys$ | async" + (click)="uiNehubaContainer.matDrawerMinor.open() && uiNehubaContainer.navSideDrawerMainSwitch.open()" + class="pe-all position-relative ml-8-n"> + <span class="pl-4"> + {{ key }} + </span> + <mat-icon (click)="unsetClearViewByKey(key)" + fontSet="fas" + iav-stop="click" + fontIcon="fa-times"> + + </mat-icon> + </mat-chip> + </ng-container> + + </ng-container> + </ng-template> + </ng-template> - </ng-container> </ng-template> <ng-template #selectedDatasetPreview let-layers="layers"> diff --git a/src/atlasViewer/atlasViewer.urlUtil.spec.ts b/src/atlasViewer/atlasViewer.urlUtil.spec.ts index 50746735a6f7bfbd7a87af316cd499a5407fe656..01787b3b1631a2d108b09f8d3e633bda635df160 100644 --- a/src/atlasViewer/atlasViewer.urlUtil.spec.ts +++ b/src/atlasViewer/atlasViewer.urlUtil.spec.ts @@ -31,46 +31,6 @@ const fetchedTemplateRootState = { describe('atlasViewer.urlService.service.ts', () => { describe('cvtSearchParamToState', () => { - /** - * for 2.3.0 onwards - * multi region selection has been temporarily disabled. - * search param parse needs to return emtpy array when encountered - */ - it('> filters out multi region selection an returns an empty array', () => { - const searchString = `?templateSelected=Waxholm+Space+rat+brain+MRI%2FDTI&parcellationSelected=Waxholm+Space+rat+brain+atlas+v2&cRegionsSelected=%7B%22v2%22%3A%2213.a.b.19.6.c.q.x.1.1L.Y.1K.r.s.y.z._.1G.-.Z.18.v.f.g.1J.1C.k.14.15.7.1E.1F.10.11.12.1D.1S.A.1V.1W.1X.1Y.1Z.1a.1i.1j.1k.1m.1n.1o.1p.U.V.W.3.1I.e.d.1T.1H.m.h.n.1U.o.t.2.17.p.w.4.5.1A.1B.u.l.j.16%22%7D&cNavigation=0.0.0.-W000..2-8Bnd.2_tvb9._yymE._tYzz..1Sjt..9Hnn%7E.Lqll%7E.Vcf..9fo` - const searchparam = new URLSearchParams(searchString) - const regionObj = JSON.parse(searchparam.get('cRegionsSelected')) - const totalRegions = [] - for (const key in regionObj) { - for (const el of regionObj[key].split('.')) { - totalRegions.push(el) - } - } - expect(totalRegions.length).toBeGreaterThan(1) - - const newState = cvtSearchParamToState(searchparam, fetchedTemplateRootState) - expect(newState?.viewerState?.regionsSelected).toEqual([]) - }) - - /** - * leaves single region selection intact - */ - it('> leaves single region selection intact', () => { - const searchString = '?templateSelected=Waxholm+Space+rat+brain+MRI%2FDTI&parcellationSelected=Waxholm+Space+rat+brain+atlas+v2&cRegionsSelected=%7B"v2"%3A"1S"%7D&cNavigation=0.0.0.-W000..2-8Bnd.2_tvb9._yymE._tYzz..1Sjt..9Hnn~.Lqll~.Vcf..9fo' - const searchparam = new URLSearchParams(searchString) - const regionObj = JSON.parse(searchparam.get('cRegionsSelected')) - const totalRegions = [] - for (const key in regionObj) { - for (const el of regionObj[key].split('.')) { - totalRegions.push(el) - } - } - expect(totalRegions.length).toEqual(1) - - const newState = cvtSearchParamToState(searchparam, fetchedTemplateRootState) - expect(newState?.viewerState?.regionsSelected?.length).toEqual(1) - }) - it('> convert empty search param to empty state', () => { const searchparam = new URLSearchParams() expect(() => cvtSearchParamToState(searchparam, defaultRootState)).toThrow() diff --git a/src/atlasViewer/atlasViewer.urlUtil.ts b/src/atlasViewer/atlasViewer.urlUtil.ts index 85ef716d875a4620fda3df65857866cd7bdb2e9f..59640205e71446281e4329b776b7f1b3bfbb133b 100644 --- a/src/atlasViewer/atlasViewer.urlUtil.ts +++ b/src/atlasViewer/atlasViewer.urlUtil.ts @@ -13,9 +13,7 @@ export const PARSING_SEARCHPARAM_ERROR = { const PARSING_SEARCHPARAM_WARNING = { UNKNOWN_PARCELLATION: 'UNKNOWN_PARCELLATION', DECODE_CIPHER_ERROR: 'DECODE_CIPHER_ERROR', - ID_ERROR: 'ID_ERROR', - - DEPRECATION_ERROR: `DEPRECATION_ERROR` + ID_ERROR: 'ID_ERROR' } export const CVT_STATE_TO_SEARCHPARAM_ERROR = { @@ -92,7 +90,7 @@ export const cvtStateToSearchParam = (state: any): URLSearchParams => { } const { TEMPLATE_NOT_FOUND, TEMPALTE_NOT_SET, PARCELLATION_NOT_UPDATED } = PARSING_SEARCHPARAM_ERROR -const { UNKNOWN_PARCELLATION, DECODE_CIPHER_ERROR, ID_ERROR, DEPRECATION_ERROR } = PARSING_SEARCHPARAM_WARNING +const { UNKNOWN_PARCELLATION, DECODE_CIPHER_ERROR, ID_ERROR } = PARSING_SEARCHPARAM_WARNING const parseSearchParamForTemplateParcellationRegion = (searchparams: URLSearchParams, state: IavRootStoreInterface, cb?: (arg: any) => void) => { @@ -195,13 +193,7 @@ const parseSearchParamForTemplateParcellationRegion = (searchparams: URLSearchPa return { templateSelected, parcellationSelected, - regionsSelected: (() => { - if (regionsSelected.length > 1) { - cb({ type: DEPRECATION_ERROR, message: `Selecting multiple regions has been temporarily disabled in v2.3.0` }) - return [] - } - return regionsSelected - })() + regionsSelected } } diff --git a/src/ui/databrowserModule/databrowser.service.spec.ts b/src/ui/databrowserModule/databrowser.service.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..174d25a175f4a905c8f4b824d63c373565f2ff0d --- /dev/null +++ b/src/ui/databrowserModule/databrowser.service.spec.ts @@ -0,0 +1,7 @@ +describe('> databrowser.service.ts', () => { + describe('> DatabrowserService', () => { + describe('> getDatasetsByRegion', () => { + it('memoize the fn call so http requests are only sent onces') + }) + }) +}) \ No newline at end of file diff --git a/src/ui/databrowserModule/databrowser.service.ts b/src/ui/databrowserModule/databrowser.service.ts index 875daac2488ed97b858083629eb13a6da0269d0b..25c601d5ef03807f31b69d475d17bc06fe8dd696 100644 --- a/src/ui/databrowserModule/databrowser.service.ts +++ b/src/ui/databrowserModule/databrowser.service.ts @@ -66,7 +66,21 @@ export class DatabrowserService implements OnDestroy { public createDatabrowser: (arg: {regions: any[], template: any, parcellation: any}) => {dataBrowser: ComponentRef<DataBrowser>, widgetUnit: ComponentRef<WidgetUnit>} public getDataByRegion: ({ regions, parcellation, template }: {regions: any[], parcellation: any, template: any}) => Promise<IDataEntry[]> = ({regions, parcellation, template}) => forkJoin(regions.map(this.getDatasetsByRegion.bind(this))).pipe( - map((arrOfArr: IDataEntry[][]) => arrOfArr.reduce((acc, curr) => acc.concat(curr), [])) + map( + (arrOfArr: IDataEntry[][]) => arrOfArr.reduce( + (acc, curr) => { + /** + * In the event of multi region selection + * It is entirely possibly that different regions can fetch the same dataset + * If that's the case, filter by fullId attribute + */ + const existSet = new Set(acc.map(v => v['fullId'])) + const filteredCurr = curr.filter(v => !existSet.has(v['fullId'])) + return acc.concat(filteredCurr) + }, + [] + ) + ) ).toPromise() private filterDEByRegion: FilterDataEntriesByRegion = new FilterDataEntriesByRegion() @@ -257,14 +271,21 @@ export class DatabrowserService implements OnDestroy { public fetchingFlag: boolean = false private mostRecentFetchToken: any + private memoizedDatasetByRegion = new Map<string, Observable<IDataEntry>>() private getDatasetsByRegion(region: { fullId: string }){ - return this.http.get<IDataEntry>( - `${this.constantService.backendUrl}datasets/byRegion/${encodeURIComponent(getIdFromFullId(region.fullId))}`, + const fullId = getIdFromFullId(region.fullId) + if (this.memoizedDatasetByRegion.has(fullId)) return this.memoizedDatasetByRegion.get(fullId) + const obs$ = this.http.get<IDataEntry>( + `${this.constantService.backendUrl}datasets/byRegion/${encodeURIComponent(fullId)}`, { headers: this.constantService.getHttpHeader(), responseType: 'json' } + ).pipe( + shareReplay(1), ) + this.memoizedDatasetByRegion.set(fullId, obs$) + return obs$ } private lowLevelQuery(templateName: string, parcellationName: string): Promise<IDataEntry[]> { diff --git a/src/ui/nehubaContainer/nehubaContainer.component.ts b/src/ui/nehubaContainer/nehubaContainer.component.ts index bd425f8e38faea6829e498ec99dc0d94433d8a51..108e1409607fb9484ac1cd9755f33f8b87d70092 100644 --- a/src/ui/nehubaContainer/nehubaContainer.component.ts +++ b/src/ui/nehubaContainer/nehubaContainer.component.ts @@ -20,14 +20,14 @@ import { NgViewerStateInterface } from "src/services/stateStore.service"; -import { getExportNehuba, isSame, getViewer } from "src/util/fn"; +import { getExportNehuba, isSame } from "src/util/fn"; import { API_SERVICE_SET_VIEWER_HANDLE_TOKEN, IUserLandmark } from "src/atlasViewer/atlasViewer.apiService.service"; import { NehubaViewerUnit } from "./nehubaViewer/nehubaViewer.component"; import { compareLandmarksChanged } from "src/util/constants"; import { PureContantService } from "src/util"; import { ARIA_LABELS, IDS, CONST } from 'common/constants' import { ngViewerActionSetPerspOctantRemoval, PANELS, ngViewerActionToggleMax, ngViewerActionAddNgLayer, ngViewerActionRemoveNgLayer } from "src/services/state/ngViewerState.store.helper"; -import { viewerStateSelectRegionWithIdDeprecated, viewerStateAddUserLandmarks, viewreStateRemoveUserLandmarks, viewerStateCustomLandmarkSelector, viewerStateSelectedParcellationSelector, viewerStateSelectedTemplateSelector } from 'src/services/state/viewerState.store.helper' +import { viewerStateSelectRegionWithIdDeprecated, viewerStateAddUserLandmarks, viewreStateRemoveUserLandmarks, viewerStateCustomLandmarkSelector, viewerStateSelectedParcellationSelector, viewerStateSelectedTemplateSelector, viewerStateSelectedRegionsSelector } from 'src/services/state/viewerState.store.helper' import { SwitchDirective } from "src/util/directives/switch.directive"; import { viewerStateDblClickOnViewer, @@ -203,8 +203,7 @@ export class NehubaContainer implements OnInit, OnChanges, OnDestroy { ) public selectedRegions: any[] = [] public selectedRegions$: Observable<any[]> = this.store.pipe( - select('viewerState'), - select('regionsSelected'), + select(viewerStateSelectedRegionsSelector), filter(rs => !!rs), ) diff --git a/src/ui/nehubaContainer/nehubaContainer.template.html b/src/ui/nehubaContainer/nehubaContainer.template.html index 69a1497ad788b73a16ad30b061ab35e51bb92a7b..f312792691b899fae30a53d625ea5c75607ee527 100644 --- a/src/ui/nehubaContainer/nehubaContainer.template.html +++ b/src/ui/nehubaContainer/nehubaContainer.template.html @@ -145,6 +145,7 @@ #previews="iavShownPreviews"> </div> + <!-- preview volumes --> <ng-container *ngIf="previews.iavAdditionalLayers$ | async | filterPreviewByType : [previews.FILETYPES.VOLUMES] as volumePreviews"> <ng-template [ngIf]="volumePreviews.length > 0" [ngIfElse]="sidenavRegionTmpl"> <ng-container *ngFor="let vPreview of volumePreviews"> @@ -195,11 +196,21 @@ [ngClass]="{'region-populated': (selectedRegions$ | async).length > 0 }"> <!-- region detail --> <ng-container *ngIf="selectedRegions$ | async as selectedRegions; else selectRegionErrorTmpl"> - <ng-container *ngFor="let region of selectedRegions"> - <ng-container *ngTemplateOutlet="singleRegionTmpl; context: { region: region }"> + <!-- single-region-wrapper --> + <ng-template [ngIf]="selectedRegions.length === 1" [ngIfElse]="multiRegionWrapperTmpl"> + <ng-container *ngTemplateOutlet="singleRegionTmpl; context: { region: selectedRegions[0] }"> </ng-container> - </ng-container> + </ng-template> + + <!-- multi region wrapper --> + <ng-template #multiRegionWrapperTmpl> + <ng-container *ngTemplateOutlet="multiRegionTmpl; context: { + regions: selectedRegions + }"> + </ng-container> + <!-- This is a wrapper for multiregion consisting of {{ selectedRegions.length }} regions --> + </ng-template> <!-- place holder if length < 0 --> <ng-container *ngIf="selectedRegions.length === 0"> @@ -275,17 +286,134 @@ </div> </ng-template> -<ng-template #singleRegionTmpl let-region="region"> - <ng-template #regionDetailTmpl> - <div class="placeholder-region-detail side-nav-cover mat-elevation-z4"> - <span class="text-muted"> - Select a region by clicking on the viewer or search from above - </span> - </div> +<!-- region tmpl placeholder --> +<ng-template #regionPlaceholderTmpl> + <div class="placeholder-region-detail side-nav-cover mat-elevation-z4"> + <span class="text-muted"> + Select a region by clicking on the viewer or search from above + </span> + </div> +</ng-template> + +<!-- expansion tmpl --> +<ng-template #ngMatAccordionTmpl + let-title="title" + let-desc="desc" + let-iconClass="iconClass" + let-iconTooltip="iconTooltip" + let-iavNgIf="iavNgIf" + let-content="content"> + <mat-expansion-panel class="mt-1 mb-1" + [attr.data-opened]="expansionPanel.expanded" + [attr.data-mat-expansion-title]="title" + hideToggle + *ngIf="iavNgIf" + #expansionPanel="matExpansionPanel"> + + <mat-expansion-panel-header> + + <!-- title --> + <mat-panel-title> + {{ title }} + </mat-panel-title> + + <!-- desc + icon --> + <mat-panel-description class="d-flex align-items-center justify-content-end" + [matTooltip]="iconTooltip"> + <span class="mr-3">{{ desc }}</span> + <span class="accordion-icon d-inline-flex justify-content-center"> + <i [class]="iconClass"></i> + </span> + </mat-panel-description> + + </mat-expansion-panel-header> + + <!-- content --> + <ng-template matExpansionPanelContent> + <ng-container *ngTemplateOutlet="content; context: { expansionPanel: expansionPanel }"> + </ng-container> + </ng-template> + </mat-expansion-panel> +</ng-template> + + +<!-- multi region tmpl --> +<ng-template #multiRegionTmpl let-regions="regions"> + <ng-template [ngIf]="regions.length > 0" [ngIfElse]="regionPlaceholderTmpl"> + <region-menu + [showRegionInOtherTmpl]="false" + [region]="{ + name: CONST.MULTI_REGION_SELECTION + }" + class="side-nav-cover mat-elevation-z4"> + </region-menu> + + <!-- other regions detail accordion --> + <mat-accordion class="side-nav-cover mt-2"> + + <!-- regional features--> + <ng-template #regionalFeaturesTmpl> + <data-browser [template]="templateSelected$ | async" + [parcellation]="selectedParcellation" + [disableVirtualScroll]="true" + [regions]="regions"> + </data-browser> + </ng-template> + + <div class="hidden" iav-databrowser-directive + [template]="templateSelected$ | async" + [parcellation]="selectedParcellation" + [regions]="regions" + #iavDbDirective="iavDatabrowserDirective"> + </div> + + <ng-container *ngTemplateOutlet="ngMatAccordionTmpl; context: { + title: CONST.REGIONAL_FEATURES, + desc: iavDbDirective?.dataentries?.length, + iconClass: 'fas fa-database', + iconTooltip: iavDbDirective?.dataentries?.length | regionAccordionTooltipTextPipe : 'regionalFeatures', + iavNgIf: iavDbDirective?.dataentries?.length, + content: regionalFeaturesTmpl + }"> + </ng-container> + + <!-- Multi regions include --> + <ng-template #multiRegionInclTmpl> + <mat-chip-list> + <mat-chip *ngFor="let r of regions" + iav-region + [region]="r" + [ngClass]="{ + 'darktheme':regionDirective.rgbDarkmode === true, + 'lighttheme': regionDirective.rgbDarkmode === false + }" + [style.backgroundColor]="regionDirective.rgbString" + #regionDirective="iavRegion"> + <span class="iv-custom-comp text text-truncate d-inline pl-4"> + {{ r.name }} + </span> + </mat-chip> + </mat-chip-list> + </ng-template> + + <ng-container *ngTemplateOutlet="ngMatAccordionTmpl; context: { + title: 'Brain regions', + desc: regions.length, + iconClass: 'fas fa-brain', + iavNgIf: true, + content: multiRegionInclTmpl + }"> + </ng-container> + + </mat-accordion> </ng-template> +</ng-template> + +<!-- single region tmpl --> +<ng-template #singleRegionTmpl let-region="region"> <!-- region detail --> - <ng-container *ngIf="region; else regionDetailTmpl"> + <ng-container *ngIf="region; else regionPlaceholderTmpl"> <region-menu [showRegionInOtherTmpl]="false" [region]="region" @@ -293,52 +421,13 @@ </region-menu> </ng-container> - + <!-- other region detail accordion --> <mat-accordion *ngIf="region" class="side-nav-cover mt-2" iav-region [region]="region" #iavRegion="iavRegion"> - <!-- expansion tmpl --> - <ng-template #ngMatAccordionTmpl - let-title="title" - let-desc="desc" - let-iconClass="iconClass" - let-iconTooltip="iconTooltip" - let-iavNgIf="iavNgIf" - let-content="content"> - <mat-expansion-panel class="mt-1 mb-1" - [attr.data-opened]="expansionPanel.expanded" - [attr.data-mat-expansion-title]="title" - hideToggle - *ngIf="iavNgIf" - #expansionPanel="matExpansionPanel"> - - <mat-expansion-panel-header> - - <!-- title --> - <mat-panel-title> - {{ title }} - </mat-panel-title> - - <!-- desc + icon --> - <mat-panel-description class="d-flex align-items-center justify-content-end" - [matTooltip]="iconTooltip"> - <span class="mr-3">{{ desc }}</span> - <span class="accordion-icon d-inline-flex justify-content-center"> - <i [class]="iconClass"></i> - </span> - </mat-panel-description> - - </mat-expansion-panel-header> - - <!-- content --> - <ng-container *ngTemplateOutlet="content; context: { expansionPanel: expansionPanel }"> - </ng-container> - </mat-expansion-panel> - </ng-template> - <!-- Explore in other template --> <ng-container *ngIf="iavRegion.regionInOtherTemplates$ | async as regionInOtherTemplates"> diff --git a/src/ui/parcellationRegion/region.base.ts b/src/ui/parcellationRegion/region.base.ts index 1655dcfb0193f25c5c3c2e57d8e91c2bbd81260e..7aedc3c988ef1a95dd1ce80b6c039548353ad3f1 100644 --- a/src/ui/parcellationRegion/region.base.ts +++ b/src/ui/parcellationRegion/region.base.ts @@ -220,6 +220,7 @@ export const regionInOtherTemplateSelector = createSelector( const regionOfInterestId = getIdFromFullId(regionOfInterest.fullId) const { fetchedTemplates, templateSelected } = viewerState + if (!templateSelected) return [] const selectedTemplateId = getIdFromFullId(templateSelected.fullId) const otherTemplates = fetchedTemplates.filter(({ fullId }) => getIdFromFullId(fullId) !== selectedTemplateId) for (const template of otherTemplates) { diff --git a/src/ui/sharedModules/angularMaterial.module.ts b/src/ui/sharedModules/angularMaterial.module.ts index 9bed0ad4ba2c4f0381c565d7a56588eb6fd10f34..d5384113b07877cc3d0c0a726387c2c38d5e64bf 100644 --- a/src/ui/sharedModules/angularMaterial.module.ts +++ b/src/ui/sharedModules/angularMaterial.module.ts @@ -1,6 +1,6 @@ import {MAT_DIALOG_DEFAULT_OPTIONS, MatDialogConfig, MatDialogModule} from "@angular/material/dialog"; import {MatButtonModule} from "@angular/material/button"; -import {MatSnackBarModule, MAT_SNACK_BAR_DEFAULT_OPTIONS} from "@angular/material/snack-bar"; +import {MatSnackBarModule} from "@angular/material/snack-bar"; import {MatCardModule} from "@angular/material/card"; import {MatCheckboxModule} from "@angular/material/checkbox"; import {MatTabsModule} from "@angular/material/tabs"; @@ -96,11 +96,6 @@ const defaultDialogOption: MatDialogConfig = new MatDialogConfig() ...defaultDialogOption, panelClass: 'iav-dialog-class', }, - },{ - provide: MAT_SNACK_BAR_DEFAULT_OPTIONS, - useValue: { - duration: 2500 - } }], }) export class AngularMaterialModule { } diff --git a/src/ui/viewerStateController/regionSearch/regionSearch.component.ts b/src/ui/viewerStateController/regionSearch/regionSearch.component.ts index 43fbe572221841a757c11b22d14a3faacd0c9c43..b397abcf5f4fe9541c385bbb2ce757ac0a0c19b0 100644 --- a/src/ui/viewerStateController/regionSearch/regionSearch.component.ts +++ b/src/ui/viewerStateController/regionSearch/regionSearch.component.ts @@ -11,7 +11,7 @@ import { MatDialog } from "@angular/material/dialog"; import { MatAutocompleteSelectedEvent } from "@angular/material/autocomplete"; import { PureContantService } from "src/util"; import { viewerStateToggleRegionSelect, viewerStateNavigateToRegion, viewerStateSetSelectedRegions, viewerStateSetSelectedRegionsWithIds } from "src/services/state/viewerState.store.helper"; -import { ARIA_LABELS } from 'common/constants' +import { ARIA_LABELS, CONST } from 'common/constants' const filterRegionBasedOnText = searchTerm => region => region.name.toLowerCase().includes(searchTerm.toLowerCase()) || (region.relatedAreas && region.relatedAreas.some(relatedArea => relatedArea.name && relatedArea.name.toLowerCase().includes(searchTerm.toLowerCase()))) @@ -30,7 +30,10 @@ const compareFn = (it, item) => it.name === item.name export class RegionTextSearchAutocomplete { public renderInputText(regionsSelected: any[]){ - return regionsSelected && regionsSelected.length > 0 && regionsSelected[0].name || '' + if (!regionsSelected) return '' + if (regionsSelected.length === 0) return '' + if (regionsSelected.length === 1) return regionsSelected[0].name || '' + return CONST.MULTI_REGION_SELECTION } public manualRenderList$: Subject<any> = new Subject()