diff --git a/src/atlasComponents/parcellation/regionSearch/regionSearch.component.ts b/src/atlasComponents/parcellation/regionSearch/regionSearch.component.ts index 87a600839de215e162a3baf9dd6ba82d7446c8eb..b3db031c4416658fed8c1de4d7936607f3f5e74e 100644 --- a/src/atlasComponents/parcellation/regionSearch/regionSearch.component.ts +++ b/src/atlasComponents/parcellation/regionSearch/regionSearch.component.ts @@ -4,7 +4,7 @@ import { select, Store } from "@ngrx/store"; import { combineLatest, Observable, Subject, merge } from "rxjs"; import { debounceTime, distinctUntilChanged, filter, map, shareReplay, startWith, take, tap, withLatestFrom } from "rxjs/operators"; import { VIEWER_STATE_ACTION_TYPES } from "src/services/effect/effect"; -import { ADD_TO_REGIONS_SELECTION_WITH_IDS, CHANGE_NAVIGATION, SELECT_REGIONS } from "src/services/state/viewerState.store"; +import { CHANGE_NAVIGATION, SELECT_REGIONS } from "src/services/state/viewerState.store"; import { getMultiNgIdsRegionsLabelIndexMap } from "src/services/stateStore.service"; import { LoggingService } from "src/logging"; import { MatDialog } from "@angular/material/dialog"; @@ -13,6 +13,7 @@ import { PureContantService } from "src/util"; import { viewerStateToggleRegionSelect, viewerStateNavigateToRegion, viewerStateSetSelectedRegions, viewerStateSetSelectedRegionsWithIds } from "src/services/state/viewerState.store.helper"; import { ARIA_LABELS, CONST } from 'common/constants' import { serialiseParcellationRegion } from "common/util" +import { actionAddToRegionsSelectionWithIds } from "src/services/state/viewerState/actions"; const filterRegionBasedOnText = searchTerm => region => `${region.name.toLowerCase()}${region.status? ' (' + region.status + ')' : null}`.includes(searchTerm.toLowerCase()) || (region.relatedAreas && region.relatedAreas.some(relatedArea => relatedArea.name && relatedArea.name.toLowerCase().includes(searchTerm.toLowerCase()))) @@ -140,10 +141,11 @@ export class RegionTextSearchAutocomplete { deselecRegionIds: [id], }) } else { - this.store$.dispatch({ - type: ADD_TO_REGIONS_SELECTION_WITH_IDS, - selectRegionIds : [id], - }) + this.store$.dispatch( + actionAddToRegionsSelectionWithIds({ + selectRegionIds : [id], + }) + ) } } diff --git a/src/services/effect/effect.ts b/src/services/effect/effect.ts index 2b6f202f21728f6d085f31e6e4c3f17e757504dc..10e5506143f0963ddaea23b81bd3e09509c8623b 100644 --- a/src/services/effect/effect.ts +++ b/src/services/effect/effect.ts @@ -4,11 +4,11 @@ import { select, Store } from "@ngrx/store"; import { merge, Observable, Subscription, combineLatest } from "rxjs"; import { filter, map, shareReplay, switchMap, take, withLatestFrom, mapTo, distinctUntilChanged } from "rxjs/operators"; import { LoggingService } from "src/logging"; -import { ADD_TO_REGIONS_SELECTION_WITH_IDS, DESELECT_REGIONS, SELECT_PARCELLATION, SELECT_REGIONS, SELECT_REGIONS_WITH_ID, SELECT_LANDMARKS } from "../state/viewerState.store"; import { IavRootStoreInterface, recursiveFindRegionWithLabelIndexId } from '../stateStore.service'; import { viewerStateNewViewer, viewerStateSelectAtlas, viewerStateSetSelectedRegionsWithIds, viewerStateToggleLayer } from "../state/viewerState.store.helper"; import { deserialiseParcRegionId, serialiseParcellationRegion } from "common/util" import { getGetRegionFromLabelIndexId } from 'src/util/fn' +import { actionAddToRegionsSelectionWithIds, actionSelectLandmarks, viewerStateSelectParcellation, viewerStateSelectRegionWithIdDeprecated, viewerStateSetSelectedRegions } from "../state/viewerState/actions"; @Injectable({ providedIn: 'root', @@ -53,26 +53,10 @@ export class UseEffects implements OnDestroy { /** * only allow 1 selection at a time */ - return { - type: SELECT_REGIONS, + return viewerStateSetSelectedRegions({ selectRegions: selectRegions.slice(0,1) - } - }) - ) - - this.onDeselectRegions = this.actions$.pipe( - ofType(DESELECT_REGIONS), - withLatestFrom(this.regionsSelected$), - map(([action, regionsSelected]) => { - const { deselectRegions } = action - const selectRegions = regionsSelected.filter(r => { - return !(deselectRegions as any[]).find(dr => compareRegions(dr, r)) }) - return { - type: SELECT_REGIONS, - selectRegions, - } - }), + }) ) this.onDeselectRegionsWithId$ = this.actions$.pipe( @@ -84,16 +68,15 @@ export class UseEffects implements OnDestroy { withLatestFrom(this.regionsSelected$), map(([ deselecRegionIds, alreadySelectedRegions ]) => { const deselectSet = new Set(deselecRegionIds) - return { - type: SELECT_REGIONS, + return viewerStateSetSelectedRegions({ selectRegions: alreadySelectedRegions .filter(({ ngId, labelIndex }) => !deselectSet.has(serialiseParcellationRegion({ ngId, labelIndex }))), - } + }) }), ) this.addToSelectedRegions$ = this.actions$.pipe( - ofType(ADD_TO_REGIONS_SELECTION_WITH_IDS), + ofType(actionAddToRegionsSelectionWithIds.type), map(action => { const { selectRegionIds } = action return selectRegionIds @@ -106,10 +89,9 @@ export class UseEffects implements OnDestroy { map(this.convertRegionIdsToRegion), withLatestFrom(this.regionsSelected$), map(([ selectedRegions, alreadySelectedRegions ]) => { - return { - type: SELECT_REGIONS, + return viewerStateSetSelectedRegions({ selectRegions: this.removeDuplicatedRegions(selectedRegions, alreadySelectedRegions), - } + }) }), ) } @@ -125,7 +107,7 @@ export class UseEffects implements OnDestroy { private subscriptions: Subscription[] = [] private parcellationSelected$ = this.actions$.pipe( - ofType(SELECT_PARCELLATION), + ofType(viewerStateSelectParcellation.type), ) @@ -136,9 +118,6 @@ export class UseEffects implements OnDestroy { shareReplay(1), ) - @Effect() - public onDeselectRegions: Observable<any> - @Effect() public onDeselectRegionsWithId$: Observable<any> @@ -192,7 +171,7 @@ export class UseEffects implements OnDestroy { */ @Effect() public onSelectRegionWithId = this.actions$.pipe( - ofType(SELECT_REGIONS_WITH_ID), + ofType(viewerStateSelectRegionWithIdDeprecated.type), map(action => { const { selectRegionIds } = action return selectRegionIds @@ -204,10 +183,9 @@ export class UseEffects implements OnDestroy { )), map(this.convertRegionIdsToRegion), map(selectRegions => { - return { - type: SELECT_REGIONS, - selectRegions, - } + return viewerStateSetSelectedRegions({ + selectRegions + }) }), ) @@ -227,10 +205,11 @@ export class UseEffects implements OnDestroy { ofType(viewerStateSelectAtlas.type) ) ).pipe( - mapTo({ - type: SELECT_REGIONS, - selectRegions: [], - }) + mapTo( + viewerStateSetSelectedRegions({ + selectRegions: [] + }) + ) ) /** @@ -241,10 +220,11 @@ export class UseEffects implements OnDestroy { @Effect() public onNewViewerResetLandmarkSelected$ = this.actions$.pipe( ofType(viewerStateNewViewer.type), - mapTo({ - type: SELECT_LANDMARKS, - landmarks: [] - }) + mapTo( + actionSelectLandmarks({ + landmarks: [] + }) + ) ) } diff --git a/src/services/state/viewerState.store.ts b/src/services/state/viewerState.store.ts index e0c165c85d0292d77789ce23a1cb7646786358f0..ef175c5b682df0cf6bbe3f8ef25da79eedd655a3 100644 --- a/src/services/state/viewerState.store.ts +++ b/src/services/state/viewerState.store.ts @@ -24,7 +24,7 @@ import { viewerStateNewViewer } from './viewerState.store.helper'; import { cvtNehubaConfigToNavigationObj } from 'src/state'; -import { viewerStateChangeNavigation, viewerStateNehubaLayerchanged } from './viewerState/actions'; +import { actionSelectLandmarks, viewerStateChangeNavigation, viewerStateNehubaLayerchanged } from './viewerState/actions'; import { serialiseParcellationRegion } from "common/util" export interface StateInterface { @@ -169,7 +169,7 @@ export const getStateStore = ({ state = defaultState } = {}) => (prevState: Part landmarksSelected : prevState.landmarksSelected.filter(lm => action.deselectLandmarks.findIndex(dLm => dLm.name === lm.name) < 0), } } - case SELECT_LANDMARKS : { + case actionSelectLandmarks.type: { return { ...prevState, landmarksSelected : action.landmarks, @@ -250,15 +250,10 @@ export const CHANGE_NAVIGATION = viewerStateChangeNavigation.type export const SELECT_PARCELLATION = viewerStateSelectParcellation.type -export const DESELECT_REGIONS = `DESELECT_REGIONS` -export const SELECT_REGIONS = `SELECT_REGIONS` -export const SELECT_REGIONS_WITH_ID = viewerStateSelectRegionWithIdDeprecated.type -export const SELECT_LANDMARKS = `SELECT_LANDMARKS` +export const SELECT_REGIONS = viewerStateSetSelectedRegions.type export const DESELECT_LANDMARKS = `DESELECT_LANDMARKS` export const USER_LANDMARKS = `USER_LANDMARKS` -export const ADD_TO_REGIONS_SELECTION_WITH_IDS = `ADD_TO_REGIONS_SELECTION_WITH_IDS` - export const SET_CONNECTIVITY_REGION = `SET_CONNECTIVITY_REGION` export const CLEAR_CONNECTIVITY_REGION = `CLEAR_CONNECTIVITY_REGION` export const SET_OVERWRITTEN_COLOR_MAP = `SET_OVERWRITTEN_COLOR_MAP` @@ -375,7 +370,7 @@ export class ViewerStateUseEffect { startWith([]), )), map(([{ segments }, regionsSelected]) => { - const selectedSet = new Set(regionsSelected.map(serialiseParcellationRegion)) + const selectedSet = new Set<string>(regionsSelected.map(serialiseParcellationRegion)) const toggleArr = segments.map(({ segment, layer }) => serialiseParcellationRegion({ ngId: layer.name, ...segment })) const deleteFlag = toggleArr.some(id => selectedSet.has(id)) @@ -384,10 +379,9 @@ export class ViewerStateUseEffect { if (deleteFlag) { selectedSet.delete(id) } else { selectedSet.add(id) } } - return { - type: SELECT_REGIONS_WITH_ID, + return viewerStateSelectRegionWithIdDeprecated({ selectRegionIds: [...selectedSet], - } + }) }), ) @@ -405,10 +399,9 @@ export class ViewerStateUseEffect { ? selectedSpatialDatas.filter((_, idx) => idx !== selectedIdx) : selectedSpatialDatas.concat(landmark) - return { - type: SELECT_LANDMARKS, + return actionSelectLandmarks({ landmarks: newSelectedSpatialDatas, - } + }) }), ) diff --git a/src/services/state/viewerState/actions.ts b/src/services/state/viewerState/actions.ts index 4dd72c7aa8268cb5b18b71b8ed8d5c5c17f235cd..7ef58b79f35746da5aa6836cf386fc89c2724c4b 100644 --- a/src/services/state/viewerState/actions.ts +++ b/src/services/state/viewerState/actions.ts @@ -88,7 +88,7 @@ export const viewerStateRemoveAdditionalLayer = createAction( export const viewerStateSelectRegionWithIdDeprecated = createAction( `[viewerState] [deprecated] selectRegionsWithId`, - props<{ selectRegionIds: number[] }>() + props<{ selectRegionIds: string[] }>() ) export const viewerStateDblClickOnViewer = createAction( @@ -124,4 +124,18 @@ export const viewerStateChangeNavigation = createAction( export const actionSetMobileUi = createAction( `[viewerState] setMobileUi`, props<{ payload: { useMobileUI: boolean } }>() -) \ No newline at end of file +) + +export const actionAddToRegionsSelectionWithIds = createAction( + `[viewerState] addToRegionSelectionWithIds`, + props<{ + selectRegionIds: string[] + }>() +) + +export const actionSelectLandmarks = createAction( + `[viewerState] selectLandmarks`, + props<{ + landmarks: any[] + }>() +) diff --git a/src/services/stateStore.service.ts b/src/services/stateStore.service.ts index 252ae89a3a54a998b4d24fdbc31b1b0b5061b30c..8a3d26cd970f83eb53bd1fd04fae703175caec1e 100644 --- a/src/services/stateStore.service.ts +++ b/src/services/stateStore.service.ts @@ -53,7 +53,7 @@ export { ViewerStateInterface, ViewerActionInterface, viewerState } export { IUiState, UIActionInterface, uiState } export { userConfigState, USER_CONFIG_ACTION_TYPES} -export { CHANGE_NAVIGATION, DESELECT_LANDMARKS, FETCHED_TEMPLATE, SELECT_LANDMARKS, SELECT_PARCELLATION, SELECT_REGIONS, USER_LANDMARKS } from './state/viewerState.store' +export { CHANGE_NAVIGATION, DESELECT_LANDMARKS, FETCHED_TEMPLATE, 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_SEGMENT, OPEN_SIDE_PANEL, COLLAPSE_SIDE_PANEL_CURRENT_VIEW, EXPAND_SIDE_PANEL_CURRENT_VIEW } from './state/uiState.store' export { UserConfigStateUseEffect } from './state/userConfigState.store' diff --git a/src/viewerModule/componentStore.ts b/src/viewerModule/componentStore.ts new file mode 100644 index 0000000000000000000000000000000000000000..ca3d8e5082ed0529924f7fdd33b792f62aa3f0aa --- /dev/null +++ b/src/viewerModule/componentStore.ts @@ -0,0 +1,24 @@ +import { Injectable } from "@angular/core"; +import { select } from "@ngrx/store"; +import { ReplaySubject, Subject } from "rxjs"; +import { shareReplay } from "rxjs/operators"; + +/** + * polyfill for ngrx component store + * until upgrade to v11 + * where component store becomes generally available + */ + +@Injectable() +export class ComponentStore<T>{ + private _state$: Subject<T> = new ReplaySubject<T>(1) + setState(state: T){ + this._state$.next(state) + } + select(selectorFn: (state: T) => unknown) { + return this._state$.pipe( + select(selectorFn), + shareReplay(1), + ) + } +} diff --git a/src/viewerModule/constants.ts b/src/viewerModule/constants.ts index cb9b4aa477635fa2a7e7591fbdb9f172cb72182a..f7fe2681a531f5830db8807bb0409ec06bb17105 100644 --- a/src/viewerModule/constants.ts +++ b/src/viewerModule/constants.ts @@ -4,3 +4,9 @@ import { Observable } from "rxjs"; export type TSupportedViewer = 'notsupported' | 'nehuba' | 'threeSurfer' | null export const VIEWERMODULE_DARKTHEME = new InjectionToken<Observable<boolean>>('VIEWERMODULE_DARKTHEME') + +export interface IViewerCmpUiState { + sideNav: { + activePanelsTitle: string[] + } +} diff --git a/src/viewerModule/viewerCmp/viewerCmp.component.ts b/src/viewerModule/viewerCmp/viewerCmp.component.ts index fa3574efa7086c1a882f7112ea87e86246e63e7b..3a7183062394728fdf901aaf1ccd4d966294d4c6 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.component.ts +++ b/src/viewerModule/viewerCmp/viewerCmp.component.ts @@ -1,6 +1,6 @@ -import { Component, ElementRef, Inject, Input, OnDestroy, Optional, TemplateRef, ViewChild } from "@angular/core"; +import { Component, ElementRef, Inject, Input, OnDestroy, Optional, ViewChild } from "@angular/core"; import { select, Store } from "@ngrx/store"; -import { combineLatest, Observable, of, Subject, Subscription } from "rxjs"; +import { combineLatest, Observable, Subject, Subscription } from "rxjs"; import { distinctUntilChanged, filter, map, startWith } from "rxjs/operators"; import { viewerStateHelperSelectParcellationWithId, viewerStateRemoveAdditionalLayer, viewerStateSetSelectedRegions } from "src/services/state/viewerState/actions"; import { viewerStateContextedSelectedRegionsSelector, viewerStateGetOverlayingAdditionalParcellations, viewerStateParcVersionSelector, viewerStateSelectedParcellationSelector, viewerStateSelectedTemplateSelector, viewerStateStandAloneVolumes } from "src/services/state/viewerState/selectors" @@ -11,9 +11,10 @@ import { uiActionHideAllDatasets, uiActionHideDatasetWithId } from "src/services import { REGION_OF_INTEREST } from "src/util/interfaces"; import { animate, state, style, transition, trigger } from "@angular/animations"; import { SwitchDirective } from "src/util/directives/switch.directive"; -import { TSupportedViewer } from "../constants"; +import { IViewerCmpUiState, TSupportedViewer } from "../constants"; import { QuickTourThis, IQuickTourData } from "src/ui/quickTour"; import { MatDrawer } from "@angular/material/sidenav"; +import { ComponentStore } from "../componentStore"; @Component({ selector: 'iav-cmp-viewer-container', @@ -59,12 +60,16 @@ import { MatDrawer } from "@angular/material/sidenav"; provide: REGION_OF_INTEREST, useFactory: (store: Store<any>) => store.pipe( select(viewerStateContextedSelectedRegionsSelector), - map(rs => rs[0] || null) + map(rs => { + if (!rs[0]) return null + return rs[0] + }) ), deps: [ Store ] - } + }, + ComponentStore ] }) @@ -172,8 +177,24 @@ export class ViewerCmp implements OnDestroy { constructor( private store$: Store<any>, + private viewerCmpLocalUiStore: ComponentStore<IViewerCmpUiState>, @Optional() @Inject(REGION_OF_INTEREST) public regionOfInterest$: Observable<any> ){ + this.viewerCmpLocalUiStore.setState({ + sideNav: { + activePanelsTitle: [] + } + }) + + this.activePanelTitles$ = this.viewerCmpLocalUiStore.select( + state => state.sideNav.activePanelsTitle + ) as Observable<string[]> + this.subscriptions.push( + this.activePanelTitles$.subscribe( + (activePanelTitles: string[]) => this.activePanelTitles = activePanelTitles + ) + ) + this.subscriptions.push( this.alwaysHideMinorPanel$.pipe( distinctUntilChanged(), @@ -188,6 +209,26 @@ export class ViewerCmp implements OnDestroy { while (this.subscriptions.length) this.subscriptions.pop().unsubscribe() } + public activePanelTitles$: Observable<string[]> + private activePanelTitles: string[] = [] + handleExpansionPanelClosedEv(title: string){ + this.viewerCmpLocalUiStore.setState({ + sideNav: { + activePanelsTitle: this.activePanelTitles.filter(n => n !== title) + } + }) + } + handleExpansionPanelAfterExpandEv(title: string){ + if (this.activePanelTitles.includes(title)) return + this.viewerCmpLocalUiStore.setState({ + sideNav: { + activePanelsTitle: [ + ...this.activePanelTitles, + title + ] + } + }) + } public bindFns(fns){ return () => { diff --git a/src/viewerModule/viewerCmp/viewerCmp.template.html b/src/viewerModule/viewerCmp/viewerCmp.template.html index cfaf7f44010a2c0d0642b707400c5886c79434bf..c401208018a6e89b00075de69239b8d976084111 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.template.html +++ b/src/viewerModule/viewerCmp/viewerCmp.template.html @@ -862,8 +862,11 @@ let-iavNgIf="iavNgIf" let-content="content"> <mat-expansion-panel + [expanded]="activePanelTitles$ | async | arrayContains : title" [attr.data-opened]="expansionPanel.expanded" [attr.data-mat-expansion-title]="title" + (closed)="handleExpansionPanelClosedEv(title)" + (afterExpand)="handleExpansionPanelAfterExpandEv(title)" hideToggle *ngIf="iavNgIf" #expansionPanel="matExpansionPanel">