From 880ce9d115ee6c8a0d58889b2b0bb5c0a326f644 Mon Sep 17 00:00:00 2001 From: Xiao Gui <xgui3783@gmail.com> Date: Sat, 18 Apr 2020 21:33:31 +0200 Subject: [PATCH] chore: reworked plugin api, added tests --- e2e/src/advanced/pluginApi.e2e-spec.js | 99 ++++ e2e/src/util.js | 4 +- .../atlasViewer.apiService.service.spec.ts | 427 ++++++++++++++++-- .../atlasViewer.apiService.service.ts | 155 +++++-- src/atlasViewer/atlasViewer.component.ts | 63 +-- src/main.module.ts | 72 ++- src/services/state/uiState.store.spec.ts | 65 +++ src/services/state/uiState.store.ts | 40 +- src/services/stateStore.service.ts | 2 +- src/services/uiService.service.ts | 39 +- src/ui/actionDialog/actionDialog.component.ts | 36 ++ .../actionDialog/actionDialog.template.html | 63 +++ src/ui/ui.module.ts | 4 + 13 files changed, 910 insertions(+), 159 deletions(-) create mode 100644 src/services/state/uiState.store.spec.ts create mode 100644 src/ui/actionDialog/actionDialog.component.ts create mode 100644 src/ui/actionDialog/actionDialog.template.html diff --git a/e2e/src/advanced/pluginApi.e2e-spec.js b/e2e/src/advanced/pluginApi.e2e-spec.js index 1a4d5f2ba..a5c14f975 100644 --- a/e2e/src/advanced/pluginApi.e2e-spec.js +++ b/e2e/src/advanced/pluginApi.e2e-spec.js @@ -46,6 +46,105 @@ describe('> plugin api', () => { expect(newTitle).toEqual(`hello world ${prevTitle}`) }) }) + + describe('> cancelPromise', () => { + let originalTitle + beforeEach(async () => { + originalTitle = await iavPage.execScript(() => window.document.title) + + await iavPage.execScript(() => { + const pr = interactiveViewer.uiHandle.getUserToSelectARegion('hello world title') + pr + .then(obj => window.document.title = 'success ' + obj.segment.name) + .catch(() => window.document.title = 'failed') + window.pr = pr + }) + + await iavPage.wait(500) + + await iavPage.execScript(() => { + const pr = window.pr + interactiveViewer.uiHandle.cancelPromise(pr) + }) + }) + + it('> cancelPromise rejects promise', async () => { + const newTitle = await iavPage.execScript(() => window.document.title) + expect(newTitle).toEqual('failed') + }) + }) + + describe('> getUserToSelectARegion', () => { + let originalTitle + beforeEach(async () => { + originalTitle = await iavPage.execScript(() => window.document.title) + + await iavPage.execScript(() => { + interactiveViewer.uiHandle.getUserToSelectARegion('hello world title') + .then(obj => window.document.title = 'success ' + obj.segment.name) + .catch(() => window.document.title = 'failed') + }) + + await iavPage.wait(500) + }) + + it('> shows modal dialog', async () => { + const text = await iavPage.getModalText() + expect(text).toContain('hello world title') + }) + + it('> modal has cancel button', async () => { + const texts = await iavPage.getModalActions() + const idx = texts.findIndex(text => /cancel/i.test(text)) + expect(idx).toBeGreaterThanOrEqual(0) + }) + + it('> cancelling by esc rejects pr', async () => { + await iavPage.clearAlerts() + await iavPage.wait(500) + const newTitle = await iavPage.execScript(() => window.document.title) + expect(newTitle).toEqual('failed') + }) + + it('> cancelling by pressing cancel rejects pr', async () => { + await iavPage.clickModalBtnByText('Cancel') + await iavPage.wait(500) + const newTitle = await iavPage.execScript(() => window.document.title) + expect(newTitle).toEqual('failed') + }) + + it('> on clicking region, resolves pr', async () => { + await iavPage.cursorMoveToAndClick({ position: [600, 490] }) + await iavPage.wait(500) + const newTitle = await iavPage.execScript(() => window.document.title) + expect(newTitle).toEqual(`success Area 6ma (preSMA, mesial SFG) - left hemisphere`) + }) + + it('> on failusre, clears modal', async () => { + + await iavPage.clearAlerts() + await iavPage.wait(500) + try { + const text = await iavPage.getModalText() + fail(`expected modal to clear, but modal has text ${text}`) + } catch (e) { + expect(true).toEqual(true) + } + }) + + it('> on success, clears modal', async () => { + + await iavPage.cursorMoveToAndClick({ position: [600, 490] }) + await iavPage.wait(500) + + try { + const text = await iavPage.getModalText() + fail(`expected modal to clear, but modal has text ${text}`) + } catch (e) { + expect(true).toEqual(true) + } + }) + }) }) }) diff --git a/e2e/src/util.js b/e2e/src/util.js index 2b000c603..f9a7519ff 100644 --- a/e2e/src/util.js +++ b/e2e/src/util.js @@ -334,9 +334,7 @@ class WdLayoutPage extends WdBase{ } _getModalBtns(){ - return this._getModal() - .findElement( By.tagName('mat-card-actions') ) - .findElements( By.tagName('button') ) + return this._getModal().findElements( By.tagName('button') ) } async getModalText(){ diff --git a/src/atlasViewer/atlasViewer.apiService.service.spec.ts b/src/atlasViewer/atlasViewer.apiService.service.spec.ts index e1164b83f..5260669fd 100644 --- a/src/atlasViewer/atlasViewer.apiService.service.spec.ts +++ b/src/atlasViewer/atlasViewer.apiService.service.spec.ts @@ -1,20 +1,23 @@ -import { } from 'jasmine' -import { AtlasViewerAPIServices } from "src/atlasViewer/atlasViewer.apiService.service"; -import { async, TestBed } from "@angular/core/testing"; -import { provideMockActions } from "@ngrx/effects/testing"; +import { AtlasViewerAPIServices, overrideNehubaClickFactory, CANCELLABLE_DIALOG } from "src/atlasViewer/atlasViewer.apiService.service"; +import { async, TestBed, fakeAsync, tick } from "@angular/core/testing"; import { provideMockStore } from "@ngrx/store/testing"; import { defaultRootState } from "src/services/stateStore.service"; -import { Observable, of } from "rxjs"; -import { Action } from "@ngrx/store"; import { AngularMaterialModule } from "src/ui/sharedModules/angularMaterial.module"; import { HttpClientModule } from '@angular/common/http'; import { WidgetModule } from './widgetUnit/widget.module'; import { PluginModule } from './pluginUnit/plugin.module'; -const actions$: Observable<Action> = of({ type: 'TEST' }) - describe('atlasViewer.apiService.service.ts', () => { - describe('getUserToSelectARegion', () => { + + describe('AtlasViewerAPIServices', () => { + + const cancelTokenSpy = jasmine.createSpy('cancelToken') + const cancellableDialogSpy = jasmine.createSpy('openCallableDialog').and.returnValue(cancelTokenSpy) + + afterEach(() => { + cancelTokenSpy.calls.reset() + cancellableDialogSpy.calls.reset() + }) beforeEach(async(() => { TestBed.configureTestingModule({ @@ -26,35 +29,391 @@ describe('atlasViewer.apiService.service.ts', () => { ], providers: [ AtlasViewerAPIServices, - provideMockActions(() => actions$), - provideMockStore({ initialState: defaultRootState }) + provideMockStore({ initialState: defaultRootState }), + { + provide: CANCELLABLE_DIALOG, + useValue: cancellableDialogSpy + } ] }).compileComponents() - })) + })) + + it('service exists', () => { + const service = TestBed.inject(AtlasViewerAPIServices) + expect(service).not.toBeNull() + }) + + describe('uiHandle', () => { + + describe('getUserToSelectARegion', () => { + + it('on init, expect getUserToSelectRegion to be length 0', () => { + const service = TestBed.inject(AtlasViewerAPIServices) + expect(service.getUserToSelectRegion.length).toEqual(0) + }) + it('calling getUserToSelectARegion() populates getUserToSelectRegion', () => { + const service = TestBed.inject(AtlasViewerAPIServices) - it('should return value on resolve', async () => { - const regionToSend = 'test-region' - let sentData: any - const apiService = TestBed.get(AtlasViewerAPIServices) - const callApi = apiService.interactiveViewer.uiHandle.getUserToSelectARegion('selecting Region mode message') - apiService.getUserToSelectARegionResolve(regionToSend) - await callApi.then(r => { - sentData = r + const pr = service.interactiveViewer.uiHandle.getUserToSelectARegion('hello world') + + expect(service.getUserToSelectRegion.length).toEqual(1) + const { promise, message, rs, rj } = service.getUserToSelectRegion[0] + expect(promise).toEqual(pr) + expect(message).toEqual('hello world') + + expect(rs).not.toBeUndefined() + expect(rs).not.toBeNull() + + expect(rj).not.toBeUndefined() + expect(rj).not.toBeNull() + }) }) - expect(sentData).toEqual(regionToSend) - }) - - it('pluginRegionSelectionEnabled should false after resolve', async () => { - const { uiState } = defaultRootState - const regionToSend = 'test-region' - let sentData: any - const apiService = TestBed.get(AtlasViewerAPIServices) - const callApi = apiService.interactiveViewer.uiHandle.getUserToSelectARegion('selecting Region mode message') - apiService.getUserToSelectARegionResolve(regionToSend) - await callApi.then(r => { - sentData = r + + describe('cancelPromise', () => { + it('calling cancelPromise removes pr from getUsertoSelectRegion', done => { + + const service = TestBed.inject(AtlasViewerAPIServices) + const pr = service.interactiveViewer.uiHandle.getUserToSelectARegion('test') + pr.catch(e => { + expect(e.userInitiated).toEqual(false) + expect(service.getUserToSelectRegion.length).toEqual(0) + done() + }) + service.interactiveViewer.uiHandle.cancelPromise(pr) + }) + + it('alling cancelPromise on non existing promise, throws ', () => { + + const service = TestBed.inject(AtlasViewerAPIServices) + const pr = service.interactiveViewer.uiHandle.getUserToSelectARegion('test') + service.interactiveViewer.uiHandle.cancelPromise(pr) + expect(() => { + service.interactiveViewer.uiHandle.cancelPromise(pr) + }).toThrow() + }) }) - expect(uiState.pluginRegionSelectionEnabled).toBe(false) + + describe('getUserToSelectARegion, cancelPromise and userCancel', () => { + it('if token is provided, on getUserToSelectRegionUI$ next should follow by call to injected function', () => { + const service = TestBed.inject(AtlasViewerAPIServices) + + const rsSpy = jasmine.createSpy('rs') + const rjSpy = jasmine.createSpy('rj') + const mockObj = { + message: 'test', + promise: new Promise((rs, rj) => {}), + rs: rsSpy, + rj: rjSpy, + } + service.getUserToSelectRegionUI$.next([ mockObj ]) + + + expect(cancellableDialogSpy).toHaveBeenCalled() + + const arg = cancellableDialogSpy.calls.mostRecent().args + expect(arg[0]).toEqual('test') + expect(arg[1].userCancelCallback).toBeTruthy() + }) + + it('if multiple regionUIs are provided, only the last one is used', () => { + const service = TestBed.inject(AtlasViewerAPIServices) + + const rsSpy = jasmine.createSpy('rs') + const rjSpy = jasmine.createSpy('rj') + const mockObj1 = { + message: 'test1', + promise: new Promise((rs, rj) => {}), + rs: rsSpy, + rj: rjSpy, + } + const mockObj2 = { + message: 'test2', + promise: new Promise((rs, rj) => {}), + rs: rsSpy, + rj: rjSpy, + } + service.getUserToSelectRegionUI$.next([ mockObj1, mockObj2 ]) + + expect(cancellableDialogSpy).toHaveBeenCalled() + + const arg = cancellableDialogSpy.calls.mostRecent().args + expect(arg[0]).toEqual('test2') + expect(arg[1].userCancelCallback).toBeTruthy() + }) + + describe('calling userCacellationCb', () => { + + it('correct usage => in removeBasedOnPr called, rj with userini as true', fakeAsync(() => { + const service = TestBed.inject(AtlasViewerAPIServices) + + const rsSpy = jasmine.createSpy('rs') + const rjSpy = jasmine.createSpy('rj') + const promise = new Promise((rs, rj) => {}) + const mockObj = { + message: 'test', + promise, + rs: rsSpy, + rj: rjSpy, + } + + const removeBaseOnPr = spyOn(service, 'removeBasedOnPr').and.returnValue(null) + + service.getUserToSelectRegionUI$.next([ mockObj ]) + const arg = cancellableDialogSpy.calls.mostRecent().args + const cb = arg[1].userCancelCallback + cb() + tick(100) + expect(rjSpy).toHaveBeenCalledWith({ userInitiated: true }) + expect(removeBaseOnPr).toHaveBeenCalledWith(promise, { userInitiated: true }) + + })) + + it('incorrect usage (resolve) => removebasedonpr, rj not called', fakeAsync(() => { + + const service = TestBed.inject(AtlasViewerAPIServices) + + const dummyObj = { + hello:'world' + } + + const rsSpy = jasmine.createSpy('rs') + const rjSpy = jasmine.createSpy('rj') + const promise = Promise.resolve(dummyObj) + const mockObj = { + message: 'test', + promise, + rs: rsSpy, + rj: rjSpy, + } + + const removeBaseOnPr = spyOn(service, 'removeBasedOnPr').and.returnValue(null) + + service.getUserToSelectRegionUI$.next([ mockObj ]) + const arg = cancellableDialogSpy.calls.mostRecent().args + const cb = arg[1].userCancelCallback + cb() + tick(100) + expect(rjSpy).not.toHaveBeenCalled() + expect(removeBaseOnPr).not.toHaveBeenCalled() + + })) + + it('incorrect usage (reject) => removebasedonpr, rj not called', fakeAsync(() => { + + const service = TestBed.inject(AtlasViewerAPIServices) + + const dummyObj = { + hello:'world' + } + + const rsSpy = jasmine.createSpy('rs') + const rjSpy = jasmine.createSpy('rj') + const promise = Promise.reject(dummyObj) + const mockObj = { + message: 'test', + promise, + rs: rsSpy, + rj: rjSpy, + } + + const removeBaseOnPr = spyOn(service, 'removeBasedOnPr').and.returnValue(null) + + service.getUserToSelectRegionUI$.next([ mockObj ]) + const arg = cancellableDialogSpy.calls.mostRecent().args + const cb = arg[1].userCancelCallback + cb() + tick(100) + expect(rjSpy).not.toHaveBeenCalled() + expect(removeBaseOnPr).not.toHaveBeenCalled() + + })) + }) + }) + }) + }) + + + describe('overrideNehubaClickFactory', () => { + + const OVERRIDE_NEHUBA_TOKEN = 'OVERRIDE_NEHUBA_TOKEN' + const MOCK_GET_MOUSEOVER_SEGMENTS_TOKEN = 'MOCK_GET_MOUSEOVER_SEGMENTS_TOKEN' + + let mockGetMouseOverSegments = [] + + afterEach(() => { + mockGetMouseOverSegments = [] + }) + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + AngularMaterialModule, + HttpClientModule, + WidgetModule, + PluginModule, + ], + providers: [ + { + provide: OVERRIDE_NEHUBA_TOKEN, + useFactory: overrideNehubaClickFactory, + deps: [ + AtlasViewerAPIServices, + MOCK_GET_MOUSEOVER_SEGMENTS_TOKEN, + ] + }, + { + provide: MOCK_GET_MOUSEOVER_SEGMENTS_TOKEN, + useValue: () => { + return mockGetMouseOverSegments + } + }, + AtlasViewerAPIServices, + provideMockStore({ initialState: defaultRootState }), + ] + }).compileComponents() + })) + + it('can obtain override fn', () => { + const fn = TestBed.inject(OVERRIDE_NEHUBA_TOKEN as any) + expect(fn).not.toBeNull() + }) + + it('by default, next fn will be called', () => { + const fn = TestBed.inject(OVERRIDE_NEHUBA_TOKEN as any) as (next: () => void) => void + const nextSpy = jasmine.createSpy('next') + fn(nextSpy) + expect(nextSpy).toHaveBeenCalled() + }) + + it('if both apiService.getUserToSelectRegion.length > 0 and mouseoverSegment.length >0, then next will not be called, but rs will be', () => { + const fn = TestBed.inject(OVERRIDE_NEHUBA_TOKEN as any) as (next: () => void) => void + const apiService = TestBed.inject(AtlasViewerAPIServices) + + const rsSpy = jasmine.createSpy('rs') + const rjSpy = jasmine.createSpy('rj') + apiService.getUserToSelectRegion = [ + { + message: 'test', + promise: null, + rs: rsSpy, + rj: rjSpy, + } + ] + + const mockSegment = { + layer: { + name: 'apple' + }, + segment: { + name: 'bananas' + } + } + mockGetMouseOverSegments = [ mockSegment ] + + const nextSpy = jasmine.createSpy('next') + fn(nextSpy) + + expect(nextSpy).not.toHaveBeenCalled() + expect(rsSpy).toHaveBeenCalledWith(mockSegment) + }) + + it('if apiService.getUserToSelectRegion.length === 0, and mouseoversegment.length > 0 calls next', () => { + const fn = TestBed.inject(OVERRIDE_NEHUBA_TOKEN as any) as (next: () => void) => void + + const mockSegment = { + layer: { + name: 'apple' + }, + segment: { + name: 'bananas' + } + } + mockGetMouseOverSegments = [ mockSegment ] + + const nextSpy = jasmine.createSpy('next') + fn(nextSpy) + + expect(nextSpy).toHaveBeenCalled() + }) + + it('if apiService.getUserToSelectRegion.length > 0, but mouseoversegment.length ===0, will not call next, will not rs, will not call rj', () => { + const fn = TestBed.inject(OVERRIDE_NEHUBA_TOKEN as any) as (next: () => void) => void + const apiService = TestBed.inject(AtlasViewerAPIServices) + + const rsSpy = jasmine.createSpy('rs') + const rjSpy = jasmine.createSpy('rj') + apiService.getUserToSelectRegion = [ + { + message: 'test', + promise: null, + rs: rsSpy, + rj: rjSpy, + } + ] + + const nextSpy = jasmine.createSpy('next') + fn(nextSpy) + + expect(rsSpy).not.toHaveBeenCalled() + expect(nextSpy).toHaveBeenCalled() + expect(rjSpy).not.toHaveBeenCalled() + }) + it('if muliple getUserToSelectRegion handler exists, it resolves in a FIFO manner', () => { + const fn = TestBed.inject(OVERRIDE_NEHUBA_TOKEN as any) as (next: () => void) => void + const apiService = TestBed.inject(AtlasViewerAPIServices) + + const rsSpy1 = jasmine.createSpy('rs1') + const rjSpy1 = jasmine.createSpy('rj1') + + const rsSpy2 = jasmine.createSpy('rs2') + const rjSpy2 = jasmine.createSpy('rj2') + apiService.getUserToSelectRegion = [ + { + message: 'test1', + promise: null, + rs: rsSpy1, + rj: rjSpy1, + }, + { + message: 'test2', + promise: null, + rs: rsSpy2, + rj: rjSpy2, + } + ] + + const mockSegment = { + layer: { + name: 'apple' + }, + segment: { + name: 'bananas' + } + } + + mockGetMouseOverSegments = [ mockSegment ] + + const nextSpy1 = jasmine.createSpy('next1') + fn(nextSpy1) + + expect(rsSpy2).toHaveBeenCalledWith(mockSegment) + expect(rjSpy2).not.toHaveBeenCalled() + + expect(nextSpy1).not.toHaveBeenCalled() + expect(rsSpy1).not.toHaveBeenCalled() + expect(rjSpy1).not.toHaveBeenCalled() + + const nextSpy2 = jasmine.createSpy('next2') + fn(nextSpy2) + + expect(nextSpy2).not.toHaveBeenCalled() + expect(rsSpy1).toHaveBeenCalledWith(mockSegment) + expect(rjSpy1).not.toHaveBeenCalled() + + const nextSpy3 = jasmine.createSpy('next3') + fn(nextSpy3) + + expect(nextSpy3).toHaveBeenCalled() }) }) -}) \ No newline at end of file +}) diff --git a/src/atlasViewer/atlasViewer.apiService.service.ts b/src/atlasViewer/atlasViewer.apiService.service.ts index b88de47af..d4e98aacc 100644 --- a/src/atlasViewer/atlasViewer.apiService.service.ts +++ b/src/atlasViewer/atlasViewer.apiService.service.ts @@ -1,10 +1,10 @@ -import {Injectable, NgZone} from "@angular/core"; +/* eslint-disable @typescript-eslint/no-empty-function */ +import {Injectable, NgZone, Optional, Inject, OnDestroy} from "@angular/core"; import { select, Store } from "@ngrx/store"; -import { Observable } from "rxjs"; -import { distinctUntilChanged, map, filter, startWith } from "rxjs/operators"; +import { Observable, Subject, Subscription, from, race, of, interval } from "rxjs"; +import { distinctUntilChanged, map, filter, startWith, take, switchMap, catchError, mapTo, tap } from "rxjs/operators"; import { DialogService } from "src/services/dialogService.service"; import { - DISABLE_PLUGIN_REGION_SELECTION, getLabelIndexMap, getMultiNgIdsRegionsLabelIndexMap, IavRootStoreInterface, @@ -13,15 +13,29 @@ import { import { ModalHandler } from "../util/pluginHandlerClasses/modalHandler"; import { ToastHandler } from "../util/pluginHandlerClasses/toastHandler"; import { IPluginManifest, PluginServices } from "./pluginUnit"; -import { ENABLE_PLUGIN_REGION_SELECTION } from "src/services/state/uiState.store"; declare let window +interface IRejectUserInput{ + userInitiated: boolean + reason?: string +} + +interface IGetUserSelectRegionPr{ + message: string + promise: Promise<any> + rs: (region:any) => void + rj: (reject:IRejectUserInput) => void +} + +export const CANCELLABLE_DIALOG = 'CANCELLABLE_DIALOG' +export const GET_TOAST_HANDLER_TOKEN = 'GET_TOAST_HANDLER_TOKEN' + @Injectable({ providedIn : 'root', }) -export class AtlasViewerAPIServices { +export class AtlasViewerAPIServices implements OnDestroy{ private loadedTemplates$: Observable<any> private selectParcellation$: Observable<any> @@ -29,15 +43,80 @@ export class AtlasViewerAPIServices { public loadedLibraries: Map<string, {counter: number, src: HTMLElement|null}> = new Map() - public getUserToSelectARegionResolve: any - public rejectUserSelectionMode: any + public removeBasedOnPr = (pr: Promise<any>, {userInitiated = false} = {}) => { + + const idx = this.getUserToSelectRegion.findIndex(({ promise }) => promise === pr) + if (idx >=0) { + const { rj } = this.getUserToSelectRegion.splice(idx, 1)[0] + this.getUserToSelectRegionUI$.next([...this.getUserToSelectRegion]) + this.zone.run(() => { }) + rj({ userInitiated }) + } + else throw new Error(`This promise has already been fulfilled.`) + + } + + private dismissDialog: Function + public getUserToSelectRegion: IGetUserSelectRegionPr[] = [] + public getUserToSelectRegionUI$: Subject<IGetUserSelectRegionPr[]> = new Subject() + + public getUserRegionSelectHandler: () => IGetUserSelectRegionPr = () => { + if (this.getUserToSelectRegion.length > 0) { + const handler = this.getUserToSelectRegion.pop() + this.getUserToSelectRegionUI$.next([...this.getUserToSelectRegion]) + return handler + } + else return null + } + + private s: Subscription[] = [] constructor( private store: Store<IavRootStoreInterface>, private dialogService: DialogService, private zone: NgZone, private pluginService: PluginServices, + @Optional() @Inject(CANCELLABLE_DIALOG) openCancellableDialog: (message: string, options: any) => () => void, + @Optional() @Inject(GET_TOAST_HANDLER_TOKEN) private getToastHandler: Function, ) { + if (openCancellableDialog) { + this.s.push( + this.getUserToSelectRegionUI$.pipe( + distinctUntilChanged(), + switchMap(arr => { + if (this.dismissDialog) { + this.dismissDialog() + this.dismissDialog = null + } + + if (arr.length === 0) return of(null) + + const last = arr[arr.length - 1] + const { message, promise } = last + return race( + from(new Promise(resolve => { + this.dismissDialog = openCancellableDialog(message, { + userCancelCallback: () => { + resolve(last) + }, + ariaLabel: message + }) + })), + from(promise).pipe( + catchError(() => of(null)), + mapTo(null), + ) + ) + }) + ).subscribe(obj => { + if (obj) { + const { promise, rj } = obj + rj({ userInitiated: true }) + this.removeBasedOnPr(promise, { userInitiated: true }) + } + }) + ) + } this.loadedTemplates$ = this.store.pipe( select('viewerState'), @@ -118,7 +197,8 @@ export class AtlasViewerAPIServices { /* to be overwritten by atlasViewer.component.ts */ getToastHandler : () => { - throw new Error('getToast Handler not overwritten by atlasViewer.component.ts') + if (this.getToastHandler) return this.getToastHandler() + else throw new Error('getToast Handler not overwritten by atlasViewer.component.ts') }, /** @@ -136,29 +216,33 @@ export class AtlasViewerAPIServices { getUserInput: config => this.dialogService.getUserInput(config) , getUserConfirmation: config => this.dialogService.getUserConfirm(config), - getUserToSelectARegion: (selectingMessage) => new Promise((resolve, reject) => { - this.zone.run(() => { - this.store.dispatch({ - type: ENABLE_PLUGIN_REGION_SELECTION, - payload: selectingMessage - }) - - this.getUserToSelectARegionResolve = resolve - this.rejectUserSelectionMode = reject + getUserToSelectARegion: message => { + const obj = { + message, + promise: null, + rs: null, + rj: null + } + const pr = new Promise((rs, rj) => { + obj.rs = rs + obj.rj = rj }) - }), - // ToDo Method should be able to cancel any pending promise. - cancelPromise: (pr) => { + obj.promise = pr + + this.getUserToSelectRegion.push(obj) + this.getUserToSelectRegionUI$.next([...this.getUserToSelectRegion]) this.zone.run(() => { - if (pr === this.interactiveViewer.uiHandle.getUserToSelectARegion) { - if (this.rejectUserSelectionMode) this.rejectUserSelectionMode() - this.store.dispatch({type: DISABLE_PLUGIN_REGION_SELECTION}) - } }) - } + return pr + }, + + cancelPromise: pr => { + this.removeBasedOnPr(pr) + this.zone.run(() => { }) + } }, pluginControl: new Proxy({}, { get: (_, prop) => { @@ -183,6 +267,12 @@ export class AtlasViewerAPIServices { this.interactiveViewer.metadata.layersRegionLabelIndexMap = getMultiNgIdsRegionsLabelIndexMap(parcellation) }) } + + ngOnDestroy(){ + while(this.s.length > 0){ + this.s.pop().unsubscribe() + } + } } export interface IInteractiveViewerInterface { @@ -267,3 +357,16 @@ export interface IUserLandmark { id: string /* probably use the it to track and remove user landmarks */ highlight: boolean } + +export const overrideNehubaClickFactory = (apiService: AtlasViewerAPIServices, getMouseoverSegments: () => any [] ) => { + return (next: () => void) => { + let moSegments = getMouseoverSegments() + if (!!moSegments && Array.isArray(moSegments) && moSegments.length > 0) { + const { rs } = apiService.getUserRegionSelectHandler() || {} + if (!!rs) { + return rs(moSegments[0]) + } + } + next() + } +} diff --git a/src/atlasViewer/atlasViewer.component.ts b/src/atlasViewer/atlasViewer.component.ts index 2ad1da357..c1d8dbee4 100644 --- a/src/atlasViewer/atlasViewer.component.ts +++ b/src/atlasViewer/atlasViewer.component.ts @@ -8,6 +8,8 @@ import { TemplateRef, ViewChild, ElementRef, + Inject, + Optional, } from "@angular/core"; import { ActionsSubject, select, Store } from "@ngrx/store"; import {combineLatest, interval, merge, Observable, of, Subscription} from "rxjs"; @@ -43,6 +45,8 @@ import { MouseHoverDirective } from "src/util/directives/mouseOver.directive"; import {MatSnackBar, MatSnackBarRef} from "@angular/material/snack-bar"; import {MatDialog, MatDialogRef} from "@angular/material/dialog"; +export const NEHUBA_CLICK_OVERRIDE = 'NEHUBA_CLICK_OVERRIDE' + /** * TODO * check against auxlillary mesh indicies, to only filter out aux indicies @@ -68,7 +72,6 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { @ViewChild('cookieAgreementComponent', {read: TemplateRef}) public cookieAgreementComponent: TemplateRef<any> private persistentStateNotifierMatDialogRef: MatDialogRef<any> - @ViewChild('persistentStateNotifierTemplate', {read: TemplateRef}) public persistentStateNotifierTemplate: TemplateRef<any> @ViewChild('kgToS', {read: TemplateRef}) public kgTosComponent: TemplateRef<any> @ViewChild(LayoutMainSide) public layoutMainSide: LayoutMainSide @@ -120,24 +123,17 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { public onhoverSegmentsForFixed$: Observable<string[]> - private pluginRegionSelectionEnabled$: Observable<boolean> - private pluginRegionSelectionEnabled: boolean = false - public persistentStateNotifierMessage$: Observable<string> - - private hoveringRegions = [] - public presentDatasetDialogRef: MatDialogRef<any> - constructor( private store: Store<IavRootStoreInterface>, private widgetServices: WidgetServices, private constantsService: AtlasViewerConstantsServices, - public apiService: AtlasViewerAPIServices, private matDialog: MatDialog, private dispatcher$: ActionsSubject, private rd: Renderer2, public localFileService: LocalFileService, private snackbar: MatSnackBar, private el: ElementRef, + @Optional() @Inject(NEHUBA_CLICK_OVERRIDE) private nehubaClickOverride: Function ) { this.snackbarMessage$ = this.store.pipe( @@ -145,17 +141,6 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { select("snackbarMessage"), ) - this.pluginRegionSelectionEnabled$ = this.store.pipe( - select('uiState'), - select("pluginRegionSelectionEnabled"), - distinctUntilChanged(), - ) - this.persistentStateNotifierMessage$ = this.store.pipe( - select('uiState'), - select("persistentStateNotifierMessage"), - distinctUntilChanged(), - ) - /** * TODO deprecated */ @@ -247,16 +232,6 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { ) - this.subscriptions.push( - this.onhoverSegments$.subscribe(hr => { - this.hoveringRegions = hr - }) - ) - - this.subscriptions.push( - this.pluginRegionSelectionEnabled$.subscribe(bool => this.pluginRegionSelectionEnabled = bool) - ) - const error = this.el.nativeElement.getAttribute('data-error') if (error) { @@ -345,15 +320,6 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { this.rd.setAttribute(document.body, 'darktheme', flag.toString()) }), ) - - this.subscriptions.push( - this.persistentStateNotifierMessage$.subscribe(msg => { - if (msg) this.persistentStateNotifierMatDialogRef = this.matDialog.open(this.persistentStateNotifierTemplate, { hasBackdrop: false, position: { top: '5px'} }) - else { - if (this.persistentStateNotifierMatDialogRef) this.persistentStateNotifierMatDialogRef.close() - } - }) - ) } public ngAfterViewInit() { @@ -421,18 +387,19 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit { public mouseClickNehuba(event) { - if (!this.rClContextualMenu) { return } - this.rClContextualMenu.mousePos = [ - event.clientX, - event.clientY, - ] + const next = () => { - // TODO what if user is hovering a landmark? - if (!this.pluginRegionSelectionEnabled) { + if (!this.rClContextualMenu) { return } + this.rClContextualMenu.mousePos = [ + event.clientX, + event.clientY, + ] + this.rClContextualMenu.show() - } else { - if (this.hoveringRegions) this.apiService.getUserToSelectARegionResolve(this.hoveringRegions) } + + this.nehubaClickOverride(next) + } public toggleSideNavMenu(opened) { diff --git a/src/main.module.ts b/src/main.module.ts index fa397086a..86d315965 100644 --- a/src/main.module.ts +++ b/src/main.module.ts @@ -2,19 +2,19 @@ import { DragDropModule } from '@angular/cdk/drag-drop' import { CommonModule } from "@angular/common"; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from "@angular/core"; import { FormsModule } from "@angular/forms"; -import { StoreModule } from "@ngrx/store"; +import { StoreModule, Store, select } from "@ngrx/store"; import { AngularMaterialModule } from 'src/ui/sharedModules/angularMaterial.module' -import { AtlasViewer } from "./atlasViewer/atlasViewer.component"; +import { AtlasViewer, NEHUBA_CLICK_OVERRIDE } from "./atlasViewer/atlasViewer.component"; import { ComponentsModule } from "./components/components.module"; import { LayoutModule } from "./layouts/layout.module"; -import { dataStore, ngViewerState, pluginState, uiState, userConfigState, UserConfigStateUseEffect, viewerConfigState, viewerState } from "./services/stateStore.service"; +import { dataStore, ngViewerState, pluginState, uiState, userConfigState, UserConfigStateUseEffect, viewerConfigState, viewerState, IavRootStoreInterface } from "./services/stateStore.service"; import { UIModule } from "./ui/ui.module"; import { GetNamePipe } from "./util/pipes/getName.pipe"; import { GetNamesPipe } from "./util/pipes/getNames.pipe"; import { HttpClientModule } from "@angular/common/http"; import { EffectsModule } from "@ngrx/effects"; -import { AtlasViewerAPIServices } from "./atlasViewer/atlasViewer.apiService.service"; +import { AtlasViewerAPIServices, overrideNehubaClickFactory, CANCELLABLE_DIALOG, GET_TOAST_HANDLER_TOKEN } from "./atlasViewer/atlasViewer.apiService.service"; import { AtlasWorkerService } from "./atlasViewer/atlasViewer.workerService.service"; import { ModalUnit } from "./atlasViewer/modalUnit/modalUnit.component"; import { TransformOnhoverSegmentPipe } from "./atlasViewer/onhoverSegment.pipe"; @@ -37,7 +37,7 @@ import { FloatingMouseContextualContainerDirective } from "./util/directives/flo import { NewViewerDisctinctViewToLayer } from "./util/pipes/newViewerDistinctViewToLayer.pipe"; import { UtilModule } from "./util/util.module"; -import { UiStateUseEffect } from "src/services/state/uiState.store"; +import { UiStateUseEffect, getMouseoverSegmentsFactory, GET_MOUSEOVER_SEGMENTS_TOKEN } from "src/services/state/uiState.store"; import { AtlasViewerHistoryUseEffect } from "./atlasViewer/atlasViewer.history.service"; import { PluginServiceUseEffect } from './services/effect/pluginUseEffect'; import { TemplateCoordinatesTransformation } from "src/services/templateCoordinatesTransformation.service"; @@ -45,13 +45,13 @@ import { NewTemplateUseEffect } from './services/effect/newTemplate.effect'; import { WidgetModule } from './atlasViewer/widgetUnit/widget.module'; import { PluginModule } from './atlasViewer/pluginUnit/plugin.module'; import { LoggingModule } from './logging/logging.module'; +import { ShareModule } from './share'; +import { AuthService } from './auth' import 'hammerjs' import 'src/res/css/extra_styles.css' import 'src/res/css/version.css' import 'src/theme.scss' -import { ShareModule } from './share'; -import { AuthService } from './auth' @NgModule({ imports : [ @@ -121,6 +121,64 @@ import { AuthService } from './auth' DialogService, UIService, TemplateCoordinatesTransformation, + { + provide: NEHUBA_CLICK_OVERRIDE, + useFactory: overrideNehubaClickFactory, + deps: [ + AtlasViewerAPIServices, + GET_MOUSEOVER_SEGMENTS_TOKEN + ] + }, + { + provide: GET_MOUSEOVER_SEGMENTS_TOKEN, + useFactory: getMouseoverSegmentsFactory, + deps: [ Store ] + }, + { + provide: GET_TOAST_HANDLER_TOKEN, + useFactory: (uiService: UIService) => { + return () => uiService.getToastHandler() + }, + deps: [ UIService ] + }, + { + provide: CANCELLABLE_DIALOG, + useFactory: (uiService: UIService) => { + return (message, option) => { + const actionBtn = { + type: 'mat-stroked-button', + color: 'default', + dismiss: true, + text: 'Cancel', + ariaLabel: 'Cancel' + } + const data = { + content: message, + config: { + sameLine: true + }, + actions: [ actionBtn ] + } + const { userCancelCallback, ariaLabel } = option + const dialogRef = uiService.showDialog(data, { + hasBackdrop: false, + position: { top: '5px'}, + ariaLabel + }) + + dialogRef.afterClosed().subscribe(closeReason => { + if (closeReason && closeReason.programmatic) return + if (closeReason && closeReason === actionBtn) return userCancelCallback() + if (!closeReason) return userCancelCallback() + }) + + return () => { + dialogRef.close({ userInitiated: false, programmatic: true }) + } + } + }, + deps: [ UIService ] + }, /** * TODO diff --git a/src/services/state/uiState.store.spec.ts b/src/services/state/uiState.store.spec.ts new file mode 100644 index 000000000..3ee389e6a --- /dev/null +++ b/src/services/state/uiState.store.spec.ts @@ -0,0 +1,65 @@ +import { TestBed, async } from "@angular/core/testing" +import { Component, Inject } from "@angular/core" +import { getMouseoverSegmentsFactory } from "./uiState.store" +import { Store } from "@ngrx/store" +import { provideMockStore } from "@ngrx/store/testing" +import { defaultRootState } from "../stateStore.service" + +const INJECTION_TOKEN = `INJECTION_TOKEN` + +@Component({ + template: '' +}) +class TestCmp{ + constructor( + @Inject(INJECTION_TOKEN) public getMouseoverSegments: Function + ){ + + } +} + +const dummySegment = { + layer: { + name: 'apple' + }, + segment: { + hello: 'world' + } +} + +describe('getMouseoverSegmentsFactory', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + TestCmp + ], + providers: [ + { + provide: INJECTION_TOKEN, + useFactory: getMouseoverSegmentsFactory, + deps: [ Store ] + }, + provideMockStore({ + initialState: { + ...defaultRootState, + uiState: { + ...defaultRootState.uiState, + mouseOverSegments: [ dummySegment ] + } + } + }) + ] + }).compileComponents() + })) + + it('should compile component', () => { + const fixture = TestBed.createComponent(TestCmp) + expect(fixture).toBeTruthy() + }) + + it('function should return dummy segment', () => { + const fixutre = TestBed.createComponent(TestCmp) + const result = fixutre.componentInstance.getMouseoverSegments() + expect(result).toEqual([dummySegment]) + }) +}) \ No newline at end of file diff --git a/src/services/state/uiState.store.ts b/src/services/state/uiState.store.ts index f02d62672..b2403dd86 100644 --- a/src/services/state/uiState.store.ts +++ b/src/services/state/uiState.store.ts @@ -3,7 +3,7 @@ import { Action, select, Store } from '@ngrx/store' import { Effect, Actions, ofType } from "@ngrx/effects"; import { Observable, Subscription } from "rxjs"; -import { filter, map, mapTo, scan, startWith } from "rxjs/operators"; +import { filter, map, mapTo, scan, startWith, take } from "rxjs/operators"; import { COOKIE_VERSION, KG_TOS_VERSION, LOCAL_STORAGE_CONST } from 'src/util/constants' import { IavRootStoreInterface } from '../stateStore.service' import { MatBottomSheetRef, MatBottomSheet } from '@angular/material/bottom-sheet'; @@ -22,9 +22,6 @@ export const defaultState: StateInterface = { snackbarMessage: null, - pluginRegionSelectionEnabled: false, - persistentStateNotifierMessage: null, - /** * replace with server side logic (?) */ @@ -108,21 +105,6 @@ export const getStateStore = ({ state = defaultState } = {}) => (prevState: Stat sidePanelCurrentViewContent: 'Dataset', } - case ENABLE_PLUGIN_REGION_SELECTION: { - return { - ...prevState, - pluginRegionSelectionEnabled: true, - persistentStateNotifierMessage: action.payload - } - } - case DISABLE_PLUGIN_REGION_SELECTION: { - return { - ...prevState, - pluginRegionSelectionEnabled: false, - persistentStateNotifierMessage: null - } - } - case AGREE_COOKIE: { /** * TODO replace with server side logic @@ -179,9 +161,6 @@ export interface StateInterface { snackbarMessage: symbol - pluginRegionSelectionEnabled: boolean - persistentStateNotifierMessage: string - agreedCookies: boolean agreedKgTos: boolean } @@ -203,6 +182,20 @@ export interface ActionInterface extends Action { payload: any } +export const GET_MOUSEOVER_SEGMENTS_TOKEN = `GET_MOUSEOVER_SEGMENTS_TOKEN` + +export const getMouseoverSegmentsFactory = (store: Store<IavRootStoreInterface>) => { + return () => { + let moSegments + store.pipe( + select('uiState'), + select('mouseOverSegments'), + take(1) + ).subscribe(v => moSegments = v) + return moSegments + } +} + @Injectable({ providedIn: 'root', }) @@ -287,9 +280,6 @@ export const HIDE_SIDE_PANEL_CONNECTIVITY = `HIDE_SIDE_PANEL_CONNECTIVITY` export const COLLAPSE_SIDE_PANEL_CURRENT_VIEW = `COLLAPSE_SIDE_PANEL_CURRENT_VIEW` export const EXPAND_SIDE_PANEL_CURRENT_VIEW = `EXPAND_SIDE_PANEL_CURRENT_VIEW` -export const ENABLE_PLUGIN_REGION_SELECTION = `ENABLE_PLUGIN_REGION_SELECTION` -export const DISABLE_PLUGIN_REGION_SELECTION = `DISABLE_PLUGIN_REGION_SELECTION` - export const AGREE_COOKIE = `AGREE_COOKIE` export const AGREE_KG_TOS = `AGREE_KG_TOS` export const SHOW_KG_TOS = `SHOW_KG_TOS` diff --git a/src/services/stateStore.service.ts b/src/services/stateStore.service.ts index 45aee75b4..654e6f439 100644 --- a/src/services/stateStore.service.ts +++ b/src/services/stateStore.service.ts @@ -52,7 +52,7 @@ export { userConfigState, USER_CONFIG_ACTION_TYPES} export { ADD_NG_LAYER, FORCE_SHOW_SEGMENT, HIDE_NG_LAYER, REMOVE_NG_LAYER, SHOW_NG_LAYER } from './state/ngViewerState.store' export { CHANGE_NAVIGATION, DESELECT_LANDMARKS, FETCHED_TEMPLATE, NEWVIEWER, SELECT_LANDMARKS, SELECT_PARCELLATION, SELECT_REGIONS, USER_LANDMARKS } from './state/viewerState.store' export { IDataEntry, IParcellationRegion, FETCHED_DATAENTRIES, FETCHED_SPATIAL_DATA, ILandmark, IOtherLandmarkGeometry, IPlaneLandmarkGeometry, IPointLandmarkGeometry, IProperty, IPublication, IReferenceSpace, IFile, IFileSupplementData } from './state/dataStore.store' -export { CLOSE_SIDE_PANEL, MOUSE_OVER_LANDMARK, MOUSE_OVER_SEGMENT, OPEN_SIDE_PANEL, SHOW_SIDE_PANEL_CONNECTIVITY, HIDE_SIDE_PANEL_CONNECTIVITY, COLLAPSE_SIDE_PANEL_CURRENT_VIEW, EXPAND_SIDE_PANEL_CURRENT_VIEW, ENABLE_PLUGIN_REGION_SELECTION, DISABLE_PLUGIN_REGION_SELECTION } from './state/uiState.store' +export { CLOSE_SIDE_PANEL, MOUSE_OVER_LANDMARK, MOUSE_OVER_SEGMENT, OPEN_SIDE_PANEL, SHOW_SIDE_PANEL_CONNECTIVITY, HIDE_SIDE_PANEL_CONNECTIVITY, COLLAPSE_SIDE_PANEL_CURRENT_VIEW, EXPAND_SIDE_PANEL_CURRENT_VIEW } from './state/uiState.store' export { UserConfigStateUseEffect } from './state/userConfigState.store' export const GENERAL_ACTION_TYPES = { diff --git a/src/services/uiService.service.ts b/src/services/uiService.service.ts index 26917c95d..dc6f4dda7 100644 --- a/src/services/uiService.service.ts +++ b/src/services/uiService.service.ts @@ -1,7 +1,8 @@ import { Injectable } from "@angular/core"; -import { AtlasViewerAPIServices } from "src/atlasViewer/atlasViewer.apiService.service"; import { ToastHandler } from "src/util/pluginHandlerClasses/toastHandler"; import {MatSnackBar, MatSnackBarConfig} from "@angular/material/snack-bar"; +import { MatDialog } from "@angular/material/dialog"; +import { ActionDialog } from "src/ui/actionDialog/actionDialog.component"; @Injectable({ providedIn: 'root', @@ -10,26 +11,34 @@ import {MatSnackBar, MatSnackBarConfig} from "@angular/material/snack-bar"; export class UIService { constructor( private snackbar: MatSnackBar, - private apiService: AtlasViewerAPIServices, + private dialog: MatDialog ) { - this.apiService.interactiveViewer.uiHandle.getToastHandler = () => { - const toasthandler = new ToastHandler() - let handle - toasthandler.show = () => { - handle = this.showMessage(toasthandler.message, null, { - duration: toasthandler.timeout, - }) - } + } + + public getToastHandler = () => { + const toasthandler = new ToastHandler() + let handle + toasthandler.show = () => { + handle = this.showMessage(toasthandler.message, null, { + duration: toasthandler.timeout, + }) + } - toasthandler.hide = () => { - if (handle) { handle.dismiss() } - handle = null - } - return toasthandler + toasthandler.hide = () => { + if (handle) { handle.dismiss() } + handle = null } + return toasthandler } public showMessage(message: string, actionBtnTxt: string = 'Dismiss', config?: Partial<MatSnackBarConfig>) { return this.snackbar.open(message, actionBtnTxt, config) } + + public showDialog(data, options){ + return this.dialog.open(ActionDialog, { + ...options, + data + }) + } } diff --git a/src/ui/actionDialog/actionDialog.component.ts b/src/ui/actionDialog/actionDialog.component.ts new file mode 100644 index 000000000..cea8c9536 --- /dev/null +++ b/src/ui/actionDialog/actionDialog.component.ts @@ -0,0 +1,36 @@ +import { Component, Optional, Inject } from "@angular/core"; +import { MAT_DIALOG_DATA } from "@angular/material/dialog"; + +interface IDialogAction{ + type: 'mat-button' | 'mat-flat-button' | 'mat-raised-button' | 'mat-stroked-button' + color: 'primary' | 'accent' | 'warn' | 'default' + dismiss: boolean + text: string + ariaLabel?: string +} + +@Component({ + templateUrl: './actionDialog.template.html' +}) + +export class ActionDialog{ + + public actions: IDialogAction[] = [] + public content: string + public sameLine: boolean = false + + constructor( + @Optional() @Inject(MAT_DIALOG_DATA) data:any + ){ + const { config, content, template, actions = [] } = data || {} + const { sameLine = false } = config || {} + + this.content = content + this.sameLine = sameLine + this.actions = actions + } + + actionHandler(event: MouseEvent, action: IDialogAction){ + // TODO fill in the actionHandler + } +} \ No newline at end of file diff --git a/src/ui/actionDialog/actionDialog.template.html b/src/ui/actionDialog/actionDialog.template.html new file mode 100644 index 000000000..095b27ee6 --- /dev/null +++ b/src/ui/actionDialog/actionDialog.template.html @@ -0,0 +1,63 @@ +<div mat-dialog-content class="d-inline-flex"> + <div class="flex-grow-1 flex-shrink-1 d-flex align-items-center mr-2"> + <span> + {{ content }} + </span> + </div> + + <!-- action btn if same line === true --> + <div *ngIf="sameLine" class="flex-grow-0 flex-shrink-0"> + <ng-container *ngTemplateOutlet="actionTemplate"> + </ng-container> + </div> +</div> + +<div *ngIf="!sameLine" mat-dialog-actions> + +</div> + +<ng-template #actionTemplate> + <ng-container *ngFor="let action of actions" + [ngSwitch]="action.type"> + + <ng-template #textNodeTmpl let-text="text"> + {{ text }} + </ng-template> + + <!-- mat-flat-button --> + <button *ngSwitchCase="'mat-flat-button'" + mat-flat-button + [color]="action.color || 'default'" + [mat-dialog-close]="action.dismiss && action" + (click)="actionHandler($event, action)"> + <ng-container *ngTemplateOutlet="textNodeTmpl; context: action"></ng-container> + </button> + + <!-- mat-raised-button --> + <button *ngSwitchCase="'mat-raised-button'" + mat-raised-button + [color]="action.color || 'default'" + [mat-dialog-close]="action.dismiss && action" + (click)="actionHandler($event, action)"> + <ng-container *ngTemplateOutlet="textNodeTmpl; context: action"></ng-container> + </button> + + <!-- mat-stroked-button --> + <button *ngSwitchCase="'mat-stroked-button'" + mat-stroked-button + [color]="action.color || 'default'" + [mat-dialog-close]="action.dismiss && action" + (click)="actionHandler($event, action)"> + <ng-container *ngTemplateOutlet="textNodeTmpl; context: action"></ng-container> + </button> + + <!-- default / mat-button --> + <button *ngSwitchDefault + mat-button + [color]="action.color || 'default'" + [mat-dialog-close]="action.dismiss && action" + (click)="actionHandler($event, action)"> + <ng-container *ngTemplateOutlet="textNodeTmpl; context: action"></ng-container> + </button> + </ng-container> +</ng-template> diff --git a/src/ui/ui.module.ts b/src/ui/ui.module.ts index 3b57eea7d..c26c918b5 100644 --- a/src/ui/ui.module.ts +++ b/src/ui/ui.module.ts @@ -84,6 +84,7 @@ import { LayerDetailComponent } from "./layerbrowser/layerDetail/layerDetail.com import { ShareModule } from "src/share"; import { StateModule } from "src/state"; import { AuthModule } from "src/auth"; +import { ActionDialog } from "./actionDialog/actionDialog.component"; @NgModule({ imports : [ @@ -142,6 +143,8 @@ import { AuthModule } from "src/auth"; RegionListSimpleViewComponent, LandmarkUIComponent, + ActionDialog, + /* pipes */ GroupDatasetByRegion, FilterRegionDataEntries, @@ -181,6 +184,7 @@ import { AuthModule } from "src/auth"; NehubaViewerUnit, LayerBrowser, PluginBannerUI, + ActionDialog, ], exports : [ SubjectViewer, -- GitLab