From 2033d7aa93570a0b3d2e774f0b3ebe58c9c86d88 Mon Sep 17 00:00:00 2001 From: Xiao Gui Date: Mon, 4 Oct 2021 13:32:40 +0200 Subject: [PATCH 1/4] chore: clean up spacebar interception --- docs/releases/v2.4.8.md | 7 ++ mkdocs.yml | 1 + package.json | 2 +- spec/test.ts | 12 ++- src/services/state/ngViewerState.store.ts | 17 +-- src/services/state/ngViewerState/actions.ts | 4 + .../keyDownListener.directive.spec.ts | 102 ++++++++++++++++++ .../directives/keyDownListener.directive.ts | 2 +- .../nehubaViewerGlue.component.ts | 14 ++- .../nehubaViewerGlue.template.html | 2 + 10 files changed, 140 insertions(+), 23 deletions(-) create mode 100644 docs/releases/v2.4.8.md create mode 100644 src/util/directives/keyDownListener.directive.spec.ts diff --git a/docs/releases/v2.4.8.md b/docs/releases/v2.4.8.md new file mode 100644 index 000000000..a448d53a7 --- /dev/null +++ b/docs/releases/v2.4.8.md @@ -0,0 +1,7 @@ +# v2.4.8 + +## Bugfixes + +## Under the hood stuff + +- Tweaked space bar capture diff --git a/mkdocs.yml b/mkdocs.yml index 028efd27c..0adf2bba2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -40,6 +40,7 @@ pages: - Fetching datasets: 'advanced/datasets.md' - Display non-atlas volumes: 'advanced/otherVolumes.md' - Release notes: + - v2.4.8: 'releases/v2.4.8.md' - v2.4.7: 'releases/v2.4.7.md' - v2.4.6: 'releases/v2.4.6.md' - v2.4.5: 'releases/v2.4.5.md' diff --git a/package.json b/package.json index 5bc603a1b..50a1c4ba9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "interactive-viewer", - "version": "2.4.7", + "version": "2.4.8", "description": "HBP interactive atlas viewer. Integrating KG query, dataset previews & more. Based on humanbrainproject/nehuba & google/neuroglancer. Built with angular", "scripts": { "build-aot": "PRODUCTION=true GIT_HASH=`node -e 'console.log(require(\"./package.json\").version)'` webpack --config ./webpack/webpack.aot.js && node ./third_party/matomo/processMatomo.js", diff --git a/spec/test.ts b/spec/test.ts index 478be25bb..194743b2e 100644 --- a/spec/test.ts +++ b/spec/test.ts @@ -18,10 +18,12 @@ getTestBed().initTestEnvironment( platformBrowserDynamicTesting() ); -const testContext = require.context('../src', true, /\.spec\.ts$/) -testContext.keys().map(testContext) +// const testContext = require.context('../src', true, /\.spec\.ts$/) +// testContext.keys().map(testContext) -const workerCtx = require.context('../worker', true, /\.spec\.js$/) -workerCtx.keys().map(workerCtx) +// const workerCtx = require.context('../worker', true, /\.spec\.js$/) +// workerCtx.keys().map(workerCtx) -require('../common/util.spec.js') +// require('../common/util.spec.js') + +require('../src/util/directives/keyDownListener.directive.spec') \ No newline at end of file diff --git a/src/services/state/ngViewerState.store.ts b/src/services/state/ngViewerState.store.ts index 694a315fb..f0ad5000d 100644 --- a/src/services/state/ngViewerState.store.ts +++ b/src/services/state/ngViewerState.store.ts @@ -9,7 +9,7 @@ import { HttpClient } from '@angular/common/http'; import { INgLayerInterface, ngViewerActionAddNgLayer, ngViewerActionRemoveNgLayer, ngViewerActionSetPerspOctantRemoval } from './ngViewerState.store.helper' import { PureContantService } from 'src/util'; import { PANELS } from './ngViewerState.store.helper' -import { ngViewerActionToggleMax, ngViewerActionClearView, ngViewerActionSetPanelOrder, ngViewerActionSwitchPanelMode, ngViewerActionForceShowSegment, ngViewerActionNehubaReady } from './ngViewerState/actions'; +import { ngViewerActionToggleMax, ngViewerActionClearView, ngViewerActionSetPanelOrder, ngViewerActionSwitchPanelMode, ngViewerActionForceShowSegment, ngViewerActionNehubaReady, ngViewerActionCycleViews } from './ngViewerState/actions'; import { generalApplyState } from '../stateStore.helper'; import { ngViewerSelectorPanelMode, ngViewerSelectorPanelOrder } from './ngViewerState/selectors'; import { uiActionSnackbarMessage } from './uiState/actions'; @@ -172,9 +172,6 @@ export class NgViewerUseEffect implements OnDestroy { @Effect() public cycleViews$: Observable - @Effect() - public spacebarListener$: Observable - @Effect() public removeAllNonBaseLayers$: Observable @@ -255,7 +252,7 @@ export class NgViewerUseEffect implements OnDestroy { ) this.cycleViews$ = this.actions.pipe( - ofType(ACTION_TYPES.CYCLE_VIEWS), + ofType(ngViewerActionCycleViews.type), withLatestFrom(this.panelOrder$), map(([_, panelOrder]) => { return ngViewerActionSetPanelOrder({ @@ -351,15 +348,6 @@ export class NgViewerUseEffect implements OnDestroy { })), ) - this.spacebarListener$ = fromEvent(document.body, 'keydown', { capture: true }).pipe( - filter((ev: KeyboardEvent) => ev.key === ' ' && (ev.target as HTMLElement).classList.contains('neuroglancer-panel')), - withLatestFrom(this.panelMode$), - filter(([_ , panelMode]) => panelMode === PANELS.SINGLE_PANEL), - mapTo({ - type: ACTION_TYPES.CYCLE_VIEWS, - }), - ) - /** * simplify with layer browser */ @@ -427,7 +415,6 @@ export class NgViewerUseEffect implements OnDestroy { export { INgLayerInterface } const ACTION_TYPES = { - CYCLE_VIEWS: 'CYCLE_VIEWS', REMOVE_ALL_NONBASE_LAYERS: `REMOVE_ALL_NONBASE_LAYERS`, } diff --git a/src/services/state/ngViewerState/actions.ts b/src/services/state/ngViewerState/actions.ts index 7f864defa..c504a085a 100644 --- a/src/services/state/ngViewerState/actions.ts +++ b/src/services/state/ngViewerState/actions.ts @@ -66,3 +66,7 @@ export const ngViewerActionClearView = createAction( `[ngViewerAction] clearView`, props<{ payload: { [key: string]: boolean }}>() ) + +export const ngViewerActionCycleViews = createAction( + `[ngViewerAction] cycleView` +) \ No newline at end of file diff --git a/src/util/directives/keyDownListener.directive.spec.ts b/src/util/directives/keyDownListener.directive.spec.ts new file mode 100644 index 000000000..7be542f0d --- /dev/null +++ b/src/util/directives/keyDownListener.directive.spec.ts @@ -0,0 +1,102 @@ +import { DOCUMENT } from "@angular/common" +import { Component } from "@angular/core" +import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing" +import { By } from "@angular/platform-browser" +import { KeyListenerConfig, KeyListner } from "./keyDownListener.directive" + +@Component({ + template: `` +}) + +class DummyCmp{ + public keyConfig: KeyListenerConfig[]=[{ + type: 'keydown', + key: 'a', + },{ + type: 'keyup', + key: 'a', + },{ + type: 'keydown', + key: 'd', + target: 'document', + capture: true + },{ + type: 'keydown', + key: 'e', + target: 'document' + }] + + // will get spied on + public listener(event: any){ + console.log('lister called') + } +} + +const inputId = `text-input` +describe('KeyListner', () => { + beforeEach(async () => { + TestBed.configureTestingModule({ + imports: [], + declarations: [ + KeyListner, + DummyCmp + ], + }).overrideComponent(DummyCmp, { + set: { + template: ` +
+
+
+
+
+ ` + } + }) + + await TestBed.compileComponents() + }) + + it('> creates component just fine', () => { + const fixture = TestBed.createComponent(DummyCmp) + expect(fixture).toBeTruthy() + }) + it('> Directive is created', () => { + const fixture = TestBed.createComponent(DummyCmp) + const keyListenerDirective = fixture.debugElement.query(By.directive(KeyListner)) + expect(keyListenerDirective).toBeTruthy() + }) + + describe('> directive working as intended', () => { + let eventListSpy: jasmine.Spy + let fixture: ComponentFixture + beforeEach(() => { + fixture = TestBed.createComponent(DummyCmp) + eventListSpy = spyOn(fixture.componentInstance, 'listener') + fixture.detectChanges() + }) + describe('> if dispatch element was host element', () => { + it('> should trigger event', () => { + const newKeybEv = new KeyboardEvent('keydown', { + key: 'd' + }) + const nativeEl = fixture.nativeElement as HTMLElement + nativeEl.dispatchEvent(newKeybEv) + + expect(eventListSpy).toHaveBeenCalled() + }) + }) + describe('> if dispatch element was input', () => { + it('> should not trigger event listener', () => { + const newKeybEv = new KeyboardEvent('keydown', { + key: 'd' + }) + const nativeEl = fixture.debugElement.query(By.css(`#${inputId}`)).nativeElement as HTMLElement + nativeEl.dispatchEvent(newKeybEv) + + expect(eventListSpy).not.toHaveBeenCalled() + }) + }) + }) +}) diff --git a/src/util/directives/keyDownListener.directive.ts b/src/util/directives/keyDownListener.directive.ts index f3fc055d1..e400fcea1 100644 --- a/src/util/directives/keyDownListener.directive.ts +++ b/src/util/directives/keyDownListener.directive.ts @@ -100,7 +100,7 @@ export interface KeyListenerConfig { key: string target?: 'document' capture?: boolean - stop: boolean + stop?: boolean // fromEvent seems to be a passive listener, wheather or not { passive: false } flag is set or not // so preventDefault cannot be called anyway } diff --git a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts index c071875bb..4a0b345d3 100644 --- a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts +++ b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts @@ -1,7 +1,7 @@ import { AfterViewInit, Component, ElementRef, EventEmitter, Inject, Input, OnChanges, OnDestroy, Optional, Output, SimpleChanges, ViewChild } from "@angular/core"; import { select, Store } from "@ngrx/store"; import { asyncScheduler, combineLatest, fromEvent, merge, Observable, of, Subject } from "rxjs"; -import { ngViewerActionToggleMax } from "src/services/state/ngViewerState/actions"; +import { ngViewerActionCycleViews, ngViewerActionToggleMax } from "src/services/state/ngViewerState/actions"; import { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR } from "src/util"; import { uiStateMouseOverSegmentsSelector } from "src/services/state/uiState/selectors"; import { debounceTime, distinctUntilChanged, filter, map, mapTo, scan, shareReplay, startWith, switchMap, switchMapTo, take, tap, throttleTime } from "rxjs/operators"; @@ -70,6 +70,8 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnChanges, OnDestroy, A public ARIA_LABELS = ARIA_LABELS public IDS = IDS + private currentPanelMode: PANELS + @ViewChild(NehubaViewerContainerDirective, { static: true }) public nehubaContainerDirective: NehubaViewerContainerDirective @@ -325,6 +327,9 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnChanges, OnDestroy, A ]).pipe( switchMap(this.waitForNehuba.bind(this)) ).subscribe(([mode, panelOrder]) => { + + this.currentPanelMode = mode + const viewPanels = panelOrder.split('').map(v => Number(v)).map(idx => this.viewPanels[idx]) as [HTMLElement, HTMLElement, HTMLElement, HTMLElement] /** @@ -636,6 +641,13 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnChanges, OnDestroy, A this.onDestroyCb.push(() => navSub.unsubscribe()) } + handleCycleViewEvent(){ + if (this.currentPanelMode !== PANELS.SINGLE_PANEL) return + this.store$.dispatch( + ngViewerActionCycleViews() + ) + } + handleViewerLoadedEvent(flag: boolean) { this.viewerEvent.emit({ type: EnumViewerEvt.VIEWERLOADED, diff --git a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.template.html b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.template.html index d79c49a59..158dd9a41 100644 --- a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.template.html +++ b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.template.html @@ -10,6 +10,8 @@ iav-nehuba-viewer-container #iavContainer="iavNehubaViewerContainer" iav-mouse-hover + [iav-key-listener]="[{ type: 'keydown', key: ' ', target: 'document', capture: true }]" + (iav-key-event)="handleCycleViewEvent()" (iavNehubaViewerContainerViewerLoading)="handleViewerLoadedEvent($event)"> From 340539092390a78961c114064dd49cb8fbb40566 Mon Sep 17 00:00:00 2001 From: Xiao Gui Date: Mon, 4 Oct 2021 15:50:50 +0200 Subject: [PATCH 2/4] feat: added no. ann badge feat: delete all ann btn feat: loading ann feedback --- common/constants.js | 8 ++- docs/releases/v2.4.8.md | 6 +++ .../annotationList.component.ts | 49 +++++++++++++++++-- .../annotationList.template.html | 9 ++++ .../confirmDialog/confirmDialog.component.ts | 5 +- .../confirmDialog/confirmDialog.template.html | 2 +- src/services/dialogService.service.ts | 19 ++++++- .../viewerCmp/viewerCmp.template.html | 12 +++-- 8 files changed, 98 insertions(+), 12 deletions(-) diff --git a/common/constants.js b/common/constants.js index 64e7626ba..c882aaec1 100644 --- a/common/constants.js +++ b/common/constants.js @@ -70,7 +70,8 @@ USER_ANNOTATION_HIDE: 'user annotations hide', USER_ANNOTATION_DELETE: 'Delete annotation', GOTO_ANNOTATION_ROI: 'Navigate to annotation location of interest', - EXIT_ANNOTATION_MODE: 'Exit annotation mode' + EXIT_ANNOTATION_MODE: 'Exit annotation mode', + BULK_DELETE_ANNOTATIONS: 'Delete all user annotations' } exports.IDS = { @@ -79,6 +80,8 @@ } exports.CONST = { + LOADING_TXT: `Loading ...`, + CANNOT_DECIPHER_HEMISPHERE: 'Cannot decipher region hemisphere.', DOES_NOT_SUPPORT_MULTI_REGION_SELECTION: `Please only select a single region.`, MULTI_REGION_SELECTION: `Multi region selection`, @@ -103,6 +106,9 @@ QUICKTOUR_OK: `Start`, QUICKTOUR_NEXTTIME: `Not now`, QUICKTOUR_CANCEL: `Dismiss`, + + DELETE_ALL_ANNOTATION_CONFIRMATION_MSG: `Are you sure you want to delete all annotations?`, + LOADING_ANNOTATION_MSG: `Loading annotations... Please wait...` } exports.QUICKTOUR_DESC ={ diff --git a/docs/releases/v2.4.8.md b/docs/releases/v2.4.8.md index a448d53a7..b55bca636 100644 --- a/docs/releases/v2.4.8.md +++ b/docs/releases/v2.4.8.md @@ -1,5 +1,11 @@ # v2.4.8 +## Enhancements + +- Added badges to annotation tab button to show number of annotations added (#1007) +- Added some feedbacks when annotations are being loaded +- Added delete all annotation button + ## Bugfixes ## Under the hood stuff diff --git a/src/atlasComponents/userAnnotations/annotationList/annotationList.component.ts b/src/atlasComponents/userAnnotations/annotationList/annotationList.component.ts index f3b0e82f7..91f2fd090 100644 --- a/src/atlasComponents/userAnnotations/annotationList/annotationList.component.ts +++ b/src/atlasComponents/userAnnotations/annotationList/annotationList.component.ts @@ -1,15 +1,16 @@ -import {Component, ViewChild} from "@angular/core"; -import {ARIA_LABELS} from "common/constants"; +import { Component, Optional, ViewChild } from "@angular/core"; +import { ARIA_LABELS, CONST } from "common/constants"; import { ModularUserAnnotationToolService } from "../tools/service"; import { IAnnotationGeometry, TExportFormats } from "../tools/type"; import { ComponentStore } from "src/viewerModule/componentStore"; import { map, shareReplay, startWith } from "rxjs/operators"; -import { Observable } from "rxjs"; +import { Observable, Subscription } from "rxjs"; import { TZipFileConfig } from "src/zipFilesOutput/type"; import { TFileInputEvent } from "src/getFileInput/type"; import { FileInputDirective } from "src/getFileInput/getFileInput.directive"; import { MatSnackBar } from "@angular/material/snack-bar"; import { unzip } from "src/zipFilesOutput/zipFilesOutput.directive"; +import { DialogService } from "src/services/dialogService.service"; const README = `{id}.sands.json file contains the data of annotations. {id}.desc.json contains the metadata of annotations.` @@ -26,12 +27,17 @@ export class AnnotationList { public ARIA_LABELS = ARIA_LABELS - @ViewChild(FileInputDirective) fileInput: FileInputDirective + private subs: Subscription[] = [] + private managedAnnotations: IAnnotationGeometry[] = [] public managedAnnotations$ = this.annotSvc.spaceFilteredManagedAnnotations$ + public badge$ = this.managedAnnotations$.pipe( + map(mann => mann.length > 0 ? mann.length : null) + ) + public manAnnExists$ = this.managedAnnotations$.pipe( map(arr => !!arr && arr.length > 0), startWith(false) @@ -64,10 +70,15 @@ export class AnnotationList { private annotSvc: ModularUserAnnotationToolService, private snackbar: MatSnackBar, cStore: ComponentStore<{ useFormat: TExportFormats }>, + @Optional() private dialogSvc: DialogService, ) { cStore.setState({ useFormat: 'sands' }) + + this.subs.push( + this.managedAnnotations$.subscribe(anns => this.managedAnnotations = anns) + ) } public hiddenAnnotations$ = this.annotSvc.hiddenAnnotations$ @@ -82,6 +93,11 @@ export class AnnotationList { } async handleImportEvent(ev: TFileInputEvent<'text' | 'file'>){ + + const { abort } = this.dialogSvc.blockUserInteraction({ + title: CONST.LOADING_TXT, + markdown: CONST.LOADING_ANNOTATION_MSG, + }) try { const clearFileInputAndInform = () => { if (this.fileInput) { @@ -135,6 +151,31 @@ export class AnnotationList { this.snackbar.open(`Error importing: ${e.toString()}`, 'Dismiss', { duration: 3000 }) + } finally { + abort() + } + } + + async deleteAllAnnotation(){ + if (this.dialogSvc) { + try { + await this.dialogSvc.getUserConfirm({ + markdown: CONST.DELETE_ALL_ANNOTATION_CONFIRMATION_MSG + }) + + for (const ann of this.managedAnnotations) { + ann.remove() + } + } catch (e) { + // aborted + } + } else { + if (window.confirm(CONST.DELETE_ALL_ANNOTATION_CONFIRMATION_MSG)) { + + for (const ann of this.managedAnnotations) { + ann.remove() + } + } } } } diff --git a/src/atlasComponents/userAnnotations/annotationList/annotationList.template.html b/src/atlasComponents/userAnnotations/annotationList/annotationList.template.html index 2bfd568c0..00bdaed21 100644 --- a/src/atlasComponents/userAnnotations/annotationList/annotationList.template.html +++ b/src/atlasComponents/userAnnotations/annotationList/annotationList.template.html @@ -33,6 +33,15 @@ [disabled]="!(manAnnExists$ | async)"> + + + diff --git a/src/components/confirmDialog/confirmDialog.component.ts b/src/components/confirmDialog/confirmDialog.component.ts index 5c3d9eb02..ec2e20c9f 100644 --- a/src/components/confirmDialog/confirmDialog.component.ts +++ b/src/components/confirmDialog/confirmDialog.component.ts @@ -25,12 +25,15 @@ export class ConfirmDialogComponent { @Input() public markdown: string + public hideActionBar = false + constructor(@Inject(MAT_DIALOG_DATA) data: any) { - const { title = null, message = null, markdown, okBtnText, cancelBtnText} = data || {} + const { title = null, message = null, markdown, okBtnText, cancelBtnText, hideActionBar} = data || {} if (title) this.title = title if (message) this.message = message if (markdown) this.markdown = markdown if (okBtnText) this.okBtnText = okBtnText if (cancelBtnText) this.cancelBtnText = cancelBtnText + if (hideActionBar) this.hideActionBar = hideActionBar } } diff --git a/src/components/confirmDialog/confirmDialog.template.html b/src/components/confirmDialog/confirmDialog.template.html index 401261f1a..f5f054946 100644 --- a/src/components/confirmDialog/confirmDialog.template.html +++ b/src/components/confirmDialog/confirmDialog.template.html @@ -17,7 +17,7 @@

- + diff --git a/src/services/dialogService.service.ts b/src/services/dialogService.service.ts index c202eda58..4426ef8c7 100644 --- a/src/services/dialogService.service.ts +++ b/src/services/dialogService.service.ts @@ -3,6 +3,10 @@ import { ConfirmDialogComponent } from "src/components/confirmDialog/confirmDial import { DialogComponent } from "src/components/dialog/dialog.component"; import {MatDialog, MatDialogRef} from "@angular/material/dialog"; +type TCancellable = { + abort: () => void +} + @Injectable({ providedIn: 'root', }) @@ -16,13 +20,26 @@ export class DialogService { } + public blockUserInteraction(config: Partial): TCancellable { + const dialogRef = this.dialog.open(ConfirmDialogComponent, { + data: { + ...config, + hideActionBar: true + }, + hasBackdrop: true, + disableClose: true + }) + const abort = () => dialogRef.close() + return { abort } + } + public getUserConfirm(config: Partial = {}): Promise { this.confirmDialogRef = this.dialog.open(ConfirmDialogComponent, { data: config, }) return new Promise((resolve, reject) => this.confirmDialogRef.afterClosed() .subscribe(val => { - if (val) { resolve() } else { reject('User cancelled') } + if (val) { resolve('') } else { reject('User cancelled') } }, reject, () => this.confirmDialogRef = null)) diff --git a/src/viewerModule/viewerCmp/viewerCmp.template.html b/src/viewerModule/viewerCmp/viewerCmp.template.html index 20997801b..ca92a5a01 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.template.html +++ b/src/viewerModule/viewerCmp/viewerCmp.template.html @@ -16,7 +16,7 @@ [autoFocus]="false" [disableClose]="true" class="p-0 pe-all col-10 col-sm-10 col-md-5 col-lg-4 col-xl-3 col-xxl-2"> - + @@ -30,7 +30,8 @@ matColor: 'primary', fontIcon: 'fa-list', tooltip: 'Annotation list', - click: annotationDrawer.toggle.bind(annotationDrawer) + click: annotationDrawer.toggle.bind(annotationDrawer), + badge: annListCmp?.badge$ | async }"> @@ -58,7 +59,6 @@ - @@ -390,6 +390,8 @@ let-customColor="customColor" let-customColorDarkmode="customColorDarkmode" let-tooltip="tooltip" + let-badge="badge" + let-badgeColor="badgeColor" let-click="click">