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:
Kabiru Mwenja
2024-12-12 12:59:03 +03:00
committed by GitHub
25 changed files with 479 additions and 100 deletions
+3 -2
View File
@@ -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
+2
View File
@@ -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"
+2
View File
@@ -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(),
);
@@ -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>
@@ -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
@@ -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
@@ -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');
@@ -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>
@@ -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();
@@ -0,0 +1,5 @@
<div
class="op-ian-relative-time"
[title]="fixedTime"
[textContent]="relativeTime$ | async">
</div>
@@ -0,0 +1,6 @@
@import "helpers"
.op-ian-relative-time
@include text-shortener
max-width: 100%
line-height: 1rem
@@ -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(),
);
}
}
@@ -0,0 +1,8 @@
<op-in-app-notification-relative-time
[notification]="reminderAlert"
[hasActorByLine]="false"
/>
<span
class="op-ian-reminder-alert--note"
[textContent]="reminderNote"
></span>
@@ -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
@@ -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
@@ -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