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