diff --git a/frontend/src/app/features/in-app-notifications/bell/in-app-notification-bell.component.ts b/frontend/src/app/features/in-app-notifications/bell/in-app-notification-bell.component.ts index 434bce9255..59ca53ab78 100644 --- a/frontend/src/app/features/in-app-notifications/bell/in-app-notification-bell.component.ts +++ b/frontend/src/app/features/in-app-notifications/bell/in-app-notification-bell.component.ts @@ -1,5 +1,6 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; import { InAppNotificationsQuery } from 'core-app/features/in-app-notifications/store/in-app-notifications.query'; +import { InAppNotificationsStore } from 'core-app/features/in-app-notifications/store/in-app-notifications.store'; import { InAppNotificationsService } from 'core-app/features/in-app-notifications/store/in-app-notifications.service'; import { OpModalService } from 'core-app/shared/components/modal/modal.service'; import { merge, timer } from 'rxjs'; @@ -15,12 +16,17 @@ const POLLING_INTERVAL = 10000; templateUrl: './in-app-notification-bell.component.html', styleUrls: ['./in-app-notification-bell.component.sass'], changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + InAppNotificationsService, + InAppNotificationsStore, + InAppNotificationsQuery, + ], }) export class InAppNotificationBellComponent { polling$ = timer(10, POLLING_INTERVAL) .pipe( filter(() => this.activeWindow.isActive), - switchMap(() => this.inAppService.count$()), + switchMap(() => this.inAppService.fetchCount()), ); unreadCount$ = merge( diff --git a/frontend/src/app/features/in-app-notifications/center/in-app-notification-center.component.html b/frontend/src/app/features/in-app-notifications/center/in-app-notification-center.component.html index a2d0f115b0..abe43f402a 100644 --- a/frontend/src/app/features/in-app-notifications/center/in-app-notification-center.component.html +++ b/frontend/src/app/features/in-app-notifications/center/in-app-notification-center.component.html @@ -26,9 +26,9 @@ diff --git a/frontend/src/app/features/in-app-notifications/center/in-app-notification-center.component.ts b/frontend/src/app/features/in-app-notifications/center/in-app-notification-center.component.ts index db1cb1f8f1..99ab65ed7f 100644 --- a/frontend/src/app/features/in-app-notifications/center/in-app-notification-center.component.ts +++ b/frontend/src/app/features/in-app-notifications/center/in-app-notification-center.component.ts @@ -7,6 +7,7 @@ import { } from '@angular/core'; import { I18nService } from 'core-app/core/i18n/i18n.service'; import { InAppNotificationsQuery } from 'core-app/features/in-app-notifications/store/in-app-notifications.query'; +import { InAppNotificationsStore } from 'core-app/features/in-app-notifications/store/in-app-notifications.store'; import { InAppNotificationsService } from 'core-app/features/in-app-notifications/store/in-app-notifications.service'; import { NOTIFICATIONS_MAX_SIZE } from 'core-app/features/in-app-notifications/store/in-app-notification.model'; import { map } from 'rxjs/operators'; @@ -19,6 +20,11 @@ import { UIRouterGlobals } from '@uirouter/core'; templateUrl: './in-app-notification-center.component.html', styleUrls: ['./in-app-notification-center.component.sass'], changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + InAppNotificationsService, + InAppNotificationsStore, + InAppNotificationsQuery, + ], }) export class InAppNotificationCenterComponent implements OnInit { activeFacet$ = this.ianQuery.activeFacet$; @@ -42,6 +48,11 @@ export class InAppNotificationCenterComponent implements OnInit { map((facet:'unread'|'all') => this.text.no_results[facet] || this.text.no_results.unread), ); + totalCountWarning$ = this.ianQuery.notLoaded$.pipe(map((notLoaded:number) => this.I18n.t( + 'js.notifications.center.total_count_warning', + { newest_count: NOTIFICATIONS_MAX_SIZE, more_count: notLoaded }, + ))); + maxSize = NOTIFICATIONS_MAX_SIZE; facets:string[] = ['unread', 'all']; @@ -65,20 +76,11 @@ export class InAppNotificationCenterComponent implements OnInit { readonly ianQuery:InAppNotificationsQuery, readonly uiRouterGlobals:UIRouterGlobals, readonly state:StateService, - ) { - } + ) { } ngOnInit():void { - this.ianService.get(); - } - - totalCountWarning():string { - const state = this.ianQuery.getValue(); - - return this.I18n.t( - 'js.notifications.center.total_count_warning', - { newest_count: NOTIFICATIONS_MAX_SIZE, more_count: state.notShowing }, - ); + this.ianService.setActiveFacet('unread'); + this.ianService.setActiveFilters([]); } openSplitView($event:WorkPackageResource):void { diff --git a/frontend/src/app/features/in-app-notifications/store/in-app-notifications.query.ts b/frontend/src/app/features/in-app-notifications/store/in-app-notifications.query.ts index 1d94ac55d1..adaf78acc6 100644 --- a/frontend/src/app/features/in-app-notifications/store/in-app-notifications.query.ts +++ b/frontend/src/app/features/in-app-notifications/store/in-app-notifications.query.ts @@ -8,11 +8,16 @@ import { } from './in-app-notifications.store'; import { InAppNotification } from 'core-app/features/in-app-notifications/store/in-app-notification.model'; -@Injectable({ providedIn: 'root' }) +@Injectable() export class InAppNotificationsQuery extends QueryEntity { /** Select the active filter facet */ activeFacet$ = this.select('activeFacet'); + activeFetchParameters$ = this.select(['activeFacet', 'activeFilters']); + + /** Select the active filter facet */ + notLoaded$ = this.select('notLoaded'); + /** Get the faceted items */ faceted$ = this.activeFacet$ .pipe( @@ -60,7 +65,7 @@ export class InAppNotificationsQuery extends QueryEntity notShowing > 0), + map(({ notLoaded }) => notLoaded > 0), ); constructor(protected store:InAppNotificationsStore) { diff --git a/frontend/src/app/features/in-app-notifications/store/in-app-notifications.service.ts b/frontend/src/app/features/in-app-notifications/store/in-app-notifications.service.ts index 7d042087fe..3220ed6797 100644 --- a/frontend/src/app/features/in-app-notifications/store/in-app-notifications.service.ts +++ b/frontend/src/app/features/in-app-notifications/store/in-app-notifications.service.ts @@ -1,17 +1,15 @@ -import { EventEmitter, Injectable } from '@angular/core'; +import { Injectable } from '@angular/core'; +import { map, switchMap, tap, take, debounceTime } from 'rxjs/operators'; import { applyTransaction, ID, setLoading } from '@datorama/akita'; -import { Observable } from 'rxjs'; +import { ApiV3ListFilter } from 'core-app/core/apiv3/paths/apiv3-list-resource.interface'; import { APIV3Service } from 'core-app/core/apiv3/api-v3.service'; -import { map, switchMap, tap } from 'rxjs/operators'; import { NotificationsService } from 'core-app/shared/components/notifications/notifications.service'; import { InAppNotificationsQuery } from 'core-app/features/in-app-notifications/store/in-app-notifications.query'; -import { take } from 'rxjs/internal/operators/take'; import { HalResource } from 'core-app/features/hal/resources/hal-resource'; import { InAppNotificationsStore } from './in-app-notifications.store'; import { InAppNotification, NOTIFICATIONS_MAX_SIZE } from './in-app-notification.model'; -import { IHALCollection } from 'core-app/core/apiv3/types/hal-collection.type'; -@Injectable({ providedIn: 'root' }) +@Injectable() export class InAppNotificationsService { constructor( private store:InAppNotificationsStore, @@ -19,43 +17,50 @@ export class InAppNotificationsService { private apiV3Service:APIV3Service, private notifications:NotificationsService, ) { + this.query.activeFetchParameters$ + .pipe(debounceTime(0)) + .subscribe(() => { + this.fetchNotifications(); + this.fetchCount(); + }); } - public notificationsOfWpLoaded:EventEmitter> = new EventEmitter>(); - - get():void { + fetchNotifications() { this.store.setLoading(true); - const facet = this.query.getValue().activeFacet; + const { activeFacet, activeFilters } = this.query.getValue(); - this + const call = this .apiV3Service .notifications - .facet(facet, { pageSize: NOTIFICATIONS_MAX_SIZE }) + .facet(activeFacet, { + pageSize: NOTIFICATIONS_MAX_SIZE, + filters: activeFilters, + }); + + call .pipe( tap((events) => this.sideLoadInvolvedWorkPackages(events._embedded.elements)), ) .subscribe( - (events) => { - applyTransaction(() => { - this.store.set(events._embedded.elements); - this.store.update({ notShowing: events.total - events.count }); - }); - }, - (error) => { - this.notifications.addError(error); - }, + (events) => applyTransaction(() => { + this.store.set(events._embedded.elements); + this.store.update({ notShowing: events.total - events.count }); + }), + (error) => this.notifications.addError(error), ) - .add( - () => this.store.setLoading(false), - ); + .add(() => this.store.setLoading(false)); + + return call; } - count$():Observable { + fetchCount() { + const { activeFilters } = this.query.getValue(); + return this .apiV3Service .notifications - .unread({ pageSize: 0 }) + .unread({ pageSize: 0, filters: activeFilters }) .pipe( map((events) => events.total), tap((unreadCount) => { @@ -72,8 +77,12 @@ export class InAppNotificationsService { this.store.update((state) => ({ ...state, activeFacet: facet })); } - markAllRead():void { - this.query + setActiveFilters(filters:ApiV3ListFilter[]):void { + this.store.update((state) => ({ ...state, activeFilters: filters })); + } + + markAllRead() { + return this.query .unread$ .pipe( take(1), @@ -88,10 +97,10 @@ export class InAppNotificationsService { }); } - markAsRead(notifications:InAppNotification[], keep = false):void { + markAsRead(notifications:InAppNotification[], keep = false) { const ids = notifications.map((n) => n.id); - this + return this .apiV3Service .notifications .markRead(ids) @@ -108,24 +117,6 @@ export class InAppNotificationsService { }); } - loadNotificationsOfWorkPackage(workPackageId:string):void { - this - .apiV3Service - .notifications - .facet( - 'unread', - { - pageSize: NOTIFICATIONS_MAX_SIZE, - filters: [ - ['resourceId', '=', [workPackageId]], - ['resourceType', '=', ['WorkPackage']], - ], - }, - ).subscribe((notificationCollection) => { - this.notificationsOfWpLoaded.emit(notificationCollection); - }); - } - private sideLoadInvolvedWorkPackages(elements:InAppNotification[]) { const wpIds = elements.map((element) => { const href = element._links.resource?.href; @@ -137,22 +128,4 @@ export class InAppNotificationsService { .work_packages .requireAll(_.compact(wpIds)); } - - collapse(notification:InAppNotification):void { - this.store.update( - notification.id, - { - expanded: false, - }, - ); - } - - expand(notification:InAppNotification):void { - this.store.update( - notification.id, - { - expanded: true, - }, - ); - } } diff --git a/frontend/src/app/features/in-app-notifications/store/in-app-notifications.store.ts b/frontend/src/app/features/in-app-notifications/store/in-app-notifications.store.ts index c53b8280da..5a415c207c 100644 --- a/frontend/src/app/features/in-app-notifications/store/in-app-notifications.store.ts +++ b/frontend/src/app/features/in-app-notifications/store/in-app-notifications.store.ts @@ -1,26 +1,29 @@ import { Injectable } from '@angular/core'; import { EntityState, EntityStore, StoreConfig } from '@datorama/akita'; import { InAppNotification } from './in-app-notification.model'; +import { ApiV3ListFilter } from 'core-app/core/apiv3/paths/apiv3-list-resource.interface'; export interface InAppNotificationsState extends EntityState { /** The entities in the store might not all be unread so we keep separate count */ unreadCount:number; /** Number of elements not showing after max values loaded */ - notShowing:number; + notLoaded:number; activeFacet:string; + activeFilters:ApiV3ListFilter[]; expanded:boolean; } export function createInitialState():InAppNotificationsState { return { unreadCount: 0, - notShowing: 0, + notLoaded: 0, activeFacet: 'unread', + activeFilters: [], expanded: false, }; } -@Injectable({ providedIn: 'root' }) +@Injectable() @StoreConfig({ name: 'in-app-notifications' }) export class InAppNotificationsStore extends EntityStore { constructor() { diff --git a/frontend/src/app/features/work-packages/components/wp-single-view-tabs/activity-panel/activity-base.controller.ts b/frontend/src/app/features/work-packages/components/wp-single-view-tabs/activity-panel/activity-base.controller.ts index 1e88addfb9..b4ad7ab6f8 100644 --- a/frontend/src/app/features/work-packages/components/wp-single-view-tabs/activity-panel/activity-base.controller.ts +++ b/frontend/src/app/features/work-packages/components/wp-single-view-tabs/activity-panel/activity-base.controller.ts @@ -28,7 +28,6 @@ import { ChangeDetectorRef, Directive, OnInit } from '@angular/core'; import { Transition } from '@uirouter/core'; -import { combineLatest } from 'rxjs'; import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource'; import { HalResource } from 'core-app/features/hal/resources/hal-resource'; import { ActivityEntryInfo } from 'core-app/features/work-packages/components/wp-single-view-tabs/activity-panel/activity-entry-info'; @@ -95,6 +94,15 @@ export class ActivityPanelBaseController extends UntilDestroyedMixin implements }); }); + + this.ianService.setActiveFacet('unread'); + this.ianService.setActiveFilters([ + ['resourceId', '=', [workPackageId]], + ['resourceType', '=', ['WorkPackage']], + ]); + this.ianService.fetchNotifications(); + + this.ianService.loadNotificationsOfWorkPackage(this.workPackageId); this.ianService.notificationsOfWpLoaded.subscribe((notificationCollection) => { this.notifications = notificationCollection._embedded.elements; diff --git a/frontend/src/app/features/work-packages/routing/wp-full-view/wp-full-view.component.ts b/frontend/src/app/features/work-packages/routing/wp-full-view/wp-full-view.component.ts index 88d0f8bd31..e548fa6e23 100644 --- a/frontend/src/app/features/work-packages/routing/wp-full-view/wp-full-view.component.ts +++ b/frontend/src/app/features/work-packages/routing/wp-full-view/wp-full-view.component.ts @@ -29,9 +29,12 @@ import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource'; import { StateService } from '@uirouter/core'; import { Component, Injector, OnInit } from '@angular/core'; +import { of } from 'rxjs'; import { WorkPackageViewSelectionService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-selection.service'; import { WorkPackageSingleViewBase } from 'core-app/features/work-packages/routing/wp-view-base/work-package-single-view.base'; -import { of } from 'rxjs'; +import { InAppNotificationsQuery } from 'core-app/features/in-app-notifications/store/in-app-notifications.query'; +import { InAppNotificationsStore } from 'core-app/features/in-app-notifications/store/in-app-notifications.store'; +import { InAppNotificationsService } from 'core-app/features/in-app-notifications/store/in-app-notifications.service'; import { HalResourceNotificationService } from 'core-app/features/hal/services/hal-resource-notification.service'; import { WorkPackageNotificationService } from 'core-app/features/work-packages/services/notifications/work-package-notification.service'; import { InAppNotificationsService } from 'core-app/features/in-app-notifications/store/in-app-notifications.service'; @@ -43,6 +46,9 @@ import { InAppNotificationsService } from 'core-app/features/in-app-notification host: { class: 'work-packages-page--ui-view' }, providers: [ { provide: HalResourceNotificationService, useExisting: WorkPackageNotificationService }, + InAppNotificationsService, + InAppNotificationsStore, + InAppNotificationsQuery, ], }) export class WorkPackagesFullViewComponent extends WorkPackageSingleViewBase implements OnInit { diff --git a/frontend/src/app/features/work-packages/routing/wp-split-view/wp-split-view.component.ts b/frontend/src/app/features/work-packages/routing/wp-split-view/wp-split-view.component.ts index aded989e16..d8b7de7695 100644 --- a/frontend/src/app/features/work-packages/routing/wp-split-view/wp-split-view.component.ts +++ b/frontend/src/app/features/work-packages/routing/wp-split-view/wp-split-view.component.ts @@ -30,6 +30,9 @@ import { ChangeDetectionStrategy, Component, Injector, OnInit } from '@angular/c import { StateService } from '@uirouter/core'; import { WorkPackageViewFocusService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-focus.service'; import { States } from 'core-app/core/states/states.service'; +import { InAppNotificationsQuery } from 'core-app/features/in-app-notifications/store/in-app-notifications.query'; +import { InAppNotificationsStore } from 'core-app/features/in-app-notifications/store/in-app-notifications.store'; +import { InAppNotificationsService } from 'core-app/features/in-app-notifications/store/in-app-notifications.service'; import { FirstRouteService } from 'core-app/core/routing/first-route-service'; import { KeepTabService } from 'core-app/features/work-packages/components/wp-single-view-tabs/keep-tab/keep-tab.service'; import { WorkPackageViewSelectionService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-selection.service'; @@ -45,6 +48,9 @@ import { InAppNotificationsService } from 'core-app/features/in-app-notification selector: 'wp-split-view-entry', providers: [ { provide: HalResourceNotificationService, useClass: WorkPackageNotificationService }, + InAppNotificationsService, + InAppNotificationsStore, + InAppNotificationsQuery, ], }) export class WorkPackageSplitViewComponent extends WorkPackageSingleViewBase implements OnInit {