From 3f0f3cfca02b14e36ab75b0e1f214fa9e7485ec6 Mon Sep 17 00:00:00 2001 From: Henriette Darge Date: Thu, 26 Mar 2026 15:15:29 +0100 Subject: [PATCH] Use reminders form component for the admin view as well and remove now outdated angular components --- .../show_page_header_component.rb | 4 +- .../my/reminders/show_page_component.html.erb | 10 +- .../my/reminders/show_page_component.rb | 6 +- app/controllers/users_controller.rb | 12 + .../my/reminders/immediate_reminders_form.rb | 2 +- app/views/my/notifications.html.erb | 9 +- app/views/users/_reminders.html.erb | 7 +- config/locales/en.yml | 3 + config/locales/js-en.yml | 34 --- config/routes.rb | 1 + frontend/src/app/app.module.ts | 6 +- .../email-alerts-settings.component.html | 19 -- .../email-alerts-settings.component.ts | 62 ----- ...immediate-reminder-settings.component.html | 29 -- .../immediate-reminder-settings.component.ts | 39 --- .../reminder-settings-page.component.html | 19 -- .../reminder-settings-page.component.sass | 6 - .../page/reminder-settings-page.component.ts | 193 -------------- .../pause-reminders.component.html | 23 -- .../pause-reminders.component.sass | 6 - .../pause-reminders.component.ts | 70 ----- ...eminder-settings-daily-time.component.html | 73 ----- ...eminder-settings-daily-time.component.sass | 37 --- .../reminder-settings-daily-time.component.ts | 251 ------------------ .../workdays/workdays-settings.component.html | 21 -- .../workdays/workdays-settings.component.sass | 0 .../workdays/workdays-settings.component.ts | 70 ----- .../user-preferences.module.ts | 18 -- lib/open_project/ui/extensible_tabs.rb | 6 +- 29 files changed, 48 insertions(+), 988 deletions(-) delete mode 100644 frontend/src/app/features/user-preferences/reminder-settings/email-alerts/email-alerts-settings.component.html delete mode 100644 frontend/src/app/features/user-preferences/reminder-settings/email-alerts/email-alerts-settings.component.ts delete mode 100644 frontend/src/app/features/user-preferences/reminder-settings/immediate-reminders/immediate-reminder-settings.component.html delete mode 100644 frontend/src/app/features/user-preferences/reminder-settings/immediate-reminders/immediate-reminder-settings.component.ts delete mode 100644 frontend/src/app/features/user-preferences/reminder-settings/page/reminder-settings-page.component.html delete mode 100644 frontend/src/app/features/user-preferences/reminder-settings/page/reminder-settings-page.component.sass delete mode 100644 frontend/src/app/features/user-preferences/reminder-settings/page/reminder-settings-page.component.ts delete mode 100644 frontend/src/app/features/user-preferences/reminder-settings/pause-reminders/pause-reminders.component.html delete mode 100644 frontend/src/app/features/user-preferences/reminder-settings/pause-reminders/pause-reminders.component.sass delete mode 100644 frontend/src/app/features/user-preferences/reminder-settings/pause-reminders/pause-reminders.component.ts delete mode 100644 frontend/src/app/features/user-preferences/reminder-settings/reminder-time/reminder-settings-daily-time.component.html delete mode 100644 frontend/src/app/features/user-preferences/reminder-settings/reminder-time/reminder-settings-daily-time.component.sass delete mode 100644 frontend/src/app/features/user-preferences/reminder-settings/reminder-time/reminder-settings-daily-time.component.ts delete mode 100644 frontend/src/app/features/user-preferences/reminder-settings/workdays/workdays-settings.component.html delete mode 100644 frontend/src/app/features/user-preferences/reminder-settings/workdays/workdays-settings.component.sass delete mode 100644 frontend/src/app/features/user-preferences/reminder-settings/workdays/workdays-settings.component.ts diff --git a/app/components/my/notifications/show_page_header_component.rb b/app/components/my/notifications/show_page_header_component.rb index 066c5be4a41..4256042d526 100644 --- a/app/components/my/notifications/show_page_header_component.rb +++ b/app/components/my/notifications/show_page_header_component.rb @@ -48,12 +48,12 @@ module My { name: "notifications", path: helpers.my_notifications_path(tab: "notifications"), - label: I18n.t("js.notifications.settings.title") + label: t("my_account.notifications_and_email.tabs.notifications") }, { name: "reminders", path: helpers.my_notifications_path(tab: "reminders"), - label: I18n.t("js.reminders.settings.title") + label: t("my_account.notifications_and_email.tabs.email_reminders") } ] end diff --git a/app/components/my/reminders/show_page_component.html.erb b/app/components/my/reminders/show_page_component.html.erb index f9f8d49ea4d..84b1c817b3a 100644 --- a/app/components/my/reminders/show_page_component.html.erb +++ b/app/components/my/reminders/show_page_component.html.erb @@ -31,7 +31,7 @@ See COPYRIGHT and LICENSE files for more details. settings_primer_form_with( model: @user.pref, scope: "pref[immediate_reminders]", - url: { action: "update_settings" }, + url: update_url, method: :patch, data: { turbo: false } ) do |form| @@ -43,7 +43,7 @@ See COPYRIGHT and LICENSE files for more details. settings_primer_form_with( model: daily_reminders_form_model, scope: "pref[daily_reminders]", - url: { action: "update_settings" }, + url: update_url, method: :patch, data: { turbo: false, @@ -59,7 +59,7 @@ See COPYRIGHT and LICENSE files for more details. settings_primer_form_with( model: @user.pref, scope: :pref, - url: { action: "update_settings" }, + url: update_url, method: :patch, data: { turbo: false } ) do |form| @@ -71,7 +71,7 @@ See COPYRIGHT and LICENSE files for more details. settings_primer_form_with( model: pause_reminders_form_model, scope: "pref[pause_reminders]", - url: { action: "update_settings" }, + url: update_url, method: :patch, data: { turbo: false, @@ -87,7 +87,7 @@ See COPYRIGHT and LICENSE files for more details. settings_primer_form_with( model: global_notification_setting, scope: :notification_setting, - url: { action: "update_email_alerts" }, + url: update_email_alerts_url, method: :patch, data: { turbo: false } ) do |form| diff --git a/app/components/my/reminders/show_page_component.rb b/app/components/my/reminders/show_page_component.rb index a6f11001be2..4b21fa7b758 100644 --- a/app/components/my/reminders/show_page_component.rb +++ b/app/components/my/reminders/show_page_component.rb @@ -33,13 +33,15 @@ module My class ShowPageComponent < ApplicationComponent include OpPrimer::FormHelpers - attr_reader :global_notification_setting + attr_reader :global_notification_setting, :update_url, :update_email_alerts_url - def initialize(user:, global_notification_setting:) + def initialize(user:, global_notification_setting:, update_url:, update_email_alerts_url:) super @user = user @global_notification_setting = global_notification_setting + @update_url = update_url + @update_email_alerts_url = update_email_alerts_url end def daily_reminders_form_model diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 66c45903af7..77bd29dc32f 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -39,6 +39,7 @@ class UsersController < ApplicationController before_action :find_user, only: %i[show edit update + update_email_alerts change_status_info change_status destroy @@ -109,6 +110,17 @@ class UsersController < ApplicationController end end + def update_email_alerts + global_setting = @user.notification_settings.find_or_initialize_by(project: nil) + if global_setting.update(permitted_params.notification_setting_email_alerts) + flash[:notice] = I18n.t(:notice_successful_update) + else + flash[:error] = I18n.t(:notice_failed_to_save_messages, count: global_setting.errors.count, + object: global_setting.class.model_name.human) + end + redirect_back_or_to edit_user_path(@user, tab: "reminders") + end + def update # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity update_params = build_user_update_params call = ::Users::UpdateService.new(model: @user, user: current_user).call(update_params) diff --git a/app/forms/my/reminders/immediate_reminders_form.rb b/app/forms/my/reminders/immediate_reminders_form.rb index 67dcf57feda..d9405a65dc4 100644 --- a/app/forms/my/reminders/immediate_reminders_form.rb +++ b/app/forms/my/reminders/immediate_reminders_form.rb @@ -30,7 +30,7 @@ class My::Reminders::ImmediateRemindersForm < ApplicationForm form do |f| - f.fieldset_group(title: helpers.t("my_account.email_reminders.immediate_reminders.title")) do |fg| + f.fieldset_group(title: helpers.t("my_account.email_reminders.immediate_reminders.title"), mt: 2) do |fg| fg.check_box( name: :mentioned, label: helpers.t("my_account.email_reminders.immediate_reminders.mentioned") diff --git a/app/views/my/notifications.html.erb b/app/views/my/notifications.html.erb index b2116ab60b9..909dc46ab6b 100644 --- a/app/views/my/notifications.html.erb +++ b/app/views/my/notifications.html.erb @@ -32,7 +32,14 @@ See COPYRIGHT and LICENSE files for more details. <%= render(My::Notifications::ShowPageHeaderComponent.new) %> <% if params[:tab] == "reminders" %> - <%= render(My::Reminders::ShowPageComponent.new(user: @user, global_notification_setting: @global_notification_setting)) %> + <%= render( + My::Reminders::ShowPageComponent.new( + user: @user, + global_notification_setting: @global_notification_setting, + update_url: { action: "update_settings" }, + update_email_alerts_url: { action: "update_email_alerts" } + ) + ) %> <% else %> <%= angular_component_tag "opce-notification-settings" %> <% end %> diff --git a/app/views/users/_reminders.html.erb b/app/views/users/_reminders.html.erb index bb5d1fb15d0..2b54f171a4c 100644 --- a/app/views/users/_reminders.html.erb +++ b/app/views/users/_reminders.html.erb @@ -26,4 +26,9 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. See COPYRIGHT and LICENSE files for more details. ++#%> -<%= angular_component_tag "opce-reminder-settings", inputs: { userId: @user.id } %> +<%= render(My::Reminders::ShowPageComponent.new( + user: @user, + global_notification_setting: @user.notification_settings.find_or_initialize_by(project: nil), + update_url: user_path(@user), + update_email_alerts_url: update_email_alerts_user_path(@user) +)) %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 5b11cae69f7..fc321c7f337 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -3528,6 +3528,9 @@ en: my_account: notifications_and_email: title: "Notification and email" + tabs: + notifications: "Notification settings" + email_reminders: "Email reminders" access_tokens: description: "Provider tokens are issued by OpenProject, allowing other applications to access it. Client tokens are issued by other applications, allowing OpenProject to access them." no_results: diff --git a/config/locales/js-en.yml b/config/locales/js-en.yml index 6e8a9765fc0..b72b48617b8 100644 --- a/config/locales/js-en.yml +++ b/config/locales/js-en.yml @@ -701,40 +701,6 @@ en: Please choose a project to create the work package in to see all attributes. You can only select projects which have the type above activated. - reminders: - settings: - daily: - add_time: "Add time" - enable: "Enable daily email reminders" - explanation: "You will receive these reminders only for unread notifications and only at hours you specify. %{no_time_zone}" - no_time_zone: "Until you configure a time zone for your account, the times will be interpreted to be in UTC." - time_label: "Time %{counter}:" - title: "Send me daily email reminders for unread notifications" - workdays: - title: "Receive email reminders on these days" - immediate: - title: "Send me an email reminder" - mentioned: "Immediately when someone @mentions me" - personal_reminder: "Immediately when I receive a personal reminder" - alerts: - title: "Email alerts for other items (that are not work packages)" - explanation: > - Notifications today are limited to work packages. - You can choose to continue receiving email alerts for these events until they are included in notifications: - news_added: "News added" - news_commented: "Comment on a news item" - document_added: "Documents added" - forum_messages: "New forum messages" - wiki_page_added: "Wiki page added" - wiki_page_updated: "Wiki page updated" - membership_added: "Membership added" - membership_updated: "Membership updated" - title: "Email reminders" - pause: - label: "Temporarily pause daily email reminders" - first_day: "First day" - last_day: "Last day" - text_are_you_sure: "Are you sure?" text_are_you_sure_to_cancel: "You have unsaved changes on this page. Are you sure you want to discard them?" breadcrumb: "Breadcrumb" diff --git a/config/routes.rb b/config/routes.rb index 98ee642589a..5da977e71fb 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -950,6 +950,7 @@ Rails.application.routes.draw do get "/change_status/:change_action" => "users#change_status_info", as: "change_status_info" post :change_status post :resend_invitation + patch :update_email_alerts get :deletion_info end end diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index d3edb6674eb..c89ea529e5e 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -138,9 +138,7 @@ import { OpenProjectJobStatusModule } from 'core-app/features/job-status/openpro import { NotificationsSettingsPageComponent, } from 'core-app/features/user-preferences/notifications-settings/page/notifications-settings-page.component'; -import { - ReminderSettingsPageComponent, -} from 'core-app/features/user-preferences/reminder-settings/page/reminder-settings-page.component'; + import { OpenProjectMyAccountModule } from 'core-app/features/user-preferences/user-preferences.module'; import { OpAttachmentsComponent } from 'core-app/shared/components/attachments/attachments.component'; import { @@ -391,7 +389,7 @@ export class OpenProjectModule implements DoBootstrap { // TODO: These elements are now registered custom elements, but are actually single-use components. They should be removed when we move these pages to Rails. registerCustomElement('opce-notification-settings', NotificationsSettingsPageComponent, { injector }); - registerCustomElement('opce-reminder-settings', ReminderSettingsPageComponent, { injector }); + registerCustomElement('opce-notification-center', InAppNotificationCenterComponent, { injector }); registerCustomElement('opce-wp-split-view', WorkPackageSplitViewEntryComponent, { injector }); registerCustomElement('opce-wp-full-view', WorkPackageFullViewEntryComponent, { injector }); diff --git a/frontend/src/app/features/user-preferences/reminder-settings/email-alerts/email-alerts-settings.component.html b/frontend/src/app/features/user-preferences/reminder-settings/email-alerts/email-alerts-settings.component.html deleted file mode 100644 index 2312f5211d6..00000000000 --- a/frontend/src/app/features/user-preferences/reminder-settings/email-alerts/email-alerts-settings.component.html +++ /dev/null @@ -1,19 +0,0 @@ - -
-

-

-
- - @for (setting of alerts; track setting) { - - - - } -
diff --git a/frontend/src/app/features/user-preferences/reminder-settings/email-alerts/email-alerts-settings.component.ts b/frontend/src/app/features/user-preferences/reminder-settings/email-alerts/email-alerts-settings.component.ts deleted file mode 100644 index 94e2847819e..00000000000 --- a/frontend/src/app/features/user-preferences/reminder-settings/email-alerts/email-alerts-settings.component.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, - OnInit, -} from '@angular/core'; -import { I18nService } from 'core-app/core/i18n/i18n.service'; -import { UserPreferencesService } from 'core-app/features/user-preferences/state/user-preferences.service'; -import { - UntypedFormGroup, - FormGroupDirective, -} from '@angular/forms'; - -export type EmailAlertType = - 'newsAdded'|'newsCommented'|'documentAdded'|'forumMessages'|'wikiPageAdded'| - 'wikiPageUpdated'|'membershipAdded'|'membershipUpdated'; - -export const emailAlerts:EmailAlertType[] = [ - 'newsAdded', - 'newsCommented', - 'documentAdded', - 'forumMessages', - 'wikiPageAdded', - 'wikiPageUpdated', - 'membershipAdded', - 'membershipUpdated', -]; - -@Component({ - selector: 'op-email-alerts-settings', - templateUrl: './email-alerts-settings.component.html', - changeDetection: ChangeDetectionStrategy.OnPush, - standalone: false, -}) -export class EmailAlertsSettingsComponent implements OnInit { - form:UntypedFormGroup; - - alerts:EmailAlertType[] = emailAlerts; - - text = { - title: this.I18n.t('js.reminders.settings.alerts.title'), - explanation: this.I18n.t('js.reminders.settings.alerts.explanation'), - newsAdded: this.I18n.t('js.reminders.settings.alerts.news_added'), - newsCommented: this.I18n.t('js.reminders.settings.alerts.news_commented'), - documentAdded: this.I18n.t('js.reminders.settings.alerts.document_added'), - forumMessages: this.I18n.t('js.reminders.settings.alerts.forum_messages'), - wikiPageAdded: this.I18n.t('js.reminders.settings.alerts.wiki_page_added'), - wikiPageUpdated: this.I18n.t('js.reminders.settings.alerts.wiki_page_updated'), - membershipAdded: this.I18n.t('js.reminders.settings.alerts.membership_added'), - membershipUpdated: this.I18n.t('js.reminders.settings.alerts.membership_updated'), - }; - - constructor( - private I18n:I18nService, - private storeService:UserPreferencesService, - private rootFormGroup:FormGroupDirective, - ) { - } - - ngOnInit():void { - this.form = this.rootFormGroup.control.get('emailAlerts') as UntypedFormGroup; - } -} diff --git a/frontend/src/app/features/user-preferences/reminder-settings/immediate-reminders/immediate-reminder-settings.component.html b/frontend/src/app/features/user-preferences/reminder-settings/immediate-reminders/immediate-reminder-settings.component.html deleted file mode 100644 index 1a1bc1accf8..00000000000 --- a/frontend/src/app/features/user-preferences/reminder-settings/immediate-reminders/immediate-reminder-settings.component.html +++ /dev/null @@ -1,29 +0,0 @@ - -
-

-
- - - - - - - - -
diff --git a/frontend/src/app/features/user-preferences/reminder-settings/immediate-reminders/immediate-reminder-settings.component.ts b/frontend/src/app/features/user-preferences/reminder-settings/immediate-reminders/immediate-reminder-settings.component.ts deleted file mode 100644 index e7bf3f707d4..00000000000 --- a/frontend/src/app/features/user-preferences/reminder-settings/immediate-reminders/immediate-reminder-settings.component.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, - OnInit, -} from '@angular/core'; -import { I18nService } from 'core-app/core/i18n/i18n.service'; -import { UserPreferencesService } from 'core-app/features/user-preferences/state/user-preferences.service'; -import { - UntypedFormGroup, - FormGroupDirective, -} from '@angular/forms'; - -@Component({ - selector: 'op-immediate-reminder-settings', - templateUrl: './immediate-reminder-settings.component.html', - changeDetection: ChangeDetectionStrategy.OnPush, - standalone: false, -}) -export class ImmediateReminderSettingsComponent implements OnInit { - form:UntypedFormGroup; - - text = { - title: this.I18n.t('js.reminders.settings.immediate.title'), - explanation: this.I18n.t('js.reminders.settings.immediate.explanation'), - mentioned: this.I18n.t('js.reminders.settings.immediate.mentioned'), - personalReminder: this.I18n.t('js.reminders.settings.immediate.personal_reminder'), - }; - - constructor( - private I18n:I18nService, - private storeService:UserPreferencesService, - private rootFormGroup:FormGroupDirective, - ) { - } - - ngOnInit():void { - this.form = this.rootFormGroup.control.get('immediateReminders') as UntypedFormGroup; - } -} diff --git a/frontend/src/app/features/user-preferences/reminder-settings/page/reminder-settings-page.component.html b/frontend/src/app/features/user-preferences/reminder-settings/page/reminder-settings-page.component.html deleted file mode 100644 index bec895d0f5b..00000000000 --- a/frontend/src/app/features/user-preferences/reminder-settings/page/reminder-settings-page.component.html +++ /dev/null @@ -1,19 +0,0 @@ -@if (formInitialized) { -
- - - - -
- -
- -} diff --git a/frontend/src/app/features/user-preferences/reminder-settings/page/reminder-settings-page.component.sass b/frontend/src/app/features/user-preferences/reminder-settings/page/reminder-settings-page.component.sass deleted file mode 100644 index f0a45e0f578..00000000000 --- a/frontend/src/app/features/user-preferences/reminder-settings/page/reminder-settings-page.component.sass +++ /dev/null @@ -1,6 +0,0 @@ -// TODO: remove once we have a standard -// styling for these section headings -.form--section-title - text-transform: initial - border-bottom: initial - margin-bottom: initial !important \ No newline at end of file diff --git a/frontend/src/app/features/user-preferences/reminder-settings/page/reminder-settings-page.component.ts b/frontend/src/app/features/user-preferences/reminder-settings/page/reminder-settings-page.component.ts deleted file mode 100644 index e9ff944f64d..00000000000 --- a/frontend/src/app/features/user-preferences/reminder-settings/page/reminder-settings-page.component.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, 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 { UserPreferencesService } from 'core-app/features/user-preferences/state/user-preferences.service'; -import { UntypedFormArray, UntypedFormBuilder } from '@angular/forms'; -import { - DailyRemindersSettings, - ImmediateRemindersSettings, - IUserPreference, - PauseRemindersSettings, -} from 'core-app/features/user-preferences/state/user-preferences.model'; -import { - emailAlerts, - EmailAlertType, -} from 'core-app/features/user-preferences/reminder-settings/email-alerts/email-alerts-settings.component'; -import { UntilDestroyedMixin } from 'core-app/shared/helpers/angular/until-destroyed.mixin'; -import { filter, withLatestFrom } from 'rxjs/operators'; -import { filterObservable } from 'core-app/shared/helpers/rxjs/filterWith'; -import { INotificationSetting } from 'core-app/features/user-preferences/state/notification-setting.model'; -import { populateInputsFromDataset } from 'core-app/shared/components/dataset-inputs'; - -interface IReminderSettingsFormValue { - immediateReminders:ImmediateRemindersSettings, - dailyReminders:DailyRemindersSettings, - pauseReminders:Partial, - emailAlerts:Record; - workdays:boolean[]; -} - -@Component({ - templateUrl: './reminder-settings-page.component.html', - styleUrls: ['./reminder-settings-page.component.sass'], - changeDetection: ChangeDetectionStrategy.OnPush, - standalone: false, -}) -export class ReminderSettingsPageComponent extends UntilDestroyedMixin implements OnInit { - @Input() userId:string; - - public form = this.fb.group({ - immediateReminders: this.fb.group({ - mentioned: this.fb.control(false), - personalReminder: this.fb.control(false), - }), - dailyReminders: this.fb.group({ - enabled: this.fb.control(false), - times: this.fb.array([]), - }), - pauseReminders: this.fb.group({ - enabled: this.fb.control(false), - firstDay: this.fb.control(''), - lastDay: this.fb.control(''), - }), - workdays: this.fb.array([ - this.fb.control(false), - this.fb.control(true), - this.fb.control(true), - this.fb.control(true), - this.fb.control(true), - this.fb.control(true), - this.fb.control(false), - ]), - emailAlerts: this.fb.group({ - newsAdded: this.fb.control(false), - newsCommented: this.fb.control(false), - documentAdded: this.fb.control(false), - forumMessages: this.fb.control(false), - wikiPageAdded: this.fb.control(false), - wikiPageUpdated: this.fb.control(false), - membershipAdded: this.fb.control(false), - membershipUpdated: this.fb.control(false), - }), - }); - - text = { - title: this.I18n.t('js.reminders.settings.title'), - save: this.I18n.t('js.button_save'), - }; - - formInitialized = false; - - constructor( - readonly elementRef:ElementRef, - readonly I18n:I18nService, - readonly storeService:UserPreferencesService, - readonly currentUserService:CurrentUserService, - readonly fb:UntypedFormBuilder, - readonly cdRef:ChangeDetectorRef, - ) { - super(); - populateInputsFromDataset(this); - } - - ngOnInit():void { - this - .currentUserService - .user$ - .pipe(take(1)) - .subscribe((user) => { - this.userId = this.userId || user?.id!; - this.storeService.get(this.userId); - }); - - this.storeService.query.select() - .pipe( - filter((settings) => !!settings), - withLatestFrom(this.storeService.query.globalNotification$), - filterObservable(this.storeService.query.selectLoading(), (val) => !val), - ) - .subscribe(([settings, globalSetting]) => { - this.buildForm(settings, globalSetting); - }); - } - - private buildForm(settings:IUserPreference, globalSetting:INotificationSetting) { - this.form.get('immediateReminders.mentioned')?.setValue(settings.immediateReminders.mentioned); - this.form.get('immediateReminders.personalReminder')?.setValue(settings.immediateReminders.personalReminder); - - this.form.get('dailyReminders.enabled')?.setValue(settings.dailyReminders.enabled); - - this.form.get('pauseReminders')?.patchValue(settings.pauseReminders); - - const dailyReminderTimes = this.form.get('dailyReminders.times') as UntypedFormArray; - dailyReminderTimes.clear({ emitEvent: false }); - [...settings.dailyReminders.times].sort().forEach((time) => { - dailyReminderTimes.push(this.fb.control(time), { emitEvent: false }); - }); - - dailyReminderTimes.enable({ emitEvent: true }); - - const workdays = this.form.get('workdays') as UntypedFormArray; - for (let i = 0; i <= 6; i++) { - const control = workdays.at(i); - control.setValue(settings.workdays.includes(i + 1)); - } - - emailAlerts.forEach((alert) => { - this.form.get(`emailAlerts.${alert}`)?.setValue(globalSetting[alert]); - }); - - this.formInitialized = true; - this.cdRef.detectChanges(); - } - - public saveChanges():void { - const prefs = this.storeService.query.getValue(); - const globalNotifications = prefs.notifications.filter((notification) => !notification._links.project.href); - const projectNotifications = prefs.notifications.filter((notification) => !!notification._links.project.href); - const reminderSettings = (this.form.value as IReminderSettingsFormValue); - const workdays = ReminderSettingsPageComponent.buildWorkdays(reminderSettings.workdays); - const pauseReminders = ReminderSettingsPageComponent.buildPauses(reminderSettings.pauseReminders); - const { dailyReminders, immediateReminders } = reminderSettings; - - this.storeService.update(this.userId, { - ...prefs, - workdays, - dailyReminders, - immediateReminders, - pauseReminders, - notifications: [ - ...globalNotifications.map((notification) => ( - { - ...notification, - ...reminderSettings.emailAlerts, - } - )), - ...projectNotifications, - ], - }); - } - - private static buildWorkdays(formValues:boolean[]):number[] { - return formValues - .reduce( - (result, val, index) => { - if (val) { - return result.concat([index + 1]); - } - - return result; - }, - [] as number[], - ); - } - - private static buildPauses(formValues:Partial):Partial { - if (formValues.enabled) { - return formValues; - } - - return { enabled: false }; - } -} diff --git a/frontend/src/app/features/user-preferences/reminder-settings/pause-reminders/pause-reminders.component.html b/frontend/src/app/features/user-preferences/reminder-settings/pause-reminders/pause-reminders.component.html deleted file mode 100644 index a5b12fe528e..00000000000 --- a/frontend/src/app/features/user-preferences/reminder-settings/pause-reminders/pause-reminders.component.html +++ /dev/null @@ -1,23 +0,0 @@ -
- - - - - @if ((enabled$ | async)) { - - } -
diff --git a/frontend/src/app/features/user-preferences/reminder-settings/pause-reminders/pause-reminders.component.sass b/frontend/src/app/features/user-preferences/reminder-settings/pause-reminders/pause-reminders.component.sass deleted file mode 100644 index 774cd70c18a..00000000000 --- a/frontend/src/app/features/user-preferences/reminder-settings/pause-reminders/pause-reminders.component.sass +++ /dev/null @@ -1,6 +0,0 @@ -.op-pause-reminders - display: flex - align-items: center - - &--checkbox - margin-right: 2rem \ No newline at end of file diff --git a/frontend/src/app/features/user-preferences/reminder-settings/pause-reminders/pause-reminders.component.ts b/frontend/src/app/features/user-preferences/reminder-settings/pause-reminders/pause-reminders.component.ts deleted file mode 100644 index fbededd5d1b..00000000000 --- a/frontend/src/app/features/user-preferences/reminder-settings/pause-reminders/pause-reminders.component.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, - OnInit, -} from '@angular/core'; -import { - UntypedFormGroup, - FormGroupDirective, -} from '@angular/forms'; -import { I18nService } from 'core-app/core/i18n/i18n.service'; -import { - map, - startWith, -} from 'rxjs/operators'; -import { Observable } from 'rxjs'; - -@Component({ - selector: 'op-pause-reminders', - templateUrl: './pause-reminders.component.html', - styleUrls: ['./pause-reminders.component.sass'], - changeDetection: ChangeDetectionStrategy.OnPush, - standalone: false, -}) -export class PauseRemindersComponent implements OnInit { - form:UntypedFormGroup; - - selectedDates$:Observable<[string, string]>; - - enabled$:Observable; - - text = { - label: this.I18n.t('js.reminders.settings.pause.label'), - date_placeholder: this.I18n.t('js.placeholders.date'), - first_day: this.I18n.t('js.reminders.settings.pause.first_day'), - last_day: this.I18n.t('js.reminders.settings.pause.first_day'), - }; - - constructor( - private I18n:I18nService, - private rootFormGroup:FormGroupDirective, - ) { - } - - ngOnInit():void { - this.form = this.rootFormGroup.control.get('pauseReminders') as UntypedFormGroup; - this.selectedDates$ = this - .form - .valueChanges - .pipe( - startWith(this.form.value), - map((form:{ firstDay:string, lastDay:string }) => [form.firstDay, form.lastDay]), - ); - - this.enabled$ = this - .form - .valueChanges - .pipe( - startWith(this.form.value), - map((form:{ enabled:boolean }) => form.enabled), - ); - } - - setDates($event:[string, string]):void { - const [firstDay, lastDay] = $event; - this.form.patchValue({ - firstDay, - lastDay, - }); - } -} diff --git a/frontend/src/app/features/user-preferences/reminder-settings/reminder-time/reminder-settings-daily-time.component.html b/frontend/src/app/features/user-preferences/reminder-settings/reminder-time/reminder-settings-daily-time.component.html deleted file mode 100644 index 01e36dce6fc..00000000000 --- a/frontend/src/app/features/user-preferences/reminder-settings/reminder-time/reminder-settings-daily-time.component.html +++ /dev/null @@ -1,73 +0,0 @@ -@if ((selectedTimes$ | async); as selectedTimes) { - -
-

-

-
- - - - @for (time of selectedTimes; track i; let i = $index) { -
- @if ((activeTimes$ | async); as activeTimes) { - - } - - - @if (timeRemovable$ | async) { - - } -
- } - -
-} diff --git a/frontend/src/app/features/user-preferences/reminder-settings/reminder-time/reminder-settings-daily-time.component.sass b/frontend/src/app/features/user-preferences/reminder-settings/reminder-time/reminder-settings-daily-time.component.sass deleted file mode 100644 index 6699f05ee65..00000000000 --- a/frontend/src/app/features/user-preferences/reminder-settings/reminder-time/reminder-settings-daily-time.component.sass +++ /dev/null @@ -1,37 +0,0 @@ -@import "helpers" - -.op-reminder-settings-daily-time - &--enable - display: flex - align-items: center - margin-top: 30px - margin-bottom: 10px - - &--row - margin-left: 40px - line-height: 45px - display: flex - align-items: center - - &:nth-of-type(1) - margin-top: 10px - - &--active - flex: 0 0 25px - - &--label - flex: 0 0 80px - margin-bottom: 0 - @include text-shortener - - &--time - height: 32px - width: 150px - - &--remove - padding-left: 10px - - &--add - margin-top: 20px - margin-left: 40px - width: auto diff --git a/frontend/src/app/features/user-preferences/reminder-settings/reminder-time/reminder-settings-daily-time.component.ts b/frontend/src/app/features/user-preferences/reminder-settings/reminder-time/reminder-settings-daily-time.component.ts deleted file mode 100644 index e0d081fc302..00000000000 --- a/frontend/src/app/features/user-preferences/reminder-settings/reminder-time/reminder-settings-daily-time.component.ts +++ /dev/null @@ -1,251 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, - OnInit, -} from '@angular/core'; -import { I18nService } from 'core-app/core/i18n/i18n.service'; -import { - map, - shareReplay, - startWith, -} from 'rxjs/operators'; -import { - combineLatest, - NEVER, - Observable, -} from 'rxjs'; -import { UserPreferencesService } from 'core-app/features/user-preferences/state/user-preferences.service'; -import { - UntypedFormArray, - UntypedFormControl, - UntypedFormGroup, - FormGroupDirective, -} from '@angular/forms'; -import { ConfigurationService } from 'core-app/core/config/configuration.service'; -import moment from 'moment'; - -@Component({ - selector: 'op-reminder-settings-daily-time', - templateUrl: './reminder-settings-daily-time.component.html', - styleUrls: ['./reminder-settings-daily-time.component.sass'], - changeDetection: ChangeDetectionStrategy.OnPush, - standalone: false, -}) -export class ReminderSettingsDailyTimeComponent implements OnInit { - // All times that are available in a day with a 1 hour gap between each. - // ['00:00', '01:00', ..., '24:00'] - public availableTimes:string[] = ReminderSettingsDailyTimeComponent.setupAvailableTimes(); - - // The times (hours) that the user deactivated. Those are only stored within the component - // as the inactive hours are not persisted. This list is then interleaved with the list - // of times stored in the backend. As the order of the times should be kept, - // the position needs to be maintained. - // Upon a reload of the page, it is accepted to loose this information. - public inactiveTimes:{ position:number, time:string }[] = []; - - public form:UntypedFormGroup; - - // Hours suggested if a new time is added by a user. - public suggestedTimes = ['08:00', '12:00', '15:00', '18:00']; - - // Whether the reminder are active at all. - public enabled$:Observable; - - // The active times as present in the store interleaved with the inactive - // times. - public selectedTimes$:Observable = NEVER; - - // Times that are truly active: - // * the reminders are not disabled completely - // * the times are not inactive individually. - public activeTimes$:Observable = NEVER; - - // Times can only be removed if the element is active and if there is more than one time present. - public timeRemovable$:Observable = NEVER; - - // Times can not be added if the element is disabled or if all the possible times have already been added (active or not). - public nonAddable$:Observable = NEVER; - - text = { - title: this.I18n.t('js.reminders.settings.daily.title'), - explanation: this.I18n.t('js.reminders.settings.daily.explanation', - { no_time_zone: this.configurationService.isTimezoneSet() ? '' : this.I18n.t('js.reminders.settings.daily.no_time_zone') }), - timeLabel: (counter:number):string => this.I18n.t('js.reminders.settings.daily.time_label', { counter }), - addTime: this.I18n.t('js.reminders.settings.daily.add_time'), - enable: this.I18n.t('js.reminders.settings.daily.enable'), - }; - - constructor( - private I18n:I18nService, - private storeService:UserPreferencesService, - private rootFormGroup:FormGroupDirective, - private configurationService:ConfigurationService, - ) { - } - - ngOnInit():void { - this.form = this.rootFormGroup.control.get('dailyReminders') as UntypedFormGroup; - - this.enabled$ = this - .form - .valueChanges - .pipe( - startWith(() => this.form.get('enabled')?.value as boolean), - map(() => this.form.get('enabled')?.value as boolean), - shareReplay(1), - ); - - this.selectedTimes$ = (this - .form - .get('times') as UntypedFormArray) - .valueChanges - .pipe( - startWith(() => this.form.get('times')?.value as UntypedFormArray), - map(() => { - const timesArray = this.form.get('times') as UntypedFormArray; - const activeTimes = timesArray.controls.map((c) => c.value as string); - - this - .inactiveTimes - .sort((a, b) => a.position - b.position) - .forEach((inactiveTime) => { - activeTimes.splice(inactiveTime.position, 0, inactiveTime.time); - }); - - return activeTimes; - }), - shareReplay(1), - ); - - this.timeRemovable$ = combineLatest([ - this.enabled$, - this.selectedTimes$, - ]).pipe(map(([enabled, selectedTimes]) => enabled && selectedTimes.length > 1)); - - this.nonAddable$ = combineLatest([ - this.enabled$, - this.selectedTimes$, - ]).pipe(map(([enabled, selectedTimes]) => !enabled || selectedTimes.length === this.availableTimes.length)); - - this.activeTimes$ = combineLatest([ - this.enabled$, - this.selectedTimes$, - ]).pipe( - map(([enabled, times]) => (enabled ? times : [])), - ); - } - - addTime(selectedTimes:string[]):void { - const time = this.firstAvailableSuggested(selectedTimes) || this.firstAfterSelected(selectedTimes); - - if (time) { - this.storeTimes(selectedTimes.concat(time)); - } - } - - changeTime(newTime:string, selectedTimes:string[], index:number):void { - selectedTimes.splice(index, 1, newTime); - - this.storeTimes(selectedTimes); - } - - isActive(time:string):boolean { - return !this.inactiveTimes.find((inactive) => inactive.time === time); - } - - removeTime(selectedTimes:string[], index:number):void { - this.inactiveTimes = this - .inactiveTimes - .filter((inactiveTime) => inactiveTime.time !== selectedTimes[index]); - - this.inactiveTimes - .forEach((inactiveTime) => { - if (inactiveTime.position > index) { - inactiveTime.position -= 1; - } - }); - - selectedTimes.splice(index, 1); - - if (selectedTimes.length === 1) { - this.inactiveTimes = []; - } - - // Activate the first time if none is active. - if (selectedTimes.length === this.inactiveTimes.length) { - this.inactiveTimes.shift(); - } - - this.storeTimes(selectedTimes); - } - - toggleActive(active:boolean, index:number, selectedTimes:string[]):void { - if (!active) { - this.inactiveTimes.push({ position: index, time: selectedTimes[index] }); - } else { - this.inactiveTimes = this.inactiveTimes.filter((inactiveTime) => inactiveTime.time !== selectedTimes[index]); - } - - this.storeTimes(selectedTimes); - } - - timeLabel(time:string):string { - return this - .I18n - .toTime( - 'time.formats.time', - ReminderSettingsDailyTimeComponent.dateForHour(parseInt(time.split(':')[0], 10)), - ); - } - - isDisabled(time:string, activeTimes:string[]):boolean { - return activeTimes.length === 0 || (activeTimes.length === 1 && activeTimes[0] === time); - } - - private storeTimes(selectedTimes:string[]) { - const times = selectedTimes - .filter( - (selected) => !this.inactiveTimes - .map((inactive) => inactive.time) - .includes(selected), - ); - - const timesForm = this.form.get('times') as UntypedFormArray; - timesForm.clear({ emitEvent: false }); - times.forEach((time) => { - timesForm.push(new UntypedFormControl(time), { emitEvent: false }); - }); - - timesForm.enable({ emitEvent: true }); - } - - private firstAvailableSuggested(selectedTimes:string[]) { - return this.availableTimes.find((v) => this.suggestedTimes.includes(v) && !selectedTimes.includes(v)); - } - - private firstAfterSelected(selectedTimes:string[]) { - const indexLastSelected = this.availableTimes.indexOf(selectedTimes[selectedTimes.length - 1]); - - for (let i = indexLastSelected; i < 24 + indexLastSelected; i++) { - if (!selectedTimes.includes(this.availableTimes[i % 24])) { - return this.availableTimes[i % 24]; - } - } - - return null; - } - - private static setupAvailableTimes() { - return Array.from({ length: 24 }, (v, i) => ReminderSettingsDailyTimeComponent - .dateForHour(i) - .toLocaleTimeString('en-US', { hour12: false, hour: 'numeric', minute: 'numeric' })); - } - - private static dateForHour(hour:number) { - const currentTime = new Date(); - currentTime.setTime(1000 * 60 * 60 * (hour - 1)); - const convertTimeObject = new Date(moment(currentTime).utc().hours(hour).format('YYYY-MM-DDTHH:mm:ss')); - - return convertTimeObject; - } -} diff --git a/frontend/src/app/features/user-preferences/reminder-settings/workdays/workdays-settings.component.html b/frontend/src/app/features/user-preferences/reminder-settings/workdays/workdays-settings.component.html deleted file mode 100644 index 5d072c119d0..00000000000 --- a/frontend/src/app/features/user-preferences/reminder-settings/workdays/workdays-settings.component.html +++ /dev/null @@ -1,21 +0,0 @@ - -
-

-
- - @for (workday of localeWorkdays; track workday; let i = $index) { - - - - } - - -
diff --git a/frontend/src/app/features/user-preferences/reminder-settings/workdays/workdays-settings.component.sass b/frontend/src/app/features/user-preferences/reminder-settings/workdays/workdays-settings.component.sass deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/frontend/src/app/features/user-preferences/reminder-settings/workdays/workdays-settings.component.ts b/frontend/src/app/features/user-preferences/reminder-settings/workdays/workdays-settings.component.ts deleted file mode 100644 index 4cf2267f5c5..00000000000 --- a/frontend/src/app/features/user-preferences/reminder-settings/workdays/workdays-settings.component.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, - OnInit, -} from '@angular/core'; -import { - UntypedFormArray, - UntypedFormControl, - FormGroupDirective, -} from '@angular/forms'; -import moment from 'moment'; -import { I18nService } from 'core-app/core/i18n/i18n.service'; - -@Component({ - selector: 'op-workdays-settings', - templateUrl: './workdays-settings.component.html', - styleUrls: ['./workdays-settings.component.sass'], - changeDetection: ChangeDetectionStrategy.OnPush, - standalone: false, -}) -export class WorkdaysSettingsComponent implements OnInit { - control:UntypedFormArray; - - /** - * The locale might render workdays in a different order, which is what moment return with localeSorted - * and used for rendering the component. - */ - localeWorkdays:string[] = moment.weekdays(true); - - /** - * Almost* ISO workdays with localized strings. - * ISO workdays are 1=Monday, ... 7=Sunday which is what we persist - * - * Working with the FormArray however, we use 0=Monday, 6=Sunday and add one before saving - * @private - */ - private isoWorkdays:string[] = WorkdaysSettingsComponent.buildISOWeekdays(); - - text = { - title: this.I18n.t('js.reminders.settings.workdays.title'), - }; - - constructor( - private I18n:I18nService, - readonly formGroup:FormGroupDirective, - ) { - } - - ngOnInit():void { - this.control = this.formGroup.control.get('workdays') as UntypedFormArray; - } - - indexOfLocalWorkday(day:string):number { - return this.isoWorkdays.indexOf(day); - } - - controlForLocalWorkday(day:string):UntypedFormControl { - const index = this.indexOfLocalWorkday(day); - return this.control.at(index) as UntypedFormControl; - } - - /** Workdays from moment.js are in non-ISO order, that means Sunday=0, Saturday=6 */ - static buildISOWeekdays():string[] { - const days = moment.weekdays(false); - - days.push(days.shift()!); - - return days; - } -} diff --git a/frontend/src/app/features/user-preferences/user-preferences.module.ts b/frontend/src/app/features/user-preferences/user-preferences.module.ts index 42915655cb2..b52163cabb7 100644 --- a/frontend/src/app/features/user-preferences/user-preferences.module.ts +++ b/frontend/src/app/features/user-preferences/user-preferences.module.ts @@ -15,18 +15,6 @@ import { 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'; -import { - ImmediateReminderSettingsComponent, -} from 'core-app/features/user-preferences/reminder-settings/immediate-reminders/immediate-reminder-settings.component'; -import { - EmailAlertsSettingsComponent, -} from 'core-app/features/user-preferences/reminder-settings/email-alerts/email-alerts-settings.component'; -import { WorkdaysSettingsComponent } from './reminder-settings/workdays/workdays-settings.component'; -import { PauseRemindersComponent } from './reminder-settings/pause-reminders/pause-reminders.component'; import { OpenprojectEnterpriseModule } from 'core-app/features/enterprise/openproject-enterprise.module'; @NgModule({ @@ -37,12 +25,6 @@ import { OpenprojectEnterpriseModule } from 'core-app/features/enterprise/openpr NotificationsSettingsPageComponent, NotificationSettingInlineCreateComponent, NotificationSettingsTableComponent, - ReminderSettingsPageComponent, - ReminderSettingsDailyTimeComponent, - ImmediateReminderSettingsComponent, - EmailAlertsSettingsComponent, - WorkdaysSettingsComponent, - PauseRemindersComponent, ], imports: [ CommonModule, diff --git a/lib/open_project/ui/extensible_tabs.rb b/lib/open_project/ui/extensible_tabs.rb index 7363a462453..b8c17de2d0f 100644 --- a/lib/open_project/ui/extensible_tabs.rb +++ b/lib/open_project/ui/extensible_tabs.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -105,13 +107,13 @@ module OpenProject name: "notifications", partial: "users/notifications", path: ->(params) { edit_user_path(params[:user], tab: :notifications) }, - label: :"js.notifications.settings.title" + label: :"my_account.notifications_and_email.tabs.notifications" }, { name: "reminders", partial: "users/reminders", path: ->(params) { edit_user_path(params[:user], tab: :reminders) }, - label: :"js.reminders.settings.title" + label: :"my_account.notifications_and_email.tabs.email_reminders" } ] end