Merge branch 'main' into CST-7755-refactoring

# Conflicts:
#	src/app/core/core.module.ts
#	src/app/shared/shared.module.ts
This commit is contained in:
Giuseppe Digilio
2023-02-10 19:52:55 +01:00
224 changed files with 5670 additions and 1622 deletions

View File

@@ -47,6 +47,12 @@ import { BatchImportPageComponent } from './admin-import-batch-page/batch-import
component: BatchImportPageComponent,
data: { title: 'admin.batch-import.title', breadcrumbKey: 'admin.batch-import' }
},
{
path: 'system-wide-alert',
resolve: { breadcrumb: I18nBreadcrumbResolver },
loadChildren: () => import('../system-wide-alert/system-wide-alert.module').then((m) => m.SystemWideAlertModule),
data: {title: 'admin.system-wide-alert.title', breadcrumbKey: 'admin.system-wide-alert'}
},
])
],
providers: [

View File

@@ -1,7 +1,7 @@
<div class="sidebar-section">
<a class="nav-item nav-link d-flex flex-row flex-nowrap"
[ngClass]="{ disabled: !hasLink }"
[attr.aria-disabled]="!hasLink"
[ngClass]="{ disabled: isDisabled }"
[attr.aria-disabled]="isDisabled"
[attr.aria-labelledby]="'sidebarName-' + section.id"
[title]="('menu.section.icon.' + section.id) | translate"
[routerLink]="itemModel.link"

View File

@@ -17,38 +17,86 @@ describe('AdminSidebarSectionComponent', () => {
const menuService = new MenuServiceStub();
const iconString = 'test';
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [NoopAnimationsModule, RouterTestingModule, TranslateModule.forRoot()],
declarations: [AdminSidebarSectionComponent, TestComponent],
providers: [
{ provide: 'sectionDataProvider', useValue: { model: { link: 'google.com' }, icon: iconString } },
{ provide: MenuService, useValue: menuService },
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
]
}).overrideComponent(AdminSidebarSectionComponent, {
set: {
entryComponents: [TestComponent]
}
})
.compileComponents();
}));
describe('when not disabled', () => {
beforeEach(() => {
fixture = TestBed.createComponent(AdminSidebarSectionComponent);
component = fixture.componentInstance;
spyOn(component as any, 'getMenuItemComponent').and.returnValue(TestComponent);
fixture.detectChanges();
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [NoopAnimationsModule, RouterTestingModule, TranslateModule.forRoot()],
declarations: [AdminSidebarSectionComponent, TestComponent],
providers: [
{provide: 'sectionDataProvider', useValue: {model: {link: 'google.com'}, icon: iconString}},
{provide: MenuService, useValue: menuService},
{provide: CSSVariableService, useClass: CSSVariableServiceStub},
]
}).overrideComponent(AdminSidebarSectionComponent, {
set: {
entryComponents: [TestComponent]
}
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AdminSidebarSectionComponent);
component = fixture.componentInstance;
spyOn(component as any, 'getMenuItemComponent').and.returnValue(TestComponent);
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should set the right icon', () => {
const icon = fixture.debugElement.query(By.css('.shortcut-icon')).query(By.css('i.fas'));
expect(icon.nativeElement.getAttribute('class')).toContain('fa-' + iconString);
});
it('should not contain the disabled class', () => {
const disabled = fixture.debugElement.query(By.css('.disabled'));
expect(disabled).toBeFalsy();
});
});
describe('when disabled', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [NoopAnimationsModule, RouterTestingModule, TranslateModule.forRoot()],
declarations: [AdminSidebarSectionComponent, TestComponent],
providers: [
{provide: 'sectionDataProvider', useValue: {model: {link: 'google.com', disabled: true}, icon: iconString}},
{provide: MenuService, useValue: menuService},
{provide: CSSVariableService, useClass: CSSVariableServiceStub},
]
}).overrideComponent(AdminSidebarSectionComponent, {
set: {
entryComponents: [TestComponent]
}
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AdminSidebarSectionComponent);
component = fixture.componentInstance;
spyOn(component as any, 'getMenuItemComponent').and.returnValue(TestComponent);
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should set the right icon', () => {
const icon = fixture.debugElement.query(By.css('.shortcut-icon')).query(By.css('i.fas'));
expect(icon.nativeElement.getAttribute('class')).toContain('fa-' + iconString);
});
it('should contain the disabled class', () => {
const disabled = fixture.debugElement.query(By.css('.disabled'));
expect(disabled).toBeTruthy();
});
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should set the right icon', () => {
const icon = fixture.debugElement.query(By.css('.shortcut-icon')).query(By.css('i.fas'));
expect(icon.nativeElement.getAttribute('class')).toContain('fa-' + iconString);
});
});
// declare a test component

View File

@@ -5,7 +5,7 @@ import { rendersSectionForMenu } from '../../../shared/menu/menu-section.decorat
import { LinkMenuItemModel } from '../../../shared/menu/menu-item/models/link.model';
import { MenuSection } from '../../../shared/menu/menu-section.model';
import { MenuID } from '../../../shared/menu/menu-id.model';
import { isNotEmpty } from '../../../shared/empty.util';
import { isEmpty } from '../../../shared/empty.util';
import { Router } from '@angular/router';
/**
@@ -26,7 +26,12 @@ export class AdminSidebarSectionComponent extends MenuSectionComponent implement
*/
menuID: MenuID = MenuID.ADMIN;
itemModel;
hasLink: boolean;
/**
* Boolean to indicate whether this section is disabled
*/
isDisabled: boolean;
constructor(
@Inject('sectionDataProvider') menuSection: MenuSection,
protected menuService: MenuService,
@@ -38,13 +43,13 @@ export class AdminSidebarSectionComponent extends MenuSectionComponent implement
}
ngOnInit(): void {
this.hasLink = isNotEmpty(this.itemModel?.link);
this.isDisabled = this.itemModel?.disabled || isEmpty(this.itemModel?.link);
super.ngOnInit();
}
navigate(event: any): void {
event.preventDefault();
if (this.hasLink) {
if (!this.isDisabled) {
this.router.navigate(this.itemModel.link);
}
}

View File

@@ -7,6 +7,7 @@
[attr.aria-labelledby]="'sidebarName-' + section.id"
[attr.aria-expanded]="expanded | async"
[title]="('menu.section.icon.' + section.id) | translate"
[class.disabled]="section.model?.disabled"
(click)="toggleSection($event)"
(keyup.space)="toggleSection($event)"
(keyup.enter)="toggleSection($event)"

View File

@@ -23,7 +23,7 @@ describe('ExpandableAdminSidebarSectionComponent', () => {
imports: [NoopAnimationsModule, TranslateModule.forRoot()],
declarations: [ExpandableAdminSidebarSectionComponent, TestComponent],
providers: [
{ provide: 'sectionDataProvider', useValue: { icon: iconString } },
{ provide: 'sectionDataProvider', useValue: { icon: iconString, model: {} } },
{ provide: MenuService, useValue: menuService },
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
{ provide: Router, useValue: new RouterStub() },

View File

@@ -1,7 +1,9 @@
<div class="container">
<ng-container *ngVar="(parent$ | async) as parent">
<ng-container *ngIf="parent?.payload as parentContext">
<header class="comcol-header border-bottom mb-4 pb-4">
<div class="d-flex flex-row border-bottom mb-4 pb-4">
<header class="comcol-header mr-auto">
<!-- Parent Name -->
<ds-comcol-page-header [name]="parentContext.name">
</ds-comcol-page-header>
@@ -22,6 +24,8 @@
<ds-comcol-page-content [content]="parentContext.sidebarText" [hasInnerHtml]="true" [title]="'community.page.news'">
</ds-comcol-page-content>
</header>
<ds-dso-edit-menu></ds-dso-edit-menu>
</div>
<!-- Browse-By Links -->
<ds-themed-comcol-page-browse-by [id]="parentContext.id" [contentType]="parentContext.type"></ds-themed-comcol-page-browse-by>
</ng-container></ng-container>

View File

@@ -4,13 +4,17 @@ import { BrowseByGuard } from './browse-by-guard';
import { BrowseByDSOBreadcrumbResolver } from './browse-by-dso-breadcrumb.resolver';
import { BrowseByI18nBreadcrumbResolver } from './browse-by-i18n-breadcrumb.resolver';
import { ThemedBrowseBySwitcherComponent } from './browse-by-switcher/themed-browse-by-switcher.component';
import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver';
@NgModule({
imports: [
RouterModule.forChild([
{
path: '',
resolve: { breadcrumb: BrowseByDSOBreadcrumbResolver },
resolve: {
breadcrumb: BrowseByDSOBreadcrumbResolver,
menu: DSOEditMenuResolver
},
children: [
{
path: ':id',

View File

@@ -10,6 +10,7 @@ import { ThemedBrowseByMetadataPageComponent } from './browse-by-metadata-page/t
import { ThemedBrowseByDatePageComponent } from './browse-by-date-page/themed-browse-by-date-page.component';
import { ThemedBrowseByTitlePageComponent } from './browse-by-title-page/themed-browse-by-title-page.component';
import { SharedBrowseByModule } from '../shared/browse-by/shared-browse-by.module';
import { DsoPageModule } from '../shared/dso-page/dso-page.module';
const ENTRY_COMPONENTS = [
// put only entry components that use custom decorator
@@ -28,6 +29,7 @@ const ENTRY_COMPONENTS = [
SharedBrowseByModule,
CommonModule,
ComcolModule,
DsoPageModule
],
declarations: [
BrowseBySwitcherComponent,

View File

@@ -21,6 +21,7 @@ import { CollectionPageAdministratorGuard } from './collection-page-administrato
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
import { ThemedCollectionPageComponent } from './themed-collection-page.component';
import { MenuItemType } from '../shared/menu/menu-item-type.model';
import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver';
@NgModule({
imports: [
@@ -34,7 +35,8 @@ import { MenuItemType } from '../shared/menu/menu-item-type.model';
path: ':id',
resolve: {
dso: CollectionPageResolver,
breadcrumb: CollectionBreadcrumbResolver
breadcrumb: CollectionBreadcrumbResolver,
menu: DSOEditMenuResolver
},
runGuardsAndResolvers: 'always',
children: [
@@ -91,7 +93,7 @@ import { MenuItemType } from '../shared/menu/menu-item-type.model';
DSOBreadcrumbsService,
LinkService,
CreateCollectionPageGuard,
CollectionPageAdministratorGuard
CollectionPageAdministratorGuard,
]
})
export class CollectionPageRoutingModule {

View File

@@ -33,9 +33,7 @@
[title]="'collection.page.news'">
</ds-comcol-page-content>
</header>
<div class="pl-2 space-children-mr">
<ds-dso-page-edit-button *ngIf="isCollectionAdmin$ | async" [pageRoute]="collectionPageRoute$ | async" [dso]="collection" [tooltipMsg]="'collection.page.edit'"></ds-dso-page-edit-button>
</div>
<ds-dso-edit-menu></ds-dso-edit-menu>
</div>
<section class="comcol-page-browse-section">
<!-- Browse-By Links -->

View File

@@ -17,6 +17,7 @@ import { CollectionFormModule } from './collection-form/collection-form.module';
import { ThemedCollectionPageComponent } from './themed-collection-page.component';
import { ComcolModule } from '../shared/comcol/comcol.module';
import { DsoSharedModule } from '../dso-shared/dso-shared.module';
import { DsoPageModule } from '../shared/dso-page/dso-page.module';
@NgModule({
imports: [
@@ -28,6 +29,7 @@ import { DsoSharedModule } from '../dso-shared/dso-shared.module';
CollectionFormModule,
ComcolModule,
DsoSharedModule,
DsoPageModule,
],
declarations: [
CollectionPageComponent,

View File

@@ -14,6 +14,7 @@ import { CommunityPageAdministratorGuard } from './community-page-administrator.
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
import { ThemedCommunityPageComponent } from './themed-community-page.component';
import { MenuItemType } from '../shared/menu/menu-item-type.model';
import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver';
@NgModule({
imports: [
@@ -27,7 +28,8 @@ import { MenuItemType } from '../shared/menu/menu-item-type.model';
path: ':id',
resolve: {
dso: CommunityPageResolver,
breadcrumb: CommunityBreadcrumbResolver
breadcrumb: CommunityBreadcrumbResolver,
menu: DSOEditMenuResolver
},
runGuardsAndResolvers: 'always',
children: [
@@ -73,7 +75,7 @@ import { MenuItemType } from '../shared/menu/menu-item-type.model';
DSOBreadcrumbsService,
LinkService,
CreateCommunityPageGuard,
CommunityPageAdministratorGuard
CommunityPageAdministratorGuard,
]
})
export class CommunityPageRoutingModule {

View File

@@ -20,9 +20,7 @@
[title]="'community.page.news'">
</ds-comcol-page-content>
</header>
<div class="pl-2 space-children-mr">
<ds-dso-page-edit-button *ngIf="isCommunityAdmin$ | async" [pageRoute]="communityPageRoute$ | async" [dso]="communityPayload" [tooltipMsg]="'community.page.edit'"></ds-dso-page-edit-button>
</div>
<ds-dso-edit-menu></ds-dso-edit-menu>
</div>
<section class="comcol-page-browse-section">

View File

@@ -19,6 +19,7 @@ import {
import {
ThemedCollectionPageSubCollectionListComponent
} from './sub-collection-list/themed-community-page-sub-collection-list.component';
import { DsoPageModule } from '../shared/dso-page/dso-page.module';
const DECLARATIONS = [CommunityPageComponent,
ThemedCommunityPageComponent,
@@ -37,6 +38,7 @@ const DECLARATIONS = [CommunityPageComponent,
StatisticsModule.forRoot(),
CommunityFormModule,
ComcolModule,
DsoPageModule,
],
declarations: [
...DECLARATIONS

View File

@@ -11,6 +11,7 @@ import { HttpOptions } from '../dspace-rest/dspace-rest.service';
import objectContaining = jasmine.objectContaining;
import { AuthStatus } from './models/auth-status.model';
import { RestRequestMethod } from '../data/rest-request-method';
import { Observable, of as observableOf } from 'rxjs';
describe(`AuthRequestService`, () => {
let halService: HALEndpointService;
@@ -34,8 +35,8 @@ describe(`AuthRequestService`, () => {
super(hes, rs, rdbs);
}
protected createShortLivedTokenRequest(href: string): PostRequest {
return new PostRequest(this.requestService.generateRequestId(), href);
protected createShortLivedTokenRequest(href: string): Observable<PostRequest> {
return observableOf(new PostRequest(this.requestService.generateRequestId(), href));
}
}

View File

@@ -100,14 +100,12 @@ export abstract class AuthRequestService {
);
}
/**
* Factory function to create the request object to send. This needs to be a POST client side and
* a GET server side. Due to CSRF validation, the server isn't allowed to send a POST, so we allow
* only the server IP to send a GET to this endpoint.
* Factory function to create the request object to send.
*
* @param href The href to send the request to
* @protected
*/
protected abstract createShortLivedTokenRequest(href: string): GetRequest | PostRequest;
protected abstract createShortLivedTokenRequest(href: string): Observable<PostRequest>;
/**
* Send a request to retrieve a short-lived token which provides download access of restricted files
@@ -117,7 +115,7 @@ export abstract class AuthRequestService {
filter((href: string) => isNotEmpty(href)),
distinctUntilChanged(),
map((href: string) => new URLCombiner(href, this.shortlivedtokensEndpoint).toString()),
map((endpointURL: string) => this.createShortLivedTokenRequest(endpointURL)),
switchMap((endpointURL: string) => this.createShortLivedTokenRequest(endpointURL)),
tap((request: RestRequest) => this.requestService.send(request)),
switchMap((request: RestRequest) => this.rdbService.buildFromRequestUUID<ShortLivedToken>(request.uuid)),
getFirstCompletedRemoteData(),

View File

@@ -1,6 +1,8 @@
import { AuthRequestService } from './auth-request.service';
import { RequestService } from '../data/request.service';
import { BrowserAuthRequestService } from './browser-auth-request.service';
import { Observable } from 'rxjs';
import { PostRequest } from '../data/request.models';
describe(`BrowserAuthRequestService`, () => {
let href: string;
@@ -16,14 +18,20 @@ describe(`BrowserAuthRequestService`, () => {
});
describe(`createShortLivedTokenRequest`, () => {
it(`should return a PostRequest`, () => {
const result = (service as any).createShortLivedTokenRequest(href);
expect(result.constructor.name).toBe('PostRequest');
it(`should return a PostRequest`, (done) => {
const obs = (service as any).createShortLivedTokenRequest(href) as Observable<PostRequest>;
obs.subscribe((result: PostRequest) => {
expect(result.constructor.name).toBe('PostRequest');
done();
});
});
it(`should return a request with the given href`, () => {
const result = (service as any).createShortLivedTokenRequest(href);
expect(result.href).toBe(href) ;
it(`should return a request with the given href`, (done) => {
const obs = (service as any).createShortLivedTokenRequest(href) as Observable<PostRequest>;
obs.subscribe((result: PostRequest) => {
expect(result.href).toBe(href);
done();
});
});
});
});

View File

@@ -4,6 +4,7 @@ import { PostRequest } from '../data/request.models';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { RequestService } from '../data/request.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { Observable, of as observableOf } from 'rxjs';
/**
* Client side version of the service to send authentication requests
@@ -20,15 +21,13 @@ export class BrowserAuthRequestService extends AuthRequestService {
}
/**
* Factory function to create the request object to send. This needs to be a POST client side and
* a GET server side. Due to CSRF validation, the server isn't allowed to send a POST, so we allow
* only the server IP to send a GET to this endpoint.
* Factory function to create the request object to send.
*
* @param href The href to send the request to
* @protected
*/
protected createShortLivedTokenRequest(href: string): PostRequest {
return new PostRequest(this.requestService.generateRequestId(), href);
protected createShortLivedTokenRequest(href: string): Observable<PostRequest> {
return observableOf(new PostRequest(this.requestService.generateRequestId(), href));
}
}

View File

@@ -1,34 +1,68 @@
import { AuthRequestService } from './auth-request.service';
import { RequestService } from '../data/request.service';
import { ServerAuthRequestService } from './server-auth-request.service';
import { HttpClient, HttpHeaders, HttpResponse } from '@angular/common/http';
import { Observable, of as observableOf } from 'rxjs';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { PostRequest } from '../data/request.models';
import {
XSRF_REQUEST_HEADER,
XSRF_RESPONSE_HEADER
} from '../xsrf/xsrf.interceptor';
describe(`ServerAuthRequestService`, () => {
let href: string;
let requestService: RequestService;
let service: AuthRequestService;
let httpClient: HttpClient;
let httpResponse: HttpResponse<any>;
let halService: HALEndpointService;
const mockToken = 'mock-token';
beforeEach(() => {
href = 'https://rest.api/auth/shortlivedtokens';
requestService = jasmine.createSpyObj('requestService', {
'generateRequestId': '8bb0582d-5013-4337-af9c-763beb25aae2'
});
service = new ServerAuthRequestService(null, requestService, null);
let headers = new HttpHeaders();
headers = headers.set(XSRF_RESPONSE_HEADER, mockToken);
httpResponse = {
body: { bar: false },
headers: headers,
statusText: '200'
} as HttpResponse<any>;
httpClient = jasmine.createSpyObj('httpClient', {
get: observableOf(httpResponse),
});
halService = jasmine.createSpyObj('halService', {
'getRootHref': '/api'
});
service = new ServerAuthRequestService(halService, requestService, null, httpClient);
});
describe(`createShortLivedTokenRequest`, () => {
it(`should return a GetRequest`, () => {
const result = (service as any).createShortLivedTokenRequest(href);
expect(result.constructor.name).toBe('GetRequest');
it(`should return a PostRequest`, (done) => {
const obs = (service as any).createShortLivedTokenRequest(href) as Observable<PostRequest>;
obs.subscribe((result: PostRequest) => {
expect(result.constructor.name).toBe('PostRequest');
done();
});
});
it(`should return a request with the given href`, () => {
const result = (service as any).createShortLivedTokenRequest(href);
expect(result.href).toBe(href) ;
it(`should return a request with the given href`, (done) => {
const obs = (service as any).createShortLivedTokenRequest(href) as Observable<PostRequest>;
obs.subscribe((result: PostRequest) => {
expect(result.href).toBe(href);
done();
});
});
it(`should have a responseMsToLive of 2 seconds`, () => {
const result = (service as any).createShortLivedTokenRequest(href);
expect(result.responseMsToLive).toBe(2 * 1000) ;
it(`should return a request with a xsrf header`, (done) => {
const obs = (service as any).createShortLivedTokenRequest(href) as Observable<PostRequest>;
obs.subscribe((result: PostRequest) => {
expect(result.options.headers.get(XSRF_REQUEST_HEADER)).toBe(mockToken);
done();
});
});
});
});

View File

@@ -1,9 +1,21 @@
import { Injectable } from '@angular/core';
import { AuthRequestService } from './auth-request.service';
import { GetRequest } from '../data/request.models';
import { PostRequest } from '../data/request.models';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { RequestService } from '../data/request.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import {
HttpHeaders,
HttpClient,
HttpResponse
} from '@angular/common/http';
import {
XSRF_REQUEST_HEADER,
XSRF_RESPONSE_HEADER,
DSPACE_XSRF_COOKIE
} from '../xsrf/xsrf.interceptor';
import { map } from 'rxjs/operators';
import { Observable } from 'rxjs';
/**
* Server side version of the service to send authentication requests
@@ -14,23 +26,42 @@ export class ServerAuthRequestService extends AuthRequestService {
constructor(
halService: HALEndpointService,
requestService: RequestService,
rdbService: RemoteDataBuildService
rdbService: RemoteDataBuildService,
protected httpClient: HttpClient,
) {
super(halService, requestService, rdbService);
}
/**
* Factory function to create the request object to send. This needs to be a POST client side and
* a GET server side. Due to CSRF validation, the server isn't allowed to send a POST, so we allow
* only the server IP to send a GET to this endpoint.
* Factory function to create the request object to send.
*
* @param href The href to send the request to
* @protected
*/
protected createShortLivedTokenRequest(href: string): GetRequest {
return Object.assign(new GetRequest(this.requestService.generateRequestId(), href), {
responseMsToLive: 2 * 1000 // A short lived token is only valid for 2 seconds.
});
protected createShortLivedTokenRequest(href: string): Observable<PostRequest> {
// First do a call to the root endpoint in order to get an XSRF token
return this.httpClient.get(this.halService.getRootHref(), { observe: 'response' }).pipe(
// retrieve the XSRF token from the response header
map((response: HttpResponse<any>) => response.headers.get(XSRF_RESPONSE_HEADER)),
// Use that token to create an HttpHeaders object
map((xsrfToken: string) => new HttpHeaders()
.set('Content-Type', 'application/json; charset=utf-8')
// set the token as the XSRF header
.set(XSRF_REQUEST_HEADER, xsrfToken)
// and as the DSPACE-XSRF-COOKIE
.set('Cookie', `${DSPACE_XSRF_COOKIE}=${xsrfToken}`)),
map((headers: HttpHeaders) =>
// Create a new PostRequest using those headers and the given href
new PostRequest(
this.requestService.generateRequestId(),
href,
{},
{
headers: headers,
},
)
)
);
}
}

View File

@@ -2,27 +2,66 @@ import { BrowseDefinitionDataService } from './browse-definition-data.service';
import { followLink } from '../../shared/utils/follow-link-config.model';
import { EMPTY } from 'rxjs';
import { FindListOptions } from '../data/find-list-options.model';
import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock';
import { RequestService } from '../data/request.service';
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
import { getMockObjectCacheService } from '../../shared/mocks/object-cache.service.mock';
describe(`BrowseDefinitionDataService`, () => {
let requestService: RequestService;
let service: BrowseDefinitionDataService;
const findAllDataSpy = jasmine.createSpyObj('findAllData', {
findAll: EMPTY,
});
let findAllDataSpy;
let searchDataSpy;
const browsesEndpointURL = 'https://rest.api/browses';
const halService: any = new HALEndpointServiceStub(browsesEndpointURL);
const options = new FindListOptions();
const linksToFollow = [
followLink('entries'),
followLink('items')
];
function initTestService() {
return new BrowseDefinitionDataService(
requestService,
getMockRemoteDataBuildService(),
getMockObjectCacheService(),
halService,
);
}
beforeEach(() => {
service = new BrowseDefinitionDataService(null, null, null, null);
service = initTestService();
findAllDataSpy = jasmine.createSpyObj('findAllData', {
findAll: EMPTY,
});
searchDataSpy = jasmine.createSpyObj('searchData', {
searchBy: EMPTY,
getSearchByHref: EMPTY,
});
(service as any).findAllData = findAllDataSpy;
(service as any).searchData = searchDataSpy;
});
describe('findByFields', () => {
it(`should call searchByHref on searchData`, () => {
service.findByFields(['test'], true, false, ...linksToFollow);
expect(searchDataSpy.getSearchByHref).toHaveBeenCalled();
});
});
describe('searchBy', () => {
it(`should call searchBy on searchData`, () => {
service.searchBy('test', options, true, false, ...linksToFollow);
expect(searchDataSpy.searchBy).toHaveBeenCalledWith('test', options, true, false, ...linksToFollow);
});
});
describe(`findAll`, () => {
it(`should call findAll on findAllData`, () => {
service.findAll(options, true, false, ...linksToFollow);
expect(findAllDataSpy.findAll).toHaveBeenCalledWith(options, true, false, ...linksToFollow);
});
});
});

View File

@@ -13,6 +13,8 @@ import { FindListOptions } from '../data/find-list-options.model';
import { IdentifiableDataService } from '../data/base/identifiable-data.service';
import { FindAllData, FindAllDataImpl } from '../data/base/find-all-data';
import { dataService } from '../data/base/data-service.decorator';
import { RequestParam } from '../cache/models/request-param.model';
import { SearchData, SearchDataImpl } from '../data/base/search-data';
/**
* Data service responsible for retrieving browse definitions from the REST server
@@ -21,8 +23,9 @@ import { dataService } from '../data/base/data-service.decorator';
providedIn: 'root',
})
@dataService(BROWSE_DEFINITION)
export class BrowseDefinitionDataService extends IdentifiableDataService<BrowseDefinition> implements FindAllData<BrowseDefinition> {
export class BrowseDefinitionDataService extends IdentifiableDataService<BrowseDefinition> implements FindAllData<BrowseDefinition>, SearchData<BrowseDefinition> {
private findAllData: FindAllDataImpl<BrowseDefinition>;
private searchData: SearchDataImpl<BrowseDefinition>;
constructor(
protected requestService: RequestService,
@@ -31,7 +34,7 @@ export class BrowseDefinitionDataService extends IdentifiableDataService<BrowseD
protected halService: HALEndpointService,
) {
super('browses', requestService, rdbService, objectCache, halService);
this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
}
@@ -52,5 +55,71 @@ export class BrowseDefinitionDataService extends IdentifiableDataService<BrowseD
findAll(options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<BrowseDefinition>[]): Observable<RemoteData<PaginatedList<BrowseDefinition>>> {
return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}
/**
* Make a new FindListRequest with given search method
*
* @param searchMethod The search method for the object
* @param options The [[FindListOptions]] object
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* no valid cached version. Defaults to true
* @param reRequestOnStale Whether or not the request should automatically be re-
* requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
* @return {Observable<RemoteData<PaginatedList<T>>}
* Return an observable that emits response from the server
*/
public searchBy(searchMethod: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<BrowseDefinition>[]): Observable<RemoteData<PaginatedList<BrowseDefinition>>> {
return this.searchData.searchBy(searchMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}
/**
* Create the HREF for a specific object's search method with given options object
*
* @param searchMethod The search method for the object
* @param options The [[FindListOptions]] object
* @return {Observable<string>}
* Return an observable that emits created HREF
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
*/
public getSearchByHref(searchMethod: string, options?: FindListOptions, ...linksToFollow: FollowLinkConfig<BrowseDefinition>[]): Observable<string> {
return this.searchData.getSearchByHref(searchMethod, options, ...linksToFollow);
}
/**
* Get the browse URL by providing a list of metadata keys. The first matching browse index definition
* for any of the fields is returned. This is used in eg. item page field component, which can be configured
* with several fields for a component like 'Author', and needs to know if and how to link the values
* to configured browse indices.
*
* @param fields an array of field strings, eg. ['dc.contributor.author', 'dc.creator']
* @param useCachedVersionIfAvailable Override the data service useCachedVersionIfAvailable parameter (default: true)
* @param reRequestOnStale Override the data service reRequestOnStale parameter (default: true)
* @param linksToFollow Override the data service linksToFollow parameter (default: empty array)
*/
findByFields(
fields: string[],
useCachedVersionIfAvailable = true,
reRequestOnStale = true,
...linksToFollow: FollowLinkConfig<BrowseDefinition>[]
): Observable<RemoteData<BrowseDefinition>> {
const searchParams = [];
searchParams.push(new RequestParam('fields', fields));
const hrefObs = this.getSearchByHref(
'byFields',
{ searchParams },
...linksToFollow
);
return this.findByHref(
hrefObs,
useCachedVersionIfAvailable,
reRequestOnStale,
...linksToFollow,
);
}
}

View File

@@ -19,9 +19,9 @@ import {
} from '../shared/operators';
import { URLCombiner } from '../url-combiner/url-combiner';
import { BrowseEntrySearchOptions } from './browse-entry-search-options.model';
import { BrowseDefinitionDataService } from './browse-definition-data.service';
import { HrefOnlyDataService } from '../data/href-only-data.service';
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { BrowseDefinitionDataService } from './browse-definition-data.service';
export const BROWSE_LINKS_TO_FOLLOW: FollowLinkConfig<BrowseEntry | Item>[] = [
@@ -35,7 +35,7 @@ export const BROWSE_LINKS_TO_FOLLOW: FollowLinkConfig<BrowseEntry | Item>[] = [
export class BrowseService {
protected linkPath = 'browses';
private static toSearchKeyArray(metadataKey: string): string[] {
public static toSearchKeyArray(metadataKey: string): string[] {
const keyParts = metadataKey.split('.');
const searchFor = [];
searchFor.push('*');

View File

@@ -7,7 +7,6 @@ import { ServerSyncBufferEffects } from './cache/server-sync-buffer.effects';
import { ObjectUpdatesEffects } from './data/object-updates/object-updates.effects';
import { RouteEffects } from './services/route.effects';
import { RouterEffects } from './router/router.effects';
import { MenuEffects } from '../shared/menu/menu.effects';
export const coreEffects = [
RequestEffects,
@@ -19,5 +18,4 @@ export const coreEffects = [
ObjectUpdatesEffects,
RouteEffects,
RouterEffects,
MenuEffects
];

View File

@@ -170,6 +170,7 @@ import { OrcidHistory } from './orcid/model/orcid-history.model';
import { OrcidAuthService } from './orcid/orcid-auth.service';
import { VocabularyDataService } from './submission/vocabularies/vocabulary.data.service';
import { VocabularyEntryDetailsDataService } from './submission/vocabularies/vocabulary-entry-details.data.service';
import { IdentifierData } from '../shared/object-list/identifier-data/identifier-data.model';
import { SupervisionOrderDataService } from './supervision-order/supervision-order-data.service';
/**
@@ -358,7 +359,8 @@ export const models =
ResearcherProfile,
OrcidQueue,
OrcidHistory,
AccessStatusObject
AccessStatusObject,
IdentifierData,
];
@NgModule({

View File

@@ -14,6 +14,7 @@ import { RemoteData } from './remote-data';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
import { HttpHeaders } from '@angular/common/http';
import { HttpParams } from '@angular/common/http';
@Injectable({
providedIn: 'root',
@@ -55,7 +56,7 @@ export class EpersonRegistrationService {
* @param email
* @param captchaToken the value of x-recaptcha-token header
*/
registerEmail(email: string, captchaToken: string = null): Observable<RemoteData<Registration>> {
registerEmail(email: string, captchaToken: string = null, type?: string): Observable<RemoteData<Registration>> {
const registration = new Registration();
registration.email = email;
@@ -70,6 +71,11 @@ export class EpersonRegistrationService {
}
options.headers = headers;
if (hasValue(type)) {
options.params = type ?
new HttpParams({ fromString: 'accountRequestType=' + type }) : new HttpParams();
}
href$.pipe(
find((href: string) => hasValue(href)),
map((href: string) => {

View File

@@ -32,4 +32,5 @@ export enum FeatureID {
CanSynchronizeWithORCID = 'canSynchronizeWithORCID',
CanSubmit = 'canSubmit',
CanEditItem = 'canEditItem',
CanRegisterDOI = 'canRegisterDOI',
}

View File

@@ -0,0 +1,85 @@
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { dataService } from './base/data-service.decorator';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { BaseDataService } from './base/base-data.service';
import { RequestService } from './request.service';
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
import { CoreState } from '../core-state.model';
import { Observable } from 'rxjs';
import { RemoteData } from './remote-data';
import { Item } from '../shared/item.model';
import { IDENTIFIERS } from '../../shared/object-list/identifier-data/identifier-data.resource-type';
import { IdentifierData } from '../../shared/object-list/identifier-data/identifier-data.model';
import { getFirstCompletedRemoteData } from '../shared/operators';
import { map, switchMap } from 'rxjs/operators';
import {ConfigurationProperty} from '../shared/configuration-property.model';
import {ConfigurationDataService} from './configuration-data.service';
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
import { PostRequest } from './request.models';
import { sendRequest } from '../shared/request.operators';
import { RestRequest } from './rest-request.model';
/**
* The service handling all REST requests to get item identifiers like handles and DOIs
* from the /identifiers endpoint, as well as the backend configuration that controls whether a 'Register DOI'
* button appears for admins in the item status page
*/
@Injectable()
@dataService(IDENTIFIERS)
export class IdentifierDataService extends BaseDataService<IdentifierData> {
constructor(
protected comparator: DefaultChangeAnalyzer<IdentifierData>,
protected halService: HALEndpointService,
protected http: HttpClient,
protected notificationsService: NotificationsService,
protected objectCache: ObjectCacheService,
protected rdbService: RemoteDataBuildService,
protected requestService: RequestService,
protected store: Store<CoreState>,
private configurationService: ConfigurationDataService,
) {
super('identifiers', requestService, rdbService, objectCache, halService);
}
/**
* Returns {@link RemoteData} of {@link IdentifierData} representing identifiers for this item
* @param item Item we are querying
*/
getIdentifierDataFor(item: Item): Observable<RemoteData<IdentifierData>> {
return this.findByHref(item._links.identifiers.href, false, true);
}
/**
* Should we allow registration of new DOIs via the item status page?
*/
public getIdentifierRegistrationConfiguration(): Observable<string[]> {
return this.configurationService.findByPropertyName('identifiers.item-status.register-doi').pipe(
getFirstCompletedRemoteData(),
map((propertyRD: RemoteData<ConfigurationProperty>) => propertyRD.hasSucceeded ? propertyRD.payload.values : [])
);
}
public registerIdentifier(item: Item, type: string): Observable<RemoteData<any>> {
const requestId = this.requestService.generateRequestId();
return this.getEndpoint().pipe(
map((endpointURL: string) => {
const options: HttpOptions = Object.create({});
let headers = new HttpHeaders();
headers = headers.append('Content-Type', 'text/uri-list');
options.headers = headers;
let params = new HttpParams();
params = params.append('type', type);
options.params = params;
return new PostRequest(requestId, endpointURL, item._links.self.href, options);
}),
sendRequest(this.requestService),
switchMap((request: RestRequest) => this.rdbService.buildFromRequestUUID(request.uuid) as Observable<RemoteData<any>>)
);
}
}

View File

@@ -232,6 +232,16 @@ export abstract class BaseItemDataService extends IdentifiableDataService<Item>
return this.rdbService.buildFromRequestUUID(requestId);
}
/**
* Get the endpoint for an item's identifiers
* @param itemId
*/
public getIdentifiersEndpoint(itemId: string): Observable<string> {
return this.halService.getEndpoint(this.linkPath).pipe(
switchMap((url: string) => this.halService.getEndpoint('identifiers', `${url}/${itemId}`))
);
}
/**
* Get the endpoint to move the item
* @param itemId

View File

@@ -0,0 +1,13 @@
import { SystemWideAlertDataService } from './system-wide-alert-data.service';
import { testFindAllDataImplementation } from './base/find-all-data.spec';
import { testPutDataImplementation } from './base/put-data.spec';
import { testCreateDataImplementation } from './base/create-data.spec';
describe('SystemWideAlertDataService', () => {
describe('composition', () => {
const initService = () => new SystemWideAlertDataService(null, null, null, null, null);
testFindAllDataImplementation(initService);
testPutDataImplementation(initService);
testCreateDataImplementation(initService);
});
});

View File

@@ -0,0 +1,104 @@
import { Injectable } from '@angular/core';
import { RequestService } from './request.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { Observable } from 'rxjs';
import { PaginatedList } from './paginated-list.model';
import { RemoteData } from './remote-data';
import { IdentifiableDataService } from './base/identifiable-data.service';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { FindAllData, FindAllDataImpl } from './base/find-all-data';
import { FindListOptions } from './find-list-options.model';
import { dataService } from './base/data-service.decorator';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { CreateData, CreateDataImpl } from './base/create-data';
import { SYSTEMWIDEALERT } from '../../system-wide-alert/system-wide-alert.resource-type';
import { SystemWideAlert } from '../../system-wide-alert/system-wide-alert.model';
import { PutData, PutDataImpl } from './base/put-data';
import { RequestParam } from '../cache/models/request-param.model';
import { SearchData, SearchDataImpl } from './base/search-data';
/**
* Dataservice representing a system-wide alert
*/
@Injectable()
@dataService(SYSTEMWIDEALERT)
export class SystemWideAlertDataService extends IdentifiableDataService<SystemWideAlert> implements FindAllData<SystemWideAlert>, CreateData<SystemWideAlert>, PutData<SystemWideAlert>, SearchData<SystemWideAlert> {
private findAllData: FindAllDataImpl<SystemWideAlert>;
private createData: CreateDataImpl<SystemWideAlert>;
private putData: PutDataImpl<SystemWideAlert>;
private searchData: SearchData<SystemWideAlert>;
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
) {
super('systemwidealerts', requestService, rdbService, objectCache, halService);
this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
this.createData = new CreateDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive);
this.putData = new PutDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
}
/**
* Returns {@link RemoteData} of all object with a list of {@link FollowLinkConfig}, to indicate which embedded
* info should be added to the objects
*
* @param options Find list options object
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* no valid cached version. Defaults to true
* @param reRequestOnStale Whether or not the request should automatically be re-
* requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
* @return {Observable<RemoteData<PaginatedList<T>>>}
* Return an observable that emits object list
*/
findAll(options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<SystemWideAlert>[]): Observable<RemoteData<PaginatedList<SystemWideAlert>>> {
return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}
/**
* Create a new object on the server, and store the response in the object cache
*
* @param object The object to create
* @param params Array with additional params to combine with query string
*/
create(object: SystemWideAlert, ...params: RequestParam[]): Observable<RemoteData<SystemWideAlert>> {
return this.createData.create(object, ...params);
}
/**
* Send a PUT request for the specified object
*
* @param object The object to send a put request for.
*/
put(object: SystemWideAlert): Observable<RemoteData<SystemWideAlert>> {
return this.putData.put(object);
}
/**
* Make a new FindListRequest with given search method
*
* @param searchMethod The search method for the object
* @param options The [[FindListOptions]] object
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* no valid cached version. Defaults to true
* @param reRequestOnStale Whether or not the request should automatically be re-
* requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
* @return {Observable<RemoteData<PaginatedList<T>>}
* Return an observable that emits response from the server
*/
searchBy(searchMethod: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<SystemWideAlert>[]): Observable<RemoteData<PaginatedList<SystemWideAlert>>> {
return this.searchData.searchBy(searchMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}
}

View File

@@ -3,8 +3,8 @@ import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { Operation, ReplaceOperation } from 'fast-json-patch';
import { Observable } from 'rxjs';
import { find, map } from 'rxjs/operators';
import { Observable, of as observableOf } from 'rxjs';
import { find, map, mergeMap } from 'rxjs/operators';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { ObjectCacheService } from '../cache/object-cache.service';
@@ -130,6 +130,24 @@ export class ResearcherProfileDataService extends IdentifiableDataService<Resear
return this.rdbService.buildFromRequestUUID(requestId, followLink('item'));
}
/**
* Creates a researcher profile starting from an external source URI and returns the related item's ID
* Emits null if the researcher profile doesn't exist after sending out the request
* @param sourceUri
*/
createFromExternalSourceAndReturnRelatedItemId(sourceUri: string): Observable<string> {
return this.createFromExternalSource(sourceUri).pipe(
getFirstCompletedRemoteData(),
mergeMap((rd: RemoteData<ResearcherProfile>) => {
if (rd.hasSucceeded) {
return this.findRelatedItemId(rd.payload);
} else {
return observableOf(null);
}
}),
);
}
/**
* Create a new object on the server, and store the response in the object cache

View File

@@ -0,0 +1,16 @@
import { XhrFactory } from '@angular/common';
import { Injectable } from '@angular/core';
import { prototype, XMLHttpRequest } from 'xhr2';
/**
* Overrides the default XhrFactory server side, to allow us to set cookies in requests to the
* backend. This was added to be able to perform a working XSRF request from the node server, as it
* needs to set a cookie for the XSRF token
*/
@Injectable()
export class ServerXhrService implements XhrFactory {
build(): XMLHttpRequest {
prototype._restrictedHeaders.cookie = false;
return new XMLHttpRequest();
}
}

View File

@@ -24,6 +24,8 @@ import { Bitstream } from './bitstream.model';
import { ACCESS_STATUS } from 'src/app/shared/object-list/access-status-badge/access-status.resource-type';
import { AccessStatusObject } from 'src/app/shared/object-list/access-status-badge/access-status.model';
import { HandleObject } from './handle-object.model';
import { IDENTIFIERS } from '../../shared/object-list/identifier-data/identifier-data.resource-type';
import { IdentifierData } from '../../shared/object-list/identifier-data/identifier-data.model';
/**
* Class representing a DSpace Item
@@ -76,6 +78,7 @@ export class Item extends DSpaceObject implements ChildHALResource, HandleObject
version: HALLink;
thumbnail: HALLink;
accessStatus: HALLink;
identifiers: HALLink;
self: HALLink;
};
@@ -121,6 +124,13 @@ export class Item extends DSpaceObject implements ChildHALResource, HandleObject
@link(ACCESS_STATUS)
accessStatus?: Observable<RemoteData<AccessStatusObject>>;
/**
* The identifier data for this Item
* Will be undefined unless the identifiers {@link HALLink} has been resolved.
*/
@link(IDENTIFIERS, false, 'identifiers')
identifiers?: Observable<RemoteData<IdentifierData>>;
/**
* Method that returns as which type of object this object should be rendered
*/

View File

@@ -1,11 +1,14 @@
/**
* An Enum defining the representation type of metadata
*/
import { BrowseDefinition } from '../browse-definition.model';
export enum MetadataRepresentationType {
None = 'none',
Item = 'item',
AuthorityControlled = 'authority_controlled',
PlainText = 'plain_text'
PlainText = 'plain_text',
BrowseLink = 'browse_link'
}
/**
@@ -24,8 +27,14 @@ export interface MetadataRepresentation {
*/
representationType: MetadataRepresentationType;
/**
* The browse definition (optional)
*/
browseDefinition?: BrowseDefinition;
/**
* Fetches the value to be displayed
*/
getValue(): string;
}

View File

@@ -1,6 +1,7 @@
import { MetadataRepresentation, MetadataRepresentationType } from '../metadata-representation.model';
import { hasValue } from '../../../../shared/empty.util';
import { MetadataValue } from '../../metadata.models';
import { BrowseDefinition } from '../../browse-definition.model';
/**
* This class defines the way the metadatum it extends should be represented
@@ -12,9 +13,15 @@ export class MetadatumRepresentation extends MetadataValue implements MetadataRe
*/
itemType: string;
constructor(itemType: string) {
/**
* The browse definition ID passed in with the metadatum, if any
*/
browseDefinition?: BrowseDefinition;
constructor(itemType: string, browseDefinition?: BrowseDefinition) {
super();
this.itemType = itemType;
this.browseDefinition = browseDefinition;
}
/**
@@ -23,6 +30,8 @@ export class MetadatumRepresentation extends MetadataValue implements MetadataRe
get representationType(): MetadataRepresentationType {
if (hasValue(this.authority)) {
return MetadataRepresentationType.AuthorityControlled;
} else if (hasValue(this.browseDefinition)) {
return MetadataRepresentationType.BrowseLink;
} else {
return MetadataRepresentationType.PlainText;
}

View File

@@ -0,0 +1,9 @@
/*
* Object model for the data returned by the REST API to present minted identifiers in a submission section
*/
import { Identifier } from '../../../shared/object-list/identifier-data/identifier.model';
export interface WorkspaceitemSectionIdentifiersObject {
identifiers?: Identifier[]
displayTypes?: string[]
}

View File

@@ -3,6 +3,7 @@ import { WorkspaceitemSectionFormObject } from './workspaceitem-section-form.mod
import { WorkspaceitemSectionLicenseObject } from './workspaceitem-section-license.model';
import { WorkspaceitemSectionUploadObject } from './workspaceitem-section-upload.model';
import { WorkspaceitemSectionCcLicenseObject } from './workspaceitem-section-cc-license.model';
import {WorkspaceitemSectionIdentifiersObject} from './workspaceitem-section-identifiers.model';
import { WorkspaceitemSectionSherpaPoliciesObject } from './workspaceitem-section-sherpa-policies.model';
/**
@@ -23,4 +24,5 @@ export type WorkspaceitemSectionDataType
| WorkspaceitemSectionCcLicenseObject
| WorkspaceitemSectionAccessesObject
| WorkspaceitemSectionSherpaPoliciesObject
| WorkspaceitemSectionIdentifiersObject
| string;

View File

@@ -19,6 +19,8 @@ export const XSRF_REQUEST_HEADER = 'X-XSRF-TOKEN';
export const XSRF_RESPONSE_HEADER = 'DSPACE-XSRF-TOKEN';
// Name of cookie where we store the XSRF token
export const XSRF_COOKIE = 'XSRF-TOKEN';
// Name of cookie the backend expects the XSRF token to be in
export const DSPACE_XSRF_COOKIE = 'DSPACE-XSRF-COOKIE';
/**
* Custom Http Interceptor intercepting Http Requests & Responses to

View File

@@ -2,12 +2,7 @@
<div class="d-flex flex-row">
<ds-item-page-title-field [item]="object" class="mr-auto">
</ds-item-page-title-field>
<div class="pl-2 space-children-mr">
<ds-dso-page-version-button (newVersionEvent)="onCreateNewVersion()" [dso]="object"
[tooltipMsgCreate]="'item.page.version.create'"
[tooltipMsgHasDraft]="'item.page.version.hasDraft'"></ds-dso-page-version-button>
<ds-dso-page-edit-button [pageRoute]="itemPageRoute" [dso]="object" [tooltipMsg]="'journalissue.page.edit'"></ds-dso-page-edit-button>
</div>
<ds-dso-edit-menu></ds-dso-edit-menu>
</div>
<div class="row">
<div class="col-xs-12 col-md-4">

View File

@@ -1,7 +1,7 @@
import { Component } from '@angular/core';
import { ViewMode } from '../../../../core/shared/view-mode.model';
import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
import { VersionedItemComponent } from '../../../../item-page/simple/item-types/versioned-item/versioned-item.component';
import { ItemComponent } from '../../../../item-page/simple/item-types/shared/item.component';
@listableObjectComponent('JournalIssue', ViewMode.StandalonePage)
@Component({
@@ -12,5 +12,5 @@ import { VersionedItemComponent } from '../../../../item-page/simple/item-types/
/**
* The component for displaying metadata and relations of an item of the type Journal Issue
*/
export class JournalIssueComponent extends VersionedItemComponent {
export class JournalIssueComponent extends ItemComponent {
}

View File

@@ -2,12 +2,7 @@
<div class="d-flex flex-row">
<ds-item-page-title-field [item]="object" class="mr-auto">
</ds-item-page-title-field>
<div class="pl-2 space-children-mr">
<ds-dso-page-version-button (newVersionEvent)="onCreateNewVersion()" [dso]="object"
[tooltipMsgCreate]="'item.page.version.create'"
[tooltipMsgHasDraft]="'item.page.version.hasDraft'"></ds-dso-page-version-button>
<ds-dso-page-edit-button [pageRoute]="itemPageRoute" [dso]="object" [tooltipMsg]="'journalvolume.page.edit'"></ds-dso-page-edit-button>
</div>
<ds-dso-edit-menu></ds-dso-edit-menu>
</div>
<div class="row">
<div class="col-xs-12 col-md-4">

View File

@@ -1,7 +1,7 @@
import { Component } from '@angular/core';
import { ViewMode } from '../../../../core/shared/view-mode.model';
import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
import { VersionedItemComponent } from '../../../../item-page/simple/item-types/versioned-item/versioned-item.component';
import { ItemComponent } from '../../../../item-page/simple/item-types/shared/item.component';
@listableObjectComponent('JournalVolume', ViewMode.StandalonePage)
@Component({
@@ -12,5 +12,5 @@ import { VersionedItemComponent } from '../../../../item-page/simple/item-types/
/**
* The component for displaying metadata and relations of an item of the type Journal Volume
*/
export class JournalVolumeComponent extends VersionedItemComponent {
export class JournalVolumeComponent extends ItemComponent {
}

View File

@@ -2,12 +2,7 @@
<div class="d-flex flex-row">
<ds-item-page-title-field [item]="object" class="mr-auto">
</ds-item-page-title-field>
<div class="pl-2 space-children-mr">
<ds-dso-page-version-button (newVersionEvent)="onCreateNewVersion()" [dso]="object"
[tooltipMsgCreate]="'item.page.version.create'"
[tooltipMsgHasDraft]="'item.page.version.hasDraft'"></ds-dso-page-version-button>
<ds-dso-page-edit-button [pageRoute]="itemPageRoute" [dso]="object" [tooltipMsg]="'journal.page.edit'"></ds-dso-page-edit-button>
</div>
<ds-dso-edit-menu></ds-dso-edit-menu>
</div>
<div class="row">
<div class="col-xs-12 col-md-4">

View File

@@ -35,6 +35,10 @@ import { VersionDataService } from '../../../../core/data/version-data.service';
import { WorkspaceitemDataService } from '../../../../core/submission/workspaceitem-data.service';
import { SearchService } from '../../../../core/shared/search/search.service';
import { mockRouteService } from '../../../../item-page/simple/item-types/shared/item.component.spec';
import {
BrowseDefinitionDataServiceStub
} from '../../../../shared/testing/browse-definition-data-service.stub';
import { BrowseDefinitionDataService } from '../../../../core/browse/browse-definition-data.service';
let comp: JournalComponent;
let fixture: ComponentFixture<JournalComponent>;
@@ -100,7 +104,8 @@ describe('JournalComponent', () => {
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
{ provide: WorkspaceitemDataService, useValue: {} },
{ provide: SearchService, useValue: {} },
{ provide: RouteService, useValue: mockRouteService }
{ provide: RouteService, useValue: mockRouteService },
{ provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub }
],
schemas: [NO_ERRORS_SCHEMA]

View File

@@ -1,7 +1,7 @@
import { Component } from '@angular/core';
import { ViewMode } from '../../../../core/shared/view-mode.model';
import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
import { VersionedItemComponent } from '../../../../item-page/simple/item-types/versioned-item/versioned-item.component';
import { ItemComponent } from '../../../../item-page/simple/item-types/shared/item.component';
@listableObjectComponent('Journal', ViewMode.StandalonePage)
@Component({
@@ -12,5 +12,5 @@ import { VersionedItemComponent } from '../../../../item-page/simple/item-types/
/**
* The component for displaying metadata and relations of an item of the type Journal
*/
export class JournalComponent extends VersionedItemComponent {
export class JournalComponent extends ItemComponent {
}

View File

@@ -21,6 +21,7 @@ import { JournalIssueSidebarSearchListElementComponent } from './item-list-eleme
import { JournalSidebarSearchListElementComponent } from './item-list-elements/sidebar-search-list-elements/journal/journal-sidebar-search-list-element.component';
import { ItemSharedModule } from '../../item-page/item-shared.module';
import { ResultsBackButtonModule } from '../../shared/results-back-button/results-back-button.module';
import { DsoPageModule } from '../../shared/dso-page/dso-page.module';
const ENTRY_COMPONENTS = [
// put only entry components that use custom decorator
@@ -49,7 +50,8 @@ const ENTRY_COMPONENTS = [
CommonModule,
ItemSharedModule,
SharedModule,
ResultsBackButtonModule
ResultsBackButtonModule,
DsoPageModule
],
declarations: [
...ENTRY_COMPONENTS

View File

@@ -2,12 +2,7 @@
<div class="d-flex flex-row">
<ds-item-page-title-field [item]="object" class="mr-auto">
</ds-item-page-title-field>
<div class="pl-2 space-children-mr">
<ds-dso-page-version-button (newVersionEvent)="onCreateNewVersion()" [dso]="object"
[tooltipMsgCreate]="'item.page.version.create'"
[tooltipMsgHasDraft]="'item.page.version.hasDraft'"></ds-dso-page-version-button>
<ds-dso-page-edit-button [pageRoute]="itemPageRoute" [dso]="object" [tooltipMsg]="'orgunit.page.edit'"></ds-dso-page-edit-button>
</div>
<ds-dso-edit-menu></ds-dso-edit-menu>
</div>
<div class="row">
<div class="col-xs-12 col-md-4">

View File

@@ -1,7 +1,7 @@
import { Component } from '@angular/core';
import { ViewMode } from '../../../../core/shared/view-mode.model';
import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
import { VersionedItemComponent } from '../../../../item-page/simple/item-types/versioned-item/versioned-item.component';
import { ItemComponent } from '../../../../item-page/simple/item-types/shared/item.component';
@listableObjectComponent('OrgUnit', ViewMode.StandalonePage)
@Component({
@@ -12,5 +12,5 @@ import { VersionedItemComponent } from '../../../../item-page/simple/item-types/
/**
* The component for displaying metadata and relations of an item of the type Organisation Unit
*/
export class OrgUnitComponent extends VersionedItemComponent {
export class OrgUnitComponent extends ItemComponent {
}

View File

@@ -2,14 +2,7 @@
<div class="d-flex flex-row">
<ds-item-page-title-field class="mr-auto" [item]="object">
</ds-item-page-title-field>
<div class="pl-2 space-children-mr">
<ds-dso-page-orcid-button [pageRoute]="itemPageRoute" [dso]="object" class="mr-2"></ds-dso-page-orcid-button>
<ds-dso-page-version-button (newVersionEvent)="onCreateNewVersion()" [dso]="object"
[tooltipMsgCreate]="'item.page.version.create'"
[tooltipMsgHasDraft]="'item.page.version.hasDraft'"></ds-dso-page-version-button>
<ds-dso-page-edit-button [pageRoute]="itemPageRoute" [dso]="object" [tooltipMsg]="'person.page.edit'"></ds-dso-page-edit-button>
<ds-person-page-claim-button [object]="object"></ds-person-page-claim-button>
</div>
<ds-dso-edit-menu></ds-dso-edit-menu>
</div>
<div class="row">
<div class="col-xs-12 col-md-4">

View File

@@ -1,7 +1,7 @@
import { Component } from '@angular/core';
import { ViewMode } from '../../../../core/shared/view-mode.model';
import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
import { VersionedItemComponent } from '../../../../item-page/simple/item-types/versioned-item/versioned-item.component';
import { ItemComponent } from '../../../../item-page/simple/item-types/shared/item.component';
@listableObjectComponent('Person', ViewMode.StandalonePage)
@Component({
@@ -12,5 +12,5 @@ import { VersionedItemComponent } from '../../../../item-page/simple/item-types/
/**
* The component for displaying metadata and relations of an item of the type Person
*/
export class PersonComponent extends VersionedItemComponent {
export class PersonComponent extends ItemComponent {
}

View File

@@ -2,12 +2,7 @@
<div class="d-flex flex-row">
<ds-item-page-title-field [item]="object" class="mr-auto">
</ds-item-page-title-field>
<div class="pl-2 space-children-mr">
<ds-dso-page-version-button (newVersionEvent)="onCreateNewVersion()" [dso]="object"
[tooltipMsgCreate]="'item.page.version.create'"
[tooltipMsgHasDraft]="'item.page.version.hasDraft'"></ds-dso-page-version-button>
<ds-dso-page-edit-button [pageRoute]="itemPageRoute" [dso]="object" [tooltipMsg]="'project.page.edit'"></ds-dso-page-edit-button>
</div>
<ds-dso-edit-menu></ds-dso-edit-menu>
</div>
<div class="row">
<div class="col-xs-12 col-md-4">

View File

@@ -1,7 +1,7 @@
import { Component } from '@angular/core';
import { ViewMode } from '../../../../core/shared/view-mode.model';
import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
import { VersionedItemComponent } from '../../../../item-page/simple/item-types/versioned-item/versioned-item.component';
import { ItemComponent } from '../../../../item-page/simple/item-types/shared/item.component';
@listableObjectComponent('Project', ViewMode.StandalonePage)
@Component({
@@ -12,5 +12,5 @@ import { VersionedItemComponent } from '../../../../item-page/simple/item-types/
/**
* The component for displaying metadata and relations of an item of the type Project
*/
export class ProjectComponent extends VersionedItemComponent {
export class ProjectComponent extends ItemComponent {
}

View File

@@ -30,6 +30,7 @@ import { PersonSidebarSearchListElementComponent } from './item-list-elements/si
import { ProjectSidebarSearchListElementComponent } from './item-list-elements/sidebar-search-list-elements/project/project-sidebar-search-list-element.component';
import { ItemSharedModule } from '../../item-page/item-shared.module';
import { ResultsBackButtonModule } from '../../shared/results-back-button/results-back-button.module';
import { DsoPageModule } from '../../shared/dso-page/dso-page.module';
const ENTRY_COMPONENTS = [
// put only entry components that use custom decorator
@@ -71,7 +72,8 @@ const COMPONENTS = [
ItemSharedModule,
SharedModule,
NgbTooltipModule,
ResultsBackButtonModule
ResultsBackButtonModule,
DsoPageModule,
],
declarations: [
...COMPONENTS,

View File

@@ -1,3 +1,3 @@
<ds-register-email-form
[MESSAGE_PREFIX]="'forgot-email.form'">
</ds-register-email-form>
[MESSAGE_PREFIX]="'forgot-email.form'" [typeRequest]="typeRequest">
</ds-register-email-form>

View File

@@ -1,4 +1,5 @@
import { Component } from '@angular/core';
import { TYPE_REQUEST_FORGOT } from '../../register-email-form/register-email-form.component';
@Component({
selector: 'ds-forgot-email',
@@ -9,5 +10,5 @@ import { Component } from '@angular/core';
* Component responsible the forgot password email step
*/
export class ForgotEmailComponent {
typeRequest = TYPE_REQUEST_FORGOT;
}

View File

@@ -1,6 +1,6 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { ContextHelpService } from '../../shared/context-help.service';
import { Observable, Subscription } from 'rxjs';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
/**
@@ -12,22 +12,15 @@ import { map } from 'rxjs/operators';
templateUrl: './context-help-toggle.component.html',
styleUrls: ['./context-help-toggle.component.scss']
})
export class ContextHelpToggleComponent implements OnInit, OnDestroy {
export class ContextHelpToggleComponent implements OnInit {
buttonVisible$: Observable<boolean>;
constructor(
private contextHelpService: ContextHelpService,
) { }
private subs: Subscription[];
ngOnInit(): void {
this.buttonVisible$ = this.contextHelpService.tooltipCount$().pipe(map(x => x > 0));
this.subs = [this.buttonVisible$.subscribe()];
}
ngOnDestroy() {
this.subs.forEach(sub => sub.unsubscribe());
}
onClick() {

View File

@@ -17,6 +17,7 @@ import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-con
import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface';
import { isPlatformBrowser } from '@angular/common';
import { setPlaceHolderAttributes } from '../../shared/utils/object-list-utils';
import { DSpaceObjectType } from '../../core/shared/dspace-object-type.model';
@Component({
selector: 'ds-recent-item-list',
@@ -67,6 +68,7 @@ export class RecentItemListComponent implements OnInit {
this.itemRD$ = this.searchService.search(
new PaginatedSearchOptions({
pagination: this.paginationConfig,
dsoTypes: [DSpaceObjectType.ITEM],
sort: this.sortConfig,
}),
undefined,

View File

@@ -22,7 +22,6 @@ import { provideMockStore } from '@ngrx/store/testing';
import { AppComponent } from './app.component';
import { RouteService } from './core/services/route.service';
import { getMockLocaleService } from './app.component.spec';
import { MenuServiceStub } from './shared/testing/menu-service.stub';
import { CorrelationIdService } from './correlation-id/correlation-id.service';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { TranslateLoaderMock } from './shared/mocks/translate-loader.mock';
@@ -124,6 +123,7 @@ describe('InitService', () => {
let transferStateSpy;
let metadataServiceSpy;
let breadcrumbsServiceSpy;
let menuServiceSpy;
const BLOCKING = {
t: { core: { auth: { blocking: true } } },
@@ -150,6 +150,9 @@ describe('InitService', () => {
metadataServiceSpy = jasmine.createSpyObj('metadataService', [
'listenForRouteChange',
]);
menuServiceSpy = jasmine.createSpyObj('menuServiceSpy', [
'listenForRouteChanges',
]);
TestBed.resetTestingModule();
@@ -175,7 +178,7 @@ describe('InitService', () => {
{ provide: AuthService, useValue: new AuthServiceMock() },
{ provide: Router, useValue: new RouterMock() },
{ provide: ActivatedRoute, useValue: new MockActivatedRoute() },
{ provide: MenuService, useValue: new MenuServiceStub() },
{ provide: MenuService, useValue: menuServiceSpy },
{ provide: ThemeService, useValue: getMockThemeService() },
provideMockStore({ initialState }),
AppComponent,
@@ -190,6 +193,7 @@ describe('InitService', () => {
service.initRouteListeners();
expect(metadataServiceSpy.listenForRouteChange).toHaveBeenCalledTimes(1);
expect(breadcrumbsServiceSpy.listenForRouteChanges).toHaveBeenCalledTimes(1);
expect(breadcrumbsServiceSpy.listenForRouteChanges).toHaveBeenCalledTimes(1);
}));
});

View File

@@ -23,6 +23,7 @@ import { ThemeService } from './shared/theme-support/theme.service';
import { isAuthenticationBlocking } from './core/auth/selectors';
import { distinctUntilChanged, find } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { MenuService } from './shared/menu/menu.service';
/**
* Performs the initialization of the app.
@@ -51,6 +52,8 @@ export abstract class InitService {
protected metadata: MetadataService,
protected breadcrumbsService: BreadcrumbsService,
protected themeService: ThemeService,
protected menuService: MenuService,
) {
}
@@ -184,6 +187,7 @@ export abstract class InitService {
this.metadata.listenForRouteChange();
this.breadcrumbsService.listenForRouteChanges();
this.themeService.listenForRouteChanges();
this.menuService.listenForRouteChanges();
}
/**

View File

@@ -3,8 +3,8 @@
<div class="col-12">
<h2 class="border-bottom">{{'item.edit.head' | translate}}</h2>
<div class="pt-2">
<ul class="nav nav-tabs justify-content-start">
<li *ngFor="let page of pages" class="nav-item">
<ul class="nav nav-tabs justify-content-start" role="tablist">
<li *ngFor="let page of pages" class="nav-item" [attr.aria-selected]="page.page === currentPage" role="tab">
<a *ngIf="(page.enabled | async)"
class="nav-link"
[ngClass]="{'active' : page.page === currentPage}"

View File

@@ -34,6 +34,9 @@ import { ItemAuthorizationsComponent } from './item-authorizations/item-authoriz
import { ObjectValuesPipe } from '../../shared/utils/object-values-pipe';
import { ResourcePoliciesModule } from '../../shared/resource-policies/resource-policies.module';
import { ItemVersionsModule } from '../versions/item-versions.module';
import { IdentifierDataService } from '../../core/data/identifier-data.service';
import { IdentifierDataComponent } from '../../shared/object-list/identifier-data/identifier-data.component';
import { ItemRegisterDoiComponent } from './item-register-doi/item-register-doi.component';
import { DsoSharedModule } from '../../dso-shared/dso-shared.module';
@@ -76,10 +79,13 @@ import { DsoSharedModule } from '../../dso-shared/dso-shared.module';
ItemMoveComponent,
ItemEditBitstreamDragHandleComponent,
VirtualMetadataComponent,
ItemAuthorizationsComponent
ItemAuthorizationsComponent,
IdentifierDataComponent,
ItemRegisterDoiComponent
],
providers: [
BundleDataService,
IdentifierDataService,
ObjectValuesPipe
],
})

View File

@@ -5,3 +5,4 @@ export const ITEM_EDIT_PUBLIC_PATH = 'public';
export const ITEM_EDIT_DELETE_PATH = 'delete';
export const ITEM_EDIT_MOVE_PATH = 'move';
export const ITEM_EDIT_AUTHORIZATIONS_PATH = 'authorizations';
export const ITEM_EDIT_REGISTER_DOI_PATH = 'register-doi';

View File

@@ -10,6 +10,7 @@ import { ItemStatusComponent } from './item-status/item-status.component';
import { ItemBitstreamsComponent } from './item-bitstreams/item-bitstreams.component';
import { ItemCollectionMapperComponent } from './item-collection-mapper/item-collection-mapper.component';
import { ItemMoveComponent } from './item-move/item-move.component';
import { ItemRegisterDoiComponent } from './item-register-doi/item-register-doi.component';
import { ItemRelationshipsComponent } from './item-relationships/item-relationships.component';
import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
import { ItemVersionHistoryComponent } from './item-version-history/item-version-history.component';
@@ -26,7 +27,8 @@ import {
ITEM_EDIT_PRIVATE_PATH,
ITEM_EDIT_PUBLIC_PATH,
ITEM_EDIT_REINSTATE_PATH,
ITEM_EDIT_WITHDRAW_PATH
ITEM_EDIT_WITHDRAW_PATH,
ITEM_EDIT_REGISTER_DOI_PATH
} from './edit-item-page.routing-paths';
import { ItemPageReinstateGuard } from './item-page-reinstate.guard';
import { ItemPageWithdrawGuard } from './item-page-withdraw.guard';
@@ -38,6 +40,7 @@ import { ItemPageRelationshipsGuard } from './item-page-relationships.guard';
import { ItemPageVersionHistoryGuard } from './item-page-version-history.guard';
import { ItemPageCollectionMapperGuard } from './item-page-collection-mapper.guard';
import { ThemedDsoEditMetadataComponent } from '../../dso-shared/dso-edit-metadata/themed-dso-edit-metadata.component';
import { ItemPageRegisterDoiGuard } from './item-page-register-doi.guard';
/**
* Routing module that handles the routing for the Edit Item page administrator functionality
@@ -142,6 +145,12 @@ import { ThemedDsoEditMetadataComponent } from '../../dso-shared/dso-edit-metada
component: ItemMoveComponent,
data: { title: 'item.edit.move.title' },
},
{
path: ITEM_EDIT_REGISTER_DOI_PATH,
component: ItemRegisterDoiComponent,
canActivate: [ItemPageRegisterDoiGuard],
data: { title: 'item.edit.register-doi.title' },
},
{
path: ITEM_EDIT_AUTHORIZATIONS_PATH,
children: [
@@ -186,6 +195,7 @@ import { ThemedDsoEditMetadataComponent } from '../../dso-shared/dso-edit-metada
ItemPageRelationshipsGuard,
ItemPageVersionHistoryGuard,
ItemPageCollectionMapperGuard,
ItemPageRegisterDoiGuard,
]
})
export class EditItemPageRoutingModule {

View File

@@ -0,0 +1,31 @@
import { Injectable } from '@angular/core';
import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard';
import { Item } from '../../core/shared/item.model';
import { ItemPageResolver } from '../item-page.resolver';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
import { Observable, of as observableOf } from 'rxjs';
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
import { AuthService } from '../../core/auth/auth.service';
@Injectable({
providedIn: 'root'
})
/**
* Guard for preventing unauthorized access to certain {@link Item} pages requiring DOI registration rights
*/
export class ItemPageRegisterDoiGuard extends DsoPageSingleFeatureGuard<Item> {
constructor(protected resolver: ItemPageResolver,
protected authorizationService: AuthorizationDataService,
protected router: Router,
protected authService: AuthService) {
super(resolver, authorizationService, router, authService);
}
/**
* Check DOI registration authorization rights
*/
getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
return observableOf(FeatureID.CanRegisterDOI);
}
}

View File

@@ -27,6 +27,6 @@ export class ItemPageStatusGuard extends DsoPageSomeFeatureGuard<Item> {
* Check authorization rights
*/
getFeatureIDs(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID[]> {
return observableOf([FeatureID.CanManageMappings, FeatureID.WithdrawItem, FeatureID.ReinstateItem, FeatureID.CanManagePolicies, FeatureID.CanMakePrivate, FeatureID.CanDelete, FeatureID.CanMove]);
return observableOf([FeatureID.CanManageMappings, FeatureID.WithdrawItem, FeatureID.ReinstateItem, FeatureID.CanManagePolicies, FeatureID.CanMakePrivate, FeatureID.CanDelete, FeatureID.CanMove, FeatureID.CanRegisterDOI]);
}
}

View File

@@ -0,0 +1,24 @@
<div class="container">
<div class="row">
<div class="col-12">
<h2>{{headerMessage | translate: {id: item.handle} }}</h2>
<p>{{descriptionMessage | translate}}</p>
<div *ngFor="let identifier of (identifiers$ | async)" class="w-100 p">
<div *ngIf="(identifier.identifierType=='doi')">
<p class="float-left">{{doiToUpdateMessage | translate}}: {{identifier.value}}
({{"item.edit.identifiers.doi.status."+identifier.identifierStatus|translate}})
</p>
</div>
</div>
<ds-modify-item-overview [item]="item"></ds-modify-item-overview>
<div class="space-children-mr">
<button (click)="performAction()" class="btn btn-outline-secondary perform-action">{{confirmMessage | translate}}
</button>
<button [routerLink]="[itemPageRoute, 'edit']" class="btn btn-outline-secondary cancel">
{{cancelMessage| translate}}
</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,106 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { Item } from '../../../core/shared/item.model';
import { RouterStub } from '../../../shared/testing/router.stub';
import { of as observableOf } from 'rxjs';
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { RouterTestingModule } from '@angular/router/testing';
import { TranslateModule } from '@ngx-translate/core';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { ActivatedRoute, Router } from '@angular/router';
import { ItemDataService } from '../../../core/data/item-data.service';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { By } from '@angular/platform-browser';
import { ItemRegisterDoiComponent } from './item-register-doi.component';
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { IdentifierDataService } from '../../../core/data/identifier-data.service';
let comp: ItemRegisterDoiComponent;
let fixture: ComponentFixture<ItemRegisterDoiComponent>;
let mockItem;
let itemPageUrl;
let routerStub;
let mockItemDataService: ItemDataService;
let mockIdentifierDataService: IdentifierDataService;
let routeStub;
let notificationsServiceStub;
describe('ItemRegisterDoiComponent', () => {
beforeEach(waitForAsync(() => {
mockItem = Object.assign(new Item(), {
id: 'fake-id',
handle: 'fake/handle',
lastModified: '2018',
isWithdrawn: true
});
itemPageUrl = `fake-url/${mockItem.id}`;
routerStub = Object.assign(new RouterStub(), {
url: `${itemPageUrl}/edit`
});
mockIdentifierDataService = jasmine.createSpyObj('mockIdentifierDataService', {
getIdentifierDataFor: createSuccessfulRemoteDataObject$({'identifiers': []}),
getIdentifierRegistrationConfiguration: createSuccessfulRemoteDataObject$('true'),
registerIdentifier: createSuccessfulRemoteDataObject$({'identifiers': []}),
});
mockItemDataService = jasmine.createSpyObj('mockItemDataService', {
registerDOI: createSuccessfulRemoteDataObject$(mockItem)
});
routeStub = {
data: observableOf({
dso: createSuccessfulRemoteDataObject(Object.assign(new Item(), {
id: 'fake-id'
}))
})
};
notificationsServiceStub = new NotificationsServiceStub();
TestBed.configureTestingModule({
imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
declarations: [ItemRegisterDoiComponent],
providers: [
{ provide: ActivatedRoute, useValue: routeStub },
{ provide: Router, useValue: routerStub },
{ provide: ItemDataService, useValue: mockItemDataService },
{ provide: IdentifierDataService, useValue: mockIdentifierDataService},
{ provide: NotificationsService, useValue: notificationsServiceStub }
], schemas: [
CUSTOM_ELEMENTS_SCHEMA
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ItemRegisterDoiComponent);
comp = fixture.componentInstance;
fixture.detectChanges();
});
it('should render a page with messages based on the \'register-doi\' messageKey', () => {
const header = fixture.debugElement.query(By.css('h2')).nativeElement;
expect(header.innerHTML).toContain('item.edit.register-doi.header');
const description = fixture.debugElement.query(By.css('p')).nativeElement;
expect(description.innerHTML).toContain('item.edit.register-doi.description');
const confirmButton = fixture.debugElement.query(By.css('button.perform-action')).nativeElement;
expect(confirmButton.innerHTML).toContain('item.edit.register-doi.confirm');
const cancelButton = fixture.debugElement.query(By.css('button.cancel')).nativeElement;
expect(cancelButton.innerHTML).toContain('item.edit.register-doi.cancel');
});
describe('performAction', () => {
it('should call registerDOI function from the ItemDataService', () => {
spyOn(comp, 'processRestResponse');
comp.performAction();
expect(mockIdentifierDataService.registerIdentifier).toHaveBeenCalledWith(comp.item, 'doi');
expect(comp.processRestResponse).toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,95 @@
import { Component } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { AbstractSimpleItemActionComponent } from '../simple-item-action/abstract-simple-item-action.component';
import { RemoteData } from '../../../core/data/remote-data';
import { Item } from '../../../core/shared/item.model';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { ItemDataService } from '../../../core/data/item-data.service';
import { getFirstSucceededRemoteData } from '../../../core/shared/operators';
import { first, map } from 'rxjs/operators';
import { hasValue } from '../../../shared/empty.util';
import { Observable } from 'rxjs';
import { getItemPageRoute } from '../../item-page-routing-paths';
import { IdentifierDataService } from '../../../core/data/identifier-data.service';
import { Identifier } from '../../../shared/object-list/identifier-data/identifier.model';
@Component({
selector: 'ds-item-register-doi',
templateUrl: './item-register-doi-component.html'
})
/**
* Component responsible for rendering the Item Register DOI page
*/
export class ItemRegisterDoiComponent extends AbstractSimpleItemActionComponent {
protected messageKey = 'register-doi';
doiToUpdateMessage = 'item.edit.' + this.messageKey + '.to-update';
identifiers$: Observable<Identifier[]>;
processing = false;
constructor(protected route: ActivatedRoute,
protected router: Router,
protected notificationsService: NotificationsService,
protected itemDataService: ItemDataService,
protected translateService: TranslateService,
protected identifierDataService: IdentifierDataService) {
super(route, router, notificationsService, itemDataService, translateService);
}
/**
* Initialise component
*/
ngOnInit(): void {
this.itemRD$ = this.route.data.pipe(
map((data) => data.dso),
getFirstSucceededRemoteData()
)as Observable<RemoteData<Item>>;
this.itemRD$.pipe(first()).subscribe((rd) => {
this.item = rd.payload;
this.itemPageRoute = getItemPageRoute(this.item);
this.identifiers$ = this.identifierDataService.getIdentifierDataFor(this.item).pipe(
map((identifierRD) => {
if (identifierRD.statusCode !== 401 && hasValue(identifierRD.payload)) {
return identifierRD.payload.identifiers;
} else {
return null;
}
}),
);
}
);
this.confirmMessage = 'item.edit.' + this.messageKey + '.confirm';
this.cancelMessage = 'item.edit.' + this.messageKey + '.cancel';
this.headerMessage = 'item.edit.' + this.messageKey + '.header';
this.descriptionMessage = 'item.edit.' + this.messageKey + '.description';
}
/**
* Perform the register DOI action to the item
*/
performAction() {
this.registerDoi();
}
/**
* Request that a pending, minted or null DOI be queued for registration
*/
registerDoi() {
this.processing = true;
this.identifierDataService.registerIdentifier(this.item, 'doi').subscribe(
(response: RemoteData<Item>) => {
if (response.hasCompleted) {
this.processing = false;
this.processRestResponse(response);
}
}
);
}
}

View File

@@ -8,6 +8,17 @@
{{statusData[statusKey]}}
</div>
</div>
<div *ngFor="let identifier of (identifiers$ | async)" class="w-100">
<div *ngIf="(identifier.identifierType=='doi')">
<div class="col-3 float-left status-label">
{{identifier.identifierType.toLocaleUpperCase()}}
</div>
<div class="col-9 float-left status-label">{{identifier.value}}
({{"item.edit.identifiers.doi.status."+identifier.identifierStatus|translate}})</div>
</div>
</div>
<div class="col-3 float-left status-label">
{{'item.edit.tabs.status.labels.itemPage' | translate}}:
</div>
@@ -18,4 +29,5 @@
<div *ngFor="let operation of (operations$ | async)" class="w-100" [ngClass]="{'pt-3': operation}">
<ds-item-operation *ngIf="operation" [operation]="operation"></ds-item-operation>
</div>
</div>

View File

@@ -11,8 +11,14 @@ import { Item } from '../../../core/shared/item.model';
import { By } from '@angular/platform-browser';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { of as observableOf } from 'rxjs';
import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils';
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
import { IdentifierDataService } from '../../../core/data/identifier-data.service';
import { ConfigurationDataService } from '../../../core/data/configuration-data.service';
import { ConfigurationProperty } from '../../../core/shared/configuration-property.model';
let mockIdentifierDataService: IdentifierDataService;
let mockConfigurationDataService: ConfigurationDataService;
describe('ItemStatusComponent', () => {
let comp: ItemStatusComponent;
@@ -28,6 +34,20 @@ describe('ItemStatusComponent', () => {
}
});
mockIdentifierDataService = jasmine.createSpyObj('mockIdentifierDataService', {
getIdentifierDataFor: createSuccessfulRemoteDataObject$({'identifiers': []}),
getIdentifierRegistrationConfiguration: createSuccessfulRemoteDataObject$('true')
});
mockConfigurationDataService = jasmine.createSpyObj('configurationDataService', {
findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), {
name: 'identifiers.item-status.register-doi',
values: [
'true'
]
}))
});
const itemPageUrl = `/items/${mockItem.uuid}`;
const routeStub = {
@@ -50,6 +70,8 @@ describe('ItemStatusComponent', () => {
{ provide: ActivatedRoute, useValue: routeStub },
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
{ provide: AuthorizationDataService, useValue: authorizationService },
{ provide: IdentifierDataService, useValue: mockIdentifierDataService },
{ provide: ConfigurationDataService, useValue: mockConfigurationDataService }
], schemas: [CUSTOM_ELEMENTS_SCHEMA]
}).compileComponents();
}));

View File

@@ -3,14 +3,21 @@ import { fadeIn, fadeInOut } from '../../../shared/animations/fade';
import { Item } from '../../../core/shared/item.model';
import { ActivatedRoute } from '@angular/router';
import { ItemOperation } from '../item-operation/itemOperation.model';
import { distinctUntilChanged, first, map, mergeMap, toArray } from 'rxjs/operators';
import { BehaviorSubject, Observable, from as observableFrom } from 'rxjs';
import { distinctUntilChanged, first, map, mergeMap, switchMap, toArray } from 'rxjs/operators';
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { RemoteData } from '../../../core/data/remote-data';
import { getItemEditRoute, getItemPageRoute } from '../../item-page-routing-paths';
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
import { hasValue } from '../../../shared/empty.util';
import { getAllSucceededRemoteDataPayload } from '../../../core/shared/operators';
import {
getAllSucceededRemoteDataPayload, getFirstSucceededRemoteData, getRemoteDataPayload,
} from '../../../core/shared/operators';
import { IdentifierDataService } from '../../../core/data/identifier-data.service';
import { Identifier } from '../../../shared/object-list/identifier-data/identifier.model';
import { ConfigurationProperty } from '../../../core/shared/configuration-property.model';
import { ConfigurationDataService } from '../../../core/data/configuration-data.service';
import { IdentifierData } from '../../../shared/object-list/identifier-data/identifier-data.model';
@Component({
selector: 'ds-item-status',
@@ -47,9 +54,15 @@ export class ItemStatusComponent implements OnInit {
operations$: BehaviorSubject<ItemOperation[]> = new BehaviorSubject<ItemOperation[]>([]);
/**
* The keys of the actions (to loop over)
* Identifiers (handles, DOIs)
*/
actionsKeys;
identifiers$: Observable<Identifier[]>;
/**
* Configuration and state variables regarding DOIs
*/
public subs: Subscription[] = [];
/**
* Route to the item's page
@@ -57,9 +70,15 @@ export class ItemStatusComponent implements OnInit {
itemPageRoute$: Observable<string>;
constructor(private route: ActivatedRoute,
private authorizationService: AuthorizationDataService) {
private authorizationService: AuthorizationDataService,
private identifierDataService: IdentifierDataService,
private configurationService: ConfigurationDataService,
) {
}
/**
* Initialise component
*/
ngOnInit(): void {
this.itemRD$ = this.route.parent.data.pipe(map((data) => data.dso));
this.itemRD$.pipe(
@@ -72,12 +91,37 @@ export class ItemStatusComponent implements OnInit {
lastModified: item.lastModified
});
this.statusDataKeys = Object.keys(this.statusData);
// Observable for item identifiers (retrieved from embedded link)
this.identifiers$ = this.identifierDataService.getIdentifierDataFor(item).pipe(
map((identifierRD) => {
if (identifierRD.statusCode !== 401 && hasValue(identifierRD.payload)) {
return identifierRD.payload.identifiers;
} else {
return null;
}
}),
);
// Observable for configuration determining whether the Register DOI feature is enabled
let registerConfigEnabled$: Observable<boolean> = this.configurationService.findByPropertyName('identifiers.item-status.register-doi').pipe(
getFirstSucceededRemoteData(),
getRemoteDataPayload(),
map((enabled: ConfigurationProperty) => {
if (enabled !== undefined && enabled.values) {
return true;
}
return false;
})
);
/*
Construct a base list of operations.
The key is used to build messages
i18n example: 'item.edit.tabs.status.buttons.<key>.label'
The value is supposed to be a href for the button
*/
const operations = [];
const operations: ItemOperation[] = [];
operations.push(new ItemOperation('authorizations', this.getCurrentUrl(item) + '/authorizations', FeatureID.CanManagePolicies, true));
operations.push(new ItemOperation('mappedCollections', this.getCurrentUrl(item) + '/mapper', FeatureID.CanManageMappings, true));
if (item.isWithdrawn) {
@@ -92,27 +136,74 @@ export class ItemStatusComponent implements OnInit {
}
operations.push(new ItemOperation('delete', this.getCurrentUrl(item) + '/delete', FeatureID.CanDelete, true));
operations.push(new ItemOperation('move', this.getCurrentUrl(item) + '/move', FeatureID.CanMove, true));
this.operations$.next(operations);
observableFrom(operations).pipe(
mergeMap((operation) => {
if (hasValue(operation.featureID)) {
return this.authorizationService.isAuthorized(operation.featureID, item.self).pipe(
/*
When the identifier data stream changes, determine whether the register DOI button should be shown or not.
This is based on whether the DOI is in the right state (minted or pending, not already queued for registration
or registered) and whether the configuration property identifiers.item-status.register-doi is true
*/
this.identifierDataService.getIdentifierDataFor(item).pipe(
getFirstSucceededRemoteData(),
getRemoteDataPayload(),
mergeMap((data: IdentifierData) => {
let identifiers = data.identifiers;
let no_doi = true;
let pending = false;
if (identifiers !== undefined && identifiers !== null) {
identifiers.forEach((identifier: Identifier) => {
if (hasValue(identifier) && identifier.identifierType === 'doi') {
// The item has some kind of DOI
no_doi = false;
if (identifier.identifierStatus === 'PENDING' || identifier.identifierStatus === 'MINTED'
|| identifier.identifierStatus == null) {
// The item's DOI is pending, minted or null.
// It isn't registered, reserved, queued for registration or reservation or update, deleted
// or queued for deletion.
pending = true;
}
}
});
}
// If there is no DOI, or a pending/minted/null DOI, and the config is enabled, return true
return registerConfigEnabled$.pipe(
map((enabled: boolean) => {
return enabled && (pending || no_doi);
}
));
}),
// Switch map pushes the register DOI operation onto a copy of the base array then returns to the pipe
switchMap((showDoi: boolean) => {
let ops = [...operations];
if (showDoi) {
ops.push(new ItemOperation('register-doi', this.getCurrentUrl(item) + '/register-doi', FeatureID.CanRegisterDOI, true));
}
return ops;
}),
// Merge map checks and transforms each operation in the array based on whether it is authorized or not (disabled)
mergeMap((op: ItemOperation) => {
if (hasValue(op.featureID)) {
return this.authorizationService.isAuthorized(op.featureID, item.self).pipe(
distinctUntilChanged(),
map((authorized) => new ItemOperation(operation.operationKey, operation.operationUrl, operation.featureID, !authorized, authorized))
map((authorized) => new ItemOperation(op.operationKey, op.operationUrl, op.featureID, !authorized, authorized))
);
} else {
return [operation];
return [op];
}
}),
toArray()
).subscribe((ops) => this.operations$.next(ops));
// Wait for all operations to be emitted and return as an array
toArray(),
).subscribe((data) => {
// Update the operations$ subject that draws the administrative buttons on the status page
this.operations$.next(data);
});
});
this.itemPageRoute$ = this.itemRD$.pipe(
getAllSucceededRemoteDataPayload(),
map((item) => getItemPageRoute(item))
);
}
/**
@@ -127,4 +218,10 @@ export class ItemStatusComponent implements OnInit {
return hasValue(operation) ? operation.operationKey : undefined;
}
ngOnDestroy(): void {
this.subs
.filter((subscription) => hasValue(subscription))
.forEach((subscription) => subscription.unsubscribe());
}
}

View File

@@ -1,16 +1,38 @@
<ds-metadata-field-wrapper [label]="label | translate">
<ng-container *ngFor="let mdValue of mdValues; let last=last;">
<ng-container *ngTemplateOutlet="(renderMarkdown ? markdown : simple); context: {value: mdValue.value}">
<!--
Choose a template. Priority: markdown, link, browse link.
-->
<ng-container *ngTemplateOutlet="(renderMarkdown ? markdown : (hasLink(mdValue) ? link : (hasBrowseDefinition() ? browselink : simple)));
context: {value: mdValue.value}">
</ng-container>
<span class="separator" *ngIf="!last" [innerHTML]="separator"></span>
</ng-container>
</ds-metadata-field-wrapper>
<!-- Render value as markdown -->
<ng-template #markdown let-value="value">
<span class="dont-break-out" [innerHTML]="value | dsMarkdown | async">
</span>
</ng-template>
<!-- Render value as a link (href and label) -->
<ng-template #link let-value="value">
<a class="dont-break-out ds-simple-metadata-link" target="_blank" [href]="value">
{{value}}
</a>
</ng-template>
<!-- Render simple value in a span -->
<ng-template #simple let-value="value">
<span class="dont-break-out preserve-line-breaks">{{value}}</span>
</ng-template>
<!-- Render value as a link to browse index -->
<ng-template #browselink let-value="value">
<a class="dont-break-out preserve-line-breaks ds-browse-link"
[routerLink]="['/browse', browseDefinition.id]"
[queryParams]="getQueryParams(value)">
{{value}}
</a>
</ng-template>

View File

@@ -52,6 +52,7 @@ describe('MetadataValuesComponent', () => {
comp.mdValues = mockMetadata;
comp.separator = mockSeperator;
comp.label = mockLabel;
comp.urlRegex = /^.*test.*$/;
fixture.detectChanges();
}));
@@ -67,4 +68,9 @@ describe('MetadataValuesComponent', () => {
expect(separators.length).toBe(mockMetadata.length - 1);
});
it('should correctly detect a pattern on string containing "test"', () => {
const mdValue = {value: 'This is a test value'} as MetadataValue;
expect(comp.hasLink(mdValue)).toBe(true);
});
});

View File

@@ -1,6 +1,8 @@
import { Component, Inject, Input, OnChanges, SimpleChanges } from '@angular/core';
import { MetadataValue } from '../../../core/shared/metadata.models';
import { APP_CONFIG, AppConfig } from '../../../../config/app-config.interface';
import { BrowseDefinition } from '../../../core/shared/browse-definition.model';
import { hasValue } from '../../../shared/empty.util';
/**
* This component renders the configured 'values' into the ds-metadata-field-wrapper component.
@@ -40,12 +42,51 @@ export class MetadataValuesComponent implements OnChanges {
*/
@Input() enableMarkdown = false;
/**
* Whether any valid HTTP(S) URL should be rendered as a link
*/
@Input() urlRegex?;
/**
* This variable will be true if both {@link environment.markdown.enabled} and {@link enableMarkdown} are true.
*/
renderMarkdown;
@Input() browseDefinition?: BrowseDefinition;
ngOnChanges(changes: SimpleChanges): void {
this.renderMarkdown = !!this.appConfig.markdown.enabled && this.enableMarkdown;
}
/**
* Does this metadata value have a configured link to a browse definition?
*/
hasBrowseDefinition(): boolean {
return hasValue(this.browseDefinition);
}
/**
* Does this metadata value have a valid URL that should be rendered as a link?
* @param value A MetadataValue being displayed
*/
hasLink(value: MetadataValue): boolean {
if (hasValue(this.urlRegex)) {
const pattern = new RegExp(this.urlRegex);
return pattern.test(value.value);
}
return false;
}
/**
* Return a queryparams object for use in a link, with the key dependent on whether this browse
* definition is metadata browse, or item browse
* @param value the specific metadata value being linked
*/
getQueryParams(value) {
let queryParams = {startsWith: value};
if (this.browseDefinition.metadataBrowse) {
return {value: value};
}
return queryParams;
}
}

View File

@@ -7,10 +7,8 @@
<div *ngIf="!item.isWithdrawn || (isAdmin$|async)" class="full-item-info">
<div class="d-flex flex-row">
<ds-item-page-title-field class="mr-auto" [item]="item"></ds-item-page-title-field>
<div class="pl-2 space-children-mr">
<ds-dso-page-edit-button [pageRoute]="itemPageRoute$ | async" [dso]="item"
[tooltipMsg]="'item.page.edit'"></ds-dso-page-edit-button>
</div>
<ds-dso-edit-menu></ds-dso-edit-menu>
</div>
<div class="simple-view-link my-3" *ngIf="!fromSubmissionObject">
<a class="btn btn-outline-primary" [routerLink]="[(itemPageRoute$ | async)]">

View File

@@ -18,6 +18,7 @@ import { BitstreamRequestACopyPageComponent } from './bitstreams/request-a-copy/
import { REQUEST_COPY_MODULE_PATH } from '../app-routing-paths';
import { OrcidPageComponent } from './orcid-page/orcid-page.component';
import { OrcidPageGuard } from './orcid-page/orcid-page.guard';
import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver';
@NgModule({
imports: [
@@ -26,7 +27,8 @@ import { OrcidPageGuard } from './orcid-page/orcid-page.guard';
path: ':id',
resolve: {
dso: ItemPageResolver,
breadcrumb: ItemBreadcrumbResolver
breadcrumb: ItemBreadcrumbResolver,
menu: DSOEditMenuResolver
},
runGuardsAndResolvers: 'always',
children: [

View File

@@ -39,7 +39,6 @@ import { MediaViewerImageComponent } from './media-viewer/media-viewer-image/med
import { NgxGalleryModule } from '@kolkov/ngx-gallery';
import { MiradorViewerComponent } from './mirador-viewer/mirador-viewer.component';
import { VersionPageComponent } from './version-page/version-page/version-page.component';
import { VersionedItemComponent } from './simple/item-types/versioned-item/versioned-item.component';
import { ThemedFileSectionComponent } from './simple/field-components/file-section/themed-file-section.component';
import { OrcidAuthComponent } from './orcid-page/orcid-auth/orcid-auth.component';
import { OrcidPageComponent } from './orcid-page/orcid-page.component';
@@ -53,6 +52,7 @@ import { ItemVersionsModule } from './versions/item-versions.module';
import { BitstreamRequestACopyPageComponent } from './bitstreams/request-a-copy/bitstream-request-a-copy-page.component';
import { FileSectionComponent } from './simple/field-components/file-section/file-section.component';
import { ItemSharedModule } from './item-shared.module';
import { DsoPageModule } from '../shared/dso-page/dso-page.module';
const ENTRY_COMPONENTS = [
@@ -91,7 +91,6 @@ const DECLARATIONS = [
OrcidSyncSettingsComponent,
OrcidQueueComponent,
ItemAlertsComponent,
VersionedItemComponent,
BitstreamRequestACopyPageComponent,
];
@@ -109,7 +108,8 @@ const DECLARATIONS = [
NgxGalleryModule,
NgbAccordionModule,
ResultsBackButtonModule,
UploadModule
UploadModule,
DsoPageModule,
],
declarations: [
...DECLARATIONS,

View File

@@ -10,16 +10,14 @@ import { TabbedRelatedEntitiesSearchComponent } from './simple/related-entities/
import { ItemVersionsDeleteModalComponent } from './versions/item-versions-delete-modal/item-versions-delete-modal.component';
import { ItemVersionsSummaryModalComponent } from './versions/item-versions-summary-modal/item-versions-summary-modal.component';
import { MetadataValuesComponent } from './field-components/metadata-values/metadata-values.component';
import { DsoPageVersionButtonComponent } from '../shared/dso-page/dso-page-version-button/dso-page-version-button.component';
import { PersonPageClaimButtonComponent } from '../shared/dso-page/person-page-claim-button/person-page-claim-button.component';
import { GenericItemPageFieldComponent } from './simple/field-components/specific-field/generic/generic-item-page-field.component';
import { MetadataRepresentationListComponent } from './simple/metadata-representation-list/metadata-representation-list.component';
import { RelatedItemsComponent } from './simple/related-items/related-items-component';
import { DsoPageOrcidButtonComponent } from '../shared/dso-page/dso-page-orcid-button/dso-page-orcid-button.component';
const ENTRY_COMPONENTS = [
ItemVersionsDeleteModalComponent,
ItemVersionsSummaryModalComponent,
];
const COMPONENTS = [
@@ -27,12 +25,9 @@ const COMPONENTS = [
RelatedEntitiesSearchComponent,
TabbedRelatedEntitiesSearchComponent,
MetadataValuesComponent,
DsoPageVersionButtonComponent,
PersonPageClaimButtonComponent,
GenericItemPageFieldComponent,
MetadataRepresentationListComponent,
RelatedItemsComponent,
DsoPageOrcidButtonComponent
];
@NgModule({

View File

@@ -7,6 +7,8 @@ import { SharedModule } from '../../../../../shared/shared.module';
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
import { environment } from '../../../../../../environments/environment';
import { By } from '@angular/platform-browser';
import { BrowseDefinitionDataService } from '../../../../../core/browse/browse-definition-data.service';
import { BrowseDefinitionDataServiceStub } from '../../../../../shared/testing/browse-definition-data-service.stub';
let comp: ItemPageAbstractFieldComponent;
let fixture: ComponentFixture<ItemPageAbstractFieldComponent>;
@@ -25,6 +27,7 @@ describe('ItemPageAbstractFieldComponent', () => {
],
providers: [
{ provide: APP_CONFIG, useValue: environment },
{ provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub }
],
declarations: [ItemPageAbstractFieldComponent],
schemas: [NO_ERRORS_SCHEMA]

View File

@@ -3,10 +3,12 @@ import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { TranslateLoaderMock } from '../../../../../shared/testing/translate-loader.mock';
import { MetadataValuesComponent } from '../../../../field-components/metadata-values/metadata-values.component';
import { mockItemWithMetadataFieldAndValue } from '../item-page-field.component.spec';
import { mockItemWithMetadataFieldsAndValue } from '../item-page-field.component.spec';
import { ItemPageAuthorFieldComponent } from './item-page-author-field.component';
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
import { environment } from '../../../../../../environments/environment';
import { BrowseDefinitionDataService } from '../../../../../core/browse/browse-definition-data.service';
import { BrowseDefinitionDataServiceStub } from '../../../../../shared/testing/browse-definition-data-service.stub';
let comp: ItemPageAuthorFieldComponent;
let fixture: ComponentFixture<ItemPageAuthorFieldComponent>;
@@ -25,6 +27,7 @@ describe('ItemPageAuthorFieldComponent', () => {
})],
providers: [
{ provide: APP_CONFIG, useValue: environment },
{ provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub }
],
declarations: [ItemPageAuthorFieldComponent, MetadataValuesComponent],
schemas: [NO_ERRORS_SCHEMA]
@@ -37,7 +40,7 @@ describe('ItemPageAuthorFieldComponent', () => {
beforeEach(waitForAsync(() => {
fixture = TestBed.createComponent(ItemPageAuthorFieldComponent);
comp = fixture.componentInstance;
comp.item = mockItemWithMetadataFieldAndValue(field, mockValue);
comp.item = mockItemWithMetadataFieldsAndValue([field], mockValue);
fixture.detectChanges();
}));

View File

@@ -3,10 +3,12 @@ import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { TranslateLoaderMock } from '../../../../../shared/testing/translate-loader.mock';
import { MetadataValuesComponent } from '../../../../field-components/metadata-values/metadata-values.component';
import { mockItemWithMetadataFieldAndValue } from '../item-page-field.component.spec';
import { mockItemWithMetadataFieldsAndValue } from '../item-page-field.component.spec';
import { ItemPageDateFieldComponent } from './item-page-date-field.component';
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
import { environment } from '../../../../../../environments/environment';
import { BrowseDefinitionDataService } from '../../../../../core/browse/browse-definition-data.service';
import { BrowseDefinitionDataServiceStub } from '../../../../../shared/testing/browse-definition-data-service.stub';
let comp: ItemPageDateFieldComponent;
let fixture: ComponentFixture<ItemPageDateFieldComponent>;
@@ -25,6 +27,7 @@ describe('ItemPageDateFieldComponent', () => {
})],
providers: [
{ provide: APP_CONFIG, useValue: environment },
{ provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub }
],
declarations: [ItemPageDateFieldComponent, MetadataValuesComponent],
schemas: [NO_ERRORS_SCHEMA]
@@ -36,7 +39,7 @@ describe('ItemPageDateFieldComponent', () => {
beforeEach(waitForAsync(() => {
fixture = TestBed.createComponent(ItemPageDateFieldComponent);
comp = fixture.componentInstance;
comp.item = mockItemWithMetadataFieldAndValue(mockField, mockValue);
comp.item = mockItemWithMetadataFieldsAndValue([mockField], mockValue);
fixture.detectChanges();
}));

View File

@@ -3,10 +3,12 @@ import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { TranslateLoaderMock } from '../../../../../shared/testing/translate-loader.mock';
import { MetadataValuesComponent } from '../../../../field-components/metadata-values/metadata-values.component';
import { mockItemWithMetadataFieldAndValue } from '../item-page-field.component.spec';
import { mockItemWithMetadataFieldsAndValue } from '../item-page-field.component.spec';
import { GenericItemPageFieldComponent } from './generic-item-page-field.component';
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
import { environment } from '../../../../../../environments/environment';
import { BrowseDefinitionDataService } from '../../../../../core/browse/browse-definition-data.service';
import { BrowseDefinitionDataServiceStub } from '../../../../../shared/testing/browse-definition-data-service.stub';
let comp: GenericItemPageFieldComponent;
let fixture: ComponentFixture<GenericItemPageFieldComponent>;
@@ -27,6 +29,7 @@ describe('GenericItemPageFieldComponent', () => {
})],
providers: [
{ provide: APP_CONFIG, useValue: environment },
{ provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub }
],
declarations: [GenericItemPageFieldComponent, MetadataValuesComponent],
schemas: [NO_ERRORS_SCHEMA]
@@ -38,7 +41,7 @@ describe('GenericItemPageFieldComponent', () => {
beforeEach(waitForAsync(() => {
fixture = TestBed.createComponent(GenericItemPageFieldComponent);
comp = fixture.componentInstance;
comp.item = mockItemWithMetadataFieldAndValue(mockField, mockValue);
comp.item = mockItemWithMetadataFieldsAndValue([mockField], mockValue);
comp.fields = mockFields;
comp.label = mockLabel;
fixture.detectChanges();

View File

@@ -40,5 +40,10 @@ export class GenericItemPageFieldComponent extends ItemPageFieldComponent {
*/
@Input() enableMarkdown = false;
/**
* Whether any valid HTTP(S) URL should be rendered as a link
*/
@Input() urlRegex?: string;
}

View File

@@ -4,5 +4,7 @@
[separator]="separator"
[label]="label"
[enableMarkdown]="enableMarkdown"
[urlRegex]="urlRegex"
[browseDefinition]="browseDefinition|async"
></ds-metadata-values>
</div>

View File

@@ -12,6 +12,10 @@ import { environment } from '../../../../../environments/environment';
import { MarkdownPipe } from '../../../../shared/utils/markdown.pipe';
import { SharedModule } from '../../../../shared/shared.module';
import { APP_CONFIG } from '../../../../../config/app-config.interface';
import { By } from '@angular/platform-browser';
import { BrowseDefinitionDataService } from '../../../../core/browse/browse-definition-data.service';
import { BrowseDefinitionDataServiceStub } from '../../../../shared/testing/browse-definition-data-service.stub';
import { RouterTestingModule } from '@angular/router/testing';
let comp: ItemPageFieldComponent;
let fixture: ComponentFixture<ItemPageFieldComponent>;
@@ -20,7 +24,9 @@ let markdownSpy;
const mockValue = 'test value';
const mockField = 'dc.test';
const mockLabel = 'test label';
const mockFields = [mockField];
const mockAuthorField = 'dc.contributor.author';
const mockDateIssuedField = 'dc.date.issued';
const mockFields = [mockField, mockAuthorField, mockDateIssuedField];
describe('ItemPageFieldComponent', () => {
@@ -34,6 +40,7 @@ describe('ItemPageFieldComponent', () => {
const buildTestEnvironment = async () => {
await TestBed.configureTestingModule({
imports: [
RouterTestingModule.withRoutes([]),
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
@@ -44,6 +51,7 @@ describe('ItemPageFieldComponent', () => {
],
providers: [
{ provide: APP_CONFIG, useValue: appConfig },
{ provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub }
],
declarations: [ItemPageFieldComponent, MetadataValuesComponent],
schemas: [NO_ERRORS_SCHEMA]
@@ -53,7 +61,7 @@ describe('ItemPageFieldComponent', () => {
markdownSpy = spyOn(MarkdownPipe.prototype, 'transform');
fixture = TestBed.createComponent(ItemPageFieldComponent);
comp = fixture.componentInstance;
comp.item = mockItemWithMetadataFieldAndValue(mockField, mockValue);
comp.item = mockItemWithMetadataFieldsAndValue(mockFields, mockValue);
comp.fields = mockFields;
comp.label = mockLabel;
fixture.detectChanges();
@@ -126,17 +134,57 @@ describe('ItemPageFieldComponent', () => {
expect(markdownSpy).toHaveBeenCalled();
});
});
});
describe('test rendering of configured browse links', () => {
beforeEach(() => {
fixture.detectChanges();
});
waitForAsync(() => {
it('should have a browse link', () => {
expect(fixture.debugElement.query(By.css('a.ds-browse-link')).nativeElement.innerHTML).toContain(mockValue);
});
});
});
describe('test rendering of configured regex-based links', () => {
beforeEach(() => {
comp.urlRegex = '^test';
fixture.detectChanges();
});
waitForAsync(() => {
it('should have a rendered (non-browse) link since the value matches ^test', () => {
expect(fixture.debugElement.query(By.css('a.ds-simple-metadata-link')).nativeElement.innerHTML).toContain(mockValue);
});
});
});
describe('test skipping of configured links that do NOT match regex', () => {
beforeEach(() => {
comp.urlRegex = '^nope';
fixture.detectChanges();
});
beforeEach(waitForAsync(() => {
it('should NOT have a rendered (non-browse) link since the value matches ^test', () => {
expect(fixture.debugElement.query(By.css('a.ds-simple-metadata-link'))).toBeNull();
});
}));
});
});
export function mockItemWithMetadataFieldAndValue(field: string, value: string): Item {
export function mockItemWithMetadataFieldsAndValue(fields: string[], value: string): Item {
const item = Object.assign(new Item(), {
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
metadata: new MetadataMap()
});
item.metadata[field] = [{
language: 'en_US',
value: value
}] as MetadataValue[];
fields.forEach((field: string) => {
item.metadata[field] = [{
language: 'en_US',
value: value
}] as MetadataValue[];
});
return item;
}

View File

@@ -1,5 +1,10 @@
import { Component, Input } from '@angular/core';
import { Item } from '../../../../core/shared/item.model';
import { map } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { BrowseDefinition } from '../../../../core/shared/browse-definition.model';
import { BrowseDefinitionDataService } from '../../../../core/browse/browse-definition-data.service';
import { getRemoteDataPayload } from '../../../../core/shared/operators';
/**
* This component can be used to represent metadata on a simple item page.
@@ -12,6 +17,9 @@ import { Item } from '../../../../core/shared/item.model';
})
export class ItemPageFieldComponent {
constructor(protected browseDefinitionDataService: BrowseDefinitionDataService) {
}
/**
* The item to display metadata for
*/
@@ -38,4 +46,19 @@ export class ItemPageFieldComponent {
*/
separator = '<br/>';
/**
* Whether any valid HTTP(S) URL should be rendered as a link
*/
urlRegex?: string;
/**
* Return browse definition that matches any field used in this component if it is configured as a browse
* link in dspace.cfg (webui.browse.link.<n>)
*/
get browseDefinition(): Observable<BrowseDefinition> {
return this.browseDefinitionDataService.findByFields(this.fields).pipe(
getRemoteDataPayload(),
map((def) => def)
);
}
}

View File

@@ -3,7 +3,7 @@ import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { TranslateLoaderMock } from '../../../../../shared/testing/translate-loader.mock';
import { MetadataValuesComponent } from '../../../../field-components/metadata-values/metadata-values.component';
import { mockItemWithMetadataFieldAndValue } from '../item-page-field.component.spec';
import { mockItemWithMetadataFieldsAndValue } from '../item-page-field.component.spec';
import { ItemPageTitleFieldComponent } from './item-page-title-field.component';
let comp: ItemPageTitleFieldComponent;
@@ -31,7 +31,7 @@ describe('ItemPageTitleFieldComponent', () => {
beforeEach(waitForAsync(() => {
fixture = TestBed.createComponent(ItemPageTitleFieldComponent);
comp = fixture.componentInstance;
comp.item = mockItemWithMetadataFieldAndValue(mockField, mockValue);
comp.item = mockItemWithMetadataFieldsAndValue([mockField], mockValue);
fixture.detectChanges();
}));

View File

@@ -2,11 +2,13 @@ import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { TranslateLoaderMock } from '../../../../../shared/testing/translate-loader.mock';
import { mockItemWithMetadataFieldAndValue } from '../item-page-field.component.spec';
import { mockItemWithMetadataFieldsAndValue } from '../item-page-field.component.spec';
import { ItemPageUriFieldComponent } from './item-page-uri-field.component';
import { MetadataUriValuesComponent } from '../../../../field-components/metadata-uri-values/metadata-uri-values.component';
import { environment } from '../../../../../../environments/environment';
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
import { BrowseDefinitionDataService } from '../../../../../core/browse/browse-definition-data.service';
import { BrowseDefinitionDataServiceStub } from '../../../../../shared/testing/browse-definition-data-service.stub';
let comp: ItemPageUriFieldComponent;
let fixture: ComponentFixture<ItemPageUriFieldComponent>;
@@ -26,6 +28,7 @@ describe('ItemPageUriFieldComponent', () => {
})],
providers: [
{ provide: APP_CONFIG, useValue: environment },
{ provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub }
],
declarations: [ItemPageUriFieldComponent, MetadataUriValuesComponent],
schemas: [NO_ERRORS_SCHEMA]
@@ -37,7 +40,7 @@ describe('ItemPageUriFieldComponent', () => {
beforeEach(waitForAsync(() => {
fixture = TestBed.createComponent(ItemPageUriFieldComponent);
comp = fixture.componentInstance;
comp.item = mockItemWithMetadataFieldAndValue(mockField, mockValue);
comp.item = mockItemWithMetadataFieldsAndValue([mockField], mockValue);
comp.fields = [mockField];
comp.label = mockLabel;
fixture.detectChanges();

View File

@@ -11,12 +11,7 @@
<div class="d-flex flex-row">
<ds-item-page-title-field [item]="object" class="mr-auto">
</ds-item-page-title-field>
<div class="pl-2 space-children-mr">
<ds-dso-page-version-button (newVersionEvent)="onCreateNewVersion()" [dso]="object"
[tooltipMsgCreate]="'item.page.version.create'"
[tooltipMsgHasDraft]="'item.page.version.hasDraft'"></ds-dso-page-version-button>
<ds-dso-page-edit-button [pageRoute]="itemPageRoute" [dso]="object" [tooltipMsg]="'publication.page.edit'"></ds-dso-page-edit-button>
</div>
<ds-dso-edit-menu></ds-dso-edit-menu>
</div>
<div class="row">
<div class="col-xs-12 col-md-4">

View File

@@ -36,6 +36,10 @@ import { VersionDataService } from '../../../../core/data/version-data.service';
import { RouterTestingModule } from '@angular/router/testing';
import { WorkspaceitemDataService } from '../../../../core/submission/workspaceitem-data.service';
import { SearchService } from '../../../../core/shared/search/search.service';
import { BrowseDefinitionDataService } from '../../../../core/browse/browse-definition-data.service';
import {
BrowseDefinitionDataServiceStub
} from '../../../../shared/testing/browse-definition-data-service.stub';
const noMetadata = new MetadataMap();
@@ -87,7 +91,8 @@ describe('PublicationComponent', () => {
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
{ provide: WorkspaceitemDataService, useValue: {} },
{ provide: SearchService, useValue: {} },
{ provide: RouteService, useValue: mockRouteService }
{ provide: RouteService, useValue: mockRouteService },
{ provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub },
],
schemas: [NO_ERRORS_SCHEMA]

View File

@@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ViewMode } from '../../../../core/shared/view-mode.model';
import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
import { VersionedItemComponent } from '../versioned-item/versioned-item.component';
import { ItemComponent } from '../shared/item.component';
/**
* Component that represents a publication Item page
@@ -14,6 +14,6 @@ import { VersionedItemComponent } from '../versioned-item/versioned-item.compone
templateUrl: './publication.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PublicationComponent extends VersionedItemComponent {
export class PublicationComponent extends ItemComponent {
}

View File

@@ -23,7 +23,9 @@ import { UUIDService } from '../../../../core/shared/uuid.service';
import { isNotEmpty } from '../../../../shared/empty.util';
import { TranslateLoaderMock } from '../../../../shared/mocks/translate-loader.mock';
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
import {
createSuccessfulRemoteDataObject$
} from '../../../../shared/remote-data.utils';
import { TruncatableService } from '../../../../shared/truncatable/truncatable.service';
import { TruncatePipe } from '../../../../shared/utils/truncate.pipe';
import { GenericItemPageFieldComponent } from '../../field-components/specific-field/generic/generic-item-page-field.component';
@@ -38,6 +40,10 @@ import { VersionHistoryDataService } from '../../../../core/data/version-history
import { RouterTestingModule } from '@angular/router/testing';
import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service';
import { ResearcherProfileDataService } from '../../../../core/profile/researcher-profile-data.service';
import { BrowseDefinitionDataService } from '../../../../core/browse/browse-definition-data.service';
import {
BrowseDefinitionDataServiceStub
} from '../../../../shared/testing/browse-definition-data-service.stub';
import { buildPaginatedList } from '../../../../core/data/paginated-list.model';
import { PageInfo } from '../../../../core/shared/page-info.model';
@@ -125,7 +131,8 @@ export function getItemPageFieldsTest(mockItem: Item, component) {
{ provide: SearchService, useValue: {} },
{ provide: RouteService, useValue: mockRouteService },
{ provide: AuthorizationDataService, useValue: authorizationService },
{ provide: ResearcherProfileDataService, useValue: {} }
{ provide: ResearcherProfileDataService, useValue: {} },
{ provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub },
],
schemas: [NO_ERRORS_SCHEMA]
@@ -444,7 +451,7 @@ describe('ItemComponent', () => {
{ provide: SearchService, useValue: {} },
{ provide: RouteService, useValue: mockRouteService },
{ provide: AuthorizationDataService, useValue: {} },
{ provide: ResearcherProfileDataService, useValue: {} }
{ provide: ResearcherProfileDataService, useValue: {} },
],
schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(ItemComponent, {

View File

@@ -12,12 +12,7 @@
<div class="d-flex flex-row">
<ds-item-page-title-field [item]="object" class="mr-auto">
</ds-item-page-title-field>
<div class="pl-2 space-children-mr">
<ds-dso-page-version-button (newVersionEvent)="onCreateNewVersion()" [dso]="object"
[tooltipMsgCreate]="'item.page.version.create'"
[tooltipMsgHasDraft]="'item.page.version.hasDraft'"></ds-dso-page-version-button>
<ds-dso-page-edit-button [pageRoute]="itemPageRoute" [dso]="object" [tooltipMsg]="'item.page.edit'"></ds-dso-page-edit-button>
</div>
<ds-dso-edit-menu></ds-dso-edit-menu>
</div>
<div class="row">
<div class="col-xs-12 col-md-4">

View File

@@ -37,6 +37,10 @@ import { RouterTestingModule } from '@angular/router/testing';
import { WorkspaceitemDataService } from '../../../../core/submission/workspaceitem-data.service';
import { SearchService } from '../../../../core/shared/search/search.service';
import { ItemVersionsSharedService } from '../../../versions/item-versions-shared.service';
import { BrowseDefinitionDataService } from '../../../../core/browse/browse-definition-data.service';
import {
BrowseDefinitionDataServiceStub
} from '../../../../shared/testing/browse-definition-data-service.stub';
const noMetadata = new MetadataMap();
@@ -90,7 +94,8 @@ describe('UntypedItemComponent', () => {
{ provide: SearchService, useValue: {} },
{ provide: ItemDataService, useValue: {} },
{ provide: ItemVersionsSharedService, useValue: {} },
{ provide: RouteService, useValue: mockRouteService }
{ provide: RouteService, useValue: mockRouteService },
{ provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub },
],
schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(UntypedItemComponent, {

View File

@@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Item } from '../../../../core/shared/item.model';
import { ViewMode } from '../../../../core/shared/view-mode.model';
import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
import { VersionedItemComponent } from '../versioned-item/versioned-item.component';
import { ItemComponent } from '../shared/item.component';
/**
* Component that represents a publication Item page
@@ -15,6 +15,6 @@ import { VersionedItemComponent } from '../versioned-item/versioned-item.compone
templateUrl: './untyped-item.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UntypedItemComponent extends VersionedItemComponent {
export class UntypedItemComponent extends ItemComponent {
}

View File

@@ -1,105 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { VersionedItemComponent } from './versioned-item.component';
import { VersionHistoryDataService } from '../../../../core/data/version-history-data.service';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { VersionDataService } from '../../../../core/data/version-data.service';
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { ItemVersionsSharedService } from '../../../versions/item-versions-shared.service';
import { Item } from '../../../../core/shared/item.model';
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
import { buildPaginatedList } from '../../../../core/data/paginated-list.model';
import { PageInfo } from '../../../../core/shared/page-info.model';
import { MetadataMap } from '../../../../core/shared/metadata.models';
import { createRelationshipsObservable, mockRouteService } from '../shared/item.component.spec';
import { RouterTestingModule } from '@angular/router/testing';
import { Component } from '@angular/core';
import { WorkspaceitemDataService } from '../../../../core/submission/workspaceitem-data.service';
import { SearchService } from '../../../../core/shared/search/search.service';
import { ItemDataService } from '../../../../core/data/item-data.service';
import { Version } from '../../../../core/shared/version.model';
import { RouteService } from '../../../../core/services/route.service';
import { TranslateLoaderMock } from '../../../../shared/testing/translate-loader.mock';
import { ItemSharedModule } from '../../../item-shared.module';
const mockItem: Item = Object.assign(new Item(), {
bundles: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])),
metadata: new MetadataMap(),
relationships: createRelationshipsObservable(),
_links: {
self: {
href: 'item-href'
},
version: {
href: 'version-href'
}
}
});
@Component({template: ''})
class DummyComponent {
}
describe('VersionedItemComponent', () => {
let component: VersionedItemComponent;
let fixture: ComponentFixture<VersionedItemComponent>;
let versionService: VersionDataService;
let versionHistoryService: VersionHistoryDataService;
const versionServiceSpy = jasmine.createSpyObj('versionService', {
findByHref: createSuccessfulRemoteDataObject$<Version>(new Version()),
});
const versionHistoryServiceSpy = jasmine.createSpyObj('versionHistoryService', {
createVersion: createSuccessfulRemoteDataObject$<Version>(new Version()),
});
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [VersionedItemComponent, DummyComponent],
imports: [
RouterTestingModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateLoaderMock,
}
}),
ItemSharedModule,
],
providers: [
{ provide: VersionHistoryDataService, useValue: versionHistoryServiceSpy },
{ provide: VersionDataService, useValue: versionServiceSpy },
{ provide: NotificationsService, useValue: {} },
{ provide: ItemVersionsSharedService, useValue: {} },
{ provide: WorkspaceitemDataService, useValue: {} },
{ provide: SearchService, useValue: {} },
{ provide: ItemDataService, useValue: {} },
{ provide: RouteService, useValue: mockRouteService }
]
}).compileComponents();
versionService = TestBed.inject(VersionDataService);
versionHistoryService = TestBed.inject(VersionHistoryDataService);
});
beforeEach(() => {
fixture = TestBed.createComponent(VersionedItemComponent);
component = fixture.componentInstance;
component.object = mockItem;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
describe('when onCreateNewVersion() is called', () => {
it('should call versionService.findByHref', () => {
component.onCreateNewVersion();
expect(versionService.findByHref).toHaveBeenCalledWith('version-href');
});
});
});

View File

@@ -1,82 +0,0 @@
import { Component } from '@angular/core';
import { ItemComponent } from '../shared/item.component';
import { ItemVersionsSummaryModalComponent } from '../../../versions/item-versions-summary-modal/item-versions-summary-modal.component';
import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../../../core/shared/operators';
import { RemoteData } from '../../../../core/data/remote-data';
import { Version } from '../../../../core/shared/version.model';
import { switchMap, tap } from 'rxjs/operators';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { VersionHistoryDataService } from '../../../../core/data/version-history-data.service';
import { TranslateService } from '@ngx-translate/core';
import { VersionDataService } from '../../../../core/data/version-data.service';
import { ItemVersionsSharedService } from '../../../versions/item-versions-shared.service';
import { Router } from '@angular/router';
import { WorkspaceitemDataService } from '../../../../core/submission/workspaceitem-data.service';
import { SearchService } from '../../../../core/shared/search/search.service';
import { Item } from '../../../../core/shared/item.model';
import { ItemDataService } from '../../../../core/data/item-data.service';
import { WorkspaceItem } from '../../../../core/submission/models/workspaceitem.model';
import { RouteService } from '../../../../core/services/route.service';
@Component({
selector: 'ds-versioned-item',
templateUrl: './versioned-item.component.html',
styleUrls: ['./versioned-item.component.scss']
})
export class VersionedItemComponent extends ItemComponent {
constructor(
private modalService: NgbModal,
private versionHistoryService: VersionHistoryDataService,
private translateService: TranslateService,
private versionService: VersionDataService,
private itemVersionShared: ItemVersionsSharedService,
protected router: Router,
private workspaceItemDataService: WorkspaceitemDataService,
private searchService: SearchService,
private itemService: ItemDataService,
protected routeService: RouteService,
) {
super(routeService, router);
}
/**
* Open a modal that allows to create a new version starting from the specified item, with optional summary
*/
onCreateNewVersion(): void {
const item = this.object;
const versionHref = item._links.version.href;
// Open modal
const activeModal = this.modalService.open(ItemVersionsSummaryModalComponent);
// Show current version in modal
this.versionService.findByHref(versionHref).pipe(getFirstCompletedRemoteData()).subscribe((res: RemoteData<Version>) => {
// if res.hasNoContent then the item is unversioned
activeModal.componentInstance.firstVersion = res.hasNoContent;
activeModal.componentInstance.versionNumber = (res.hasNoContent ? undefined : res.payload.version);
});
// On createVersionEvent emitted create new version and notify
activeModal.componentInstance.createVersionEvent.pipe(
switchMap((summary: string) => this.versionHistoryService.createVersion(item._links.self.href, summary)),
getFirstCompletedRemoteData(),
// close model (should be displaying loading/waiting indicator) when version creation failed/succeeded
tap(() => activeModal.close()),
// show success/failure notification
tap((res: RemoteData<Version>) => { this.itemVersionShared.notifyCreateNewVersion(res); }),
// get workspace item
getFirstSucceededRemoteDataPayload<Version>(),
switchMap((newVersion: Version) => this.itemService.findByHref(newVersion._links.item.href)),
getFirstSucceededRemoteDataPayload<Item>(),
switchMap((newVersionItem: Item) => this.workspaceItemDataService.findByItem(newVersionItem.uuid, true, false)),
getFirstSucceededRemoteDataPayload<WorkspaceItem>(),
).subscribe((wsItem) => {
const wsiId = wsItem.id;
const route = 'workspaceitems/' + wsiId + '/edit';
this.router.navigateByUrl(route);
});
}
}

View File

@@ -11,6 +11,8 @@ import { MetadataValue } from '../../../core/shared/metadata.models';
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { ItemMetadataRepresentation } from '../../../core/shared/metadata-representation/item/item-metadata-representation.model';
import { MetadatumRepresentation } from '../../../core/shared/metadata-representation/metadatum/metadatum-representation.model';
import { BrowseDefinitionDataService } from '../../../core/browse/browse-definition-data.service';
import { BrowseDefinitionDataServiceStub } from '../../../shared/testing/browse-definition-data-service.stub';
const itemType = 'Person';
const metadataFields = ['dc.contributor.author', 'dc.creator'];
@@ -104,7 +106,8 @@ describe('MetadataRepresentationListComponent', () => {
imports: [TranslateModule.forRoot()],
declarations: [MetadataRepresentationListComponent, VarDirective],
providers: [
{ provide: RelationshipDataService, useValue: relationshipService }
{ provide: RelationshipDataService, useValue: relationshipService },
{ provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub }
],
schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(MetadataRepresentationListComponent, {

Some files were not shown because too many files have changed in this diff Show More