From 9d1d1cc8c65b94bf47f17eba5d06d07f0c1706a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Tue, 7 Jun 2022 13:30:36 +0200 Subject: [PATCH] Move all state services under a common base class --- .../state/attachments/attachments.service.ts | 38 ++++------- .../capabilities/capabilities.service.ts | 17 +++-- .../src/app/core/state/collection-store.ts | 15 +++++ .../state/file-links/file-links.service.ts | 32 ++++----- .../core/state/principals/principals.query.ts | 10 --- .../state/principals/principals.service.ts | 58 ++++------------ .../core/state/projects/projects.service.ts | 30 ++++----- .../core/state/resource-collection.service.ts | 32 +++++++++ .../core/state/storages/storages.service.ts | 41 ++++-------- .../src/app/core/state/views/views.service.ts | 67 ++++--------------- .../center/menu/menu.component.ts | 4 +- .../center/menu/state/ian-menu.query.ts | 54 --------------- .../center/menu/state/ian-menu.service.ts | 40 +++++++++-- .../center/state/ian-center.service.ts | 7 +- .../planner/team-planner.component.ts | 2 +- .../attachment-list-item.component.ts | 4 +- .../attachment-list.component.ts | 9 ++- 17 files changed, 187 insertions(+), 273 deletions(-) delete mode 100644 frontend/src/app/core/state/principals/principals.query.ts delete mode 100644 frontend/src/app/features/in-app-notifications/center/menu/state/ian-menu.query.ts diff --git a/frontend/src/app/core/state/attachments/attachments.service.ts b/frontend/src/app/core/state/attachments/attachments.service.ts index 195e7a2afe..ac63347ffb 100644 --- a/frontend/src/app/core/state/attachments/attachments.service.ts +++ b/frontend/src/app/core/state/attachments/attachments.service.ts @@ -31,10 +31,7 @@ import { HttpClient, HttpHeaders, } from '@angular/common/http'; -import { - applyTransaction, - QueryEntity, -} from '@datorama/akita'; +import { applyTransaction } from '@datorama/akita'; import { from, Observable, @@ -42,7 +39,6 @@ import { import { catchError, map, - switchMap, tap, } from 'rxjs/operators'; import { AttachmentsStore } from 'core-app/core/state/attachments/attachments.store'; @@ -61,13 +57,13 @@ import { HalLink } from 'core-app/features/hal/hal-link/hal-link'; import isNewResource, { HAL_NEW_RESOURCE_ID } from 'core-app/features/hal/helpers/is-new-resource'; import { ConfigurationService } from 'core-app/core/config/configuration.service'; import { insertCollectionIntoState } from 'core-app/core/state/collection-store'; +import { + CollectionStore, + ResourceCollectionService, +} from 'core-app/core/state/resource-collection.service'; @Injectable() -export class AttachmentsResourceService { - protected store = new AttachmentsStore(); - - public query = new QueryEntity(this.store); - +export class AttachmentsResourceService extends ResourceCollectionService { constructor( private readonly I18n:I18nService, private readonly http:HttpClient, @@ -76,7 +72,9 @@ export class AttachmentsResourceService { private readonly directFileUploadService:OpenProjectDirectFileUploadService, private readonly configurationService:ConfigurationService, private readonly toastService:ToastService, - ) { } + ) { + super(); + } /** * This method ensures that a specific collection is fetched, if not available. @@ -91,20 +89,6 @@ export class AttachmentsResourceService { this.fetchAttachments(key).subscribe(); } - /** - * Returns an observable of all attachments in a collection identified by the collection key. - * - * @param key the collection key - */ - collection(key:string):Observable { - return this.query - .select() - .pipe( - map((state) => state.collections[key]?.ids), - switchMap((attachmentIds) => this.query.selectMany(attachmentIds)), - ); - } - /** * Fetches attachments by the attachment collection self link. * This link is used as key to store the result collection in the resource store. @@ -273,4 +257,8 @@ export class AttachmentsResourceService { const attachments = resource.attachments as unknown&{ href?:string }; return attachments?.href || null; } + + protected createStore():CollectionStore { + return new AttachmentsStore(); + } } diff --git a/frontend/src/app/core/state/capabilities/capabilities.service.ts b/frontend/src/app/core/state/capabilities/capabilities.service.ts index fa5468fd88..76d42a6bf0 100644 --- a/frontend/src/app/core/state/capabilities/capabilities.service.ts +++ b/frontend/src/app/core/state/capabilities/capabilities.service.ts @@ -4,7 +4,6 @@ import { tap, } from 'rxjs/operators'; import { Observable } from 'rxjs'; -import { ID } from '@datorama/akita'; import { HttpClient } from '@angular/common/http'; import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service'; import { ToastService } from 'core-app/shared/components/toaster/toast.service'; @@ -16,14 +15,13 @@ import { } from 'core-app/core/state/collection-store'; import { ICapability } from 'core-app/core/state/capabilities/capability.model'; import { CapabilitiesStore } from 'core-app/core/state/capabilities/capabilities.store'; -import { CapabilitiesQuery } from 'core-app/core/state/capabilities/capabilities.query'; +import { + CollectionStore, + ResourceCollectionService, +} from 'core-app/core/state/resource-collection.service'; @Injectable() -export class CapabilitiesResourceService { - protected store = new CapabilitiesStore(); - - readonly query = new CapabilitiesQuery(this.store); - +export class CapabilitiesResourceService extends ResourceCollectionService { private get capabilitiesPath():string { return this .apiV3Service @@ -36,6 +34,7 @@ export class CapabilitiesResourceService { private apiV3Service:ApiV3Service, private toastService:ToastService, ) { + super(); } fetchCapabilities(params:ApiV3ListParameters):Observable> { @@ -53,7 +52,7 @@ export class CapabilitiesResourceService { ); } - update(id:ID, project:Partial):void { - this.store.update(id, project); + protected createStore():CollectionStore { + return new CapabilitiesStore(); } } diff --git a/frontend/src/app/core/state/collection-store.ts b/frontend/src/app/core/state/collection-store.ts index d6a5487ceb..5ef7242b5e 100644 --- a/frontend/src/app/core/state/collection-store.ts +++ b/frontend/src/app/core/state/collection-store.ts @@ -82,3 +82,18 @@ export function insertCollectionIntoState( )); }); } + +export function collectionFrom(elements:T[]):IHALCollection { + const count = elements.length; + + return { + _type: 'Collection', + count, + total: count, + pageSize: count, + offset: 1, + _embedded: { + elements, + }, + }; +} diff --git a/frontend/src/app/core/state/file-links/file-links.service.ts b/frontend/src/app/core/state/file-links/file-links.service.ts index c955ac32d4..6c4474414e 100644 --- a/frontend/src/app/core/state/file-links/file-links.service.ts +++ b/frontend/src/app/core/state/file-links/file-links.service.ts @@ -28,10 +28,14 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; -import { QueryEntity } from '@datorama/akita'; -import { from, Observable } from 'rxjs'; +import { from } from 'rxjs'; import { - catchError, groupBy, map, mergeMap, reduce, switchMap, tap, + catchError, + groupBy, + mergeMap, + reduce, + switchMap, + tap, } from 'rxjs/operators'; import { IFileLink } from 'core-app/core/state/file-links/file-link.model'; import { IHALCollection } from 'core-app/core/apiv3/types/hal-collection.type'; @@ -39,15 +43,19 @@ import { ToastService } from 'core-app/shared/components/toaster/toast.service'; import { FileLinksStore } from 'core-app/core/state/file-links/file-links.store'; import { insertCollectionIntoState } from 'core-app/core/state/collection-store'; import idFromLink from 'core-app/features/hal/helpers/id-from-link'; +import { + CollectionStore, + ResourceCollectionService, +} from 'core-app/core/state/resource-collection.service'; @Injectable() -export class FileLinksResourceService { - private store = new FileLinksStore(); - +export class FileLinksResourceService extends ResourceCollectionService { constructor( private readonly http:HttpClient, private readonly toastService:ToastService, - ) {} + ) { + super(); + } updateCollectionsForWorkPackage(fileLinksSelfLink:string):void { this.http @@ -79,13 +87,7 @@ export class FileLinksResourceService { }); } - collection(key:string):Observable { - const query = new QueryEntity(this.store); - return query - .select() - .pipe( - map((state) => state.collections[key]?.ids), - switchMap((fileLinkIds) => query.selectMany(fileLinkIds)), - ); + protected createStore():CollectionStore { + return new FileLinksStore(); } } diff --git a/frontend/src/app/core/state/principals/principals.query.ts b/frontend/src/app/core/state/principals/principals.query.ts deleted file mode 100644 index 508fbf87d8..0000000000 --- a/frontend/src/app/core/state/principals/principals.query.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { QueryEntity } from '@datorama/akita'; -import { Observable } from 'rxjs'; -import { IPrincipal } from './principal.model'; -import { PrincipalsState } from './principals.store'; - -export class PrincipalsQuery extends QueryEntity { - public byIds(ids:string[]):Observable { - return this.selectMany(ids); - } -} diff --git a/frontend/src/app/core/state/principals/principals.service.ts b/frontend/src/app/core/state/principals/principals.service.ts index 059b702fa7..e98a3da802 100644 --- a/frontend/src/app/core/state/principals/principals.service.ts +++ b/frontend/src/app/core/state/principals/principals.service.ts @@ -4,35 +4,29 @@ import { tap, } from 'rxjs/operators'; import { Observable } from 'rxjs'; -import { - applyTransaction, - ID, -} from '@datorama/akita'; +import { applyTransaction } from '@datorama/akita'; import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service'; import { ToastService } from 'core-app/shared/components/toaster/toast.service'; import { IHALCollection } from 'core-app/core/apiv3/types/hal-collection.type'; import { HttpClient } from '@angular/common/http'; -import { PrincipalsQuery } from 'core-app/core/state/principals/principals.query'; import { ApiV3ListParameters } from 'core-app/core/apiv3/paths/apiv3-list-resource.interface'; import { collectionKey, insertCollectionIntoState, } from 'core-app/core/state/collection-store'; -import { - EffectHandler, -} from 'core-app/core/state/effects/effect-handler.decorator'; +import { EffectHandler } from 'core-app/core/state/effects/effect-handler.decorator'; import { ActionsService } from 'core-app/core/state/actions/actions.service'; import { PrincipalsStore } from './principals.store'; import { IPrincipal } from './principal.model'; import { IUser } from 'core-app/core/state/principals/user.model'; +import { + CollectionStore, + ResourceCollectionService, +} from 'core-app/core/state/resource-collection.service'; @EffectHandler @Injectable() -export class PrincipalsResourceService { - protected store = new PrincipalsStore(); - - readonly query = new PrincipalsQuery(this.store); - +export class PrincipalsResourceService extends ResourceCollectionService { private get principalsPath():string { return this .apiV3Service @@ -45,7 +39,9 @@ export class PrincipalsResourceService { private http:HttpClient, private apiV3Service:ApiV3Service, private toastService:ToastService, - ) { } + ) { + super(); + } fetchUser(id:string|number):Observable { return this.http @@ -78,37 +74,7 @@ export class PrincipalsResourceService { ); } - update(id:ID, principal:Partial):void { - this.store.update(id, principal); - } - - modifyCollection(params:ApiV3ListParameters, callback:(collection:ID[]) => ID[]):void { - const key = collectionKey(params); - this.store.update(({ collections }) => ( - { - collections: { - ...collections, - [key]: { - ...collections[key], - ids: [...callback(collections[key]?.ids || [])], - }, - }, - } - )); - } - - removeFromCollection(params:ApiV3ListParameters, ids:ID[]):void { - const key = collectionKey(params); - this.store.update(({ collections }) => ( - { - collections: { - ...collections, - [key]: { - ...collections[key], - ids: (collections[key]?.ids || []).filter((id) => !ids.includes(id)), - }, - }, - } - )); + protected createStore():CollectionStore { + return new PrincipalsStore(); } } diff --git a/frontend/src/app/core/state/projects/projects.service.ts b/frontend/src/app/core/state/projects/projects.service.ts index fb134e2be3..42c6afc0bb 100644 --- a/frontend/src/app/core/state/projects/projects.service.ts +++ b/frontend/src/app/core/state/projects/projects.service.ts @@ -27,32 +27,29 @@ //++ import { Injectable } from '@angular/core'; -import { catchError, filter, tap } from 'rxjs/operators'; +import { + catchError, + tap, +} from 'rxjs/operators'; import { Observable } from 'rxjs'; -import { ID } from '@datorama/akita'; import { HttpClient } from '@angular/common/http'; import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service'; import { ToastService } from 'core-app/shared/components/toaster/toast.service'; import { IHALCollection } from 'core-app/core/apiv3/types/hal-collection.type'; -import { ProjectsQuery } from 'core-app/core/state/projects/projects.query'; import { ApiV3ListParameters } from 'core-app/core/apiv3/paths/apiv3-list-resource.interface'; import { collectionKey, insertCollectionIntoState, } from 'core-app/core/state/collection-store'; -import { ProjectsStore } from './projects.store'; import { IProject } from './project.model'; - -export function isIProject(input:IProject|undefined):input is IProject { - return input !== undefined; -} +import { + CollectionStore, + ResourceCollectionService, +} from 'core-app/core/state/resource-collection.service'; +import { ProjectsStore } from 'core-app/core/state/projects/projects.store'; @Injectable() -export class ProjectsResourceService { - protected store = new ProjectsStore(); - - readonly query = new ProjectsQuery(this.store); - +export class ProjectsResourceService extends ResourceCollectionService { private get projectsPath():string { return this .apiV3Service @@ -65,6 +62,7 @@ export class ProjectsResourceService { private apiV3Service:ApiV3Service, private toastService:ToastService, ) { + super(); } fetchProjects(params:ApiV3ListParameters):Observable> { @@ -91,9 +89,7 @@ export class ProjectsResourceService { ); } - lookup(id:ID):Observable { - return this.query - .selectEntity(id) - .pipe(filter(isIProject)); + protected createStore():CollectionStore { + return new ProjectsStore(); } } diff --git a/frontend/src/app/core/state/resource-collection.service.ts b/frontend/src/app/core/state/resource-collection.service.ts index d2e43d68e9..4f56cca6c4 100644 --- a/frontend/src/app/core/state/resource-collection.service.ts +++ b/frontend/src/app/core/state/resource-collection.service.ts @@ -74,6 +74,38 @@ export abstract class ResourceCollectionService { ) as Observable; } + /** + * Lookup multiple entities from the store + */ + lookupMany(ids:ID[]):Observable { + return this + .query + .selectMany(ids); + } + + /** + * Lookup a single entity from the store + * @param id + */ + lookupMultiple(id:ID):Observable { + return this + .query + .selectEntity(id) + .pipe( + filter((entity) => entity !== undefined), + ) as Observable; + } + + /** + * Update a single entity in the store + * + * @param id The id to update + * @param entity A section of the entity to update + */ + update(id:ID, entity:Partial):void { + this.store.update(id, entity); + } + /** * Create a new instance of this resource service's underyling store. * @protected diff --git a/frontend/src/app/core/state/storages/storages.service.ts b/frontend/src/app/core/state/storages/storages.service.ts index cabb7ed852..8a14cd2bbd 100644 --- a/frontend/src/app/core/state/storages/storages.service.ts +++ b/frontend/src/app/core/state/storages/storages.service.ts @@ -28,42 +28,21 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; -import { forkJoin, Observable } from 'rxjs'; -import { filter, map, switchMap } from 'rxjs/operators'; +import { forkJoin } from 'rxjs'; import { IStorage } from 'core-app/core/state/storages/storage.model'; import { StoragesStore } from 'core-app/core/state/storages/storages.store'; -import { StoragesQuery } from 'core-app/core/state/storages/storages.query'; import { insertCollectionIntoState } from 'core-app/core/state/collection-store'; import { IHALCollection } from 'core-app/core/apiv3/types/hal-collection.type'; import { IHalResourceLink } from 'core-app/core/state/hal-resource'; - -export function isIStorage(input:IStorage|undefined):input is IStorage { - return input !== undefined; -} +import { + CollectionStore, + ResourceCollectionService, +} from 'core-app/core/state/resource-collection.service'; @Injectable() -export class StoragesResourceService { - private readonly store = new StoragesStore(); - - private readonly query = new StoragesQuery(this.store); - - constructor(private readonly http:HttpClient) {} - - collection(key:string):Observable { - return this - .query - .select() - .pipe( - map((state) => state.collections[key]?.ids), - switchMap((ids) => this.query.selectMany(ids)), - ); - } - - lookup(id:string):Observable { - return this - .query - .selectEntity(id) - .pipe(filter(isIStorage)); +export class StoragesResourceService extends ResourceCollectionService { + constructor(private readonly http:HttpClient) { + super(); } updateCollection(key:string, storageLinks:IHalResourceLink[]):void { @@ -74,4 +53,8 @@ export class StoragesResourceService { insertCollectionIntoState(this.store, storageCollection, key); }); } + + protected createStore():CollectionStore { + return new StoragesStore(); + } } diff --git a/frontend/src/app/core/state/views/views.service.ts b/frontend/src/app/core/state/views/views.service.ts index e51a65281a..b00bcfd2c2 100644 --- a/frontend/src/app/core/state/views/views.service.ts +++ b/frontend/src/app/core/state/views/views.service.ts @@ -11,7 +11,10 @@ import { import { ToastService } from 'core-app/shared/components/toaster/toast.service'; import { IHALCollection } from 'core-app/core/apiv3/types/hal-collection.type'; import { HttpClient } from '@angular/common/http'; -import { collectionKey } from 'core-app/core/state/collection-store'; +import { + collectionKey, + insertCollectionIntoState, +} from 'core-app/core/state/collection-store'; import { EffectHandler, } from 'core-app/core/state/effects/effect-handler.decorator'; @@ -22,14 +25,14 @@ import { IView } from 'core-app/core/state/views/view.model'; import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service'; import { ApiV3ListParameters } from 'core-app/core/apiv3/paths/apiv3-list-resource.interface'; import { addParamToHref } from 'core-app/shared/helpers/url-helpers'; +import { + CollectionStore, + ResourceCollectionService, +} from 'core-app/core/state/resource-collection.service'; @EffectHandler @Injectable() -export class ViewsResourceService { - protected store = new ViewsStore(); - - readonly query = new ViewsQuery(this.store); - +export class ViewsResourceService extends ResourceCollectionService { private get viewsPath():string { return this .apiV3Service @@ -43,6 +46,7 @@ export class ViewsResourceService { private apiV3Service:ApiV3Service, private toastService:ToastService, ) { + super(); } fetchViews(params:ApiV3ListParameters):Observable> { @@ -52,22 +56,7 @@ export class ViewsResourceService { .http .get>(addParamToHref(this.viewsPath + collectionURL, { pageSize: '-1' })) .pipe( - tap((events) => { - applyTransaction(() => { - this.store.upsertMany(events._embedded.elements); - this.store.update(({ collections }) => ( - { - collections: { - ...collections, - [collectionURL]: { - ...collections[collectionURL], - ids: events._embedded.elements.map((el) => el.id), - }, - }, - } - )); - }); - }), + tap((collection) => insertCollectionIntoState(this.store, collection, collectionURL)), catchError((error) => { this.toastService.addError(error); throw error; @@ -75,37 +64,7 @@ export class ViewsResourceService { ); } - update(id:ID, view:Partial):void { - this.store.update(id, view); - } - - modifyCollection(params:ApiV3ListParameters, callback:(collection:ID[]) => ID[]):void { - const key = collectionKey(params); - this.store.update(({ collections }) => ( - { - collections: { - ...collections, - [key]: { - ...collections[key], - ids: [...callback(collections[key]?.ids || [])], - }, - }, - } - )); - } - - removeFromCollection(params:ApiV3ListParameters, ids:ID[]):void { - const key = collectionKey(params); - this.store.update(({ collections }) => ( - { - collections: { - ...collections, - [key]: { - ...collections[key], - ids: (collections[key]?.ids || []).filter((id) => !ids.includes(id)), - }, - }, - } - )); + protected createStore():CollectionStore { + return new ViewsStore(); } } diff --git a/frontend/src/app/features/in-app-notifications/center/menu/menu.component.ts b/frontend/src/app/features/in-app-notifications/center/menu/menu.component.ts index f24f2e7908..fc87015535 100644 --- a/frontend/src/app/features/in-app-notifications/center/menu/menu.component.ts +++ b/frontend/src/app/features/in-app-notifications/center/menu/menu.component.ts @@ -94,7 +94,7 @@ export class IanMenuComponent implements OnInit { }, ]; - notificationsByProject$ = this.ianMenuService.query.notificationsByProject$.pipe( + notificationsByProject$ = this.ianMenuService.notificationsByProject$.pipe( map((items) => items .map((item) => ({ ...item, @@ -110,7 +110,7 @@ export class IanMenuComponent implements OnInit { })), ); - notificationsByReason$ = this.ianMenuService.query.notificationsByReason$.pipe( + notificationsByReason$ = this.ianMenuService.notificationsByReason$.pipe( map((items) => this.reasonMenuItems.map((reason) => ({ ...items.find((item) => item.value === reason.key), ...reason, diff --git a/frontend/src/app/features/in-app-notifications/center/menu/state/ian-menu.query.ts b/frontend/src/app/features/in-app-notifications/center/menu/state/ian-menu.query.ts deleted file mode 100644 index 3c2cd59fe7..0000000000 --- a/frontend/src/app/features/in-app-notifications/center/menu/state/ian-menu.query.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Query } from '@datorama/akita'; -import { combineLatest } from 'rxjs'; -import { map } from 'rxjs/operators'; -import idFromLink from 'core-app/features/hal/helpers/id-from-link'; -import { InAppNotificationsResourceService } from 'core-app/core/state/in-app-notifications/in-app-notifications.service'; -import { ProjectsResourceService } from 'core-app/core/state/projects/projects.service'; -import { IProject } from 'core-app/core/state/projects/project.model'; -import { collectionKey } from 'core-app/core/state/collection-store'; -import { - IanMenuState, - IanMenuStore, -} from './ian-menu.store'; - -export class IanMenuQuery extends Query { - projectsFilter$ = this.select('projectsFilter'); - - projectsForNotifications$ = combineLatest([ - this.projectsFilter$, - this.projectsResourceService.query.select(), - ]).pipe( - map(([filterParams, collectionData]) => { - const key = collectionKey(filterParams); - const collection = collectionData.collections[key]; - const ids = collection?.ids || []; - - return ids - .map((id:string) => this.projectsResourceService.query.getEntity(id)) - .filter((item:IProject|undefined) => !!item) as IProject[]; - }), - ); - - notificationsByProject$ = combineLatest([ - this.select('notificationsByProject'), - this.projectsForNotifications$, - ]).pipe( - map(([notifications, projects]) => notifications.map((notification) => { - const project = projects.find((p) => p.id.toString() === idFromLink(notification._links.valueLink[0].href)); - return { - ...notification, - projectHasParent: !!project?._links.parent.href, - }; - })), - ); - - notificationsByReason$ = this.select('notificationsByReason'); - - constructor( - protected store:IanMenuStore, - protected resourceService:InAppNotificationsResourceService, - protected projectsResourceService:ProjectsResourceService, - ) { - super(store); - } -} diff --git a/frontend/src/app/features/in-app-notifications/center/menu/state/ian-menu.service.ts b/frontend/src/app/features/in-app-notifications/center/menu/state/ian-menu.service.ts index 30cae6178f..37cfa3f84e 100644 --- a/frontend/src/app/features/in-app-notifications/center/menu/state/ian-menu.service.ts +++ b/frontend/src/app/features/in-app-notifications/center/menu/state/ian-menu.service.ts @@ -3,8 +3,8 @@ import { Injector, } from '@angular/core'; import { - notificationsMarkedRead, notificationCountIncreased, + notificationsMarkedRead, } from 'core-app/core/state/in-app-notifications/in-app-notifications.actions'; import { EffectCallback, @@ -16,12 +16,18 @@ import { ApiV3ListParameters } from 'core-app/core/apiv3/paths/apiv3-list-resour import idFromLink from 'core-app/features/hal/helpers/id-from-link'; import { InAppNotificationsResourceService } from 'core-app/core/state/in-app-notifications/in-app-notifications.service'; import { ProjectsResourceService } from 'core-app/core/state/projects/projects.service'; -import { IanMenuQuery } from './ian-menu.query'; import { - IanMenuStore, IAN_MENU_PROJECT_FILTERS, IAN_MENU_REASON_FILTERS, + IanMenuStore, } from './ian-menu.store'; +import { Query } from '@datorama/akita'; +import { + map, + switchMap, +} from 'rxjs/operators'; +import { collectionKey } from 'core-app/core/state/collection-store'; +import { combineLatest } from 'rxjs'; @Injectable() @EffectHandler @@ -30,7 +36,33 @@ export class IanMenuService { readonly store = new IanMenuStore(); - readonly query = new IanMenuQuery(this.store, this.ianResourceService, this.projectsResourceService); + readonly query = new Query(this.store); + + projectsFilter$ = this.query.select('projectsFilter'); + + projectsForNotifications$ = this + .projectsFilter$ + .pipe( + switchMap((filterParams) => { + const key = collectionKey(filterParams); + return this.projectsResourceService.collection(key); + }), + ); + + notificationsByProject$ = combineLatest([ + this.query.select('notificationsByProject'), + this.projectsForNotifications$, + ]).pipe( + map(([notifications, projects]) => notifications.map((notification) => { + const project = projects.find((p) => p.id.toString() === idFromLink(notification._links.valueLink[0].href)); + return { + ...notification, + projectHasParent: !!project?._links.parent.href, + }; + })), + ); + + notificationsByReason$ = this.query.select('notificationsByReason'); constructor( readonly injector:Injector, diff --git a/frontend/src/app/features/in-app-notifications/center/state/ian-center.service.ts b/frontend/src/app/features/in-app-notifications/center/state/ian-center.service.ts index cea0247ec8..874574dc74 100644 --- a/frontend/src/app/features/in-app-notifications/center/state/ian-center.service.ts +++ b/frontend/src/app/features/in-app-notifications/center/state/ian-center.service.ts @@ -4,6 +4,7 @@ import { } from '@angular/core'; import { debounceTime, + defaultIfEmpty, distinctUntilChanged, map, mapTo, @@ -85,7 +86,11 @@ export class IanCenterService extends UntilDestroyedMixin { selectNotifications$:Observable = this .activeCollection$ .pipe( - switchMap((collection) => forkJoin(collection.ids.map((id) => this.resourceService.lookup(id)))), + switchMap((collection) => { + const lookupId = (id:ID) => this.resourceService.lookup(id).pipe(take(1)); + return forkJoin(collection.ids.map(lookupId)) + .pipe(defaultIfEmpty([])); + }), ); aggregatedCenterNotifications$ = this diff --git a/frontend/src/app/features/team-planner/team-planner/planner/team-planner.component.ts b/frontend/src/app/features/team-planner/team-planner/planner/team-planner.component.ts index 415e7cbe71..674de47b37 100644 --- a/frontend/src/app/features/team-planner/team-planner/planner/team-planner.component.ts +++ b/frontend/src/app/features/team-planner/team-planner/planner/team-planner.component.ts @@ -266,7 +266,7 @@ export class TeamPlannerComponent extends UntilDestroyedMixin implements OnInit, principals$ = this.principalIds$ .pipe( this.untilDestroyed(), - mergeMap((ids:string[]) => this.principalsResourceService.query.byIds(ids)), + mergeMap((ids:string[]) => this.principalsResourceService.lookupMany(ids)), debounceTime(50), distinctUntilChanged((prev, curr) => prev.length === curr.length && prev.length === 0), shareReplay(1), diff --git a/frontend/src/app/shared/components/attachments/attachment-list/attachment-list-item.component.ts b/frontend/src/app/shared/components/attachments/attachment-list/attachment-list-item.component.ts index 2258445f76..07b6a04141 100644 --- a/frontend/src/app/shared/components/attachments/attachment-list/attachment-list-item.component.ts +++ b/frontend/src/app/shared/components/attachments/attachment-list/attachment-list-item.component.ts @@ -77,7 +77,9 @@ export class AttachmentListItemComponent implements OnInit { ngOnInit():void { const authorId = idFromLink(this.attachment._links.author.href); - this.author$ = this.principalsResourceService.query.selectEntity(authorId) + this.author$ = this + .principalsResourceService + .lookup(authorId) .pipe( switchMap((user) => (user ? of(user) : this.principalsResourceService.fetchUser(authorId))), map((user) => user as IUser), diff --git a/frontend/src/app/shared/components/attachments/attachment-list/attachment-list.component.ts b/frontend/src/app/shared/components/attachments/attachment-list/attachment-list.component.ts index 6b9608bb7f..13438b0428 100644 --- a/frontend/src/app/shared/components/attachments/attachment-list/attachment-list.component.ts +++ b/frontend/src/app/shared/components/attachments/attachment-list/attachment-list.component.ts @@ -37,7 +37,7 @@ import { HalResource } from 'core-app/features/hal/resources/hal-resource'; import { IAttachment } from 'core-app/core/state/attachments/attachment.model'; import { UntilDestroyedMixin } from 'core-app/shared/helpers/angular/until-destroyed.mixin'; import { AttachmentsResourceService } from 'core-app/core/state/attachments/attachments.service'; -import { map, switchMap, tap } from 'rxjs/operators'; +import { tap } from 'rxjs/operators'; import isNewResource from 'core-app/features/hal/helpers/is-new-resource'; import { Observable } from 'rxjs'; @@ -72,12 +72,11 @@ export class AttachmentListComponent extends UntilDestroyedMixin implements OnIn this.attachmentsResourceService.requireCollection(this.attachmentsSelfLink); } - this.$attachments = this.attachmentsResourceService.query.select() + this.$attachments = this + .attachmentsResourceService + .collection(this.collectionKey) .pipe( this.untilDestroyed(), - map((state) => state.collections[this.collectionKey]?.ids), - switchMap((attachmentIds) => this.attachmentsResourceService.query.selectMany(attachmentIds)), - // store attachments for new resources directly into the resource. This way, the POST request to create the // resource embeds the attachments and the backend reroutes the anonymous attachments to the resource. tap((attachments) => {