From 39f7c923a4e2b5d27bbda378e39dc31d1ec542a3 Mon Sep 17 00:00:00 2001 From: Xiao Gui <xgui3783@gmail.com> Date: Thu, 23 Apr 2020 15:23:45 +0200 Subject: [PATCH] chore: update tight int of pmap --- common/constants.js | 7 +- e2e/src/navigating/originDataset.e2e-spec.js | 80 +++++ e2e/src/util.js | 8 + src/components/components.module.ts | 3 + src/components/vButton/vButton.component.ts | 18 ++ src/components/vButton/vButton.style.css | 28 ++ src/components/vButton/vButton.template.html | 14 + src/services/state/dataStore.store.ts | 14 +- src/services/state/ngViewerState.store.ts | 42 +-- src/theme.scss | 30 ++ .../databrowserModule/databrowser.module.ts | 51 +++- .../databrowser.useEffect.ts | 80 ++++- .../previewDatasetFile.directive.spec.ts | 112 ++++++- .../previewDatasetFile.directive.ts | 28 +- src/ui/layerbrowser/layerbrowser.component.ts | 6 +- .../nehubaContainer.component.ts | 2 +- src/ui/parcellationRegion/region.base.ts | 4 + .../regionMenu/regionMenu.style.css | 13 +- .../regionMenu/regionMenu.template.html | 277 +++++++++++------- .../searchSideNav/searchSideNav.component.ts | 1 - src/util/directives/switch.directive.ts | 21 ++ src/util/pipes/addUnitAndJoin.pipe.ts | 12 + src/util/pipes/numbers.pipe.ts | 12 + src/util/util.module.ts | 11 +- 24 files changed, 703 insertions(+), 171 deletions(-) create mode 100644 e2e/src/navigating/originDataset.e2e-spec.js create mode 100644 src/components/vButton/vButton.component.ts create mode 100644 src/components/vButton/vButton.style.css create mode 100644 src/components/vButton/vButton.template.html create mode 100644 src/util/directives/switch.directive.ts create mode 100644 src/util/pipes/addUnitAndJoin.pipe.ts create mode 100644 src/util/pipes/numbers.pipe.ts diff --git a/common/constants.js b/common/constants.js index 363efa6a8..bfb603d31 100644 --- a/common/constants.js +++ b/common/constants.js @@ -6,6 +6,11 @@ SHARE_BTN: `Share this view`, SHARE_COPY_URL_CLIPBOARD: `Copy URL to clipboard`, SHARE_CUSTOM_URL: 'Create a custom URL', - SHARE_CUSTOM_URL_DIALOG: 'Dialog for creating a custom URL' + SHARE_CUSTOM_URL_DIALOG: 'Dialog for creating a custom URL', + + // parcellation region specific + SHOW_ORIGIN_DATASET: `Show probabilistic map`, + SHOW_CONNECTIVITY_DATA: `Show connectivity data`, + SHOW_IN_OTHER_REF_SPACE: `Show in other reference space`, } })(typeof exports === 'undefined' ? module.exports : exports) diff --git a/e2e/src/navigating/originDataset.e2e-spec.js b/e2e/src/navigating/originDataset.e2e-spec.js new file mode 100644 index 000000000..22d774177 --- /dev/null +++ b/e2e/src/navigating/originDataset.e2e-spec.js @@ -0,0 +1,80 @@ +const { AtlasPage } = require("../util") +const { ARIA_LABELS } = require('../../../common/constants') +const { SHOW_CONNECTIVITY_DATA, SHOW_IN_OTHER_REF_SPACE, SHOW_ORIGIN_DATASET } = ARIA_LABELS + +const cssSelector = `[aria-label="${SHOW_ORIGIN_DATASET}"]` + +const dict = { + 'ICBM 2009c Nonlinear Asymmetric': { + 'JuBrain Cytoarchitectonic Atlas': { + tests: [ + { + position: [600, 490], + expectedLabelName: 'Area 6ma (preSMA, mesial SFG) - left hemisphere', + } + ] + } + } +} + +describe('origin dataset pmap', () => { + let iavPage + + beforeAll(async () => { + iavPage = new AtlasPage() + await iavPage.init() + }) + + for (const templateName in dict) { + for (const parcellationName in dict[templateName]) { + describe(`testing template: ${templateName}, parcellation name: ${parcellationName}`, () => { + + const {url, tests} = dict[templateName][parcellationName] + beforeAll(async () => { + if (url) { + await iavPage.goto(url) + } else { + await iavPage.goto() + await iavPage.selectTitleTemplateParcellation(templateName, parcellationName) + } + + const tag = await iavPage.getSideNavTag() + await tag.click() + await iavPage.wait(5000) + await iavPage.waitUntilAllChunksLoaded() + }) + + for (const test of tests) { + it('> original pmap btn exists, and on click, show pmap', async () => { + + const { position, expectedLabelName } = test + await iavPage.cursorMoveToAndClick({ position }) + + await iavPage.click(cssSelector) + await iavPage.wait(5000) + await iavPage.waitForAsync() + + const additionalLayerControlIsShown = await iavPage.additionalLayerControlIsVisible() + expect(additionalLayerControlIsShown).toEqual(true) + + const checked = await iavPage.switchIsChecked(cssSelector) + expect(checked).toEqual(true) + }) + + it('> on second click, dismisses the pmap', async () => { + + await iavPage.click(cssSelector) + await iavPage.wait(5000) + await iavPage.waitForAsync() + + const additionalLayerControlIsShown = await iavPage.additionalLayerControlIsVisible() + expect(additionalLayerControlIsShown).toEqual(false) + + const checked = await iavPage.switchIsChecked(cssSelector) + expect(checked).toEqual(false) + }) + } + }) + } + } +}) \ No newline at end of file diff --git a/e2e/src/util.js b/e2e/src/util.js index f9a7519ff..2c53dd6e2 100644 --- a/e2e/src/util.js +++ b/e2e/src/util.js @@ -92,6 +92,14 @@ class WdBase{ return result } + async switchIsChecked(cssSelector){ + if (!cssSelector) throw new Error(`switchChecked method requies css selector`) + const checked = await this._browser + .findElement( By.css(cssSelector) ) + .getAttribute('aria-checked') + return checked === 'true' + } + async click(cssSelector){ if (!cssSelector) throw new Error(`click method needs to define a css selector`) await this._browser.findElement( By.css(cssSelector) ).click() diff --git a/src/components/components.module.ts b/src/components/components.module.ts index c16f932a5..f6cdb091c 100644 --- a/src/components/components.module.ts +++ b/src/components/components.module.ts @@ -31,6 +31,7 @@ import { SleightOfHand } from './sleightOfHand/soh.component'; import { TimerComponent } from './timer/timer.component'; import { TreeComponent } from './tree/tree.component'; import { TreeBaseDirective } from './tree/treeBase.directive'; +import { IAVVerticalButton } from './vButton/vButton.component'; @NgModule({ imports : [ @@ -56,6 +57,7 @@ import { TreeBaseDirective } from './tree/treeBase.directive'; SleightOfHand, DialogComponent, ConfirmDialogComponent, + IAVVerticalButton, /* directive */ HoverableBlockDirective, @@ -88,6 +90,7 @@ import { TreeBaseDirective } from './tree/treeBase.directive'; SleightOfHand, DialogComponent, ConfirmDialogComponent, + IAVVerticalButton, SearchResultPaginationPipe, TreeSearchPipe, diff --git a/src/components/vButton/vButton.component.ts b/src/components/vButton/vButton.component.ts new file mode 100644 index 000000000..63eda444b --- /dev/null +++ b/src/components/vButton/vButton.component.ts @@ -0,0 +1,18 @@ +import { Component, Input, ChangeDetectionStrategy } from "@angular/core"; + +@Component({ + selector: 'iav-v-button', + templateUrl: './vButton.template.html', + styleUrls: [ + './vButton.style.css' + ], + exportAs: 'iavVButton', + changeDetection: ChangeDetectionStrategy.OnPush +}) + +export class IAVVerticalButton{ + @Input() color: 'default' | 'primary' | 'accent' | 'warng' = 'default' + get class(){ + return `d-flex flex-column align-items-center iv-custom-comp ${this.color} h-100` + } +} \ No newline at end of file diff --git a/src/components/vButton/vButton.style.css b/src/components/vButton/vButton.style.css new file mode 100644 index 000000000..d5201cbe5 --- /dev/null +++ b/src/components/vButton/vButton.style.css @@ -0,0 +1,28 @@ +:host +{ + padding: 1rem 2rem; + /* background-color:rgba(128,128,128,0.3) */ +} + +:host:hover +{ + cursor: pointer; +} + +.icon-container +{ + flex-basis: 2.5rem; +} + +.text-container +{ + flex-basis: 0; + overflow: visible; + text-align: center; +} + +.footer-container +{ + flex-basis: 0rem; + margin-bottom: -1rem; +} \ No newline at end of file diff --git a/src/components/vButton/vButton.template.html b/src/components/vButton/vButton.template.html new file mode 100644 index 000000000..539d1b897 --- /dev/null +++ b/src/components/vButton/vButton.template.html @@ -0,0 +1,14 @@ +<div [class]="class"> + <div class="icon-container flex-grow-0 flex-shrink-0"> + <ng-content select="[iav-v-button-icon]"> + </ng-content> + </div> + <div class="text-container flex-grow-1 flex-shrink-1"> + <ng-content select="[iav-v-button-text]"> + </ng-content> + </div> + <div class="footer-container flex-grow-0 flex-shrink-0"> + <ng-content select="[iav-v-button-footer]"> + </ng-content> + </div> +</div> \ No newline at end of file diff --git a/src/services/state/dataStore.store.ts b/src/services/state/dataStore.store.ts index 5d16c11a6..07782dfec 100644 --- a/src/services/state/dataStore.store.ts +++ b/src/services/state/dataStore.store.ts @@ -18,8 +18,6 @@ export interface IStateInterface { datasetPreviews: DatasetPreview[] } - - export const defaultState = { fetchedDataEntries: [], favDataEntries: (() => { @@ -74,6 +72,17 @@ export const getStateStore = ({ state: state = defaultState } = {}) => (prevStat }) } } + case ACTION_TYPES.CLEAR_PREVIEW_DATASET: { + const { payload = {}} = action + const { file , dataset } = payload + const { fullId } = dataset + const { filename } = file + return { + ...prevState, + datasetPreviews: prevState.datasetPreviews + .filter(({ datasetId, filename: fName }) => !(datasetId === fullId && fName === filename)) + } + } case ACTION_TYPES.CLEAR_PREVIEW_DATASETS: { return { ...prevState, @@ -233,6 +242,7 @@ const ACTION_TYPES = { 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/services/state/ngViewerState.store.ts b/src/services/state/ngViewerState.store.ts index aaa71a273..652310cec 100644 --- a/src/services/state/ngViewerState.store.ts +++ b/src/services/state/ngViewerState.store.ts @@ -6,6 +6,8 @@ import { AtlasViewerConstantsServices } from 'src/atlasViewer/atlasViewer.consta import { SNACKBAR_MESSAGE } from './uiState.store'; import { getNgIds, IavRootStoreInterface, GENERAL_ACTION_TYPES } from '../stateStore.service'; import { Action, select, Store } from '@ngrx/store' +import { BACKENDURL } from 'src/util/constants'; +import { HttpClient } from '@angular/common/http'; export const FOUR_PANEL = 'FOUR_PANEL' export const V_ONE_THREE = 'V_ONE_THREE' @@ -78,26 +80,7 @@ export const getStateStore = ({ state = defaultState } = {}) => (prevState: Stat case ADD_NG_LAYER: return { ...prevState, - - /* this configration hides the layer if a non mixable layer already present */ - - /* this configuration does not the addition of multiple non mixable layers */ - // layers : action.layer.mixability === 'nonmixable' && prevState.layers.findIndex(l => l.mixability === 'nonmixable') >= 0 - // ? prevState.layers - // : prevState.layers.concat(action.layer) - - /* this configuration allows the addition of multiple non mixables */ - // layers : prevState.layers.map(l => mapLayer(l, action.layer)).concat(action.layer) layers : mixNgLayers(prevState.layers, action.layer), - - // action.layer.constructor === Array - // ? prevState.layers.concat(action.layer) - // : prevState.layers.concat({ - // ...action.layer, - // ...( action.layer.mixability === 'nonmixable' && prevState.layers.findIndex(l => l.mixability === 'nonmixable') >= 0 - // ? {visible: false} - // : {}) - // }) } case REMOVE_NG_LAYERS: { const { layers } = action @@ -198,30 +181,34 @@ export class NgViewerUseEffect implements OnDestroy { constructor( private actions: Actions, private store$: Store<IavRootStoreInterface>, - private constantService: AtlasViewerConstantsServices + private constantService: AtlasViewerConstantsServices, + private http: HttpClient, ){ // TODO either split backend user to be more granular, or combine the user config into a single subscription this.subscriptions.push( this.store$.pipe( select('ngViewerState'), - distinctUntilChanged(), debounceTime(200), skip(1), // Max frequency save once every second + + // properties to be saved + map(({ panelMode, panelOrder }) => { + return { panelMode, panelOrder } + }), + distinctUntilChanged(), throttleTime(1000) - ).subscribe(({panelMode, panelOrder}) => { - fetch(`${this.constantService.backendUrl}user/config`, { - method: 'POST', + ).subscribe(ngViewerState => { + this.http.post(`${BACKENDURL}user/config`, JSON.stringify({ ngViewerState }), { headers: { 'Content-type': 'application/json' - }, - body: JSON.stringify({ ngViewerState: { panelMode, panelOrder } }) + } }) }) ) - this.applySavedUserConfig$ = from(fetch(`${this.constantService.backendUrl}user/config`).then(r => r.json())).pipe( + this.applySavedUserConfig$ = this.http.get(`${BACKENDURL}user/config`).pipe( catchError((err,caught) => of(null)), filter(v => !!v), withLatestFrom(this.store$), @@ -449,6 +436,7 @@ export interface INgLayerInterface { name: string source: string mixability: string // base | mixable | nonmixable + annotation?: string // visible?: boolean shader?: string transform?: any diff --git a/src/theme.scss b/src/theme.scss index 71c0a7c3d..abd448e9a 100644 --- a/src/theme.scss +++ b/src/theme.scss @@ -2,6 +2,34 @@ @include mat-core(); +@mixin custom-cmp($theme) { + $primary: map-get($theme, primary); + $accent: map-get($theme, accent); + $warn: map-get($theme, warn); + + [iv-custom-comp], + .iv-custom-comp + { + &[primary], + &.primary + { + color: mat-color($primary); + } + + &[accent], + &.accent + { + color: mat-color($accent); + } + + &[warn], + &.warn + { + color: mat-color($warn); + } + } +} + $iv-theme-primary: mat-palette($mat-indigo); $iv-theme-accent: mat-palette($mat-amber); $iv-theme-warn: mat-palette($mat-red); @@ -9,6 +37,7 @@ $iv-theme-warn: mat-palette($mat-red); $iv-theme: mat-light-theme($iv-theme-primary, $iv-theme-accent, $iv-theme-warn); @include angular-material-theme($iv-theme); +@include custom-cmp($iv-theme); $iv-dark-theme-primary: mat-palette($mat-blue); $iv-dark-theme-accent: mat-palette($mat-amber, A200, A100, A400); @@ -18,6 +47,7 @@ $iv-dark-theme: mat-dark-theme($iv-dark-theme-primary, $iv-dark-theme-accent, [darktheme=true] { @include angular-material-theme($iv-dark-theme); + @include custom-cmp($iv-dark-theme); } .iav-dialog-class diff --git a/src/ui/databrowserModule/databrowser.module.ts b/src/ui/databrowserModule/databrowser.module.ts index 00ced43a6..422d9cb01 100644 --- a/src/ui/databrowserModule/databrowser.module.ts +++ b/src/ui/databrowserModule/databrowser.module.ts @@ -1,5 +1,5 @@ import { CommonModule } from "@angular/common"; -import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { NgModule, CUSTOM_ELEMENTS_SCHEMA, OnDestroy } from "@angular/core"; import { FormsModule } from "@angular/forms"; import { ComponentsModule } from "src/components/components.module"; import { AngularMaterialModule } from 'src/ui/sharedModules/angularMaterial.module' @@ -29,21 +29,46 @@ import { DatasetPreviewList, UnavailableTooltip } from "./singleDataset/datasetP import { PreviewComponentWrapper } from "./preview/previewComponentWrapper/previewCW.component"; import { BulkDownloadBtn, TransformDatasetToIdPipe } from "./bulkDownload/bulkDownloadBtn.component"; import { ShowDatasetDialogDirective, IAV_DATASET_SHOW_DATASET_DIALOG_CMP } from "./showDatasetDialog.directive"; -import { PreviewDatasetFile, IAV_DATASET_PREVIEW_DATASET_FN } from "./singleDataset/datasetPreviews/previewDatasetFile.directive"; -import { Store } from "@ngrx/store"; +import { PreviewDatasetFile, IAV_DATASET_PREVIEW_DATASET_FN, IAV_DATASET_PREVIEW_ACTIVE } from "./singleDataset/datasetPreviews/previewDatasetFile.directive"; +import { Store, select } from "@ngrx/store"; import { DATASETS_ACTIONS_TYPES } from "src/services/state/dataStore.store"; +import { startWith, map, take, debounceTime } from "rxjs/operators"; +import { Observable } from "rxjs"; +const previewDisplayedFactory = (store: Store<any>) => { + + return (file, dataset) => store.pipe( + select('dataStore'), + select('datasetPreviews'), + startWith([]), + map(datasetPreviews => { + const { fullId } = dataset || {} + const { filename } = file + return (datasetPreviews as any[]).findIndex(({ datasetId, filename: fName }) => + datasetId === fullId && fName === filename) >= 0 + }) + ) +} // TODO not too sure if this is the correct place for providing the callback token -const previewEmitFactory = (store) => { +const previewEmitFactory = (store: Store<any>, previewDisplayed: (file,dataset) => Observable<boolean>) => { + return (file, dataset) => { - store.dispatch({ - type: DATASETS_ACTIONS_TYPES.PREVIEW_DATASET, - payload: { - file, - dataset - } - }) + previewDisplayed(file, dataset).pipe( + debounceTime(10), + take(1), + ).subscribe(flag => + + store.dispatch({ + type: flag + ? DATASETS_ACTIONS_TYPES.CLEAR_PREVIEW_DATASET + : DATASETS_ACTIONS_TYPES.PREVIEW_DATASET, + payload: { + file, + dataset + } + }) + ) } } @@ -116,6 +141,10 @@ const previewEmitFactory = (store) => { },{ provide: IAV_DATASET_PREVIEW_DATASET_FN, useFactory: previewEmitFactory, + deps: [ Store, IAV_DATASET_PREVIEW_ACTIVE ] + },{ + provide: IAV_DATASET_PREVIEW_ACTIVE, + useFactory: previewDisplayedFactory, deps: [ Store ] } ], diff --git a/src/ui/databrowserModule/databrowser.useEffect.ts b/src/ui/databrowserModule/databrowser.useEffect.ts index b760603e5..c107a7d27 100644 --- a/src/ui/databrowserModule/databrowser.useEffect.ts +++ b/src/ui/databrowserModule/databrowser.useEffect.ts @@ -4,7 +4,7 @@ import { select, Store } from "@ngrx/store"; import { from, merge, Observable, of, Subscription, forkJoin, combineLatest } from "rxjs"; import { filter, map, scan, switchMap, withLatestFrom, mapTo, shareReplay, startWith, distinctUntilChanged, concatMap } from "rxjs/operators"; import { LoggingService } from "src/logging"; -import { DATASETS_ACTIONS_TYPES, IDataEntry, ViewerPreviewFile } from "src/services/state/dataStore.store"; +import { DATASETS_ACTIONS_TYPES, IDataEntry, ViewerPreviewFile, DatasetPreview } from "src/services/state/dataStore.store"; import { IavRootStoreInterface, ADD_NG_LAYER, CHANGE_NAVIGATION } from "src/services/stateStore.service"; import { LOCAL_STORAGE_CONST, DS_PREVIEW_URL } from "src/util/constants"; import { KgSingleDatasetService } from "./kgSingleDatasetService.service"; @@ -16,6 +16,9 @@ import { MatDialog } from "@angular/material/dialog"; import { PreviewComponentWrapper } from "./preview/previewComponentWrapper/previewCW.component"; import { getKgSchemaIdFromFullId } from "./util/getKgSchemaIdFromFullId.pipe"; import { HttpClient } from "@angular/common/http"; +import { INgLayerInterface, REMOVE_NG_LAYERS } from "src/services/state/ngViewerState.store"; + +const DATASET_PREVIEW_ANNOTATION = `DATASET_PREVIEW_ANNOTATION` @Injectable({ providedIn: 'root', @@ -30,6 +33,9 @@ export class DataBrowserUseEffect implements OnDestroy { @Effect() previewNgLayer$: Observable<any> + @Effect() + removePreviewNgLayers$: Observable<any> + // registerd layers (to be further developed) @Effect() previewRegisteredVolumes$: Observable<any> @@ -45,6 +51,8 @@ export class DataBrowserUseEffect implements OnDestroy { public previewDatasetFile$: Observable<ViewerPreviewFile> private storePreviewDatasetFile$: Observable<{dataset: IDataEntry,file: ViewerPreviewFile}[]> + private datasetPreviews$: Observable<DatasetPreview[]> + constructor( private store$: Store<IavRootStoreInterface>, private actions$: Actions<any>, @@ -55,12 +63,51 @@ export class DataBrowserUseEffect implements OnDestroy { private http: HttpClient ) { + const ngViewerStateLayers$ = this.store$.pipe( + select('ngViewerState'), + select('layers'), + startWith([]), + shareReplay(1) + ) as Observable<INgLayerInterface[]> + + this.datasetPreviews$ = this.store$.pipe( + select('dataStore'), + select('datasetPreviews'), + startWith([]), + shareReplay(1), + ) + + this.removePreviewDataset$ = ngViewerStateLayers$.pipe( + map(layers => layers.filter(({ annotation }) => annotation && annotation.indexOf(DATASET_PREVIEW_ANNOTATION) >= 0)), + withLatestFrom(this.datasetPreviews$), + map(([ ngViewerLayers, datasetPreviews ]) => { + return datasetPreviews.filter(({ filename }) => { + return ngViewerLayers.findIndex(({ annotation }) => annotation.indexOf(filename) >= 0) < 0 + }) + }), + filter(arr => arr.length > 0), + concatMap(arr => from(arr).pipe( + map(item => { + const { filename, datasetId } = item + return { + type: DATASETS_ACTIONS_TYPES.CLEAR_PREVIEW_DATASET, + payload: { + dataset: { + fullId: datasetId + }, + file: { + filename + } + } + } + }) + )) + ) + // TODO this is almost definitely wrong // possibily causing https://github.com/HumanBrainProject/interactive-viewer/issues/502 this.subscriptions.push( - this.store$.pipe( - select('dataStore'), - select('datasetPreviews'), + this.datasetPreviews$.pipe( filter(datasetPreviews => datasetPreviews.length > 0), map((datasetPreviews) => datasetPreviews[datasetPreviews.length - 1]), switchMap(({ datasetId, filename }) =>{ @@ -207,16 +254,34 @@ export class DataBrowserUseEffect implements OnDestroy { }) ) + this.removePreviewNgLayers$ = this.datasetPreviews$.pipe( + withLatestFrom( ngViewerStateLayers$ ), + map(([ datasetPreviews, ngLayers ]) => { + const previewingFilesName = datasetPreviews.map(({ filename }) => filename) + return ngLayers.filter(({ name, annotation }) => + annotation && annotation.indexOf(DATASET_PREVIEW_ANNOTATION) >= 0 + && previewingFilesName.indexOf(name) < 0) + }), + filter(layers => layers.length > 0), + map(layers => { + return { + type: REMOVE_NG_LAYERS, + layers + } + }) + ) + this.previewNgLayer$ = this.previewDatasetFile$.pipe( filter(file => determinePreviewFileType(file) === PREVIEW_FILE_TYPES.NIFTI ), - map(({ url }) => { + map(({ url, filename }) => { const layer = { - name: url, + name: filename, source : `nifti://${url}`, mixability : 'nonmixable', shader : GLSL_COLORMAP_JET, + annotation: `${DATASET_PREVIEW_ANNOTATION} ${filename}` } return { type: ADD_NG_LAYER, @@ -357,4 +422,7 @@ export class DataBrowserUseEffect implements OnDestroy { @Effect() public toggleDataset$: Observable<any> + + @Effect() + public removePreviewDataset$: Observable<any> } diff --git a/src/ui/databrowserModule/singleDataset/datasetPreviews/previewDatasetFile.directive.spec.ts b/src/ui/databrowserModule/singleDataset/datasetPreviews/previewDatasetFile.directive.spec.ts index cde8f3681..87c32a9fc 100644 --- a/src/ui/databrowserModule/singleDataset/datasetPreviews/previewDatasetFile.directive.spec.ts +++ b/src/ui/databrowserModule/singleDataset/datasetPreviews/previewDatasetFile.directive.spec.ts @@ -3,23 +3,29 @@ import { async, TestBed } from "@angular/core/testing"; import { By } from "@angular/platform-browser"; import { MatSnackBar } from "@angular/material/snack-bar"; import { AngularMaterialModule } from "src/ui/sharedModules/angularMaterial.module"; -import { PreviewDatasetFile, IAV_DATASET_PREVIEW_DATASET_FN } from './previewDatasetFile.directive' +import { PreviewDatasetFile, IAV_DATASET_PREVIEW_DATASET_FN, IAV_DATASET_PREVIEW_ACTIVE } from './previewDatasetFile.directive' +import { Subject } from "rxjs"; @Component({ template: '' }) -class TestCmp{} +class TestCmp{ + testmethod(arg) {} +} const dummyMatSnackBar = { open: jasmine.createSpy('open') } const previewDatasetFnSpy = jasmine.createSpy('previewDatasetFn') +const mockDatasetActiveObs = new Subject() +const getDatasetActiveObs = jasmine.createSpy('getDatasetActive').and.returnValue(mockDatasetActiveObs) describe('ShowDatasetDialogDirective', () => { + let testModule beforeEach(async(() => { - TestBed + testModule = TestBed .configureTestingModule({ imports: [ AngularMaterialModule @@ -36,7 +42,11 @@ describe('ShowDatasetDialogDirective', () => { { provide: IAV_DATASET_PREVIEW_DATASET_FN, useValue: previewDatasetFnSpy - } + }, + // { + // provide: IAV_DATASET_PREVIEW_ACTIVE, + // useValue: getDatasetActiveObs + // } ] }) @@ -61,6 +71,100 @@ describe('ShowDatasetDialogDirective', () => { expect(directive).not.toBeNull() }) + describe('> DI', () => { + describe(`> ${IAV_DATASET_PREVIEW_ACTIVE}`, () => { + + afterEach(() => { + getDatasetActiveObs.calls.reset() + }) + + describe('> if not provided', () => { + beforeEach(() => { + TestBed.overrideComponent(TestCmp, { + set: { + template: ` + <div iav-dataset-preview-dataset-file + (iav-dataset-preview-active-changed)="testmethod($event)" + iav-dataset-preview-dataset-file-filename="banana"> + </div> + `, + } + }).compileComponents() + }) + + + it('> should init directive', () => { + const fixture = TestBed.createComponent(TestCmp) + fixture.detectChanges() + const directive = fixture.debugElement.query( By.directive( PreviewDatasetFile ) ) + expect(directive).toBeTruthy() + }) + + it('> should not call getDatasetActiveObs', () => { + + const fixture = TestBed.createComponent(TestCmp) + fixture.detectChanges() + expect(getDatasetActiveObs).not.toHaveBeenCalled() + }) + + it('> if not provided, on subject next, should not emit active$', () => { + + const fixture = TestBed.createComponent(TestCmp) + const cmp = fixture.debugElement.componentInstance + + const testmethodSpy = spyOn(cmp, 'testmethod') + + fixture.detectChanges() + mockDatasetActiveObs.next(true) + fixture.detectChanges() + + expect(testmethodSpy).not.toHaveBeenCalled() + }) + }) + + describe('> if provided', () => { + beforeEach(() => { + TestBed.overrideComponent(TestCmp, { + set: { + template: ` + <div iav-dataset-preview-dataset-file + (iav-dataset-preview-active-changed)="testmethod($event)" + iav-dataset-preview-dataset-file-filename="banana"> + </div> + `, + providers: [ + { + provide: IAV_DATASET_PREVIEW_ACTIVE, + useValue: getDatasetActiveObs + } + ] + } + }).compileComponents() + }) + + it('> should call getDatasetObs', () => { + const fixture = TestBed.createComponent(TestCmp) + fixture.detectChanges() + expect(getDatasetActiveObs).toHaveBeenCalled() + }) + + it('> on obs.next, should emit active$,', () => { + + const fixture = TestBed.createComponent(TestCmp) + const cmp = fixture.debugElement.componentInstance + + const testmethodSpy = spyOn(cmp, 'testmethod') + + fixture.detectChanges() + mockDatasetActiveObs.next(true) + fixture.detectChanges() + + expect(testmethodSpy).toHaveBeenCalledWith(true) + }) + }) + }) + }) + it('without providing file or filename, should not call emitFn', () => { TestBed.overrideComponent(TestCmp, { diff --git a/src/ui/databrowserModule/singleDataset/datasetPreviews/previewDatasetFile.directive.ts b/src/ui/databrowserModule/singleDataset/datasetPreviews/previewDatasetFile.directive.ts index 26683fb73..edc8fae81 100644 --- a/src/ui/databrowserModule/singleDataset/datasetPreviews/previewDatasetFile.directive.ts +++ b/src/ui/databrowserModule/singleDataset/datasetPreviews/previewDatasetFile.directive.ts @@ -1,15 +1,18 @@ -import { Directive, Input, HostListener, Inject, Output, EventEmitter, Optional } from "@angular/core"; +import { Directive, Input, HostListener, Inject, Output, EventEmitter, Optional, OnChanges } from "@angular/core"; import { MatSnackBar } from "@angular/material/snack-bar"; import { ViewerPreviewFile, IDataEntry } from 'src/services/state/dataStore.store' +import { Observable, Subscription } from "rxjs"; +import { distinctUntilChanged } from "rxjs/operators"; export const IAV_DATASET_PREVIEW_DATASET_FN = 'IAV_DATASET_PREVIEW_DATASET_FN' +export const IAV_DATASET_PREVIEW_ACTIVE = `IAV_DATASET_PREVIEW_ACTIVE` @Directive({ selector: '[iav-dataset-preview-dataset-file]', exportAs: 'iavDatasetPreviewDatasetFile' }) -export class PreviewDatasetFile{ +export class PreviewDatasetFile implements OnChanges{ @Input('iav-dataset-preview-dataset-file') file: ViewerPreviewFile @@ -31,10 +34,29 @@ export class PreviewDatasetFile{ @Output('iav-dataset-preview-dataset-file-emit') emitter: EventEmitter<{file: Partial<ViewerPreviewFile>, dataset: Partial<IDataEntry>}> = new EventEmitter() + @Output('iav-dataset-preview-active-changed') + active$: EventEmitter<boolean> = new EventEmitter() + + public active: boolean = false + + private dataActiveObs: Subscription constructor( private snackbar: MatSnackBar, - @Optional() @Inject(IAV_DATASET_PREVIEW_DATASET_FN) private emitFn: any + @Optional() @Inject(IAV_DATASET_PREVIEW_DATASET_FN) private emitFn: any, + @Optional() @Inject(IAV_DATASET_PREVIEW_ACTIVE) private getDatasetActiveObs: (file, dataset) => Observable<boolean> ){ + + } + + ngOnChanges(){ + if (this.dataActiveObs) this.dataActiveObs.unsubscribe() + + if (this.getDatasetActiveObs) this.dataActiveObs = this.getDatasetActiveObs(this.getFile(), this.getDataset()).pipe( + distinctUntilChanged() + ).subscribe(flag => { + this.active = flag + this.active$.emit(flag) + }) } private getFile(): Partial<ViewerPreviewFile>{ diff --git a/src/ui/layerbrowser/layerbrowser.component.ts b/src/ui/layerbrowser/layerbrowser.component.ts index cc14ee88c..f72381f13 100644 --- a/src/ui/layerbrowser/layerbrowser.component.ts +++ b/src/ui/layerbrowser/layerbrowser.component.ts @@ -205,10 +205,8 @@ export class LayerBrowser implements OnInit, OnDestroy { return } this.store.dispatch({ - type : REMOVE_NG_LAYER, - layer : { - name : layer.name, - }, + type: REMOVE_NG_LAYER, + layer }) } diff --git a/src/ui/nehubaContainer/nehubaContainer.component.ts b/src/ui/nehubaContainer/nehubaContainer.component.ts index 0ddb3de68..94fcdc3f9 100644 --- a/src/ui/nehubaContainer/nehubaContainer.component.ts +++ b/src/ui/nehubaContainer/nehubaContainer.component.ts @@ -616,7 +616,7 @@ export class NehubaContainer implements OnInit, OnChanges, OnDestroy { type : ADD_NG_LAYER, layer : dispatchLayers, }) - }), + }) ) this.subscriptions.push( diff --git a/src/ui/parcellationRegion/region.base.ts b/src/ui/parcellationRegion/region.base.ts index cec5a668c..17b84af7f 100644 --- a/src/ui/parcellationRegion/region.base.ts +++ b/src/ui/parcellationRegion/region.base.ts @@ -9,6 +9,7 @@ import { import { VIEWERSTATE_CONTROLLER_ACTION_TYPES } from "../viewerStateController/viewerState.base"; import {distinctUntilChanged, shareReplay} from "rxjs/operators"; import {Observable} from "rxjs"; +import { ARIA_LABELS } from 'common/constants' export class RegionBase { @@ -149,4 +150,7 @@ export class RegionBase { } } + 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.style.css b/src/ui/parcellationRegion/regionMenu/regionMenu.style.css index c56194bd3..278de0373 100644 --- a/src/ui/parcellationRegion/regionMenu/regionMenu.style.css +++ b/src/ui/parcellationRegion/regionMenu/regionMenu.style.css @@ -1,6 +1,11 @@ -.speed-dial +mat-list.sm mat-list-item { - right: 0; - bottom: 0; - height: 0; + height:36px; + font-size: 90%; + cursor: default; +} + +mat-icon +{ + transform: scale(0.75); } \ No newline at end of file diff --git a/src/ui/parcellationRegion/regionMenu/regionMenu.template.html b/src/ui/parcellationRegion/regionMenu/regionMenu.template.html index 44d38e797..7ba29a29e 100644 --- a/src/ui/parcellationRegion/regionMenu/regionMenu.template.html +++ b/src/ui/parcellationRegion/regionMenu/regionMenu.template.html @@ -4,62 +4,6 @@ <span class="mr-8"> {{ region.name }} </span> - - <!-- only show speed dial if originDatasets exists and are greater than 0 --> - <div *ngIf="((region && region.originDatasets) || []).length > 0" - class="speed-dial position-absolute d-flex flex-row-reverse align-items-center" - (iav-onclick-outside)="fab.close()" - #fab="iavFabSpeedDialContainer" - iav-fab-speed-dial-scale-origin="right" - iav-fab-speed-dial-container> - - <!-- main btn --> - <button mat-fab class="ml-3" - iav-fab-speed-dial-trigger - [color]="fab.isOpen ? 'default' : 'accent'"> - - <!-- TODO add test to ensure this does not happen --> - <!-- nb pe-none is essential for iav-onclick-outside directive to work properly --> - <ng-container *ngIf="fab.isOpen; else closedTmpl"> - <i class="pe-none fas fa-times"></i> - </ng-container> - <ng-template #closedTmpl> - <i class="pe-none fas fa-ellipsis-v"></i> - </ng-template> - </button> - - <!-- action btns --> - <!-- originDatasets --> - <ng-container *ngFor="let originDataset of (region.originDatasets || []); let index = index"> - - <!-- preview file --> - <button *ngIf="originDataset.kgSchema && originDataset.kgId && originDataset.filename" - mat-mini-fab - iav-dataset-preview-dataset-file - [iav-dataset-preview-dataset-file-kgid]="originDataset.kgId" - [iav-dataset-preview-dataset-file-filename]="originDataset.filename" - class="m-1" - [iav-fab-speed-dial-child-index]="index * 2" - matTooltip="Preview file" - iav-fab-speed-dial-child> - <i class="far fa-eye"></i> - </button> - - <!-- show dataset --> - <button *ngIf="originDataset.kgSchema && originDataset.kgId" - mat-mini-fab - class="m-1" - (click)="fab.close()" - matTooltip="More detail on dataset" - [iav-dataset-show-dataset-dialog-kgid]="originDataset.kgId" - iav-dataset-show-dataset-dialog - [iav-fab-speed-dial-child-index]="index * 2 + 1" - iav-fab-speed-dial-child> - <i class="fas fa-info"></i> - </button> - </ng-container> - - </div> </div> </mat-card-title> <mat-card-subtitle> @@ -68,61 +12,185 @@ Brain region </span> </mat-card-subtitle> + <mat-card-content> - {{ region.description }} - </mat-card-content> - <div class="d-flex flex-row flex-wrap"> - <button mat-button - (click)="toggleRegionSelected()" - [color]="isSelected ? 'primary': 'basic'"> - <i class="far" [ngClass]="{'fa-check-square': isSelected, 'fa-square': !isSelected}"></i> - <span> - {{isSelected? 'Deselect' : 'Select'}} - </span> - </button> - <button mat-button (click)="navigateToRegion()"> - <i class="fas fa-map-marked-alt"></i> - <span> - Navigate - </span> - </button> - <button *ngIf="hasConnectivity" - mat-button - [matMenuTriggerFor]="connectivitySourceDatasets" - #connectivityMenuButton="matMenuTrigger" - iav-captureClickListenerDirective - [iav-captureClickListenerDirective-captureDocument]="true" - (iav-captureClickListenerDirective-onMousedown)="connectivityMenuButton.closeMenu()"> - <i class="fab fa-connectdevelop"></i> - <span> - Connectivity - </span> - <i class="fas fa-angle-right"></i> - </button> + <mat-divider></mat-divider> + + <!-- region actions --> + <mat-grid-list cols="2" rowHeight="6rem" gutterSize="0px"> + + <mat-grid-tile> + <iav-v-button + class="h-100 w-100" + mat-ripple + [color]="isSelected ? 'primary' : 'default'" + (click)="toggleRegionSelected()"> + <i iav-v-button-icon class="far" [ngClass]="{'fa-check-square': isSelected, 'fa-square': !isSelected}"></i> + <span iav-v-button-text>{{isSelected ? 'Deselect' : 'Select'}}</span> + </iav-v-button> + </mat-grid-tile> + <mat-grid-tile> + <iav-v-button + class="h-100 w-100" + mat-ripple + (click)="navigateToRegion()"> + <i class="fas fa-map-marked-alt" iav-v-button-icon></i> + <span iav-v-button-text>Navigate</span> + </iav-v-button> + </mat-grid-tile> + <ng-template #safeHarbour> + <ng-container *ngFor="let originDataset of (region.originDatasets || [])"> + <mat-grid-tile class="iv-custom-comp"> + <iav-v-button + iav-dataset-preview-dataset-file + [iav-dataset-preview-dataset-file-kgid]="originDataset.kgId" + [iav-dataset-preview-dataset-file-filename]="originDataset.filename" + #previewDirective="iavDatasetPreviewDatasetFile" + class="h-100 w-100" + mat-ripple> + <i class="far fa-eye" iav-v-button-icon></i> + <span iav-v-button-text [class]="(previewDirective.active | async) ? 'iv-custom-comp primary' : ''">Probability Map {{ previewDirective.active | async }}</span> + </iav-v-button> + </mat-grid-tile> + </ng-container> + + <mat-grid-tile> + <iav-v-button *ngIf="hasConnectivity" + class="h-100 w-100" + mat-ripple + [matMenuTriggerFor]="connectivitySourceDatasets" + #connectivityMenuButton="matMenuTrigger" + iav-captureClickListenerDirective + [iav-captureClickListenerDirective-captureDocument]="true" + (iav-captureClickListenerDirective-onMousedown)="connectivityMenuButton.closeMenu()"> + <i class="fab fa-connectdevelop" iav-v-button-icon></i> + <span iav-v-button-text>Connectivity</span> + <i class="fas fa-chevron-down" iav-v-button-footer></i> + </iav-v-button> + </mat-grid-tile> - <!-- Menu to navigate between template spaces to explore same region --> - <div> - <button mat-button - aria-label="Show availability in other reference spaces" - *ngIf="sameRegionTemplate.length" - [matMenuTriggerFor]="additionalActions" - #changeTmplTrigger="matMenuTrigger" - iav-captureClickListenerDirective - [iav-captureClickListenerDirective-captureDocument]="true" - (iav-captureClickListenerDirective-onMousedown)="changeTmplTrigger.closeMenu()"> - <i class="fas fa-brain"></i> - <span> - Change template - </span> - <i class="fas fa-angle-right"></i> - </button> + <mat-grid-tile> + <iav-v-button *ngIf="sameRegionTemplate.length" + [attr.aria-label]="'Show availability in other reference spaces'" + class="h-100 w-100" + mat-ripple + [matMenuTriggerFor]="additionalActions" + #changeTmplTrigger="matMenuTrigger" + iav-captureClickListenerDirective + [iav-captureClickListenerDirective-captureDocument]="true" + (iav-captureClickListenerDirective-onMousedown)="changeTmplTrigger.closeMenu()"> + <i class="fas fa-brain" iav-v-button-icon></i> + <span iav-v-button-text>Change template</span> + <i class="fas fa-chevron-down" iav-v-button-footer></i> + </iav-v-button> + </mat-grid-tile> + </ng-template> + </mat-grid-list> + + + <!-- region desc --> + <ng-container *ngIf="region?.description?.length > 0"> + <mat-divider></mat-divider> + <div> + {{ region.description }} </div> + </ng-container> - </div> -</mat-card> + <mat-divider></mat-divider> + + <mat-list class="sm"> + + <!-- position --> + <mat-list-item *ngIf="region?.position" (click)="navigateToRegion()" mat-ripple> + <mat-icon scaled-down fontSet="fas" fontIcon="fa-map-marked-alt" mat-list-icon></mat-icon> + <div mat-line> + {{ region.position | nmToMm | addUnitAndJoin : 'mm' }} + </div> + </mat-list-item> + + <!-- originData --> + <mat-list-item *ngFor="let originDataset of (region.originDatasets || [])" + iav-dataset-preview-dataset-file + [iav-dataset-preview-dataset-file-kgid]="originDataset.kgId" + [iav-dataset-preview-dataset-file-filename]="originDataset.filename" + #previewDirective="iavDatasetPreviewDatasetFile" + iv-custom-comp + [attr.primary]="previewDirective.active || null" + role="switch" + [attr.aria-checked]="previewDirective.active" + [attr.aria-label]="SHOW_ORIGIN_DATASET" + mat-ripple> + <mat-icon fontSet="fas" fontIcon="fa-eye" mat-list-icon></mat-icon> + <div mat-line> + Preview probability map + </div> + </mat-list-item> + <!-- connectivity --> + <div iav-switch #connectivitySwitch="iavSwitch"> + + <mat-list-item mat-ripple + (click)="connectivitySwitch.toggle()" + [attr.aria-label]="SHOW_CONNECTIVITY_DATA"> + <mat-icon fontSet="fab" fontIcon="fa-connectdevelop" mat-list-icon></mat-icon > + <div mat-line> + <span> + Connectivity + </span> + <span class="muted"> + ({{ 1 }}) + </span> + </div> + <mat-icon fontSet="fas" [fontIcon]="connectivitySwitch.switchState ? 'fa-chevron-up' : 'fa-chevron-down'"></mat-icon> + </mat-list-item> + + <!-- connectivity --> + <mat-list-item *ngIf="connectivitySwitch.switchState" mat-ripple (click)="showConnectivity(region.name)"> + <mat-icon fontSet="fas" fontIcon="fa-none" mat-list-icon></mat-icon> + <div mat-line>1000 Brain Study - DTI connectivity</div> + </mat-list-item> + + </div> + + <!-- change template --> + <div iav-switch #changeTmplSwitch="iavSwitch"> + + <mat-list-item *ngIf="sameRegionTemplate.length" + 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> + <div mat-line> + <span> + Explore in other templates + </span> + <span class="muted"> + ({{ sameRegionTemplate.length }}) + </span> + </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" + aria-label="Availability in other reference spaces"> + <mat-list-item *ngFor="let sameRegion of sameRegionTemplate; let i = index" + (click)="changeView(i)" + mat-ripple> + <mat-icon fontSet="fas" fontIcon="fa-none" mat-list-icon></mat-icon> + <div mat-line> + <span class="overflow-x-hidden text-truncate"> {{ sameRegion.template.name }} </span> + <span *ngIf="sameRegion.hemisphere"> - {{ sameRegion.hemisphere }}</span> + </div> + </mat-list-item> + </div> + + </div> + </mat-list> + </mat-card-content> +</mat-card> <!-- ToDo make dynamic with AVAILABLE CONNECTIVITY DATASETS data - get info from atlas viewer core --> <mat-menu @@ -138,7 +206,6 @@ <mat-menu - [aria-label]="'Availability in other reference spaces'" #additionalActions="matMenu" xPosition="before" hasBackdrop="false"> diff --git a/src/ui/searchSideNav/searchSideNav.component.ts b/src/ui/searchSideNav/searchSideNav.component.ts index f514928bd..a0647a652 100644 --- a/src/ui/searchSideNav/searchSideNav.component.ts +++ b/src/ui/searchSideNav/searchSideNav.component.ts @@ -10,7 +10,6 @@ import { EXPAND_SIDE_PANEL_CURRENT_VIEW, } from "src/services/state/uiState.store"; import { IavRootStoreInterface, SELECT_REGIONS } from "src/services/stateStore.service"; -import { LayerBrowser } from "../layerbrowser/layerbrowser.component"; import { trackRegionBy } from '../viewerStateController/regionHierachy/regionHierarchy.component' import { MatDialog, MatDialogRef } from "@angular/material/dialog"; import { MatSnackBar } from "@angular/material/snack-bar"; diff --git a/src/util/directives/switch.directive.ts b/src/util/directives/switch.directive.ts new file mode 100644 index 000000000..cbdbe4dfc --- /dev/null +++ b/src/util/directives/switch.directive.ts @@ -0,0 +1,21 @@ +import { Directive, Input } from "@angular/core"; + +@Directive({ + selector: '[iav-switch]', + exportAs: 'iavSwitch' +}) +export class SwitchDirective{ + @Input() switchState: boolean = false + + toggle(){ + this.switchState = !this.switchState + } + + close(){ + this.switchState = false + } + + open(){ + this.switchState = true + } +} \ No newline at end of file diff --git a/src/util/pipes/addUnitAndJoin.pipe.ts b/src/util/pipes/addUnitAndJoin.pipe.ts new file mode 100644 index 000000000..000e245da --- /dev/null +++ b/src/util/pipes/addUnitAndJoin.pipe.ts @@ -0,0 +1,12 @@ +import { Pipe, PipeTransform } from "@angular/core"; + +@Pipe({ + name: 'addUnitAndJoin', + pure: true +}) + +export class AddUnitAndJoin implements PipeTransform{ + public transform(arr: (string | number)[], unit: string, separator: string = ', '): string { + return arr.map(v => `${v}${unit}`).join(separator) + } +} \ No newline at end of file diff --git a/src/util/pipes/numbers.pipe.ts b/src/util/pipes/numbers.pipe.ts new file mode 100644 index 000000000..0390ea90d --- /dev/null +++ b/src/util/pipes/numbers.pipe.ts @@ -0,0 +1,12 @@ +import { Pipe, PipeTransform } from "@angular/core"; + +@Pipe({ + name: 'nmToMm', + pure: true +}) + +export class NmToMm implements PipeTransform{ + public transform(nums: number[], decimal: number = 2): number[] { + return nums.map(num => (num / 1e6).toFixed(decimal)).map(Number) + } +} \ No newline at end of file diff --git a/src/util/util.module.ts b/src/util/util.module.ts index 0675aa46c..47d094f5f 100644 --- a/src/util/util.module.ts +++ b/src/util/util.module.ts @@ -8,6 +8,9 @@ import { FilterNullPipe } from "./pipes/filterNull.pipe"; import { IncludesPipe } from "./pipes/includes.pipe"; import { SafeResourcePipe } from "./pipes/safeResource.pipe"; import { CaptureClickListenerDirective } from "./directives/captureClickListener.directive"; +import { AddUnitAndJoin } from "./pipes/addUnitAndJoin.pipe"; +import { NmToMm } from "./pipes/numbers.pipe"; +import { SwitchDirective } from "./directives/switch.directive"; @NgModule({ declarations: [ @@ -22,6 +25,9 @@ import { CaptureClickListenerDirective } from "./directives/captureClickListener IncludesPipe, SafeResourcePipe, CaptureClickListenerDirective, + AddUnitAndJoin, + NmToMm, + SwitchDirective, ], exports: [ FilterNullPipe, @@ -35,8 +41,9 @@ import { CaptureClickListenerDirective } from "./directives/captureClickListener IncludesPipe, SafeResourcePipe, CaptureClickListenerDirective, - ], - providers: [ + AddUnitAndJoin, + NmToMm, + SwitchDirective, ] }) -- GitLab