From 15737a8d8fe8ef941db1263531b5503670be4e47 Mon Sep 17 00:00:00 2001 From: Jannic Veith Date: Thu, 5 Sep 2024 15:13:06 +0200 Subject: [PATCH 01/14] Refactor Search flow in NGRX, fix routing --- apps/client-asset-sg/src/app/app.component.ts | 8 +- .../src/environments/environment.ts | 2 +- .../src/features/ocr/ocr.controller.ts | 1 - .../asset-editor-tab-page.component.ts | 3 - .../asset-search-filter-list.component.ts | 4 +- .../asset-search-refine.component.ts | 2 +- .../asset-search-results.component.html | 2 +- .../asset-search-results.component.ts | 6 +- .../asset-viewer-page.component.html | 6 +- .../asset-viewer-page.component.ts | 22 +- .../src/lib/components/map/map-controller.ts | 26 +- .../src/lib/components/map/map.component.ts | 40 ++- .../src/lib/services/asset-search.service.ts | 2 - .../asset-search/asset-search.actions.ts | 26 +- .../asset-search/asset-search.effects.ts | 232 ++++++++++-------- .../asset-search/asset-search.reducer.ts | 25 +- .../asset-search/asset-search.selector.ts | 20 +- .../lib/components/button/button.component.ts | 1 - 18 files changed, 197 insertions(+), 231 deletions(-) diff --git a/apps/client-asset-sg/src/app/app.component.ts b/apps/client-asset-sg/src/app/app.component.ts index 03620109..0b1ac07a 100644 --- a/apps/client-asset-sg/src/app/app.component.ts +++ b/apps/client-asset-sg/src/app/app.component.ts @@ -1,12 +1,11 @@ import { HttpClient } from '@angular/common/http'; import { Component, inject } from '@angular/core'; -import { Router } from '@angular/router'; import { AuthService, AuthState, ErrorService } from '@asset-sg/auth'; import { AppPortalService, appSharedStateActions, setCssCustomProperties } from '@asset-sg/client-shared'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { Store } from '@ngrx/store'; import { WINDOW } from 'ngx-window-token'; -import { debounceTime, fromEvent, startWith, tap } from 'rxjs'; +import { debounceTime, fromEvent, startWith, switchMap } from 'rxjs'; import { assert } from 'tsafe'; import { AppState } from './state/app-state'; @@ -23,7 +22,6 @@ export class AppComponent { private _httpClient = inject(HttpClient); public appPortalService = inject(AppPortalService); - private readonly router: Router = inject(Router); readonly errorService = inject(ErrorService); readonly authService = inject(AuthService); private readonly store = inject(Store); @@ -31,8 +29,8 @@ export class AppComponent { constructor() { this._httpClient .get>('api/oauth-config/config') - .pipe(tap(async (config) => await this.authService.initialize(config))) - .subscribe(async (oAuthConfig) => { + .pipe(switchMap(async (config) => await this.authService.initialize(config))) + .subscribe(async () => { this.store.dispatch(appSharedStateActions.loadWorkgroups()); this.store.dispatch(appSharedStateActions.loadReferenceData()); }); diff --git a/apps/client-asset-sg/src/environments/environment.ts b/apps/client-asset-sg/src/environments/environment.ts index a1267e9c..40c57800 100644 --- a/apps/client-asset-sg/src/environments/environment.ts +++ b/apps/client-asset-sg/src/environments/environment.ts @@ -1,5 +1,5 @@ import { CompileTimeEnvironment } from './environment-type'; export const environment: CompileTimeEnvironment = { - ngrxStoreLoggerEnabled: false, + ngrxStoreLoggerEnabled: true, }; diff --git a/apps/server-asset-sg/src/features/ocr/ocr.controller.ts b/apps/server-asset-sg/src/features/ocr/ocr.controller.ts index 5e228aab..e2c5d3ea 100644 --- a/apps/server-asset-sg/src/features/ocr/ocr.controller.ts +++ b/apps/server-asset-sg/src/features/ocr/ocr.controller.ts @@ -45,7 +45,6 @@ export class OcrController { private config: Config; constructor(private prismaService: PrismaService, private httpService: HttpService) { - console.log('hello ocr controller'); this.config = pipe( Config.decode({ ocrUrl: process.env.OCR_URL, diff --git a/libs/asset-editor/src/lib/components/asset-editor-tab-page/asset-editor-tab-page.component.ts b/libs/asset-editor/src/lib/components/asset-editor-tab-page/asset-editor-tab-page.component.ts index 120a2a18..4db4a093 100644 --- a/libs/asset-editor/src/lib/components/asset-editor-tab-page/asset-editor-tab-page.component.ts +++ b/libs/asset-editor/src/lib/components/asset-editor-tab-page/asset-editor-tab-page.component.ts @@ -148,9 +148,6 @@ export class AssetEditorTabPageComponent { this._form.controls.administration.controls.newStatusWorkItemCode.disable(); } }); - this._form.valueChanges.subscribe(() => { - // console.log('value', this._form.value); - }); this._lc.afterViewInit$ .pipe( diff --git a/libs/asset-viewer/src/lib/components/asset-search-filter-list/asset-search-filter-list.component.ts b/libs/asset-viewer/src/lib/components/asset-search-filter-list/asset-search-filter-list.component.ts index e2bd230f..bc03ba71 100644 --- a/libs/asset-viewer/src/lib/components/asset-search-filter-list/asset-search-filter-list.component.ts +++ b/libs/asset-viewer/src/lib/components/asset-search-filter-list/asset-search-filter-list.component.ts @@ -23,8 +23,8 @@ export class AssetSearchFilterListComponent { activeValues.add(filter.value); } this.store.dispatch( - actions.searchByFilterConfiguration({ - filterConfiguration: { [filter.queryKey]: activeValues.size > 0 ? [...activeValues] : undefined }, + actions.search({ + query: { [filter.queryKey]: activeValues.size > 0 ? [...activeValues] : undefined }, }) ); } diff --git a/libs/asset-viewer/src/lib/components/asset-search-refine/asset-search-refine.component.ts b/libs/asset-viewer/src/lib/components/asset-search-refine/asset-search-refine.component.ts index 872938f6..73672c34 100644 --- a/libs/asset-viewer/src/lib/components/asset-search-refine/asset-search-refine.component.ts +++ b/libs/asset-viewer/src/lib/components/asset-search-refine/asset-search-refine.component.ts @@ -101,7 +101,7 @@ export class AssetSearchRefineComponent implements OnInit, OnDestroy, AfterViewI public updateSearch(filterConfiguration: Partial) { if (this.isFiltersOpen) { - this.store.dispatch(actions.searchByFilterConfiguration({ filterConfiguration })); + this.store.dispatch(actions.search({ query: filterConfiguration })); } } diff --git a/libs/asset-viewer/src/lib/components/asset-search-results/asset-search-results.component.html b/libs/asset-viewer/src/lib/components/asset-search-results/asset-search-results.component.html index b8a5f83e..48a41684 100644 --- a/libs/asset-viewer/src/lib/components/asset-search-results/asset-search-results.component.html +++ b/libs/asset-viewer/src/lib/components/asset-search-results/asset-search-results.component.html @@ -2,7 +2,7 @@
{{ "search.searchResults" | translate }}: - +
- - - - menuBar.assets - - + diff --git a/libs/asset-viewer/src/lib/components/asset-viewer-page/asset-viewer-page.component.ts b/libs/asset-viewer/src/lib/components/asset-viewer-page/asset-viewer-page.component.ts index 94f52d48..7b829b11 100644 --- a/libs/asset-viewer/src/lib/components/asset-viewer-page/asset-viewer-page.component.ts +++ b/libs/asset-viewer/src/lib/components/asset-viewer-page/asset-viewer-page.component.ts @@ -1,6 +1,5 @@ import { TemplatePortal } from '@angular/cdk/portal'; import { - AfterViewInit, ApplicationRef, ChangeDetectorRef, Component, @@ -8,12 +7,12 @@ import { inject, NgZone, OnDestroy, + OnInit, TemplateRef, ViewChild, ViewContainerRef, } from '@angular/core'; import { AppPortalService, AppState, LifecycleHooks, LifecycleHooksDirective } from '@asset-sg/client-shared'; -import { isTruthy } from '@asset-sg/core'; import { AssetEditDetail } from '@asset-sg/shared'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { Store } from '@ngrx/store'; @@ -24,7 +23,6 @@ import * as O from 'fp-ts/Option'; import { asyncScheduler, combineLatest, - delay, filter, map, Observable, @@ -33,7 +31,6 @@ import { share, Subject, switchMap, - take, withLatestFrom, } from 'rxjs'; @@ -56,7 +53,7 @@ import { styleUrls: ['./asset-viewer-page.component.scss'], hostDirectives: [LifecycleHooksDirective], }) -export class AssetViewerPageComponent implements AfterViewInit, OnDestroy { +export class AssetViewerPageComponent implements OnInit, OnDestroy { @ViewChild('templateAppBarPortalContent') templateAppBarPortalContent!: TemplateRef; @ViewChild('searchInput') searchInput!: ElementRef; @@ -64,9 +61,7 @@ export class AssetViewerPageComponent implements AfterViewInit, OnDestroy { private _appPortalService = inject(AppPortalService); private _viewContainerRef = inject(ViewContainerRef); private _store = inject(Store); - private _appRef = inject(ApplicationRef); private _cd = inject(ChangeDetectorRef); - private _ngZone = inject(NgZone); public isLoading$ = combineLatest( [ @@ -96,11 +91,10 @@ export class AssetViewerPageComponent implements AfterViewInit, OnDestroy { ); public assetClicked$ = new Subject(); - public closeSearchResultsClicked$ = new Subject(); public assetsForPicker$: Observable; public highlightedAssetId: number | null = null; - public ngAfterViewInit() { + public ngOnInit() { this._store.dispatch(actions.initializeSearch()); this._appPortalService.setAppBarPortalContent(null); } @@ -171,7 +165,6 @@ export class AssetViewerPageComponent implements AfterViewInit, OnDestroy { } ngOnDestroy() { - this._store.dispatch(actions.closeFilters()); this._appPortalService.setAppBarPortalContent(null); } } diff --git a/libs/asset-viewer/src/lib/state/asset-search/asset-search.reducer.ts b/libs/asset-viewer/src/lib/state/asset-search/asset-search.reducer.ts index 52ecfd31..c04b1af9 100644 --- a/libs/asset-viewer/src/lib/state/asset-search/asset-search.reducer.ts +++ b/libs/asset-viewer/src/lib/state/asset-search/asset-search.reducer.ts @@ -141,7 +141,7 @@ export const assetSearchReducer = createReducer( assetDetailLoadingState: initialState.assetDetailLoadingState, }) ), - on(appSharedStateActions.openPanel, (state): AssetSearchState => ({ ...state, isFiltersOpen: true })), + on(appSharedStateActions.openPanel, (state): AssetSearchState => ({ ...state })), on(actions.openFilters, (state): AssetSearchState => ({ ...state, isFiltersOpen: true })), on(actions.closeFilters, (state): AssetSearchState => ({ ...state, isFiltersOpen: false })), on( From 789871019571dd4ec561cce312b3b31b1c2d1640 Mon Sep 17 00:00:00 2001 From: Jannic Veith Date: Mon, 9 Sep 2024 15:21:22 +0200 Subject: [PATCH 07/14] Initialize search only once --- .../src/lib/services/asset-search.service.ts | 2 +- .../lib/state/asset-search/asset-search.effects.ts | 11 +++-------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/libs/asset-viewer/src/lib/services/asset-search.service.ts b/libs/asset-viewer/src/lib/services/asset-search.service.ts index 5d25075c..f996c80a 100644 --- a/libs/asset-viewer/src/lib/services/asset-search.service.ts +++ b/libs/asset-viewer/src/lib/services/asset-search.service.ts @@ -17,7 +17,7 @@ export class AssetSearchService { constructor(private _httpClient: HttpClient) {} public search(searchQuery: AssetSearchQuery): Observable { - return this._httpClient.post('/api/assets/search?limit=10000', searchQuery).pipe( + return this._httpClient.post('/api/assets/search?limit=1000', searchQuery).pipe( map((res) => plainToInstance(AssetSearchResultDTO, res)), tap((result) => { result.data = result.data.map((asset) => (AssetEditDetail.decode(asset) as E.Right).right); diff --git a/libs/asset-viewer/src/lib/state/asset-search/asset-search.effects.ts b/libs/asset-viewer/src/lib/state/asset-search/asset-search.effects.ts index 8e65d2a4..0187950c 100644 --- a/libs/asset-viewer/src/lib/state/asset-search/asset-search.effects.ts +++ b/libs/asset-viewer/src/lib/state/asset-search/asset-search.effects.ts @@ -46,7 +46,7 @@ export class AssetSearchEffects { public loadSearch$ = createEffect(() => this.actions$.pipe( - ofType(actions.initializeSearch, actions.search, actions.resetSearch, actions.removePolygon), + ofType(actions.search, actions.resetSearch, actions.removePolygon), withLatestFrom(this.store.select(selectAssetSearchQuery)), map(([_, query]) => actions.loadSearch({ query })) ) @@ -141,12 +141,8 @@ export class AssetSearchEffects { share() ); - // noinspection JSUnusedGlobalSymbols public readSearchQueryParams$ = createEffect(() => - this.queryParams$.pipe( - filter(({ query }) => Object.values(query).some((value) => value !== undefined)), - map(({ query }) => actions.search({ query: query })) - ) + this.queryParams$.pipe(map(({ query }) => actions.search({ query: query }))) ); public readAssetIdQueryParam$ = createEffect(() => @@ -157,11 +153,10 @@ export class AssetSearchEffects { ) ); - // noinspection JSUnusedGlobalSymbols public updateQueryParams$ = createEffect( () => this.actions$.pipe( - ofType(actions.initializeSearch, actions.loadSearch, actions.updateAssetDetail, actions.resetAssetDetail), + ofType(actions.loadSearch, actions.updateAssetDetail, actions.resetAssetDetail), concatLatestFrom(() => [ this.store.select(selectAssetSearchQuery), this.store.select(selectCurrentAssetDetail), From 43d31e1778336030280bd500e51bfec98b20a65b Mon Sep 17 00:00:00 2001 From: Jannic Veith Date: Mon, 9 Sep 2024 16:47:42 +0200 Subject: [PATCH 08/14] Prevent polygon interactions from triggering the search multiple times --- .../src/lib/components/map/map.component.ts | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/libs/asset-viewer/src/lib/components/map/map.component.ts b/libs/asset-viewer/src/lib/components/map/map.component.ts index a5901d75..415df82e 100644 --- a/libs/asset-viewer/src/lib/components/map/map.component.ts +++ b/libs/asset-viewer/src/lib/components/map/map.component.ts @@ -15,7 +15,7 @@ import { import { AppState } from '@asset-sg/client-shared'; import { isNotNull } from '@asset-sg/core'; import { Store } from '@ngrx/store'; -import { asapScheduler, filter, first, skip, Subscription } from 'rxjs'; +import { asapScheduler, distinctUntilChanged, filter, first, pairwise, skip, Subscription, take, takeLast } from 'rxjs'; import { AllStudyService } from '../../services/all-study.service'; import * as searchActions from '../../state/asset-search/asset-search.actions'; import { @@ -28,6 +28,7 @@ import { import { DrawControl } from '../map-controls/draw-controls'; import { ZoomControl } from '../map-controls/zoom-control'; import { MapController } from './map-controller'; +import { filterNullish } from '@asset-sg/shared/v2'; @Component({ selector: 'asset-sg-map', @@ -178,16 +179,22 @@ export class MapComponent implements AfterViewInit, OnChanges, OnDestroy { private initializePolygonBindings(): void { this.subscription.add( this.store.select(selectAssetSearchPolygon).subscribe((polygon) => { + console.log('1polygon', polygon); this.controls.draw.setPolygon(polygon ?? null); }) ); - this.controls.draw.polygon$.pipe(skip(1)).subscribe((polygon) => { - this.store.dispatch( - searchActions.search({ - query: { polygon: polygon ?? undefined }, - }) + this.controls.draw.polygon$ + .pipe( + filterNullish(), + distinctUntilChanged((a, b) => this.equalsCheck(a, b)) + ) + .subscribe((polygon) => + this.store.dispatch( + searchActions.search({ + query: { polygon: polygon }, + }) + ) ); - }); } private handleHighlightedAssetIdChange() { @@ -198,6 +205,10 @@ export class MapComponent implements AfterViewInit, OnChanges, OnDestroy { } } + private equalsCheck(a: T[] | null, b: T[] | null) { + return !!a && !!b && a.length === b.length && a.every((v, i) => v === b[i]); + } + @HostBinding('class.is-loading') get isLoading(): boolean { return !this.isInitialized; From ebf339e4bcf811fdf961944e5f4d785ef520a6f0 Mon Sep 17 00:00:00 2001 From: Jannic Veith Date: Tue, 10 Sep 2024 10:33:53 +0200 Subject: [PATCH 09/14] Fix routing and asset detail zoom --- apps/client-asset-sg/src/app/app-matchers.ts | 37 ------------------- apps/client-asset-sg/src/app/app.module.ts | 6 +-- .../src/lib/components/map/map.component.ts | 5 +-- .../asset-search/asset-search.actions.ts | 2 +- .../asset-search/asset-search.effects.ts | 31 +++++++++------- libs/client-shared/src/lib/i18n.ts | 4 ++ .../src/lib/utils/app-matchers.ts | 35 ++++++++++++++++++ libs/client-shared/src/lib/utils/index.ts | 1 + libs/core/src/lib/utils/eq.ts | 31 ++++++++++++++++ 9 files changed, 93 insertions(+), 59 deletions(-) delete mode 100644 apps/client-asset-sg/src/app/app-matchers.ts create mode 100644 libs/client-shared/src/lib/utils/app-matchers.ts diff --git a/apps/client-asset-sg/src/app/app-matchers.ts b/apps/client-asset-sg/src/app/app-matchers.ts deleted file mode 100644 index 7b979c7a..00000000 --- a/apps/client-asset-sg/src/app/app-matchers.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { UrlMatchResult, UrlSegment } from '@angular/router'; -import { Lang } from '@asset-sg/shared'; -import { pipe } from 'fp-ts/function'; -import * as NEA from 'fp-ts/NonEmptyArray'; -import * as O from 'fp-ts/Option'; -import * as D from 'io-ts/Decoder'; - -const validSegments = D.union(D.literal('assets'), D.literal('favourites')); - -export function assetsPageMatcher(segments: UrlSegment[]): UrlMatchResult { - return pipe( - segments, - NEA.fromArray, - O.chain((ss) => { - const lang = NEA.head(ss); - return pipe( - O.fromEither(Lang.decode(lang.path)), - O.map(() => - pipe( - ss, - NEA.tail, - NEA.fromArray, - O.map(NEA.head), - O.chainFirstEitherK((a) => validSegments.decode(a.path)), - O.map((path) => ({ lang, path })), - O.getOrElse(() => ({ lang })) - ) - ) - ); - }), - O.map((posParams) => ({ - consumed: segments, - posParams, - })), - O.getOrElse(() => (null as unknown)) - ); -} diff --git a/apps/client-asset-sg/src/app/app.module.ts b/apps/client-asset-sg/src/app/app.module.ts index d0c49edc..75eaeab2 100644 --- a/apps/client-asset-sg/src/app/app.module.ts +++ b/apps/client-asset-sg/src/app/app.module.ts @@ -21,6 +21,7 @@ import { icons, TranslateTsLoader, } from '@asset-sg/client-shared'; +import { assetsPageMatcher } from '@asset-sg/client-shared'; import { storeLogger } from '@asset-sg/core'; import { provideSvgIcons, SvgIconComponent } from '@ngneat/svg-icon'; import { EffectsModule } from '@ngrx/effects'; @@ -34,7 +35,6 @@ import { PushModule } from '@rx-angular/template/push'; import { environment } from '../environments/environment'; import { adminGuard, notAnonymousGuard } from './app-guards'; -import { assetsPageMatcher } from './app-matchers'; import { AppComponent } from './app.component'; import { AppBarComponent, MenuBarComponent, NotFoundComponent, RedirectToLangComponent } from './components'; import { SplashScreenComponent } from './components/splash-screen/splash-screen.component'; @@ -59,10 +59,6 @@ registerLocaleData(locale_deCH, 'de-CH'); BrowserAnimationsModule, HttpClientModule, RouterModule.forRoot([ - { - path: ':lang/auth', - loadChildren: () => AuthModule, - }, { path: ':lang/profile', loadChildren: () => import('@asset-sg/profile').then((m) => m.ProfileModule), diff --git a/libs/asset-viewer/src/lib/components/map/map.component.ts b/libs/asset-viewer/src/lib/components/map/map.component.ts index 415df82e..03aacab8 100644 --- a/libs/asset-viewer/src/lib/components/map/map.component.ts +++ b/libs/asset-viewer/src/lib/components/map/map.component.ts @@ -14,6 +14,7 @@ import { } from '@angular/core'; import { AppState } from '@asset-sg/client-shared'; import { isNotNull } from '@asset-sg/core'; +import { filterNullish } from '@asset-sg/shared/v2'; import { Store } from '@ngrx/store'; import { asapScheduler, distinctUntilChanged, filter, first, pairwise, skip, Subscription, take, takeLast } from 'rxjs'; import { AllStudyService } from '../../services/all-study.service'; @@ -28,7 +29,6 @@ import { import { DrawControl } from '../map-controls/draw-controls'; import { ZoomControl } from '../map-controls/zoom-control'; import { MapController } from './map-controller'; -import { filterNullish } from '@asset-sg/shared/v2'; @Component({ selector: 'asset-sg-map', @@ -163,7 +163,7 @@ export class MapComponent implements AfterViewInit, OnChanges, OnDestroy { if (asset == null) { this.controller.clearActiveAsset(); } else { - this.controller.setActiveAsset(asset); + setTimeout(() => this.controller.setActiveAsset(asset)); } }) ); @@ -179,7 +179,6 @@ export class MapComponent implements AfterViewInit, OnChanges, OnDestroy { private initializePolygonBindings(): void { this.subscription.add( this.store.select(selectAssetSearchPolygon).subscribe((polygon) => { - console.log('1polygon', polygon); this.controls.draw.setPolygon(polygon ?? null); }) ); diff --git a/libs/asset-viewer/src/lib/state/asset-search/asset-search.actions.ts b/libs/asset-viewer/src/lib/state/asset-search/asset-search.actions.ts index 0f948016..739d260f 100644 --- a/libs/asset-viewer/src/lib/state/asset-search/asset-search.actions.ts +++ b/libs/asset-viewer/src/lib/state/asset-search/asset-search.actions.ts @@ -35,4 +35,4 @@ export const closeFilters = createAction('[Asset Viewer] Close Filters'); export const openResults = createAction('[Asset Viewer] Open Results'); export const closeResults = createAction('[Asset Viewer] Close Results'); export const setStudies = createAction('[Asset Viewer] Load Studies', props<{ studies: AllStudyDTOs }>()); -export const loadSearch = createAction('[Asset Viewer] Load Search', props<{ query: AssetSearchQuery }>()); +export const loadSearch = createAction('[Asset Search] Load Search', props<{ query: AssetSearchQuery }>()); diff --git a/libs/asset-viewer/src/lib/state/asset-search/asset-search.effects.ts b/libs/asset-viewer/src/lib/state/asset-search/asset-search.effects.ts index 0187950c..1abbdf5b 100644 --- a/libs/asset-viewer/src/lib/state/asset-search/asset-search.effects.ts +++ b/libs/asset-viewer/src/lib/state/asset-search/asset-search.effects.ts @@ -1,15 +1,15 @@ import { inject, Injectable } from '@angular/core'; import { ActivatedRoute, Params, Router } from '@angular/router'; -import { AppState, fromAppShared } from '@asset-sg/client-shared'; -import { isDecodeError, isNotNull, ORD } from '@asset-sg/core'; +import { AppState, assetsPageMatcher, fromAppShared } from '@asset-sg/client-shared'; +import { deepEqual, isDecodeError, isNotNull, ORD } from '@asset-sg/core'; import { AssetSearchQuery, AssetSearchResult, LV95, Polygon } from '@asset-sg/shared'; -import { filterNullish } from '@asset-sg/shared/v2'; import * as RD from '@devexperts/remote-data-ts'; import { UntilDestroy } from '@ngneat/until-destroy'; import { Actions, concatLatestFrom, createEffect, ofType } from '@ngrx/effects'; +import { ROUTER_NAVIGATED } from '@ngrx/router-store'; import { Store } from '@ngrx/store'; import * as D from 'io-ts/Decoder'; -import { filter, first, map, merge, of, share, switchMap, withLatestFrom } from 'rxjs'; +import { filter, map, merge, of, share, switchMap, withLatestFrom } from 'rxjs'; import { AllStudyService } from '../../services/all-study.service'; import { AssetSearchService } from '../../services/asset-search.service'; @@ -116,12 +116,11 @@ export class AssetSearchEffects { /** * Query Parameter Interactions */ - public queryParams$ = this.actions$.pipe( - ofType(actions.initializeSearch), - first(), // only read query params once - concatLatestFrom(() => this.route.queryParams), - map(([_, params]) => { + ofType(ROUTER_NAVIGATED), + filter((x) => assetsPageMatcher(x.payload.routerState.root.firstChild.url) !== null), + map(({ payload }) => { + const params = payload.routerState.root.queryParams; const query: AssetSearchQuery = {}; const assetId: number | undefined = readNumberParam(params, QUERY_PARAM_MAPPING.assetId); query.text = readStringParam(params, QUERY_PARAM_MAPPING.text); @@ -142,14 +141,20 @@ export class AssetSearchEffects { ); public readSearchQueryParams$ = createEffect(() => - this.queryParams$.pipe(map(({ query }) => actions.search({ query: query }))) + this.queryParams$.pipe( + withLatestFrom(this.store.select(selectAssetSearchQuery)), + filter(([params, query]) => !deepEqual(params.query, query)), + map(([params, query]) => actions.search({ query: params.query })) + ) ); public readAssetIdQueryParam$ = createEffect(() => this.queryParams$.pipe( - map(({ assetId }) => assetId), - filterNullish(), - map((assetId) => actions.assetClicked({ assetId })) + withLatestFrom(this.store.select(selectCurrentAssetDetail)), + filter(([params, assetDetail]) => params.assetId !== assetDetail?.assetId), + map(([{ assetId }, _]) => + assetId === undefined ? actions.resetAssetDetail() : actions.assetClicked({ assetId }) + ) ) ); diff --git a/libs/client-shared/src/lib/i18n.ts b/libs/client-shared/src/lib/i18n.ts index e0fd20ef..1af358d9 100644 --- a/libs/client-shared/src/lib/i18n.ts +++ b/libs/client-shared/src/lib/i18n.ts @@ -3,6 +3,10 @@ import { Observable, of } from 'rxjs'; export type Langs = 'de' | 'en' | 'fr' | 'it' | 'rm'; +export function isSupportedLang(lang: string): lang is Langs { + return ['de', 'en', 'fr', 'it', 'rm'].includes(lang); +} + export interface TranslationsStruct { de: T; en: T; diff --git a/libs/client-shared/src/lib/utils/app-matchers.ts b/libs/client-shared/src/lib/utils/app-matchers.ts new file mode 100644 index 00000000..52ff1cf1 --- /dev/null +++ b/libs/client-shared/src/lib/utils/app-matchers.ts @@ -0,0 +1,35 @@ +import { UrlMatchResult, UrlSegment } from '@angular/router'; + +import { isSupportedLang } from '../i18n'; + +const validSegments = ['assets', 'favourites']; + +export function assetsPageMatcher(segments: UrlSegment[]): UrlMatchResult | null { + if (segments.length === 0) { + return null; + } + + if (segments.length === 1) { + const lang = segments[0].path; + if (isSupportedLang(lang)) { + return { consumed: segments, posParams: { lang: segments[0] } }; + } + + return null; + } + + if (segments.length === 2) { + const lang = segments[0].path; + if (!isSupportedLang(lang)) { + return null; + } + + const path = segments[1].path; + if (!validSegments.includes(path)) { + return null; + } + return { consumed: segments, posParams: { lang: segments[0], path: segments[1] } }; + } + + return null; +} diff --git a/libs/client-shared/src/lib/utils/index.ts b/libs/client-shared/src/lib/utils/index.ts index 39122da0..dddb060a 100644 --- a/libs/client-shared/src/lib/utils/index.ts +++ b/libs/client-shared/src/lib/utils/index.ts @@ -1,3 +1,4 @@ +export * from './app-matchers'; export * from './control-value-accessor'; export * from './current-lang'; export * from './custom-property'; diff --git a/libs/core/src/lib/utils/eq.ts b/libs/core/src/lib/utils/eq.ts index fa59067b..f5b515e3 100644 --- a/libs/core/src/lib/utils/eq.ts +++ b/libs/core/src/lib/utils/eq.ts @@ -31,3 +31,34 @@ export function getEqArrayUnordered(E: Eq): Eq> { )), }; } + +/** + * Deep equality check for objects from https://medium.com/@stheodorejohn/javascript-object-deep-equality-comparison-in-javascript-7aa227e889d4. + * @param obj1 + * @param obj2 + */ +export function deepEqual(obj1: any, obj2: any): boolean { + // Base case: If both objects are identical, return true. + if (obj1 === obj2) { + return true; + } + // Check if both objects are objects and not null. + if (typeof obj1 !== 'object' || typeof obj2 !== 'object' || obj1 === null || obj2 === null) { + return false; + } + // Get the keys of both objects. + const keys1 = Object.keys(obj1); + const keys2 = Object.keys(obj2); + // Check if the number of keys is the same. + if (keys1.length !== keys2.length) { + return false; + } + // Iterate through the keys and compare their values recursively. + for (const key of keys1) { + if (!keys2.includes(key) || !deepEqual(obj1[key], obj2[key])) { + return false; + } + } + // If all checks pass, the objects are deep equal. + return true; +} From 49c630da3189489a0e57fa196792091559dfe697 Mon Sep 17 00:00:00 2001 From: Jannic Veith Date: Tue, 10 Sep 2024 10:39:01 +0200 Subject: [PATCH 10/14] Synchronize heatmap and studies visibility --- libs/asset-viewer/src/lib/components/map/map-controller.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/asset-viewer/src/lib/components/map/map-controller.ts b/libs/asset-viewer/src/lib/components/map/map-controller.ts index 2f4cb675..bf895c9d 100644 --- a/libs/asset-viewer/src/lib/components/map/map-controller.ts +++ b/libs/asset-viewer/src/lib/components/map/map-controller.ts @@ -105,6 +105,7 @@ export class MapController { setShowHeatmap(showHeatmap: boolean): void { this.showHeatmap = showHeatmap; this.layers.heatmap.setVisible(showHeatmap); + this.layers.studies.setVisible(showHeatmap); } setClickEnabled(isEnabled: boolean): void { From 27b3755dca2e2941ee9a65fb72e1b0b2f0d22e29 Mon Sep 17 00:00:00 2001 From: Jannic Veith Date: Tue, 10 Sep 2024 11:24:31 +0200 Subject: [PATCH 11/14] Show current search when returning to assets --- .../components/app-bar/app-bar.component.scss | 2 +- .../asset-search-refine.component.scss | 1 + .../asset-search/asset-search.actions.ts | 1 + .../asset-search/asset-search.effects.ts | 33 ++++++++++++++++--- 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/apps/client-asset-sg/src/app/components/app-bar/app-bar.component.scss b/apps/client-asset-sg/src/app/components/app-bar/app-bar.component.scss index 6ec9cda6..35aa58bb 100644 --- a/apps/client-asset-sg/src/app/components/app-bar/app-bar.component.scss +++ b/apps/client-asset-sg/src/app/components/app-bar/app-bar.component.scss @@ -2,7 +2,7 @@ :host { box-shadow: 0px 4px 4px #00000029; - z-index: 1; + z-index: 10; display: flex; align-items: center; } diff --git a/libs/asset-viewer/src/lib/components/asset-search-refine/asset-search-refine.component.scss b/libs/asset-viewer/src/lib/components/asset-search-refine/asset-search-refine.component.scss index ca4bb81a..3e94f472 100644 --- a/libs/asset-viewer/src/lib/components/asset-search-refine/asset-search-refine.component.scss +++ b/libs/asset-viewer/src/lib/components/asset-search-refine/asset-search-refine.component.scss @@ -8,6 +8,7 @@ padding: 0 1rem 1rem 0; display: flex; flex-direction: column; + z-index: 1; } @include drawerPanel.draw-panel-header; diff --git a/libs/asset-viewer/src/lib/state/asset-search/asset-search.actions.ts b/libs/asset-viewer/src/lib/state/asset-search/asset-search.actions.ts index 739d260f..a4eaa568 100644 --- a/libs/asset-viewer/src/lib/state/asset-search/asset-search.actions.ts +++ b/libs/asset-viewer/src/lib/state/asset-search/asset-search.actions.ts @@ -25,6 +25,7 @@ export const resetSearch = createAction('[Asset Search] Reset Search'); export const removePolygon = createAction('[Asset Search] Remove polygon'); export const initializeSearch = createAction('[Asset Search] Initialize search'); export const assetClicked = createAction('[Asset Search] Asset clicked', props<{ assetId: number }>()); +export const showAssetDetail = createAction('[Asset Viewer] Show Asset Detail', props<{ assetId: number }>()); export const updateAssetDetail = createAction( '[Asset Search] Update Asset Detail', props<{ assetDetail: AssetEditDetail }>() diff --git a/libs/asset-viewer/src/lib/state/asset-search/asset-search.effects.ts b/libs/asset-viewer/src/lib/state/asset-search/asset-search.effects.ts index 1abbdf5b..f0369f37 100644 --- a/libs/asset-viewer/src/lib/state/asset-search/asset-search.effects.ts +++ b/libs/asset-viewer/src/lib/state/asset-search/asset-search.effects.ts @@ -9,7 +9,7 @@ import { Actions, concatLatestFrom, createEffect, ofType } from '@ngrx/effects'; import { ROUTER_NAVIGATED } from '@ngrx/router-store'; import { Store } from '@ngrx/store'; import * as D from 'io-ts/Decoder'; -import { filter, map, merge, of, share, switchMap, withLatestFrom } from 'rxjs'; +import { EMPTY, filter, map, merge, of, share, switchMap, withLatestFrom } from 'rxjs'; import { AllStudyService } from '../../services/all-study.service'; import { AssetSearchService } from '../../services/asset-search.service'; @@ -88,6 +88,20 @@ export class AssetSearchEffects { ); }); + public showAssetDetail$ = createEffect(() => { + return this.actions$.pipe( + ofType(actions.showAssetDetail), + withLatestFrom(this.store.select(selectCurrentAssetDetail)), + switchMap(([{ assetId }, currentAssetDetail]) => + assetId !== currentAssetDetail?.assetId + ? this.assetSearchService + .loadAssetDetailData(assetId) + .pipe(map((assetDetail) => actions.updateAssetDetail({ assetDetail }))) + : EMPTY + ) + ); + }); + public closeDetailOnUpdateSearch$ = createEffect(() => { return this.actions$.pipe( ofType(actions.search), @@ -144,7 +158,14 @@ export class AssetSearchEffects { this.queryParams$.pipe( withLatestFrom(this.store.select(selectAssetSearchQuery)), filter(([params, query]) => !deepEqual(params.query, query)), - map(([params, query]) => actions.search({ query: params.query })) + map(([params, storeQuery]) => { + const paramsEmpty = Object.values(params.query).every((v) => v == null); + if (paramsEmpty) { + return actions.loadSearch({ query: storeQuery }); + } else { + return actions.search({ query: params.query }); + } + }) ) ); @@ -152,9 +173,11 @@ export class AssetSearchEffects { this.queryParams$.pipe( withLatestFrom(this.store.select(selectCurrentAssetDetail)), filter(([params, assetDetail]) => params.assetId !== assetDetail?.assetId), - map(([{ assetId }, _]) => - assetId === undefined ? actions.resetAssetDetail() : actions.assetClicked({ assetId }) - ) + map(([params, storeAsset]) => { + const paramsEmpty = Object.values(params.query).every((v) => v == null); + const assetId = paramsEmpty ? storeAsset?.assetId : params.assetId; + return assetId === undefined ? actions.resetAssetDetail() : actions.showAssetDetail({ assetId }); + }) ) ); From af219294901a481ab52f04026d50e1dbe743c86f Mon Sep 17 00:00:00 2001 From: Jannic Veith Date: Thu, 12 Sep 2024 09:24:31 +0200 Subject: [PATCH 12/14] Dont reload data when its already present --- .../state/asset-search/asset-search.actions.ts | 1 + .../state/asset-search/asset-search.effects.ts | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/libs/asset-viewer/src/lib/state/asset-search/asset-search.actions.ts b/libs/asset-viewer/src/lib/state/asset-search/asset-search.actions.ts index a4eaa568..511be6ee 100644 --- a/libs/asset-viewer/src/lib/state/asset-search/asset-search.actions.ts +++ b/libs/asset-viewer/src/lib/state/asset-search/asset-search.actions.ts @@ -37,3 +37,4 @@ export const openResults = createAction('[Asset Viewer] Open Results'); export const closeResults = createAction('[Asset Viewer] Close Results'); export const setStudies = createAction('[Asset Viewer] Load Studies', props<{ studies: AllStudyDTOs }>()); export const loadSearch = createAction('[Asset Search] Load Search', props<{ query: AssetSearchQuery }>()); +export const updateQueryParams = createAction('[Asset Search] Update Query Params'); diff --git a/libs/asset-viewer/src/lib/state/asset-search/asset-search.effects.ts b/libs/asset-viewer/src/lib/state/asset-search/asset-search.effects.ts index f0369f37..230b8b7b 100644 --- a/libs/asset-viewer/src/lib/state/asset-search/asset-search.effects.ts +++ b/libs/asset-viewer/src/lib/state/asset-search/asset-search.effects.ts @@ -20,6 +20,7 @@ import { selectCurrentAssetDetail, selectAssetSearchNoActiveFilters, selectStudies, + selectAssetSearchResultData, } from './asset-search.selector'; @UntilDestroy() @@ -156,12 +157,19 @@ export class AssetSearchEffects { public readSearchQueryParams$ = createEffect(() => this.queryParams$.pipe( - withLatestFrom(this.store.select(selectAssetSearchQuery)), + withLatestFrom( + this.store.select(selectAssetSearchQuery), + this.store.select(selectAssetSearchResultData).pipe(map((r) => r.length > 0)) + ), filter(([params, query]) => !deepEqual(params.query, query)), - map(([params, storeQuery]) => { + map(([params, storeQuery, searchResultsLoaded]) => { const paramsEmpty = Object.values(params.query).every((v) => v == null); if (paramsEmpty) { - return actions.loadSearch({ query: storeQuery }); + if (searchResultsLoaded) { + return actions.updateQueryParams(); + } else { + return actions.loadSearch({ query: storeQuery }); + } } else { return actions.search({ query: params.query }); } @@ -184,7 +192,7 @@ export class AssetSearchEffects { public updateQueryParams$ = createEffect( () => this.actions$.pipe( - ofType(actions.loadSearch, actions.updateAssetDetail, actions.resetAssetDetail), + ofType(actions.updateQueryParams, actions.loadSearch, actions.updateAssetDetail, actions.resetAssetDetail), concatLatestFrom(() => [ this.store.select(selectAssetSearchQuery), this.store.select(selectCurrentAssetDetail), From 3c712b96e097cd1e91a6af14c2d35e46189eec94 Mon Sep 17 00:00:00 2001 From: Jannic Veith Date: Thu, 12 Sep 2024 13:15:02 +0200 Subject: [PATCH 13/14] Dont open filters after someone manually changed them --- .../menu-bar/menu-bar.component.html | 39 +++++++------------ .../components/menu-bar/menu-bar.component.ts | 38 +++++------------- .../asset-search-results.component.ts | 6 +-- .../src/lib/components/map/map.component.ts | 23 ++++------- .../asset-search/asset-search.actions.ts | 13 ++++--- .../asset-search/asset-search.effects.ts | 20 +++++++--- .../asset-search/asset-search.reducer.ts | 1 + .../lib/state/app-shared-state.selectors.ts | 14 +++++++ libs/core/src/lib/utils/eq.ts | 4 ++ 9 files changed, 73 insertions(+), 85 deletions(-) diff --git a/apps/client-asset-sg/src/app/components/menu-bar/menu-bar.component.html b/apps/client-asset-sg/src/app/components/menu-bar/menu-bar.component.html index 093a722f..a66abde4 100644 --- a/apps/client-asset-sg/src/app/components/menu-bar/menu-bar.component.html +++ b/apps/client-asset-sg/src/app/components/menu-bar/menu-bar.component.html @@ -16,39 +16,28 @@ [disabled]="true" aria-disabled="true" class="menu-bar-item" - [ngClass]="{ active: (isFavouritesActive$ | async) }" + routerLinkActive="active" > menuBar.favourites - - - - - - menuBar.admin - - - + + + + menuBar.admin + menuBar.userManagement @@ -60,7 +49,7 @@ [routerLink]="[_translateService.currentLang, 'profile']" asset-sg-reset class="menu-bar-item" - [ngClass]="{ active: (isProfileActive$ | async) }" + routerLinkActive="active" > menuBar.profile diff --git a/apps/client-asset-sg/src/app/components/menu-bar/menu-bar.component.ts b/apps/client-asset-sg/src/app/components/menu-bar/menu-bar.component.ts index 2a20a6d6..bf41d5ba 100644 --- a/apps/client-asset-sg/src/app/components/menu-bar/menu-bar.component.ts +++ b/apps/client-asset-sg/src/app/components/menu-bar/menu-bar.component.ts @@ -1,12 +1,11 @@ import { ChangeDetectionStrategy, Component, HostBinding, inject } from '@angular/core'; import { NavigationEnd, Router } from '@angular/router'; -import { appSharedStateActions, fromAppShared } from '@asset-sg/client-shared'; +import { appSharedStateActions, assetsPageMatcher, fromAppShared } from '@asset-sg/client-shared'; import { AssetEditPolicy } from '@asset-sg/shared/v2'; -import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; +import { UntilDestroy } from '@ngneat/until-destroy'; import { Store } from '@ngrx/store'; import { TranslateService } from '@ngx-translate/core'; -import queryString from 'query-string'; -import { filter, map, shareReplay } from 'rxjs'; +import { filter, map } from 'rxjs'; import { AppState } from '../../state/app-state'; @@ -24,30 +23,13 @@ export class MenuBarComponent { private _store = inject(Store); public userExists$ = this._store.select(fromAppShared.selectIsAnonymousMode).pipe(map((anonymous) => !anonymous)); - public isAssetsActive$ = this.createIsRouteActive$((url) => Boolean(url.match(/^\/\w\w$/))); - public isEditActive$ = this.isSegmentActive('asset-admin'); - public isFavouritesActive$ = this.isSegmentActive('favourites'); - public isAdminActive$ = this.isSegmentActive('admin'); - public isProfileActive$ = this.isSegmentActive('profile'); - - private createIsRouteActive$(fn: (url: string) => boolean) { - const o$ = this._router.events.pipe( - filter((event) => event instanceof NavigationEnd), - map(() => { - const { url } = queryString.parseUrl(this._router.url); - return fn(url); - }), - shareReplay({ bufferSize: 1, refCount: true }) - ); - o$.pipe(untilDestroyed(this)).subscribe(); - return o$; - } - - private isSegmentActive(segment: string) { - return this.createIsRouteActive$((url) => { - return Boolean(url.match(`^/\\w\\w/${segment}`)); - }); - } + public isAssetsActive$ = this._router.events.pipe( + filter((event) => event instanceof NavigationEnd), + map(() => { + const segments = this._router.parseUrl(this._router.url).root.children['primary'].segments; + return assetsPageMatcher(segments) !== null; + }) + ); public openAssetDrawer() { this._store.dispatch(appSharedStateActions.toggleSearchFilter()); diff --git a/libs/asset-viewer/src/lib/components/asset-search-results/asset-search-results.component.ts b/libs/asset-viewer/src/lib/components/asset-search-results/asset-search-results.component.ts index 2e5f14e3..3f19ad40 100644 --- a/libs/asset-viewer/src/lib/components/asset-search-results/asset-search-results.component.ts +++ b/libs/asset-viewer/src/lib/components/asset-search-results/asset-search-results.component.ts @@ -69,11 +69,7 @@ export class AssetSearchResultsComponent implements OnInit, OnDestroy { } public toggleResultsOpen(isCurrentlyOpen: boolean) { - if (isCurrentlyOpen) { - this._store.dispatch(actions.closeResults()); - } else { - this._store.dispatch(actions.openResults()); - } + this._store.dispatch(actions.manualToggleResult()); } public onScroll(event: Event) { diff --git a/libs/asset-viewer/src/lib/components/map/map.component.ts b/libs/asset-viewer/src/lib/components/map/map.component.ts index 03aacab8..a544fdf2 100644 --- a/libs/asset-viewer/src/lib/components/map/map.component.ts +++ b/libs/asset-viewer/src/lib/components/map/map.component.ts @@ -13,7 +13,7 @@ import { ViewChild, } from '@angular/core'; import { AppState } from '@asset-sg/client-shared'; -import { isNotNull } from '@asset-sg/core'; +import { arrayEqual, isNotNull } from '@asset-sg/core'; import { filterNullish } from '@asset-sg/shared/v2'; import { Store } from '@ngrx/store'; import { asapScheduler, distinctUntilChanged, filter, first, pairwise, skip, Subscription, take, takeLast } from 'rxjs'; @@ -182,18 +182,13 @@ export class MapComponent implements AfterViewInit, OnChanges, OnDestroy { this.controls.draw.setPolygon(polygon ?? null); }) ); - this.controls.draw.polygon$ - .pipe( - filterNullish(), - distinctUntilChanged((a, b) => this.equalsCheck(a, b)) + this.controls.draw.polygon$.pipe(filterNullish(), distinctUntilChanged(arrayEqual)).subscribe((polygon) => + this.store.dispatch( + searchActions.search({ + query: { polygon: polygon }, + }) ) - .subscribe((polygon) => - this.store.dispatch( - searchActions.search({ - query: { polygon: polygon }, - }) - ) - ); + ); } private handleHighlightedAssetIdChange() { @@ -204,10 +199,6 @@ export class MapComponent implements AfterViewInit, OnChanges, OnDestroy { } } - private equalsCheck(a: T[] | null, b: T[] | null) { - return !!a && !!b && a.length === b.length && a.every((v, i) => v === b[i]); - } - @HostBinding('class.is-loading') get isLoading(): boolean { return !this.isInitialized; diff --git a/libs/asset-viewer/src/lib/state/asset-search/asset-search.actions.ts b/libs/asset-viewer/src/lib/state/asset-search/asset-search.actions.ts index 511be6ee..1f7a2880 100644 --- a/libs/asset-viewer/src/lib/state/asset-search/asset-search.actions.ts +++ b/libs/asset-viewer/src/lib/state/asset-search/asset-search.actions.ts @@ -25,16 +25,17 @@ export const resetSearch = createAction('[Asset Search] Reset Search'); export const removePolygon = createAction('[Asset Search] Remove polygon'); export const initializeSearch = createAction('[Asset Search] Initialize search'); export const assetClicked = createAction('[Asset Search] Asset clicked', props<{ assetId: number }>()); -export const showAssetDetail = createAction('[Asset Viewer] Show Asset Detail', props<{ assetId: number }>()); +export const showAssetDetail = createAction('[Asset Search] Show Asset Detail', props<{ assetId: number }>()); export const updateAssetDetail = createAction( '[Asset Search] Update Asset Detail', props<{ assetDetail: AssetEditDetail }>() ); export const resetAssetDetail = createAction('[Asset Search] Reset Asset Detail'); -export const openFilters = createAction('[Asset Viewer] Open Filters'); -export const closeFilters = createAction('[Asset Viewer] Close Filters'); -export const openResults = createAction('[Asset Viewer] Open Results'); -export const closeResults = createAction('[Asset Viewer] Close Results'); -export const setStudies = createAction('[Asset Viewer] Load Studies', props<{ studies: AllStudyDTOs }>()); +export const openFilters = createAction('[Asset Search] Open Filters'); +export const closeFilters = createAction('[Asset Search] Close Filters'); +export const openResults = createAction('[Asset Search] Open Results'); +export const closeResults = createAction('[Asset Search] Close Results'); +export const setStudies = createAction('[Asset Search] Load Studies', props<{ studies: AllStudyDTOs }>()); export const loadSearch = createAction('[Asset Search] Load Search', props<{ query: AssetSearchQuery }>()); export const updateQueryParams = createAction('[Asset Search] Update Query Params'); +export const manualToggleResult = createAction('[Asset Search] Manual Toggle Params'); diff --git a/libs/asset-viewer/src/lib/state/asset-search/asset-search.effects.ts b/libs/asset-viewer/src/lib/state/asset-search/asset-search.effects.ts index 230b8b7b..43263126 100644 --- a/libs/asset-viewer/src/lib/state/asset-search/asset-search.effects.ts +++ b/libs/asset-viewer/src/lib/state/asset-search/asset-search.effects.ts @@ -9,7 +9,7 @@ import { Actions, concatLatestFrom, createEffect, ofType } from '@ngrx/effects'; import { ROUTER_NAVIGATED } from '@ngrx/router-store'; import { Store } from '@ngrx/store'; import * as D from 'io-ts/Decoder'; -import { EMPTY, filter, map, merge, of, share, switchMap, withLatestFrom } from 'rxjs'; +import { EMPTY, filter, map, merge, of, share, switchMap, takeUntil, withLatestFrom } from 'rxjs'; import { AllStudyService } from '../../services/all-study.service'; import { AssetSearchService } from '../../services/asset-search.service'; @@ -113,11 +113,21 @@ export class AssetSearchEffects { public openSearchResults$ = createEffect(() => { return this.actions$.pipe( ofType(actions.updateSearchResults), + takeUntil(this.actions$.pipe(ofType(actions.manualToggleResult))), withLatestFrom(this.store.select(selectAssetSearchNoActiveFilters)), map(([_, showStudies]) => (showStudies ? actions.closeResults() : actions.openResults())) ); }); + public closeSearchResults$ = createEffect(() => { + return this.actions$.pipe( + ofType(actions.updateSearchResults), + withLatestFrom(this.store.select(selectAssetSearchNoActiveFilters)), + filter(([_, showStudies]) => showStudies), + map(() => actions.closeResults()) + ); + }); + public loadStudies$ = createEffect(() => { return this.actions$.pipe( ofType(actions.initializeSearch), @@ -161,7 +171,7 @@ export class AssetSearchEffects { this.store.select(selectAssetSearchQuery), this.store.select(selectAssetSearchResultData).pipe(map((r) => r.length > 0)) ), - filter(([params, query]) => !deepEqual(params.query, query)), + filter(([params, storeQuery]) => !deepEqual(params.query, storeQuery)), map(([params, storeQuery, searchResultsLoaded]) => { const paramsEmpty = Object.values(params.query).every((v) => v == null); if (paramsEmpty) { @@ -180,10 +190,10 @@ export class AssetSearchEffects { public readAssetIdQueryParam$ = createEffect(() => this.queryParams$.pipe( withLatestFrom(this.store.select(selectCurrentAssetDetail)), - filter(([params, assetDetail]) => params.assetId !== assetDetail?.assetId), - map(([params, storeAsset]) => { + filter(([params, storeAssetDetail]) => params.assetId !== storeAssetDetail?.assetId), + map(([params, storeAssetDetail]) => { const paramsEmpty = Object.values(params.query).every((v) => v == null); - const assetId = paramsEmpty ? storeAsset?.assetId : params.assetId; + const assetId = paramsEmpty ? storeAssetDetail?.assetId : params.assetId; return assetId === undefined ? actions.resetAssetDetail() : actions.showAssetDetail({ assetId }); }) ) diff --git a/libs/asset-viewer/src/lib/state/asset-search/asset-search.reducer.ts b/libs/asset-viewer/src/lib/state/asset-search/asset-search.reducer.ts index c04b1af9..a73455fc 100644 --- a/libs/asset-viewer/src/lib/state/asset-search/asset-search.reducer.ts +++ b/libs/asset-viewer/src/lib/state/asset-search/asset-search.reducer.ts @@ -150,5 +150,6 @@ export const assetSearchReducer = createReducer( ), on(actions.openResults, (state): AssetSearchState => ({ ...state, isResultsOpen: true })), on(actions.closeResults, (state): AssetSearchState => ({ ...state, isResultsOpen: false })), + on(actions.manualToggleResult, (state): AssetSearchState => ({ ...state, isResultsOpen: !state.isResultsOpen })), on(actions.setStudies, (state, { studies }): AssetSearchState => ({ ...state, studies })) ); diff --git a/libs/client-shared/src/lib/state/app-shared-state.selectors.ts b/libs/client-shared/src/lib/state/app-shared-state.selectors.ts index 1b64f7ee..6f62bbe4 100644 --- a/libs/client-shared/src/lib/state/app-shared-state.selectors.ts +++ b/libs/client-shared/src/lib/state/app-shared-state.selectors.ts @@ -1,5 +1,6 @@ import { Contact, emptyValueItem, ReferenceData, valueItemRecordToArray } from '@asset-sg/shared'; import * as RD from '@devexperts/remote-data-ts'; +import { getRouterSelectors } from '@ngrx/router-store'; import { createSelector } from '@ngrx/store'; import * as A from 'fp-ts/Array'; import { flow, pipe } from 'fp-ts/function'; @@ -94,3 +95,16 @@ export const selectRDReferenceDataVM = createSelector( ); export const selectLocale = createSelector(appSharedFeature, (state) => (state.lang === 'en' ? 'en-GB' : 'de-CH')); + +export const { + selectCurrentRoute, // select the current route + selectFragment, // select the current route fragment + selectQueryParams, // select the current route query params + selectQueryParam, // factory function to select a query param + selectRouteParams, // select the current route params + selectRouteParam, // factory function to select a route param + selectRouteData, // select the current route data + selectRouteDataParam, // factory function to select a route data param + selectUrl, // select the current url + selectTitle, // select the title if available +} = getRouterSelectors(); diff --git a/libs/core/src/lib/utils/eq.ts b/libs/core/src/lib/utils/eq.ts index f5b515e3..562fc607 100644 --- a/libs/core/src/lib/utils/eq.ts +++ b/libs/core/src/lib/utils/eq.ts @@ -62,3 +62,7 @@ export function deepEqual(obj1: any, obj2: any): boolean { // If all checks pass, the objects are deep equal. return true; } + +export function arrayEqual(a: T[] | null, b: T[] | null) { + return !!a && !!b && a.length === b.length && a.every((v, i) => v === b[i]); +} From 2f9aed42e0b772cbfe9eea7d37a85aac2b3598fb Mon Sep 17 00:00:00 2001 From: Jannic Veith Date: Thu, 12 Sep 2024 13:42:33 +0200 Subject: [PATCH 14/14] DRY allowed languages --- .../src/app/components/app-bar/app-bar.component.ts | 3 ++- .../splash-screen/splash-screen.component.ts | 13 ------------- libs/client-shared/src/lib/i18n.ts | 5 +++-- 3 files changed, 5 insertions(+), 16 deletions(-) diff --git a/apps/client-asset-sg/src/app/components/app-bar/app-bar.component.ts b/apps/client-asset-sg/src/app/components/app-bar/app-bar.component.ts index 7c351c9d..b9e96067 100644 --- a/apps/client-asset-sg/src/app/components/app-bar/app-bar.component.ts +++ b/apps/client-asset-sg/src/app/components/app-bar/app-bar.component.ts @@ -2,6 +2,7 @@ import { ENTER } from '@angular/cdk/keycodes'; import { HttpClient } from '@angular/common/http'; import { ChangeDetectionStrategy, Component, ElementRef, Input, OnInit, Output, ViewChild } from '@angular/core'; import { NavigationEnd, Router } from '@angular/router'; +import { supportedLangs } from '@asset-sg/client-shared'; import { isTruthy } from '@asset-sg/core'; import { Lang } from '@asset-sg/shared'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; @@ -52,7 +53,7 @@ export class AppBarComponent implements OnInit { public links$ = this._currentLang$.pipe( debounceTime(0), map((currentLang) => ({ - links: ['de', 'fr', 'it', 'rm', 'en'].map( + links: supportedLangs.map( (lang) => pipe( currentLang, diff --git a/apps/client-asset-sg/src/app/components/splash-screen/splash-screen.component.ts b/apps/client-asset-sg/src/app/components/splash-screen/splash-screen.component.ts index 2fcdeaed..440cd2de 100644 --- a/apps/client-asset-sg/src/app/components/splash-screen/splash-screen.component.ts +++ b/apps/client-asset-sg/src/app/components/splash-screen/splash-screen.component.ts @@ -2,7 +2,6 @@ import { Component, inject } from '@angular/core'; import { AuthService, AuthState } from '@asset-sg/auth'; import { CURRENT_LANG } from '@asset-sg/client-shared'; import { TranslateService } from '@ngx-translate/core'; -import { Observable, map, startWith } from 'rxjs'; @Component({ selector: 'app-splash-screen', @@ -18,18 +17,6 @@ export class SplashScreenComponent { return window.location.host; } - get languages$(): Observable> { - return this.currentLang$.pipe( - startWith('de'), - map((lang) => - ['de', 'fr', 'it', 'rm', 'en'].map((it) => ({ - name: it, - isActive: lang === it, - })) - ) - ); - } - selectLanguage(language: string): void { this.translateService.use(language); } diff --git a/libs/client-shared/src/lib/i18n.ts b/libs/client-shared/src/lib/i18n.ts index 1af358d9..e61d359e 100644 --- a/libs/client-shared/src/lib/i18n.ts +++ b/libs/client-shared/src/lib/i18n.ts @@ -1,10 +1,11 @@ import { TranslateLoader } from '@ngx-translate/core'; import { Observable, of } from 'rxjs'; -export type Langs = 'de' | 'en' | 'fr' | 'it' | 'rm'; +export const supportedLangs = ['de', 'en', 'fr', 'it', 'rm'] as const; +export type Langs = (typeof supportedLangs)[number]; export function isSupportedLang(lang: string): lang is Langs { - return ['de', 'en', 'fr', 'it', 'rm'].includes(lang); + return supportedLangs.includes(lang as Langs); } export interface TranslationsStruct {