mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
Merge pull request #17323 from opf/implementation/59434-visualize-reminders-in-notification-center
implementation/59434 visualize reminders in notification center
This commit is contained in:
@@ -63,7 +63,7 @@ module Notifications
|
||||
end
|
||||
|
||||
def reason_filters
|
||||
%w[mentioned assigned responsible watched dateAlert shared].map do |reason|
|
||||
%w[mentioned assigned responsible watched dateAlert shared reminder].map do |reason|
|
||||
count = unread_by_reason[reason]
|
||||
menu_item(title: I18n.t("notifications.reasons.#{reason}"),
|
||||
icon_key: reason,
|
||||
@@ -128,7 +128,8 @@ module Notifications
|
||||
"responsible" => :"op-person-accountable",
|
||||
"watched" => :eye,
|
||||
"shared" => :"share-android",
|
||||
"dateAlert" => :"op-calendar-alert"
|
||||
"dateAlert" => :"op-calendar-alert",
|
||||
"reminder" => :clock
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
@@ -1522,6 +1522,7 @@ en:
|
||||
login: "Username"
|
||||
mail: "Email"
|
||||
name: "Name"
|
||||
note: "Note"
|
||||
password: "Password"
|
||||
priority: "Priority"
|
||||
project: "Project"
|
||||
@@ -2204,6 +2205,7 @@ en:
|
||||
responsible: "Accountable"
|
||||
shared: "Shared"
|
||||
watched: "Watcher"
|
||||
reminder: "Reminder"
|
||||
facets:
|
||||
unread: "Unread"
|
||||
unread_title: "Show unread"
|
||||
|
||||
@@ -709,6 +709,8 @@ en:
|
||||
new_notifications:
|
||||
message: "There are new notifications."
|
||||
link_text: "Click here to load them."
|
||||
reminders:
|
||||
note: "Note: “%{note}”"
|
||||
settings:
|
||||
change_notification_settings: 'You can modify your <a target="_blank" href="%{url}">notification settings</a> to ensure you never miss an important update.'
|
||||
title: "Notification settings"
|
||||
|
||||
@@ -13,7 +13,7 @@ export interface IInAppNotificationHalResourceLinks extends IHalResourceLinks {
|
||||
activity:IHalResourceLink;
|
||||
}
|
||||
|
||||
export type IInAppNotificationDetailsAttribute = 'startDate'|'dueDate'|'date';
|
||||
export type IInAppNotificationDetailsAttribute = 'startDate'|'dueDate'|'date'|'note';
|
||||
|
||||
export interface IInAppNotificationDetailsResource {
|
||||
property:IInAppNotificationDetailsAttribute;
|
||||
|
||||
@@ -111,7 +111,28 @@ export class IanCenterService extends UntilDestroyedMixin {
|
||||
notifications$ = this
|
||||
.aggregatedCenterNotifications$
|
||||
.pipe(
|
||||
map((items) => Object.values(items)),
|
||||
map((items) => {
|
||||
return Object.values(items).reduce((acc, workPackageNotificationGroup) => {
|
||||
const { reminders, others } = workPackageNotificationGroup.reduce((result, notification) => {
|
||||
if (notification.reason === 'reminder') {
|
||||
result.reminders.push(notification);
|
||||
} else {
|
||||
result.others.push(notification);
|
||||
}
|
||||
return result;
|
||||
}, { reminders: [] as INotification[], others: [] as INotification[] });
|
||||
|
||||
// Extract reminders into standalone groups so they can be displayed individually
|
||||
if (reminders.length > 0) {
|
||||
reminders.forEach((reminder) => acc.push([reminder]));
|
||||
}
|
||||
if (others.length > 0) {
|
||||
acc.push(others);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [] as INotification[][]);
|
||||
}),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
|
||||
|
||||
+3
-5
@@ -1,8 +1,6 @@
|
||||
<div
|
||||
class="op-ian-actors--date"
|
||||
[title]="fixedTime"
|
||||
[textContent]="relativeTime$ | async"
|
||||
></div>
|
||||
<op-in-app-notification-relative-time
|
||||
[notification]="notification"
|
||||
/>
|
||||
<div class="op-ian-actors--container">
|
||||
<ng-container *ngFor="let actor of actors | slice:0:3; let idx = index; let last = last">
|
||||
<span *ngIf="last && actors.length > 1 && actors.length < 4" textContent=" {{ text.and }} "></span>
|
||||
|
||||
-5
@@ -7,11 +7,6 @@
|
||||
align-items: center
|
||||
color: var(--fgColor-muted)
|
||||
|
||||
&--date
|
||||
@include text-shortener
|
||||
max-width: 100%
|
||||
line-height: 1rem
|
||||
|
||||
&--container
|
||||
@include text-shortener
|
||||
display: flex
|
||||
|
||||
+2
-35
@@ -1,11 +1,8 @@
|
||||
import { ChangeDetectionStrategy, Component, HostBinding, Input, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { DeviceService } from 'core-app/core/browser/device.service';
|
||||
import { I18nService } from 'core-app/core/i18n/i18n.service';
|
||||
import { INotification } from 'core-app/core/state/in-app-notifications/in-app-notification.model';
|
||||
import { PrincipalLike } from 'core-app/shared/components/principal/principal-types';
|
||||
import { Observable, timer } from 'rxjs';
|
||||
import { distinctUntilChanged, map } from 'rxjs/operators';
|
||||
import { I18nService } from 'core-app/core/i18n/i18n.service';
|
||||
import { TimezoneService } from 'core-app/core/datetime/timezone.service';
|
||||
import { DeviceService } from 'core-app/core/browser/device.service';
|
||||
|
||||
@Component({
|
||||
selector: 'op-in-app-notification-actors-line',
|
||||
@@ -24,13 +21,6 @@ export class InAppNotificationActorsLineComponent implements OnInit {
|
||||
// The actor, if any
|
||||
actors:PrincipalLike[] = [];
|
||||
|
||||
// Fixed notification time
|
||||
fixedTime:string;
|
||||
|
||||
// Format relative elapsed time (n seconds/minutes/hours ago)
|
||||
// at an interval for auto updating
|
||||
relativeTime$:Observable<string>;
|
||||
|
||||
text = {
|
||||
and: this.I18n.t('js.notifications.center.label_actor_and'),
|
||||
and_other_singular: this.I18n.t('js.notifications.center.and_more_users.one'),
|
||||
@@ -41,21 +31,14 @@ export class InAppNotificationActorsLineComponent implements OnInit {
|
||||
loading: this.I18n.t('js.ajax.loading'),
|
||||
placeholder: this.I18n.t('js.placeholders.default'),
|
||||
mark_as_read: this.I18n.t('js.notifications.center.mark_as_read'),
|
||||
updated_by_at: (age:string):string => this.I18n.t(
|
||||
'js.notifications.center.text_update_date_by',
|
||||
{ date: age },
|
||||
),
|
||||
};
|
||||
|
||||
constructor(
|
||||
readonly deviceService:DeviceService,
|
||||
private I18n:I18nService,
|
||||
private timezoneService:TimezoneService,
|
||||
) { }
|
||||
|
||||
ngOnInit():void {
|
||||
this.buildTime();
|
||||
|
||||
// Don't show the actor if the first item is actor-less (date alert)
|
||||
if (this.notification._links.actor) {
|
||||
this.buildActors();
|
||||
@@ -70,22 +53,6 @@ export class InAppNotificationActorsLineComponent implements OnInit {
|
||||
return this.text.and_other_plural(number);
|
||||
}
|
||||
|
||||
private buildTime() {
|
||||
this.fixedTime = this.timezoneService.formattedDatetime(this.notification.createdAt);
|
||||
this.relativeTime$ = timer(0, 10000)
|
||||
.pipe(
|
||||
map(() => {
|
||||
const time = this.timezoneService.formattedRelativeDateTime(this.notification.createdAt);
|
||||
if (this.notification._links.actor) {
|
||||
return this.text.updated_by_at(time);
|
||||
}
|
||||
|
||||
return time;
|
||||
}),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
}
|
||||
|
||||
private buildActors() {
|
||||
const actors = this
|
||||
.aggregatedNotifications
|
||||
|
||||
+4
-3
@@ -6,9 +6,9 @@ import {
|
||||
OnInit,
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core';
|
||||
import { INotification } from 'core-app/core/state/in-app-notifications/in-app-notification.model';
|
||||
import { I18nService } from 'core-app/core/i18n/i18n.service';
|
||||
import { TimezoneService } from 'core-app/core/datetime/timezone.service';
|
||||
import { I18nService } from 'core-app/core/i18n/i18n.service';
|
||||
import { IInAppNotificationDetailsAttribute, INotification } from 'core-app/core/state/in-app-notifications/in-app-notification.model';
|
||||
import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource';
|
||||
import * as moment from 'moment';
|
||||
import { Moment } from 'moment';
|
||||
@@ -49,6 +49,7 @@ export class InAppNotificationDateAlertComponent implements OnInit {
|
||||
dueDate: this.I18n.t('js.work_packages.properties.dueDate'),
|
||||
date: this.I18n.t('js.notifications.date_alerts.milestone_date'),
|
||||
due_today: this.I18n.t('js.notifications.date_alerts.property_today'),
|
||||
note: '', // date alerts do not have notes
|
||||
};
|
||||
|
||||
constructor(
|
||||
@@ -71,7 +72,7 @@ export class InAppNotificationDateAlertComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
private deriveDueDate(value:string, property:'startDate'|'dueDate'|'date') {
|
||||
private deriveDueDate(value:string, property:IInAppNotificationDetailsAttribute) {
|
||||
const dateValue = this.timezoneService.parseISODate(value).startOf('day');
|
||||
const today = moment();
|
||||
this.dateIsPast = dateValue.isBefore(today, 'day');
|
||||
|
||||
+15
-9
@@ -88,16 +88,22 @@
|
||||
<div
|
||||
class="op-ian-item--bottom-line"
|
||||
>
|
||||
<op-in-app-notification-date-alert
|
||||
*ngIf="showDateAlert"
|
||||
[workPackage]="workPackage"
|
||||
<op-in-app-notification-reminder-alert
|
||||
*ngIf="hasReminderAlert"
|
||||
[aggregatedNotifications]="aggregatedNotifications"
|
||||
></op-in-app-notification-date-alert>
|
||||
<op-in-app-notification-actors-line
|
||||
*ngIf="!showDateAlert"
|
||||
[notification]="notification"
|
||||
[aggregatedNotifications]="aggregatedNotifications"
|
||||
></op-in-app-notification-actors-line>
|
||||
/>
|
||||
<ng-container *ngIf="!hasReminderAlert">
|
||||
<op-in-app-notification-date-alert
|
||||
*ngIf="showDateAlert"
|
||||
[workPackage]="workPackage"
|
||||
[aggregatedNotifications]="aggregatedNotifications"
|
||||
></op-in-app-notification-date-alert>
|
||||
<op-in-app-notification-actors-line
|
||||
*ngIf="!showDateAlert"
|
||||
[notification]="notification"
|
||||
[aggregatedNotifications]="aggregatedNotifications"
|
||||
></op-in-app-notification-actors-line>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #workPackageLoading>
|
||||
|
||||
+2
@@ -29,6 +29,7 @@ export class InAppNotificationEntryComponent implements OnInit {
|
||||
workPackage$:Observable<WorkPackageResource>|null = null;
|
||||
|
||||
showDateAlert = false;
|
||||
hasReminderAlert = false;
|
||||
|
||||
loading$ = this.storeService.query.selectLoading();
|
||||
|
||||
@@ -62,6 +63,7 @@ export class InAppNotificationEntryComponent implements OnInit {
|
||||
const href = this.notification._links.resource?.href;
|
||||
this.workPackageId = href && HalResource.matchFromLink(href, 'work_packages');
|
||||
|
||||
this.hasReminderAlert = this.aggregatedNotifications.some((notification) => notification.reason === 'reminder');
|
||||
this.showDateAlert = this.hasActiveDateAlert();
|
||||
this.buildTranslatedReason();
|
||||
this.buildProject();
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
<div
|
||||
class="op-ian-relative-time"
|
||||
[title]="fixedTime"
|
||||
[textContent]="relativeTime$ | async">
|
||||
</div>
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
@import "helpers"
|
||||
|
||||
.op-ian-relative-time
|
||||
@include text-shortener
|
||||
max-width: 100%
|
||||
line-height: 1rem
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
Input,
|
||||
OnInit,
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core';
|
||||
import { TimezoneService } from 'core-app/core/datetime/timezone.service';
|
||||
import { I18nService } from 'core-app/core/i18n/i18n.service';
|
||||
import { INotification } from 'core-app/core/state/in-app-notifications/in-app-notification.model';
|
||||
import { Observable, timer } from 'rxjs';
|
||||
import { distinctUntilChanged, map } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'op-in-app-notification-relative-time',
|
||||
templateUrl: './in-app-notification-relative-time.component.html',
|
||||
styleUrls: ['./in-app-notification-relative-time.component.sass'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
})
|
||||
export class InAppNotificationRelativeTimeComponent implements OnInit {
|
||||
@Input() notification:INotification;
|
||||
@Input() hasActorByLine:boolean = true;
|
||||
|
||||
// Fixed notification time
|
||||
fixedTime:string;
|
||||
|
||||
// Format relative elapsed time (n seconds/minutes/hours ago)
|
||||
// at an interval for auto updating
|
||||
relativeTime$:Observable<string>;
|
||||
|
||||
text = {
|
||||
updated_by_at: (age:string):string => this.I18n.t(
|
||||
'js.notifications.center.text_update_date_by',
|
||||
{ date: age },
|
||||
),
|
||||
};
|
||||
|
||||
constructor(
|
||||
private I18n:I18nService,
|
||||
private timezoneService:TimezoneService,
|
||||
) { }
|
||||
|
||||
ngOnInit():void {
|
||||
this.buildTime();
|
||||
}
|
||||
|
||||
private buildTime() {
|
||||
this.fixedTime = this.timezoneService.formattedDatetime(this.notification.createdAt);
|
||||
this.relativeTime$ = timer(0, 10000)
|
||||
.pipe(
|
||||
map(() => {
|
||||
const time = this.timezoneService.formattedRelativeDateTime(this.notification.createdAt);
|
||||
if (this.hasActorByLine && this.notification._links.actor) {
|
||||
return this.text.updated_by_at(time);
|
||||
}
|
||||
|
||||
return `${time}.`;
|
||||
}),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<op-in-app-notification-relative-time
|
||||
[notification]="reminderAlert"
|
||||
[hasActorByLine]="false"
|
||||
/>
|
||||
<span
|
||||
class="op-ian-reminder-alert--note"
|
||||
[textContent]="reminderNote"
|
||||
></span>
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
@import "helpers"
|
||||
|
||||
.op-ian-reminder-alert
|
||||
display: grid
|
||||
grid-template-columns: auto 1fr
|
||||
grid-column-gap: $spot-spacing-0_25
|
||||
align-items: center
|
||||
color: var(--fgColor-muted)
|
||||
|
||||
&--note
|
||||
@include text-shortener
|
||||
line-height: 1rem
|
||||
color: var(--fgColor-default)
|
||||
|
||||
> *
|
||||
flex-shrink: 0
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
HostBinding,
|
||||
Input,
|
||||
OnInit,
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core';
|
||||
import { I18nService } from 'core-app/core/i18n/i18n.service';
|
||||
import { IInAppNotificationDetailsResource, INotification } from 'core-app/core/state/in-app-notifications/in-app-notification.model';
|
||||
|
||||
@Component({
|
||||
selector: 'op-in-app-notification-reminder-alert',
|
||||
templateUrl: './in-app-notification-reminder-alert.component.html',
|
||||
styleUrls: ['./in-app-notification-reminder-alert.component.sass'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
})
|
||||
export class InAppNotificationReminderAlertComponent implements OnInit {
|
||||
@Input() aggregatedNotifications:INotification[];
|
||||
|
||||
@HostBinding('class.op-ian-reminder-alert') className = true;
|
||||
|
||||
reminderNote:string;
|
||||
reminderAlert:INotification;
|
||||
|
||||
constructor(
|
||||
private I18n:I18nService,
|
||||
) { }
|
||||
|
||||
ngOnInit():void {
|
||||
this.reminderAlert = this.deriveMostRecentReminder(this.aggregatedNotifications);
|
||||
this.reminderNote = this.extractReminderNoteValue(this.reminderAlert._embedded.details);
|
||||
}
|
||||
|
||||
private deriveMostRecentReminder(aggregatedNotifications:INotification[]):INotification {
|
||||
const reminderAlerts = aggregatedNotifications.filter((notification:INotification) => notification.reason === 'reminder');
|
||||
|
||||
if (reminderAlerts.length > 1) {
|
||||
const mostRecent = reminderAlerts.reduce((prev:INotification, current:INotification) => {
|
||||
const prevDate = new Date(prev.createdAt);
|
||||
const currentDate = new Date(current.createdAt);
|
||||
return prevDate > currentDate ? prev : current;
|
||||
});
|
||||
return mostRecent;
|
||||
}
|
||||
|
||||
return reminderAlerts[0];
|
||||
}
|
||||
|
||||
private extractReminderNoteValue(details:IInAppNotificationDetailsResource[]):string {
|
||||
const noteDetail = details.find((detail:IInAppNotificationDetailsResource) => detail.property === 'note');
|
||||
if (noteDetail?.value) {
|
||||
return this.I18n.t('js.notifications.reminders.note', { note: (noteDetail?.value) });
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,33 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { OpSharedModule } from 'core-app/shared/shared.module';
|
||||
import { ScrollingModule } from '@angular/cdk/scrolling';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { IconModule } from 'core-app/shared/components/icon/icon.module';
|
||||
import { NgModule } from '@angular/core';
|
||||
import {
|
||||
InAppNotificationBellComponent,
|
||||
} from 'core-app/features/in-app-notifications/bell/in-app-notification-bell.component';
|
||||
import {
|
||||
InAppNotificationEntryComponent,
|
||||
} from 'core-app/features/in-app-notifications/entry/in-app-notification-entry.component';
|
||||
import { OpenprojectPrincipalRenderingModule } from 'core-app/shared/components/principal/principal-rendering.module';
|
||||
import { ScrollingModule } from '@angular/cdk/scrolling';
|
||||
import { IanBellService } from 'core-app/features/in-app-notifications/bell/state/ian-bell.service';
|
||||
import {
|
||||
InAppNotificationCenterComponent,
|
||||
} from 'core-app/features/in-app-notifications/center/in-app-notification-center.component';
|
||||
import { OpenprojectWorkPackagesModule } from 'core-app/features/work-packages/openproject-work-packages.module';
|
||||
import { DynamicModule } from 'ng-dynamic-component';
|
||||
import { InAppNotificationStatusComponent } from './entry/status/in-app-notification-status.component';
|
||||
import {
|
||||
OpenprojectContentLoaderModule,
|
||||
} from 'core-app/shared/components/op-content-loader/openproject-content-loader.module';
|
||||
import { IanBellService } from 'core-app/features/in-app-notifications/bell/state/ian-bell.service';
|
||||
import { InAppNotificationActorsLineComponent } from './entry/actors-line/in-app-notification-actors-line.component';
|
||||
import { InAppNotificationDateAlertComponent } from './entry/date-alert/in-app-notification-date-alert.component';
|
||||
import { IanCenterService } from 'core-app/features/in-app-notifications/center/state/ian-center.service';
|
||||
import {
|
||||
InAppNotificationsDateAlertsUpsaleComponent,
|
||||
} from 'core-app/features/in-app-notifications/date-alerts-upsale/ian-date-alerts-upsale.component';
|
||||
import { IanCenterService } from 'core-app/features/in-app-notifications/center/state/ian-center.service';
|
||||
import {
|
||||
InAppNotificationEntryComponent,
|
||||
} from 'core-app/features/in-app-notifications/entry/in-app-notification-entry.component';
|
||||
import { OpenprojectWorkPackagesModule } from 'core-app/features/work-packages/openproject-work-packages.module';
|
||||
import { IconModule } from 'core-app/shared/components/icon/icon.module';
|
||||
import {
|
||||
OpenprojectContentLoaderModule,
|
||||
} from 'core-app/shared/components/op-content-loader/openproject-content-loader.module';
|
||||
import { OpenprojectPrincipalRenderingModule } from 'core-app/shared/components/principal/principal-rendering.module';
|
||||
import { OpSharedModule } from 'core-app/shared/shared.module';
|
||||
import { DynamicModule } from 'ng-dynamic-component';
|
||||
import { InAppNotificationActorsLineComponent } from './entry/actors-line/in-app-notification-actors-line.component';
|
||||
import { InAppNotificationDateAlertComponent } from './entry/date-alert/in-app-notification-date-alert.component';
|
||||
import { InAppNotificationRelativeTimeComponent } from './entry/relative-time/in-app-notification-relative-time.component';
|
||||
import { InAppNotificationReminderAlertComponent } from './entry/reminder-alert/in-app-notification-reminder-alert.component';
|
||||
import { InAppNotificationStatusComponent } from './entry/status/in-app-notification-status.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -36,6 +38,8 @@ import { IanCenterService } from 'core-app/features/in-app-notifications/center/
|
||||
InAppNotificationActorsLineComponent,
|
||||
InAppNotificationDateAlertComponent,
|
||||
InAppNotificationsDateAlertsUpsaleComponent,
|
||||
InAppNotificationRelativeTimeComponent,
|
||||
InAppNotificationReminderAlertComponent,
|
||||
],
|
||||
imports: [
|
||||
OpSharedModule,
|
||||
|
||||
@@ -104,7 +104,7 @@ module API
|
||||
"Notification"
|
||||
end
|
||||
|
||||
self.to_eager_load = %i[actor journal]
|
||||
self.to_eager_load = %i[actor journal reminder]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# --copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
# ++
|
||||
|
||||
module API::V3::Notifications::PropertyFactory
|
||||
module Reminder
|
||||
extend ::API::V3::Utilities::PathHelper
|
||||
|
||||
module_function
|
||||
|
||||
def for(notification)
|
||||
return [] unless notification.reminder
|
||||
|
||||
[
|
||||
::API::V3::Values::PropertyGenericRepresenter
|
||||
.new(::API::V3::Values::PropertyModel.new(:note, notification.reminder.note),
|
||||
self_link: api_v3_paths.notification_detail(notification.id, 0))
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,33 @@
|
||||
# --copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
# ++
|
||||
|
||||
module API::V3::Values
|
||||
class PropertyGenericRepresenter < PropertyRepresenter
|
||||
property :value
|
||||
end
|
||||
end
|
||||
@@ -29,7 +29,7 @@
|
||||
module API::V3::Values::Schemas
|
||||
module ValueSchemaFactory
|
||||
extend ::API::V3::Utilities::PathHelper
|
||||
SUPPORTED = %w(start_date due_date date).freeze
|
||||
SUPPORTED = %w(start_date due_date date note).freeze
|
||||
|
||||
module_function
|
||||
|
||||
@@ -64,10 +64,13 @@ module API::V3::Values::Schemas
|
||||
I18n.t("attributes.#{property}")
|
||||
end
|
||||
|
||||
def type_for(_property)
|
||||
# This is but a stub. Currently, only 'start_date' and 'due_date'
|
||||
# need to be supported so this simple approach works.
|
||||
"Date"
|
||||
def type_for(property)
|
||||
case property
|
||||
when "start_date", "due_date", "date"
|
||||
"Date"
|
||||
when "note"
|
||||
"String"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
require "spec_helper"
|
||||
require "features/page_objects/notification"
|
||||
|
||||
RSpec.describe "Notification center reminder, mention and date alert",
|
||||
:js,
|
||||
:with_cuprite,
|
||||
with_ee: %i[date_alerts],
|
||||
with_settings: { journal_aggregation_time_minutes: 0 } do
|
||||
shared_let(:project) { create(:project) }
|
||||
shared_let(:actor) { create(:user, firstname: "Actor", lastname: "User") }
|
||||
shared_let(:user) do
|
||||
create(:user,
|
||||
member_with_permissions: { project => %w[view_work_packages] })
|
||||
end
|
||||
shared_let(:work_package) { create(:work_package, project:, due_date: 1.day.ago) }
|
||||
|
||||
shared_let(:notification_mention) do
|
||||
create(:notification,
|
||||
reason: :mentioned,
|
||||
recipient: user,
|
||||
resource: work_package,
|
||||
actor:)
|
||||
end
|
||||
|
||||
shared_let(:notification_date_alert) do
|
||||
create(:notification,
|
||||
reason: :date_alert_due_date,
|
||||
recipient: user,
|
||||
resource: work_package)
|
||||
end
|
||||
|
||||
shared_let(:notification_reminder) do
|
||||
reminder = create(:reminder, remindable: work_package, creator: user, note: "This is an important reminder")
|
||||
notification = create(:notification,
|
||||
reason: :reminder,
|
||||
recipient: user,
|
||||
resource: work_package)
|
||||
create(:reminder_notification, reminder:, notification:)
|
||||
notification
|
||||
end
|
||||
|
||||
let(:center) { Pages::Notifications::Center.new }
|
||||
|
||||
before do
|
||||
login_as user
|
||||
visit notifications_center_path
|
||||
wait_for_reload
|
||||
end
|
||||
|
||||
it "shows the reminder alert in own entry" do
|
||||
center.within_item(notification_reminder) do
|
||||
expect(page).to have_text("##{work_package.id}\n- #{project.name} -\nReminder")
|
||||
expect(page).to have_no_text("Actor user")
|
||||
expect(page).to have_text("a few seconds ago.\nNote: “This is an important reminder”")
|
||||
end
|
||||
end
|
||||
|
||||
it "shows other notification reasons aggregated" do
|
||||
center.within_item(notification_date_alert) do
|
||||
expect(page).to have_text("##{work_package.id}\n- #{project.name} -\nDate alert, Mentioned")
|
||||
expect(page).to have_no_text("Actor user")
|
||||
end
|
||||
end
|
||||
end
|
||||
+38
-15
@@ -24,6 +24,7 @@ RSpec.describe "Notification center sidemenu",
|
||||
shared_let(:work_package4) { create(:work_package, project: project3, author: other_user) }
|
||||
shared_let(:work_package5) { create(:work_package, :is_milestone, project: project3, author: other_user) }
|
||||
shared_let(:work_package6) { create(:work_package, :is_milestone, project: project3, author: other_user) }
|
||||
shared_let(:work_package7) { create(:work_package, project: project3, author: other_user) }
|
||||
|
||||
let(:notification_watched) do
|
||||
create(:notification,
|
||||
@@ -67,9 +68,19 @@ RSpec.describe "Notification center sidemenu",
|
||||
reason: :shared)
|
||||
end
|
||||
|
||||
let(:notification_reminder) do
|
||||
reminder = create(:reminder, remindable: work_package7, creator: other_user, note: "This is an important reminder")
|
||||
notification = create(:notification,
|
||||
recipient:,
|
||||
resource: work_package7,
|
||||
reason: :reminder)
|
||||
create(:reminder_notification, reminder:, notification:)
|
||||
notification
|
||||
end
|
||||
|
||||
let(:notifications) do
|
||||
[notification_watched, notification_assigned, notification_responsible, notification_mentioned, notification_date,
|
||||
notification_shared]
|
||||
notification_shared, notification_reminder]
|
||||
end
|
||||
|
||||
let(:center) { Pages::Notifications::Center.new }
|
||||
@@ -99,6 +110,7 @@ RSpec.describe "Notification center sidemenu",
|
||||
side_menu.expect_item_with_no_count "Watcher"
|
||||
side_menu.expect_item_with_no_count "Date alert"
|
||||
side_menu.expect_item_with_no_count "Shared"
|
||||
side_menu.expect_item_with_no_count "Reminder"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -106,35 +118,37 @@ RSpec.describe "Notification center sidemenu",
|
||||
side_menu.expect_open
|
||||
|
||||
# Expect standard filters
|
||||
side_menu.expect_item_with_count "Inbox", 6
|
||||
side_menu.expect_item_with_count "Inbox", 7
|
||||
side_menu.expect_item_with_count "Assignee", 1
|
||||
side_menu.expect_item_with_count "Mentioned", 1
|
||||
side_menu.expect_item_with_count "Accountable", 1
|
||||
side_menu.expect_item_with_count "Watcher", 1
|
||||
side_menu.expect_item_with_count "Date alert", 1
|
||||
side_menu.expect_item_with_count "Shared", 1
|
||||
side_menu.expect_item_with_count "Reminder", 1
|
||||
|
||||
# Expect project filters
|
||||
side_menu.expect_item_with_count project.name, 1
|
||||
side_menu.expect_item_with_count project2.name, 1
|
||||
side_menu.expect_item_with_count "... #{project3.name}", 4
|
||||
side_menu.expect_item_with_count "... #{project3.name}", 5
|
||||
|
||||
# Reading a notification...
|
||||
center.mark_notification_as_read notification_watched
|
||||
|
||||
# ... will change the filter counts
|
||||
side_menu.expect_item_with_count "Inbox", 5
|
||||
side_menu.expect_item_with_count "Inbox", 6
|
||||
side_menu.expect_item_with_count "Assignee", 1
|
||||
side_menu.expect_item_with_count "Mentioned", 1
|
||||
side_menu.expect_item_with_count "Accountable", 1
|
||||
side_menu.expect_item_with_count "Date alert", 1
|
||||
side_menu.expect_item_with_count "Shared", 1
|
||||
side_menu.expect_item_with_count "Reminder", 1
|
||||
side_menu.expect_item_with_no_count "Watcher"
|
||||
|
||||
# ... and show only those projects with a notification
|
||||
side_menu.expect_no_item project.name
|
||||
side_menu.expect_item_with_count project2.name, 1
|
||||
side_menu.expect_item_with_count "... #{project3.name}", 4
|
||||
side_menu.expect_item_with_count "... #{project3.name}", 5
|
||||
|
||||
# Empty filter sets have a separate message
|
||||
side_menu.click_item "Watcher"
|
||||
@@ -152,6 +166,7 @@ RSpec.describe "Notification center sidemenu",
|
||||
side_menu.expect_item_with_no_count "Watcher"
|
||||
side_menu.expect_item_with_no_count "Date alert"
|
||||
side_menu.expect_item_with_no_count "Shared"
|
||||
side_menu.expect_item_with_no_count "Reminder"
|
||||
|
||||
side_menu.expect_no_item project.name
|
||||
side_menu.expect_no_item project2.name
|
||||
@@ -160,66 +175,74 @@ RSpec.describe "Notification center sidemenu",
|
||||
|
||||
it "updates the content when a filter is clicked" do
|
||||
# All notifications are shown
|
||||
center.expect_work_package_item *notifications
|
||||
center.expect_work_package_item(*notifications)
|
||||
|
||||
# Filter for "Watcher"
|
||||
side_menu.click_item "Watcher"
|
||||
side_menu.finished_loading
|
||||
center.expect_work_package_item notification_watched
|
||||
center.expect_no_item notification_assigned, notification_responsible, notification_mentioned, notification_date,
|
||||
notification_shared
|
||||
notification_shared, notification_reminder
|
||||
|
||||
# Filter for "Assignee"
|
||||
side_menu.click_item "Assignee"
|
||||
side_menu.finished_loading
|
||||
center.expect_work_package_item notification_assigned
|
||||
center.expect_no_item notification_watched, notification_responsible, notification_mentioned, notification_date,
|
||||
notification_shared
|
||||
notification_shared, notification_reminder
|
||||
|
||||
# Filter for "Accountable"
|
||||
side_menu.click_item "Accountable"
|
||||
side_menu.finished_loading
|
||||
center.expect_work_package_item notification_responsible
|
||||
center.expect_no_item notification_watched, notification_assigned, notification_mentioned, notification_date,
|
||||
notification_shared
|
||||
notification_shared, notification_reminder
|
||||
|
||||
# Filter for "Mentioned"
|
||||
side_menu.click_item "Mentioned"
|
||||
side_menu.finished_loading
|
||||
center.expect_work_package_item notification_mentioned
|
||||
center.expect_no_item notification_watched, notification_assigned, notification_responsible, notification_date,
|
||||
notification_shared
|
||||
notification_shared, notification_reminder
|
||||
|
||||
# Filter for "Date alert"
|
||||
side_menu.click_item "Date alert"
|
||||
side_menu.finished_loading
|
||||
center.expect_work_package_item notification_date
|
||||
center.expect_no_item notification_watched, notification_assigned, notification_responsible, notification_mentioned,
|
||||
notification_shared
|
||||
notification_shared, notification_reminder
|
||||
|
||||
# Filter for "Shared"
|
||||
side_menu.click_item "Shared"
|
||||
side_menu.finished_loading
|
||||
center.expect_work_package_item notification_shared
|
||||
center.expect_no_item notification_watched, notification_assigned, notification_responsible, notification_mentioned,
|
||||
notification_date
|
||||
notification_date, notification_reminder
|
||||
|
||||
# Filter for "Reminder"
|
||||
side_menu.click_item "Reminder"
|
||||
side_menu.finished_loading
|
||||
center.expect_work_package_item notification_reminder
|
||||
center.expect_no_item notification_watched, notification_assigned, notification_responsible, notification_mentioned,
|
||||
notification_date, notification_shared
|
||||
|
||||
# Filter for project1
|
||||
side_menu.click_item project.name
|
||||
side_menu.finished_loading
|
||||
center.expect_work_package_item notification_watched
|
||||
center.expect_no_item notification_assigned, notification_responsible, notification_mentioned, notification_date,
|
||||
notification_shared
|
||||
notification_shared, notification_reminder
|
||||
|
||||
# Filter for project3
|
||||
side_menu.click_item "... #{project3.name}"
|
||||
side_menu.finished_loading
|
||||
center.expect_work_package_item notification_responsible, notification_mentioned, notification_date, notification_shared
|
||||
center.expect_work_package_item notification_responsible, notification_mentioned, notification_date, notification_shared,
|
||||
notification_reminder
|
||||
center.expect_no_item notification_watched, notification_assigned
|
||||
|
||||
# Reset by clicking on the Inbox
|
||||
side_menu.click_item "Inbox"
|
||||
side_menu.finished_loading
|
||||
center.expect_work_package_item *notifications
|
||||
center.expect_work_package_item(*notifications)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -267,5 +267,60 @@ RSpec.describe API::V3::Notifications::NotificationRepresenter, "rendering" do
|
||||
.at_path("_embedded/details")
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples_for "embeds a Values::Property for reminder note" do
|
||||
it "embeds a Values::Property" do
|
||||
expect(generated)
|
||||
.to be_json_eql("Values::Property".to_json)
|
||||
.at_path("_embedded/details/0/_type")
|
||||
end
|
||||
|
||||
it "has a note value for the `property` property" do
|
||||
expect(generated)
|
||||
.to be_json_eql("note".to_json)
|
||||
.at_path("_embedded/details/0/property")
|
||||
end
|
||||
|
||||
it "has a reminder`s note for the value" do
|
||||
expect(generated)
|
||||
.to be_json_eql(notification.reminder.note.to_json)
|
||||
.at_path("_embedded/details/0/value")
|
||||
end
|
||||
end
|
||||
|
||||
context "for a reminder when embedding" do
|
||||
let(:reminder) { build_stubbed(:reminder) }
|
||||
let(:reason) { :reminder }
|
||||
let(:embed_links) { true }
|
||||
|
||||
before do
|
||||
allow(notification).to receive(:reminder).and_return(reminder)
|
||||
end
|
||||
|
||||
it_behaves_like "embeds a Values::Property for reminder note"
|
||||
end
|
||||
|
||||
context "for a reminder when not embedding" do
|
||||
let(:reminder) { build_stubbed(:reminder) }
|
||||
let(:reason) { :reminder }
|
||||
let(:embed_links) { false }
|
||||
|
||||
before do
|
||||
allow(notification).to receive(:reminder).and_return(reminder)
|
||||
end
|
||||
|
||||
it_behaves_like "embeds a Values::Property for reminder note"
|
||||
end
|
||||
|
||||
context "for a reminder with no notification" do
|
||||
let(:reminder) { nil }
|
||||
let(:reason) { :reminder }
|
||||
|
||||
it "has an empty details array" do
|
||||
expect(generated)
|
||||
.to have_json_size(0)
|
||||
.at_path("_embedded/details")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user