From fc7511f2d856a95537e0b6f2e629b6518d425463 Mon Sep 17 00:00:00 2001 From: Xiao Gui <xgui3783@gmail.com> Date: Fri, 20 Sep 2019 18:01:53 +0200 Subject: [PATCH] WIP new search UI --- src/atlasViewer/atlasViewer.component.ts | 9 +- src/atlasViewer/atlasViewer.template.html | 38 ++- src/res/css/extra_styles.css | 68 +++++ src/services/effect/effect.ts | 13 +- src/services/state/uiState.store.ts | 10 - .../databrowserModule/databrowser.module.ts | 10 +- .../databrowser/databrowser.component.ts | 14 +- .../databrowser/databrowser.style.css | 188 +------------ .../databrowser/databrowser.template.html | 247 +++++++++--------- .../detailedView/singleDataset.component.ts | 28 ++ .../singleDataset.style.css | 0 .../singleDataset.template.html | 0 .../singleDatasetListView.component.ts | 27 ++ .../listView/singleDatasetListView.style.css | 0 .../singleDatasetListView.template.html | 55 ++++ ...set.component.ts => singleDataset.base.ts} | 30 +-- .../util/datasetIsFaved.pipe.ts | 1 + src/ui/menuicons/menuicons.component.ts | 19 +- src/ui/menuicons/menuicons.template.html | 2 - .../searchSideNav/searchSideNav.component.ts | 77 ++++++ src/ui/searchSideNav/searchSideNav.style.css | 0 .../searchSideNav/searchSideNav.template.html | 60 +++++ src/ui/ui.module.ts | 6 +- .../currentlySelectedRegions.component.ts | 46 ++++ .../currentlySelectedRegions.style.css | 4 + .../currentlySelectedRegions.template.html | 24 ++ .../regionHierachy/regionHierarchy.style.css | 8 +- .../regionHierarchy.template.html | 32 +-- .../regionSearch/regionSearch.component.ts | 7 +- .../regionSearch/regionSearch.template.html | 9 +- .../viewerState.component.ts | 27 -- .../viewerState.template.html | 103 +------- .../directives/stopPropagation.directive.ts | 47 ++++ src/util/util.module.ts | 7 +- 34 files changed, 673 insertions(+), 543 deletions(-) create mode 100644 src/ui/databrowserModule/singleDataset/detailedView/singleDataset.component.ts rename src/ui/databrowserModule/singleDataset/{ => detailedView}/singleDataset.style.css (100%) rename src/ui/databrowserModule/singleDataset/{ => detailedView}/singleDataset.template.html (100%) create mode 100644 src/ui/databrowserModule/singleDataset/listView/singleDatasetListView.component.ts create mode 100644 src/ui/databrowserModule/singleDataset/listView/singleDatasetListView.style.css create mode 100644 src/ui/databrowserModule/singleDataset/listView/singleDatasetListView.template.html rename src/ui/databrowserModule/singleDataset/{singleDataset.component.ts => singleDataset.base.ts} (87%) create mode 100644 src/ui/searchSideNav/searchSideNav.component.ts create mode 100644 src/ui/searchSideNav/searchSideNav.style.css create mode 100644 src/ui/searchSideNav/searchSideNav.template.html create mode 100644 src/ui/viewerStateController/currentlySelectedRegions/currentlySelectedRegions.component.ts create mode 100644 src/ui/viewerStateController/currentlySelectedRegions/currentlySelectedRegions.style.css create mode 100644 src/ui/viewerStateController/currentlySelectedRegions/currentlySelectedRegions.template.html create mode 100644 src/util/directives/stopPropagation.directive.ts diff --git a/src/atlasViewer/atlasViewer.component.ts b/src/atlasViewer/atlasViewer.component.ts index af0debbaa..8a50086a1 100644 --- a/src/atlasViewer/atlasViewer.component.ts +++ b/src/atlasViewer/atlasViewer.component.ts @@ -14,7 +14,7 @@ import { NehubaContainer } from "../ui/nehubaContainer/nehubaContainer.component import { colorAnimation } from "./atlasViewer.animation" import { FixedMouseContextualContainerDirective } from "src/util/directives/FixedMouseContextualContainerDirective.directive"; import { DatabrowserService } from "src/ui/databrowserModule/databrowser.service"; -import { AGREE_COOKIE, AGREE_KG_TOS, SHOW_KG_TOS, SHOW_SIDEBAR_TEMPLATE, SHOW_BOTTOM_SHEET } from "src/services/state/uiState.store"; +import { AGREE_COOKIE, AGREE_KG_TOS, SHOW_KG_TOS, SHOW_BOTTOM_SHEET } from "src/services/state/uiState.store"; import { TabsetComponent } from "ngx-bootstrap/tabs"; import { LocalFileService } from "src/services/localFile.service"; import { MatDialog, MatDialogRef, MatSnackBar, MatSnackBarRef, MatBottomSheet, MatBottomSheetRef } from "@angular/material"; @@ -97,7 +97,6 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { public unsupportedPreviews: any[] = UNSUPPORTED_PREVIEW public sidePanelOpen$: Observable<boolean> - public sideNavTemplate$: Observable<TemplateRef<any>> get toggleMessage(){ return this.constantsService.toggleMessage @@ -153,12 +152,6 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { map(state => state.sidePanelOpen) ) - this.sideNavTemplate$ = this.store.pipe( - select('uiState'), - select('sidebarTemplate'), - distinctUntilChanged() - ) - this.showHelp$ = this.constantsService.showHelpSubject$.pipe( debounceTime(170) ) diff --git a/src/atlasViewer/atlasViewer.template.html b/src/atlasViewer/atlasViewer.template.html index 5ca66e94d..5347548f8 100644 --- a/src/atlasViewer/atlasViewer.template.html +++ b/src/atlasViewer/atlasViewer.template.html @@ -36,8 +36,6 @@ </layout-floating-container> </tab> <tab heading="Containers"> - <menu-icons hidden #MenuIcons> - </menu-icons> <panel-component> <div class="m-2" heading> Layer Browser @@ -156,25 +154,41 @@ <div class="z-index-10 position-absolute pe-none w-100 h-100"> <mat-drawer-container + *ngIf="newViewer$ | async" [hasBackdrop]="false" - class="w-100 h-100 bg-none"> + class="w-100 h-100 bg-none mat-drawer-content-overflow-visible"> <mat-drawer mode="push" - class="pe-all col-sm-12 col-md-3" + class="col-sm-10 col-md-4 col-lg-3 col-xl-2 p-2 bg-none box-shadow-none overflow-visible" [disableClose]="true" [autoFocus]="false" - [opened]="sideNavTemplate$ | async"> + [opened]="true" + #sideNavDrawer> <!-- template outlet --> - <mat-card class="h-100"> - <ng-container *ngTemplateOutlet="sideNavTemplate$ | async"> - </ng-container> - </mat-card> + <search-side-nav + (dismiss)="sideNavDrawer.close()" + (open)="sideNavDrawer.open()" + class="h-100 d-block overflow-visible" + #searchSideNav> + </search-side-nav> </mat-drawer> - <div class="d-flex h-100 justify-content-between align-items-start bg-none pe-none"> - <menu-icons> - </menu-icons> + <div class="d-flex h-100 align-items-start bg-none pe-none"> + + <button mat-flat-button + matBadgePosition="above after" + matBadgeColor="accent" + [matBadge]="!sideNavDrawer.opened && (selectedRegions$ | async)?.length ? (selectedRegions$ | async)?.length : null" + [matTooltip]="!sideNavDrawer.opened ? (selectedRegions$ | async)?.length ? ('Explore ' + (selectedRegions$ | async)?.length + ' selected regions.') : 'Explore current view' : null" + [ngClass]="{'translate-x-6-n': !sideNavDrawer.opened, 'translate-x-7-n': sideNavDrawer.opened}" + class="pe-all mt-5" + (click)="sideNavDrawer.toggle()"> + <i [ngClass]="{'fa-chevron-left': sideNavDrawer.opened, 'fa-chevron-right': !sideNavDrawer.opened}" class="fas translate-x-3"></i> + + </button> + + <!-- TODO clean up menu icon --> </div> </mat-drawer-container> </div> diff --git a/src/res/css/extra_styles.css b/src/res/css/extra_styles.css index a835d33bc..709122816 100644 --- a/src/res/css/extra_styles.css +++ b/src/res/css/extra_styles.css @@ -293,6 +293,11 @@ markdown-dom pre code transform: rotate(90deg)!important; } +.r-270 +{ + transform: rotate(270deg)!important; +} + .ws-no-wrap { white-space: nowrap!important; @@ -512,3 +517,66 @@ cdk-virtual-scroll-viewport > .cdk-virtual-scroll-content-wrapper { flex-grow:1; } + +.transform-origin-left-center +{ + transform-origin: 0% 50%; +} + +.transform-origin-center +{ + transform-origin: 50% 50%; +} + +.box-shadow-none +{ + box-shadow: none!important; +} + +.translate-x-2 +{ + transform: translateX(1em); +} + +.translate-x-3 +{ + transform: translateX(1.5em); +} + +.translate-x-4 +{ + transform: translateX(2em); +} + +.translate-x-6 +{ + transform: translateX(3em); +} + +.translate-x-6-n +{ + transform: translateX(-3em); +} + +.translate-x-7-n +{ + transform: translateX(-3.5em); +} + +.translate-x-8-n +{ + transform: translateX(-4em); +} + +/* this is required to physically link label with side bar */ +.mat-drawer-content-overflow-visible > mat-drawer-content, +/* this is required to show the popout info of template and parcellation */ +.mat-drawer-content-overflow-visible > mat-drawer > .mat-drawer-inner-container +{ + overflow: visible!important; +} + +.overflow-visible +{ + overflow: visible!important; +} diff --git a/src/services/effect/effect.ts b/src/services/effect/effect.ts index 603ed7be8..054568c67 100644 --- a/src/services/effect/effect.ts +++ b/src/services/effect/effect.ts @@ -36,8 +36,9 @@ export class UseEffects implements OnDestroy{ withLatestFrom(this.regionsSelected$), map(([action, regionsSelected]) => { const { deselectRegions } = action - const deselectSet = new Set((deselectRegions as any[]).map(r => r.name)) - const selectRegions = regionsSelected.filter(r => !deselectSet.has(r.name)) + const selectRegions = regionsSelected.filter(r => { + return !(deselectRegions as any[]).find(dr => compareRegions(dr, r)) + }) return { type: SELECT_REGIONS, selectRegions @@ -206,4 +207,12 @@ export class UseEffects implements OnDestroy{ updatedParcellation: parcellation })) ) +} + +export const compareRegions: (r1: any,r2: any) => boolean = (r1, r2) => { + if (!r1) return !r2 + if (!r2) return !r1 + return r1.ngId === r2.ngId + && r1.labelIndex === r2.labelIndex + && r1.name === r2.name } \ No newline at end of file diff --git a/src/services/state/uiState.store.ts b/src/services/state/uiState.store.ts index 1caa0f942..80cfaaef4 100644 --- a/src/services/state/uiState.store.ts +++ b/src/services/state/uiState.store.ts @@ -13,7 +13,6 @@ const defaultState : UIStateInterface = { snackbarMessage: null, - sidebarTemplate: null, bottomSheetTemplate: null, /** @@ -87,12 +86,6 @@ export function uiState(state:UIStateInterface = defaultState,action:UIAction){ ...state, agreedKgTos: true } - case SHOW_SIDEBAR_TEMPLATE: - const { sidebarTemplate } = action - return { - ...state, - sidebarTemplate - } case SHOW_BOTTOM_SHEET: const { bottomSheetTemplate } = action return { @@ -121,7 +114,6 @@ export interface UIStateInterface{ agreedCookies: boolean agreedKgTos: boolean - sidebarTemplate: TemplateRef<any> bottomSheetTemplate: TemplateRef<any> } @@ -137,7 +129,6 @@ export interface UIAction extends Action{ }[], snackbarMessage: string - sidebarTemplate: TemplateRef<any> bottomSheetTemplate: TemplateRef<any> } @@ -154,5 +145,4 @@ export const AGREE_KG_TOS = `AGREE_KG_TOS` export const SHOW_KG_TOS = `SHOW_KG_TOS` export const SNACKBAR_MESSAGE = `SNACKBAR_MESSAGE` -export const SHOW_SIDEBAR_TEMPLATE = `SHOW_SIDEBAR_TEMPLATE` export const SHOW_BOTTOM_SHEET = `SHOW_BOTTOM_SHEET` \ No newline at end of file diff --git a/src/ui/databrowserModule/databrowser.module.ts b/src/ui/databrowserModule/databrowser.module.ts index 25b236e98..d2d2644e7 100644 --- a/src/ui/databrowserModule/databrowser.module.ts +++ b/src/ui/databrowserModule/databrowser.module.ts @@ -21,7 +21,7 @@ import { PopoverModule } from "ngx-bootstrap/popover"; import { UtilModule } from "src/util/util.module"; import { AggregateArrayIntoRootPipe } from "./util/aggregateArrayIntoRoot.pipe"; import { KgSingleDatasetService } from "./kgSingleDatasetService.service" -import { SingleDatasetView } from './singleDataset/singleDataset.component' +import { SingleDatasetView } from './singleDataset/detailedView/singleDataset.component' import { AngularMaterialModule } from 'src/ui/sharedModules/angularMaterial.module' import { DoiParserPipe } from "src/util/pipes/doiPipe.pipe"; import { DatasetIsFavedPipe } from "./util/datasetIsFaved.pipe"; @@ -32,6 +32,8 @@ import { ScrollingModule } from "@angular/cdk/scrolling"; import { GetKgSchemaIdFromFullIdPipe } from "./util/getKgSchemaIdFromFullId.pipe"; import { PreviewFileIconPipe } from "./preview/previewFileIcon.pipe"; import { PreviewFileTypePipe } from "./preview/previewFileType.pipe"; +import { SingleDatasetListView } from "./singleDataset/listView/singleDatasetListView.component"; +import { CurrentlySelectedRegions } from "../viewerStateController/currentlySelectedRegions/currentlySelectedRegions.component"; @NgModule({ imports:[ @@ -55,8 +57,10 @@ import { PreviewFileTypePipe } from "./preview/previewFileType.pipe"; LineChart, DedicatedViewer, SingleDatasetView, + SingleDatasetListView, RegionTextSearchAutocomplete, RegionHierarchy, + CurrentlySelectedRegions, /** * pipes @@ -76,12 +80,14 @@ import { PreviewFileTypePipe } from "./preview/previewFileType.pipe"; exports:[ DataBrowser, SingleDatasetView, + SingleDatasetListView, PreviewComponent, ModalityPicker, FilterDataEntriesbyMethods, FileViewer, RegionTextSearchAutocomplete, - RegionHierarchy + RegionHierarchy, + CurrentlySelectedRegions, ], entryComponents:[ DataBrowser, diff --git a/src/ui/databrowserModule/databrowser/databrowser.component.ts b/src/ui/databrowserModule/databrowser/databrowser.component.ts index 8190e7d30..eb67dcfc0 100644 --- a/src/ui/databrowserModule/databrowser/databrowser.component.ts +++ b/src/ui/databrowserModule/databrowser/databrowser.component.ts @@ -3,7 +3,7 @@ import { DataEntry } from "src/services/stateStore.service"; import { Subscription, merge, Observable } from "rxjs"; import { DatabrowserService, CountedDataModality } from "../databrowser.service"; import { ModalityPicker } from "../modalityPicker/modalityPicker.component"; -import { MatDialog } from "@angular/material"; +import { MatDialog, MatExpansionPanel } from "@angular/material"; import { KgSingleDatasetService } from "../kgSingleDatasetService.service"; import { scan, shareReplay } from "rxjs/operators"; import { ViewerPreviewFile } from "src/services/state/dataStore.store"; @@ -22,6 +22,8 @@ const scanFn: (acc: any[], curr: any) => any[] = (acc, curr) => [curr, ...acc] export class DataBrowser implements OnChanges, OnDestroy,OnInit{ + @ViewChild('selectedRegionExpansionPanel') selectedRegionExpansionPanel: MatExpansionPanel + @Input() public regions: any[] = [] @@ -86,6 +88,8 @@ export class DataBrowser implements OnChanges, OnDestroy,OnInit{ ngOnChanges(changes){ + if (this.regions.length === 0) this.selectedRegionExpansionPanel && (this.selectedRegionExpansionPanel.expanded = false) + this.regions = this.regions.map(r => { /** * TODO to be replaced with properly region UUIDs from KG @@ -98,6 +102,9 @@ export class DataBrowser implements OnChanges, OnDestroy,OnInit{ 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. @@ -171,6 +178,7 @@ export class DataBrowser implements OnChanges, OnDestroy,OnInit{ handleModalityFilterEvent(modalityFilter:CountedDataModality[]){ this.countedDataM = modalityFilter this.visibleCountedDataM = modalityFilter.filter(dm => dm.visible) + this.cdr.markForCheck() this.resetCurrentPage() } @@ -215,6 +223,10 @@ export class DataBrowser implements OnChanges, OnDestroy,OnInit{ this.focusedDataset = dataset this.dialog.open(this.detailDatasetTemplateRef) } + + trackbyFn(index:number, dataset:DataEntry) { + return dataset.id + } } export interface DataEntryFilter{ diff --git a/src/ui/databrowserModule/databrowser/databrowser.style.css b/src/ui/databrowserModule/databrowser/databrowser.style.css index 1e2d2b616..831c351d9 100644 --- a/src/ui/databrowserModule/databrowser/databrowser.style.css +++ b/src/ui/databrowserModule/databrowser/databrowser.style.css @@ -1,167 +1,9 @@ -div[heading] -{ - padding : 0.5em 1em; - white-space: nowrap; - overflow: hidden; -} - -div[heading][displayflex] -{ - display:flex; - flex-direction: row; -} - - div[heading][displayflex] > [maintext] - { - flex : 1 1 0px; - white-space: nowrap; - overflow: hidden; - } - - div[heading][displayflex] > [propertyicons] - { - flex : 0 0 auto; - } - - div[heading][displayflex] > [propertyicons] > * - { - margin-left:0.5em; - } - -[dataentry] -{ - width:100%; -} - -[dataentry] div[heading] -{ - padding : 0.4em 1.6em; - background-color:rgba(0,0,0,0.2); -} - -[dataentry] div[body] -{ - padding : 0.4em 2.0em; - background-color:rgba(0,0,0,0.3); -} - -div[databrowserheader] -{ - padding : 0.5em 1.0em; - background-color:rgba(128,128,128,0.05); -} - -div[databrowserheader] > * -{ - padding : 0; - margin : 0 ; - border : 0 ; -} - -div[databrowserheader] -{ - white-space: nowrap; -} - -div[filterBlock] > * -{ - white-space: nowrap; - overflow: hidden; -} - -.clickable:hover -{ - color:#dbb556; - cursor:default; -} - -div.unclickable -{ - color:rgba(128,128,128,0.8); -} - -div.unclickable:hover -{ - cursor:default; -} - -div.noResultClass -{ - color:rgba(128,128,128,0.8); - text-decoration: line-through; -} - -div[spatialSearchCell] -{ - padding: 0.3em 1em; - overflow:hidden; - white-space: nowrap; -} - -div[noSelectedRegion] -{ - padding: 1em 1em; - font-size:2em; - color: rgba(128,128,128,0.5); - max-height: 6em; -} - -:host-context([darktheme="true"]) div[noSelectedRegion] -{ - background-color: rgba(0,0,0,0.2); -} - -div[regionsContainer] -{ - background-color:rgba(0, 0, 0, 0.3); - padding:0.5em 0.7em; -} - -div[regionContainer] -{ - margin-top:0.5em; -} - modality-picker { font-size: 90%; display:inline-block; } -.spinnerAnimationCircleContainer -{ - display: flex; - flex-direction: column; - align-items: center; -} - -.spinnerAnimationCircleContainer > .spinnerAnimationCircle -{ - font-size: 300%; -} - -.parcellationSelectionWrapper -{ - width: 100%; - display:flex; - flex-direction: row; - align-items: flex-start; -} -.parcellationSelectionWrapper > *:first-child -{ - flex: 1 0 0; -} -.parcellationSelectionWrapper > *:last-child -{ - flex: 0 0 0; -} - -.toggleParcellationBtn -{ - margin-left: -2.5em; - z-index: 1; -} - radio-list { display: block; @@ -173,33 +15,7 @@ radio-list width: 100%; } -/* datawrapper */ -:host .dataEntryWrapper -{ - white-space: nowrap; - overflow: hidden; - width:200%; - padding: 0; - transition: transform 190ms ease; -} - -.dataEntryWrapper > * -{ - vertical-align: top; - white-space: initial; - width: 50%; - display:inline-block; -} - -.filePreviewContainer -{ - max-height: 100%; - overflow:auto; -} - - -div[regionTagsContainer] +cdk-virtual-scroll-viewport { - max-height: 4em; - overflow:hidden; + width: calc(100% + 1em); } \ No newline at end of file diff --git a/src/ui/databrowserModule/databrowser/databrowser.template.html b/src/ui/databrowserModule/databrowser/databrowser.template.html index 7e802978d..90a524586 100644 --- a/src/ui/databrowserModule/databrowser/databrowser.template.html +++ b/src/ui/databrowserModule/databrowser/databrowser.template.html @@ -1,102 +1,59 @@ -<mat-tab-group [dynamicHeight]="false" class="h-100 w-100 overflow-hidden"> - <mat-tab label="Search Criteria"> - <ng-container *ngTemplateOutlet="searchPanel"> +<mat-card + [ngClass]="{'h-100': (!fetchingFlag && !fetchError)}" + class="w-100 overflow-hidden d-block flex-grow-1 flex-shrink-1 d-flex flex-column"> - </ng-container> - </mat-tab> - <mat-tab label="History"> - <ng-container *ngTemplateOutlet="history"> + <!-- transclusion header --> + <mat-card-header> + <ng-content select="[card-header]"> + + </ng-content> + </mat-card-header> + + <!-- transclusion header --> + <ng-content select="[card-content='prepend']"> + </ng-content> + + <!-- modality filter --> + <ng-container *ngTemplateOutlet="modalitySelector"> + </ng-container> + <!-- if still loading, show spinner --> + <ng-template [ngIf]="fetchingFlag"> + <ng-container *ngTemplateOutlet="loadingSpinner"> </ng-container> - </mat-tab> -</mat-tab-group> + </ng-template> -<ng-template #searchPanel> + <!-- else, show fetched --> + <ng-template [ngIf]="!fetchingFlag"> - <div class="h-100 w-100 d-flex flex-column overflow-hidden"> + <!-- if error, show error only --> + <ng-template [ngIf]="fetchError"> + <ng-container *ngTemplateOutlet="errorTemplate"> + </ng-container> + </ng-template> + + <!-- if not error, show dataset template --> - <!-- search criterial --> - <mat-selection-list class="flex-grow-0 flex-shrink-0 mb-2" checkboxPosition="before"> - <h3 mat-subheader> - Search criteria - </h3> - - <!-- selected regions --> - <mat-list-option - selected="true" - checkboxPosition="before"> - <h4 mat-line> - {{ regions.length }} selected region{{ regions.length !== 1 ? 's' : ''}} - - <!-- stop mousedown propagation to avoid ripple from mat-list-option --> - <region-text-search-autocomplete (mousedown)="$event.stopPropagation()" (click)="$event.stopPropagation()" [showAutoComplete]="false"> - </region-text-search-autocomplete> - </h4> - </mat-list-option> - - <mat-list-option - *ngIf="dbService.viewportBoundingBox$ | async as bbox" - checkboxPosition="before" - [selected]="true"> - <h4 class="mat-line"> - viewport bounding box - </h4> - - <!-- from --> - <p class="mat-line"> - <small> - <span *ngFor="let v of bbox[0]; let lastval = last"> - {{ v | number : '1.2-2' }}<span *ngIf="!lastval">, </span> - </span> - <span> - to - </span> - <span *ngFor="let v of bbox[1]; let lastval = last"> - {{ v | number : '1.2-2' }}<span *ngIf="!lastval">, </span> - </span> - </small> - </p> - - </mat-list-option> - </mat-selection-list> - - <mat-divider class="position-relative"></mat-divider> - - <!-- modality picker / filter --> - <mat-accordion class="flex-grow-0 flex-shrink-0 mb-2"> - <mat-expansion-panel> - <mat-expansion-panel-header> - <mat-panel-title> - Filter - </mat-panel-title> - <mat-panel-description> - <i *ngIf="dataentries.length > 0"> - <span *ngIf="visibleCountedDataM && visibleCountedDataM.length > 0 "> - {{ (dataentries | filterDataEntriesByMethods : visibleCountedDataM).length }} filtered / - </span> - {{ dataentries.length }} results - </i> - <i *ngIf="dataentries.length === 0"> - No results to show. - </i> - </mat-panel-description> - </mat-expansion-panel-header> - - <ng-container *ngTemplateOutlet="modalityPicker"> - - </ng-container> - </mat-expansion-panel> - </mat-accordion> - - <!-- datasets container --> - <div *ngIf="fetchingFlag; else fetched" - class="spinnerAnimationCircleContainer"> - <div class="spinnerAnimationCircle"></div> - <div>Fetching datasets...</div> + <ng-template [ngIf]="!fetchError"> + <ng-container *ngTemplateOutlet="datasetTemplate"> + </ng-container> + </ng-template> + </ng-template> + + <!-- footer, populated by content transclusion --> + <mat-card-footer> + <ng-content select="[card-footer]"> + </ng-content> + </mat-card-footer> +</mat-card> + +<ng-template #loadingSpinner> + <mat-card-content> + <div class="m-2 d-flex flex-row align-items-center justify-content star"> + <div class="d-inline-block mr-2 spinnerAnimationCircle"></div> + <span>Fetching datasets...</span> </div> - - </div> - + </mat-card-content> </ng-template> <ng-template #modalityPicker> @@ -109,35 +66,35 @@ </modality-picker> </ng-template> -<ng-template #fetched> - <div class="ml-2 mr-2 alert alert-danger" *ngIf="fetchError; else showData"> - <i class="fas fa-exclamation-triangle"></i> Error fetching data. <a href="#" (click)="retryFetchData($event)" class="btn btn-link text-info">retry</a> - </div> +<ng-template #errorTemplate> + <mat-card-content> + <div class="ml-2 mr-2 alert alert-danger"> + <i class="fas fa-exclamation-triangle"></i> Error fetching data. <a href="#" (click)="retryFetchData($event)" class="btn btn-link text-info">retry</a> + </div> + </mat-card-content> </ng-template> -<ng-template #showData> +<ng-template #datasetTemplate> <!-- datawrapper --> - <ng-container *ngIf="dataentries | filterDataEntriesByMethods : visibleCountedDataM as filteredDataEntry"> - <!-- dataentries --> - <div class="h-100 w-100"> - <cdk-virtual-scroll-viewport + <ng-container *ngIf="dataentries | filterDataEntriesByMethods : visibleCountedDataM as filteredDataEntry"> + <mat-card-content class="h-100 w-100 overflow-hidden"> + <cdk-virtual-scroll-viewport class="h-100" - itemSize="50"> - <single-dataset-view - *cdkVirtualFor="let dataset of filteredDataEntry" - (click)="showFocusedDataset(dataset)" - class="m-2" - [kgSchema]="(dataset.fullId | getKgSchemaIdFromFullIdPipe)[0]" - [kgId]="(dataset.fullId | getKgSchemaIdFromFullIdPipe)[1]" - [prefetched]="dataset" - [simpleMode]="true" - [ripple]="true"> - - </single-dataset-view> - </cdk-virtual-scroll-viewport> - </div> - + itemSize="40"> + <mat-nav-list dense> + <single-dataset-list-view + *cdkVirtualFor="let dataset of filteredDataEntry; trackBy: trackbyFn; templateCacheSize: 0" + (click)="showFocusedDataset(dataset)" + [kgSchema]="(dataset.fullId | getKgSchemaIdFromFullIdPipe)[0]" + [kgId]="(dataset.fullId | getKgSchemaIdFromFullIdPipe)[1]" + [dataset]="dataset" + [ripple]="true"> + + </single-dataset-list-view> + </mat-nav-list> + </cdk-virtual-scroll-viewport> + </mat-card-content> </ng-container> </ng-template> @@ -150,13 +107,63 @@ </ng-template> <ng-template #detailDataset> + <single-dataset-view [dataset]="focusedDataset"> + </single-dataset-view> +</ng-template> - <single-dataset-view - [prefetched]="focusedDataset"> +<!-- modality picker / filter --> +<ng-template #modalitySelector> + <mat-accordion class="flex-grow-0 flex-shrink-0"> + + <!-- currently selected regions --> + <mat-expansion-panel + [disabled]="regions.length === 0" + #selectedRegionExpansionPanel + hideToggle> + <mat-expansion-panel-header> + <mat-panel-title> + {{ regions.length > 0 ? regions.length : 'No' }} region{{ regions.length > 1 ? 's' : '' }} selected + </mat-panel-title> + + <mat-panel-description class="d-flex flex-row justify-content-end align-items-center"> + <i class="fas fa-brain"></i> + </mat-panel-description> + </mat-expansion-panel-header> + + <div class="h-10em"> + <currently-selected-regions class="h-100 d-block"> + </currently-selected-regions> + </div> + </mat-expansion-panel> + + <!-- Filters --> + <mat-expansion-panel hideToggle> + + <mat-expansion-panel-header class="align-items-center"> + <mat-panel-title> + <span *ngIf="visibleCountedDataM && visibleCountedDataM.length > 0; else defaultCount"> + {{ (dataentries | filterDataEntriesByMethods : visibleCountedDataM).length }} filtered datasets + </span> + <ng-template #defaultCount> + {{ dataentries.length }} related datasets + </ng-template> + </mat-panel-title> + + <mat-panel-description class="d-flex flex-row justify-content-end align-items-center"> + <i class="fas fa-filter"></i> + </mat-panel-description> + + </mat-expansion-panel-header> + + <ng-container *ngTemplateOutlet="modalityPicker"> + </ng-container> + + </mat-expansion-panel> + </mat-accordion> - </single-dataset-view> </ng-template> - + +<!-- currently unused --> <ng-template #history> <mat-list> <mat-list-item *ngFor="let item of (history$ | async)"> diff --git a/src/ui/databrowserModule/singleDataset/detailedView/singleDataset.component.ts b/src/ui/databrowserModule/singleDataset/detailedView/singleDataset.component.ts new file mode 100644 index 000000000..29f499577 --- /dev/null +++ b/src/ui/databrowserModule/singleDataset/detailedView/singleDataset.component.ts @@ -0,0 +1,28 @@ +import { Component, ChangeDetectionStrategy, ChangeDetectorRef} from "@angular/core"; +import { + SingleDatasetBase, + DatabrowserService, + KgSingleDatasetService, + AtlasViewerConstantsServices +} from "../singleDataset.base"; + +@Component({ + selector: 'single-dataset-view', + templateUrl: './singleDataset.template.html', + styleUrls: [ + `./singleDataset.style.css` + ], + changeDetection: ChangeDetectionStrategy.OnPush +}) + +export class SingleDatasetView extends SingleDatasetBase{ + + constructor( + dbService: DatabrowserService, + singleDatasetService: KgSingleDatasetService, + cdr: ChangeDetectorRef, + constantService: AtlasViewerConstantsServices + ){ + super(dbService, singleDatasetService, cdr, constantService) + } +} diff --git a/src/ui/databrowserModule/singleDataset/singleDataset.style.css b/src/ui/databrowserModule/singleDataset/detailedView/singleDataset.style.css similarity index 100% rename from src/ui/databrowserModule/singleDataset/singleDataset.style.css rename to src/ui/databrowserModule/singleDataset/detailedView/singleDataset.style.css diff --git a/src/ui/databrowserModule/singleDataset/singleDataset.template.html b/src/ui/databrowserModule/singleDataset/detailedView/singleDataset.template.html similarity index 100% rename from src/ui/databrowserModule/singleDataset/singleDataset.template.html rename to src/ui/databrowserModule/singleDataset/detailedView/singleDataset.template.html diff --git a/src/ui/databrowserModule/singleDataset/listView/singleDatasetListView.component.ts b/src/ui/databrowserModule/singleDataset/listView/singleDatasetListView.component.ts new file mode 100644 index 000000000..1f7811c6e --- /dev/null +++ b/src/ui/databrowserModule/singleDataset/listView/singleDatasetListView.component.ts @@ -0,0 +1,27 @@ +import { Component,ChangeDetectionStrategy, ChangeDetectorRef } from "@angular/core";import { + SingleDatasetBase, + DatabrowserService, + KgSingleDatasetService, + AtlasViewerConstantsServices +} from "../singleDataset.base"; + +@Component({ + selector: 'single-dataset-list-view', + templateUrl: './singleDatasetListView.template.html', + styleUrls: [ + './singleDatasetListView.style.css' + ], + changeDetection: ChangeDetectionStrategy.OnPush +}) + +export class SingleDatasetListView extends SingleDatasetBase { + + constructor( + dbService: DatabrowserService, + singleDatasetService: KgSingleDatasetService, + cdr: ChangeDetectorRef, + constantService: AtlasViewerConstantsServices + ){ + super(dbService, singleDatasetService, cdr, constantService) + } +} \ No newline at end of file diff --git a/src/ui/databrowserModule/singleDataset/listView/singleDatasetListView.style.css b/src/ui/databrowserModule/singleDataset/listView/singleDatasetListView.style.css new file mode 100644 index 000000000..e69de29bb diff --git a/src/ui/databrowserModule/singleDataset/listView/singleDatasetListView.template.html b/src/ui/databrowserModule/singleDataset/listView/singleDatasetListView.template.html new file mode 100644 index 000000000..a98815d99 --- /dev/null +++ b/src/ui/databrowserModule/singleDataset/listView/singleDatasetListView.template.html @@ -0,0 +1,55 @@ +<mat-list-item [matTooltip]="name"> + + <small mat-line> + {{ name }} + </small> + + <!-- preview --> + <button mat-icon-button + *ngIf="preview" + iav-stop="click mousedown" + (click)="showPreviewList(previewFilesListTemplate)"> + <i class="far fa-eye"></i> + </button> + + <button mat-icon-button> + <i class="fas fa-info"></i> + </button> +</mat-list-item> + +<ng-template #previewFilesListTemplate> + <preview-component + (previewFile)="handlePreviewFile($event)" + [datasetName]="name"> + </preview-component> +</ng-template> + +<ng-template #fullIcons> + + <!-- references --> + <a *ngFor="let kgRef of kgReference" + [href]="kgRef | doiParserPipe" + target="_blank" + iav-stop="click mousedown" + mat-icon-button> + <mat-icon fontSet="fas" fontIcon="fa-external-link-alt"></mat-icon> + </a> + + <!-- pin dataset --> + <button mat-icon-button + *ngIf="downloadEnabled" + iav-stop="click mousedown" + (click)="toggleFav()" + [color]="(favedDataentries$ | async | datasetIsFaved : dataset) ? 'primary' : 'basic'"> + <i class="fas fa-thumbtack"></i> + </button> + + <!-- download dataset --> + <button mat-icon-button + *ngIf="downloadEnabled" + iav-stop="click mousedown" + (click)="downloadZipFromKg()" + [disabled]="downloadInProgress"> + <i class="fas" [ngClass]="!downloadInProgress? 'fa-download' :'fa-spinner fa-pulse'"></i> + </button> +</ng-template> \ No newline at end of file diff --git a/src/ui/databrowserModule/singleDataset/singleDataset.component.ts b/src/ui/databrowserModule/singleDataset/singleDataset.base.ts similarity index 87% rename from src/ui/databrowserModule/singleDataset/singleDataset.component.ts rename to src/ui/databrowserModule/singleDataset/singleDataset.base.ts index 3cb41fc60..034d7e250 100644 --- a/src/ui/databrowserModule/singleDataset/singleDataset.component.ts +++ b/src/ui/databrowserModule/singleDataset/singleDataset.base.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnInit, ChangeDetectionStrategy, ChangeDetectorRef, TemplateRef, Output, EventEmitter } from "@angular/core"; +import { Input, OnInit, ChangeDetectorRef, TemplateRef, Output, EventEmitter } from "@angular/core"; import { KgSingleDatasetService } from "../kgSingleDatasetService.service"; import { Publication, File, DataEntry, ViewerPreviewFile } from 'src/services/state/dataStore.store' import { AtlasViewerConstantsServices } from "src/atlasViewer/atlasViewer.constantService.service"; @@ -6,15 +6,14 @@ import { HumanReadableFileSizePipe } from "src/util/pipes/humanReadableFileSize. import { DatabrowserService } from "../databrowser.service"; import { Observable } from "rxjs"; -@Component({ - selector: 'single-dataset-view', - templateUrl: './singleDataset.template.html', - styleUrls: [ - `./singleDataset.style.css` - ], - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class SingleDatasetView implements OnInit { +export { + DatabrowserService, + KgSingleDatasetService, + ChangeDetectorRef, + AtlasViewerConstantsServices +} + +export class SingleDatasetBase implements OnInit { @Input() ripple: boolean = false @@ -29,7 +28,7 @@ export class SingleDatasetView implements OnInit { @Input() kgSchema?: string @Input() kgId?: string - @Input() prefetched: any = null + @Input() dataset: any = null @Input() simpleMode: boolean = false @Output() previewingFile: EventEmitter<ViewerPreviewFile> = new EventEmitter() @@ -54,8 +53,6 @@ export class SingleDatasetView implements OnInit { public downloadInProgress = false public favedDataentries$: Observable<DataEntry[]> - public dataset: any - constructor( private dbService: DatabrowserService, private singleDatasetService: KgSingleDatasetService, @@ -66,9 +63,9 @@ export class SingleDatasetView implements OnInit { } ngOnInit() { - const { kgId, kgSchema, prefetched } = this - if ( prefetched ) { - const { name, description, kgReference, publications, files, preview, ...rest } = prefetched + const { kgId, kgSchema, dataset } = this + if ( dataset ) { + const { name, description, kgReference, publications, files, preview, ...rest } = dataset this.name = name this.description = description this.kgReference = kgReference @@ -76,7 +73,6 @@ export class SingleDatasetView implements OnInit { this.files = files this.preview = preview - this.dataset = prefetched return } if (!kgSchema || !kgId) return diff --git a/src/ui/databrowserModule/util/datasetIsFaved.pipe.ts b/src/ui/databrowserModule/util/datasetIsFaved.pipe.ts index 2befe6fed..35ec1bea3 100644 --- a/src/ui/databrowserModule/util/datasetIsFaved.pipe.ts +++ b/src/ui/databrowserModule/util/datasetIsFaved.pipe.ts @@ -6,6 +6,7 @@ import { DataEntry } from "src/services/stateStore.service"; }) export class DatasetIsFavedPipe implements PipeTransform{ public transform(favedDataEntry: DataEntry[], dataentry: DataEntry):boolean{ + if (!dataentry) return false return favedDataEntry.findIndex(ds => ds.id === dataentry.id) >= 0 } } \ No newline at end of file diff --git a/src/ui/menuicons/menuicons.component.ts b/src/ui/menuicons/menuicons.component.ts index e71f41c69..2c1da620c 100644 --- a/src/ui/menuicons/menuicons.component.ts +++ b/src/ui/menuicons/menuicons.component.ts @@ -21,7 +21,6 @@ import { PluginServices, PluginManifest } from "src/atlasViewer/atlasViewer.plug import { Store, select } from "@ngrx/store"; import { Observable, combineLatest, Subscription } from "rxjs"; import { map, shareReplay, startWith } from "rxjs/operators"; -import { SHOW_SIDEBAR_TEMPLATE } from "src/services/state/uiState.store"; import { LayerBrowser } from "../layerbrowser/layerbrowser.component"; import { MatDialogRef, MatDialog } from "@angular/material"; import { NgLayerInterface } from "src/atlasViewer/atlasViewer.component"; @@ -166,19 +165,6 @@ export class MenuIconsBar implements OnInit, OnDestroy { } ngOnInit(){ - /** - * on opening nifti volume, collapse side bar - */ - this.subscriptions.push( - this.singleDatasetService.previewingFile$.subscribe(({ file }) => { - if (determinePreviewFileType(file) === PREVIEW_FILE_TYPES.NIFTI) { - this.store.dispatch({ - type: SHOW_SIDEBAR_TEMPLATE, - sidebarTemplate: null - }) - } - }) - ) } ngOnDestroy(){ @@ -263,10 +249,7 @@ export class MenuIconsBar implements OnInit, OnDestroy { } public showKgSearchSideNav(kgSearchTemplate: TemplateRef<any> = null){ - this.store.dispatch({ - type: SHOW_SIDEBAR_TEMPLATE, - sidebarTemplate: kgSearchTemplate - }) + } handleNonbaseLayerEvent(layers: NgLayerInterface[]){ diff --git a/src/ui/menuicons/menuicons.template.html b/src/ui/menuicons/menuicons.template.html index aa83120e4..2db3ad3d3 100644 --- a/src/ui/menuicons/menuicons.template.html +++ b/src/ui/menuicons/menuicons.template.html @@ -1,6 +1,4 @@ <div class="w-0 ml-4 d-flex flex-column align-items-start" root> - <logo-container *ngIf="!isMobile"> - </logo-container> <!-- hide icons when templates has yet been selected --> <ng-template [ngIf]="selectedTemplate$ | async"> diff --git a/src/ui/searchSideNav/searchSideNav.component.ts b/src/ui/searchSideNav/searchSideNav.component.ts new file mode 100644 index 000000000..d1629dbd8 --- /dev/null +++ b/src/ui/searchSideNav/searchSideNav.component.ts @@ -0,0 +1,77 @@ +import { Component, Output, EventEmitter, OnInit, OnDestroy } from "@angular/core"; +import { MatDialogRef, MatDialog } from "@angular/material"; +import { NgLayerInterface } from "src/atlasViewer/atlasViewer.component"; +import { LayerBrowser } from "../layerbrowser/layerbrowser.component"; +import { Observable, Subscription } from "rxjs"; +import { Store, select } from "@ngrx/store"; +import { map, startWith, scan, filter, mapTo } from "rxjs/operators"; + +@Component({ + selector: 'search-side-nav', + templateUrl: './searchSideNav.template.html', + styleUrls:[ + './searchSideNav.style.css' + ] +}) + +export class SearchSideNav implements OnInit, OnDestroy { + public showDataset: boolean = false + public availableDatasets: number = 0 + + private subscriptions: Subscription[] = [] + private layerBrowserDialogRef: MatDialogRef<any> + + @Output() dismiss: EventEmitter<any> = new EventEmitter() + @Output() open: EventEmitter<any> = new EventEmitter() + + public autoOpenSideNav$: Observable<any> + + constructor( + private dialog: MatDialog, + store$: Store<any> + ){ + this.autoOpenSideNav$ = store$.pipe( + select('viewerState'), + select('regionsSelected'), + map(arr => arr.length), + startWith(0), + scan((acc, curr) => [curr, ...acc], []), + filter(([curr, prev]) => prev === 0 && curr > 0), + mapTo(true) + ) + } + + ngOnInit(){ + this.subscriptions.push( + this.autoOpenSideNav$.subscribe(() => { + this.open.emit(true) + this.showDataset = true + }) + ) + } + + ngOnDestroy(){ + while(this.subscriptions.length > 0) { + this.subscriptions.pop().unsubscribe() + } + } + + handleNonbaseLayerEvent(layers: NgLayerInterface[]){ + if (layers.length === 0) { + this.layerBrowserDialogRef && this.layerBrowserDialogRef.close() + this.layerBrowserDialogRef = null + return + } + if (this.layerBrowserDialogRef) return + + this.dismiss.emit(true) + this.layerBrowserDialogRef = this.dialog.open(LayerBrowser, { + hasBackdrop: false, + autoFocus: false, + position: { + top: '1em' + }, + disableClose: true + }) + } +} \ No newline at end of file diff --git a/src/ui/searchSideNav/searchSideNav.style.css b/src/ui/searchSideNav/searchSideNav.style.css new file mode 100644 index 000000000..e69de29bb diff --git a/src/ui/searchSideNav/searchSideNav.template.html b/src/ui/searchSideNav/searchSideNav.template.html new file mode 100644 index 000000000..10d9c6953 --- /dev/null +++ b/src/ui/searchSideNav/searchSideNav.template.html @@ -0,0 +1,60 @@ +<div class="d-flex flex-column h-100"> + <viewer-state-controller class="pe-all mb-2" #viewerStateController> + + <!-- content append --> + <ng-container card-content="append"> + <mat-card-content (focusin)="showDataset = true"> + <region-text-search-autocomplete + [showBadge]="true" + class="d-block w-100"> + </region-text-search-autocomplete> + </mat-card-content> + </ng-container> + + <!-- footer content --> + <div class="d-flex flex-row justify-content-center" card-footer> + <button mat-stroked-button + *ngIf="!showDataset" + (click)="showDataset = true" + class="m-1 flex-grow-1 overflow-hidden" > + <i class="fas fa-chevron-down"></i> + <ng-container *ngIf="viewerStateController.regionsSelected$ | async as regionsSelected"> + {{ regionsSelected.length === 0 ? 'Explore the current view' : regionsSelected.length === 1 ? ('Explore ' + regionsSelected[0].name) : ('Explore selected regions (' + regionsSelected.length + ' selected)') }} + </ng-container> + </button> + </div> + </viewer-state-controller> + + <data-browser + class="pe-all" + *ngIf="showDataset" + [template]="viewerStateController.templateSelected$ | async" + [parcellation]="viewerStateController.parcellationSelected$ | async" + [regions]="viewerStateController.regionsSelected$ | async" + (dataentriesUpdated)="availableDatasets = $event.length"> + + + <!-- content prepend --> + <ng-container *ngIf="viewerStateController.regionsSelected$ | async as regionsSelected" card-content="prepend"> + <mat-card-content> + <p class="font-weight-bold"> + {{ regionsSelected.length === 0 ? 'In the current view' : regionsSelected.length === 1 ? regionsSelected[0].name : 'Multi-region selection' }} + </p> + </mat-card-content> + </ng-container> + + <!-- footer content --> + <div class="d-flex flex-row justify-content-center" card-footer> + <button mat-stroked-button + class="m-1" + (click)="showDataset = false" > + <i class="fas fa-chevron-up"></i> + </button> + </div> + </data-browser> +</div> + +<div [hidden]> + <layer-browser (nonBaseLayersChanged)="handleNonbaseLayerEvent($event)" #layerBrowser> + </layer-browser> +</div> \ No newline at end of file diff --git a/src/ui/ui.module.ts b/src/ui/ui.module.ts index 8ffb79888..884c73ce5 100644 --- a/src/ui/ui.module.ts +++ b/src/ui/ui.module.ts @@ -67,6 +67,7 @@ import {ElementOutClickDirective} from "src/util/directives/elementOutClick.dire import {SearchItemPreviewComponent} from "src/ui/searchItemPreview/searchItemPreview.component"; import {SelectedRegionsComponent} from "src/ui/selectedRegions/selectedRegions.component"; import {FilterWithStringPipe} from "src/util/pipes/filterWithString.pipe"; +import { SearchSideNav } from "./searchSideNav/searchSideNav.component"; @NgModule({ imports : [ @@ -106,10 +107,12 @@ import {FilterWithStringPipe} from "src/util/pipes/filterWithString.pipe"; SinglePanel, CurrentLayout, ViewerStateController, + MaximmisePanelButton, SearchPanel, SearchItemPreviewComponent, SelectedRegionsComponent, + SearchSideNav, /* pipes */ GroupDatasetByRegion, @@ -170,7 +173,8 @@ import {FilterWithStringPipe} from "src/util/pipes/filterWithString.pipe"; KGToS, StatusCardComponent, SearchPanel, - ElementOutClickDirective + ElementOutClickDirective, + SearchSideNav, ] }) diff --git a/src/ui/viewerStateController/currentlySelectedRegions/currentlySelectedRegions.component.ts b/src/ui/viewerStateController/currentlySelectedRegions/currentlySelectedRegions.component.ts new file mode 100644 index 000000000..ca1231d80 --- /dev/null +++ b/src/ui/viewerStateController/currentlySelectedRegions/currentlySelectedRegions.component.ts @@ -0,0 +1,46 @@ +import { Component } from "@angular/core"; +import { Store, select } from "@ngrx/store"; +import { Observable } from "rxjs"; +import { distinctUntilChanged, startWith } from "rxjs/operators"; +import { DESELECT_REGIONS } from "src/services/state/viewerState.store"; +import { VIEWERSTATE_ACTION_TYPES } from "../viewerState.component"; + +@Component({ + selector: 'currently-selected-regions', + templateUrl: './currentlySelectedRegions.template.html', + styleUrls: [ + './currentlySelectedRegions.style.css' + ] +}) + +export class CurrentlySelectedRegions { + + + public regionSelected$: Observable<any[]> + + constructor( + private store$: Store<any> + ){ + + this.regionSelected$ = this.store$.pipe( + select('viewerState'), + select('regionsSelected'), + startWith([]), + distinctUntilChanged() + ) + } + + public deselectRegion(event: MouseEvent, region: any){ + this.store$.dispatch({ + type: DESELECT_REGIONS, + deselectRegions: [region] + }) + } + + public gotoRegion(event: MouseEvent, region:any){ + this.store$.dispatch({ + type: VIEWERSTATE_ACTION_TYPES.DOUBLE_CLICK_ON_REGIONHIERARCHY, + payload: { region } + }) + } +} \ No newline at end of file diff --git a/src/ui/viewerStateController/currentlySelectedRegions/currentlySelectedRegions.style.css b/src/ui/viewerStateController/currentlySelectedRegions/currentlySelectedRegions.style.css new file mode 100644 index 000000000..0a4edb055 --- /dev/null +++ b/src/ui/viewerStateController/currentlySelectedRegions/currentlySelectedRegions.style.css @@ -0,0 +1,4 @@ +mat-chip-list >>> .mat-chip-list-wrapper +{ + height: 100%; +} diff --git a/src/ui/viewerStateController/currentlySelectedRegions/currentlySelectedRegions.template.html b/src/ui/viewerStateController/currentlySelectedRegions/currentlySelectedRegions.template.html new file mode 100644 index 000000000..d1bf21a6f --- /dev/null +++ b/src/ui/viewerStateController/currentlySelectedRegions/currentlySelectedRegions.template.html @@ -0,0 +1,24 @@ +<mat-chip-list class="d-block w-100 h-100"> + <cdk-virtual-scroll-viewport + class="w-100 h-100 overflow-x-hidden" + [itemSize]="32"> + <mat-chip + *cdkVirtualFor="let region of (regionSelected$ | async)" + class="w-90"> + <span class="flex-grow-1 flex-shrink-1 text-truncate"> + {{ region.name }} + </span> + <button + *ngIf="region.position" + (click)="gotoRegion($event, region)" + mat-icon-button> + <i class="fas fa-map-marked-alt"></i> + </button> + <button + (click)="deselectRegion($event, region)" + mat-icon-button> + <i class="fas fa-trash"></i> + </button> + </mat-chip> + </cdk-virtual-scroll-viewport> +</mat-chip-list> \ No newline at end of file diff --git a/src/ui/viewerStateController/regionHierachy/regionHierarchy.style.css b/src/ui/viewerStateController/regionHierachy/regionHierarchy.style.css index 483444c3d..20624e141 100644 --- a/src/ui/viewerStateController/regionHierachy/regionHierarchy.style.css +++ b/src/ui/viewerStateController/regionHierachy/regionHierarchy.style.css @@ -25,7 +25,7 @@ div[treeContainer] [hideScrollbarcontainer] [hideScrollbarInnerContainer] { - width: calc(100% + 8em); + width: calc(100% + 2em); } input[type="text"] @@ -54,9 +54,3 @@ input[type="text"] { flex: 0 0 auto; } - -mat-chip-list >>> .mat-chip-list-wrapper -{ - height: 100%; - width:110%; -} \ No newline at end of file diff --git a/src/ui/viewerStateController/regionHierachy/regionHierarchy.template.html b/src/ui/viewerStateController/regionHierachy/regionHierarchy.template.html index 308e53b6b..0e4d9475b 100644 --- a/src/ui/viewerStateController/regionHierachy/regionHierarchy.template.html +++ b/src/ui/viewerStateController/regionHierachy/regionHierarchy.template.html @@ -26,34 +26,10 @@ </div> <div hideScrollbarcontainer> - <mat-chip-list - class="w-100 flex-grow-1 flex-shrink-1"> - <cdk-virtual-scroll-viewport - hideScrollbarInnerContainer - class="h-100 p-4 overflow-x-hidden" - [itemSize]="32"> - <mat-chip - *cdkVirtualFor="let region of (selectedRegions | filterRowsByVisbilityPipe : null : filterTreeBySearch)" - class="w-90"> - - <span class="dot mr-1 flex-grow-0 flex-shrink-0" [ngStyle]="{backgroundColor: (region | regionBackgroundToRgbPipe)}"></span> - <span class="flex-grow-1 flex-shrink-1 text-truncate"> - {{ region.name }} - </span> - <button - *ngIf="region.position" - (click)="gotoRegion(region)" - mat-icon-button> - <i class="fas fa-map-marked-alt"></i> - </button> - <button - (click)="deselectRegion(region)" - mat-icon-button> - <i class="fas fa-trash"></i> - </button> - </mat-chip> - </cdk-virtual-scroll-viewport> - </mat-chip-list> + <currently-selected-regions + class="d-block h-100" + hideScrollbarInnerContainer> + </currently-selected-regions> </div> </div> diff --git a/src/ui/viewerStateController/regionSearch/regionSearch.component.ts b/src/ui/viewerStateController/regionSearch/regionSearch.component.ts index 379923331..40ae397d4 100644 --- a/src/ui/viewerStateController/regionSearch/regionSearch.component.ts +++ b/src/ui/viewerStateController/regionSearch/regionSearch.component.ts @@ -1,7 +1,7 @@ import { Component, EventEmitter, Output, ViewChild, ElementRef, TemplateRef, Input } from "@angular/core"; import { Store, select } from "@ngrx/store"; import { Observable } from "rxjs"; -import { map, distinctUntilChanged, startWith, withLatestFrom, debounceTime, shareReplay, take } from "rxjs/operators"; +import { map, distinctUntilChanged, startWith, withLatestFrom, debounceTime, shareReplay, take, filter } from "rxjs/operators"; import { getMultiNgIdsRegionsLabelIndexMap, generateLabelIndexId } from "src/services/stateStore.service"; import { FormControl } from "@angular/forms"; import { MatAutocompleteSelectedEvent, MatDialog } from "@angular/material"; @@ -20,8 +20,8 @@ const filterRegionBasedOnText = searchTerm => region => region.name.toLowerCase( export class RegionTextSearchAutocomplete{ - @Input() - public showAutoComplete: boolean = true + @Input() public showBadge: boolean = false + @Input() public showAutoComplete: boolean = true @ViewChild('autoTrigger', {read: ElementRef}) autoTrigger: ElementRef @ViewChild('regionHierarchy', {read:TemplateRef}) regionHierarchyTemplate: TemplateRef<any> @@ -58,6 +58,7 @@ export class RegionTextSearchAutocomplete{ this.autocompleteList$ = this.formControl.valueChanges.pipe( startWith(''), debounceTime(200), + filter(string => string.length > 0), withLatestFrom(this.regionsWithLabelIndex$.pipe( startWith([]) )), diff --git a/src/ui/viewerStateController/regionSearch/regionSearch.template.html b/src/ui/viewerStateController/regionSearch/regionSearch.template.html index 8da2718d7..bf67e5428 100644 --- a/src/ui/viewerStateController/regionSearch/regionSearch.template.html +++ b/src/ui/viewerStateController/regionSearch/regionSearch.template.html @@ -1,9 +1,9 @@ -<div class="d-inline-flex flex-row align-items-center"> +<div class="w-100 d-inline-flex flex-row align-items-center"> <form *ngIf="showAutoComplete" class="flex-grow-1 flex-shrink-1"> <mat-form-field class="w-100"> <input - placeholder="Regions" + placeholder="Search for regions" #autoTrigger #trigger="matAutocompleteTrigger" type="text" @@ -26,9 +26,12 @@ </form> <button + matBadgeColor="accent" + [matBadge]="showBadge && (regionsSelected$ | async).length ? (regionsSelected$ | async).length : null" class="flex-grow-0 flex-shrink-0" (click)="showHierarchy($event)" - mat-icon-button color="primary"> + mat-icon-button + color="primary"> <i class="fas fa-sitemap"></i> </button> </div> diff --git a/src/ui/viewerStateController/viewerState.component.ts b/src/ui/viewerStateController/viewerState.component.ts index a0c33fd2b..b83181f7a 100644 --- a/src/ui/viewerStateController/viewerState.component.ts +++ b/src/ui/viewerStateController/viewerState.component.ts @@ -3,7 +3,6 @@ import { Store, select } from "@ngrx/store"; import { Observable, Subscription } from "rxjs"; import { distinctUntilChanged, shareReplay, filter } from "rxjs/operators"; import { SELECT_REGIONS, USER_CONFIG_ACTION_TYPES } from "src/services/stateStore.service"; -import { DESELECT_REGIONS, CHANGE_NAVIGATION } from "src/services/state/viewerState.store"; import { ToastService } from "src/services/toastService.service"; import { MatSelectChange, MatBottomSheet, MatBottomSheetRef } from "@angular/material"; import { DialogService } from "src/services/dialogService.service"; @@ -212,32 +211,6 @@ export class ViewerStateController implements OnInit{ }) } - public deselectRegion(event: MouseEvent, region: any){ - this.store$.dispatch({ - type: DESELECT_REGIONS, - deselectRegions: [region] - }) - } - - public gotoRegion(event: MouseEvent, region:any){ - if (region.position) { - this.store$.dispatch({ - type: CHANGE_NAVIGATION, - navigation: { - position: region.position, - animation: {} - } - }) - } else { - /** - * TODO convert to snack bar - */ - this.toastService.showToast(`${region.name} does not have a position defined`, { - timeout: 5000, - dismissable: true - }) - } - } } const ACTION_TYPES = { diff --git a/src/ui/viewerStateController/viewerState.template.html b/src/ui/viewerStateController/viewerState.template.html index 8d80cc044..7b4b40449 100644 --- a/src/ui/viewerStateController/viewerState.template.html +++ b/src/ui/viewerStateController/viewerState.template.html @@ -1,8 +1,8 @@ <mat-card> <!-- template selection --> - <mat-card-content class="d-inline-flex flex-nowrap"> - <mat-form-field> + <mat-card-content class="d-inline-flex flex-nowrap w-100"> + <mat-form-field class="flex-grow-1"> <mat-label> Template </mat-label> @@ -18,9 +18,6 @@ </mat-select> </mat-form-field> - <!-- padding so that info icon lines up --> - <button mat-icon-button class="pe-none invisible"></button> - <ng-container *ngIf="templateSelected$ | async as templateSelected"> <!-- show on hover component --> <sleight-of-hand @@ -64,11 +61,11 @@ </mat-card-content> <!-- parcellation selection --> - <mat-card-content class="d-inline-flex flex-nowrap"> + <mat-card-content class="d-inline-flex flex-nowrap w-100"> <mat-form-field *ngIf="templateSelected$ | async as templateSelected" - class="d-inline-flex flex-nowrap"> + class="flex-grow-1"> <mat-label> Parcellation </mat-label> @@ -84,12 +81,6 @@ </mat-select> </mat-form-field> - <!-- selected regions --> - <region-text-search-autocomplete - [showAutoComplete]="false" - (focusedStateChanged)="focused = $event"> - </region-text-search-autocomplete> - <ng-container *ngIf="parcellationSelected$ | async as parcellationSelected"> <!-- show on hover component --> <sleight-of-hand @@ -125,93 +116,17 @@ [kgId]="originDataset && originDataset.kgId"> </single-dataset-view> </div> - </div> - </sleight-of-hand> - + </sleight-of-hand> </ng-container> - - </mat-card-content> - - <!-- selected regions --> - <mat-card-content> - <mat-chip-list *ngIf="(regionsSelected$ | async)?.length > 0 "> - <cdk-virtual-scroll-viewport - class="w-100 h-10em overflow-x-hidden" - [itemSize]="32"> - <mat-chip - *cdkVirtualFor="let region of (regionsSelected$ | async)" - class="w-90"> - <span class="flex-grow-1 flex-shrink-1 text-truncate"> - {{ region.name }} - </span> - <button - *ngIf="region.position" - (click)="gotoRegion($event, region)" - mat-icon-button> - <i class="fas fa-map-marked-alt"></i> - </button> - <button - (click)="deselectRegion($event, region)" - mat-icon-button> - <i class="fas fa-trash"></i> - </button> - </mat-chip> - </cdk-virtual-scroll-viewport> - </mat-chip-list> - - <!-- place holder when no regions has been selected --> - <span class="muted" *ngIf="(regionsSelected$ | async).length === 0"> - No regions selected. Double click on any regions in the viewer, or use the search tool to select regions of interest. - </span> </mat-card-content> - <!-- control btns --> - <mat-card-actions class="d-flex justify-content-between"> - - <div class="d-flex"> - <!-- save --> - <button - matTooltip="Bookmark this selection of regions" - matTooltipPosition="below" - mat-button - (click)="saveSelection($event)" - color="primary"> - <i class="far fa-bookmark"></i> - - </button> + <ng-content select="[card-content='append']"> + </ng-content> - <!-- load --> - <button - (click)="loadSelection($event)" - matTooltip="Load a bookmarked region selection" - matTooltipPosition="below" - mat-button - color="primary" - [disabled]="(savedRegionsSelections$ | async)?.length === 0"> - <i - matBadgeColor="accent" - [matBadgeOverlap]="false" - [matBadge]="(savedRegionsSelections$ | async)?.length > 0 ? (savedRegionsSelections$ | async)?.length : null" - class="fas fa-folder-open"></i> - - </button> - </div> - - <!-- deselect all --> - <button - (click)="deselectAllRegions($event)" - matTooltip="Deselect all selected regions" - matTooltipPosition="below" - mat-raised-button - color="warn" - [disabled]="(regionsSelected$ | async)?.length === 0"> - <i class="fas fa-trash"></i> - </button> - </mat-card-actions> - <mat-card-footer> - + <ng-content select="[card-footer]"> + </ng-content> </mat-card-footer> </mat-card> diff --git a/src/util/directives/stopPropagation.directive.ts b/src/util/directives/stopPropagation.directive.ts new file mode 100644 index 000000000..e211a5668 --- /dev/null +++ b/src/util/directives/stopPropagation.directive.ts @@ -0,0 +1,47 @@ +import { Directive, Input, ElementRef, OnDestroy, OnChanges } from "@angular/core"; + +const VALID_EVENTNAMES = new Set([ + 'mousedown', + 'mouseup', + 'click', + 'mouseenter', + 'mouseleave' +]) + +const stopPropagation = ev => ev.stopPropagation() + +@Directive({ + selector: '[iav-stop]' +}) + +export class StopPropagationDirective implements OnChanges, OnDestroy{ + + @Input('iav-stop') stopString: string = '' + + private destroyCb: (() => void)[] = [] + + constructor(private el: ElementRef){} + + ngOnChanges(){ + + this.ngOnDestroy() + + const element = (this.el.nativeElement as HTMLElement) + for (const evName of this.stopString.split(' ')){ + if(VALID_EVENTNAMES.has(evName)){ + element.addEventListener(evName, stopPropagation) + this.destroyCb.push(() => { + element.removeEventListener(evName, stopPropagation) + }) + } else { + console.warn(`${evName} is not a valid event name in the supported set: `, VALID_EVENTNAMES) + } + } + } + + ngOnDestroy(){ + while (this.destroyCb.length > 0) { + this.destroyCb.pop()() + } + } +} \ No newline at end of file diff --git a/src/util/util.module.ts b/src/util/util.module.ts index c19d9046c..0ec4c182d 100644 --- a/src/util/util.module.ts +++ b/src/util/util.module.ts @@ -1,15 +1,18 @@ import { NgModule } from "@angular/core"; import { FilterNullPipe } from "./pipes/filterNull.pipe"; import { FilterRowsByVisbilityPipe } from "src/components/flatTree/filterRowsByVisibility.pipe"; +import { StopPropagationDirective } from "./directives/stopPropagation.directive"; @NgModule({ declarations: [ FilterNullPipe, - FilterRowsByVisbilityPipe + FilterRowsByVisbilityPipe, + StopPropagationDirective ], exports: [ FilterNullPipe, - FilterRowsByVisbilityPipe + FilterRowsByVisbilityPipe, + StopPropagationDirective ] }) -- GitLab