WIP: Implement scoped stores for notification state

pull/9599/head
Benjamin Bädorf 3 years ago
parent e6ce5f8a87
commit f582824e68
No known key found for this signature in database
GPG Key ID: 069CA2D117AB5CCF
  1. 8
      frontend/src/app/features/in-app-notifications/bell/in-app-notification-bell.component.ts
  2. 6
      frontend/src/app/features/in-app-notifications/center/in-app-notification-center.component.html
  3. 26
      frontend/src/app/features/in-app-notifications/center/in-app-notification-center.component.ts
  4. 9
      frontend/src/app/features/in-app-notifications/store/in-app-notifications.query.ts
  5. 105
      frontend/src/app/features/in-app-notifications/store/in-app-notifications.service.ts
  6. 9
      frontend/src/app/features/in-app-notifications/store/in-app-notifications.store.ts
  7. 10
      frontend/src/app/features/work-packages/components/wp-single-view-tabs/activity-panel/activity-base.controller.ts
  8. 8
      frontend/src/app/features/work-packages/routing/wp-full-view/wp-full-view.component.ts
  9. 6
      frontend/src/app/features/work-packages/routing/wp-split-view/wp-split-view.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(

@ -26,9 +26,9 @@
</div>
<div class="op-ian-center--footer">
<p
class="op-ian-center--max-warning"
*ngIf="hasMoreThanPageSize$ | async"
[textContent]="totalCountWarning()"
class="op-ian-center--max-warning"
*ngIf="hasMoreThanPageSize$ | async"
[textContent]="totalCountWarning$ | async"
>
</p>
</div>

@ -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 {

@ -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<InAppNotificationsState> {
/** 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<InAppNotificationsState
hasMoreThanPageSize$ = this
.select()
.pipe(
map(({ notShowing }) => notShowing > 0),
map(({ notLoaded }) => notLoaded > 0),
);
constructor(protected store:InAppNotificationsStore) {

@ -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<IHALCollection<InAppNotification>> = new EventEmitter<IHALCollection<InAppNotification>>();
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<number> {
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,
},
);
}
}

@ -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<InAppNotification> {
/** 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<InAppNotificationsState> {
constructor() {

@ -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;

@ -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 {

@ -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 {

Loading…
Cancel
Save