diff --git a/ui/src/app/account/login/login.component.spec.ts b/ui/src/app/account/login/login.component.spec.ts index e6db57f6f..90ea33014 100644 --- a/ui/src/app/account/login/login.component.spec.ts +++ b/ui/src/app/account/login/login.component.spec.ts @@ -27,7 +27,7 @@ describe('LoginComponent', () => { { provide: StateStorageService, useValue: stateStorageServiceSpy }, { provide: AccountService, useValue: accountServiceSpy }, ], - }).compileComponents() + }) fixture = TestBed.createComponent(LoginComponent) component = fixture.componentInstance diff --git a/ui/src/app/account/service/account.service.ts b/ui/src/app/account/service/account.service.ts index 4144bc03c..13945b1ad 100644 --- a/ui/src/app/account/service/account.service.ts +++ b/ui/src/app/account/service/account.service.ts @@ -21,14 +21,12 @@ export class AccountService { private languageService: LanguageService, private sessionStorage: SessionStorageService, private router: Router, - private http: HttpClient - ) // TODO: uncomment when memberservice is added or change the account service so that this logic is absent from the account service - //private memberService: MSMemberService + private http: HttpClient // TODO: uncomment when memberservice is added or change the account service so that this logic is absent from the account service + ) //private memberService: MSMemberService {} private fetchAccountData() { - console.log('Fetching account data from the back end') - + this.isFetchingAccountData = true return this.http .get('/services/userservice/api/account', { observe: 'response', @@ -139,7 +137,6 @@ export class AccountService { this.stopFetchingAccountData.next(true) } if ((this.accountData.value === undefined && !this.isFetchingAccountData) || force) { - this.isFetchingAccountData = true this.fetchAccountData().subscribe() } diff --git a/ui/src/app/app.module.ts b/ui/src/app/app.module.ts index cce40b255..442050540 100644 --- a/ui/src/app/app.module.ts +++ b/ui/src/app/app.module.ts @@ -15,20 +15,13 @@ import { HeaderInterceptor } from './shared/interceptor/header.interceptor' import { ErrorService } from './error/service/error.service' import { ErrorComponent } from './error/error.component' import { FormsModule } from '@angular/forms' -import { UserModule } from './user/user.module' -import { AffiliationModule } from './affiliation/affiliation.module' -import { MembersComponent } from './member/members.component' -import { MemberModule } from './member/member.module' @NgModule({ declarations: [AppComponent, NavbarComponent, FooterComponent, ErrorComponent], imports: [ BrowserModule, - UserModule, - AffiliationModule, - MemberModule, - AccountModule, HomeModule, + AccountModule, HttpClientModule, AppRoutingModule, NgxWebstorageModule.forRoot(), diff --git a/ui/src/app/home/generic-landing.component.html b/ui/src/app/home/generic-landing.component.html new file mode 100644 index 000000000..33b55f35e --- /dev/null +++ b/ui/src/app/home/generic-landing.component.html @@ -0,0 +1,14 @@ +
+

Something has gone wrong...

+

+ We can't display your member details at the moment. Please try refreshing your browser window to see if that fixes + the problem. +

+

+ If your member details are still not being displayed please contact + membership@orcid.org + for more help. +

+
diff --git a/ui/src/app/home/generic-landing.component.scss b/ui/src/app/home/generic-landing.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/src/app/home/generic-landing.component.spec.ts b/ui/src/app/home/generic-landing.component.spec.ts new file mode 100644 index 000000000..46590901f --- /dev/null +++ b/ui/src/app/home/generic-landing.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { GenericLandingComponent } from './generic-landing.component'; + +describe('GenericLandingComponent', () => { + let component: GenericLandingComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [GenericLandingComponent] + }); + fixture = TestBed.createComponent(GenericLandingComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/ui/src/app/home/generic-landing.component.ts b/ui/src/app/home/generic-landing.component.ts new file mode 100644 index 000000000..d9c22bb47 --- /dev/null +++ b/ui/src/app/home/generic-landing.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-generic-landing', + templateUrl: './generic-landing.component.html', + styleUrls: ['./generic-landing.component.scss'] +}) +export class GenericLandingComponent { + +} diff --git a/ui/src/app/home/home.component.html b/ui/src/app/home/home.component.html index 5f2c53ffd..547ec8173 100644 --- a/ui/src/app/home/home.component.html +++ b/ui/src/app/home/home.component.html @@ -1 +1,13 @@ -

home works!

+
+
+
+
+
{{ loggedInMessage }}
+
+ + +
+
+
+
+
diff --git a/ui/src/app/home/home.component.scss b/ui/src/app/home/home.component.scss index e69de29bb..c6e17cf05 100644 --- a/ui/src/app/home/home.component.scss +++ b/ui/src/app/home/home.component.scss @@ -0,0 +1,12 @@ +:host { + max-width: 1250px; + display: block; + margin: auto; + height: 100%; + } + + .home-container { + border: 2px solid #eeeeee; + border-radius: 5px 0px 0px 5px; + } + \ No newline at end of file diff --git a/ui/src/app/home/home.component.spec.ts b/ui/src/app/home/home.component.spec.ts index 55d817b9d..42d2c88ab 100644 --- a/ui/src/app/home/home.component.spec.ts +++ b/ui/src/app/home/home.component.spec.ts @@ -1,21 +1,69 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' import { HomeComponent } from './home.component' +import { MemberService } from '../member/service/member.service' +import { AccountService } from '../account' +import { of } from 'rxjs' describe('HomeComponent', () => { let component: HomeComponent let fixture: ComponentFixture + let memberServiceSpy: jasmine.SpyObj + let accountServiceSpy: jasmine.SpyObj beforeEach(() => { + accountServiceSpy = jasmine.createSpyObj('AccountService', ['getAccountData']) + memberServiceSpy = jasmine.createSpyObj('MemberService', ['getMemberData']) + TestBed.configureTestingModule({ declarations: [HomeComponent], + providers: [ + { provide: MemberService, useValue: memberServiceSpy }, + { provide: AccountService, useValue: accountServiceSpy }, + ], }) fixture = TestBed.createComponent(HomeComponent) component = fixture.componentInstance - fixture.detectChanges() + + accountServiceSpy = TestBed.inject(AccountService) as jasmine.SpyObj + memberServiceSpy = TestBed.inject(MemberService) as jasmine.SpyObj }) - it('should create', () => { + it('should call getAccountData but not getMemberData', () => { + accountServiceSpy.getAccountData.and.returnValue(of(null)) + expect(component).toBeTruthy() + + component.ngOnInit() + + expect(accountServiceSpy.getAccountData).toHaveBeenCalled() + expect(memberServiceSpy.getMemberData).toHaveBeenCalledTimes(0) + }) + + it('should call getMemberData if account data is not null', () => { + accountServiceSpy.getAccountData.and.returnValue( + of({ + activated: true, + authorities: ['test', 'test'], + email: 'email@email.com', + firstName: 'name', + langKey: 'en', + lastName: 'surname', + imageUrl: 'url', + salesforceId: 'sfid', + loggedAs: false, + loginAs: 'sfid', + mainContact: false, + mfaEnabled: false, + }) + ) + memberServiceSpy.getMemberData.and.returnValue(of({})) + + expect(component).toBeTruthy() + + component.ngOnInit() + + expect(accountServiceSpy.getAccountData).toHaveBeenCalled() + expect(memberServiceSpy.getMemberData).toHaveBeenCalled() }) }) diff --git a/ui/src/app/home/home.component.ts b/ui/src/app/home/home.component.ts index 68fb47086..cb1168db3 100644 --- a/ui/src/app/home/home.component.ts +++ b/ui/src/app/home/home.component.ts @@ -1,8 +1,50 @@ -import { Component } from '@angular/core' +import { Component, OnDestroy, OnInit } from '@angular/core' +import { AccountService } from '../account' +import { MemberService } from '../member/service/member.service' +import { Subscription } from 'rxjs/internal/Subscription' +import { ISFMemberData } from '../member/model/salesforce-member-data.model' +import { IAccount } from '../account/model/account.model' @Component({ selector: 'app-home', templateUrl: './home.component.html', styleUrls: ['./home.component.scss'], }) -export class HomeComponent {} +export class HomeComponent implements OnInit, OnDestroy { + account: IAccount | undefined | null + memberData: ISFMemberData | undefined | null + authenticationStateSubscription: Subscription | undefined + memberDataSubscription: Subscription | undefined + manageMemberSubscription: Subscription | undefined + salesforceId: string | undefined + loggedInMessage: string | undefined + + constructor( + private accountService: AccountService, + private memberService: MemberService + ) {} + + ngOnInit() { + this.accountService.getAccountData().subscribe((account) => { + this.account = account + if (account) { + this.memberDataSubscription = this.memberService.getMemberData(account.salesforceId).subscribe((data) => { + this.memberData = data + }) + this.loggedInMessage = $localize`:@@home.loggedIn.message.string:You are logged in as user ${account.email}` + } + }) + } + + ngOnDestroy() { + if (this.authenticationStateSubscription) { + this.authenticationStateSubscription.unsubscribe() + } + if (this.memberDataSubscription) { + this.memberDataSubscription.unsubscribe() + } + if (this.manageMemberSubscription) { + this.manageMemberSubscription.unsubscribe() + } + } +} diff --git a/ui/src/app/home/home.module.ts b/ui/src/app/home/home.module.ts index fdf6e1f58..a5e185ea5 100644 --- a/ui/src/app/home/home.module.ts +++ b/ui/src/app/home/home.module.ts @@ -3,9 +3,10 @@ import { CommonModule } from '@angular/common' import { RouterModule } from '@angular/router' import { routes } from './home.route' import { HomeComponent } from './home.component' +import { GenericLandingComponent } from './generic-landing.component' @NgModule({ - declarations: [HomeComponent], imports: [CommonModule, RouterModule.forChild(routes)], + declarations: [HomeComponent, GenericLandingComponent], }) export class HomeModule {} diff --git a/ui/src/app/member/service/member.service.ts b/ui/src/app/member/service/member.service.ts index 33b5e06a4..7b88ee5c6 100644 --- a/ui/src/app/member/service/member.service.ts +++ b/ui/src/app/member/service/member.service.ts @@ -1,10 +1,34 @@ import { Injectable } from '@angular/core' -import { BehaviorSubject, Observable, of, map, catchError } from 'rxjs' +import { + BehaviorSubject, + Observable, + of, + map, + catchError, + Subject, + switchMap, + throwError, + takeUntil, + tap, + combineLatest, + EMPTY, + filter, +} from 'rxjs' import { HttpClient, HttpResponse } from '@angular/common/http' import { IMember } from '../model/member.model' import * as moment from 'moment' import { createRequestOption } from 'src/app/shared/request-util' import { IMemberPage, MemberPage } from '../model/member-page.model' +import { + ISFMemberData, + ISFRawConsortiumMemberData, + ISFRawMemberData, + SFConsortiumMemberData, + SFMemberData, +} from '../model/salesforce-member-data.model' +import { ISFCountry } from '../model/salesforce-country.model' +import { ISFRawMemberContact, ISFRawMemberContacts, SFMemberContact } from '../model/salesforce-member-contact.model' +import { ISFRawMemberOrgIds, SFMemberOrgIds } from '../model/salesforce-member-org-id.model' @Injectable({ providedIn: 'root' }) export class MemberService { @@ -13,6 +37,11 @@ export class MemberService { public resourceUrl = '/services/memberservice/api' public managedMember = new BehaviorSubject(null) + private memberData = new BehaviorSubject(undefined) + private fetchingMemberDataState = false + public stopFetchingMemberData = new Subject() + private countries = new BehaviorSubject(undefined) + find(id: string): Observable { return this.http.get(`${this.resourceUrl}/members/${id}`).pipe( map((res: IMember) => this.convertDateFromServer(res)), @@ -103,4 +132,224 @@ export class MemberService { } return null } + + getMemberData(salesforceId: string, force?: boolean): Observable { + if (force) { + this.stopFetchingMemberData.next(true) + } + + if (!this.memberData.value || this.memberData.value.id !== this.managedMember.value || force) { + this.fetchMemberData(salesforceId) + } + + return this.memberData.asObservable() + } + + fetchMemberData(salesforceId: string) { + this.fetchingMemberDataState = true + this.getSFMemberData(salesforceId) + .pipe( + switchMap((res) => { + this.memberData.next(res) + return combineLatest([ + this.getMemberContacts(salesforceId), + this.getMemberOrgIds(salesforceId), + this.getConsortiaLeadName(res.consortiaLeadId!), + this.getIsConsortiumLead(salesforceId), + ]) + }), + tap((res) => { + this.fetchingMemberDataState = false + }), + catchError(() => { + this.memberData.next(null) + this.fetchingMemberDataState = false + return EMPTY + }) + ) + .subscribe() + } + + getMemberContacts(salesforceId: string): Observable { + return this.http + .get(`${this.resourceUrl}/members/${salesforceId}/member-contacts`, { observe: 'response' }) + .pipe( + takeUntil(this.stopFetchingMemberData), + map((res: HttpResponse) => this.convertToSalesforceMemberContacts(res)), + tap((res) => this.memberData.next({ ...this.memberData.value, contacts: res })), + catchError((err) => { + return of(err) + }) + ) + } + + getSFMemberData(salesforceId: string): Observable { + return this.http + .get(`${this.resourceUrl}/members/${salesforceId}/member-details`, { observe: 'response' }) + .pipe( + catchError((err) => { + return of(err) + }), + map((res: HttpResponse) => this.convertToSalesforceMemberData(res)), + switchMap((value) => { + if (value && !value.id) { + return throwError(value) + } else { + return of(value) + } + }) + ) + } + + getMemberOrgIds(salesforceId: string): Observable { + return this.http + .get(`${this.resourceUrl}/members/${salesforceId}/member-org-ids`, { observe: 'response' }) + .pipe( + takeUntil(this.stopFetchingMemberData), + map((res: HttpResponse) => this.convertToMemberOrgIds(res)), + filter((res): res is SFMemberOrgIds => !!res), + tap((res) => this.memberData.next({ ...this.memberData.value, orgIds: res })), + catchError((err) => { + return of(err) + }) + ) + } + + getConsortiaLeadName(consortiaLeadId: string): Observable { + if (consortiaLeadId) { + return this.find(consortiaLeadId).pipe( + tap((member) => { + if (member) { + this.memberData.next({ ...this.memberData.value, consortiumLeadName: member.clientName }) + } + }) + ) + } + return of(null) + } + + getIsConsortiumLead(salesforceId: string): Observable { + if (salesforceId) { + return this.find(salesforceId).pipe( + tap((member) => { + if (member) { + const { isConsortiumLead } = member + this.memberData.next({ ...this.memberData.value, isConsortiumLead }) + } + }) + ) + } + return of(null) + } + + private convertToSalesforceMemberData(res: HttpResponse): SFMemberData { + if (res.body) { + return { + ...new SFMemberData(), + id: res.body.Id, + consortiaMember: res.body.Consortia_Member__c, + consortiaLeadId: res.body.Consortium_Lead__c, + name: res.body.Name, + publicDisplayName: res.body.Public_Display_Name__c, + website: res.body.Website, + billingCountry: res.body.BillingCountry, + memberType: res.body.Research_Community__c, + publicDisplayDescriptionHtml: res.body.Public_Display_Description__c, + logoUrl: res.body.Logo_Description__c, + publicDisplayEmail: res.body.Public_Display_Email__c, + membershipStartDateString: res.body.Last_membership_start_date__c, + membershipEndDateString: res.body.Last_membership_end_date__c, + consortiumMembers: res.body.consortiumOpportunities + ? this.convertToConsortiumMembers(res.body.consortiumOpportunities) + : undefined, + billingAddress: res.body.BillingAddress, + trademarkLicense: res.body.Trademark_License__c, + } + } else { + return new SFMemberData() + } + } + + private convertToConsortiumMembers(consortiumOpportunities: ISFRawConsortiumMemberData[]): SFConsortiumMemberData[] { + const consortiumMembers: SFConsortiumMemberData[] = [] + for (const consortiumOpportunity of consortiumOpportunities) { + consortiumMembers.push(this.convertToConsortiumMember(consortiumOpportunity)) + } + return consortiumMembers + } + + private convertToConsortiumMember(consortiumOpportunity: ISFRawConsortiumMemberData): SFConsortiumMemberData { + const consortiumMember: SFConsortiumMemberData = new SFConsortiumMemberData() + consortiumMember.orgName = consortiumOpportunity?.Account?.Public_Display_Name__c + consortiumMember.salesforceId = consortiumOpportunity.AccountId + return consortiumMember + } + + private convertToSalesforceMemberContacts(res: HttpResponse): SFMemberContact[] { + const contacts: { [email: string]: SFMemberContact } = {} + if (res.body && res.body.records && res.body.records.length > 0) { + for (const contact of res.body.records) { + // Merge contacts with different roles to a single entry if they have a matching email + if (contact.Contact_Curr_Email__c) { + if (!contacts[contact.Contact_Curr_Email__c]) { + contacts[contact.Contact_Curr_Email__c] = this.convertToSalesforceMemberContact(contact) + } + if (contact.Voting_Contact__c) { + contacts[contact.Contact_Curr_Email__c].memberOrgRole!.push('Voting contact') + } + if (contact.Member_Org_Role__c) { + contacts[contact.Contact_Curr_Email__c].memberOrgRole!.unshift(contact.Member_Org_Role__c) + } + } + } + return Object.values(contacts) + } else { + return [] + } + } + + private convertToSalesforceMemberContact(res: ISFRawMemberContact): SFMemberContact { + return { + ...new SFMemberContact(), + memberId: res.Organization__c, + votingContact: res.Voting_Contact__c, + name: res.Name, + contactEmail: res.Contact_Curr_Email__c, + phone: res.Phone, + title: res.Title, + } + } + + private convertToMemberOrgIds(res: HttpResponse): SFMemberOrgIds | null { + if (res.body && res.body.records && res.body.records.length > 0) { + const ids = res.body.records + const ROR = [], + GRID = [], + Ringgold = [], + Fundref = [] + for (let i = 0; i < ids.length; i++) { + if (ids[i].Identifier_Type__c === 'ROR') { + ROR.push(ids[i].Name) + } + if (ids[i].Identifier_Type__c === 'GRID') { + GRID.push(ids[i].Name) + } + if (ids[i].Identifier_Type__c === 'Ringgold ID') { + Ringgold.push(ids[i].Name) + } + if (ids[i].Identifier_Type__c === 'FundRef ID') { + Fundref.push(ids[i].Name) + } + } + return { + ...new SFMemberOrgIds(), + ROR, + GRID, + Ringgold, + Fundref, + } + } else { + return null + } + } }