Skip to content

Commit d454e20

Browse files
authored
Fix/improvements (CenterForOpenScience#310)
* fix(meetings): fixed meetings small issues * fix(tooltips): added tooltips * fix(table): updated sorting * fix(settings): fixed update project * fix(bookmarks): updated bookmarks * fix(my-registrations): fixed my registrations * fix(developer-apps): fixed developer apps * fix(settings): updated tokens and notifications * fix(translation): removed dot * fix(info-icon): updated info icon translate * fix(profile-settings): fixed profile settings * fix(test): updated tests * fix(settings): updated settings * fix(user-emails): updated adding emails to user account * fix(tests): updated tests
1 parent 81b460d commit d454e20

File tree

55 files changed

+648
-552
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+648
-552
lines changed

src/app/app.component.spec.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
88
import { By } from '@angular/platform-browser';
99

1010
import { GetCurrentUser, UserState } from '@core/store/user';
11+
import { UserEmailsState } from '@core/store/user-emails';
1112

1213
import { FullScreenLoaderComponent, ToastComponent } from './shared/components';
14+
import { TranslateServiceMock } from './shared/mocks';
1315
import { AppComponent } from './app.component';
1416

1517
describe('AppComponent', () => {
@@ -19,7 +21,12 @@ describe('AppComponent', () => {
1921
beforeEach(async () => {
2022
await TestBed.configureTestingModule({
2123
imports: [AppComponent, ...MockComponents(ToastComponent, FullScreenLoaderComponent)],
22-
providers: [provideStore([UserState]), provideHttpClient(), provideHttpClientTesting()],
24+
providers: [
25+
provideStore([UserState, UserEmailsState]),
26+
provideHttpClient(),
27+
provideHttpClientTesting(),
28+
TranslateServiceMock,
29+
],
2330
}).compileComponents();
2431

2532
fixture = TestBed.createComponent(AppComponent);

src/app/app.component.ts

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
1-
import { createDispatchMap } from '@ngxs/store';
1+
import { createDispatchMap, select } from '@ngxs/store';
2+
3+
import { TranslateService } from '@ngx-translate/core';
4+
5+
import { DialogService } from 'primeng/dynamicdialog';
26

37
import { filter } from 'rxjs/operators';
48

5-
import { ChangeDetectionStrategy, Component, DestroyRef, inject, OnInit } from '@angular/core';
9+
import { ChangeDetectionStrategy, Component, DestroyRef, effect, inject, OnInit } from '@angular/core';
610
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
711
import { NavigationEnd, Router, RouterOutlet } from '@angular/router';
812

913
import { GetCurrentUser } from '@core/store/user';
14+
import { GetEmails, UserEmailsSelectors } from '@core/store/user-emails';
15+
import { ConfirmEmailComponent } from '@shared/components';
1016

1117
import { FullScreenLoaderComponent, ToastComponent } from './shared/components';
1218
import { MetaTagsService } from './shared/services/meta-tags.service';
@@ -17,23 +23,31 @@ import { MetaTagsService } from './shared/services/meta-tags.service';
1723
templateUrl: './app.component.html',
1824
styleUrl: './app.component.scss',
1925
changeDetection: ChangeDetectionStrategy.OnPush,
26+
providers: [DialogService],
2027
})
2128
export class AppComponent implements OnInit {
22-
private destroyRef = inject(DestroyRef);
29+
private readonly destroyRef = inject(DestroyRef);
30+
private readonly dialogService = inject(DialogService);
31+
private readonly router = inject(Router);
32+
private readonly translateService = inject(TranslateService);
33+
private readonly metaTagsService = inject(MetaTagsService);
34+
35+
private readonly actions = createDispatchMap({ getCurrentUser: GetCurrentUser, getEmails: GetEmails });
2336

24-
actions = createDispatchMap({
25-
getCurrentUser: GetCurrentUser,
26-
});
37+
unverifiedEmails = select(UserEmailsSelectors.getUnverifiedEmails);
2738

28-
constructor(
29-
private router: Router,
30-
private metaTagsService: MetaTagsService
31-
) {
39+
constructor() {
3240
this.setupMetaTagsCleanup();
41+
effect(() => {
42+
if (this.unverifiedEmails().length) {
43+
this.showEmailDialog();
44+
}
45+
});
3346
}
3447

3548
ngOnInit(): void {
3649
this.actions.getCurrentUser();
50+
this.actions.getEmails();
3751
}
3852

3953
private setupMetaTagsCleanup(): void {
@@ -44,4 +58,15 @@ export class AppComponent implements OnInit {
4458
)
4559
.subscribe((event: NavigationEnd) => this.metaTagsService.clearMetaTagsIfNeeded(event.url));
4660
}
61+
62+
private showEmailDialog() {
63+
this.dialogService.open(ConfirmEmailComponent, {
64+
width: '448px',
65+
focusOnShow: false,
66+
header: this.translateService.instant('home.confirmEmail.title'),
67+
modal: true,
68+
closable: false,
69+
data: this.unverifiedEmails(),
70+
});
71+
}
4772
}

src/app/core/constants/ngxs-states.constant.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ProviderState } from '@core/store/provider';
22
import { UserState } from '@core/store/user';
3+
import { UserEmailsState } from '@core/store/user-emails';
34
import { FilesState } from '@osf/features/files/store';
45
import { ProjectMetadataState } from '@osf/features/project/metadata/store';
56
import { ProjectOverviewState } from '@osf/features/project/overview/store';
@@ -13,6 +14,7 @@ import { RegionsState } from '@shared/stores/regions';
1314
export const STATES = [
1415
AddonsState,
1516
UserState,
17+
UserEmailsState,
1618
ProviderState,
1719
MyResourcesState,
1820
InstitutionsState,

src/app/core/services/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export { AuthService } from './auth.service';
22
export { RequestAccessService } from './request-access.service';
33
export { UserService } from './user.service';
4+
export { UserEmailsService } from './user-emails.service';
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { map, Observable } from 'rxjs';
2+
3+
import { inject, Injectable } from '@angular/core';
4+
5+
import { MapEmail, MapEmails } from '@osf/shared/mappers';
6+
import { AccountEmailModel, EmailResponseJsonApi, EmailsDataJsonApi, EmailsResponseJsonApi } from '@osf/shared/models';
7+
import { JsonApiService } from '@osf/shared/services';
8+
9+
import { environment } from 'src/environments/environment';
10+
11+
@Injectable({
12+
providedIn: 'root',
13+
})
14+
export class UserEmailsService {
15+
private readonly jsonApiService = inject(JsonApiService);
16+
private readonly baseUrl = `${environment.apiUrl}/users`;
17+
18+
getEmails(): Observable<AccountEmailModel[]> {
19+
const params: Record<string, string> = {
20+
page: '1',
21+
'page[size]': '10',
22+
};
23+
24+
return this.jsonApiService
25+
.get<EmailsResponseJsonApi>(`${this.baseUrl}/me/settings/emails/`, params)
26+
.pipe(map((response) => MapEmails(response.data)));
27+
}
28+
29+
resendConfirmation(emailId: string): Observable<AccountEmailModel> {
30+
const params: Record<string, string> = {
31+
resend_confirmation: 'true',
32+
};
33+
34+
return this.jsonApiService
35+
.get<EmailResponseJsonApi>(`${this.baseUrl}/me/settings/emails/${emailId}/`, params)
36+
.pipe(map((response) => MapEmail(response.data)));
37+
}
38+
39+
addEmail(userId: string, email: string): Observable<AccountEmailModel> {
40+
const body = {
41+
data: {
42+
attributes: {
43+
email_address: email,
44+
},
45+
relationships: {
46+
user: {
47+
data: {
48+
id: userId,
49+
type: 'users',
50+
},
51+
},
52+
},
53+
type: 'user_emails',
54+
},
55+
};
56+
57+
return this.jsonApiService
58+
.post<EmailResponseJsonApi>(`${this.baseUrl}/${userId}/settings/emails/`, body)
59+
.pipe(map((response) => MapEmail(response.data)));
60+
}
61+
62+
verifyEmail(emailId: string): Observable<AccountEmailModel> {
63+
const body = {
64+
data: {
65+
id: emailId,
66+
attributes: {
67+
verified: true,
68+
},
69+
type: 'user_emails',
70+
},
71+
};
72+
73+
return this.jsonApiService
74+
.patch<EmailsDataJsonApi>(`${this.baseUrl}/me/settings/emails/${emailId}/`, body)
75+
.pipe(map((response) => MapEmail(response)));
76+
}
77+
78+
makePrimary(emailId: string): Observable<AccountEmailModel> {
79+
const body = {
80+
data: {
81+
id: emailId,
82+
attributes: {
83+
primary: true,
84+
},
85+
type: 'user_emails',
86+
},
87+
};
88+
89+
return this.jsonApiService
90+
.patch<EmailsDataJsonApi>(`${this.baseUrl}/me/settings/emails/${emailId}/`, body)
91+
.pipe(map((response) => MapEmail(response)));
92+
}
93+
94+
deleteEmail(emailId: string): Observable<void> {
95+
return this.jsonApiService.delete(`${this.baseUrl}/me/settings/emails/${emailId}/`);
96+
}
97+
}

src/app/core/services/user.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
UserSettingsGetResponse,
1616
} from '@osf/shared/models';
1717

18-
import { JsonApiService } from '../../shared/services/json-api.service';
18+
import { JsonApiService } from '../../shared/services';
1919

2020
import { environment } from 'src/environments/environment';
2121

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export * from './user-emails.actions';
2+
export * from './user-emails.model';
3+
export * from './user-emails.selectors';
4+
export * from './user-emails.state';
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
export class GetEmails {
2+
static readonly type = '[UserEmails] Get Emails';
3+
}
4+
5+
export class AddEmail {
6+
static readonly type = '[UserEmails] Add Email';
7+
8+
constructor(public email: string) {}
9+
}
10+
11+
export class DeleteEmail {
12+
static readonly type = '[UserEmails] Remove Email';
13+
14+
constructor(public emailId: string) {}
15+
}
16+
17+
export class ResendConfirmation {
18+
static readonly type = '[UserEmails] Resend Confirmation';
19+
20+
constructor(public emailId: string) {}
21+
}
22+
23+
export class VerifyEmail {
24+
static readonly type = '[UserEmails] Verify Email';
25+
26+
constructor(public emailId: string) {}
27+
}
28+
29+
export class MakePrimary {
30+
static readonly type = '[UserEmails] Make Primary';
31+
32+
constructor(public emailId: string) {}
33+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { AccountEmailModel, AsyncStateModel } from '@shared/models';
2+
3+
export interface UserEmailsStateModel {
4+
emails: AsyncStateModel<AccountEmailModel[]>;
5+
}
6+
7+
export const USER_EMAILS_STATE_DEFAULTS: UserEmailsStateModel = {
8+
emails: {
9+
data: [],
10+
isLoading: false,
11+
error: null,
12+
isSubmitting: false,
13+
},
14+
};
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Selector } from '@ngxs/store';
2+
3+
import { AccountEmailModel } from '@osf/shared/models';
4+
5+
import { UserEmailsStateModel } from './user-emails.model';
6+
import { UserEmailsState } from './user-emails.state';
7+
8+
export class UserEmailsSelectors {
9+
@Selector([UserEmailsState])
10+
static getEmails(state: UserEmailsStateModel): AccountEmailModel[] {
11+
return state.emails.data;
12+
}
13+
14+
@Selector([UserEmailsState])
15+
static isEmailsLoading(state: UserEmailsStateModel): boolean {
16+
return state.emails.isLoading;
17+
}
18+
19+
@Selector([UserEmailsState])
20+
static isEmailsSubmitting(state: UserEmailsStateModel): boolean | undefined {
21+
return state.emails.isSubmitting;
22+
}
23+
24+
@Selector([UserEmailsState])
25+
static getUnverifiedEmails(state: UserEmailsStateModel): AccountEmailModel[] {
26+
return state.emails.data.filter((email) => email.confirmed && !email.verified);
27+
}
28+
}

0 commit comments

Comments
 (0)