Skip to content

Commit bf56c47

Browse files
authored
[ENG-10047] Display Affiliated Institution(s) on User Profile Page (#859)
- Ticket: https://openscience.atlassian.net/browse/ENG-10047 - Feature flag: n/a ## Purpose User profile pages do not currently display a user’s affiliated institution(s), even when the user has active institutional affiliations set in OSF. This makes it difficult for others to understand a user’s institutional context and reduces the visibility of institutional participation on the platform. ## Summary of Changes Implement affiliated Institution(s) on User Profile Page showing
1 parent 26f117e commit bf56c47

11 files changed

Lines changed: 72 additions & 8 deletions

File tree

src/app/features/profile/components/profile-information/profile-information.component.html

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,25 @@ <h1>{{ currentUser()?.fullName }}</h1>
3535
}
3636
</div>
3737

38+
<div class="flex flex-wrap align-items-center gap-3">
39+
@for (institution of currentUserInstitutions(); track $index) {
40+
<a
41+
class="cursor-pointer custom-light-hover"
42+
[routerLink]="['/institutions', institution.id]"
43+
target="_blank"
44+
rel="noopener noreferrer"
45+
>
46+
<img
47+
[ngSrc]="institution.assets.logo"
48+
class="fit-contain"
49+
width="80"
50+
height="80"
51+
[alt]="institution.name"
52+
/>
53+
</a>
54+
}
55+
</div>
56+
3857
@if (!isMedium() && showEdit()) {
3958
<div class="btn-full-width">
4059
<p-button

src/app/features/profile/components/profile-information/profile-information.component.spec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
77
import { EducationHistoryComponent } from '@osf/shared/components/education-history/education-history.component';
88
import { EmploymentHistoryComponent } from '@osf/shared/components/employment-history/employment-history.component';
99
import { IS_MEDIUM } from '@osf/shared/helpers/breakpoints.tokens';
10+
import { Institution } from '@shared/models/institutions/institutions.models';
1011
import { SocialModel } from '@shared/models/user/social.model';
1112
import { UserModel } from '@shared/models/user/user.models';
1213

1314
import { ProfileInformationComponent } from './profile-information.component';
1415

1516
import { MOCK_USER } from '@testing/mocks/data.mock';
17+
import { MOCK_INSTITUTION } from '@testing/mocks/institution.mock';
1618
import { MOCK_EDUCATION, MOCK_EMPLOYMENT } from '@testing/mocks/user-employment-education.mock';
1719
import { OSFTestingModule } from '@testing/osf.testing.module';
1820

@@ -44,6 +46,7 @@ describe('ProfileInformationComponent', () => {
4446
it('should initialize with default inputs', () => {
4547
expect(component.currentUser()).toBeUndefined();
4648
expect(component.showEdit()).toBe(false);
49+
expect(component.currentUserInstitutions()).toBeUndefined();
4750
});
4851

4952
it('should accept user input', () => {
@@ -172,4 +175,28 @@ describe('ProfileInformationComponent', () => {
172175
component.toProfileSettings();
173176
expect(component.editProfile.emit).toHaveBeenCalled();
174177
});
178+
179+
it('should accept currentUserInstitutions input', () => {
180+
const mockInstitutions: Institution[] = [MOCK_INSTITUTION];
181+
fixture.componentRef.setInput('currentUserInstitutions', mockInstitutions);
182+
fixture.detectChanges();
183+
expect(component.currentUserInstitutions()).toEqual(mockInstitutions);
184+
});
185+
186+
it('should not render institution logos when currentUserInstitutions is undefined', () => {
187+
fixture.componentRef.setInput('currentUserInstitutions', undefined);
188+
fixture.detectChanges();
189+
const logos = fixture.nativeElement.querySelectorAll('img.fit-contain');
190+
expect(logos.length).toBe(0);
191+
});
192+
193+
it('should render institution logos when currentUserInstitutions is provided', () => {
194+
const institutions: Institution[] = [MOCK_INSTITUTION];
195+
fixture.componentRef.setInput('currentUserInstitutions', institutions);
196+
fixture.detectChanges();
197+
198+
const logos = fixture.nativeElement.querySelectorAll('img.fit-contain');
199+
expect(logos.length).toBe(institutions.length);
200+
expect(logos[0].alt).toBe(institutions[0].name);
201+
});
175202
});

src/app/features/profile/components/profile-information/profile-information.component.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ import { Button } from 'primeng/button';
55
import { DatePipe, NgOptimizedImage } from '@angular/common';
66
import { ChangeDetectionStrategy, Component, computed, inject, input, output } from '@angular/core';
77
import { toSignal } from '@angular/core/rxjs-interop';
8+
import { RouterLink } from '@angular/router';
89

910
import { EducationHistoryComponent } from '@osf/shared/components/education-history/education-history.component';
1011
import { EmploymentHistoryComponent } from '@osf/shared/components/employment-history/employment-history.component';
1112
import { SOCIAL_LINKS } from '@osf/shared/constants/social-links.const';
1213
import { IS_MEDIUM } from '@osf/shared/helpers/breakpoints.tokens';
1314
import { UserModel } from '@osf/shared/models/user/user.models';
1415
import { SortByDatePipe } from '@osf/shared/pipes/sort-by-date.pipe';
16+
import { Institution } from '@shared/models/institutions/institutions.models';
1517

1618
import { mapUserSocials } from '../../helpers';
1719

@@ -25,13 +27,16 @@ import { mapUserSocials } from '../../helpers';
2527
DatePipe,
2628
NgOptimizedImage,
2729
SortByDatePipe,
30+
RouterLink,
2831
],
2932
templateUrl: './profile-information.component.html',
3033
styleUrl: './profile-information.component.scss',
3134
changeDetection: ChangeDetectionStrategy.OnPush,
3235
})
3336
export class ProfileInformationComponent {
3437
currentUser = input<UserModel | null>();
38+
39+
currentUserInstitutions = input<Institution[]>();
3540
showEdit = input(false);
3641
editProfile = output<void>();
3742

src/app/features/profile/profile.component.html

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@
1313
</ng-template>
1414
</p-message>
1515
}
16-
<osf-profile-information [currentUser]="user()" [showEdit]="isMyProfile()" (editProfile)="toProfileSettings()" />
16+
<osf-profile-information
17+
[currentUserInstitutions]="institutions() || []"
18+
[currentUser]="user()"
19+
[showEdit]="isMyProfile()"
20+
(editProfile)="toProfileSettings()"
21+
/>
1722
</div>
1823

1924
@if (defaultSearchFiltersInitialized()) {

src/app/features/profile/profile.component.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { SEARCH_TAB_OPTIONS } from '@osf/shared/constants/search-tab-options.con
2525
import { ResourceType } from '@osf/shared/enums/resource-type.enum';
2626
import { UserModel } from '@osf/shared/models/user/user.models';
2727
import { SetDefaultFilterValue } from '@osf/shared/stores/global-search';
28+
import { FetchUserInstitutions, InstitutionsSelectors } from '@shared/stores/institutions';
2829

2930
import { ProfileInformationComponent } from './components';
3031
import { FetchUserProfile, ProfileSelectors, SetUserProfile } from './store';
@@ -46,11 +47,13 @@ export class ProfileComponent implements OnInit, OnDestroy {
4647
fetchUserProfile: FetchUserProfile,
4748
setDefaultFilterValue: SetDefaultFilterValue,
4849
setUserProfile: SetUserProfile,
50+
fetchUserInstitutions: FetchUserInstitutions,
4951
});
5052

5153
loggedInUser = select(UserSelectors.getCurrentUser);
5254
userProfile = select(ProfileSelectors.getUserProfile);
5355
isUserLoading = select(ProfileSelectors.isUserProfileLoading);
56+
institutions = select(InstitutionsSelectors.getUserInstitutions);
5457

5558
resourceTabOptions = SEARCH_TAB_OPTIONS.filter((x) => x.value !== ResourceType.Agent);
5659

@@ -67,6 +70,8 @@ export class ProfileComponent implements OnInit, OnDestroy {
6770
} else if (currentUser) {
6871
this.setupMyProfile(currentUser);
6972
}
73+
74+
this.actions.fetchUserInstitutions(userId || currentUser?.id);
7075
}
7176

7277
ngOnDestroy(): void {

src/app/features/settings/account-settings/store/account-settings.actions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ export class DeleteExternalIdentity {
2424

2525
export class GetUserInstitutions {
2626
static readonly type = '[AccountSettings] Get User Institutions';
27+
28+
constructor(public userId = 'me') {}
2729
}
2830

2931
export class DeleteUserInstitution {

src/app/features/settings/account-settings/store/account-settings.state.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,8 @@ export class AccountSettingsState {
8484
}
8585

8686
@Action(GetUserInstitutions)
87-
getUserInstitutions(ctx: StateContext<AccountSettingsStateModel>) {
88-
return this.institutionsService.getUserInstitutions().pipe(
87+
getUserInstitutions(ctx: StateContext<AccountSettingsStateModel>, action: GetUserInstitutions) {
88+
return this.institutionsService.getUserInstitutions(action.userId).pipe(
8989
tap((userInstitutions) => ctx.patchState({ userInstitutions })),
9090
catchError((error) => throwError(() => error))
9191
);

src/app/shared/components/add-project-form/add-project-form.component.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ import { FormControl, FormGroup, Validators } from '@angular/forms';
99
import { UserSelectors } from '@core/store/user';
1010
import { ProjectFormControls } from '@osf/shared/enums/create-project-form-controls.enum';
1111
import { CustomValidators } from '@osf/shared/helpers/custom-form-validators.helper';
12-
import { ProjectModel } from '@osf/shared/models/projects';
1312
import { InstitutionsSelectors } from '@osf/shared/stores/institutions';
1413
import { ProjectsSelectors } from '@osf/shared/stores/projects';
1514
import { RegionsSelectors } from '@osf/shared/stores/regions';
1615
import { ProjectForm } from '@shared/models/projects/create-project-form.model';
16+
import { ProjectModel } from '@shared/models/projects/projects.models';
1717

1818
import { AffiliatedInstitutionSelectComponent } from '../affiliated-institution-select/affiliated-institution-select.component';
1919
import { ProjectSelectorComponent } from '../project-selector/project-selector.component';

src/app/shared/services/institutions.service.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ export class InstitutionsService {
4747
.pipe(map((response) => InstitutionsMapper.fromResponseWithMeta(response)));
4848
}
4949

50-
getUserInstitutions(): Observable<Institution[]> {
51-
const url = `${this.apiUrl}/users/me/institutions/`;
50+
getUserInstitutions(userId: string): Observable<Institution[]> {
51+
const url = `${this.apiUrl}/users/${userId}/institutions/`;
5252

5353
return this.jsonApiService
5454
.get<InstitutionsJsonApiResponse>(url)

src/app/shared/stores/institutions/institutions.actions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Institution } from '@shared/models/institutions/institutions.models';
33

44
export class FetchUserInstitutions {
55
static readonly type = '[Institutions] Fetch User Institutions';
6+
constructor(public userId = 'me') {}
67
}
78

89
export class FetchInstitutions {

0 commit comments

Comments
 (0)