Merge pull request #4017 from 4Science/task/main/CST-18016

OpenAlex Integration
This commit is contained in:
Tim Donohue
2025-03-24 17:05:28 -05:00
committed by GitHub
68 changed files with 850 additions and 286 deletions

View File

@@ -1 +1 @@
<ds-publication-claim [source]="'openaire'"></ds-publication-claim>
<ds-suggestion-sources></ds-suggestion-sources>

View File

@@ -6,8 +6,9 @@ import {
waitForAsync,
} from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core';
import { MockComponent } from 'ng-mocks';
import { PublicationClaimComponent } from '../../../notifications/suggestion-targets/publication-claim/publication-claim.component';
import { SuggestionSourcesComponent } from '../../../notifications/suggestions/sources/suggestion-sources.component';
import { AdminNotificationsPublicationClaimPageComponent } from './admin-notifications-publication-claim-page.component';
describe('AdminNotificationsPublicationClaimPageComponent', () => {
@@ -20,17 +21,10 @@ describe('AdminNotificationsPublicationClaimPageComponent', () => {
CommonModule,
TranslateModule.forRoot(),
AdminNotificationsPublicationClaimPageComponent,
],
providers: [
AdminNotificationsPublicationClaimPageComponent,
MockComponent(SuggestionSourcesComponent),
],
schemas: [NO_ERRORS_SCHEMA],
}).overrideComponent(AdminNotificationsPublicationClaimPageComponent, {
remove: {
imports: [PublicationClaimComponent],
},
})
.compileComponents();
}).compileComponents();
}));
beforeEach(() => {

View File

@@ -1,14 +1,12 @@
import { Component } from '@angular/core';
import { PublicationClaimComponent } from '../../../notifications/suggestion-targets/publication-claim/publication-claim.component';
import { SuggestionSourcesComponent } from '../../../notifications/suggestions/sources/suggestion-sources.component';
@Component({
selector: 'ds-admin-notifications-publication-claim-page',
templateUrl: './admin-notifications-publication-claim-page.component.html',
styleUrls: ['./admin-notifications-publication-claim-page.component.scss'],
imports: [
PublicationClaimComponent,
],
imports: [ SuggestionSourcesComponent ],
standalone: true,
})
export class AdminNotificationsPublicationClaimPageComponent {

View File

@@ -2,7 +2,8 @@ import { Route } from '@angular/router';
import { authenticatedGuard } from '../../core/auth/authenticated.guard';
import { i18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
import { qualityAssuranceBreadcrumbResolver } from '../../core/breadcrumbs/quality-assurance-breadcrumb.resolver';
import { sourcesBreadcrumbResolver } from '../../core/breadcrumbs/sources-breadcrumb.resolver';
import { PublicationClaimComponent } from '../../notifications/suggestions/targets/publication-claim/publication-claim.component';
import { AdminNotificationsPublicationClaimPageResolver } from '../../quality-assurance-notifications-pages/notifications-suggestion-targets-page/notifications-suggestion-targets-page-resolver.service';
import { QualityAssuranceEventsPageComponent } from '../../quality-assurance-notifications-pages/quality-assurance-events-page/quality-assurance-events-page.component';
import { qualityAssuranceEventsPageResolver } from '../../quality-assurance-notifications-pages/quality-assurance-events-page/quality-assurance-events-page.resolver';
@@ -33,13 +34,28 @@ export const ROUTES: Route[] = [
showBreadcrumbsFluid: false,
},
},
{
canActivate: [ authenticatedGuard ],
path: `${PUBLICATION_CLAIMS_PATH}/:sourceId`,
pathMatch: 'full',
component: PublicationClaimComponent,
resolve: {
breadcrumb: sourcesBreadcrumbResolver,
openaireQualityAssuranceEventsParams: AdminNotificationsPublicationClaimPageResolver,
},
data: {
title: 'admin.notifications.publicationclaim.page.title',
breadcrumbKey: 'admin.notifications.publicationclaim',
showBreadcrumbsFluid: false,
},
},
{
canActivate: [authenticatedGuard],
path: `${QUALITY_ASSURANCE_EDIT_PATH}/:sourceId`,
component: QualityAssuranceTopicsPageComponent,
pathMatch: 'full',
resolve: {
breadcrumb: qualityAssuranceBreadcrumbResolver,
breadcrumb: sourcesBreadcrumbResolver,
openaireQualityAssuranceTopicsParams: QualityAssuranceTopicsPageResolver,
},
data: {
@@ -85,7 +101,7 @@ export const ROUTES: Route[] = [
component: QualityAssuranceEventsPageComponent,
pathMatch: 'full',
resolve: {
breadcrumb: qualityAssuranceBreadcrumbResolver,
breadcrumb: sourcesBreadcrumbResolver,
openaireQualityAssuranceEventsParams: qualityAssuranceEventsPageResolver,
},
data: {

View File

@@ -10,6 +10,7 @@ import {
import {
NoPreloading,
provideRouter,
withComponentInputBinding,
withEnabledBlockingInitialNavigation,
withInMemoryScrolling,
withPreloading,
@@ -109,6 +110,7 @@ export const commonAppConfig: ApplicationConfig = {
withInMemoryScrolling(APP_ROUTING_SCROLL_CONF),
withEnabledBlockingInitialNavigation(),
withPreloading(NoPreloading),
withComponentInputBinding(),
),
{
provide: APP_BASE_HREF,

View File

@@ -1,43 +0,0 @@
import {
TestBed,
waitForAsync,
} from '@angular/core/testing';
import { getTestScheduler } from 'jasmine-marbles';
import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model';
import { QualityAssuranceBreadcrumbService } from './quality-assurance-breadcrumb.service';
describe('QualityAssuranceBreadcrumbService', () => {
let service: QualityAssuranceBreadcrumbService;
let translateService: any = {
instant: (str) => str,
};
let exampleString;
let exampleURL;
let exampleQaKey;
function init() {
exampleString = 'sourceId';
exampleURL = '/test/quality-assurance/';
exampleQaKey = 'admin.quality-assurance.breadcrumbs';
}
beforeEach(waitForAsync(() => {
init();
TestBed.configureTestingModule({}).compileComponents();
}));
beforeEach(() => {
service = new QualityAssuranceBreadcrumbService(translateService);
});
describe('getBreadcrumbs', () => {
it('should return a breadcrumb based on a string', () => {
const breadcrumbs = service.getBreadcrumbs(exampleString, exampleURL);
getTestScheduler().expectObservable(breadcrumbs).toBe('(a|)', { a: [new Breadcrumb(exampleQaKey, exampleURL),
new Breadcrumb(exampleString, exampleURL + exampleString)],
});
});
});
});

View File

@@ -1,15 +1,17 @@
import { qualityAssuranceBreadcrumbResolver } from './quality-assurance-breadcrumb.resolver';
import { sourcesBreadcrumbResolver } from './sources-breadcrumb.resolver';
describe('qualityAssuranceBreadcrumbResolver', () => {
describe('sourcesBreadcrumbResolver', () => {
describe('resolve', () => {
let resolver: any;
let qualityAssuranceBreadcrumbService: any;
let sourcesBreadcrumbService: any;
let route: any;
const i18nKey = 'breadcrumbKey';
const fullPath = '/test/quality-assurance/';
const expectedKey = 'testSourceId:testTopicId';
const expectedKey = 'breadcrumbKey:testSourceId:testTopicId';
beforeEach(() => {
route = {
data: { breadcrumbKey: i18nKey },
paramMap: {
get: function (param) {
return this[param];
@@ -18,13 +20,13 @@ describe('qualityAssuranceBreadcrumbResolver', () => {
topicId: 'testTopicId',
},
};
qualityAssuranceBreadcrumbService = {};
resolver = qualityAssuranceBreadcrumbResolver;
sourcesBreadcrumbService = {};
resolver = sourcesBreadcrumbResolver;
});
it('should resolve the breadcrumb config', () => {
const resolvedConfig = resolver(route as any, { url: fullPath + 'testSourceId' } as any, qualityAssuranceBreadcrumbService);
const expectedConfig = { provider: qualityAssuranceBreadcrumbService, key: expectedKey, url: fullPath };
const resolvedConfig = resolver(route as any, { url: fullPath + 'testSourceId' } as any, sourcesBreadcrumbService);
const expectedConfig = { provider: sourcesBreadcrumbService, key: expectedKey, url: fullPath };
expect(resolvedConfig).toEqual(expectedConfig);
});
});

View File

@@ -6,16 +6,17 @@ import {
} from '@angular/router';
import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model';
import { QualityAssuranceBreadcrumbService } from './quality-assurance-breadcrumb.service';
import { SourcesBreadcrumbService } from './sources-breadcrumb.service';
export const qualityAssuranceBreadcrumbResolver: ResolveFn<BreadcrumbConfig<string>> = (
export const sourcesBreadcrumbResolver: ResolveFn<BreadcrumbConfig<string>> = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
breadcrumbService: QualityAssuranceBreadcrumbService = inject(QualityAssuranceBreadcrumbService),
breadcrumbService: SourcesBreadcrumbService = inject(SourcesBreadcrumbService),
): BreadcrumbConfig<string> => {
const breadcrumbKey = route.data.breadcrumbKey;
const sourceId = route.paramMap.get('sourceId');
const topicId = route.paramMap.get('topicId');
let key = sourceId;
let key = `${breadcrumbKey}:${sourceId}`;
if (topicId) {
key += `:${topicId}`;

View File

@@ -0,0 +1,60 @@
import {
TestBed,
waitForAsync,
} from '@angular/core/testing';
import { getTestScheduler } from 'jasmine-marbles';
import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model';
import { SourcesBreadcrumbService } from './sources-breadcrumb.service';
describe('SourcesBreadcrumbService', () => {
let service: SourcesBreadcrumbService;
let translateService: any = {
instant: (str) => str,
};
let exampleString;
let exampleSource;
let exampleTopic;
let exampleArg;
let exampleArgTopic;
let exampleURL;
let exampleQaKey;
function init() {
exampleString = 'admin.quality-assurance';
exampleSource = 'sourceId';
exampleTopic = 'topic';
exampleArg = `${exampleString}:${exampleSource}`;
exampleArgTopic = `${exampleString}:${exampleSource}:${exampleTopic}`;
exampleURL = '/test/quality-assurance/';
exampleQaKey = 'admin.quality-assurance.breadcrumbs';
}
beforeEach(waitForAsync(() => {
init();
TestBed.configureTestingModule({}).compileComponents();
}));
beforeEach(() => {
service = new SourcesBreadcrumbService(translateService);
});
describe('getBreadcrumbs', () => {
it('should return a breadcrumb based on source only', () => {
const breadcrumbs = service.getBreadcrumbs(exampleArg, exampleURL);
getTestScheduler().expectObservable(breadcrumbs).toBe('(a|)', { a: [new Breadcrumb(exampleQaKey, exampleURL),
new Breadcrumb(exampleSource, exampleURL + exampleSource)],
});
});
it('should return a breadcrumb based also on topic', () => {
const breadcrumbs = service.getBreadcrumbs(exampleArgTopic, exampleURL);
getTestScheduler().expectObservable(breadcrumbs).toBe('(a|)', { a: [new Breadcrumb(exampleQaKey, exampleURL),
new Breadcrumb(exampleSource, exampleURL + exampleSource),
new Breadcrumb(exampleTopic, undefined)],
});
});
});
});

View File

@@ -14,9 +14,9 @@ import { BreadcrumbsProviderService } from './breadcrumbsProviderService';
@Injectable({
providedIn: 'root',
})
export class QualityAssuranceBreadcrumbService implements BreadcrumbsProviderService<string> {
export class SourcesBreadcrumbService implements BreadcrumbsProviderService<string> {
private QUALITY_ASSURANCE_BREADCRUMB_KEY = 'admin.quality-assurance.breadcrumbs';
private BREADCRUMB_SUFFIX = '.breadcrumbs';
constructor(
private translationService: TranslateService,
) {
@@ -31,15 +31,16 @@ export class QualityAssuranceBreadcrumbService implements BreadcrumbsProviderSer
*/
getBreadcrumbs(key: string, url: string): Observable<Breadcrumb[]> {
const args = key.split(':');
const sourceId = args[0];
const topicId = args.length > 2 ? args[args.length - 1] : args[1];
const breadcrumbKey = args[0] + this.BREADCRUMB_SUFFIX;
const sourceId = args[1];
const topicId = args.length > 3 ? args[args.length - 1] : args[2];
if (topicId) {
return observableOf( [new Breadcrumb(this.translationService.instant(this.QUALITY_ASSURANCE_BREADCRUMB_KEY), url),
return observableOf( [new Breadcrumb(this.translationService.instant(breadcrumbKey), url),
new Breadcrumb(sourceId, `${url}${sourceId}`),
new Breadcrumb(topicId, undefined)]);
} else {
return observableOf([new Breadcrumb(this.translationService.instant(this.QUALITY_ASSURANCE_BREADCRUMB_KEY), url),
return observableOf([new Breadcrumb(this.translationService.instant(breadcrumbKey), url),
new Breadcrumb(sourceId, `${url}${sourceId}`)]);
}

View File

@@ -18,7 +18,7 @@ import {
} from 'src/config/app-config.interface';
import { Site } from '../core/shared/site.model';
import { SuggestionsPopupComponent } from '../notifications/suggestions-popup/suggestions-popup.component';
import { SuggestionsPopupComponent } from '../notifications/suggestions/popup/suggestions-popup.component';
import { ThemedConfigurationSearchPageComponent } from '../search-page/themed-configuration-search-page.component';
import { ThemedSearchFormComponent } from '../shared/search-form/themed-search-form.component';
import { PageWithSidebarComponent } from '../shared/sidebar/page-with-sidebar.component';

View File

@@ -28,7 +28,7 @@ import { MyDSpaceConfigurationValueType } from './my-dspace-configuration-value-
import { MyDSpaceNewSubmissionComponent } from './my-dspace-new-submission/my-dspace-new-submission.component';
import { MyDSpacePageComponent } from './my-dspace-page.component';
import SpyObj = jasmine.SpyObj;
import { SuggestionsNotificationComponent } from '../notifications/suggestions-notification/suggestions-notification.component';
import { SuggestionsNotificationComponent } from '../notifications/suggestions/notification/suggestions-notification.component';
import { MyDspaceQaEventsNotificationsComponent } from './my-dspace-qa-events-notifications/my-dspace-qa-events-notifications.component';
describe('MyDSpacePageComponent', () => {

View File

@@ -14,7 +14,7 @@ import { RoleType } from '../core/roles/role-types';
import { Context } from '../core/shared/context.model';
import { SearchService } from '../core/shared/search/search.service';
import { ViewMode } from '../core/shared/view-mode.model';
import { SuggestionsNotificationComponent } from '../notifications/suggestions-notification/suggestions-notification.component';
import { SuggestionsNotificationComponent } from '../notifications/suggestions/notification/suggestions-notification.component';
import { RoleDirective } from '../shared/roles/role.directive';
import { SearchConfigurationOption } from '../shared/search/search-switch-configuration/search-configuration-option.model';
import { ThemedSearchComponent } from '../shared/search/themed-search.component';

View File

@@ -1,6 +1,6 @@
import { QualityAssuranceSourceEffects } from './qa/source/quality-assurance-source.effects';
import { QualityAssuranceTopicsEffects } from './qa/topics/quality-assurance-topics.effects';
import { SuggestionTargetsEffects } from './suggestion-targets/suggestion-targets.effects';
import { SuggestionTargetsEffects } from './suggestions/targets/suggestion-targets.effects';
export const notificationsEffects = [
QualityAssuranceTopicsEffects,

View File

@@ -14,7 +14,7 @@ import {
import {
SuggestionTargetsReducer,
SuggestionTargetState,
} from './suggestion-targets/suggestion-targets.reducer';
} from './suggestions/targets/suggestion-targets.reducer';
/**
* The OpenAIRE State

View File

@@ -5,65 +5,12 @@
<ds-alert [type]="'alert-info'" [content]="'quality-assurance.source.description'"></ds-alert>
</div>
</div>
<div class="row">
<div class="col-12">
<h2 class="h4 border-bottom pb-2">{{'quality-assurance.source'| translate}}</h2>
@if ((isSourceLoading() | async)) {
<ds-loading class="container" message="{{'quality-assurance.loading' | translate}}"></ds-loading>
}
@if ((isSourceLoading() | async) !== true) {
<ds-pagination
[paginationOptions]="paginationConfig"
[collectionSize]="(totalElements$ | async)"
[hideGear]="false"
[hideSortOptions]="true"
(paginationChange)="getQualityAssuranceSource()">
@if ((isSourceProcessing() | async)) {
<ds-loading class="container" message="'quality-assurance.loading' | translate"></ds-loading>
}
@if ((isSourceProcessing() | async) !== true) {
@if ((sources$ | async)?.length === 0) {
<div class="alert alert-info w-100 mb-2 mt-2" role="alert">
{{'quality-assurance.noSource' | translate}}
</div>
}
@if ((sources$ | async)?.length !== 0) {
<div class="table-responsive mt-2">
<table id="epeople" class="table table-striped table-hover table-bordered">
<thead>
<tr>
<th scope="col">{{'quality-assurance.table.source' | translate}}</th>
<th scope="col">{{'quality-assurance.table.last-event' | translate}}</th>
<th scope="col">{{'quality-assurance.table.actions' | translate}}</th>
</tr>
</thead>
<tbody>
@for (sourceElement of (sources$ | async); track sourceElement; let i = $index) {
<tr>
<td>{{sourceElement.id}}</td>
<td>{{sourceElement.lastEvent | date: 'dd/MM/yyyy hh:mm' }}</td>
<td>
<div class="btn-group edit-field">
<button
class="btn btn-outline-primary btn-sm"
title="{{'quality-assurance.source-list.button.detail' | translate : { param: sourceElement.id } }}"
[routerLink]="[sourceElement.id]">
<span class="badge bg-info">{{sourceElement.totalEvents}}</span>
<i class="fas fa-info fa-fw"></i>
</button>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
}
}
</ds-pagination>
}
</div>
</div>
<ds-source-list [loading]="(isSourceLoading() | async) === true || (isSourceProcessing() | async) === true"
[paginationConfig]="paginationConfig"
[showLastEvent]="true"
[sources]="sources$ | async"
[totalElements]="totalElements$ | async"
(paginationChange)="getQualityAssuranceSource()"
(sourceSelected)="onSelect($event)"></ds-source-list>
</div>

View File

@@ -16,16 +16,18 @@ import { of as observableOf } from 'rxjs';
import { PaginationService } from '../../../core/pagination/pagination.service';
import { AlertComponent } from '../../../shared/alert/alert.component';
import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component';
import {
getMockNotificationsStateService,
qualityAssuranceSourceObjectMoreAbstract,
qualityAssuranceSourceObjectMorePid,
} from '../../../shared/mocks/notifications.mock';
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
import { createTestComponent } from '../../../shared/testing/utils.test';
import { NotificationsStateService } from '../../notifications-state.service';
import {
SourceListComponent,
SourceObject,
} from '../../shared/source-list.component';
import { QualityAssuranceSourceComponent } from './quality-assurance-source.component';
describe('QualityAssuranceSourceComponent test suite', () => {
@@ -61,8 +63,7 @@ describe('QualityAssuranceSourceComponent test suite', () => {
remove: {
imports: [
AlertComponent,
ThemedLoadingComponent,
PaginationComponent,
SourceListComponent,
],
},
})
@@ -119,12 +120,19 @@ describe('QualityAssuranceSourceComponent test suite', () => {
it(('Should init component properly'), () => {
comp.ngOnInit();
fixture.detectChanges();
const expected: SourceObject[] = [
{ id: qualityAssuranceSourceObjectMorePid.id,
lastEvent: qualityAssuranceSourceObjectMorePid.lastEvent,
total: qualityAssuranceSourceObjectMorePid.totalEvents,
},
{ id: qualityAssuranceSourceObjectMoreAbstract.id,
lastEvent: qualityAssuranceSourceObjectMoreAbstract.lastEvent,
total: qualityAssuranceSourceObjectMoreAbstract.totalEvents,
},
];
expect(comp.sources$).toBeObservable(cold('(a|)', {
a: [
qualityAssuranceSourceObjectMorePid,
qualityAssuranceSourceObjectMoreAbstract,
],
a: expected,
}));
expect(comp.totalElements$).toBeObservable(cold('(a|)', {
a: 2,

View File

@@ -8,7 +8,11 @@ import {
OnDestroy,
OnInit,
} from '@angular/core';
import { RouterLink } from '@angular/router';
import {
ActivatedRoute,
Router,
RouterLink,
} from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import {
Observable,
@@ -16,6 +20,7 @@ import {
} from 'rxjs';
import {
distinctUntilChanged,
map,
take,
} from 'rxjs/operators';
@@ -29,6 +34,10 @@ import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.c
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
import { NotificationsStateService } from '../../notifications-state.service';
import {
SourceListComponent,
SourceObject,
} from '../../shared/source-list.component';
/**
* Component to display the Quality Assurance source list.
@@ -38,7 +47,7 @@ import { NotificationsStateService } from '../../notifications-state.service';
templateUrl: './quality-assurance-source.component.html',
styleUrls: ['./quality-assurance-source.component.scss'],
standalone: true,
imports: [AlertComponent, ThemedLoadingComponent, PaginationComponent, RouterLink, AsyncPipe, TranslateModule, DatePipe],
imports: [AlertComponent, ThemedLoadingComponent, PaginationComponent, RouterLink, AsyncPipe, TranslateModule, DatePipe, SourceListComponent],
})
export class QualityAssuranceSourceComponent implements OnDestroy, OnInit, AfterViewInit {
@@ -59,7 +68,7 @@ export class QualityAssuranceSourceComponent implements OnDestroy, OnInit, After
/**
* The Quality Assurance source list.
*/
public sources$: Observable<QualityAssuranceSourceObject[]>;
public sources$: Observable<SourceObject[]>;
/**
* The total number of Quality Assurance sources.
*/
@@ -74,10 +83,14 @@ export class QualityAssuranceSourceComponent implements OnDestroy, OnInit, After
* Initialize the component variables.
* @param {PaginationService} paginationService
* @param {NotificationsStateService} notificationsStateService
* @param {Router} router
* @param {ActivatedRoute} route
*/
constructor(
private paginationService: PaginationService,
private notificationsStateService: NotificationsStateService,
private router: Router,
private route: ActivatedRoute,
) {
}
@@ -85,7 +98,15 @@ export class QualityAssuranceSourceComponent implements OnDestroy, OnInit, After
* Component initialization.
*/
ngOnInit(): void {
this.sources$ = this.notificationsStateService.getQualityAssuranceSource();
this.sources$ = this.notificationsStateService.getQualityAssuranceSource().pipe(
map((sources: QualityAssuranceSourceObject[])=> {
return sources.map((source: QualityAssuranceSourceObject) => ({
id: source.id,
lastEvent: source.lastEvent,
total: source.totalEvents,
}));
}),
);
this.totalElements$ = this.notificationsStateService.getQualityAssuranceSourceTotals();
}
@@ -102,6 +123,14 @@ export class QualityAssuranceSourceComponent implements OnDestroy, OnInit, After
);
}
/**
* Navigate to the specified source
* @param sourceId
*/
onSelect(sourceId: string) {
this.router.navigate([sourceId], { relativeTo: this.route });
}
/**
* Returns the information about the loading status of the Quality Assurance source (if it's running or not).
*

View File

@@ -0,0 +1,59 @@
<div class="col-12">
<h2 class="h4 border-bottom pb-2">{{'quality-assurance.source'| translate}}</h2>
@if (loading()) {
<ds-loading class="container" message="{{'quality-assurance.loading' | translate}}"></ds-loading>
} @else {
<ds-pagination
[paginationOptions]="paginationConfig()"
[collectionSize]="totalElements()"
[hideGear]="false"
[hideSortOptions]="true"
(paginationChange)="paginationChange.emit($event)">
@if (sources()?.length === 0) {
<div class="alert alert-info w-100 mb-2 mt-2" role="alert">
{{'quality-assurance.noSource' | translate}}
</div>
}
@if (sources()?.length !== 0) {
<div class="table-responsive mt-2">
<table id="epeople" class="table table-striped table-hover table-bordered">
<thead>
<tr>
<th scope="col">{{'quality-assurance.table.source' | translate}}</th>
@if (showLastEvent()) {
<th scope="col">{{'quality-assurance.table.last-event' | translate}}</th>
}
<th scope="col">{{'quality-assurance.table.actions' | translate}}</th>
</tr>
</thead>
<tbody>
@for (sourceElement of sources(); track sourceElement; let i = $index) {
<tr>
<td>{{sourceElement.id}}</td>
@if (showLastEvent()) {
<td>{{sourceElement.lastEvent | date: 'dd/MM/yyyy hh:mm' }}</td>
}
<td>
<div class="btn-group edit-field">
<button
class="btn btn-outline-primary btn-sm"
title="{{'quality-assurance.source-list.button.detail' | translate : { param: sourceElement.id } }}"
(click)="sourceSelected.emit(sourceElement.id)">
<span class="badge bg-info">{{sourceElement.total}}</span>
<i class="fas fa-info fa-fw"></i>
</button>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
}
</ds-pagination>
}
</div>

View File

@@ -0,0 +1,114 @@
import {
AsyncPipe,
DatePipe,
} from '@angular/common';
import {
ComponentFixture,
TestBed,
} from '@angular/core/testing';
import { provideRouter } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { MockComponent } from 'ng-mocks';
import { AlertComponent } from '../../shared/alert/alert.component';
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
import { PaginationComponent } from '../../shared/pagination/pagination.component';
import {
SourceListComponent,
SourceObject,
} from './source-list.component';
describe('SourceListComponent', () => {
let component: SourceListComponent;
let fixture: ComponentFixture<SourceListComponent>;
const paginationConfig = {
currentPage: 1,
pageSize: 10,
};
const sources: SourceObject[] = [
{ id: 'source1', lastEvent: '2025-03-12T12:00:00', total: 5 },
{ id: 'source1', lastEvent: '2025-03-13T12:00:00', total: 10 },
];
const sourcesWithoutEvent: SourceObject[] = [
{ id: 'source1', total: 5 },
{ id: 'source1', total: 10 },
];
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [SourceListComponent],
imports: [
TranslateModule.forRoot(),
AlertComponent,
AsyncPipe,
DatePipe,
MockComponent(PaginationComponent),
MockComponent(ThemedLoadingComponent),
],
providers: [
provideRouter([]),
],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(SourceListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should display loading message when loading is true', () => {
fixture.componentRef.setInput('loading', true);
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('ds-loading')).toBeTruthy();
});
it('should display sources when loading is false and sources are available', () => {
fixture.componentRef.setInput('loading', false);
fixture.componentRef.setInput('showLastEvent', true);
fixture.componentRef.setInput('sources', sources);
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('table')).toBeTruthy();
expect(fixture.nativeElement.querySelector('thead tr').children.length).toBe(3);
expect(fixture.nativeElement.querySelector('tbody').children.length).toBe(2);
expect(fixture.nativeElement.querySelector('tbody tr td').textContent).toContain('source1');
});
it('should not display last event column', () => {
fixture.componentRef.setInput('loading', false);
fixture.componentRef.setInput('showLastEvent', false);
fixture.componentRef.setInput('sources', sourcesWithoutEvent);
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('table')).toBeTruthy();
expect(fixture.nativeElement.querySelector('thead tr').children.length).toBe(2);
expect(fixture.nativeElement.querySelector('tbody').children.length).toBe(2);
expect(fixture.nativeElement.querySelector('table tbody tr td').textContent).toContain('source1');
});
it('should emit sourceSelected event when a source is clicked', () => {
spyOn(component.sourceSelected, 'emit');
fixture.componentRef.setInput('loading', false);
fixture.componentRef.setInput('paginationConfig', paginationConfig);
fixture.componentRef.setInput('sources', sources);
fixture.detectChanges();
const button = fixture.nativeElement.querySelector('.btn-outline-primary');
button.click();
expect(component.sourceSelected.emit).toHaveBeenCalledWith('source1');
});
it('should emit paginationChange event when pagination changes', () => {
spyOn(component.paginationChange, 'emit');
fixture.componentRef.setInput('loading', false);
fixture.componentRef.setInput('paginationConfig', paginationConfig);
fixture.detectChanges();
const paginationComponent = fixture.nativeElement.querySelector('ds-pagination');
paginationComponent.dispatchEvent(new Event('paginationChange'));
expect(component.paginationChange.emit).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,75 @@
import {
AsyncPipe,
DatePipe,
} from '@angular/common';
import {
Component,
input,
InputSignal,
output,
} from '@angular/core';
import { RouterLink } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { AlertComponent } from '../../shared/alert/alert.component';
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
import { PaginationComponent } from '../../shared/pagination/pagination.component';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
export interface SourceObject {
id: string;
lastEvent?: string;
total: number;
}
/**
* Component to display the Quality Assurance source list.
*/
@Component({
selector: 'ds-source-list',
templateUrl: './source-list.component.html',
styleUrls: ['./source-list.component.scss'],
standalone: true,
imports: [AlertComponent, ThemedLoadingComponent, PaginationComponent, RouterLink, AsyncPipe, TranslateModule, DatePipe],
})
export class SourceListComponent {
/**
* A boolean indicating whether the sources are in a loading state.
*/
loading: InputSignal<boolean> = input<boolean>(false);
/**
* The pagination system configuration for HTML listing.
* @type {PaginationComponentOptions}
*/
paginationConfig: InputSignal<PaginationComponentOptions> = input<PaginationComponentOptions>();
/**
* A boolean indicating whether to show the last event column.
*/
showLastEvent: InputSignal<boolean> = input<boolean>();
/**
* The source list.
*/
sources: InputSignal<SourceObject[]|null> = input<SourceObject[]|null>();
/**
* The total number of Quality Assurance sources.
*/
totalElements: InputSignal<number> = input<number>();
/**
* Event emitter for when a source is selected.
* Emits the ID of the selected source.
*/
sourceSelected = output<string>();
/**
* Event emitter for when the pagination changes.
* Emits a string representation of the pagination change.
*/
paginationChange = output<string>();
}

View File

@@ -12,13 +12,13 @@ import {
import { TranslateModule } from '@ngx-translate/core';
import { take } from 'rxjs/operators';
import { Suggestion } from '../../core/notifications/suggestions/models/suggestion.model';
import { Collection } from '../../core/shared/collection.model';
import { ItemType } from '../../core/shared/item-relationships/item-type.model';
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
import { ThemedCreateItemParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-item-parent-selector/themed-create-item-parent-selector.component';
import { EntityDropdownComponent } from '../../shared/entity-dropdown/entity-dropdown.component';
import { SuggestionApproveAndImport } from '../suggestion-list-element/suggestion-approve-and-import';
import { Suggestion } from '../../../core/notifications/suggestions/models/suggestion.model';
import { Collection } from '../../../core/shared/collection.model';
import { ItemType } from '../../../core/shared/item-relationships/item-type.model';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { ThemedCreateItemParentSelectorComponent } from '../../../shared/dso-selector/modal-wrappers/create-item-parent-selector/themed-create-item-parent-selector.component';
import { EntityDropdownComponent } from '../../../shared/entity-dropdown/entity-dropdown.component';
import { SuggestionApproveAndImport } from '../list-element/suggestion-approve-and-import';
/**
* Show and trigger the actions to submit for a suggestion

View File

@@ -1,4 +1,4 @@
import { Suggestion } from '../../core/notifications/suggestions/models/suggestion.model';
import { Suggestion } from '../../../core/notifications/suggestions/models/suggestion.model';
/**
* A simple interface to unite a specific suggestion and the id of the chosen collection

View File

@@ -5,9 +5,9 @@ import {
} from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';
import { SuggestionEvidences } from '../../../core/notifications/suggestions/models/suggestion.model';
import { fadeIn } from '../../../shared/animations/fade';
import { ObjectKeysPipe } from '../../../shared/utils/object-keys-pipe';
import { SuggestionEvidences } from '../../../../core/notifications/suggestions/models/suggestion.model';
import { fadeIn } from '../../../../shared/animations/fade';
import { ObjectKeysPipe } from '../../../../shared/utils/object-keys-pipe';
/**
* Show suggestion evidences such as score (authorScore, dateScore)

View File

@@ -9,10 +9,10 @@ import { TranslateModule } from '@ngx-translate/core';
import { getTestScheduler } from 'jasmine-marbles';
import { TestScheduler } from 'rxjs/testing';
import { Item } from '../../core/shared/item.model';
import { mockSuggestionPublicationOne } from '../../shared/mocks/publication-claim.mock';
import { ItemSearchResultListElementComponent } from '../../shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component';
import { SuggestionActionsComponent } from '../suggestion-actions/suggestion-actions.component';
import { Item } from '../../../core/shared/item.model';
import { mockSuggestionPublicationOne } from '../../../shared/mocks/publication-claim.mock';
import { ItemSearchResultListElementComponent } from '../../../shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component';
import { SuggestionActionsComponent } from '../actions/suggestion-actions.component';
import { SuggestionEvidencesComponent } from './suggestion-evidences/suggestion-evidences.component';
import { SuggestionListElementComponent } from './suggestion-list-element.component';

View File

@@ -9,11 +9,11 @@ import {
import { TranslateModule } from '@ngx-translate/core';
import { Suggestion } from 'src/app/core/notifications/suggestions/models/suggestion.model';
import { Item } from '../../core/shared/item.model';
import { fadeIn } from '../../shared/animations/fade';
import { isNotEmpty } from '../../shared/empty.util';
import { ItemSearchResultListElementComponent } from '../../shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component';
import { SuggestionActionsComponent } from '../suggestion-actions/suggestion-actions.component';
import { Item } from '../../../core/shared/item.model';
import { fadeIn } from '../../../shared/animations/fade';
import { isNotEmpty } from '../../../shared/empty.util';
import { ItemSearchResultListElementComponent } from '../../../shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component';
import { SuggestionActionsComponent } from '../actions/suggestion-actions.component';
import { SuggestionApproveAndImport } from './suggestion-approve-and-import';
import { SuggestionEvidencesComponent } from './suggestion-evidences/suggestion-evidences.component';

View File

@@ -7,9 +7,9 @@ import { RouterLink } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { Observable } from 'rxjs';
import { SuggestionTarget } from '../../core/notifications/suggestions/models/suggestion-target.model';
import { SuggestionTargetsStateService } from '../suggestion-targets/suggestion-targets.state.service';
import { SuggestionTarget } from '../../../core/notifications/suggestions/models/suggestion-target.model';
import { SuggestionsService } from '../suggestions.service';
import { SuggestionTargetsStateService } from '../targets/suggestion-targets.state.service';
/**
* Show suggestions notification, used on myDSpace and Profile pages

View File

@@ -8,10 +8,10 @@ import { ActivatedRoute } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { of as observableOf } from 'rxjs';
import { mockSuggestionTargetsObjectOne } from '../../shared/mocks/publication-claim-targets.mock';
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
import { SuggestionTargetsStateService } from '../suggestion-targets/suggestion-targets.state.service';
import { mockSuggestionTargetsObjectOne } from '../../../shared/mocks/publication-claim-targets.mock';
import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub';
import { SuggestionsService } from '../suggestions.service';
import { SuggestionTargetsStateService } from '../targets/suggestion-targets.state.service';
import { SuggestionsPopupComponent } from './suggestions-popup.component';
describe('SuggestionsPopupComponent', () => {

View File

@@ -19,11 +19,11 @@ import {
takeUntil,
} from 'rxjs/operators';
import { SuggestionTarget } from '../../core/notifications/suggestions/models/suggestion-target.model';
import { fromTopEnter } from '../../shared/animations/fromTop';
import { isNotEmpty } from '../../shared/empty.util';
import { SuggestionTargetsStateService } from '../suggestion-targets/suggestion-targets.state.service';
import { SuggestionTarget } from '../../../core/notifications/suggestions/models/suggestion-target.model';
import { fromTopEnter } from '../../../shared/animations/fromTop';
import { isNotEmpty } from '../../../shared/empty.util';
import { SuggestionsService } from '../suggestions.service';
import { SuggestionTargetsStateService } from '../targets/suggestion-targets.state.service';
/**
* Show suggestions on a popover window, used on the homepage

View File

@@ -0,0 +1,14 @@
<div class="container">
<div class="row">
<div class="col-12">
<h1 class="border-bottom pb-2">{{'publication-claim.title'| translate}}</h1>
<ds-alert [type]="'alert-info'" [content]="'publication-claim.source.description'"></ds-alert>
</div>
</div>
<ds-source-list [loading]="loading$ | async"
[paginationConfig]="paginationConfig"
[showLastEvent]="false"
[sources]="sources$ | async"
[totalElements]="totalElements$ | async"
(sourceSelected)="onSelect($event)"></ds-source-list>
</div>

View File

@@ -0,0 +1,121 @@
import {
ComponentFixture,
TestBed,
waitForAsync,
} from '@angular/core/testing';
import {
ActivatedRoute,
provideRouter,
Router,
} from '@angular/router';
import {
TranslateLoader,
TranslateModule,
} from '@ngx-translate/core';
import { Observable } from 'rxjs';
import {
buildPaginatedList,
PaginatedList,
} from '../../../core/data/paginated-list.model';
import { RemoteData } from '../../../core/data/remote-data';
import { SuggestionSource } from '../../../core/notifications/suggestions/models/suggestion-source.model';
import { SuggestionSourceDataService } from '../../../core/notifications/suggestions/source/suggestion-source-data.service';
import { PaginationService } from '../../../core/pagination/pagination.service';
import { PageInfo } from '../../../core/shared/page-info.model';
import { AlertComponent } from '../../../shared/alert/alert.component';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
import { TranslateLoaderMock } from '../../../shared/testing/translate-loader.mock';
import { SourceListComponent } from '../../shared/source-list.component';
import { SuggestionSourcesComponent } from './suggestion-sources.component';
describe('SuggestionSourcesComponent', () => {
let component: SuggestionSourcesComponent;
let fixture: ComponentFixture<SuggestionSourcesComponent>;
let mockPaginationService: PaginationServiceStub;
const mockSources: any[] = [
{ id: 'source1', total: 5 },
{ id: 'source2', total: 10 },
];
const pageInfo = new PageInfo({
elementsPerPage: 5,
totalElements: 2,
totalPages: 1,
currentPage: 1,
});
const mockPaginatedList: PaginatedList<SuggestionSource> = buildPaginatedList(pageInfo, mockSources) ;
const mockPaginatedListRD: Observable<RemoteData<PaginatedList<SuggestionSource>>> = createSuccessfulRemoteDataObject$(mockPaginatedList) ;
const mockSuggestionSourceDataService: jasmine.SpyObj<SuggestionSourceDataService> = jasmine.createSpyObj('SuggestionSourceDataService', {
'getSources': jasmine.createSpy('getSources'),
});
beforeEach(waitForAsync(() => {
mockPaginationService = new PaginationServiceStub();
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateLoaderMock,
},
}),
SuggestionSourcesComponent,
],
providers: [
provideRouter([]),
{ provide: SuggestionSourceDataService, useValue: mockSuggestionSourceDataService },
{ provide: PaginationService, useValue: mockPaginationService },
],
}).overrideComponent(SuggestionSourcesComponent, {
remove: {
imports: [
AlertComponent,
SourceListComponent,
],
},
}).compileComponents();
}));
beforeEach(waitForAsync(() => {
// Mock the suggestion source data service to return an empty list
mockSuggestionSourceDataService.getSources.and.returnValue(mockPaginatedListRD);
console.log(mockSuggestionSourceDataService);
fixture = TestBed.createComponent(SuggestionSourcesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
}));
it('should create', () => {
expect(component).toBeTruthy();
});
it('should initialize with default pagination config', () => {
expect(component.paginationConfig.pageSize).toBe(10);
expect(component.paginationConfig.pageSizeOptions).toEqual([5, 10, 20, 40, 60]);
});
it('should load suggestion sources on init', () => {
expect(mockSuggestionSourceDataService.getSources).toHaveBeenCalled();
expect(component.sources$.value).toEqual(mockSources);
expect(component.totalElements$.value).toBe(2);
});
it('should update loading status', () => {
expect(component.loading$.value).toEqual(false);
});
it('should navigate to the specified source on select', () => {
const router = TestBed.inject(Router);
const route = TestBed.inject(ActivatedRoute);
spyOn(router, 'navigate');
const sourceId = 'test-source-id';
component.onSelect(sourceId);
expect(router.navigate).toHaveBeenCalledWith([sourceId], { relativeTo: route });
});
});

View File

@@ -0,0 +1,114 @@
import { AsyncPipe } from '@angular/common';
import { Component } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import {
ActivatedRoute,
Router,
} from '@angular/router';
import { TranslatePipe } from '@ngx-translate/core';
import {
BehaviorSubject,
Observable,
} from 'rxjs';
import {
distinctUntilChanged,
map,
switchMap,
tap,
} from 'rxjs/operators';
import { PaginatedList } from '../../../core/data/paginated-list.model';
import { RemoteData } from '../../../core/data/remote-data';
import { SuggestionSource } from '../../../core/notifications/suggestions/models/suggestion-source.model';
import { SuggestionSourceDataService } from '../../../core/notifications/suggestions/source/suggestion-source-data.service';
import { PaginationService } from '../../../core/pagination/pagination.service';
import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
import { AlertComponent } from '../../../shared/alert/alert.component';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
import {
SourceListComponent,
SourceObject,
} from '../../shared/source-list.component';
@Component({
selector: 'ds-suggestion-sources',
standalone: true,
imports: [
SourceListComponent,
AsyncPipe,
AlertComponent,
TranslatePipe,
],
templateUrl: './suggestion-sources.component.html',
styleUrl: './suggestion-sources.component.scss',
})
export class SuggestionSourcesComponent {
/**
* The pagination system configuration for HTML listing.
* @type {PaginationComponentOptions}
*/
public paginationConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
id: 'sl',
pageSize: 10,
pageSizeOptions: [5, 10, 20, 40, 60],
});
/**
* Returns the information about the loading status of the suggestion sources.
*/
public loading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
/**
* The suggestion source list.
*/
public sources$: BehaviorSubject<SourceObject[]> = new BehaviorSubject<SourceObject[]>([]);
/**
* The total number of Quality Assurance sources.
*/
public totalElements$: BehaviorSubject<number> = new BehaviorSubject<number>(0);
constructor(
protected paginationService: PaginationService,
protected router: Router,
protected route: ActivatedRoute,
protected suggestionSourceDataService: SuggestionSourceDataService) {
this.paginationService.getCurrentPagination(this.paginationConfig.id, this.paginationConfig).pipe(
distinctUntilChanged(),
switchMap((options: PaginationComponentOptions) => {
return this.retrieveSuggestionsSources(options.currentPage, options.pageSize);
}),
takeUntilDestroyed(),
).subscribe((results: Partial<PaginatedList<SuggestionSource>>) => {
console.log(results);
this.sources$.next(results.page);
this.totalElements$.next(results.pageInfo?.totalElements ?? 0);
this.loading$.next(false);
});
}
/**
* Navigate to the specified source
* @param sourceId
*/
onSelect(sourceId: string) {
this.router.navigate([sourceId], { relativeTo: this.route });
}
private retrieveSuggestionsSources(page: number, pageSize: number): Observable<Partial<PaginatedList<SuggestionSource>>> {
this.loading$.next(true);
const options = {
elementsPerPage: pageSize,
currentPage: page,
};
return this.suggestionSourceDataService.getSources(options).pipe(
getFirstCompletedRemoteData(),
tap(console.log),
map((result: RemoteData<PaginatedList<SuggestionSource>>) => {
return result.hasSucceeded ? result.payload : { page: [], pageInfo: null };
}),
);
}
}

View File

@@ -5,18 +5,18 @@ import { TestScheduler } from 'rxjs/testing';
import {
SortDirection,
SortOptions,
} from '../core/cache/models/sort-options.model';
import { FindListOptions } from '../core/data/find-list-options.model';
import { SuggestionTarget } from '../core/notifications/suggestions/models/suggestion-target.model';
import { SuggestionDataService } from '../core/notifications/suggestions/suggestion-data.service';
import { SuggestionTargetDataService } from '../core/notifications/suggestions/target/suggestion-target-data.service';
import { ResearcherProfile } from '../core/profile/model/researcher-profile.model';
import { ResearcherProfileDataService } from '../core/profile/researcher-profile-data.service';
import { ResourceType } from '../core/shared/resource-type';
import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service';
import { mockSuggestionPublicationOne } from '../shared/mocks/publication-claim.mock';
import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
import { followLink } from '../shared/utils/follow-link-config.model';
} from '../../core/cache/models/sort-options.model';
import { FindListOptions } from '../../core/data/find-list-options.model';
import { SuggestionTarget } from '../../core/notifications/suggestions/models/suggestion-target.model';
import { SuggestionDataService } from '../../core/notifications/suggestions/suggestion-data.service';
import { SuggestionTargetDataService } from '../../core/notifications/suggestions/target/suggestion-target-data.service';
import { ResearcherProfile } from '../../core/profile/model/researcher-profile.model';
import { ResearcherProfileDataService } from '../../core/profile/researcher-profile-data.service';
import { ResourceType } from '../../core/shared/resource-type';
import { WorkspaceitemDataService } from '../../core/submission/workspaceitem-data.service';
import { mockSuggestionPublicationOne } from '../../shared/mocks/publication-claim.mock';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { followLink } from '../../shared/utils/follow-link-config.model';
import { SuggestionsService } from './suggestions.service';
describe('SuggestionsService test', () => {

View File

@@ -12,37 +12,37 @@ import {
take,
} from 'rxjs/operators';
import { SuggestionConfig } from '../../config/suggestion-config.interfaces';
import { environment } from '../../environments/environment';
import { SuggestionConfig } from '../../../config/suggestion-config.interfaces';
import { environment } from '../../../environments/environment';
import {
SortDirection,
SortOptions,
} from '../core/cache/models/sort-options.model';
import { FindListOptions } from '../core/data/find-list-options.model';
import { PaginatedList } from '../core/data/paginated-list.model';
import { RemoteData } from '../core/data/remote-data';
import { Suggestion } from '../core/notifications/suggestions/models/suggestion.model';
import { SuggestionTarget } from '../core/notifications/suggestions/models/suggestion-target.model';
import { SuggestionDataService } from '../core/notifications/suggestions/suggestion-data.service';
import { SuggestionTargetDataService } from '../core/notifications/suggestions/target/suggestion-target-data.service';
import { ResearcherProfile } from '../core/profile/model/researcher-profile.model';
import { ResearcherProfileDataService } from '../core/profile/researcher-profile-data.service';
import { NoContent } from '../core/shared/NoContent.model';
} from '../../core/cache/models/sort-options.model';
import { FindListOptions } from '../../core/data/find-list-options.model';
import { PaginatedList } from '../../core/data/paginated-list.model';
import { RemoteData } from '../../core/data/remote-data';
import { Suggestion } from '../../core/notifications/suggestions/models/suggestion.model';
import { SuggestionTarget } from '../../core/notifications/suggestions/models/suggestion-target.model';
import { SuggestionDataService } from '../../core/notifications/suggestions/suggestion-data.service';
import { SuggestionTargetDataService } from '../../core/notifications/suggestions/target/suggestion-target-data.service';
import { ResearcherProfile } from '../../core/profile/model/researcher-profile.model';
import { ResearcherProfileDataService } from '../../core/profile/researcher-profile-data.service';
import { NoContent } from '../../core/shared/NoContent.model';
import {
getFinishedRemoteData,
getFirstCompletedRemoteData,
getFirstSucceededRemoteDataPayload,
getFirstSucceededRemoteListPayload,
} from '../core/shared/operators';
import { WorkspaceItem } from '../core/submission/models/workspaceitem.model';
import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service';
} from '../../core/shared/operators';
import { WorkspaceItem } from '../../core/submission/models/workspaceitem.model';
import { WorkspaceitemDataService } from '../../core/submission/workspaceitem-data.service';
import {
hasNoValue,
hasValue,
isNotEmpty,
} from '../shared/empty.util';
import { followLink } from '../shared/utils/follow-link-config.model';
import { getSuggestionPageRoute } from '../suggestions-page/suggestions-page-routing-paths';
} from '../../shared/empty.util';
import { followLink } from '../../shared/utils/follow-link-config.model';
import { getSuggestionPageRoute } from '../../suggestions-page/suggestions-page-routing-paths';
/**
* useful for multiple approvals and ignores operation

View File

@@ -2,7 +2,7 @@ import { AsyncPipe } from '@angular/common';
import {
AfterViewInit,
Component,
Input,
input,
OnDestroy,
OnInit,
} from '@angular/core';
@@ -20,13 +20,13 @@ import {
take,
} from 'rxjs/operators';
import { SuggestionTarget } from '../../../core/notifications/suggestions/models/suggestion-target.model';
import { PaginationService } from '../../../core/pagination/pagination.service';
import { hasValue } from '../../../shared/empty.util';
import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component';
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
import { getSuggestionPageRoute } from '../../../suggestions-page/suggestions-page-routing-paths';
import { SuggestionTarget } from '../../../../core/notifications/suggestions/models/suggestion-target.model';
import { PaginationService } from '../../../../core/pagination/pagination.service';
import { hasValue } from '../../../../shared/empty.util';
import { ThemedLoadingComponent } from '../../../../shared/loading/themed-loading.component';
import { PaginationComponent } from '../../../../shared/pagination/pagination.component';
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
import { getSuggestionPageRoute } from '../../../../suggestions-page/suggestions-page-routing-paths';
import { SuggestionsService } from '../../suggestions.service';
import { SuggestionTargetsStateService } from '../suggestion-targets.state.service';
@@ -51,14 +51,14 @@ export class PublicationClaimComponent implements AfterViewInit, OnDestroy, OnIn
/**
* The source for which to list targets
*/
@Input() source = '';
sourceId = input<string>();
/**
* The pagination system configuration for HTML listing.
* @type {PaginationComponentOptions}
*/
public paginationConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
id: 'stp_' + this.source,
id: 'stp_' + this.sourceId,
pageSizeOptions: [5, 10, 20, 40, 60],
});
@@ -95,8 +95,8 @@ export class PublicationClaimComponent implements AfterViewInit, OnDestroy, OnIn
* Component initialization.
*/
ngOnInit(): void {
this.targets$ = this.suggestionTargetsStateService.getSuggestionTargets(this.source);
this.totalElements$ = this.suggestionTargetsStateService.getSuggestionTargetsTotals(this.source);
this.targets$ = this.suggestionTargetsStateService.getSuggestionTargets(this.sourceId());
this.totalElements$ = this.suggestionTargetsStateService.getSuggestionTargetsTotals(this.sourceId());
}
/**
@@ -104,7 +104,7 @@ export class PublicationClaimComponent implements AfterViewInit, OnDestroy, OnIn
*/
ngAfterViewInit(): void {
this.subs.push(
this.suggestionTargetsStateService.isSuggestionTargetsLoaded(this.source).pipe(
this.suggestionTargetsStateService.isSuggestionTargetsLoaded(this.sourceId()).pipe(
take(1),
).subscribe(() => {
this.getSuggestionTargets();
@@ -119,7 +119,7 @@ export class PublicationClaimComponent implements AfterViewInit, OnDestroy, OnIn
* 'true' if the targets are loading, 'false' otherwise.
*/
public isTargetsLoading(): Observable<boolean> {
return this.suggestionTargetsStateService.isSuggestionTargetsLoading(this.source);
return this.suggestionTargetsStateService.isSuggestionTargetsLoading(this.sourceId());
}
/**
@@ -129,7 +129,7 @@ export class PublicationClaimComponent implements AfterViewInit, OnDestroy, OnIn
* 'true' if there are operations running on the targets (ex.: a REST call), 'false' otherwise.
*/
public isTargetsProcessing(): Observable<boolean> {
return this.suggestionTargetsStateService.isSuggestionTargetsProcessing(this.source);
return this.suggestionTargetsStateService.isSuggestionTargetsProcessing(this.sourceId());
}
/**
@@ -146,7 +146,7 @@ export class PublicationClaimComponent implements AfterViewInit, OnDestroy, OnIn
* Unsubscribe from all subscriptions.
*/
ngOnDestroy(): void {
this.suggestionTargetsStateService.dispatchClearSuggestionTargetsAction(this.source);
this.suggestionTargetsStateService.dispatchClearSuggestionTargetsAction(this.sourceId());
this.subs
.filter((sub) => hasValue(sub))
.forEach((sub) => sub.unsubscribe());
@@ -161,7 +161,7 @@ export class PublicationClaimComponent implements AfterViewInit, OnDestroy, OnIn
take(1),
).subscribe((options: PaginationComponentOptions) => {
this.suggestionTargetsStateService.dispatchRetrieveSuggestionTargets(
this.source,
this.sourceId(),
options.pageSize,
options.currentPage,
);

View File

@@ -4,12 +4,12 @@ import {
MemoizedSelector,
} from '@ngrx/store';
import { SuggestionTarget } from '../../core/notifications/suggestions/models/suggestion-target.model';
import { subStateSelector } from '../../submission/selectors';
import { SuggestionTarget } from '../../../core/notifications/suggestions/models/suggestion-target.model';
import { subStateSelector } from '../../../submission/selectors';
import {
suggestionNotificationsSelector,
SuggestionNotificationsState,
} from '../notifications.reducer';
} from '../../notifications.reducer';
import {
SuggestionTargetEntry,
SuggestionTargetState,

View File

@@ -1,8 +1,8 @@
/* eslint-disable max-classes-per-file */
import { Action } from '@ngrx/store';
import { SuggestionTarget } from '../../core/notifications/suggestions/models/suggestion-target.model';
import { type } from '../../shared/ngrx/type';
import { SuggestionTarget } from '../../../core/notifications/suggestions/models/suggestion-target.model';
import { type } from '../../../shared/ngrx/type';
/**
* For each action type in an action group, make a simple

View File

@@ -21,10 +21,10 @@ import { getFirstCompletedRemoteData } from 'src/app/core/shared/operators';
import {
AuthActionTypes,
RetrieveAuthenticatedEpersonSuccessAction,
} from '../../core/auth/auth.actions';
import { PaginatedList } from '../../core/data/paginated-list.model';
import { SuggestionTarget } from '../../core/notifications/suggestions/models/suggestion-target.model';
import { NotificationsService } from '../../shared/notifications/notifications.service';
} from '../../../core/auth/auth.actions';
import { PaginatedList } from '../../../core/data/paginated-list.model';
import { SuggestionTarget } from '../../../core/notifications/suggestions/models/suggestion-target.model';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { SuggestionsService } from '../suggestions.service';
import {
AddTargetAction,

View File

@@ -1,4 +1,4 @@
import { SuggestionTarget } from '../../core/notifications/suggestions/models/suggestion-target.model';
import { SuggestionTarget } from '../../../core/notifications/suggestions/models/suggestion-target.model';
import {
SuggestionTargetActionTypes,
SuggestionTargetsActions,

View File

@@ -6,8 +6,8 @@ import {
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { SuggestionTarget } from '../../core/notifications/suggestions/models/suggestion-target.model';
import { SuggestionNotificationsState } from '../notifications.reducer';
import { SuggestionTarget } from '../../../core/notifications/suggestions/models/suggestion-target.model';
import { SuggestionNotificationsState } from '../../notifications.reducer';
import {
getCurrentUserSuggestionTargetsSelector,
getCurrentUserSuggestionTargetsVisitedSelector,

View File

@@ -29,7 +29,7 @@ import { AuthorizationDataService } from '../core/data/feature-authorization/aut
import { EPersonDataService } from '../core/eperson/eperson-data.service';
import { EPerson } from '../core/eperson/models/eperson.model';
import { ConfigurationProperty } from '../core/shared/configuration-property.model';
import { SuggestionsNotificationComponent } from '../notifications/suggestions-notification/suggestions-notification.component';
import { SuggestionsNotificationComponent } from '../notifications/suggestions/notification/suggestions-notification.component';
import { ErrorComponent } from '../shared/error/error.component';
import { ThemedLoadingComponent } from '../shared/loading/themed-loading.component';
import { NotificationsService } from '../shared/notifications/notifications.service';

View File

@@ -40,7 +40,7 @@ import {
getFirstCompletedRemoteData,
getRemoteDataPayload,
} from '../core/shared/operators';
import { SuggestionsNotificationComponent } from '../notifications/suggestions-notification/suggestions-notification.component';
import { SuggestionsNotificationComponent } from '../notifications/suggestions/notification/suggestions-notification.component';
import {
hasValue,
isNotEmpty,

View File

@@ -2,7 +2,7 @@ import { Route } from '@angular/router';
import { authenticatedGuard } from '../core/auth/authenticated.guard';
import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
import { qualityAssuranceBreadcrumbResolver } from '../core/breadcrumbs/quality-assurance-breadcrumb.resolver';
import { sourcesBreadcrumbResolver } from '../core/breadcrumbs/sources-breadcrumb.resolver';
import {
NOTIFICATIONS_RECITER_SUGGESTION_PATH,
QUALITY_ASSURANCE_EDIT_PATH,
@@ -39,7 +39,7 @@ export const ROUTES: Route[] = [
component: QualityAssuranceTopicsPageComponent,
pathMatch: 'full',
resolve: {
breadcrumb: qualityAssuranceBreadcrumbResolver,
breadcrumb: sourcesBreadcrumbResolver,
openaireQualityAssuranceTopicsParams: QualityAssuranceTopicsPageResolver,
},
data: {
@@ -85,7 +85,7 @@ export const ROUTES: Route[] = [
component: QualityAssuranceEventsPageComponent,
pathMatch: 'full',
resolve: {
breadcrumb: qualityAssuranceBreadcrumbResolver,
breadcrumb: sourcesBreadcrumbResolver,
openaireQualityAssuranceEventsParams: qualityAssuranceEventsPageResolver,
},
data: {

View File

@@ -1 +1 @@
<ds-publication-claim [source]="'openaire'"></ds-publication-claim>
<ds-suggestion-sources></ds-suggestion-sources>

View File

@@ -7,9 +7,10 @@ import {
} from '@angular/core/testing';
import { ActivatedRoute } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { MockComponent } from 'ng-mocks';
import { AdminNotificationsPublicationClaimPageComponent } from '../../admin/admin-notifications/admin-notifications-publication-claim-page/admin-notifications-publication-claim-page.component';
import { PublicationClaimComponent } from '../../notifications/suggestion-targets/publication-claim/publication-claim.component';
import { SuggestionSourcesComponent } from '../../notifications/suggestions/sources/suggestion-sources.component';
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
import { NotificationsSuggestionTargetsPageComponent } from './notifications-suggestion-targets-page.component';
@@ -23,19 +24,14 @@ describe('NotificationsSuggestionTargetsPageComponent', () => {
CommonModule,
TranslateModule.forRoot(),
NotificationsSuggestionTargetsPageComponent,
MockComponent(SuggestionSourcesComponent),
],
providers: [
AdminNotificationsPublicationClaimPageComponent,
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
],
schemas: [NO_ERRORS_SCHEMA],
})
.overrideComponent(NotificationsSuggestionTargetsPageComponent, {
remove: {
imports: [PublicationClaimComponent],
},
})
.compileComponents();
}).compileComponents();
}));
beforeEach(() => {

View File

@@ -1,13 +1,13 @@
import { Component } from '@angular/core';
import { PublicationClaimComponent } from '../../notifications/suggestion-targets/publication-claim/publication-claim.component';
import { SuggestionSourcesComponent } from '../../notifications/suggestions/sources/suggestion-sources.component';
@Component({
selector: 'ds-notifications-reciter-page',
templateUrl: './notifications-suggestion-targets-page.component.html',
styleUrls: ['./notifications-suggestion-targets-page.component.scss'],
imports: [
PublicationClaimComponent,
SuggestionSourcesComponent,
],
standalone: true,
})

View File

@@ -34,7 +34,7 @@ export const qualityAssuranceSourceDataResolver: ResolveFn<QualityAssuranceSourc
): Observable<QualityAssuranceSourceObject[]> => {
const pageSize = appConfig.qualityAssuranceConfig.pageSize;
return qualityAssuranceSourceService.getSources(pageSize, 0).pipe(
return qualityAssuranceSourceService.getSources(pageSize, 1).pipe(
map((sources: PaginatedList<QualityAssuranceSourceObject>) => {
if (sources.page.length === 1) {
router.navigate([getResolvedUrl(route) + '/' + sources.page[0].id]);

View File

@@ -1,9 +1,14 @@
import {
ChangeDetectorRef,
Component,
EventEmitter,
Input,
Output,
} from '@angular/core';
import { ThemedComponent } from 'src/app/shared/theme-support/themed.component';
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
import { ThemeService } from '../../../theme-support/theme.service';
import { CreateItemParentSelectorComponent } from './create-item-parent-selector.component';
/**
@@ -19,8 +24,17 @@ import { CreateItemParentSelectorComponent } from './create-item-parent-selector
export class ThemedCreateItemParentSelectorComponent
extends ThemedComponent<CreateItemParentSelectorComponent> {
@Input() entityType: string;
@Output() select: EventEmitter<DSpaceObject> = new EventEmitter<DSpaceObject>();
@Input() emitOnly = false;
protected inAndOutputNames: (keyof CreateItemParentSelectorComponent & keyof this)[] = ['entityType'];
protected inAndOutputNames: (keyof CreateItemParentSelectorComponent & keyof this)[] = ['entityType', 'select', 'emitOnly'];
constructor(
protected cdr: ChangeDetectorRef,
protected themeService: ThemeService,
) {
super(cdr, themeService);
}
protected getComponentName(): string {
return 'CreateItemParentSelectorComponent';

View File

@@ -22,11 +22,11 @@ import { TestScheduler } from 'rxjs/testing';
import { AuthService } from '../core/auth/auth.service';
import { PaginationService } from '../core/pagination/pagination.service';
import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service';
import { SuggestionApproveAndImport } from '../notifications/suggestion-list-element/suggestion-approve-and-import';
import { SuggestionEvidencesComponent } from '../notifications/suggestion-list-element/suggestion-evidences/suggestion-evidences.component';
import { SuggestionListElementComponent } from '../notifications/suggestion-list-element/suggestion-list-element.component';
import { SuggestionTargetsStateService } from '../notifications/suggestion-targets/suggestion-targets.state.service';
import { SuggestionsService } from '../notifications/suggestions.service';
import { SuggestionApproveAndImport } from '../notifications/suggestions/list-element/suggestion-approve-and-import';
import { SuggestionEvidencesComponent } from '../notifications/suggestions/list-element/suggestion-evidences/suggestion-evidences.component';
import { SuggestionListElementComponent } from '../notifications/suggestions/list-element/suggestion-list-element.component';
import { SuggestionsService } from '../notifications/suggestions/suggestions.service';
import { SuggestionTargetsStateService } from '../notifications/suggestions/targets/suggestion-targets.state.service';
import {
mockSuggestionPublicationOne,
mockSuggestionPublicationTwo,

View File

@@ -43,14 +43,14 @@ import {
} from '../core/shared/operators';
import { WorkspaceItem } from '../core/submission/models/workspaceitem.model';
import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service';
import { SuggestionActionsComponent } from '../notifications/suggestion-actions/suggestion-actions.component';
import { SuggestionApproveAndImport } from '../notifications/suggestion-list-element/suggestion-approve-and-import';
import { SuggestionListElementComponent } from '../notifications/suggestion-list-element/suggestion-list-element.component';
import { SuggestionTargetsStateService } from '../notifications/suggestion-targets/suggestion-targets.state.service';
import { SuggestionActionsComponent } from '../notifications/suggestions/actions/suggestion-actions.component';
import { SuggestionApproveAndImport } from '../notifications/suggestions/list-element/suggestion-approve-and-import';
import { SuggestionListElementComponent } from '../notifications/suggestions/list-element/suggestion-list-element.component';
import {
SuggestionBulkResult,
SuggestionsService,
} from '../notifications/suggestions.service';
} from '../notifications/suggestions/suggestions.service';
import { SuggestionTargetsStateService } from '../notifications/suggestions/targets/suggestion-targets.state.service';
import { AlertComponent } from '../shared/alert/alert.component';
import { ThemedLoadingComponent } from '../shared/loading/themed-loading.component';
import { NotificationsService } from '../shared/notifications/notifications.service';

View File

@@ -2832,10 +2832,18 @@
"item.preview.dc.type": "Type:",
"item.preview.oaire.version": "Version",
"item.preview.oaire.citation.issue": "Issue",
"item.preview.oaire.citation.volume": "Volume",
"item.preview.oaire.citation.title": "Citation container",
"item.preview.oaire.citation.startPage": "Citation start page",
"item.preview.oaire.citation.endPage": "Citation end page",
"item.preview.dc.relation.issn": "ISSN",
"item.preview.dc.identifier.isbn": "ISBN",
@@ -2854,6 +2862,8 @@
"item.preview.person.identifier.orcid": "ORCID:",
"item.preview.person.affiliation.name": "Affiliations:",
"item.preview.project.funder.name": "Funder:",
"item.preview.project.funder.identifier": "Funder Identifier:",
@@ -2884,6 +2894,16 @@
"item.preview.dspace.entity.type": "Entity Type:",
"item.preview.creativework.publisher": "Publisher",
"item.preview.creativeworkseries.issn": "ISSN",
"item.preview.dc.identifier.issn": "ISSN",
"item.preview.dc.identifier.openalex": "OpenAlex Identifier",
"item.preview.dc.description": "Description",
"item.select.confirm": "Confirm selected",
"item.select.empty": "No items to show",
@@ -3536,6 +3556,10 @@
"none.listelement.badge": "Item",
"publication-claim.title": "Publication claim",
"publication-claim.source.description": "Below you can see all the sources.",
"quality-assurance.title": "Quality Assurance",
"quality-assurance.topics.description": "Below you can see all the topics received from the subscriptions to {{source}}.",
@@ -4064,6 +4088,8 @@
"suggestion.source.openaire": "OpenAIRE Graph",
"suggestion.source.openalex": "OpenAlex",
"suggestion.from.source": "from the ",
"suggestion.count.missing": "You have no publication claims left",
@@ -4854,6 +4880,22 @@
"submission.import-external.source.ror": "Research Organization Registry (ROR)",
"submission.import-external.source.openalexPublication": "OpenAlex Search by Title",
"submission.import-external.source.openalexPublicationByAuthorId": "OpenAlex Search by Author ID",
"submission.import-external.source.openalexPublicationByDOI": "OpenAlex Search by DOI",
"submission.import-external.source.openalexPerson": "OpenAlex Search by name",
"submission.import-external.source.openalexJournal": "OpenAlex Journals",
"submission.import-external.source.openalexInstitution": "OpenAlex Institutions",
"submission.import-external.source.openalexPublisher": "OpenAlex Publishers",
"submission.import-external.source.openalexFunder": "OpenAlex Funders",
"submission.import-external.preview.title": "Item Preview",
"submission.import-external.preview.title.Publication": "Publication Preview",

View File

@@ -11,7 +11,7 @@ import { ThemedHomeNewsComponent } from '../../../../app/home-page/home-news/the
import { HomePageComponent as BaseComponent } from '../../../../app/home-page/home-page.component';
import { RecentItemListComponent } from '../../../../app/home-page/recent-item-list/recent-item-list.component';
import { ThemedTopLevelCommunityListComponent } from '../../../../app/home-page/top-level-community-list/themed-top-level-community-list.component';
import { SuggestionsPopupComponent } from '../../../../app/notifications/suggestions-popup/suggestions-popup.component';
import { SuggestionsPopupComponent } from '../../../../app/notifications/suggestions/popup/suggestions-popup.component';
import { ThemedConfigurationSearchPageComponent } from '../../../../app/search-page/themed-configuration-search-page.component';
import { ThemedSearchFormComponent } from '../../../../app/shared/search-form/themed-search-form.component';
import { PageWithSidebarComponent } from '../../../../app/shared/sidebar/page-with-sidebar.component';

View File

@@ -11,7 +11,7 @@ import {
import { MyDSpaceNewSubmissionComponent } from '../../../../app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component';
import { MyDSpacePageComponent as BaseComponent } from '../../../../app/my-dspace-page/my-dspace-page.component';
import { MyDspaceQaEventsNotificationsComponent } from '../../../../app/my-dspace-page/my-dspace-qa-events-notifications/my-dspace-qa-events-notifications.component';
import { SuggestionsNotificationComponent } from '../../../../app/notifications/suggestions-notification/suggestions-notification.component';
import { SuggestionsNotificationComponent } from '../../../../app/notifications/suggestions/notification/suggestions-notification.component';
import { pushInOut } from '../../../../app/shared/animations/push';
import { RoleDirective } from '../../../../app/shared/roles/role.directive';
import { ThemedSearchComponent } from '../../../../app/shared/search/themed-search.component';

View File

@@ -5,7 +5,7 @@ import {
import { Component } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';
import { SuggestionsNotificationComponent } from '../../../../app/notifications/suggestions-notification/suggestions-notification.component';
import { SuggestionsNotificationComponent } from '../../../../app/notifications/suggestions/notification/suggestions-notification.component';
import { ProfilePageComponent as BaseComponent } from '../../../../app/profile-page/profile-page.component';
import { ThemedProfilePageMetadataFormComponent } from '../../../../app/profile-page/profile-page-metadata-form/themed-profile-page-metadata-form.component';
import { ProfilePageResearcherFormComponent } from '../../../../app/profile-page/profile-page-researcher-form/profile-page-researcher-form.component';