Skip to content

Commit 30718cb

Browse files
authored
Merge pull request #3216 from 4Science/task/dspace-8_x/CST-14903
[Port dspace-8_x] Orcid Authorization / Synchronization Page Fixes
2 parents 686f284 + 1a498f8 commit 30718cb

File tree

5 files changed

+177
-49
lines changed

5 files changed

+177
-49
lines changed

src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
} from '@ngx-translate/core';
2020
import {
2121
BehaviorSubject,
22+
catchError,
2223
Observable,
2324
} from 'rxjs';
2425
import { map } from 'rxjs/operators';
@@ -34,6 +35,7 @@ import { Item } from '../../../core/shared/item.model';
3435
import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
3536
import { AlertComponent } from '../../../shared/alert/alert.component';
3637
import { NotificationsService } from '../../../shared/notifications/notifications.service';
38+
import { createFailedRemoteDataObjectFromError$ } from '../../../shared/remote-data.utils';
3739

3840
@Component({
3941
selector: 'ds-orcid-auth',
@@ -203,13 +205,14 @@ export class OrcidAuthComponent implements OnInit, OnChanges {
203205
this.unlinkProcessing.next(true);
204206
this.orcidAuthService.unlinkOrcidByItem(this.item).pipe(
205207
getFirstCompletedRemoteData(),
208+
catchError(createFailedRemoteDataObjectFromError$<ResearcherProfile>),
206209
).subscribe((remoteData: RemoteData<ResearcherProfile>) => {
207210
this.unlinkProcessing.next(false);
208-
if (remoteData.isSuccess) {
211+
if (remoteData.hasFailed) {
212+
this.notificationsService.error(this.translateService.get('person.page.orcid.unlink.error'));
213+
} else {
209214
this.notificationsService.success(this.translateService.get('person.page.orcid.unlink.success'));
210215
this.unlink.emit();
211-
} else {
212-
this.notificationsService.error(this.translateService.get('person.page.orcid.unlink.error'));
213216
}
214217
});
215218
}

src/app/item-page/orcid-page/orcid-page.component.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
combineLatest,
2121
} from 'rxjs';
2222
import {
23+
filter,
2324
map,
2425
take,
2526
} from 'rxjs/operators';
@@ -187,8 +188,20 @@ export class OrcidPageComponent implements OnInit {
187188
*/
188189
private clearRouteParams(): void {
189190
// update route removing the code from query params
190-
const redirectUrl = this.router.url.split('?')[0];
191-
this.router.navigate([redirectUrl]);
191+
this.route.queryParamMap
192+
.pipe(
193+
filter((paramMap: ParamMap) => isNotEmpty(paramMap.keys)),
194+
map(_ => Object.assign({})),
195+
take(1),
196+
).subscribe(queryParams =>
197+
this.router.navigate(
198+
[],
199+
{
200+
relativeTo: this.route,
201+
queryParams,
202+
},
203+
),
204+
);
192205
}
193206

194207
}

src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ describe('OrcidSyncSettingsComponent test suite', () => {
180180
scheduler = getTestScheduler();
181181
fixture = TestBed.createComponent(OrcidSyncSettingsComponent);
182182
comp = fixture.componentInstance;
183+
researcherProfileService.findByRelatedItem.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile));
183184
comp.item = mockItemLinkedToOrcid;
184185
fixture.detectChanges();
185186
}));
@@ -216,7 +217,6 @@ describe('OrcidSyncSettingsComponent test suite', () => {
216217
});
217218

218219
it('should call updateByOrcidOperations properly', () => {
219-
researcherProfileService.findByRelatedItem.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile));
220220
researcherProfileService.patch.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile));
221221
const expectedOps: Operation[] = [
222222
{
@@ -245,7 +245,6 @@ describe('OrcidSyncSettingsComponent test suite', () => {
245245
});
246246

247247
it('should show notification on success', () => {
248-
researcherProfileService.findByRelatedItem.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile));
249248
researcherProfileService.patch.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile));
250249

251250
scheduler.schedule(() => comp.onSubmit(formGroup));
@@ -257,6 +256,8 @@ describe('OrcidSyncSettingsComponent test suite', () => {
257256

258257
it('should show notification on error', () => {
259258
researcherProfileService.findByRelatedItem.and.returnValue(createFailedRemoteDataObject$());
259+
comp.item = mockItemLinkedToOrcid;
260+
fixture.detectChanges();
260261

261262
scheduler.schedule(() => comp.onSubmit(formGroup));
262263
scheduler.flush();
@@ -266,7 +267,6 @@ describe('OrcidSyncSettingsComponent test suite', () => {
266267
});
267268

268269
it('should show notification on error', () => {
269-
researcherProfileService.findByRelatedItem.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile));
270270
researcherProfileService.patch.and.returnValue(createFailedRemoteDataObject$());
271271

272272
scheduler.schedule(() => comp.onSubmit(formGroup));

src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.ts

Lines changed: 128 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
Component,
44
EventEmitter,
55
Input,
6+
OnDestroy,
67
OnInit,
78
Output,
89
} from '@angular/core';
@@ -15,17 +16,32 @@ import {
1516
TranslateService,
1617
} from '@ngx-translate/core';
1718
import { Operation } from 'fast-json-patch';
18-
import { of } from 'rxjs';
19-
import { switchMap } from 'rxjs/operators';
19+
import {
20+
BehaviorSubject,
21+
Observable,
22+
} from 'rxjs';
23+
import {
24+
catchError,
25+
filter,
26+
map,
27+
switchMap,
28+
take,
29+
takeUntil,
30+
} from 'rxjs/operators';
2031

2132
import { RemoteData } from '../../../core/data/remote-data';
2233
import { ResearcherProfile } from '../../../core/profile/model/researcher-profile.model';
2334
import { ResearcherProfileDataService } from '../../../core/profile/researcher-profile-data.service';
2435
import { Item } from '../../../core/shared/item.model';
25-
import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
36+
import {
37+
getFirstCompletedRemoteData,
38+
getRemoteDataPayload,
39+
} from '../../../core/shared/operators';
2640
import { AlertComponent } from '../../../shared/alert/alert.component';
2741
import { AlertType } from '../../../shared/alert/alert-type';
42+
import { hasValue } from '../../../shared/empty.util';
2843
import { NotificationsService } from '../../../shared/notifications/notifications.service';
44+
import { createFailedRemoteDataObjectFromError$ } from '../../../shared/remote-data.utils';
2945

3046
@Component({
3147
selector: 'ds-orcid-sync-setting',
@@ -39,14 +55,9 @@ import { NotificationsService } from '../../../shared/notifications/notification
3955
],
4056
standalone: true,
4157
})
42-
export class OrcidSyncSettingsComponent implements OnInit {
58+
export class OrcidSyncSettingsComponent implements OnInit, OnDestroy {
4359
protected readonly AlertType = AlertType;
4460

45-
/**
46-
* The item for which showing the orcid settings
47-
*/
48-
@Input() item: Item;
49-
5061
/**
5162
* The prefix used for i18n keys
5263
*/
@@ -91,12 +102,39 @@ export class OrcidSyncSettingsComponent implements OnInit {
91102
* An event emitted when settings are updated
92103
*/
93104
@Output() settingsUpdated: EventEmitter<void> = new EventEmitter<void>();
105+
/**
106+
* Emitter that triggers onDestroy lifecycle
107+
* @private
108+
*/
109+
readonly #destroy$ = new EventEmitter<void>();
110+
/**
111+
* {@link BehaviorSubject} that reflects {@link item} input changes
112+
* @private
113+
*/
114+
readonly #item$ = new BehaviorSubject<Item>(null);
115+
/**
116+
* {@link Observable} that contains {@link ResearcherProfile} linked to the {@link #item$}
117+
* @private
118+
*/
119+
#researcherProfile$: Observable<ResearcherProfile>;
94120

95121
constructor(private researcherProfileService: ResearcherProfileDataService,
96122
private notificationsService: NotificationsService,
97123
private translateService: TranslateService) {
98124
}
99125

126+
/**
127+
* The item for which showing the orcid settings
128+
*/
129+
@Input()
130+
set item(item: Item) {
131+
this.#item$.next(item);
132+
}
133+
134+
ngOnDestroy(): void {
135+
this.#destroy$.next();
136+
}
137+
100138
/**
101139
* Init orcid settings form
102140
*/
@@ -128,20 +166,21 @@ export class OrcidSyncSettingsComponent implements OnInit {
128166
};
129167
});
130168

131-
const syncProfilePreferences = this.item.allMetadataValues('dspace.orcid.sync-profile');
169+
this.updateSyncProfileOptions(this.#item$.asObservable());
170+
this.updateSyncPreferences(this.#item$.asObservable());
132171

133-
this.syncProfileOptions = ['BIOGRAPHICAL', 'IDENTIFIERS']
134-
.map((value) => {
135-
return {
136-
label: this.messagePrefix + '.sync-profile.' + value.toLowerCase(),
137-
value: value,
138-
checked: syncProfilePreferences.includes(value),
139-
};
140-
});
141-
142-
this.currentSyncMode = this.getCurrentPreference('dspace.orcid.sync-mode', ['BATCH', 'MANUAL'], 'MANUAL');
143-
this.currentSyncPublications = this.getCurrentPreference('dspace.orcid.sync-publications', ['DISABLED', 'ALL'], 'DISABLED');
144-
this.currentSyncFunding = this.getCurrentPreference('dspace.orcid.sync-fundings', ['DISABLED', 'ALL'], 'DISABLED');
172+
this.#researcherProfile$ =
173+
this.#item$.pipe(
174+
switchMap(item =>
175+
this.researcherProfileService.findByRelatedItem(item)
176+
.pipe(
177+
getFirstCompletedRemoteData(),
178+
catchError(createFailedRemoteDataObjectFromError$<ResearcherProfile>),
179+
getRemoteDataPayload(),
180+
),
181+
),
182+
takeUntil(this.#destroy$),
183+
);
145184
}
146185

147186
/**
@@ -166,37 +205,84 @@ export class OrcidSyncSettingsComponent implements OnInit {
166205
return;
167206
}
168207

169-
this.researcherProfileService.findByRelatedItem(this.item).pipe(
170-
getFirstCompletedRemoteData(),
171-
switchMap((profileRD: RemoteData<ResearcherProfile>) => {
172-
if (profileRD.hasSucceeded) {
173-
return this.researcherProfileService.patch(profileRD.payload, operations).pipe(
174-
getFirstCompletedRemoteData(),
175-
);
208+
this.#researcherProfile$
209+
.pipe(
210+
switchMap(researcherProfile => this.researcherProfileService.patch(researcherProfile, operations)),
211+
getFirstCompletedRemoteData(),
212+
catchError(createFailedRemoteDataObjectFromError$<ResearcherProfile>),
213+
take(1),
214+
)
215+
.subscribe((remoteData: RemoteData<ResearcherProfile>) => {
216+
if (remoteData.hasFailed) {
217+
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.synchronization-settings-update.error'));
176218
} else {
177-
return of(profileRD);
219+
this.notificationsService.success(this.translateService.get(this.messagePrefix + '.synchronization-settings-update.success'));
220+
this.settingsUpdated.emit();
178221
}
179-
}),
180-
).subscribe((remoteData: RemoteData<ResearcherProfile>) => {
181-
if (remoteData.isSuccess) {
182-
this.notificationsService.success(this.translateService.get(this.messagePrefix + '.synchronization-settings-update.success'));
183-
this.settingsUpdated.emit();
184-
} else {
185-
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.synchronization-settings-update.error'));
186-
}
187-
});
222+
});
223+
}
224+
225+
/**
226+
*
227+
* Handles subscriptions to populate sync preferences
228+
*
229+
* @param item observable that emits update on item changes
230+
* @private
231+
*/
232+
private updateSyncPreferences(item: Observable<Item>) {
233+
item.pipe(
234+
filter(hasValue),
235+
map(i => this.getCurrentPreference(i, 'dspace.orcid.sync-mode', ['BATCH', 'MANUAL'], 'MANUAL')),
236+
takeUntil(this.#destroy$),
237+
).subscribe(val => this.currentSyncMode = val);
238+
item.pipe(
239+
filter(hasValue),
240+
map(i => this.getCurrentPreference(i, 'dspace.orcid.sync-publications', ['DISABLED', 'ALL'], 'DISABLED')),
241+
takeUntil(this.#destroy$),
242+
).subscribe(val => this.currentSyncPublications = val);
243+
item.pipe(
244+
filter(hasValue),
245+
map(i => this.getCurrentPreference(i, 'dspace.orcid.sync-fundings', ['DISABLED', 'ALL'], 'DISABLED')),
246+
takeUntil(this.#destroy$),
247+
).subscribe(val => this.currentSyncFunding = val);
248+
}
249+
250+
/**
251+
* Handles subscription to populate the {@link syncProfileOptions} field
252+
*
253+
* @param item observable that emits update on item changes
254+
* @private
255+
*/
256+
private updateSyncProfileOptions(item: Observable<Item>) {
257+
item.pipe(
258+
filter(hasValue),
259+
map(i => i.allMetadataValues('dspace.orcid.sync-profile')),
260+
map(metadata =>
261+
['BIOGRAPHICAL', 'IDENTIFIERS']
262+
.map((value) => {
263+
return {
264+
label: this.messagePrefix + '.sync-profile.' + value.toLowerCase(),
265+
value: value,
266+
checked: metadata.includes(value),
267+
};
268+
}),
269+
),
270+
takeUntil(this.#destroy$),
271+
)
272+
.subscribe(value => this.syncProfileOptions = value);
188273
}
189274

190275
/**
191276
* Retrieve setting saved in the item's metadata
192277
*
278+
* @param item The item from which retrieve settings
193279
* @param metadataField The metadata name that contains setting
194280
* @param allowedValues The allowed values
195281
* @param defaultValue The default value
196282
* @private
197283
*/
198-
private getCurrentPreference(metadataField: string, allowedValues: string[], defaultValue: string): string {
199-
const currentPreference = this.item.firstMetadataValue(metadataField);
284+
private getCurrentPreference(item: Item, metadataField: string, allowedValues: string[], defaultValue: string): string {
285+
const currentPreference = item.firstMetadataValue(metadataField);
200286
return (currentPreference && allowedValues.includes(currentPreference)) ? currentPreference : defaultValue;
201287
}
202288

@@ -216,3 +302,4 @@ export class OrcidSyncSettingsComponent implements OnInit {
216302
}
217303

218304
}
305+

src/app/shared/remote-data.utils.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { HttpErrorResponse } from '@angular/common/http';
12
import {
23
Observable,
34
of as observableOf,
@@ -107,3 +108,27 @@ export function createNoContentRemoteDataObject<T>(timeCompleted?: number): Remo
107108
export function createNoContentRemoteDataObject$<T>(timeCompleted?: number): Observable<RemoteData<T>> {
108109
return createSuccessfulRemoteDataObject$(undefined, timeCompleted);
109110
}
111+
112+
/**
113+
* Method to create a remote data object that has failed starting from a given error
114+
*
115+
* @param error
116+
*/
117+
export function createFailedRemoteDataObjectFromError<T>(error: unknown): RemoteData<T> {
118+
const remoteData = createFailedRemoteDataObject<T>();
119+
if (error instanceof Error) {
120+
remoteData.errorMessage = error.message;
121+
}
122+
if (error instanceof HttpErrorResponse) {
123+
remoteData.statusCode = error.status;
124+
}
125+
return remoteData;
126+
}
127+
128+
/**
129+
* Method to create a remote data object that has failed starting from a given error
130+
* @param error
131+
*/
132+
export function createFailedRemoteDataObjectFromError$<T>(error: unknown): Observable<RemoteData<T>> {
133+
return observableOf(createFailedRemoteDataObjectFromError<T>(error));
134+
}

0 commit comments

Comments
 (0)