Skip to content

Commit 92af918

Browse files
committed
Merge branch 'release/26.7.0'
2 parents 731e0d8 + e3933bd commit 92af918

17 files changed

Lines changed: 271 additions & 11 deletions

CHANGELOG

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
We follow the CalVer (https://calver.org/) versioning scheme: YY.MINOR.MICRO.
44

5+
26.7.0 (2026-04-08)
6+
===================
7+
8+
* ORCiD Integration Project - FE Part
9+
510
26.6.1 (2026-03-26)
611
===================
712

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "osf",
3-
"version": "26.6.1",
3+
"version": "26.7.0",
44
"scripts": {
55
"ng": "ng",
66
"analyze-bundle": "ng build --configuration=analyze-bundle && source-map-explorer dist/**/*.js --no-border-checks",

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,13 @@ export class AuthService {
7373
window.location.href = loginUrl;
7474
}
7575

76-
logout(): void {
76+
logout(nextUrl?: string): void {
7777
this.loaderService.show();
7878
this.actions.clearCurrentUser();
7979

8080
if (isPlatformBrowser(this.platformId)) {
8181
this.cookieService.deleteAll();
82-
window.location.href = `${this.webUrl}/logout/?next=${encodeURIComponent('/')}`;
82+
window.location.href = `${this.webUrl}/logout/?next=${encodeURIComponent(nextUrl || '/')}`;
8383
}
8484
}
8585

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { RouterLink } from '@angular/router';
1010
import { EducationHistoryComponent } from '@osf/shared/components/education-history/education-history.component';
1111
import { EmploymentHistoryComponent } from '@osf/shared/components/employment-history/employment-history.component';
1212
import { SOCIAL_LINKS } from '@osf/shared/constants/social-links.const';
13+
import { ExternalIdentityStatus } from '@osf/shared/enums/external-identity-status.enum';
1314
import { IS_MEDIUM } from '@osf/shared/helpers/breakpoints.tokens';
1415
import { Institution } from '@osf/shared/models/institutions/institutions.model';
1516
import { UserModel } from '@osf/shared/models/user/user.model';
@@ -50,7 +51,7 @@ export class ProfileInformationComponent {
5051

5152
orcidId = computed(() => {
5253
const orcid = this.currentUser()?.external_identity?.ORCID;
53-
return orcid?.status?.toUpperCase() === 'VERIFIED' ? orcid.id : undefined;
54+
return orcid?.status?.toUpperCase() === ExternalIdentityStatus.VERIFIED ? orcid.id : undefined;
5455
});
5556

5657
toProfileSettings() {
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<div class="flex flex-column row-gap-4 border-1 border-round-lg grey-border-color p-3 md:p-4 xl:p-5">
2+
<div class="flex flex-row justify-content-between">
3+
<h2>
4+
{{ 'settings.profileSettings.social.labels.authenticatedIdentity' | translate }}
5+
</h2>
6+
</div>
7+
<div class="flex flex-column row-gap-4 w-full md:flex-row md:align-items-end md:column-gap-3">
8+
<div class="w-full md:w-12">
9+
@if (existingOrcid()) {
10+
<div class="flex flex-row align-items-center gap-2">
11+
<img ngSrc="assets/icons/colored/orcid.svg" width="16" height="16" alt="orcid" />
12+
<a class="font-bold" [href]="orcidUrl()"> {{ orcidUrl() }} </a>
13+
<p-button
14+
icon="fas fa-times"
15+
class="w-6 md:w-auto"
16+
severity="danger"
17+
variant="text"
18+
[pTooltip]="'settings.profileSettings.social.disconnectOrcid' | translate"
19+
[ariaLabel]="'settings.profileSettings.social.disconnectOrcid' | translate"
20+
(onClick)="disconnectOrcid()"
21+
>
22+
</p-button>
23+
</div>
24+
} @else {
25+
<img ngSrc="assets/images/integrations/orcid-logotype.png" width="130" height="40" alt="orcid" />
26+
<p class="mt-2" [innerHTML]="'settings.profileSettings.social.orcidDescription' | translate"></p>
27+
<p class="mt-2 font-bold">{{ 'settings.profileSettings.social.orcidWarning' | translate }}</p>
28+
<div class="mt-2">
29+
<p-button
30+
class="w-6 md:w-auto"
31+
[label]="'settings.profileSettings.social.connectOrcid' | translate"
32+
severity="secondary"
33+
(onClick)="connectOrcid()"
34+
>
35+
</p-button>
36+
</div>
37+
}
38+
</div>
39+
</div>
40+
</div>

src/app/features/settings/profile-settings/components/authenticated-identity/authenticated-identity.component.scss

Whitespace-only changes.
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { MockProvider } from 'ng-mocks';
2+
3+
import { signal } from '@angular/core';
4+
import { ComponentFixture, TestBed } from '@angular/core/testing';
5+
6+
import { AccountSettingsSelectors } from '@osf/features/settings/account-settings/store/account-settings.selectors';
7+
import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service';
8+
9+
import { AuthenticatedIdentityComponent } from './authenticated-identity.component';
10+
11+
import { OSFTestingModule } from '@testing/osf.testing.module';
12+
import {
13+
CustomConfirmationServiceMock,
14+
CustomConfirmationServiceMockType,
15+
} from '@testing/providers/custom-confirmation-provider.mock';
16+
import { provideMockStore } from '@testing/providers/store-provider.mock';
17+
18+
describe('AuthenticatedIdentityComponent', () => {
19+
let component: AuthenticatedIdentityComponent;
20+
let fixture: ComponentFixture<AuthenticatedIdentityComponent>;
21+
let customConfirmationServiceMock: CustomConfirmationServiceMockType;
22+
23+
const mockExternalIdentities = signal([
24+
{
25+
id: 'ORCID',
26+
externalId: '0001-0002-0003-0004',
27+
status: 'VERIFIED',
28+
},
29+
]);
30+
31+
beforeEach(async () => {
32+
customConfirmationServiceMock = CustomConfirmationServiceMock.simple();
33+
await TestBed.configureTestingModule({
34+
imports: [AuthenticatedIdentityComponent, OSFTestingModule],
35+
providers: [
36+
MockProvider(CustomConfirmationService, customConfirmationServiceMock),
37+
provideMockStore({
38+
signals: [
39+
{
40+
selector: AccountSettingsSelectors.getExternalIdentities,
41+
value: mockExternalIdentities,
42+
},
43+
],
44+
}),
45+
],
46+
}).compileComponents();
47+
48+
fixture = TestBed.createComponent(AuthenticatedIdentityComponent);
49+
component = fixture.componentInstance;
50+
fixture.detectChanges();
51+
});
52+
53+
it('should create', () => {
54+
expect(component).toBeTruthy();
55+
});
56+
57+
it('should show existing user ORCID when present in external identities', () => {
58+
expect(component.existingOrcid()).toEqual('0001-0002-0003-0004');
59+
expect(component.orcidUrl()).toEqual('https://orcid.org/0001-0002-0003-0004');
60+
component.disconnectOrcid();
61+
expect(customConfirmationServiceMock.confirmDelete).toHaveBeenCalled();
62+
});
63+
64+
it('should show connect button when no existing ORCID is present in external identities', () => {
65+
mockExternalIdentities.set([]);
66+
fixture.detectChanges();
67+
68+
expect(component.existingOrcid()).toBeUndefined();
69+
expect(component.orcidUrl()).toBeNull();
70+
});
71+
});
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { createDispatchMap, select } from '@ngxs/store';
2+
3+
import { TranslatePipe } from '@ngx-translate/core';
4+
5+
import { Button } from 'primeng/button';
6+
import { Tooltip } from 'primeng/tooltip';
7+
8+
import { finalize } from 'rxjs';
9+
10+
import { NgOptimizedImage } from '@angular/common';
11+
import { ChangeDetectionStrategy, Component, computed, inject, OnInit } from '@angular/core';
12+
13+
import { ENVIRONMENT } from '@core/provider/environment.provider';
14+
import { AuthService } from '@core/services/auth.service';
15+
import { ExternalIdentityStatus } from '@osf/shared/enums/external-identity-status.enum';
16+
import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service';
17+
import { LoaderService } from '@osf/shared/services/loader.service';
18+
import { ToastService } from '@osf/shared/services/toast.service';
19+
20+
import {
21+
AccountSettingsSelectors,
22+
DeleteExternalIdentity,
23+
GetExternalIdentities,
24+
} from '../../../account-settings/store';
25+
import { ProfileSettingsTabOption } from '../../enums';
26+
27+
@Component({
28+
selector: 'osf-authenticated-identity',
29+
imports: [NgOptimizedImage, Button, Tooltip, TranslatePipe],
30+
templateUrl: './authenticated-identity.component.html',
31+
styleUrl: './authenticated-identity.component.scss',
32+
changeDetection: ChangeDetectionStrategy.OnPush,
33+
})
34+
export class AuthenticatedIdentityComponent implements OnInit {
35+
private readonly authService = inject(AuthService);
36+
private readonly environment = inject(ENVIRONMENT);
37+
private readonly customConfirmationService = inject(CustomConfirmationService);
38+
private readonly toastService = inject(ToastService);
39+
private readonly loaderService = inject(LoaderService);
40+
41+
private readonly ORCID_PROVIDER = 'ORCID';
42+
43+
ngOnInit() {
44+
this.actions.getExternalIdentities();
45+
}
46+
47+
readonly actions = createDispatchMap({
48+
deleteExternalIdentity: DeleteExternalIdentity,
49+
getExternalIdentities: GetExternalIdentities,
50+
});
51+
52+
readonly externalIdentities = select(AccountSettingsSelectors.getExternalIdentities);
53+
54+
readonly orcidUrl = computed(() => {
55+
return this.existingOrcid() ? `https://orcid.org/${this.existingOrcid()}` : null;
56+
});
57+
58+
readonly existingOrcid = computed(
59+
(): string | undefined =>
60+
this.externalIdentities()?.find((i) => i.id === 'ORCID' && i.status === ExternalIdentityStatus.VERIFIED)
61+
?.externalId
62+
);
63+
64+
disconnectOrcid(): void {
65+
this.customConfirmationService.confirmDelete({
66+
headerKey: 'settings.accountSettings.connectedIdentities.deleteDialog.header',
67+
messageParams: { name: this.ORCID_PROVIDER },
68+
messageKey: 'settings.accountSettings.connectedIdentities.deleteDialog.message',
69+
onConfirm: () => {
70+
this.loaderService.show();
71+
this.actions
72+
.deleteExternalIdentity(this.ORCID_PROVIDER)
73+
.pipe(finalize(() => this.loaderService.hide()))
74+
.subscribe(() => this.toastService.showSuccess('settings.accountSettings.connectedIdentities.successDelete'));
75+
},
76+
});
77+
}
78+
79+
connectOrcid(): void {
80+
const webUrl = this.environment.webUrl;
81+
const casUrl = this.environment.casUrl;
82+
const finalDestination = new URL(`${webUrl}/settings/profile`);
83+
finalDestination.searchParams.set('tab', ProfileSettingsTabOption.Social.toString());
84+
const casLoginUrl = new URL(`${casUrl}/login`);
85+
casLoginUrl.search = new URLSearchParams({
86+
redirectOrcid: 'true',
87+
service: `${webUrl}/login`,
88+
next: encodeURIComponent(finalDestination.toString()),
89+
}).toString();
90+
this.authService.logout(casLoginUrl.toString());
91+
}
92+
}

src/app/features/settings/profile-settings/components/social/social.component.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
<osf-authenticated-identity />
2+
13
<form [formGroup]="socialLinksForm">
24
<div formArrayName="links" class="flex flex-column row-gap-4 social">
35
@for (link of links.controls; track index; let index = $index) {

src/app/features/settings/profile-settings/components/social/social.component.spec.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { CustomConfirmationService } from '@osf/shared/services/custom-confirmat
88
import { LoaderService } from '@osf/shared/services/loader.service';
99
import { ToastService } from '@osf/shared/services/toast.service';
1010

11+
import { AuthenticatedIdentityComponent } from '../authenticated-identity/authenticated-identity.component';
1112
import { SocialFormComponent } from '../social-form/social-form.component';
1213

1314
import { SocialComponent } from './social.component';
@@ -24,7 +25,12 @@ describe('SocialComponent', () => {
2425
jest.clearAllMocks();
2526

2627
await TestBed.configureTestingModule({
27-
imports: [SocialComponent, MockComponent(SocialFormComponent), MockPipe(TranslatePipe)],
28+
imports: [
29+
SocialComponent,
30+
MockComponent(SocialFormComponent),
31+
MockComponent(AuthenticatedIdentityComponent),
32+
MockPipe(TranslatePipe),
33+
],
2834
providers: [
2935
provideMockStore({
3036
signals: [{ selector: UserSelectors.getSocialLinks, value: MOCK_USER.social }],

0 commit comments

Comments
 (0)