diff --git a/ui/src/app/home/generic-landing.component.scss b/ui/src/app/home/generic-landing.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/ui/src/app/home/generic-landing.component.spec.ts b/ui/src/app/home/generic-landing.component.spec.ts deleted file mode 100644 index 46590901f..000000000 --- a/ui/src/app/home/generic-landing.component.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -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/home.component.html b/ui/src/app/home/home.component.html index 547ec8173..218a8bb02 100644 --- a/ui/src/app/home/home.component.html +++ b/ui/src/app/home/home.component.html @@ -3,10 +3,7 @@
{{ loggedInMessage }}
-
- - -
+
diff --git a/ui/src/app/home/home.component.scss b/ui/src/app/home/home.component.scss index c6e17cf05..b47a816ce 100644 --- a/ui/src/app/home/home.component.scss +++ b/ui/src/app/home/home.component.scss @@ -1,12 +1,6 @@ :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 + max-width: 1250px; + display: block; + margin: auto; + height: 100%; +} diff --git a/ui/src/app/home/home.component.ts b/ui/src/app/home/home.component.ts index cb1168db3..5fca0beef 100644 --- a/ui/src/app/home/home.component.ts +++ b/ui/src/app/home/home.component.ts @@ -28,9 +28,9 @@ export class HomeComponent implements OnInit, OnDestroy { this.accountService.getAccountData().subscribe((account) => { this.account = account if (account) { - this.memberDataSubscription = this.memberService.getMemberData(account.salesforceId).subscribe((data) => { + /* 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}` } }) @@ -40,11 +40,11 @@ export class HomeComponent implements OnInit, OnDestroy { if (this.authenticationStateSubscription) { this.authenticationStateSubscription.unsubscribe() } - if (this.memberDataSubscription) { + /* if (this.memberDataSubscription) { this.memberDataSubscription.unsubscribe() - } - if (this.manageMemberSubscription) { + } */ + /* 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 a5e185ea5..b8d49c2b6 100644 --- a/ui/src/app/home/home.module.ts +++ b/ui/src/app/home/home.module.ts @@ -3,10 +3,11 @@ 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' +import { MemberInfoComponent } from './member-info/member-info.component' +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome' @NgModule({ - imports: [CommonModule, RouterModule.forChild(routes)], - declarations: [HomeComponent, GenericLandingComponent], + imports: [CommonModule, RouterModule.forChild(routes), FontAwesomeModule], + declarations: [HomeComponent, MemberInfoComponent], }) export class HomeModule {} diff --git a/ui/src/app/home/home.route.ts b/ui/src/app/home/home.route.ts index 811895f76..07334cb94 100644 --- a/ui/src/app/home/home.route.ts +++ b/ui/src/app/home/home.route.ts @@ -1,6 +1,39 @@ -import { Routes } from '@angular/router' +import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, Routes } from '@angular/router' import { HomeComponent } from '../home/home.component' import { AuthGuard } from '../account/auth.guard' +import { MemberInfoComponent } from './member-info/member-info.component' +import { Injectable, inject } from '@angular/core' +import { MemberService } from '../member/service/member.service' +import { Observable, map } from 'rxjs' + +export const ManageMemberGuard = (route: ActivatedRouteSnapshot): Observable | boolean => { + const router = inject(Router) + const memberService = inject(MemberService) + + return memberService.getManagedMember().pipe( + map((salesforceId) => { + if (salesforceId) { + const segments = ['manage', salesforceId] + + if (route['routeConfig']?.path === 'edit') { + segments.push('edit') + } + + if (route['routeConfig']?.path === 'contact/new') { + segments.push('contact', 'new') + } + + if (route['routeConfig']?.path === 'contact/:contactId/edit') { + segments.push('contact', route.params?.['contactId'], 'edit') + } + + router.navigate(segments) + return false + } + return true + }) + ) +} export const routes: Routes = [ { @@ -11,5 +44,16 @@ export const routes: Routes = [ pageTitle: 'home.title.string', }, canActivate: [AuthGuard], + children: [ + { + path: '', + component: MemberInfoComponent, + data: { + authorities: ['ROLE_USER'], + pageTitle: 'home.title.string', + }, + canActivate: [ManageMemberGuard], + }, + ], }, ] diff --git a/ui/src/app/home/member-info/member-info.component.html b/ui/src/app/home/member-info/member-info.component.html new file mode 100644 index 000000000..39f615afc --- /dev/null +++ b/ui/src/app/home/member-info/member-info.component.html @@ -0,0 +1,291 @@ +
+ +
+
+

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. +

+
+
+ + +
+ +
+
+
+
+
Consortium/Parent organization:
+
+ {{ memberData.consortiumLeadName }} +
+ +
+
+
+
+
Membership:
+
Active
+
Inactive
+
+
+
+
+
Consortium lead
+
+
+
+ + +
+

{{ memberData.name }}

+ +
+
+
Public display name
+
{{ memberData.publicDisplayName }}
+
+
+ Your ORCID membership is currently inactive. Please contact your consortium lead or ORCID to reinstate your + membership. +
+
+ +
+
+
+ No organisation description added +
+
+ + +
+

Billing address

+
+ {{ memberData.billingAddress.street ? memberData.billingAddress.street + ', ' : '' }} + {{ memberData.billingAddress.city ? memberData.billingAddress.city + ', ' : '' }} + {{ memberData.billingAddress.state ? memberData.billingAddress.state + ', ' : '' }} + {{ memberData.billingAddress.postalCode ? memberData.billingAddress.postalCode + ', ' : '' }} + {{ memberData.billingAddress.country }} +
+
+ + +
+

Trademark license

+
+ + + + + YES - ORCID can use trademarked assets +
+
+ + + + NO - ORCID cannot use this organization's trademarked name and logos +
+
+ + +
+

Contacts

+ + Add new + Add a new contact + +
+
+

Name

+

Member roles

+

Email

+

Phone

+ +
+
+
+ +
+
+ + +
+

+ Consortium Members + ({{ memberData.consortiumMembers?.length }}) +

+ + Add new + Add new consortium member + +

Member name

+
+ +
+
+
+
+
diff --git a/ui/src/app/home/member-info/member-info.component.scss b/ui/src/app/home/member-info/member-info.component.scss new file mode 100644 index 000000000..7654dcb6f --- /dev/null +++ b/ui/src/app/home/member-info/member-info.component.scss @@ -0,0 +1,103 @@ +@use '../../../content/scss/bootstrap-variables' as global; + +.side-bar { + max-width: 250px; + height: 100%; + padding: 20px 22px 20px 20px; + flex: 1; + background-color: global.$gray-2; + border-right: 2px solid #eeeeee; +} +.main-section { + flex: 1; + padding: 20px 40px 20px 40px; +} +.side-bar-subsection { + div { + font-size: 0.9rem; + } + a { + font-weight: normal; + } + word-wrap: break-word; +} +.main-contacts { + font-size: 0.9rem; +} +.main-section-empty-description { + color: global.$black-transparent; +} + +.main-section-description { + line-height: 21px; +} + +.logo-container { + img { + width: 200px; + height: auto; + align-self: center; + } + width: 208px; + height: 208px; + display: flex; + justify-content: center; +} + +.contact { + > div, + h3 { + width: 10%; + text-align: left; + flex-grow: 1; + max-width: 200px; + font-size: 0.9rem; + align-self: center; + margin-bottom: 0px; + } + word-wrap: break-word; +} + +.edit-button { + &:active { + color: black !important; + background-color: white !important; + } + background: #ffffff; + border: 1px solid #a6ce39; + border-radius: 5px; + color: black; + padding: 12px 16px; +} + +.add-new-contact-icon { + width: 1.5rem; + height: 1.5rem; + margin-top: -3px; +} + +.contacts-edit-column { + max-width: 8% !important; +} + +h1 { + line-height: 60px; +} + +h2 { + line-height: 27px; +} + +h3 { + line-height: 150%; + margin-bottom: 0.25rem; +} + +.license-icon { + margin-top: -0.125rem; +} + +.home-container { + border: 2px solid #eeeeee; + border-radius: 5px 0px 0px 5px; +} diff --git a/ui/src/app/home/member-info/member-info.component.spec.ts b/ui/src/app/home/member-info/member-info.component.spec.ts new file mode 100644 index 000000000..e7cea2b5c --- /dev/null +++ b/ui/src/app/home/member-info/member-info.component.spec.ts @@ -0,0 +1,132 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { MemberInfoComponent } from './member-info.component' +import { AccountService } from 'src/app/account' +import { MemberService } from 'src/app/member/service/member.service' +import { RouterTestingModule } from '@angular/router/testing' +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core' +import { ActivatedRoute } from '@angular/router' +import { of } from 'rxjs' +import { IAccount } from 'src/app/account/model/account.model' + +describe('MemberInfoComponent', () => { + let component: MemberInfoComponent + let fixture: ComponentFixture + let accountService: jasmine.SpyObj + let memberService: jasmine.SpyObj + let activatedRoute: jasmine.SpyObj + + beforeEach(() => { + const accountServiceSpy = jasmine.createSpyObj('AccountService', ['getAccountData']) + const memberServiceSpy = jasmine.createSpyObj('MemberService', ['getMemberData', 'setManagedMember']) + TestBed.configureTestingModule({ + imports: [RouterTestingModule.withRoutes([])], + providers: [ + { provide: AccountService, useValue: accountServiceSpy }, + { provide: MemberService, useValue: memberServiceSpy }, + ], + declarations: [MemberInfoComponent], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }) + activatedRoute = TestBed.inject(ActivatedRoute) as jasmine.SpyObj + accountService = TestBed.inject(AccountService) as jasmine.SpyObj + memberService = TestBed.inject(MemberService) as jasmine.SpyObj + fixture = TestBed.createComponent(MemberInfoComponent) + component = fixture.componentInstance + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + + it('should not call the member service without a provided account', () => { + activatedRoute.params = of({ id: 'test' }) + accountService.getAccountData.and.returnValue(of(undefined)) + fixture.detectChanges() + + expect(accountService.getAccountData).toHaveBeenCalled() + expect(memberService.setManagedMember).toHaveBeenCalledTimes(0) + expect(component.managedMember).toEqual('test') + expect(memberService.getMemberData).toHaveBeenCalledTimes(0) + expect(component.memberData).toBeUndefined() + }) + + it('should call the member service while managing a member', () => { + activatedRoute.params = of({ id: 'test' }) + accountService.getAccountData.and.returnValue(of({ salesforceId: 'test2' } as IAccount)) + fixture.detectChanges() + + expect(memberService.setManagedMember).toHaveBeenCalledWith('test') + expect(memberService.getMemberData).toHaveBeenCalledWith('test') + }) + + it('should call the member service without managing a member', () => { + accountService.getAccountData.and.returnValue(of({ salesforceId: 'test2' } as IAccount)) + fixture.detectChanges() + + expect(memberService.setManagedMember).toHaveBeenCalledTimes(0) + expect(memberService.getMemberData).toHaveBeenCalledWith('test2') + }) + + it('should stop managing member', () => { + accountService.getAccountData.and.returnValue(of({ salesforceId: 'test2' } as IAccount)) + fixture.detectChanges() + + component.stopManagingMember() + expect(memberService.setManagedMember).toHaveBeenCalledWith(null) + expect(memberService.getMemberData).toHaveBeenCalledWith('test2', true) + }) + + it('member should be active', () => { + accountService.getAccountData.and.returnValue(of({ salesforceId: 'test2' } as IAccount)) + memberService.getMemberData.and.returnValue(of({ membershipEndDateString: '2050' })) + fixture.detectChanges() + + const res = component.isActive() + expect(res).toEqual(true) + }) + + it('member should be inactive', () => { + accountService.getAccountData.and.returnValue(of({ salesforceId: 'test2' } as IAccount)) + memberService.getMemberData.and.returnValue(of({ membershipEndDateString: '2022' })) + fixture.detectChanges() + + const res = component.isActive() + expect(res).toEqual(false) + }) + + it('test crossref id filter', () => { + let res = component.filterCRFID('test') + expect(res).toEqual('test') + res = component.filterCRFID('http://dx.doi.org/123') + expect(res).toEqual('123') + res = component.filterCRFID('https://dx.doi.org/12345') + expect(res).toEqual('12345') + res = component.filterCRFID('dx.doi.org/123456.123/123') + expect(res).toEqual('123456.123/123') + }) + + it('should add protocol to websites where it is missing', () => { + accountService.getAccountData.and.returnValue(of({ salesforceId: 'test' } as IAccount)) + memberService.getMemberData.and.returnValue(of({})) + fixture.detectChanges() + + expect(component.memberData).toBeDefined + expect(component.memberData!.website).toBeUndefined() + + component.validateUrl() + expect(component.memberData!.website).toBeUndefined() + + component.memberData!.website = 'example' + component.validateUrl() + expect(component.memberData!.website).toEqual('http://example') + + component.memberData!.website = 'example.com' + component.validateUrl() + expect(component.memberData!.website).toEqual('http://example.com') + + component.memberData!.website = 'https://example.com' + component.validateUrl() + expect(component.memberData!.website).toEqual('https://example.com') + }) +}) diff --git a/ui/src/app/home/member-info/member-info.component.ts b/ui/src/app/home/member-info/member-info.component.ts new file mode 100644 index 000000000..52d1f9152 --- /dev/null +++ b/ui/src/app/home/member-info/member-info.component.ts @@ -0,0 +1,81 @@ +import { Component, OnDestroy, OnInit } from '@angular/core' +import { ActivatedRoute, Router } from '@angular/router' +import { faPencilAlt, faTrashAlt } from '@fortawesome/free-solid-svg-icons' +import { EMPTY, Subject, Subscription, combineLatest } from 'rxjs' +import { switchMap, takeUntil } from 'rxjs/operators' +import { AccountService } from 'src/app/account' +import { IAccount } from 'src/app/account/model/account.model' +import { ISFMemberData } from 'src/app/member/model/salesforce-member-data.model' +import { MemberService } from 'src/app/member/service/member.service' + +@Component({ + selector: 'app-member-info', + templateUrl: './member-info.component.html', + styleUrls: ['member-info.component.scss'], +}) +export class MemberInfoComponent implements OnInit, OnDestroy { + account: IAccount | undefined + memberData: ISFMemberData | undefined | null + alertSubscription: Subscription | undefined + managedMember: string | undefined + destroy$ = new Subject() + faTrashAlt = faTrashAlt + faPencilAlt = faPencilAlt + constructor( + private memberService: MemberService, + private accountService: AccountService, + protected activatedRoute: ActivatedRoute, + protected router: Router + ) {} + + isActive() { + return this.memberData?.membershipEndDateString && new Date(this.memberData.membershipEndDateString) > new Date() + } + + filterCRFID(id: string) { + return id.replace(/^.*dx.doi.org\//g, '') + } + + validateUrl() { + if (this.memberData?.website && !/(http(s?)):\/\//i.test(this.memberData.website)) { + this.memberData.website = 'http://' + this.memberData.website + } + } + + ngOnInit() { + combineLatest([this.activatedRoute.params, this.accountService.getAccountData()]) + .pipe( + switchMap(([params, account]) => { + if (params['id']) { + this.managedMember = params['id'] + } + + if (account) { + this.account = account + if (this.managedMember) { + this.memberService.setManagedMember(params['id']) + return this.memberService.getMemberData(this.managedMember) + } else { + return this.memberService.getMemberData(account?.salesforceId) + } + } else { + return EMPTY + } + }), + takeUntil(this.destroy$) + ) + .subscribe((data) => { + this.memberData = data + }) + } + + stopManagingMember() { + this.memberService.setManagedMember(null) + this.memberService.getMemberData(this.account?.salesforceId, true) + } + + ngOnDestroy() { + this.destroy$.next(true) + this.destroy$.complete() + } +} diff --git a/ui/src/app/member/service/member.service.ts b/ui/src/app/member/service/member.service.ts index 7b88ee5c6..eb38bd3c7 100644 --- a/ui/src/app/member/service/member.service.ts +++ b/ui/src/app/member/service/member.service.ts @@ -133,19 +133,19 @@ export class MemberService { return null } - getMemberData(salesforceId: string, force?: boolean): Observable { + getMemberData(salesforceId?: string, force?: boolean): Observable { if (force) { this.stopFetchingMemberData.next(true) } - if (!this.memberData.value || this.memberData.value.id !== this.managedMember.value || force) { + if (salesforceId && (!this.memberData.value || this.memberData.value.id !== salesforceId || force)) { this.fetchMemberData(salesforceId) } return this.memberData.asObservable() } - fetchMemberData(salesforceId: string) { + private fetchMemberData(salesforceId: string) { this.fetchingMemberDataState = true this.getSFMemberData(salesforceId) .pipe(