add separate menu for reminder mails

pull/9618/head
ulferts 3 years ago
parent f1b30c547f
commit 1cca351d31
No known key found for this signature in database
GPG Key ID: A205708DE1284017
  1. 42
      app/controllers/my_controller.rb
  2. 4
      config/initializers/menus.rb
  3. 9
      config/locales/js-en.yml
  4. 1
      config/routes.rb
  5. 19
      frontend/src/app/features/user-preferences/reminder-settings/page/reminder-settings-page.component.html
  6. 55
      frontend/src/app/features/user-preferences/reminder-settings/page/reminder-settings-page.component.ts
  7. 20
      frontend/src/app/features/user-preferences/reminder-settings/reminder-time/reminder-settings-daily-time.component.html
  8. 30
      frontend/src/app/features/user-preferences/reminder-settings/reminder-time/reminder-settings-daily-time.component.ts
  9. 5
      frontend/src/app/features/user-preferences/user-preferences.lazy-routes.ts
  10. 4
      frontend/src/app/features/user-preferences/user-preferences.module.ts
  11. 6
      frontend/src/app/features/user-preferences/user-preferences.routes.ts
  12. 375
      frontend/src/assets/fonts/openproject_icon/openproject-icon-font.svg
  13. BIN
      frontend/src/assets/fonts/openproject_icon/openproject-icon-font.ttf
  14. BIN
      frontend/src/assets/fonts/openproject_icon/openproject-icon-font.woff
  15. BIN
      frontend/src/assets/fonts/openproject_icon/openproject-icon-font.woff2
  16. 750
      frontend/src/global_styles/fonts/_openproject_icon_definitions.scss
  17. 1
      frontend/src/global_styles/fonts/_openproject_icon_font.lsg
  18. 1
      spec/features/notifications/digest_mail_spec.rb
  19. 65
      spec/features/notifications/reminder_mail_spec.rb
  20. 12
      spec/routing/my_spec.rb
  21. 39
      spec/support/pages/my/reminders.rb
  22. 64
      spec/support/pages/reminders/settings.rb
  23. 4
      vendor/openproject-icon-font/src/email-alert.svg

@ -46,6 +46,7 @@ class MyController < ApplicationController
menu_item :password, only: [:password]
menu_item :access_token, only: [:access_token]
menu_item :notifications, only: [:notifications]
menu_item :reminders, only: [:reminders]
def account; end
@ -81,23 +82,30 @@ class MyController < ApplicationController
# Administer access tokens
def access_token; end
# Configure user's mail notifications
# Configure user's in app notifications
def notifications
render html: '',
layout: 'angular',
locals: { menu_name: :my_menu }
end
# Configure user's mail reminders
def reminders
render html: '',
layout: 'angular',
locals: { menu_name: :my_menu }
end
# Create a new feeds key
def generate_rss_key
if request.post?
token = Token::RSS.create!(user: current_user)
flash[:info] = [
t('my.access_token.notice_reset_token', type: 'RSS').html_safe,
content_tag(:strong, token.plain_value),
t('my.access_token.token_value_warning')
]
end
token = Token::RSS.create!(user: current_user)
flash[:info] = [
# rubocop:disable Rails/OutputSafety
t('my.access_token.notice_reset_token', type: 'RSS').html_safe,
# rubocop:enable Rails/OutputSafety
content_tag(:strong, token.plain_value),
t('my.access_token.token_value_warning')
]
rescue StandardError => e
Rails.logger.error "Failed to reset user ##{current_user.id} RSS key: #{e}"
flash[:error] = t('my.access_token.failed_to_reset_token', error: e.message)
@ -107,14 +115,14 @@ class MyController < ApplicationController
# Create a new API key
def generate_api_key
if request.post?
token = Token::API.create!(user: current_user)
flash[:info] = [
t('my.access_token.notice_reset_token', type: 'API').html_safe,
content_tag(:strong, token.plain_value),
t('my.access_token.token_value_warning')
]
end
token = Token::API.create!(user: current_user)
flash[:info] = [
# rubocop:disable Rails/OutputSafety
t('my.access_token.notice_reset_token', type: 'API').html_safe,
# rubocop:enable Rails/OutputSafety
content_tag(:strong, token.plain_value),
t('my.access_token.token_value_warning')
]
rescue StandardError => e
Rails.logger.error "Failed to reset user ##{current_user.id} API key: #{e}"
flash[:error] = t('my.access_token.failed_to_reset_token', error: e.message)

@ -137,6 +137,10 @@ Redmine::MenuManager.map :my_menu do |menu|
{ controller: '/my', action: 'notifications' },
caption: I18n.t('js.notifications.settings.title'),
icon: 'icon2 icon-bell'
menu.push :reminders,
{ controller: '/my', action: 'reminders' },
caption: I18n.t('js.reminders.settings.title'),
icon: 'icon2 icon-email-alert'
menu.push :delete_account, :delete_my_account_info_path,
caption: I18n.t('account.delete'),

@ -627,6 +627,15 @@ en:
autocompleter:
label: 'Project autocompletion'
reminders:
settings:
daily:
title: 'Send me daily email reminders for unread notifications'
explanation: 'You will receive these reminders only for unread notifications and only at hours you specify.'
label: 'Time %{counter}'
title: 'Email reminders'
text_are_you_sure: "Are you sure?"
text_data_lost: "All entered data will be lost."

@ -552,6 +552,7 @@ OpenProject::Application.routes.draw do
get '/my/account', action: 'account'
get '/my/settings', action: 'settings'
get '/my/notifications', action: 'notifications'
get '/my/reminders', action: 'reminders'
patch '/my/account', action: 'update_account'
patch '/my/settings', action: 'update_settings'

@ -0,0 +1,19 @@
<div class="title-container">
<h2 [textContent]="text.title"></h2>
</div>
<form>
<section class="form--section">
<h3 [textContent]="text.daily.title"></h3>
<span [textContent]="text.daily.explanation"></span>
<op-reminder-settings-daily-time>
</op-reminder-settings-daily-time>
</section>
<button
class="button -highlight"
[textContent]="text.save"
(click)="saveChanges()"
>
</button>
</form>

@ -0,0 +1,55 @@
import {
ChangeDetectionStrategy, Component, Input, OnInit,
} from '@angular/core';
import { I18nService } from 'core-app/core/i18n/i18n.service';
import { CurrentUserService } from 'core-app/core/current-user/current-user.service';
import { take } from 'rxjs/internal/operators/take';
import { UIRouterGlobals } from '@uirouter/core';
import { UserPreferencesService } from 'core-app/features/user-preferences/state/user-preferences.service';
import { UserPreferencesQuery } from 'core-app/features/user-preferences/state/user-preferences.query';
export const myReminderPageComponentSelector = 'op-reminders-page';
@Component({
selector: myReminderPageComponentSelector,
templateUrl: './reminder-settings-page.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ReminderSettingsPageComponent implements OnInit {
@Input() userId:string;
text = {
title: this.I18n.t('js.reminders.settings.title'),
save: this.I18n.t('js.button_save'),
daily: {
title: this.I18n.t('js.reminders.settings.daily.title'),
explanation: this.I18n.t('js.reminders.settings.daily.explanation'),
},
};
constructor(
private I18n:I18nService,
private stateService:UserPreferencesService,
private query:UserPreferencesQuery,
private currentUserService:CurrentUserService,
private uiRouterGlobals:UIRouterGlobals,
) {
}
ngOnInit():void {
this.userId = this.userId || this.uiRouterGlobals.params.userId;
this
.currentUserService
.user$
.pipe(take(1))
.subscribe((user) => {
this.userId = this.userId || user.id!;
this.stateService.get(this.userId);
});
}
public saveChanges():void {
const prefs = this.query.getValue();
this.stateService.update(this.userId, prefs);
}
}

@ -0,0 +1,20 @@
<label
*ngFor="let time of dailyReminderTimes; index as i"
class="form--label-with-check-box">
<div class="form--check-box-container">
<input type="checkbox" class="form--check-box" checked>
</div>
{{text.label(i + 1)}}
<div class="form--field -no-label">
<div class="form--field-container">
<div class="form--text-field-container">
<input
type="time"
[value]="time"
step="1800"
required
attr.data-qa-selector="op-settings-daily-time--time-{{i + 1}}">
</div>
</div>
</div>
</label>

@ -0,0 +1,30 @@
import { Component, Input, OnInit } from "@angular/core";
import { ChangeDetectionStrategy } from "@angular/core";
import { I18nService } from 'core-app/core/i18n/i18n.service';
import { UserPreferencesStore } from 'core-app/features/user-preferences/state/user-preferences.store';
@Component({
selector: 'op-reminder-settings-daily-time',
templateUrl: './reminder-settings-daily-time.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ReminderSettingsDailyTimeComponent implements OnInit {
public dailyReminderTimes = ["08:00", "12:00", "16:00"]
text = {
label: (counter:number):string => this.I18n.t('js.reminders.settings.daily.label', { counter: counter }),
};
constructor(
private I18n:I18nService,
private store:UserPreferencesStore,
) {
}
ngOnInit():void {
}
public saveChanges():void {
}
}

@ -39,4 +39,9 @@ export const MY_ACCOUNT_LAZY_ROUTES:Ng2StateDeclaration[] = [
url: '/users/:userId/edit/notifications',
loadChildren: () => import('./user-preferences.module').then((m) => m.OpenProjectMyAccountModule),
},
{
name: 'my_reminders.**',
url: '/my/reminders',
loadChildren: () => import('./user-preferences.module').then((m) => m.OpenProjectMyAccountModule),
},
];

@ -13,6 +13,8 @@ import { NotificationSettingInlineCreateComponent } from 'core-app/features/user
import { MY_ACCOUNT_ROUTES } from 'core-app/features/user-preferences/user-preferences.routes';
import { NotificationsSettingsToolbarComponent } from './notifications-settings/toolbar/notifications-settings-toolbar.component';
import { NotificationSettingsTableComponent } from './notifications-settings/table/notification-settings-table.component';
import { ReminderSettingsPageComponent } from './reminder-settings/page/reminder-settings-page.component';
import { ReminderSettingsDailyTimeComponent } from 'core-app/features/user-preferences/reminder-settings/reminder-time/reminder-settings-daily-time.component';
@NgModule({
providers: [
@ -26,6 +28,8 @@ import { NotificationSettingsTableComponent } from './notifications-settings/tab
NotificationSettingInlineCreateComponent,
NotificationsSettingsToolbarComponent,
NotificationSettingsTableComponent,
ReminderSettingsPageComponent,
ReminderSettingsDailyTimeComponent
],
imports: [
CommonModule,

@ -28,6 +28,7 @@
import { Ng2StateDeclaration } from '@uirouter/angular';
import { NotificationsSettingsPageComponent } from 'core-app/features/user-preferences/notifications-settings/page/notifications-settings-page.component';
import { ReminderSettingsPageComponent } from './reminder-settings/page/reminder-settings-page.component';
export const MY_ACCOUNT_ROUTES:Ng2StateDeclaration[] = [
{
@ -40,4 +41,9 @@ export const MY_ACCOUNT_ROUTES:Ng2StateDeclaration[] = [
url: '/users/:userId/edit/notifications',
component: NotificationsSettingsPageComponent,
},
{
name: 'my_reminders',
url: '/my/reminders',
component: ReminderSettingsPageComponent,
},
];

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 153 KiB

After

Width:  |  Height:  |  Size: 155 KiB

@ -78,6 +78,7 @@
<li><span class="icon icon-drag-handle"></span>drag-handle</li>
<li><span class="icon icon-duplicate"></span>duplicate</li>
<li><span class="icon icon-edit"></span>edit</li>
<li><span class="icon icon-email-alert"></span>email-alert</li>
<li><span class="icon icon-enterprise"></span>enterprise</li>
<li><span class="icon icon-enumerations"></span>enumerations</li>
<li><span class="icon icon-error"></span>error</li>

@ -1,6 +1,7 @@
require 'spec_helper'
require 'support/pages/my/notifications'
# TODO: This feature spec is to be replaced by the reminder_mail_spec.rb in the same directory.
describe "Digest email", type: :feature, js: true do
let!(:project) { FactoryBot.create :project, members: { current_user => role } }
let!(:mute_project) { FactoryBot.create :project, members: { current_user => role } }

@ -0,0 +1,65 @@
require 'spec_helper'
require 'support/pages/my/notifications'
describe "Reminder email", type: :feature, js: true do
let!(:project) { FactoryBot.create :project, members: { current_user => role } }
let!(:mute_project) { FactoryBot.create :project, members: { current_user => role } }
let(:reminders_settings_page) { Pages::My::Reminders.new(current_user) }
let(:role) { FactoryBot.create(:role, permissions: %i[view_work_packages]) }
let(:other_user) { FactoryBot.create(:user) }
let(:work_package) { FactoryBot.create(:work_package, project: project) }
let(:watched_work_package) { FactoryBot.create(:work_package, project: project, watcher_users: [current_user]) }
let(:involved_work_package) { FactoryBot.create(:work_package, project: project, assigned_to: current_user) }
current_user do
FactoryBot.create :user,
notification_settings: [
FactoryBot.build(:mail_notification_setting,
involved: false,
watched: false,
mentioned: false,
work_package_commented: false,
work_package_created: false,
work_package_processed: false,
work_package_prioritized: false,
work_package_scheduled: false,
all: false),
FactoryBot.build(:in_app_notification_setting,
involved: false,
watched: false,
mentioned: false,
work_package_commented: false,
work_package_created: false,
work_package_processed: false,
work_package_prioritized: false,
work_package_scheduled: false,
all: false),
FactoryBot.build(:mail_digest_notification_setting,
involved: true,
watched: true,
mentioned: true,
work_package_commented: true,
work_package_created: true,
work_package_processed: true,
work_package_prioritized: true,
work_package_scheduled: true,
all: false)
]
end
before do
watched_work_package
work_package
involved_work_package
ActiveJob::Base.queue_adapter.enqueued_jobs.clear
end
it 'sends a reminder mail based on the configuration', with_settings: { journal_aggregation_time_minutes: 0 } do
# Configure the digest
reminders_settings_page.visit!
# By default a reminder timed for 8:00 should be configured
reminders_settings_page.expect_active_daily_times("08:00")
end
end

@ -45,6 +45,14 @@ describe 'my routes', type: :routing do
expect(patch('/my/settings')).to route_to('my#update_settings')
end
it '/my/notifications GET routes to my#notifications' do
expect(get('/my/notifications')).to route_to('my#notifications')
end
it '/my/reminders GET routes to my#notifications' do
expect(get('/my/reminders')).to route_to('my#reminders')
end
it '/my/generate_rss_key POST routes to my#generate_rss_key' do
expect(post('/my/generate_rss_key')).to route_to('my#generate_rss_key')
end
@ -53,8 +61,8 @@ describe 'my routes', type: :routing do
expect(post('/my/generate_api_key')).to route_to('my#generate_api_key')
end
it {
it '/my/deletion_info GET routes to users#deletion_info' do
expect(get('/my/deletion_info')).to route_to(controller: 'users',
action: 'deletion_info')
}
end
end

@ -0,0 +1,39 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
#++
require 'support/pages/reminders/settings'
module Pages
module My
class Reminders < ::Pages::Reminders::Settings
def path
my_reminders_path
end
end
end
end

@ -0,0 +1,64 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
#++
require 'support/pages/page'
module Pages
module Reminders
class Settings < ::Pages::Page
attr_reader :user
def initialize(user)
super()
@user = user
end
def path
edit_user_path(user, tab: :reminders)
end
def expect_active_daily_times(*times)
times.each_with_index do |time, index|
expect(page)
.to have_checked_field "Time #{index + 1}"
expect(page)
.to have_css("input[data-qa-selector='op-settings-daily-time--time-#{index + 1}']")
expect(page.find("input[data-qa-selector='op-settings-daily-time--time-#{index + 1}']").value)
.to eql(time)
end
end
def save
click_button 'Save'
end
end
end
end

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path style=" stroke:none;fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 220.71875 145.367188 L 35.273438 145.367188 C 25.675781 145.367188 16.472656 149.023438 9.6875 155.535156 C 2.902344 162.046875 -0.910156 170.878906 -0.910156 180.089844 L -0.910156 457.890625 C -0.910156 467.101562 2.902344 475.933594 9.6875 482.445312 C 16.472656 488.957031 25.675781 492.613281 35.273438 492.613281 L 469.464844 492.613281 C 479.058594 492.613281 488.265625 488.957031 495.054688 482.445312 C 501.835938 475.933594 505.652344 467.101562 505.652344 457.890625 L 505.652344 249.679688 C 495.246094 261.734375 483.046875 272.25 469.464844 280.816406 L 469.464844 457.890625 L 35.273438 457.890625 L 35.273438 195.890625 L 242.058594 333.226562 C 245.082031 335.246094 248.683594 336.324219 252.371094 336.324219 C 256.058594 336.324219 259.65625 335.246094 262.683594 333.226562 L 321.476562 294.175781 C 309.035156 289.199219 297.371094 282.730469 286.707031 275.007812 L 252.371094 297.804688 L 75.078125 180.089844 L 224.277344 180.089844 C 221.941406 169.320312 220.710938 158.144531 220.710938 146.691406 C 220.710938 146.25 220.710938 145.804688 220.71875 145.367188 Z M 220.71875 145.367188 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 381.703125 19.089844 C 310.277344 19.089844 252.371094 76.835938 252.371094 148.046875 C 252.371094 219.261719 310.277344 277 381.703125 277 C 453.125 277 511.03125 219.261719 511.03125 148.046875 C 511.03125 76.835938 453.125 19.089844 381.703125 19.089844 Z M 372.460938 85.875 C 372.460938 84.601562 373.5 83.566406 374.777344 83.566406 L 388.625 83.566406 C 389.902344 83.566406 390.9375 84.601562 390.9375 85.875 C 390.9375 116.445312 390.9375 133.589844 390.9375 164.167969 C 390.9375 165.433594 389.902344 166.46875 388.625 166.46875 L 374.777344 166.46875 C 373.5 166.46875 372.460938 165.433594 372.460938 164.167969 Z M 381.703125 212.527344 C 378.074219 212.453125 374.621094 210.960938 372.082031 208.382812 C 369.546875 205.800781 368.125 202.324219 368.125 198.707031 C 368.125 195.09375 369.546875 191.621094 372.082031 189.035156 C 374.621094 186.453125 378.074219 184.96875 381.703125 184.890625 C 385.328125 184.96875 388.78125 186.453125 391.320312 189.035156 C 393.855469 191.621094 395.273438 195.09375 395.273438 198.707031 C 395.273438 202.324219 393.855469 205.800781 391.320312 208.382812 C 388.78125 210.960938 385.328125 212.453125 381.703125 212.527344 Z M 381.703125 212.527344 "/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

Loading…
Cancel
Save