From 11d225c871de26699365c22e9b890ba61f010834 Mon Sep 17 00:00:00 2001 From: Roma Date: Thu, 8 May 2025 20:00:04 +0300 Subject: [PATCH 01/14] feat(user-settings): refactored user management, implemented fetching user settings --- .../underscore-entites/user/user-us.entity.ts | 26 ------- .../services/mappers/users/users.mapper.ts | 20 ------ src/app/core/services/user/user.entity.ts | 28 -------- src/app/core/services/user/user.models.ts | 67 +++++++++++++++++++ src/app/core/services/user/user.service.ts | 26 +++++-- src/app/core/services/user/users.mapper.ts | 37 ++++++++++ src/app/core/store/user/index.ts | 2 +- src/app/core/store/user/user.actions.ts | 8 +-- src/app/core/store/user/user.models.ts | 5 -- src/app/core/store/user/user.selectors.ts | 9 ++- src/app/core/store/user/user.state-model.ts | 6 ++ src/app/core/store/user/user.state.ts | 23 ++++--- .../notifications/notifications.component.ts | 29 +++++++- .../profile-settings/name/name.entities.ts | 2 +- .../profile-settings.actions.ts | 2 +- .../profile-settings.api.service.ts | 4 +- .../profile-settings.entities.ts | 2 +- .../profile-settings.selectors.ts | 2 +- 18 files changed, 189 insertions(+), 109 deletions(-) delete mode 100644 src/app/core/services/json-api/underscore-entites/user/user-us.entity.ts delete mode 100644 src/app/core/services/mappers/users/users.mapper.ts delete mode 100644 src/app/core/services/user/user.entity.ts create mode 100644 src/app/core/services/user/user.models.ts create mode 100644 src/app/core/services/user/users.mapper.ts delete mode 100644 src/app/core/store/user/user.models.ts create mode 100644 src/app/core/store/user/user.state-model.ts diff --git a/src/app/core/services/json-api/underscore-entites/user/user-us.entity.ts b/src/app/core/services/json-api/underscore-entites/user/user-us.entity.ts deleted file mode 100644 index 628f0ed44..000000000 --- a/src/app/core/services/json-api/underscore-entites/user/user-us.entity.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Employment } from '@osf/features/settings/profile-settings/employment/employment.entities'; -import { Education } from '@osf/features/settings/profile-settings/education/educations.entities'; -import { Social } from '@osf/features/settings/profile-settings/social/social.entities'; - -export interface UserUS { - id: string; - type: string; - attributes: { - full_name: string; - given_name: string; - family_name: string; - email?: string; - employment: Employment[]; - education: Education[]; - middle_names?: string; - suffix?: string; - social: Social; - date_registered: string; - }; - relationships: Record; - links: { - html: string; - profile_image: string; - iri: string; - }; -} diff --git a/src/app/core/services/mappers/users/users.mapper.ts b/src/app/core/services/mappers/users/users.mapper.ts deleted file mode 100644 index 2fab33d11..000000000 --- a/src/app/core/services/mappers/users/users.mapper.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { User } from '@core/services/user/user.entity'; -import { UserUS } from '@core/services/json-api/underscore-entites/user/user-us.entity'; - -export function mapUserUStoUser(user: UserUS): User { - return { - id: user.id, - fullName: user.attributes.full_name, - givenName: user.attributes.given_name, - middleNames: user.attributes.middle_names, - suffix: user.attributes.suffix, - familyName: user.attributes.family_name, - email: user.attributes.email, - dateRegistered: new Date(user.attributes.date_registered), - link: user.links.html, - education: user.attributes.education, - employment: user.attributes.employment, - iri: user.links.iri, - social: user.attributes.social, - }; -} diff --git a/src/app/core/services/user/user.entity.ts b/src/app/core/services/user/user.entity.ts deleted file mode 100644 index 47ffc35a4..000000000 --- a/src/app/core/services/user/user.entity.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Education } from '@osf/features/settings/profile-settings/education/educations.entities'; -import { Employment } from '@osf/features/settings/profile-settings/employment/employment.entities'; -import { Social } from '@osf/features/settings/profile-settings/social/social.entities'; - -export interface User { - id: string; - fullName: string; - givenName: string; - familyName: string; - email?: string; - middleNames?: string; - suffix?: string; - education: Education[]; - employment: Employment[]; - social: Social; - dateRegistered: Date; - link?: string; - iri?: string; - socials?: { - orcid?: string; - github?: string; - scholar?: string; - twitter?: string; - linkedIn?: string; - impactStory?: string; - researcherId?: string; - }; -} diff --git a/src/app/core/services/user/user.models.ts b/src/app/core/services/user/user.models.ts new file mode 100644 index 000000000..e98deb5f1 --- /dev/null +++ b/src/app/core/services/user/user.models.ts @@ -0,0 +1,67 @@ +import { Education } from '@osf/features/settings/profile-settings/education/educations.entities'; +import { Employment } from '@osf/features/settings/profile-settings/employment/employment.entities'; +import { Social } from '@osf/features/settings/profile-settings/social/social.entities'; + +//Domain models +export interface User { + id: string; + fullName: string; + givenName: string; + familyName: string; + email?: string; + middleNames?: string; + suffix?: string; + education: Education[]; + employment: Employment[]; + social: Social; + dateRegistered: Date; + link?: string; + iri?: string; + socials?: { + orcid?: string; + github?: string; + scholar?: string; + twitter?: string; + linkedIn?: string; + impactStory?: string; + researcherId?: string; + }; +} + +export interface UserSettings { + subscribeOsfGeneralEmail: boolean; + subscribeOsfHelpEmail: boolean; +} + +// API Request/Response Models +export interface UserGetResponse { + id: string; + type: string; + attributes: { + full_name: string; + given_name: string; + family_name: string; + email?: string; + employment: Employment[]; + education: Education[]; + middle_names?: string; + suffix?: string; + social: Social; + date_registered: string; + }; + relationships: Record; + links: { + html: string; + profile_image: string; + iri: string; + }; +} + +export interface UserSettingsGetResponse { + id: string; + type: string; + attributes: { + subscribe_osf_general_email: boolean; + subscribe_osf_help_email: boolean; + }; +} diff --git a/src/app/core/services/user/user.service.ts b/src/app/core/services/user/user.service.ts index 44bfda377..2d9501af9 100644 --- a/src/app/core/services/user/user.service.ts +++ b/src/app/core/services/user/user.service.ts @@ -1,10 +1,14 @@ import { inject, Injectable } from '@angular/core'; import { map, Observable } from 'rxjs'; import { JsonApiService } from '@core/services/json-api/json-api.service'; -import { User } from '@core/services/user/user.entity'; -import { UserUS } from '@core/services/json-api/underscore-entites/user/user-us.entity'; -import { mapUserUStoUser } from '@core/services/mappers/users/users.mapper'; +import { + User, + UserGetResponse, + UserSettings, + UserSettingsGetResponse, +} from '@core/services/user/user.models'; import { JsonApiResponse } from '@core/services/json-api/json-api.entity'; +import { UserMapper } from '@core/services/user/users.mapper'; @Injectable({ providedIn: 'root', @@ -15,7 +19,19 @@ export class UserService { getCurrentUser(): Observable { return this.jsonApiService - .get>(this.baseUrl + 'users/me') - .pipe(map((user) => mapUserUStoUser(user.data))); + .get>(this.baseUrl + 'users/me/') + .pipe(map((user) => UserMapper.fromUserGetResponse(user.data))); + } + + getCurrentUserSettings(): Observable { + return this.jsonApiService + .get< + JsonApiResponse + >(this.baseUrl + 'users/me/settings/') + .pipe( + map((response) => + UserMapper.fromUserSettingsGetResponse(response.data), + ), + ); } } diff --git a/src/app/core/services/user/users.mapper.ts b/src/app/core/services/user/users.mapper.ts new file mode 100644 index 000000000..30ec0fc01 --- /dev/null +++ b/src/app/core/services/user/users.mapper.ts @@ -0,0 +1,37 @@ +import { + User, + UserGetResponse, + UserSettings, + UserSettingsGetResponse, +} from '@core/services/user/user.models'; + +export class UserMapper { + static fromUserGetResponse(user: UserGetResponse): User { + return { + id: user.id, + fullName: user.attributes.full_name, + givenName: user.attributes.given_name, + middleNames: user.attributes.middle_names, + suffix: user.attributes.suffix, + familyName: user.attributes.family_name, + email: user.attributes.email, + dateRegistered: new Date(user.attributes.date_registered), + link: user.links.html, + education: user.attributes.education, + employment: user.attributes.employment, + iri: user.links.iri, + social: user.attributes.social, + }; + } + + static fromUserSettingsGetResponse( + userSettingsResponse: UserSettingsGetResponse, + ): UserSettings { + return { + subscribeOsfGeneralEmail: + userSettingsResponse.attributes.subscribe_osf_general_email, + subscribeOsfHelpEmail: + userSettingsResponse.attributes.subscribe_osf_help_email, + }; + } +} diff --git a/src/app/core/store/user/index.ts b/src/app/core/store/user/index.ts index e8c90712a..8d9e41b25 100644 --- a/src/app/core/store/user/index.ts +++ b/src/app/core/store/user/index.ts @@ -1,3 +1,3 @@ -export * from './user.models'; +export * from './user.state-model'; export * from './user.actions'; export * from './user.state'; diff --git a/src/app/core/store/user/user.actions.ts b/src/app/core/store/user/user.actions.ts index 0e255ffd8..e99cad14e 100644 --- a/src/app/core/store/user/user.actions.ts +++ b/src/app/core/store/user/user.actions.ts @@ -1,11 +1,7 @@ -import { User } from '@core/services/user/user.entity'; - export class GetCurrentUser { static readonly type = '[User] Get Current User'; } -export class SetCurrentUser { - static readonly type = '[User] Set Current User'; - - constructor(public user: User) {} +export class GetCurrentUserSettings { + static readonly type = '[User] Get Current User Settings'; } diff --git a/src/app/core/store/user/user.models.ts b/src/app/core/store/user/user.models.ts deleted file mode 100644 index 64e37b79c..000000000 --- a/src/app/core/store/user/user.models.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { User } from '@core/services/user/user.entity'; - -export interface UserStateModel { - currentUser: User | null; -} diff --git a/src/app/core/store/user/user.selectors.ts b/src/app/core/store/user/user.selectors.ts index 586f27935..fd79751f5 100644 --- a/src/app/core/store/user/user.selectors.ts +++ b/src/app/core/store/user/user.selectors.ts @@ -1,6 +1,6 @@ import { Selector } from '@ngxs/store'; -import { UserStateModel } from '@core/store/user/user.models'; -import { User } from '@core/services/user/user.entity'; +import { UserStateModel } from '@core/store/user/user.state-model'; +import { User, UserSettings } from '@core/services/user/user.models'; import { UserState } from '@core/store/user/user.state'; import { ProfileSettingsStateModel } from '@osf/features/settings/profile-settings/profile-settings.entities'; import { Social } from '@osf/features/settings/profile-settings/social/social.entities'; @@ -28,4 +28,9 @@ export class UserSelectors { }, } satisfies ProfileSettingsStateModel; } + + @Selector([UserState]) + static getCurrentUserSettings(state: UserStateModel): UserSettings | null { + return state.currentUserSettings; + } } diff --git a/src/app/core/store/user/user.state-model.ts b/src/app/core/store/user/user.state-model.ts new file mode 100644 index 000000000..8cc015a5e --- /dev/null +++ b/src/app/core/store/user/user.state-model.ts @@ -0,0 +1,6 @@ +import { User, UserSettings } from '@core/services/user/user.models'; + +export interface UserStateModel { + currentUser: User | null; + currentUserSettings: UserSettings | null; +} diff --git a/src/app/core/store/user/user.state.ts b/src/app/core/store/user/user.state.ts index b1fe72c53..49b199293 100644 --- a/src/app/core/store/user/user.state.ts +++ b/src/app/core/store/user/user.state.ts @@ -1,7 +1,7 @@ import { Injectable, inject } from '@angular/core'; import { State, Action, StateContext } from '@ngxs/store'; -import { UserStateModel } from './user.models'; -import { GetCurrentUser, SetCurrentUser } from './user.actions'; +import { UserStateModel } from './user.state-model'; +import { GetCurrentUser, GetCurrentUserSettings } from './user.actions'; import { UserService } from '@core/services/user/user.service'; import { tap } from 'rxjs'; import { SetupProfileSettings } from '@osf/features/settings/profile-settings/profile-settings.actions'; @@ -10,6 +10,7 @@ import { SetupProfileSettings } from '@osf/features/settings/profile-settings/pr name: 'user', defaults: { currentUser: null, + currentUserSettings: null, }, }) @Injectable() @@ -20,16 +21,22 @@ export class UserState { getCurrentUser(ctx: StateContext) { return this.userService.getCurrentUser().pipe( tap((user) => { - ctx.dispatch(new SetCurrentUser(user)); + ctx.patchState({ + currentUser: user, + }); ctx.dispatch(new SetupProfileSettings()); }), ); } - @Action(SetCurrentUser) - setCurrentUser(ctx: StateContext, action: SetCurrentUser) { - ctx.patchState({ - currentUser: action.user, - }); + @Action(GetCurrentUserSettings) + getCurrentUserSettings(ctx: StateContext) { + return this.userService.getCurrentUserSettings().pipe( + tap((userSettings) => { + ctx.patchState({ + currentUserSettings: userSettings, + }); + }), + ); } } diff --git a/src/app/features/settings/notifications/notifications.component.ts b/src/app/features/settings/notifications/notifications.component.ts index 675471cc4..640c9b08e 100644 --- a/src/app/features/settings/notifications/notifications.component.ts +++ b/src/app/features/settings/notifications/notifications.component.ts @@ -1,8 +1,18 @@ -import { ChangeDetectionStrategy, Component, HostBinding } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + effect, + HostBinding, + inject, + OnInit, +} from '@angular/core'; import { SubHeaderComponent } from '@shared/components/sub-header/sub-header.component'; import { Checkbox } from 'primeng/checkbox'; import { Button } from 'primeng/button'; import { DropdownModule } from 'primeng/dropdown'; +import { Store } from '@ngxs/store'; +import { GetCurrentUserSettings } from '@core/store/user'; +import { UserSelectors } from '@core/store/user/user.selectors'; @Component({ selector: 'osf-notifications', @@ -11,6 +21,21 @@ import { DropdownModule } from 'primeng/dropdown'; styleUrl: './notifications.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class NotificationsComponent { +export class NotificationsComponent implements OnInit { @HostBinding('class') classes = 'flex flex-1 flex-column'; + + readonly #store = inject(Store); + readonly #userSettings = this.#store.selectSignal( + UserSelectors.getCurrentUserSettings, + ); + + constructor() { + effect(() => { + console.log('Current user settings:', this.#userSettings()); + }); + } + + ngOnInit(): void { + this.#store.dispatch(new GetCurrentUserSettings()); + } } diff --git a/src/app/features/settings/profile-settings/name/name.entities.ts b/src/app/features/settings/profile-settings/name/name.entities.ts index 723c1da40..e0180a138 100644 --- a/src/app/features/settings/profile-settings/name/name.entities.ts +++ b/src/app/features/settings/profile-settings/name/name.entities.ts @@ -1,5 +1,5 @@ import { FormControl } from '@angular/forms'; -import { User } from '@core/services/user/user.entity'; +import { User } from '@core/services/user/user.models'; export interface Name { fullName: string; diff --git a/src/app/features/settings/profile-settings/profile-settings.actions.ts b/src/app/features/settings/profile-settings/profile-settings.actions.ts index 778bca44d..a0ad39c3d 100644 --- a/src/app/features/settings/profile-settings/profile-settings.actions.ts +++ b/src/app/features/settings/profile-settings/profile-settings.actions.ts @@ -1,6 +1,6 @@ import { Employment } from '@osf/features/settings/profile-settings/employment/employment.entities'; import { Education } from '@osf/features/settings/profile-settings/education/educations.entities'; -import { User } from '@core/services/user/user.entity'; +import { User } from '@core/services/user/user.models'; import { Social } from '@osf/features/settings/profile-settings/social/social.entities'; export class SetupProfileSettings { diff --git a/src/app/features/settings/profile-settings/profile-settings.api.service.ts b/src/app/features/settings/profile-settings/profile-settings.api.service.ts index 1d5f4301e..0790802ed 100644 --- a/src/app/features/settings/profile-settings/profile-settings.api.service.ts +++ b/src/app/features/settings/profile-settings/profile-settings.api.service.ts @@ -1,11 +1,11 @@ import { inject, Injectable } from '@angular/core'; import { JsonApiService } from '@core/services/json-api/json-api.service'; import { JsonApiResponse } from '@osf/core/services/json-api/json-api.entity'; -import { UserUS } from '@core/services/json-api/underscore-entites/user/user-us.entity'; import { ProfileSettingsStateModel, ProfileSettingsUpdate, } from '@osf/features/settings/profile-settings/profile-settings.entities'; +import { UserGetResponse } from '@core/services/user/user.models'; @Injectable({ providedIn: 'root', @@ -20,7 +20,7 @@ export class ProfileSettingsApiService { data: ProfileSettingsUpdate, ) { const patchedData = { [key]: data }; - return this.#jsonApiService.patch>( + return this.#jsonApiService.patch>( `${this.#baseUrl}users/${userId}/`, { data: { type: 'users', id: userId, attributes: patchedData } }, ); diff --git a/src/app/features/settings/profile-settings/profile-settings.entities.ts b/src/app/features/settings/profile-settings/profile-settings.entities.ts index 3999db85e..3a539c494 100644 --- a/src/app/features/settings/profile-settings/profile-settings.entities.ts +++ b/src/app/features/settings/profile-settings/profile-settings.entities.ts @@ -1,6 +1,6 @@ import { Employment } from '@osf/features/settings/profile-settings/employment/employment.entities'; import { Education } from '@osf/features/settings/profile-settings/education/educations.entities'; -import { User } from '@core/services/user/user.entity'; +import { User } from '@core/services/user/user.models'; import { Social } from '@osf/features/settings/profile-settings/social/social.entities'; export const PROFILE_SETTINGS_STATE_NAME = 'profileSettings'; diff --git a/src/app/features/settings/profile-settings/profile-settings.selectors.ts b/src/app/features/settings/profile-settings/profile-settings.selectors.ts index f9e0acef7..d8bfe581d 100644 --- a/src/app/features/settings/profile-settings/profile-settings.selectors.ts +++ b/src/app/features/settings/profile-settings/profile-settings.selectors.ts @@ -3,7 +3,7 @@ import { ProfileSettingsState } from '@osf/features/settings/profile-settings/pr import { Education } from '@osf/features/settings/profile-settings/education/educations.entities'; import { ProfileSettingsStateModel } from '@osf/features/settings/profile-settings/profile-settings.entities'; import { Employment } from '@osf/features/settings/profile-settings/employment/employment.entities'; -import { User } from '@core/services/user/user.entity'; +import { User } from '@core/services/user/user.models'; import { Social } from '@osf/features/settings/profile-settings/social/social.entities'; export class ProfileSettingsSelectors { From 4b48001a9722a40732fa0287a62439d54a33a6d8 Mon Sep 17 00:00:00 2001 From: Roma Date: Fri, 9 May 2025 16:47:06 +0300 Subject: [PATCH 02/14] feat(notifications): updating email preferences --- src/app/core/services/user/user.models.ts | 13 +++- src/app/core/services/user/user.service.ts | 25 ++++++- src/app/core/services/user/users.mapper.ts | 17 +++++ src/app/core/store/user/index.ts | 3 + src/app/core/store/user/user.actions.ts | 8 +++ src/app/core/store/user/user.selectors.ts | 3 +- src/app/core/store/user/user.state.ts | 25 +++++-- .../notifications-form.entities.ts | 11 +++ .../notifications.component.html | 19 ++++-- .../notifications/notifications.component.ts | 67 ++++++++++++++++--- .../profile-settings/name/name.entities.ts | 1 + .../profile-settings.actions.ts | 4 +- .../profile-settings.entities.ts | 4 +- 13 files changed, 174 insertions(+), 26 deletions(-) create mode 100644 src/app/features/settings/notifications/notifications-form.entities.ts diff --git a/src/app/core/services/user/user.models.ts b/src/app/core/services/user/user.models.ts index e98deb5f1..2ef25b9ff 100644 --- a/src/app/core/services/user/user.models.ts +++ b/src/app/core/services/user/user.models.ts @@ -59,9 +59,20 @@ export interface UserGetResponse { export interface UserSettingsGetResponse { id: string; - type: string; + type: "user_settings"; attributes: { subscribe_osf_general_email: boolean; subscribe_osf_help_email: boolean; }; } + +export interface UserSettingsUpdateRequest { + data: { + id: string; + type: "user_settings"; + attributes: { + subscribe_osf_general_email: boolean; + subscribe_osf_help_email: boolean; + }; + } +} diff --git a/src/app/core/services/user/user.service.ts b/src/app/core/services/user/user.service.ts index 2d9501af9..0b2d00e0d 100644 --- a/src/app/core/services/user/user.service.ts +++ b/src/app/core/services/user/user.service.ts @@ -1,5 +1,8 @@ -import { inject, Injectable } from '@angular/core'; import { map, Observable } from 'rxjs'; + +import { inject, Injectable } from '@angular/core'; + +import { JsonApiResponse } from '@core/services/json-api/json-api.entity'; import { JsonApiService } from '@core/services/json-api/json-api.service'; import { User, @@ -7,7 +10,6 @@ import { UserSettings, UserSettingsGetResponse, } from '@core/services/user/user.models'; -import { JsonApiResponse } from '@core/services/json-api/json-api.entity'; import { UserMapper } from '@core/services/user/users.mapper'; @Injectable({ @@ -34,4 +36,23 @@ export class UserService { ), ); } + + updateUserSettings( + userId: string, + userSettings: UserSettings, + ): Observable { + const request = UserMapper.toUpdateUserSettingsRequest( + userId, + userSettings, + ); + + return this.jsonApiService + .patch( + this.baseUrl + `users/${userId}/settings/`, + request, + ) + .pipe( + map((response) => UserMapper.fromUserSettingsGetResponse(response)), + ); + } } diff --git a/src/app/core/services/user/users.mapper.ts b/src/app/core/services/user/users.mapper.ts index 30ec0fc01..37e38a01c 100644 --- a/src/app/core/services/user/users.mapper.ts +++ b/src/app/core/services/user/users.mapper.ts @@ -3,6 +3,7 @@ import { UserGetResponse, UserSettings, UserSettingsGetResponse, + UserSettingsUpdateRequest, } from '@core/services/user/user.models'; export class UserMapper { @@ -34,4 +35,20 @@ export class UserMapper { userSettingsResponse.attributes.subscribe_osf_help_email, }; } + + static toUpdateUserSettingsRequest( + userId: string, + userSettings: UserSettings, + ): UserSettingsUpdateRequest { + return { + data: { + id: userId, + type: 'user_settings', + attributes: { + subscribe_osf_general_email: userSettings.subscribeOsfGeneralEmail, + subscribe_osf_help_email: userSettings.subscribeOsfHelpEmail, + }, + }, + }; + } } diff --git a/src/app/core/store/user/index.ts b/src/app/core/store/user/index.ts index e69de29bb..e8184d398 100644 --- a/src/app/core/store/user/index.ts +++ b/src/app/core/store/user/index.ts @@ -0,0 +1,3 @@ +export * from './user.actions'; +export * from './user.state'; +export * from './user.state-model'; diff --git a/src/app/core/store/user/user.actions.ts b/src/app/core/store/user/user.actions.ts index e99cad14e..83cd4f901 100644 --- a/src/app/core/store/user/user.actions.ts +++ b/src/app/core/store/user/user.actions.ts @@ -1,3 +1,5 @@ +import { UserSettings } from '@core/services/user/user.models'; + export class GetCurrentUser { static readonly type = '[User] Get Current User'; } @@ -5,3 +7,9 @@ export class GetCurrentUser { export class GetCurrentUserSettings { static readonly type = '[User] Get Current User Settings'; } + +export class UpdateUserSettings { + static readonly type = '[User] Update User Settings'; + + constructor(public userId: string, public updatedUserSettings: UserSettings) {} +} diff --git a/src/app/core/store/user/user.selectors.ts b/src/app/core/store/user/user.selectors.ts index fd79751f5..98cfde1e6 100644 --- a/src/app/core/store/user/user.selectors.ts +++ b/src/app/core/store/user/user.selectors.ts @@ -1,7 +1,8 @@ import { Selector } from '@ngxs/store'; -import { UserStateModel } from '@core/store/user/user.state-model'; + import { User, UserSettings } from '@core/services/user/user.models'; import { UserState } from '@core/store/user/user.state'; +import { UserStateModel } from '@core/store/user/user.state-model'; import { ProfileSettingsStateModel } from '@osf/features/settings/profile-settings/profile-settings.entities'; import { Social } from '@osf/features/settings/profile-settings/social/social.entities'; diff --git a/src/app/core/store/user/user.state.ts b/src/app/core/store/user/user.state.ts index 49b199293..750851e04 100644 --- a/src/app/core/store/user/user.state.ts +++ b/src/app/core/store/user/user.state.ts @@ -1,11 +1,15 @@ -import { Injectable, inject } from '@angular/core'; -import { State, Action, StateContext } from '@ngxs/store'; -import { UserStateModel } from './user.state-model'; -import { GetCurrentUser, GetCurrentUserSettings } from './user.actions'; -import { UserService } from '@core/services/user/user.service'; +import { Action, State, StateContext } from '@ngxs/store'; + import { tap } from 'rxjs'; + +import { inject,Injectable } from '@angular/core'; + +import { UserService } from '@core/services/user/user.service'; import { SetupProfileSettings } from '@osf/features/settings/profile-settings/profile-settings.actions'; +import { GetCurrentUser, GetCurrentUserSettings, UpdateUserSettings } from './user.actions'; +import { UserStateModel } from './user.state-model'; + @State({ name: 'user', defaults: { @@ -39,4 +43,15 @@ export class UserState { }), ); } + + @Action(UpdateUserSettings) + updateUserSettings(ctx: StateContext, action: UpdateUserSettings) { + return this.userService.updateUserSettings(action.userId, action.updatedUserSettings).pipe( + tap((userSettings) => { + ctx.patchState({ + currentUserSettings: userSettings, + }); + }), + ); + } } diff --git a/src/app/features/settings/notifications/notifications-form.entities.ts b/src/app/features/settings/notifications/notifications-form.entities.ts new file mode 100644 index 000000000..4d7df9899 --- /dev/null +++ b/src/app/features/settings/notifications/notifications-form.entities.ts @@ -0,0 +1,11 @@ +import { FormControl, FormGroup } from '@angular/forms'; + +export enum EmailPreferencesFormControls { + SubscribeOsfGeneralEmail = 'subscribeOsfGeneralEmail', + SubscribeOsfHelpEmail = 'subscribeOsfHelpEmail', +} + +export type EmailPreferencesForm = FormGroup<{ + [EmailPreferencesFormControls.SubscribeOsfGeneralEmail]: FormControl; + [EmailPreferencesFormControls.SubscribeOsfHelpEmail]: FormControl; +}>; diff --git a/src/app/features/settings/notifications/notifications.component.html b/src/app/features/settings/notifications/notifications.component.html index 8fc3bc434..0df465f71 100644 --- a/src/app/features/settings/notifications/notifications.component.html +++ b/src/app/features/settings/notifications/notifications.component.html @@ -8,9 +8,14 @@

{{ "settings.notifications.emailPreferences.title" | translate }}

-
+
- + +
- + +
+
diff --git a/src/app/features/settings/notifications/notifications.component.ts b/src/app/features/settings/notifications/notifications.component.ts index 1529cd74e..b0f64fec2 100644 --- a/src/app/features/settings/notifications/notifications.component.ts +++ b/src/app/features/settings/notifications/notifications.component.ts @@ -1,3 +1,11 @@ +import { Store } from '@ngxs/store'; + +import { TranslatePipe } from '@ngx-translate/core'; + +import { Button } from 'primeng/button'; +import { Checkbox } from 'primeng/checkbox'; +import { DropdownModule } from 'primeng/dropdown'; + import { ChangeDetectionStrategy, Component, @@ -6,14 +14,16 @@ import { inject, OnInit, } from '@angular/core'; -import { SubHeaderComponent } from '@shared/components/sub-header/sub-header.component'; -import { Checkbox } from 'primeng/checkbox'; -import { Button } from 'primeng/button'; -import { DropdownModule } from 'primeng/dropdown'; -import { TranslatePipe } from '@ngx-translate/core'; -import { Store } from '@ngxs/store'; -import { GetCurrentUserSettings } from '@core/store/user'; +import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; + +import { UserSettings } from '@core/services/user/user.models'; +import { GetCurrentUserSettings, UpdateUserSettings } from '@core/store/user/user.actions'; import { UserSelectors } from '@core/store/user/user.selectors'; +import { + EmailPreferencesForm, + EmailPreferencesFormControls, +} from '@osf/features/settings/notifications/notifications-form.entities'; +import { SubHeaderComponent } from '@shared/components/sub-header/sub-header.component'; @Component({ selector: 'osf-notifications', @@ -24,6 +34,7 @@ import { UserSelectors } from '@core/store/user/user.selectors'; Button, DropdownModule, TranslatePipe, + ReactiveFormsModule, ], templateUrl: './notifications.component.html', styleUrl: './notifications.component.scss', @@ -33,17 +44,57 @@ export class NotificationsComponent implements OnInit { @HostBinding('class') classes = 'flex flex-1 flex-column'; readonly #store = inject(Store); + readonly #fb = inject(FormBuilder); + + readonly #currentUser = this.#store.selectSignal( + UserSelectors.getCurrentUser, + ); readonly #userSettings = this.#store.selectSignal( UserSelectors.getCurrentUserSettings, ); + protected readonly EmailPreferencesFormControls = + EmailPreferencesFormControls; + protected readonly emailPreferencesForm: EmailPreferencesForm = new FormGroup( + { + [EmailPreferencesFormControls.SubscribeOsfGeneralEmail]: this.#fb.control( + false, + { nonNullable: true }, + ), + [EmailPreferencesFormControls.SubscribeOsfHelpEmail]: this.#fb.control( + false, + { nonNullable: true }, + ), + }, + ); + constructor() { effect(() => { - console.log('Current user settings:', this.#userSettings()); + if (this.#userSettings()) { + this.updateEmailPreferencesForm(); + } }); } ngOnInit(): void { this.#store.dispatch(new GetCurrentUserSettings()); } + + emailPreferencesFormSubmit(): void { + if(!this.#currentUser()) { + return; + } + + const formValue = this.emailPreferencesForm.value as UserSettings; + this.#store.dispatch(new UpdateUserSettings(this.#currentUser()!.id, formValue)); + } + + private updateEmailPreferencesForm() { + this.emailPreferencesForm.patchValue({ + [EmailPreferencesFormControls.SubscribeOsfGeneralEmail]: + this.#userSettings()?.subscribeOsfGeneralEmail, + [EmailPreferencesFormControls.SubscribeOsfHelpEmail]: + this.#userSettings()?.subscribeOsfHelpEmail, + }); + } } diff --git a/src/app/features/settings/profile-settings/name/name.entities.ts b/src/app/features/settings/profile-settings/name/name.entities.ts index e0180a138..f3c67beeb 100644 --- a/src/app/features/settings/profile-settings/name/name.entities.ts +++ b/src/app/features/settings/profile-settings/name/name.entities.ts @@ -1,6 +1,7 @@ import { FormControl } from '@angular/forms'; import { User } from '@core/services/user/user.models'; + export interface Name { fullName: string; givenName: string; diff --git a/src/app/features/settings/profile-settings/profile-settings.actions.ts b/src/app/features/settings/profile-settings/profile-settings.actions.ts index a0ad39c3d..d4a998d53 100644 --- a/src/app/features/settings/profile-settings/profile-settings.actions.ts +++ b/src/app/features/settings/profile-settings/profile-settings.actions.ts @@ -1,6 +1,6 @@ -import { Employment } from '@osf/features/settings/profile-settings/employment/employment.entities'; -import { Education } from '@osf/features/settings/profile-settings/education/educations.entities'; import { User } from '@core/services/user/user.models'; +import { Education } from '@osf/features/settings/profile-settings/education/educations.entities'; +import { Employment } from '@osf/features/settings/profile-settings/employment/employment.entities'; import { Social } from '@osf/features/settings/profile-settings/social/social.entities'; export class SetupProfileSettings { diff --git a/src/app/features/settings/profile-settings/profile-settings.entities.ts b/src/app/features/settings/profile-settings/profile-settings.entities.ts index 3a539c494..0fae11e0b 100644 --- a/src/app/features/settings/profile-settings/profile-settings.entities.ts +++ b/src/app/features/settings/profile-settings/profile-settings.entities.ts @@ -1,6 +1,6 @@ -import { Employment } from '@osf/features/settings/profile-settings/employment/employment.entities'; -import { Education } from '@osf/features/settings/profile-settings/education/educations.entities'; import { User } from '@core/services/user/user.models'; +import { Education } from '@osf/features/settings/profile-settings/education/educations.entities'; +import { Employment } from '@osf/features/settings/profile-settings/employment/employment.entities'; import { Social } from '@osf/features/settings/profile-settings/social/social.entities'; export const PROFILE_SETTINGS_STATE_NAME = 'profileSettings'; From 1d00fa9b64f4c3bdfdce7fce03511b2e3837e831 Mon Sep 17 00:00:00 2001 From: Roma Date: Fri, 16 May 2025 11:44:18 +0300 Subject: [PATCH 03/14] fix(app-component): removed unused field --- src/app/app.component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 9c0c95b99..c534ef9ef 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -14,7 +14,6 @@ import { GetCurrentUser } from '@core/store/user/user.actions'; }) export class AppComponent implements OnInit { #store = inject(Store); - title = 'osf'; ngOnInit(): void { this.#store.dispatch(GetCurrentUser); From d2aec9743be6b449ff243c9790ad977895263f7c Mon Sep 17 00:00:00 2001 From: Roma Date: Fri, 16 May 2025 15:24:21 +0300 Subject: [PATCH 04/14] feat(settings/notifications): added models, enums, service --- .../settings/notifications/enums/index.ts | 1 + .../enums/notification-subscription.enums.ts | 13 +++++++++++++ .../mappers/notification-subscription.mapper.ts | 0 .../settings/notifications/models/index.ts | 1 + .../models/notification-subscription.models.ts | 17 +++++++++++++++++ .../notifications-form.models.ts} | 0 .../notifications/notifications.component.ts | 2 +- .../settings/notifications/service/index.ts | 1 + .../service/notifications.service.ts | 11 +++++++++++ 9 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 src/app/features/settings/notifications/enums/index.ts create mode 100644 src/app/features/settings/notifications/enums/notification-subscription.enums.ts create mode 100644 src/app/features/settings/notifications/mappers/notification-subscription.mapper.ts create mode 100644 src/app/features/settings/notifications/models/index.ts create mode 100644 src/app/features/settings/notifications/models/notification-subscription.models.ts rename src/app/features/settings/notifications/{notifications-form.entities.ts => models/notifications-form.models.ts} (100%) create mode 100644 src/app/features/settings/notifications/service/index.ts create mode 100644 src/app/features/settings/notifications/service/notifications.service.ts diff --git a/src/app/features/settings/notifications/enums/index.ts b/src/app/features/settings/notifications/enums/index.ts new file mode 100644 index 000000000..9500d582d --- /dev/null +++ b/src/app/features/settings/notifications/enums/index.ts @@ -0,0 +1 @@ +export * from './notification-subscription.enums'; diff --git a/src/app/features/settings/notifications/enums/notification-subscription.enums.ts b/src/app/features/settings/notifications/enums/notification-subscription.enums.ts new file mode 100644 index 000000000..4d5cb1939 --- /dev/null +++ b/src/app/features/settings/notifications/enums/notification-subscription.enums.ts @@ -0,0 +1,13 @@ +export enum SubscriptionEvent { + GlobalCommentReplies = 'global_comment_replies', + GlobalComments = 'global_comments', + GlobalFileUpdated = 'global_file_updated', + GlobalMentions = 'global_mentions', + GlobalReviews = 'global_reviews', +} + +export enum SubscriptionFrequency { + None = 'none', + Daily = 'daily', + Instant = 'instant', +} diff --git a/src/app/features/settings/notifications/mappers/notification-subscription.mapper.ts b/src/app/features/settings/notifications/mappers/notification-subscription.mapper.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/settings/notifications/models/index.ts b/src/app/features/settings/notifications/models/index.ts new file mode 100644 index 000000000..d0d774f09 --- /dev/null +++ b/src/app/features/settings/notifications/models/index.ts @@ -0,0 +1 @@ +export * from './notification-subscription.models'; diff --git a/src/app/features/settings/notifications/models/notification-subscription.models.ts b/src/app/features/settings/notifications/models/notification-subscription.models.ts new file mode 100644 index 000000000..5e3734d14 --- /dev/null +++ b/src/app/features/settings/notifications/models/notification-subscription.models.ts @@ -0,0 +1,17 @@ +import { SubscriptionEvent, SubscriptionFrequency } from '@osf/features/settings/notifications/enums'; + +//domain models +export interface NotificationSubscription { + event: SubscriptionEvent; + frequency: SubscriptionFrequency; +} + +//api models +interface NotificationSubscriptionGetResponse { + id: string; + type: 'subscription'; + attributes: { + event_name: string; + frequency: string; + }; +} diff --git a/src/app/features/settings/notifications/notifications-form.entities.ts b/src/app/features/settings/notifications/models/notifications-form.models.ts similarity index 100% rename from src/app/features/settings/notifications/notifications-form.entities.ts rename to src/app/features/settings/notifications/models/notifications-form.models.ts diff --git a/src/app/features/settings/notifications/notifications.component.ts b/src/app/features/settings/notifications/notifications.component.ts index b502d9936..83cc0b2a5 100644 --- a/src/app/features/settings/notifications/notifications.component.ts +++ b/src/app/features/settings/notifications/notifications.component.ts @@ -15,7 +15,7 @@ import { UserSelectors } from '@core/store/user/user.selectors'; import { EmailPreferencesForm, EmailPreferencesFormControls, -} from '@osf/features/settings/notifications/notifications-form.entities'; +} from '@osf/features/settings/notifications/models/notifications-form.models'; import { SubHeaderComponent } from '@shared/components/sub-header/sub-header.component'; @Component({ diff --git a/src/app/features/settings/notifications/service/index.ts b/src/app/features/settings/notifications/service/index.ts new file mode 100644 index 000000000..9427f824d --- /dev/null +++ b/src/app/features/settings/notifications/service/index.ts @@ -0,0 +1 @@ +export * from './notifications.service'; diff --git a/src/app/features/settings/notifications/service/notifications.service.ts b/src/app/features/settings/notifications/service/notifications.service.ts new file mode 100644 index 000000000..3fc7cc70a --- /dev/null +++ b/src/app/features/settings/notifications/service/notifications.service.ts @@ -0,0 +1,11 @@ +import { inject, Injectable } from '@angular/core'; + +import { JsonApiService } from '@core/services/json-api/json-api.service'; + +@Injectable({ + providedIn: 'root', +}) +export class NotificationsService { + jsonApiService = inject(JsonApiService); + baseUrl = 'https://api.staging4.osf.io/v2/subscriptions/'; +} From ea3dd31c6180a333dc5b5451244bee78bebf78a8 Mon Sep 17 00:00:00 2001 From: Roma Date: Mon, 19 May 2025 15:46:25 +0300 Subject: [PATCH 05/14] feat(notifications): added store --- .../notifications/notifications.component.ts | 14 +++++++++ .../settings/notifications/service/index.ts | 2 +- .../notification-subscription.service.ts | 19 ++++++++++++ .../service/notifications.service.ts | 11 ------- .../settings/notifications/store/index.ts | 4 +++ .../notification-subscription.actions.ts | 3 ++ .../notification-subscription.selectors.ts | 12 ++++++++ .../notification-subscription.state-model.ts | 5 ++++ .../store/notification-subscription.state.ts | 30 +++++++++++++++++++ 9 files changed, 88 insertions(+), 12 deletions(-) create mode 100644 src/app/features/settings/notifications/service/notification-subscription.service.ts delete mode 100644 src/app/features/settings/notifications/service/notifications.service.ts create mode 100644 src/app/features/settings/notifications/store/index.ts create mode 100644 src/app/features/settings/notifications/store/notification-subscription.actions.ts create mode 100644 src/app/features/settings/notifications/store/notification-subscription.selectors.ts create mode 100644 src/app/features/settings/notifications/store/notification-subscription.state-model.ts create mode 100644 src/app/features/settings/notifications/store/notification-subscription.state.ts diff --git a/src/app/features/settings/notifications/notifications.component.ts b/src/app/features/settings/notifications/notifications.component.ts index 83cc0b2a5..3c196cfb8 100644 --- a/src/app/features/settings/notifications/notifications.component.ts +++ b/src/app/features/settings/notifications/notifications.component.ts @@ -16,6 +16,10 @@ import { EmailPreferencesForm, EmailPreferencesFormControls, } from '@osf/features/settings/notifications/models/notifications-form.models'; +import { + GetAllGlobalNotificationSubscriptions, + NotificationSubscriptionSelectors, +} from '@osf/features/settings/notifications/store'; import { SubHeaderComponent } from '@shared/components/sub-header/sub-header.component'; @Component({ @@ -35,6 +39,9 @@ export class NotificationsComponent implements OnInit { readonly #currentUser = this.#store.selectSignal(UserSelectors.getCurrentUser); readonly #userSettings = this.#store.selectSignal(UserSelectors.getCurrentUserSettings); + readonly #notificationSubscriptions = this.#store.selectSignal( + NotificationSubscriptionSelectors.getAllGlobalNotificationSubscriptions + ); protected readonly EmailPreferencesFormControls = EmailPreferencesFormControls; protected readonly emailPreferencesForm: EmailPreferencesForm = new FormGroup({ [EmailPreferencesFormControls.SubscribeOsfGeneralEmail]: this.#fb.control(false, { nonNullable: true }), @@ -47,10 +54,17 @@ export class NotificationsComponent implements OnInit { this.updateEmailPreferencesForm(); } }); + + effect(() => { + if (this.#notificationSubscriptions()) { + //[RN] TODO: set form + } + }); } ngOnInit(): void { this.#store.dispatch(new GetCurrentUserSettings()); + this.#store.dispatch(new GetAllGlobalNotificationSubscriptions()); } emailPreferencesFormSubmit(): void { diff --git a/src/app/features/settings/notifications/service/index.ts b/src/app/features/settings/notifications/service/index.ts index 9427f824d..3796c7c94 100644 --- a/src/app/features/settings/notifications/service/index.ts +++ b/src/app/features/settings/notifications/service/index.ts @@ -1 +1 @@ -export * from './notifications.service'; +export * from './notification-subscription.service'; diff --git a/src/app/features/settings/notifications/service/notification-subscription.service.ts b/src/app/features/settings/notifications/service/notification-subscription.service.ts new file mode 100644 index 000000000..ca75932ca --- /dev/null +++ b/src/app/features/settings/notifications/service/notification-subscription.service.ts @@ -0,0 +1,19 @@ +import { Observable } from 'rxjs'; + +import { inject, Injectable } from '@angular/core'; + +import { JsonApiService } from '@core/services/json-api/json-api.service'; +import { NotificationSubscription } from '@osf/features/settings/notifications/models'; + +@Injectable({ + providedIn: 'root', +}) +export class NotificationSubscriptionService { + jsonApiService = inject(JsonApiService); + baseUrl = 'https://api.staging4.osf.io/v2/subscriptions/'; + + getAllGlobalNotificationSubscriptions(): Observable { + //[RN] TODO: add mapper add filter params + return this.jsonApiService.get(this.baseUrl); + } +} diff --git a/src/app/features/settings/notifications/service/notifications.service.ts b/src/app/features/settings/notifications/service/notifications.service.ts deleted file mode 100644 index 3fc7cc70a..000000000 --- a/src/app/features/settings/notifications/service/notifications.service.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { inject, Injectable } from '@angular/core'; - -import { JsonApiService } from '@core/services/json-api/json-api.service'; - -@Injectable({ - providedIn: 'root', -}) -export class NotificationsService { - jsonApiService = inject(JsonApiService); - baseUrl = 'https://api.staging4.osf.io/v2/subscriptions/'; -} diff --git a/src/app/features/settings/notifications/store/index.ts b/src/app/features/settings/notifications/store/index.ts new file mode 100644 index 000000000..d36c5fcf8 --- /dev/null +++ b/src/app/features/settings/notifications/store/index.ts @@ -0,0 +1,4 @@ +export * from './notification-subscription.actions'; +export * from './notification-subscription.selectors'; +export * from './notification-subscription.state'; +export * from './notification-subscription.state-model'; diff --git a/src/app/features/settings/notifications/store/notification-subscription.actions.ts b/src/app/features/settings/notifications/store/notification-subscription.actions.ts new file mode 100644 index 000000000..78e9d1d7c --- /dev/null +++ b/src/app/features/settings/notifications/store/notification-subscription.actions.ts @@ -0,0 +1,3 @@ +export class GetAllGlobalNotificationSubscriptions { + static readonly type = '[Notification Subscriptions] Get All Global'; +} diff --git a/src/app/features/settings/notifications/store/notification-subscription.selectors.ts b/src/app/features/settings/notifications/store/notification-subscription.selectors.ts new file mode 100644 index 000000000..d7efb8c19 --- /dev/null +++ b/src/app/features/settings/notifications/store/notification-subscription.selectors.ts @@ -0,0 +1,12 @@ +import { Selector } from '@ngxs/store'; + +import { DeveloperAppsState } from '@osf/features/settings/developer-apps/store/developer-apps.state'; +import { NotificationSubscription } from '@osf/features/settings/notifications/models'; +import { NotificationSubscriptionStateModel } from '@osf/features/settings/notifications/store/notification-subscription.state-model'; + +export class NotificationSubscriptionSelectors { + @Selector([DeveloperAppsState]) + static getAllGlobalNotificationSubscriptions(state: NotificationSubscriptionStateModel): NotificationSubscription[] { + return state.notificationSubscriptions; + } +} diff --git a/src/app/features/settings/notifications/store/notification-subscription.state-model.ts b/src/app/features/settings/notifications/store/notification-subscription.state-model.ts new file mode 100644 index 000000000..4d950ffcb --- /dev/null +++ b/src/app/features/settings/notifications/store/notification-subscription.state-model.ts @@ -0,0 +1,5 @@ +import { NotificationSubscription } from '@osf/features/settings/notifications/models'; + +export interface NotificationSubscriptionStateModel { + notificationSubscriptions: NotificationSubscription[]; +} diff --git a/src/app/features/settings/notifications/store/notification-subscription.state.ts b/src/app/features/settings/notifications/store/notification-subscription.state.ts new file mode 100644 index 000000000..54e7bc359 --- /dev/null +++ b/src/app/features/settings/notifications/store/notification-subscription.state.ts @@ -0,0 +1,30 @@ +import { Action, State, StateContext } from '@ngxs/store'; +import { patch } from '@ngxs/store/operators'; + +import { tap } from 'rxjs'; + +import { inject, Injectable } from '@angular/core'; + +import { NotificationSubscriptionService } from '@osf/features/settings/notifications/service'; +import { GetAllGlobalNotificationSubscriptions } from '@osf/features/settings/notifications/store/notification-subscription.actions'; +import { NotificationSubscriptionStateModel } from '@osf/features/settings/notifications/store/notification-subscription.state-model'; + +@State({ + name: 'notificationSubscriptions', + defaults: { + notificationSubscriptions: [], + }, +}) +@Injectable() +export class NotificationSubscriptionState { + #notificationSubscriptionService = inject(NotificationSubscriptionService); + + @Action(GetAllGlobalNotificationSubscriptions) + getAllGlobalNotificationSubscriptions(ctx: StateContext) { + return this.#notificationSubscriptionService.getAllGlobalNotificationSubscriptions().pipe( + tap((notificationSubscriptions) => { + ctx.setState(patch({ notificationSubscriptions })); + }) + ); + } +} From dae7b0a108a013ccd6870e8ad03368e86052e324 Mon Sep 17 00:00:00 2001 From: Roma Date: Tue, 20 May 2025 14:30:55 +0300 Subject: [PATCH 06/14] refactor(notifications): update exports and splitted subscription enums --- src/app/core/constants/ngxs-states.constant.ts | 2 ++ src/app/core/store/user/index.ts | 1 + src/app/features/settings/notifications/enums/index.ts | 3 ++- ...ion-subscription.enums.ts => subscription-event.enum.ts} | 6 ------ .../notifications/enums/subscription-frequency.enum.ts | 5 +++++ src/app/features/settings/notifications/mappers/index.ts | 1 + src/app/features/settings/notifications/models/index.ts | 1 + .../settings/notifications/{service => services}/index.ts | 0 8 files changed, 12 insertions(+), 7 deletions(-) rename src/app/features/settings/notifications/enums/{notification-subscription.enums.ts => subscription-event.enum.ts} (71%) create mode 100644 src/app/features/settings/notifications/enums/subscription-frequency.enum.ts create mode 100644 src/app/features/settings/notifications/mappers/index.ts rename src/app/features/settings/notifications/{service => services}/index.ts (100%) diff --git a/src/app/core/constants/ngxs-states.constant.ts b/src/app/core/constants/ngxs-states.constant.ts index 155f99579..374de8ffd 100644 --- a/src/app/core/constants/ngxs-states.constant.ts +++ b/src/app/core/constants/ngxs-states.constant.ts @@ -6,6 +6,7 @@ import { SearchState } from '@osf/features/search/store'; import { AccountSettingsState } from '@osf/features/settings/account-settings/store/account-settings.state'; import { AddonsState } from '@osf/features/settings/addons/store'; import { DeveloperAppsState } from '@osf/features/settings/developer-apps/store'; +import { NotificationSubscriptionState } from '@osf/features/settings/notifications/store'; import { ProfileSettingsState } from '@osf/features/settings/profile-settings/profile-settings.state'; import { TokensState } from '@osf/features/settings/tokens/store'; @@ -20,4 +21,5 @@ export const STATES = [ ProfileSettingsState, DeveloperAppsState, AccountSettingsState, + NotificationSubscriptionState, ]; diff --git a/src/app/core/store/user/index.ts b/src/app/core/store/user/index.ts index e8184d398..10669f497 100644 --- a/src/app/core/store/user/index.ts +++ b/src/app/core/store/user/index.ts @@ -1,3 +1,4 @@ export * from './user.actions'; +export * from './user.selectors'; export * from './user.state'; export * from './user.state-model'; diff --git a/src/app/features/settings/notifications/enums/index.ts b/src/app/features/settings/notifications/enums/index.ts index 9500d582d..655cab199 100644 --- a/src/app/features/settings/notifications/enums/index.ts +++ b/src/app/features/settings/notifications/enums/index.ts @@ -1 +1,2 @@ -export * from './notification-subscription.enums'; +export * from './subscription-event.enum'; +export * from './subscription-frequency.enum'; diff --git a/src/app/features/settings/notifications/enums/notification-subscription.enums.ts b/src/app/features/settings/notifications/enums/subscription-event.enum.ts similarity index 71% rename from src/app/features/settings/notifications/enums/notification-subscription.enums.ts rename to src/app/features/settings/notifications/enums/subscription-event.enum.ts index 4d5cb1939..011f639a9 100644 --- a/src/app/features/settings/notifications/enums/notification-subscription.enums.ts +++ b/src/app/features/settings/notifications/enums/subscription-event.enum.ts @@ -5,9 +5,3 @@ export enum SubscriptionEvent { GlobalMentions = 'global_mentions', GlobalReviews = 'global_reviews', } - -export enum SubscriptionFrequency { - None = 'none', - Daily = 'daily', - Instant = 'instant', -} diff --git a/src/app/features/settings/notifications/enums/subscription-frequency.enum.ts b/src/app/features/settings/notifications/enums/subscription-frequency.enum.ts new file mode 100644 index 000000000..2c9f96421 --- /dev/null +++ b/src/app/features/settings/notifications/enums/subscription-frequency.enum.ts @@ -0,0 +1,5 @@ +export enum SubscriptionFrequency { + Never = 'none', + Daily = 'daily', + Instant = 'instant', +} diff --git a/src/app/features/settings/notifications/mappers/index.ts b/src/app/features/settings/notifications/mappers/index.ts new file mode 100644 index 000000000..a10b45bdd --- /dev/null +++ b/src/app/features/settings/notifications/mappers/index.ts @@ -0,0 +1 @@ +export * from './notification-subscription.mapper'; diff --git a/src/app/features/settings/notifications/models/index.ts b/src/app/features/settings/notifications/models/index.ts index d0d774f09..0f9926c54 100644 --- a/src/app/features/settings/notifications/models/index.ts +++ b/src/app/features/settings/notifications/models/index.ts @@ -1 +1,2 @@ export * from './notification-subscription.models'; +export * from './notifications-form.models'; diff --git a/src/app/features/settings/notifications/service/index.ts b/src/app/features/settings/notifications/services/index.ts similarity index 100% rename from src/app/features/settings/notifications/service/index.ts rename to src/app/features/settings/notifications/services/index.ts From bd1aea1f23337f8902680aac4fee023e93ca6a67 Mon Sep 17 00:00:00 2001 From: Roma Date: Tue, 20 May 2025 14:32:23 +0300 Subject: [PATCH 07/14] feat(notifications): Implemented update notification subscription functionality --- src/app/core/store/user/user.state.ts | 4 +- .../notification-subscription.mapper.ts | 28 +++ .../notification-subscription.models.ts | 13 +- .../notifications.component.html | 185 ++++++++---------- .../notifications/notifications.component.ts | 140 ++++++++++--- .../notification-subscription.service.ts | 19 -- .../notification-subscription.service.ts | 42 ++++ .../notification-subscription.actions.ts | 8 + .../notification-subscription.selectors.ts | 4 +- .../store/notification-subscription.state.ts | 29 ++- 10 files changed, 313 insertions(+), 159 deletions(-) delete mode 100644 src/app/features/settings/notifications/service/notification-subscription.service.ts create mode 100644 src/app/features/settings/notifications/services/notification-subscription.service.ts diff --git a/src/app/core/store/user/user.state.ts b/src/app/core/store/user/user.state.ts index 9c4ed46c3..c0f5b5c2f 100644 --- a/src/app/core/store/user/user.state.ts +++ b/src/app/core/store/user/user.state.ts @@ -54,9 +54,9 @@ export class UserState { @Action(UpdateUserSettings) updateUserSettings(ctx: StateContext, action: UpdateUserSettings) { return this.userService.updateUserSettings(action.userId, action.updatedUserSettings).pipe( - tap((userSettings) => { + tap(() => { ctx.patchState({ - currentUserSettings: userSettings, + currentUserSettings: action.updatedUserSettings, }); }) ); diff --git a/src/app/features/settings/notifications/mappers/notification-subscription.mapper.ts b/src/app/features/settings/notifications/mappers/notification-subscription.mapper.ts index e69de29bb..202ecc431 100644 --- a/src/app/features/settings/notifications/mappers/notification-subscription.mapper.ts +++ b/src/app/features/settings/notifications/mappers/notification-subscription.mapper.ts @@ -0,0 +1,28 @@ +import { SubscriptionEvent, SubscriptionFrequency } from '@osf/features/settings/notifications/enums'; +import { + NotificationSubscription, + NotificationSubscriptionGetResponse, + NotificationSubscriptionUpdateRequest, +} from '@osf/features/settings/notifications/models'; + +export class NotificationSubscriptionMapper { + static fromGetResponse(response: NotificationSubscriptionGetResponse): NotificationSubscription { + return { + id: response.id, + event: response.attributes.event_name as SubscriptionEvent, + frequency: response.attributes.frequency as SubscriptionFrequency, + }; + } + + static toUpdateRequest(id: string, frequency: SubscriptionFrequency): NotificationSubscriptionUpdateRequest { + return { + data: { + id: id, + attributes: { + frequency: frequency, + }, + type: 'subscription', + }, + }; + } +} diff --git a/src/app/features/settings/notifications/models/notification-subscription.models.ts b/src/app/features/settings/notifications/models/notification-subscription.models.ts index 5e3734d14..65842edda 100644 --- a/src/app/features/settings/notifications/models/notification-subscription.models.ts +++ b/src/app/features/settings/notifications/models/notification-subscription.models.ts @@ -2,12 +2,13 @@ import { SubscriptionEvent, SubscriptionFrequency } from '@osf/features/settings //domain models export interface NotificationSubscription { + id: string; event: SubscriptionEvent; frequency: SubscriptionFrequency; } //api models -interface NotificationSubscriptionGetResponse { +export interface NotificationSubscriptionGetResponse { id: string; type: 'subscription'; attributes: { @@ -15,3 +16,13 @@ interface NotificationSubscriptionGetResponse { frequency: string; }; } + +export interface NotificationSubscriptionUpdateRequest { + data: { + id: string; + type: 'subscription'; + attributes: { + frequency: SubscriptionFrequency; + }; + }; +} diff --git a/src/app/features/settings/notifications/notifications.component.html b/src/app/features/settings/notifications/notifications.component.html index 53dfbdcf2..d36bedbb4 100644 --- a/src/app/features/settings/notifications/notifications.component.html +++ b/src/app/features/settings/notifications/notifications.component.html @@ -1,125 +1,106 @@ + [icon]="'settings'"/> -
-
-

{{ "settings.notifications.emailPreferences.title" | translate }}

+
+ @if (!isEmailPreferencesLoading()) { +
+

{{ "settings.notifications.emailPreferences.title" | translate }}

-
-
- - + +
+ -
- +
+ -
- {{ - "settings.notifications.emailPreferences.items.general.description" - | translate - }} +
+ {{ "settings.notifications.emailPreferences.items.general.description"| translate }} +
-
-
- - +
+ -
- +
+ -
- {{ - "settings.notifications.emailPreferences.items.help.description" - | translate - }} +
+ {{ "settings.notifications.emailPreferences.items.help.description"| translate }} +
-
+
+ +
+ +
+ } @else { +
+ + +
+ @for (_ of [1, 2]; track $index) { +
+ + +
+ } +
- - +
- -
- -
-

- {{ "settings.notifications.notificationPreferences.title" | translate }} -

- -
- {{ "settings.notifications.notificationPreferences.note" | translate }}
+ } -
-
- {{ - "settings.notifications.notificationPreferences.items.replies" - | translate - }} -
- - -
- {{ - "settings.notifications.notificationPreferences.items.comments" - | translate - }} -
- + @if (!isNotificationSubscriptionsLoading()) { +
+

{{ "settings.notifications.notificationPreferences.title" | translate }}

-
- {{ - "settings.notifications.notificationPreferences.items.files" - | translate - }} -
- +

{{ "settings.notifications.notificationPreferences.note" | translate }}

-
- {{ - "settings.notifications.notificationPreferences.items.mentions" - | translate - }} -
- +
+ @for (subscriptionEvent of subscriptionItems; track $index) { +
+ {{ subscriptionEvent.labelKey | translate }} +
+ + } +
+
+ } @else { +
+ + -
- {{ - "settings.notifications.notificationPreferences.items.preprints" - | translate - }} -
- +
+ @for (_ of [1, 2, 3, 4, 5]; track $index) { + + + } +
-
-
+ } + diff --git a/src/app/features/settings/notifications/notifications.component.ts b/src/app/features/settings/notifications/notifications.component.ts index 3c196cfb8..b9b1f35ee 100644 --- a/src/app/features/settings/notifications/notifications.component.ts +++ b/src/app/features/settings/notifications/notifications.component.ts @@ -1,31 +1,30 @@ -import { Store } from '@ngxs/store'; +import { select, Store } from '@ngxs/store'; import { TranslatePipe } from '@ngx-translate/core'; import { Button } from 'primeng/button'; import { Checkbox } from 'primeng/checkbox'; import { DropdownModule } from 'primeng/dropdown'; +import { Skeleton } from 'primeng/skeleton'; -import { ChangeDetectionStrategy, Component, effect, HostBinding, inject, OnInit } from '@angular/core'; -import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { ChangeDetectionStrategy, Component, effect, HostBinding, inject, OnInit, signal } from '@angular/core'; +import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; import { UserSettings } from '@core/services/user/user.models'; -import { GetCurrentUserSettings, UpdateUserSettings } from '@core/store/user/user.actions'; -import { UserSelectors } from '@core/store/user/user.selectors'; -import { - EmailPreferencesForm, - EmailPreferencesFormControls, -} from '@osf/features/settings/notifications/models/notifications-form.models'; +import { GetCurrentUserSettings, UpdateUserSettings, UserSelectors } from '@core/store/user'; +import { SubscriptionEvent, SubscriptionFrequency } from '@osf/features/settings/notifications/enums'; +import { EmailPreferencesForm, EmailPreferencesFormControls } from '@osf/features/settings/notifications/models'; import { GetAllGlobalNotificationSubscriptions, NotificationSubscriptionSelectors, + UpdateNotificationSubscription, } from '@osf/features/settings/notifications/store'; import { SubHeaderComponent } from '@shared/components/sub-header/sub-header.component'; @Component({ selector: 'osf-notifications', standalone: true, - imports: [SubHeaderComponent, Checkbox, Button, DropdownModule, TranslatePipe, ReactiveFormsModule], + imports: [SubHeaderComponent, Checkbox, Button, DropdownModule, TranslatePipe, ReactiveFormsModule, Skeleton], templateUrl: './notifications.component.html', styleUrl: './notifications.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, @@ -33,53 +32,134 @@ import { SubHeaderComponent } from '@shared/components/sub-header/sub-header.com export class NotificationsComponent implements OnInit { @HostBinding('class') classes = 'flex flex-1 flex-column'; - readonly #store = inject(Store); - readonly #fb = inject(FormBuilder); + private readonly store = inject(Store); + private readonly fb = inject(FormBuilder); - readonly #currentUser = this.#store.selectSignal(UserSelectors.getCurrentUser); - readonly #userSettings = this.#store.selectSignal(UserSelectors.getCurrentUserSettings); + private currentUser = select(UserSelectors.getCurrentUser); + private userSettings = select(UserSelectors.getCurrentUserSettings); + private notificationSubscriptions = select(NotificationSubscriptionSelectors.getAllGlobalNotificationSubscriptions); - readonly #notificationSubscriptions = this.#store.selectSignal( - NotificationSubscriptionSelectors.getAllGlobalNotificationSubscriptions - ); - protected readonly EmailPreferencesFormControls = EmailPreferencesFormControls; - protected readonly emailPreferencesForm: EmailPreferencesForm = new FormGroup({ - [EmailPreferencesFormControls.SubscribeOsfGeneralEmail]: this.#fb.control(false, { nonNullable: true }), - [EmailPreferencesFormControls.SubscribeOsfHelpEmail]: this.#fb.control(false, { nonNullable: true }), + protected isEmailPreferencesLoading = signal(false); + protected isNotificationSubscriptionsLoading = signal(false); + protected isSubmittingEmailPreferences = signal(false); + protected EmailPreferencesFormControls = EmailPreferencesFormControls; + protected emailPreferencesForm: EmailPreferencesForm = new FormGroup({ + [EmailPreferencesFormControls.SubscribeOsfGeneralEmail]: this.fb.control(false, { nonNullable: true }), + [EmailPreferencesFormControls.SubscribeOsfHelpEmail]: this.fb.control(false, { nonNullable: true }), }); + protected subscriptionFrequencyOptions = Object.entries(SubscriptionFrequency).map(([key, value]) => ({ + label: key, + value, + })); + + protected subscriptionItems: { + event: SubscriptionEvent; + labelKey: string; + }[] = [ + { + event: SubscriptionEvent.GlobalCommentReplies, + labelKey: 'settings.notifications.notificationPreferences.items.replies', + }, + { + event: SubscriptionEvent.GlobalComments, + labelKey: 'settings.notifications.notificationPreferences.items.comments', + }, + { + event: SubscriptionEvent.GlobalFileUpdated, + labelKey: 'settings.notifications.notificationPreferences.items.files', + }, + { + event: SubscriptionEvent.GlobalMentions, + labelKey: 'settings.notifications.notificationPreferences.items.mentions', + }, + { + event: SubscriptionEvent.GlobalReviews, + labelKey: 'settings.notifications.notificationPreferences.items.preprints', + }, + ]; + + protected loadingEvents = signal([]); + + protected notificationSubscriptionsForm = this.fb.group( + this.subscriptionItems.reduce( + (ctrls, { event }) => { + ctrls[event] = this.fb.control(SubscriptionFrequency.Never, { nonNullable: true }); + return ctrls; + }, + {} as Record> + ) + ); + constructor() { effect(() => { - if (this.#userSettings()) { + if (this.userSettings()) { this.updateEmailPreferencesForm(); } }); effect(() => { - if (this.#notificationSubscriptions()) { - //[RN] TODO: set form + if (this.notificationSubscriptions()) { + this.updateNotificationSubscriptionsForm(); } }); } ngOnInit(): void { - this.#store.dispatch(new GetCurrentUserSettings()); - this.#store.dispatch(new GetAllGlobalNotificationSubscriptions()); + if (!this.notificationSubscriptions().length) { + this.isNotificationSubscriptionsLoading.set(true); + this.store.dispatch(new GetAllGlobalNotificationSubscriptions()).subscribe({ + complete: () => this.isNotificationSubscriptionsLoading.set(false), + }); + } + + if (!this.userSettings()) { + this.isEmailPreferencesLoading.set(true); + + this.store.dispatch(new GetCurrentUserSettings()).subscribe({ + complete: () => this.isEmailPreferencesLoading.set(false), + }); + } } emailPreferencesFormSubmit(): void { - if (!this.#currentUser()) { + if (!this.currentUser()) { return; } const formValue = this.emailPreferencesForm.value as UserSettings; - this.#store.dispatch(new UpdateUserSettings(this.#currentUser()!.id, formValue)); + this.isSubmittingEmailPreferences.set(true); + this.store.dispatch(new UpdateUserSettings(this.currentUser()!.id, formValue)).subscribe({ + complete: () => this.isSubmittingEmailPreferences.set(false), + }); + } + + onSubscriptionChange(event: SubscriptionEvent, frequency: SubscriptionFrequency) { + const user = this.currentUser(); + if (!user) return; + const id = `${user.id}_${event}`; + + this.loadingEvents.update((list) => [...list, event]); + + this.store.dispatch(new UpdateNotificationSubscription({ id, frequency })).subscribe({ + complete: () => { + this.loadingEvents.update((list) => list.filter((item) => item !== event)); + }, + }); } private updateEmailPreferencesForm() { this.emailPreferencesForm.patchValue({ - [EmailPreferencesFormControls.SubscribeOsfGeneralEmail]: this.#userSettings()?.subscribeOsfGeneralEmail, - [EmailPreferencesFormControls.SubscribeOsfHelpEmail]: this.#userSettings()?.subscribeOsfHelpEmail, + [EmailPreferencesFormControls.SubscribeOsfGeneralEmail]: this.userSettings()?.subscribeOsfGeneralEmail, + [EmailPreferencesFormControls.SubscribeOsfHelpEmail]: this.userSettings()?.subscribeOsfHelpEmail, }); } + + private updateNotificationSubscriptionsForm() { + const patch: Record = {}; + for (const sub of this.notificationSubscriptions()) { + patch[sub.event] = sub.frequency; + } + this.notificationSubscriptionsForm.patchValue(patch); + } } diff --git a/src/app/features/settings/notifications/service/notification-subscription.service.ts b/src/app/features/settings/notifications/service/notification-subscription.service.ts deleted file mode 100644 index ca75932ca..000000000 --- a/src/app/features/settings/notifications/service/notification-subscription.service.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Observable } from 'rxjs'; - -import { inject, Injectable } from '@angular/core'; - -import { JsonApiService } from '@core/services/json-api/json-api.service'; -import { NotificationSubscription } from '@osf/features/settings/notifications/models'; - -@Injectable({ - providedIn: 'root', -}) -export class NotificationSubscriptionService { - jsonApiService = inject(JsonApiService); - baseUrl = 'https://api.staging4.osf.io/v2/subscriptions/'; - - getAllGlobalNotificationSubscriptions(): Observable { - //[RN] TODO: add mapper add filter params - return this.jsonApiService.get(this.baseUrl); - } -} diff --git a/src/app/features/settings/notifications/services/notification-subscription.service.ts b/src/app/features/settings/notifications/services/notification-subscription.service.ts new file mode 100644 index 000000000..1876d1ebc --- /dev/null +++ b/src/app/features/settings/notifications/services/notification-subscription.service.ts @@ -0,0 +1,42 @@ +import { map, Observable } from 'rxjs'; + +import { inject, Injectable } from '@angular/core'; + +import { JsonApiResponse } from '@core/services/json-api/json-api.entity'; +import { JsonApiService } from '@core/services/json-api/json-api.service'; +import { SubscriptionFrequency } from '@osf/features/settings/notifications/enums'; +import { NotificationSubscriptionMapper } from '@osf/features/settings/notifications/mappers'; +import { + NotificationSubscription, + NotificationSubscriptionGetResponse, +} from '@osf/features/settings/notifications/models'; + +@Injectable({ + providedIn: 'root', +}) +export class NotificationSubscriptionService { + jsonApiService = inject(JsonApiService); + baseUrl = 'https://api.staging4.osf.io/v2/subscriptions/'; + + getAllGlobalNotificationSubscriptions(): Observable { + const params: Record = { + 'filter[event_name]': 'global_reviews,global_comments,global_comment_replies,global_file_updated,global_mentions', + }; + + return this.jsonApiService + .get>(this.baseUrl, params) + .pipe( + map((responses) => { + return responses.data.map((response) => NotificationSubscriptionMapper.fromGetResponse(response)); + }) + ); + } + + updateSubscription(id: string, frequency: SubscriptionFrequency): Observable { + const request = NotificationSubscriptionMapper.toUpdateRequest(id, frequency); + + return this.jsonApiService + .patch(this.baseUrl + id + '/', request) + .pipe(map((response) => NotificationSubscriptionMapper.fromGetResponse(response))); + } +} diff --git a/src/app/features/settings/notifications/store/notification-subscription.actions.ts b/src/app/features/settings/notifications/store/notification-subscription.actions.ts index 78e9d1d7c..a90de60fe 100644 --- a/src/app/features/settings/notifications/store/notification-subscription.actions.ts +++ b/src/app/features/settings/notifications/store/notification-subscription.actions.ts @@ -1,3 +1,11 @@ +import { SubscriptionFrequency } from '@osf/features/settings/notifications/enums'; + export class GetAllGlobalNotificationSubscriptions { static readonly type = '[Notification Subscriptions] Get All Global'; } + +export class UpdateNotificationSubscription { + static readonly type = '[Notification Subscriptions] Update'; + + constructor(public payload: { id: string; frequency: SubscriptionFrequency }) {} +} diff --git a/src/app/features/settings/notifications/store/notification-subscription.selectors.ts b/src/app/features/settings/notifications/store/notification-subscription.selectors.ts index d7efb8c19..4e920e7c1 100644 --- a/src/app/features/settings/notifications/store/notification-subscription.selectors.ts +++ b/src/app/features/settings/notifications/store/notification-subscription.selectors.ts @@ -1,11 +1,11 @@ import { Selector } from '@ngxs/store'; -import { DeveloperAppsState } from '@osf/features/settings/developer-apps/store/developer-apps.state'; import { NotificationSubscription } from '@osf/features/settings/notifications/models'; +import { NotificationSubscriptionState } from '@osf/features/settings/notifications/store/notification-subscription.state'; import { NotificationSubscriptionStateModel } from '@osf/features/settings/notifications/store/notification-subscription.state-model'; export class NotificationSubscriptionSelectors { - @Selector([DeveloperAppsState]) + @Selector([NotificationSubscriptionState]) static getAllGlobalNotificationSubscriptions(state: NotificationSubscriptionStateModel): NotificationSubscription[] { return state.notificationSubscriptions; } diff --git a/src/app/features/settings/notifications/store/notification-subscription.state.ts b/src/app/features/settings/notifications/store/notification-subscription.state.ts index 54e7bc359..5337297da 100644 --- a/src/app/features/settings/notifications/store/notification-subscription.state.ts +++ b/src/app/features/settings/notifications/store/notification-subscription.state.ts @@ -1,12 +1,16 @@ import { Action, State, StateContext } from '@ngxs/store'; -import { patch } from '@ngxs/store/operators'; +import { patch, updateItem } from '@ngxs/store/operators'; import { tap } from 'rxjs'; import { inject, Injectable } from '@angular/core'; -import { NotificationSubscriptionService } from '@osf/features/settings/notifications/service'; -import { GetAllGlobalNotificationSubscriptions } from '@osf/features/settings/notifications/store/notification-subscription.actions'; +import { NotificationSubscription } from '@osf/features/settings/notifications/models'; +import { NotificationSubscriptionService } from '@osf/features/settings/notifications/services'; +import { + GetAllGlobalNotificationSubscriptions, + UpdateNotificationSubscription, +} from '@osf/features/settings/notifications/store/notification-subscription.actions'; import { NotificationSubscriptionStateModel } from '@osf/features/settings/notifications/store/notification-subscription.state-model'; @State({ @@ -27,4 +31,23 @@ export class NotificationSubscriptionState { }) ); } + + @Action(UpdateNotificationSubscription) + updateNotificationSubscription( + ctx: StateContext, + action: UpdateNotificationSubscription + ) { + return this.#notificationSubscriptionService.updateSubscription(action.payload.id, action.payload.frequency).pipe( + tap((updatedSubscription) => { + ctx.setState( + patch({ + notificationSubscriptions: updateItem( + (app) => app.id === action.payload.id, + updatedSubscription + ), + }) + ); + }) + ); + } } From 54b1402230bbf71f46aa492bffa9314cf27f83b5 Mon Sep 17 00:00:00 2001 From: Roma Date: Tue, 20 May 2025 18:39:43 +0300 Subject: [PATCH 08/14] fix(notifications): Changed media query max width value --- .../settings/notifications/notifications.component.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/features/settings/notifications/notifications.component.scss b/src/app/features/settings/notifications/notifications.component.scss index 4e31a193a..b9e14d13c 100644 --- a/src/app/features/settings/notifications/notifications.component.scss +++ b/src/app/features/settings/notifications/notifications.component.scss @@ -17,7 +17,7 @@ color: var.$dark-blue-1; } - @media (max-width: 599.99px) { + @media (max-width: 576px) { padding: 12px; } } @@ -33,7 +33,7 @@ width: 50%; } - @media (max-width: 599.99px) { + @media (max-width: 576px) { grid-template-columns: 1fr; grid-template-rows: repeat(10, 1fr); grid-row-gap: 0; From 44b5b58c73b46f62e793baadce756440f5d62bc9 Mon Sep 17 00:00:00 2001 From: Roma Date: Tue, 20 May 2025 18:40:16 +0300 Subject: [PATCH 09/14] fix(notifications.spec): Fixed test to run --- .../notifications.component.spec.ts | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/app/features/settings/notifications/notifications.component.spec.ts b/src/app/features/settings/notifications/notifications.component.spec.ts index 98d1d5f84..d74aed34c 100644 --- a/src/app/features/settings/notifications/notifications.component.spec.ts +++ b/src/app/features/settings/notifications/notifications.component.spec.ts @@ -1,8 +1,17 @@ -import { TranslatePipe, TranslateService } from '@ngx-translate/core'; +import { Store } from '@ngxs/store'; + +import { TranslatePipe } from '@ngx-translate/core'; import { MockPipe, MockProvider } from 'ng-mocks'; +import { of } from 'rxjs'; + +import { provideHttpClient } from '@angular/common/http'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { mockStore } from '@shared/mocks'; + import { NotificationsComponent } from './notifications.component'; describe('NotificationsComponent', () => { @@ -10,9 +19,22 @@ describe('NotificationsComponent', () => { let fixture: ComponentFixture; beforeEach(async () => { + const store = mockStore; + store.selectSignal.mockImplementation(() => { + return signal([]); + }); + store.dispatch.mockImplementation(() => { + return of(); + }); + await TestBed.configureTestingModule({ imports: [NotificationsComponent, MockPipe(TranslatePipe)], - providers: [MockProvider(TranslateService)], + providers: [ + provideHttpClient(), + provideHttpClientTesting(), + MockProvider(TranslatePipe), + MockProvider(Store, store), + ], }).compileComponents(); fixture = TestBed.createComponent(NotificationsComponent); From 9eff271e4c6efbb5d44348e5083152079e186b0c Mon Sep 17 00:00:00 2001 From: Roma Date: Tue, 20 May 2025 19:09:43 +0300 Subject: [PATCH 10/14] feat(notifications): Refactored state models and selectors to use AsyncStateModel --- src/app/core/store/user/index.ts | 2 +- .../{user.state-model.ts => user.model.ts} | 3 +- src/app/core/store/user/user.selectors.ts | 15 +++- src/app/core/store/user/user.state.ts | 36 +++++++-- .../settings/notifications/constants/index.ts | 1 + .../constants/notifications-constants.ts | 27 +++++++ .../notifications.component.html | 15 ++-- .../notifications/notifications.component.ts | 75 +++++-------------- .../settings/notifications/store/index.ts | 2 +- .../store/notification-subscription.model.ts | 6 ++ .../notification-subscription.selectors.ts | 15 +++- .../notification-subscription.state-model.ts | 5 -- .../store/notification-subscription.state.ts | 32 +++++--- .../shared/models/store/async-state.model.ts | 1 + 14 files changed, 138 insertions(+), 97 deletions(-) rename src/app/core/store/user/{user.state-model.ts => user.model.ts} (53%) create mode 100644 src/app/features/settings/notifications/constants/index.ts create mode 100644 src/app/features/settings/notifications/constants/notifications-constants.ts create mode 100644 src/app/features/settings/notifications/store/notification-subscription.model.ts delete mode 100644 src/app/features/settings/notifications/store/notification-subscription.state-model.ts diff --git a/src/app/core/store/user/index.ts b/src/app/core/store/user/index.ts index 10669f497..09a5e6d0e 100644 --- a/src/app/core/store/user/index.ts +++ b/src/app/core/store/user/index.ts @@ -1,4 +1,4 @@ export * from './user.actions'; +export * from './user.model'; export * from './user.selectors'; export * from './user.state'; -export * from './user.state-model'; diff --git a/src/app/core/store/user/user.state-model.ts b/src/app/core/store/user/user.model.ts similarity index 53% rename from src/app/core/store/user/user.state-model.ts rename to src/app/core/store/user/user.model.ts index 8cc015a5e..4a3a8068e 100644 --- a/src/app/core/store/user/user.state-model.ts +++ b/src/app/core/store/user/user.model.ts @@ -1,6 +1,7 @@ import { User, UserSettings } from '@core/services/user/user.models'; +import { AsyncStateModel } from '@shared/models/store'; export interface UserStateModel { currentUser: User | null; - currentUserSettings: UserSettings | null; + currentUserSettings: AsyncStateModel; } diff --git a/src/app/core/store/user/user.selectors.ts b/src/app/core/store/user/user.selectors.ts index 98cfde1e6..adbe26cdb 100644 --- a/src/app/core/store/user/user.selectors.ts +++ b/src/app/core/store/user/user.selectors.ts @@ -1,8 +1,7 @@ import { Selector } from '@ngxs/store'; import { User, UserSettings } from '@core/services/user/user.models'; -import { UserState } from '@core/store/user/user.state'; -import { UserStateModel } from '@core/store/user/user.state-model'; +import { UserState, UserStateModel } from '@core/store/user'; import { ProfileSettingsStateModel } from '@osf/features/settings/profile-settings/profile-settings.entities'; import { Social } from '@osf/features/settings/profile-settings/social/social.entities'; @@ -32,6 +31,16 @@ export class UserSelectors { @Selector([UserState]) static getCurrentUserSettings(state: UserStateModel): UserSettings | null { - return state.currentUserSettings; + return state.currentUserSettings.data; + } + + @Selector([UserState]) + static isUserSettingsLoading(state: UserStateModel): boolean { + return state.currentUserSettings.isLoading; + } + + @Selector([UserState]) + static isUserSettingsSubmitting(state: UserStateModel): boolean { + return state.currentUserSettings.isSubmitting!; } } diff --git a/src/app/core/store/user/user.state.ts b/src/app/core/store/user/user.state.ts index c0f5b5c2f..ff5dc51ce 100644 --- a/src/app/core/store/user/user.state.ts +++ b/src/app/core/store/user/user.state.ts @@ -1,4 +1,5 @@ import { Action, State, StateContext } from '@ngxs/store'; +import { patch } from '@ngxs/store/operators'; import { tap } from 'rxjs'; @@ -8,13 +9,18 @@ import { UserService } from '@core/services/user/user.service'; import { SetupProfileSettings } from '@osf/features/settings/profile-settings/profile-settings.actions'; import { GetCurrentUser, GetCurrentUserSettings, SetCurrentUser, UpdateUserSettings } from './user.actions'; -import { UserStateModel } from './user.state-model'; +import { UserStateModel } from './user.model'; @State({ name: 'user', defaults: { currentUser: null, - currentUserSettings: null, + currentUserSettings: { + data: null, + isLoading: false, + isSubmitting: false, + error: '', + }, }, }) @Injectable() @@ -42,22 +48,36 @@ export class UserState { @Action(GetCurrentUserSettings) getCurrentUserSettings(ctx: StateContext) { + ctx.setState(patch({ currentUserSettings: patch({ isLoading: true }) })); + return this.userService.getCurrentUserSettings().pipe( tap((userSettings) => { - ctx.patchState({ - currentUserSettings: userSettings, - }); + ctx.setState( + patch({ + currentUserSettings: patch({ + data: userSettings, + isLoading: false, + }), + }) + ); }) ); } @Action(UpdateUserSettings) updateUserSettings(ctx: StateContext, action: UpdateUserSettings) { + ctx.setState(patch({ currentUserSettings: patch({ isSubmitting: true }) })); + return this.userService.updateUserSettings(action.userId, action.updatedUserSettings).pipe( tap(() => { - ctx.patchState({ - currentUserSettings: action.updatedUserSettings, - }); + ctx.setState( + patch({ + currentUserSettings: patch({ + data: action.updatedUserSettings, + isSubmitting: false, + }), + }) + ); }) ); } diff --git a/src/app/features/settings/notifications/constants/index.ts b/src/app/features/settings/notifications/constants/index.ts new file mode 100644 index 000000000..b52eea378 --- /dev/null +++ b/src/app/features/settings/notifications/constants/index.ts @@ -0,0 +1 @@ +export * from './notifications-constants'; diff --git a/src/app/features/settings/notifications/constants/notifications-constants.ts b/src/app/features/settings/notifications/constants/notifications-constants.ts new file mode 100644 index 000000000..d20785817 --- /dev/null +++ b/src/app/features/settings/notifications/constants/notifications-constants.ts @@ -0,0 +1,27 @@ +import { SubscriptionEvent } from '@osf/features/settings/notifications/enums'; + +export const SUBSCRIPTION_EVENTS: { + event: SubscriptionEvent; + labelKey: string; +}[] = [ + { + event: SubscriptionEvent.GlobalCommentReplies, + labelKey: 'settings.notifications.notificationPreferences.items.replies', + }, + { + event: SubscriptionEvent.GlobalComments, + labelKey: 'settings.notifications.notificationPreferences.items.comments', + }, + { + event: SubscriptionEvent.GlobalFileUpdated, + labelKey: 'settings.notifications.notificationPreferences.items.files', + }, + { + event: SubscriptionEvent.GlobalMentions, + labelKey: 'settings.notifications.notificationPreferences.items.mentions', + }, + { + event: SubscriptionEvent.GlobalReviews, + labelKey: 'settings.notifications.notificationPreferences.items.preprints', + }, +]; diff --git a/src/app/features/settings/notifications/notifications.component.html b/src/app/features/settings/notifications/notifications.component.html index d36bedbb4..e7b4a10d9 100644 --- a/src/app/features/settings/notifications/notifications.component.html +++ b/src/app/features/settings/notifications/notifications.component.html @@ -1,9 +1,8 @@ - + -
+
@if (!isEmailPreferencesLoading()) {

{{ "settings.notifications.emailPreferences.title" | translate }}

@@ -77,10 +76,8 @@

{{ "settings.notifications.notificationPreferences.title" | translate }}

{{ "settings.notifications.notificationPreferences.note" | translate }}

- @for (subscriptionEvent of subscriptionItems; track $index) { -
- {{ subscriptionEvent.labelKey | translate }} -
+ @for (subscriptionEvent of SUBSCRIPTION_EVENTS; track $index) { +

{{ subscriptionEvent.labelKey | translate }}

([]); + protected EmailPreferencesFormControls = EmailPreferencesFormControls; protected emailPreferencesForm: EmailPreferencesForm = new FormGroup({ [EmailPreferencesFormControls.SubscribeOsfGeneralEmail]: this.fb.control(false, { nonNullable: true }), [EmailPreferencesFormControls.SubscribeOsfHelpEmail]: this.fb.control(false, { nonNullable: true }), }); + protected readonly SUBSCRIPTION_EVENTS = SUBSCRIPTION_EVENTS; protected subscriptionFrequencyOptions = Object.entries(SubscriptionFrequency).map(([key, value]) => ({ label: key, value, })); - - protected subscriptionItems: { - event: SubscriptionEvent; - labelKey: string; - }[] = [ - { - event: SubscriptionEvent.GlobalCommentReplies, - labelKey: 'settings.notifications.notificationPreferences.items.replies', - }, - { - event: SubscriptionEvent.GlobalComments, - labelKey: 'settings.notifications.notificationPreferences.items.comments', - }, - { - event: SubscriptionEvent.GlobalFileUpdated, - labelKey: 'settings.notifications.notificationPreferences.items.files', - }, - { - event: SubscriptionEvent.GlobalMentions, - labelKey: 'settings.notifications.notificationPreferences.items.mentions', - }, - { - event: SubscriptionEvent.GlobalReviews, - labelKey: 'settings.notifications.notificationPreferences.items.preprints', - }, - ]; - - protected loadingEvents = signal([]); - protected notificationSubscriptionsForm = this.fb.group( - this.subscriptionItems.reduce( - (ctrls, { event }) => { - ctrls[event] = this.fb.control(SubscriptionFrequency.Never, { nonNullable: true }); - return ctrls; + SUBSCRIPTION_EVENTS.reduce( + (control, { event }) => { + control[event] = this.fb.control(SubscriptionFrequency.Never, { nonNullable: true }); + return control; }, {} as Record> ) @@ -93,7 +69,7 @@ export class NotificationsComponent implements OnInit { constructor() { effect(() => { - if (this.userSettings()) { + if (this.emailPreferences()) { this.updateEmailPreferencesForm(); } }); @@ -107,18 +83,11 @@ export class NotificationsComponent implements OnInit { ngOnInit(): void { if (!this.notificationSubscriptions().length) { - this.isNotificationSubscriptionsLoading.set(true); - this.store.dispatch(new GetAllGlobalNotificationSubscriptions()).subscribe({ - complete: () => this.isNotificationSubscriptionsLoading.set(false), - }); + this.store.dispatch(new GetAllGlobalNotificationSubscriptions()); } - if (!this.userSettings()) { - this.isEmailPreferencesLoading.set(true); - - this.store.dispatch(new GetCurrentUserSettings()).subscribe({ - complete: () => this.isEmailPreferencesLoading.set(false), - }); + if (!this.emailPreferences()) { + this.store.dispatch(new GetCurrentUserSettings()); } } @@ -128,10 +97,7 @@ export class NotificationsComponent implements OnInit { } const formValue = this.emailPreferencesForm.value as UserSettings; - this.isSubmittingEmailPreferences.set(true); - this.store.dispatch(new UpdateUserSettings(this.currentUser()!.id, formValue)).subscribe({ - complete: () => this.isSubmittingEmailPreferences.set(false), - }); + this.store.dispatch(new UpdateUserSettings(this.currentUser()!.id, formValue)); } onSubscriptionChange(event: SubscriptionEvent, frequency: SubscriptionFrequency) { @@ -140,7 +106,6 @@ export class NotificationsComponent implements OnInit { const id = `${user.id}_${event}`; this.loadingEvents.update((list) => [...list, event]); - this.store.dispatch(new UpdateNotificationSubscription({ id, frequency })).subscribe({ complete: () => { this.loadingEvents.update((list) => list.filter((item) => item !== event)); @@ -150,8 +115,8 @@ export class NotificationsComponent implements OnInit { private updateEmailPreferencesForm() { this.emailPreferencesForm.patchValue({ - [EmailPreferencesFormControls.SubscribeOsfGeneralEmail]: this.userSettings()?.subscribeOsfGeneralEmail, - [EmailPreferencesFormControls.SubscribeOsfHelpEmail]: this.userSettings()?.subscribeOsfHelpEmail, + [EmailPreferencesFormControls.SubscribeOsfGeneralEmail]: this.emailPreferences()?.subscribeOsfGeneralEmail, + [EmailPreferencesFormControls.SubscribeOsfHelpEmail]: this.emailPreferences()?.subscribeOsfHelpEmail, }); } diff --git a/src/app/features/settings/notifications/store/index.ts b/src/app/features/settings/notifications/store/index.ts index d36c5fcf8..c7a0d5e09 100644 --- a/src/app/features/settings/notifications/store/index.ts +++ b/src/app/features/settings/notifications/store/index.ts @@ -1,4 +1,4 @@ export * from './notification-subscription.actions'; +export * from './notification-subscription.model'; export * from './notification-subscription.selectors'; export * from './notification-subscription.state'; -export * from './notification-subscription.state-model'; diff --git a/src/app/features/settings/notifications/store/notification-subscription.model.ts b/src/app/features/settings/notifications/store/notification-subscription.model.ts new file mode 100644 index 000000000..24487bbca --- /dev/null +++ b/src/app/features/settings/notifications/store/notification-subscription.model.ts @@ -0,0 +1,6 @@ +import { NotificationSubscription } from '@osf/features/settings/notifications/models'; +import { AsyncStateModel } from '@shared/models/store'; + +export interface NotificationSubscriptionModel { + notificationSubscriptions: AsyncStateModel; +} diff --git a/src/app/features/settings/notifications/store/notification-subscription.selectors.ts b/src/app/features/settings/notifications/store/notification-subscription.selectors.ts index 4e920e7c1..190a2e03b 100644 --- a/src/app/features/settings/notifications/store/notification-subscription.selectors.ts +++ b/src/app/features/settings/notifications/store/notification-subscription.selectors.ts @@ -1,12 +1,19 @@ import { Selector } from '@ngxs/store'; import { NotificationSubscription } from '@osf/features/settings/notifications/models'; -import { NotificationSubscriptionState } from '@osf/features/settings/notifications/store/notification-subscription.state'; -import { NotificationSubscriptionStateModel } from '@osf/features/settings/notifications/store/notification-subscription.state-model'; +import { + NotificationSubscriptionModel, + NotificationSubscriptionState, +} from '@osf/features/settings/notifications/store'; export class NotificationSubscriptionSelectors { @Selector([NotificationSubscriptionState]) - static getAllGlobalNotificationSubscriptions(state: NotificationSubscriptionStateModel): NotificationSubscription[] { - return state.notificationSubscriptions; + static getAllGlobalNotificationSubscriptions(state: NotificationSubscriptionModel): NotificationSubscription[] { + return state.notificationSubscriptions.data; + } + + @Selector([NotificationSubscriptionState]) + static isLoading(state: NotificationSubscriptionModel): boolean { + return state.notificationSubscriptions.isLoading; } } diff --git a/src/app/features/settings/notifications/store/notification-subscription.state-model.ts b/src/app/features/settings/notifications/store/notification-subscription.state-model.ts deleted file mode 100644 index 4d950ffcb..000000000 --- a/src/app/features/settings/notifications/store/notification-subscription.state-model.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { NotificationSubscription } from '@osf/features/settings/notifications/models'; - -export interface NotificationSubscriptionStateModel { - notificationSubscriptions: NotificationSubscription[]; -} diff --git a/src/app/features/settings/notifications/store/notification-subscription.state.ts b/src/app/features/settings/notifications/store/notification-subscription.state.ts index 5337297da..f9b6b342b 100644 --- a/src/app/features/settings/notifications/store/notification-subscription.state.ts +++ b/src/app/features/settings/notifications/store/notification-subscription.state.ts @@ -11,12 +11,16 @@ import { GetAllGlobalNotificationSubscriptions, UpdateNotificationSubscription, } from '@osf/features/settings/notifications/store/notification-subscription.actions'; -import { NotificationSubscriptionStateModel } from '@osf/features/settings/notifications/store/notification-subscription.state-model'; +import { NotificationSubscriptionModel } from '@osf/features/settings/notifications/store/notification-subscription.model'; -@State({ +@State({ name: 'notificationSubscriptions', defaults: { - notificationSubscriptions: [], + notificationSubscriptions: { + data: [], + isLoading: false, + error: '', + }, }, }) @Injectable() @@ -24,27 +28,35 @@ export class NotificationSubscriptionState { #notificationSubscriptionService = inject(NotificationSubscriptionService); @Action(GetAllGlobalNotificationSubscriptions) - getAllGlobalNotificationSubscriptions(ctx: StateContext) { + getAllGlobalNotificationSubscriptions(ctx: StateContext) { + ctx.setState(patch({ notificationSubscriptions: patch({ isLoading: true }) })); + return this.#notificationSubscriptionService.getAllGlobalNotificationSubscriptions().pipe( tap((notificationSubscriptions) => { - ctx.setState(patch({ notificationSubscriptions })); + ctx.setState( + patch({ + notificationSubscriptions: patch({ + data: notificationSubscriptions, + isLoading: false, + }), + }) + ); }) ); } @Action(UpdateNotificationSubscription) updateNotificationSubscription( - ctx: StateContext, + ctx: StateContext, action: UpdateNotificationSubscription ) { return this.#notificationSubscriptionService.updateSubscription(action.payload.id, action.payload.frequency).pipe( tap((updatedSubscription) => { ctx.setState( patch({ - notificationSubscriptions: updateItem( - (app) => app.id === action.payload.id, - updatedSubscription - ), + notificationSubscriptions: patch({ + data: updateItem((app) => app.id === action.payload.id, updatedSubscription), + }), }) ); }) diff --git a/src/app/shared/models/store/async-state.model.ts b/src/app/shared/models/store/async-state.model.ts index acce4324e..8cdd5abf3 100644 --- a/src/app/shared/models/store/async-state.model.ts +++ b/src/app/shared/models/store/async-state.model.ts @@ -1,5 +1,6 @@ export interface AsyncStateModel { data: T; isLoading: boolean; + isSubmitting?: boolean; error: string | null; } From ead08932817ba35f9a096e62ecb0777f2522381d Mon Sep 17 00:00:00 2001 From: Roma Date: Tue, 20 May 2025 19:23:22 +0300 Subject: [PATCH 11/14] refactor(user): consolidate user imports and restructure user service files --- src/app/app.component.ts | 2 +- src/app/core/services/user/index.ts | 3 +++ .../core/services/user/{users.mapper.ts => user.mapper.ts} | 0 src/app/core/services/user/user.service.ts | 2 +- src/app/core/store/user/user.actions.ts | 2 +- src/app/core/store/user/user.model.ts | 2 +- src/app/core/store/user/user.selectors.ts | 2 +- .../account-settings/services/account-settings.service.ts | 5 ++--- .../settings/notifications/notifications.component.ts | 2 +- .../settings/profile-settings/profile-settings.actions.ts | 2 +- .../profile-settings/profile-settings.api.service.ts | 2 +- .../settings/profile-settings/profile-settings.entities.ts | 2 +- .../settings/profile-settings/profile-settings.selectors.ts | 2 +- 13 files changed, 15 insertions(+), 13 deletions(-) create mode 100644 src/app/core/services/user/index.ts rename src/app/core/services/user/{users.mapper.ts => user.mapper.ts} (100%) diff --git a/src/app/app.component.ts b/src/app/app.component.ts index c534ef9ef..7ca737092 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -3,7 +3,7 @@ import { Store } from '@ngxs/store'; import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core'; import { RouterOutlet } from '@angular/router'; -import { GetCurrentUser } from '@core/store/user/user.actions'; +import { GetCurrentUser } from '@core/store/user'; @Component({ selector: 'osf-root', diff --git a/src/app/core/services/user/index.ts b/src/app/core/services/user/index.ts new file mode 100644 index 000000000..0b302dca6 --- /dev/null +++ b/src/app/core/services/user/index.ts @@ -0,0 +1,3 @@ +export * from './user.mapper'; +export * from './user.models'; +export * from './user.service'; diff --git a/src/app/core/services/user/users.mapper.ts b/src/app/core/services/user/user.mapper.ts similarity index 100% rename from src/app/core/services/user/users.mapper.ts rename to src/app/core/services/user/user.mapper.ts diff --git a/src/app/core/services/user/user.service.ts b/src/app/core/services/user/user.service.ts index de6a9b67f..4d33895d4 100644 --- a/src/app/core/services/user/user.service.ts +++ b/src/app/core/services/user/user.service.ts @@ -4,8 +4,8 @@ import { inject, Injectable } from '@angular/core'; import { JsonApiResponse } from '@core/services/json-api/json-api.entity'; import { JsonApiService } from '@core/services/json-api/json-api.service'; +import { UserMapper } from '@core/services/user/user.mapper'; import { User, UserGetResponse, UserSettings, UserSettingsGetResponse } from '@core/services/user/user.models'; -import { UserMapper } from '@core/services/user/users.mapper'; @Injectable({ providedIn: 'root', diff --git a/src/app/core/store/user/user.actions.ts b/src/app/core/store/user/user.actions.ts index 2bf32d46b..584af0fa8 100644 --- a/src/app/core/store/user/user.actions.ts +++ b/src/app/core/store/user/user.actions.ts @@ -1,4 +1,4 @@ -import { User, UserSettings } from '@core/services/user/user.models'; +import { User, UserSettings } from '@core/services/user'; export class GetCurrentUser { static readonly type = '[User] Get Current User'; diff --git a/src/app/core/store/user/user.model.ts b/src/app/core/store/user/user.model.ts index 4a3a8068e..c6df22f0c 100644 --- a/src/app/core/store/user/user.model.ts +++ b/src/app/core/store/user/user.model.ts @@ -1,4 +1,4 @@ -import { User, UserSettings } from '@core/services/user/user.models'; +import { User, UserSettings } from '@core/services/user'; import { AsyncStateModel } from '@shared/models/store'; export interface UserStateModel { diff --git a/src/app/core/store/user/user.selectors.ts b/src/app/core/store/user/user.selectors.ts index adbe26cdb..13b9231c5 100644 --- a/src/app/core/store/user/user.selectors.ts +++ b/src/app/core/store/user/user.selectors.ts @@ -1,6 +1,6 @@ import { Selector } from '@ngxs/store'; -import { User, UserSettings } from '@core/services/user/user.models'; +import { User, UserSettings } from '@core/services/user'; import { UserState, UserStateModel } from '@core/store/user'; import { ProfileSettingsStateModel } from '@osf/features/settings/profile-settings/profile-settings.entities'; import { Social } from '@osf/features/settings/profile-settings/social/social.entities'; diff --git a/src/app/features/settings/account-settings/services/account-settings.service.ts b/src/app/features/settings/account-settings/services/account-settings.service.ts index 0af4d6ed1..98d7ffb80 100644 --- a/src/app/features/settings/account-settings/services/account-settings.service.ts +++ b/src/app/features/settings/account-settings/services/account-settings.service.ts @@ -5,9 +5,8 @@ import { map, Observable } from 'rxjs'; import { inject, Injectable } from '@angular/core'; import { JsonApiService } from '@core/services/json-api/json-api.service'; -import { User, UserGetResponse } from '@core/services/user/user.models'; -import { UserMapper } from '@core/services/user/users.mapper'; -import { UserSelectors } from '@core/store/user/user.selectors'; +import { User, UserGetResponse, UserMapper } from '@core/services/user'; +import { UserSelectors } from '@core/store/user'; import { ApiData, JsonApiResponse } from '@osf/core/services/json-api/json-api.entity'; import { environment } from '../../../../../environments/environment'; diff --git a/src/app/features/settings/notifications/notifications.component.ts b/src/app/features/settings/notifications/notifications.component.ts index 23b1978d6..86e4279b5 100644 --- a/src/app/features/settings/notifications/notifications.component.ts +++ b/src/app/features/settings/notifications/notifications.component.ts @@ -10,7 +10,7 @@ import { Skeleton } from 'primeng/skeleton'; import { ChangeDetectionStrategy, Component, effect, HostBinding, inject, OnInit, signal } from '@angular/core'; import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; -import { UserSettings } from '@core/services/user/user.models'; +import { UserSettings } from '@core/services/user'; import { GetCurrentUserSettings, UpdateUserSettings, UserSelectors } from '@core/store/user'; import { SUBSCRIPTION_EVENTS } from '@osf/features/settings/notifications/constants'; import { SubscriptionEvent, SubscriptionFrequency } from '@osf/features/settings/notifications/enums'; diff --git a/src/app/features/settings/profile-settings/profile-settings.actions.ts b/src/app/features/settings/profile-settings/profile-settings.actions.ts index d4a998d53..9703bd206 100644 --- a/src/app/features/settings/profile-settings/profile-settings.actions.ts +++ b/src/app/features/settings/profile-settings/profile-settings.actions.ts @@ -1,4 +1,4 @@ -import { User } from '@core/services/user/user.models'; +import { User } from '@core/services/user'; import { Education } from '@osf/features/settings/profile-settings/education/educations.entities'; import { Employment } from '@osf/features/settings/profile-settings/employment/employment.entities'; import { Social } from '@osf/features/settings/profile-settings/social/social.entities'; diff --git a/src/app/features/settings/profile-settings/profile-settings.api.service.ts b/src/app/features/settings/profile-settings/profile-settings.api.service.ts index 531c78065..0643cdbdd 100644 --- a/src/app/features/settings/profile-settings/profile-settings.api.service.ts +++ b/src/app/features/settings/profile-settings/profile-settings.api.service.ts @@ -1,7 +1,7 @@ import { inject, Injectable } from '@angular/core'; import { JsonApiService } from '@core/services/json-api/json-api.service'; -import { UserGetResponse } from '@core/services/user/user.models'; +import { UserGetResponse } from '@core/services/user'; import { JsonApiResponse } from '@osf/core/services/json-api/json-api.entity'; import { ProfileSettingsStateModel, diff --git a/src/app/features/settings/profile-settings/profile-settings.entities.ts b/src/app/features/settings/profile-settings/profile-settings.entities.ts index 8ae450c51..29063b648 100644 --- a/src/app/features/settings/profile-settings/profile-settings.entities.ts +++ b/src/app/features/settings/profile-settings/profile-settings.entities.ts @@ -1,4 +1,4 @@ -import { User } from '@core/services/user/user.models'; +import { User } from '@core/services/user'; import { Education } from '@osf/features/settings/profile-settings/education/educations.entities'; import { Employment } from '@osf/features/settings/profile-settings/employment/employment.entities'; import { Social } from '@osf/features/settings/profile-settings/social/social.entities'; diff --git a/src/app/features/settings/profile-settings/profile-settings.selectors.ts b/src/app/features/settings/profile-settings/profile-settings.selectors.ts index 02baf7ac7..dd7377e87 100644 --- a/src/app/features/settings/profile-settings/profile-settings.selectors.ts +++ b/src/app/features/settings/profile-settings/profile-settings.selectors.ts @@ -1,6 +1,6 @@ import { Selector } from '@ngxs/store'; -import { User } from '@core/services/user/user.models'; +import { User } from '@core/services/user'; import { Education } from '@osf/features/settings/profile-settings/education/educations.entities'; import { Employment } from '@osf/features/settings/profile-settings/employment/employment.entities'; import { ProfileSettingsStateModel } from '@osf/features/settings/profile-settings/profile-settings.entities'; From 8951a72be65ceafc9d16e2d3d0edf615f8ac5575 Mon Sep 17 00:00:00 2001 From: Roma Date: Wed, 21 May 2025 11:44:20 +0300 Subject: [PATCH 12/14] fix(merge): Fixed merge issues --- src/app/core/services/user/user.models.ts | 9 -------- .../meeting-details.component.html | 13 ++++++----- .../meetings-landing.component.html | 23 ++++++++++--------- 3 files changed, 19 insertions(+), 26 deletions(-) diff --git a/src/app/core/services/user/user.models.ts b/src/app/core/services/user/user.models.ts index eb174e168..7264cd532 100644 --- a/src/app/core/services/user/user.models.ts +++ b/src/app/core/services/user/user.models.ts @@ -17,15 +17,6 @@ export interface User { dateRegistered: Date; link?: string; iri?: string; - socials?: { - orcid?: string; - github?: string; - scholar?: string; - twitter?: string; - linkedIn?: string; - impactStory?: string; - researcherId?: string; - }; defaultRegionId: string; allowIndexing: boolean | undefined; } diff --git a/src/app/features/meetings/pages/meeting-details/meeting-details.component.html b/src/app/features/meetings/pages/meeting-details/meeting-details.component.html index 234762b69..da38585ee 100644 --- a/src/app/features/meetings/pages/meeting-details/meeting-details.component.html +++ b/src/app/features/meetings/pages/meeting-details/meeting-details.component.html @@ -1,4 +1,4 @@ - +
{{ 'meetings.details.table.columns.title' | translate }} - + {{ 'meetings.details.table.columns.author' | translate }} - + {{ 'meetings.details.table.columns.category' | translate }} - + {{ 'meetings.details.table.columns.dateCreated' | translate }} - + {{ 'meetings.details.table.columns.downloads' | translate }} - + diff --git a/src/app/features/meetings/pages/meetings-landing/meetings-landing.component.html b/src/app/features/meetings/pages/meetings-landing/meetings-landing.component.html index ea3d603df..07c837a02 100644 --- a/src/app/features/meetings/pages/meetings-landing/meetings-landing.component.html +++ b/src/app/features/meetings/pages/meetings-landing/meetings-landing.component.html @@ -17,6 +17,7 @@ /> {{ 'meetings.landing.table.columns.title' | translate }} - + {{ 'meetings.landing.table.columns.submissions' | translate }} - + {{ 'meetings.landing.table.columns.location' | translate }} - + {{ 'meetings.landing.table.columns.date' | translate }} - + @@ -68,7 +69,7 @@
- Discover + Discover

{{ 'meetings.landing.features.discover.title' | translate }}

{{ 'meetings.landing.features.discover.description' | translate }}

@@ -76,7 +77,7 @@

{{ 'meetings.landing.features.discover.title' | translate }}

- Share + Share

{{ 'meetings.landing.features.share.title' | translate }}

{{ 'meetings.landing.features.share.description' | translate }}

@@ -84,7 +85,7 @@

{{ 'meetings.landing.features.share.title' | translate }}

- Enhance + Enhance

{{ 'meetings.landing.features.enhance.title' | translate }}

{{ 'meetings.landing.features.enhance.description' | translate }}

@@ -95,10 +96,10 @@

{{ 'meetings.landing.features.enhance.title' | translate }}

{{ 'meetings.landing.users.title' | translate }}

- APS - BITSS - NRAO - SPSP + APS + BITSS + NRAO + SPSP
From 8ad71c798bf6a2761d0f6f34181088c6d65eee84 Mon Sep 17 00:00:00 2001 From: Roma Date: Wed, 21 May 2025 11:45:23 +0300 Subject: [PATCH 13/14] refactor(notifications): replace dropdown with select component --- .../notifications/notifications.component.html | 12 ++++++------ .../notifications/notifications.component.ts | 5 ++--- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/app/features/settings/notifications/notifications.component.html b/src/app/features/settings/notifications/notifications.component.html index e7b4a10d9..64162bf4a 100644 --- a/src/app/features/settings/notifications/notifications.component.html +++ b/src/app/features/settings/notifications/notifications.component.html @@ -78,12 +78,12 @@

{{ "settings.notifications.notificationPreferences.title" | translate }}

@for (subscriptionEvent of SUBSCRIPTION_EVENTS; track $index) {

{{ subscriptionEvent.labelKey | translate }}

- + }
diff --git a/src/app/features/settings/notifications/notifications.component.ts b/src/app/features/settings/notifications/notifications.component.ts index 86e4279b5..b84a7c133 100644 --- a/src/app/features/settings/notifications/notifications.component.ts +++ b/src/app/features/settings/notifications/notifications.component.ts @@ -4,7 +4,7 @@ import { TranslatePipe } from '@ngx-translate/core'; import { Button } from 'primeng/button'; import { Checkbox } from 'primeng/checkbox'; -import { DropdownModule } from 'primeng/dropdown'; +import { Select } from 'primeng/select'; import { Skeleton } from 'primeng/skeleton'; import { ChangeDetectionStrategy, Component, effect, HostBinding, inject, OnInit, signal } from '@angular/core'; @@ -24,8 +24,7 @@ import { SubHeaderComponent } from '@shared/components/sub-header/sub-header.com @Component({ selector: 'osf-notifications', - standalone: true, - imports: [SubHeaderComponent, Checkbox, Button, DropdownModule, TranslatePipe, ReactiveFormsModule, Skeleton], + imports: [SubHeaderComponent, Checkbox, Button, TranslatePipe, ReactiveFormsModule, Skeleton, Select], templateUrl: './notifications.component.html', styleUrl: './notifications.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, From 30ba06a10fa682fa1ab6ac916ccd9fe81e2b4550 Mon Sep 17 00:00:00 2001 From: Roma Date: Wed, 21 May 2025 12:02:07 +0300 Subject: [PATCH 14/14] refactor(styles): reformating --- .../meeting-details.component.html | 12 ++-- .../meetings-landing.component.html | 22 +++---- .../notifications.component.html | 65 ++++++++++--------- 3 files changed, 52 insertions(+), 47 deletions(-) diff --git a/src/app/features/meetings/pages/meeting-details/meeting-details.component.html b/src/app/features/meetings/pages/meeting-details/meeting-details.component.html index da38585ee..783b22a80 100644 --- a/src/app/features/meetings/pages/meeting-details/meeting-details.component.html +++ b/src/app/features/meetings/pages/meeting-details/meeting-details.component.html @@ -1,4 +1,4 @@ - +
{{ 'meetings.details.table.columns.title' | translate }} - + {{ 'meetings.details.table.columns.author' | translate }} - + {{ 'meetings.details.table.columns.category' | translate }} - + {{ 'meetings.details.table.columns.dateCreated' | translate }} - + {{ 'meetings.details.table.columns.downloads' | translate }} - + diff --git a/src/app/features/meetings/pages/meetings-landing/meetings-landing.component.html b/src/app/features/meetings/pages/meetings-landing/meetings-landing.component.html index 07c837a02..3a74df3b2 100644 --- a/src/app/features/meetings/pages/meetings-landing/meetings-landing.component.html +++ b/src/app/features/meetings/pages/meetings-landing/meetings-landing.component.html @@ -40,19 +40,19 @@ {{ 'meetings.landing.table.columns.title' | translate }} - + {{ 'meetings.landing.table.columns.submissions' | translate }} - + {{ 'meetings.landing.table.columns.location' | translate }} - + {{ 'meetings.landing.table.columns.date' | translate }} - + @@ -69,7 +69,7 @@
- Discover + Discover

{{ 'meetings.landing.features.discover.title' | translate }}

{{ 'meetings.landing.features.discover.description' | translate }}

@@ -77,7 +77,7 @@

{{ 'meetings.landing.features.discover.title' | translate }}

- Share + Share

{{ 'meetings.landing.features.share.title' | translate }}

{{ 'meetings.landing.features.share.description' | translate }}

@@ -85,7 +85,7 @@

{{ 'meetings.landing.features.share.title' | translate }}

- Enhance + Enhance

{{ 'meetings.landing.features.enhance.title' | translate }}

{{ 'meetings.landing.features.enhance.description' | translate }}

@@ -96,10 +96,10 @@

{{ 'meetings.landing.features.enhance.title' | translate }}

{{ 'meetings.landing.users.title' | translate }}

- APS - BITSS - NRAO - SPSP + APS + BITSS + NRAO + SPSP
diff --git a/src/app/features/settings/notifications/notifications.component.html b/src/app/features/settings/notifications/notifications.component.html index 64162bf4a..9560aa354 100644 --- a/src/app/features/settings/notifications/notifications.component.html +++ b/src/app/features/settings/notifications/notifications.component.html @@ -1,53 +1,55 @@ - +
@if (!isEmailPreferencesLoading()) {
-

{{ "settings.notifications.emailPreferences.title" | translate }}

+

{{ 'settings.notifications.emailPreferences.title' | translate }}

-
+
- +
- {{ "settings.notifications.emailPreferences.items.general.description"| translate }} + {{ 'settings.notifications.emailPreferences.items.general.description' | translate }}
- +
- {{ "settings.notifications.emailPreferences.items.help.description"| translate }} + {{ 'settings.notifications.emailPreferences.items.help.description' | translate }}
- +
@@ -71,19 +73,22 @@

{{ "settings.notifications.emailPreferences.title" | translate }}

@if (!isNotificationSubscriptionsLoading()) {
-

{{ "settings.notifications.notificationPreferences.title" | translate }}

+

{{ 'settings.notifications.notificationPreferences.title' | translate }}

-

{{ "settings.notifications.notificationPreferences.note" | translate }}

+

{{ 'settings.notifications.notificationPreferences.note' | translate }}

@for (subscriptionEvent of SUBSCRIPTION_EVENTS; track $index) {

{{ subscriptionEvent.labelKey | translate }}

- + }