mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge remote-tracking branch 'upstream/main' into w2p-77205_issue-927-Non-site-admin-edit-authorization-group
This commit is contained in:
@@ -11,6 +11,8 @@ import { Collection } from '../../../../../core/shared/collection.model';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { getCollectionEditRoute } from '../../../../../+collection-page/collection-page-routing-paths';
|
||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
|
||||
|
||||
describe('CollectionAdminSearchResultListElementComponent', () => {
|
||||
let component: CollectionAdminSearchResultListElementComponent;
|
||||
@@ -33,7 +35,8 @@ describe('CollectionAdminSearchResultListElementComponent', () => {
|
||||
RouterTestingModule.withRoutes([])
|
||||
],
|
||||
declarations: [CollectionAdminSearchResultListElementComponent],
|
||||
providers: [{ provide: TruncatableService, useValue: {} }],
|
||||
providers: [{ provide: TruncatableService, useValue: {} },
|
||||
{ provide: DSONameService, useClass: DSONameServiceMock }],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
})
|
||||
.compileComponents();
|
||||
|
@@ -11,6 +11,8 @@ import { CommunityAdminSearchResultListElementComponent } from './community-admi
|
||||
import { CommunitySearchResult } from '../../../../../shared/object-collection/shared/community-search-result.model';
|
||||
import { Community } from '../../../../../core/shared/community.model';
|
||||
import { getCommunityEditRoute } from '../../../../../+community-page/community-page-routing-paths';
|
||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
|
||||
|
||||
describe('CommunityAdminSearchResultListElementComponent', () => {
|
||||
let component: CommunityAdminSearchResultListElementComponent;
|
||||
@@ -33,7 +35,8 @@ describe('CommunityAdminSearchResultListElementComponent', () => {
|
||||
RouterTestingModule.withRoutes([])
|
||||
],
|
||||
declarations: [CommunityAdminSearchResultListElementComponent],
|
||||
providers: [{ provide: TruncatableService, useValue: {} }],
|
||||
providers: [{ provide: TruncatableService, useValue: {} },
|
||||
{ provide: DSONameService, useClass: DSONameServiceMock }],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
})
|
||||
.compileComponents();
|
||||
|
@@ -8,6 +8,8 @@ import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model';
|
||||
import { ItemAdminSearchResultListElementComponent } from './item-admin-search-result-list-element.component';
|
||||
import { Item } from '../../../../../core/shared/item.model';
|
||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
|
||||
|
||||
describe('ItemAdminSearchResultListElementComponent', () => {
|
||||
let component: ItemAdminSearchResultListElementComponent;
|
||||
@@ -30,7 +32,8 @@ describe('ItemAdminSearchResultListElementComponent', () => {
|
||||
RouterTestingModule.withRoutes([])
|
||||
],
|
||||
declarations: [ItemAdminSearchResultListElementComponent],
|
||||
providers: [{ provide: TruncatableService, useValue: {} }],
|
||||
providers: [{ provide: TruncatableService, useValue: {} },
|
||||
{ provide: DSONameService, useClass: DSONameServiceMock }],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
})
|
||||
.compileComponents();
|
||||
|
@@ -16,6 +16,8 @@ import { Item } from '../../../../../core/shared/item.model';
|
||||
import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils';
|
||||
import { getMockLinkService } from '../../../../../shared/mocks/link-service.mock';
|
||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
|
||||
|
||||
describe('WorkflowItemAdminWorkflowListElementComponent', () => {
|
||||
let component: WorkflowItemSearchResultAdminWorkflowListElementComponent;
|
||||
@@ -49,6 +51,7 @@ describe('WorkflowItemAdminWorkflowListElementComponent', () => {
|
||||
providers: [
|
||||
{ provide: TruncatableService, useValue: mockTruncatableService },
|
||||
{ provide: LinkService, useValue: linkService },
|
||||
{ provide: DSONameService, useClass: DSONameServiceMock }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
})
|
||||
|
@@ -12,6 +12,7 @@ import { Item } from '../../../../../core/shared/item.model';
|
||||
import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component';
|
||||
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||
import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
|
||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||
|
||||
@listableObjectComponent(WorkflowItemSearchResult, ViewMode.ListElement, Context.AdminWorkflowSearch)
|
||||
@Component({
|
||||
@@ -29,8 +30,11 @@ export class WorkflowItemSearchResultAdminWorkflowListElementComponent extends S
|
||||
*/
|
||||
public item$: Observable<Item>;
|
||||
|
||||
constructor(private linkService: LinkService, protected truncatableService: TruncatableService) {
|
||||
super(truncatableService);
|
||||
constructor(private linkService: LinkService,
|
||||
protected truncatableService: TruncatableService,
|
||||
protected dsoNameService: DSONameService
|
||||
) {
|
||||
super(truncatableService, dsoNameService);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -0,0 +1,28 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { ThemedComponent } from '../../shared/theme-support/themed.component';
|
||||
import { BrowseBySwitcherComponent } from './browse-by-switcher.component';
|
||||
|
||||
/**
|
||||
* Themed wrapper for BrowseBySwitcherComponent
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-themed-browse-by-switcher',
|
||||
styleUrls: [],
|
||||
templateUrl: '../../shared/theme-support/themed.component.html'
|
||||
})
|
||||
export class ThemedBrowseBySwitcherComponent extends ThemedComponent<BrowseBySwitcherComponent> {
|
||||
protected getComponentName(): string {
|
||||
return 'BrowseBySwitcherComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../../themes/${themeName}/app/+browse-by/+browse-by-switcher/browse-by-switcher.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import(`./browse-by-switcher.component`);
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -1,9 +1,9 @@
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { BrowseByGuard } from './browse-by-guard';
|
||||
import { BrowseBySwitcherComponent } from './+browse-by-switcher/browse-by-switcher.component';
|
||||
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';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -14,7 +14,7 @@ import { BrowseByI18nBreadcrumbResolver } from './browse-by-i18n-breadcrumb.reso
|
||||
children: [
|
||||
{
|
||||
path: ':id',
|
||||
component: BrowseBySwitcherComponent,
|
||||
component: ThemedBrowseBySwitcherComponent,
|
||||
canActivate: [BrowseByGuard],
|
||||
resolve: { breadcrumb: BrowseByI18nBreadcrumbResolver },
|
||||
data: { title: 'browse.title', breadcrumbKey: 'browse.metadata' }
|
||||
|
@@ -5,6 +5,7 @@ import { SharedModule } from '../shared/shared.module';
|
||||
import { BrowseByMetadataPageComponent } from './+browse-by-metadata-page/browse-by-metadata-page.component';
|
||||
import { BrowseByDatePageComponent } from './+browse-by-date-page/browse-by-date-page.component';
|
||||
import { BrowseBySwitcherComponent } from './+browse-by-switcher/browse-by-switcher.component';
|
||||
import { ThemedBrowseBySwitcherComponent } from './+browse-by-switcher/themed-browse-by-switcher.component';
|
||||
|
||||
const ENTRY_COMPONENTS = [
|
||||
// put only entry components that use custom decorator
|
||||
@@ -20,6 +21,7 @@ const ENTRY_COMPONENTS = [
|
||||
],
|
||||
declarations: [
|
||||
BrowseBySwitcherComponent,
|
||||
ThemedBrowseBySwitcherComponent,
|
||||
...ENTRY_COMPONENTS
|
||||
],
|
||||
exports: [
|
||||
|
@@ -24,6 +24,10 @@ export function getCollectionEditRolesRoute(id) {
|
||||
return new URLCombiner(getCollectionPageRoute(id), COLLECTION_EDIT_PATH, COLLECTION_EDIT_ROLES_PATH).toString();
|
||||
}
|
||||
|
||||
export function getCollectionItemTemplateRoute(id) {
|
||||
return new URLCombiner(getCollectionPageRoute(id), ITEMTEMPLATE_PATH).toString();
|
||||
}
|
||||
|
||||
export const COLLECTION_CREATE_PATH = 'create';
|
||||
export const COLLECTION_EDIT_PATH = 'edit';
|
||||
export const COLLECTION_EDIT_ROLES_PATH = 'roles';
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { CollectionPageComponent } from './collection-page.component';
|
||||
import { CollectionPageResolver } from './collection-page.resolver';
|
||||
import { CreateCollectionPageComponent } from './create-collection-page/create-collection-page.component';
|
||||
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
||||
@@ -21,6 +20,7 @@ import {
|
||||
import { CollectionPageAdministratorGuard } from './collection-page-administrator.guard';
|
||||
import { MenuItemType } from '../shared/menu/initial-menus-state';
|
||||
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
|
||||
import { ThemedCollectionPageComponent } from './themed-collection-page.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -62,7 +62,7 @@ import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
component: CollectionPageComponent,
|
||||
component: ThemedCollectionPageComponent,
|
||||
pathMatch: 'full',
|
||||
}
|
||||
],
|
||||
|
@@ -13,6 +13,7 @@ import { CollectionItemMapperComponent } from './collection-item-mapper/collecti
|
||||
import { SearchService } from '../core/shared/search/search.service';
|
||||
import { StatisticsModule } from '../statistics/statistics.module';
|
||||
import { CollectionFormModule } from './collection-form/collection-form.module';
|
||||
import { ThemedCollectionPageComponent } from './themed-collection-page.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -25,6 +26,7 @@ import { CollectionFormModule } from './collection-form/collection-form.module';
|
||||
],
|
||||
declarations: [
|
||||
CollectionPageComponent,
|
||||
ThemedCollectionPageComponent,
|
||||
CreateCollectionPageComponent,
|
||||
DeleteCollectionPageComponent,
|
||||
EditItemTemplatePageComponent,
|
||||
|
@@ -15,6 +15,7 @@ import { Collection } from '../../../core/shared/collection.model';
|
||||
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
|
||||
import { RequestService } from '../../../core/data/request.service';
|
||||
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||
import { getCollectionItemTemplateRoute } from '../../collection-page-routing-paths';
|
||||
|
||||
describe('CollectionMetadataComponent', () => {
|
||||
let comp: CollectionMetadataComponent;
|
||||
@@ -35,11 +36,13 @@ describe('CollectionMetadataComponent', () => {
|
||||
self: { href: 'collection-selflink' }
|
||||
}
|
||||
});
|
||||
const collectionTemplateHref = 'rest/api/test/collections/template';
|
||||
|
||||
const itemTemplateServiceStub = Object.assign({
|
||||
findByCollectionID: () => createSuccessfulRemoteDataObject$(template),
|
||||
create: () => createSuccessfulRemoteDataObject$(template),
|
||||
deleteByCollectionID: () => observableOf(true)
|
||||
const itemTemplateServiceStub = jasmine.createSpyObj('itemTemplateService', {
|
||||
findByCollectionID: createSuccessfulRemoteDataObject$(template),
|
||||
create: createSuccessfulRemoteDataObject$(template),
|
||||
deleteByCollectionID: observableOf(true),
|
||||
getCollectionEndpoint: observableOf(collectionTemplateHref),
|
||||
});
|
||||
|
||||
const notificationsService = jasmine.createSpyObj('notificationsService', {
|
||||
@@ -50,7 +53,7 @@ describe('CollectionMetadataComponent', () => {
|
||||
remove: {}
|
||||
});
|
||||
const requestService = jasmine.createSpyObj('requestService', {
|
||||
removeByHrefSubstring: {}
|
||||
setStaleByHrefSubstring: {}
|
||||
});
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
@@ -87,14 +90,14 @@ describe('CollectionMetadataComponent', () => {
|
||||
it('should navigate to the collection\'s itemtemplate page', () => {
|
||||
spyOn(router, 'navigate');
|
||||
comp.addItemTemplate();
|
||||
expect(router.navigate).toHaveBeenCalledWith(['collections', collection.uuid, 'itemtemplate']);
|
||||
expect(router.navigate).toHaveBeenCalledWith([getCollectionItemTemplateRoute(collection.uuid)]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteItemTemplate', () => {
|
||||
describe('when delete returns a success', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(itemTemplateService, 'deleteByCollectionID').and.returnValue(observableOf(true));
|
||||
(itemTemplateService.deleteByCollectionID as jasmine.Spy).and.returnValue(observableOf(true));
|
||||
comp.deleteItemTemplate();
|
||||
});
|
||||
|
||||
@@ -103,14 +106,15 @@ describe('CollectionMetadataComponent', () => {
|
||||
});
|
||||
|
||||
it('should reset related object and request cache', () => {
|
||||
expect(objectCache.remove).toHaveBeenCalledWith(template.self);
|
||||
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(collection.self);
|
||||
expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith(collectionTemplateHref);
|
||||
expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith(template.self);
|
||||
expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith(collection.self);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when delete returns a failure', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(itemTemplateService, 'deleteByCollectionID').and.returnValue(observableOf(false));
|
||||
(itemTemplateService.deleteByCollectionID as jasmine.Spy).and.returnValue(observableOf(false));
|
||||
comp.deleteItemTemplate();
|
||||
});
|
||||
|
||||
|
@@ -7,12 +7,13 @@ import { ItemTemplateDataService } from '../../../core/data/item-template-data.s
|
||||
import { combineLatest as combineLatestObservable, Observable } from 'rxjs';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { getRemoteDataPayload, getFirstSucceededRemoteData } from '../../../core/shared/operators';
|
||||
import { switchMap, take } from 'rxjs/operators';
|
||||
import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators';
|
||||
import { switchMap, tap } from 'rxjs/operators';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
|
||||
import { RequestService } from '../../../core/data/request.service';
|
||||
import { getCollectionItemTemplateRoute } from '../../collection-page-routing-paths';
|
||||
|
||||
/**
|
||||
* Component for editing a collection's metadata
|
||||
@@ -53,8 +54,7 @@ export class CollectionMetadataComponent extends ComcolMetadataComponent<Collect
|
||||
*/
|
||||
initTemplateItem() {
|
||||
this.itemTemplateRD$ = this.dsoRD$.pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
switchMap((collection: Collection) => this.itemTemplateService.findByCollectionID(collection.uuid))
|
||||
);
|
||||
}
|
||||
@@ -64,19 +64,20 @@ export class CollectionMetadataComponent extends ComcolMetadataComponent<Collect
|
||||
*/
|
||||
addItemTemplate() {
|
||||
const collection$ = this.dsoRD$.pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
take(1)
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
);
|
||||
const template$ = collection$.pipe(
|
||||
switchMap((collection: Collection) => this.itemTemplateService.create(new Item(), collection.uuid)),
|
||||
getFirstSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
take(1)
|
||||
switchMap((collection: Collection) => this.itemTemplateService.create(new Item(), collection.uuid).pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
)),
|
||||
);
|
||||
const templateHref$ = collection$.pipe(
|
||||
switchMap((collection) => this.itemTemplateService.getCollectionEndpoint(collection.id)),
|
||||
);
|
||||
|
||||
combineLatestObservable(collection$, template$).subscribe(([collection, template]) => {
|
||||
this.router.navigate(['collections', collection.uuid, 'itemtemplate']);
|
||||
combineLatestObservable(collection$, template$, templateHref$).subscribe(([collection, template, templateHref]) => {
|
||||
this.requestService.setStaleByHrefSubstring(templateHref);
|
||||
this.router.navigate([getCollectionItemTemplateRoute(collection.uuid)]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -85,23 +86,30 @@ export class CollectionMetadataComponent extends ComcolMetadataComponent<Collect
|
||||
*/
|
||||
deleteItemTemplate() {
|
||||
const collection$ = this.dsoRD$.pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
take(1)
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
);
|
||||
const template$ = collection$.pipe(
|
||||
switchMap((collection: Collection) => this.itemTemplateService.findByCollectionID(collection.uuid)),
|
||||
getFirstSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
take(1)
|
||||
switchMap((collection: Collection) => this.itemTemplateService.findByCollectionID(collection.uuid).pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
)),
|
||||
);
|
||||
const templateHref$ = collection$.pipe(
|
||||
switchMap((collection) => this.itemTemplateService.getCollectionEndpoint(collection.id)),
|
||||
);
|
||||
|
||||
combineLatestObservable(collection$, template$).pipe(
|
||||
switchMap(([collection, template]) => {
|
||||
const success$ = this.itemTemplateService.deleteByCollectionID(template, collection.uuid);
|
||||
this.objectCache.remove(template.self);
|
||||
this.requestService.removeByHrefSubstring(collection.self);
|
||||
return success$;
|
||||
combineLatestObservable(collection$, template$, templateHref$).pipe(
|
||||
switchMap(([collection, template, templateHref]) => {
|
||||
return this.itemTemplateService.deleteByCollectionID(template, collection.uuid).pipe(
|
||||
tap((success: boolean) => {
|
||||
if (success) {
|
||||
this.objectCache.remove(templateHref);
|
||||
this.objectCache.remove(template.self);
|
||||
this.requestService.setStaleByHrefSubstring(template.self);
|
||||
this.requestService.setStaleByHrefSubstring(templateHref);
|
||||
this.requestService.setStaleByHrefSubstring(collection.self);
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
).subscribe((success: boolean) => {
|
||||
if (success) {
|
||||
|
@@ -1,9 +1,13 @@
|
||||
<div class="container" *ngVar="(collectionRD$ | async)?.payload as collection">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h2 class="border-bottom">{{ 'collection.edit.template.head' | translate:{ collection: collection?.name } }}</h2>
|
||||
<ds-item-metadata [updateService]="itemTemplateService"></ds-item-metadata>
|
||||
<button [routerLink]="getCollectionEditUrl(collection)" class="btn btn-outline-secondary">{{ 'collection.edit.template.cancel' | translate }}</button>
|
||||
<div class="col-12" *ngVar="(itemRD$ | async) as itemRD">
|
||||
<ng-container *ngIf="itemRD?.hasSucceeded">
|
||||
<h2 class="border-bottom">{{ 'collection.edit.template.head' | translate:{ collection: collection?.name } }}</h2>
|
||||
<ds-item-metadata [updateService]="itemTemplateService" [item]="itemRD?.payload"></ds-item-metadata>
|
||||
<button [routerLink]="getCollectionEditUrl(collection)" class="btn btn-outline-secondary">{{ 'collection.edit.template.cancel' | translate }}</button>
|
||||
</ng-container>
|
||||
<ds-loading *ngIf="itemRD?.isLoading" [message]="'collection.edit.template.loading' | translate"></ds-loading>
|
||||
<ds-alert *ngIf="itemRD?.hasFailed" [type]="AlertTypeEnum.Error" [content]="'collection.edit.template.error' | translate"></ds-alert>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -9,7 +9,7 @@ import { ActivatedRoute } from '@angular/router';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { Collection } from '../../core/shared/collection.model';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
|
||||
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import { getCollectionEditRoute } from '../collection-page-routing-paths';
|
||||
|
||||
describe('EditItemTemplatePageComponent', () => {
|
||||
@@ -24,11 +24,14 @@ describe('EditItemTemplatePageComponent', () => {
|
||||
id: 'collection-id',
|
||||
name: 'Fake Collection'
|
||||
});
|
||||
itemTemplateService = jasmine.createSpyObj('itemTemplateService', {
|
||||
findByCollectionID: createSuccessfulRemoteDataObject$({})
|
||||
});
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule],
|
||||
declarations: [EditItemTemplatePageComponent],
|
||||
providers: [
|
||||
{ provide: ItemTemplateDataService, useValue: {} },
|
||||
{ provide: ItemTemplateDataService, useValue: itemTemplateService },
|
||||
{ provide: ActivatedRoute, useValue: { parent: { data: observableOf({ dso: createSuccessfulRemoteDataObject(collection) }) } } }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
@@ -38,7 +41,6 @@ describe('EditItemTemplatePageComponent', () => {
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(EditItemTemplatePageComponent);
|
||||
comp = fixture.componentInstance;
|
||||
itemTemplateService = (comp as any).itemTemplateService;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
|
@@ -3,9 +3,12 @@ import { Observable } from 'rxjs';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { Collection } from '../../core/shared/collection.model';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { first, map } from 'rxjs/operators';
|
||||
import { first, map, switchMap } from 'rxjs/operators';
|
||||
import { ItemTemplateDataService } from '../../core/data/item-template-data.service';
|
||||
import { getCollectionEditRoute } from '../collection-page-routing-paths';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { getFirstSucceededRemoteDataPayload } from '../../core/shared/operators';
|
||||
import { AlertType } from '../../shared/alert/aletr-type';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-edit-item-template-page',
|
||||
@@ -21,12 +24,27 @@ export class EditItemTemplatePageComponent implements OnInit {
|
||||
*/
|
||||
collectionRD$: Observable<RemoteData<Collection>>;
|
||||
|
||||
/**
|
||||
* The template item
|
||||
*/
|
||||
itemRD$: Observable<RemoteData<Item>>;
|
||||
|
||||
/**
|
||||
* The AlertType enumeration
|
||||
* @type {AlertType}
|
||||
*/
|
||||
AlertTypeEnum = AlertType;
|
||||
|
||||
constructor(protected route: ActivatedRoute,
|
||||
public itemTemplateService: ItemTemplateDataService) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.collectionRD$ = this.route.parent.data.pipe(first(), map((data) => data.dso));
|
||||
this.itemRD$ = this.collectionRD$.pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
switchMap((collection) => this.itemTemplateService.findByCollectionID(collection.id)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
26
src/app/+collection-page/themed-collection-page.component.ts
Normal file
26
src/app/+collection-page/themed-collection-page.component.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ThemedComponent } from '../shared/theme-support/themed.component';
|
||||
import { CollectionPageComponent } from './collection-page.component';
|
||||
|
||||
/**
|
||||
* Themed wrapper for CollectionPageComponent
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-themed-community-page',
|
||||
styleUrls: [],
|
||||
templateUrl: '../shared/theme-support/themed.component.html',
|
||||
})
|
||||
export class ThemedCollectionPageComponent extends ThemedComponent<CollectionPageComponent> {
|
||||
protected getComponentName(): string {
|
||||
return 'CollectionPageComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../themes/${themeName}/app/+collection-page/collection-page.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import(`./collection-page.component`);
|
||||
}
|
||||
|
||||
}
|
@@ -1,7 +1,6 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { CommunityPageComponent } from './community-page.component';
|
||||
import { CommunityPageResolver } from './community-page.resolver';
|
||||
import { CreateCommunityPageComponent } from './create-community-page/create-community-page.component';
|
||||
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
||||
@@ -14,6 +13,7 @@ import { COMMUNITY_EDIT_PATH, COMMUNITY_CREATE_PATH } from './community-page-rou
|
||||
import { CommunityPageAdministratorGuard } from './community-page-administrator.guard';
|
||||
import { MenuItemType } from '../shared/menu/initial-menus-state';
|
||||
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
|
||||
import { ThemedCommunityPageComponent } from './themed-community-page.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -45,7 +45,7 @@ import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
component: CommunityPageComponent,
|
||||
component: ThemedCommunityPageComponent,
|
||||
pathMatch: 'full',
|
||||
}
|
||||
],
|
||||
|
@@ -11,6 +11,14 @@ import { CreateCommunityPageComponent } from './create-community-page/create-com
|
||||
import { DeleteCommunityPageComponent } from './delete-community-page/delete-community-page.component';
|
||||
import { StatisticsModule } from '../statistics/statistics.module';
|
||||
import { CommunityFormModule } from './community-form/community-form.module';
|
||||
import { ThemedCommunityPageComponent } from './themed-community-page.component';
|
||||
|
||||
const DECLARATIONS = [CommunityPageComponent,
|
||||
ThemedCommunityPageComponent,
|
||||
CommunityPageSubCollectionListComponent,
|
||||
CommunityPageSubCommunityListComponent,
|
||||
CreateCommunityPageComponent,
|
||||
DeleteCommunityPageComponent];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -21,11 +29,10 @@ import { CommunityFormModule } from './community-form/community-form.module';
|
||||
CommunityFormModule
|
||||
],
|
||||
declarations: [
|
||||
CommunityPageComponent,
|
||||
CommunityPageSubCollectionListComponent,
|
||||
CommunityPageSubCommunityListComponent,
|
||||
CreateCommunityPageComponent,
|
||||
DeleteCommunityPageComponent
|
||||
...DECLARATIONS
|
||||
],
|
||||
exports: [
|
||||
...DECLARATIONS
|
||||
]
|
||||
})
|
||||
|
||||
|
26
src/app/+community-page/themed-community-page.component.ts
Normal file
26
src/app/+community-page/themed-community-page.component.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ThemedComponent } from '../shared/theme-support/themed.component';
|
||||
import { CommunityPageComponent } from './community-page.component';
|
||||
|
||||
/**
|
||||
* Themed wrapper for CommunityPageComponent
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-themed-community-page',
|
||||
styleUrls: [],
|
||||
templateUrl: '../shared/theme-support/themed.component.html',
|
||||
})
|
||||
export class ThemedCommunityPageComponent extends ThemedComponent<CommunityPageComponent> {
|
||||
protected getComponentName(): string {
|
||||
return 'CommunityPageComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../themes/${themeName}/app/+community-page/community-page.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import(`./community-page.component`);
|
||||
}
|
||||
|
||||
}
|
@@ -1,7 +1,7 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
||||
import { SubmissionImportExternalComponent } from '../submission/import-external/submission-import-external.component';
|
||||
import { ThemedSubmissionImportExternalComponent } from '../submission/import-external/themed-submission-import-external.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -9,7 +9,7 @@ import { SubmissionImportExternalComponent } from '../submission/import-external
|
||||
{
|
||||
canActivate: [ AuthenticatedGuard ],
|
||||
path: '',
|
||||
component: SubmissionImportExternalComponent,
|
||||
component: ThemedSubmissionImportExternalComponent,
|
||||
pathMatch: 'full',
|
||||
data: {
|
||||
title: 'submission.import-external.page.title'
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { Component, OnInit, OnDestroy, Input } from '@angular/core';
|
||||
import {
|
||||
FieldUpdate,
|
||||
FieldUpdates
|
||||
@@ -30,7 +30,7 @@ export class AbstractItemUpdateComponent extends AbstractTrackableComponent impl
|
||||
/**
|
||||
* The item to display the edit page for
|
||||
*/
|
||||
item: Item;
|
||||
@Input() item: Item;
|
||||
/**
|
||||
* The current values and updates for all this item's fields
|
||||
* Should be initialized in the initializeUpdates method of the child component
|
||||
@@ -63,22 +63,24 @@ export class AbstractItemUpdateComponent extends AbstractTrackableComponent impl
|
||||
* Initialize common properties between item-update components
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.itemUpdateSubscription = observableCombineLatest([this.route.data, this.route.parent.data]).pipe(
|
||||
map(([data, parentData]: [Data, Data]) => Object.assign({}, data, parentData)),
|
||||
map((data: any) => data.dso),
|
||||
tap((rd: RemoteData<Item>) => {
|
||||
this.item = rd.payload;
|
||||
}),
|
||||
switchMap((rd: RemoteData<Item>) => {
|
||||
return this.itemService.findByHref(rd.payload._links.self.href, true, true, ...ITEM_PAGE_LINKS_TO_FOLLOW);
|
||||
}),
|
||||
getAllSucceededRemoteData()
|
||||
).subscribe((rd: RemoteData<Item>) => {
|
||||
this.item = rd.payload;
|
||||
this.itemPageRoute = getItemPageRoute(this.item);
|
||||
this.postItemInit();
|
||||
this.initializeUpdates();
|
||||
});
|
||||
if (hasValue(this.item)) {
|
||||
this.setItem(this.item);
|
||||
} else {
|
||||
// The item wasn't provided through an input, retrieve it from the route instead.
|
||||
this.itemUpdateSubscription = observableCombineLatest([this.route.data, this.route.parent.data]).pipe(
|
||||
map(([data, parentData]: [Data, Data]) => Object.assign({}, data, parentData)),
|
||||
map((data: any) => data.dso),
|
||||
tap((rd: RemoteData<Item>) => {
|
||||
this.item = rd.payload;
|
||||
}),
|
||||
switchMap((rd: RemoteData<Item>) => {
|
||||
return this.itemService.findByHref(rd.payload._links.self.href, true, true, ...ITEM_PAGE_LINKS_TO_FOLLOW);
|
||||
}),
|
||||
getAllSucceededRemoteData()
|
||||
).subscribe((rd: RemoteData<Item>) => {
|
||||
this.setItem(rd.payload);
|
||||
});
|
||||
}
|
||||
|
||||
this.discardTimeOut = environment.item.edit.undoTimeout;
|
||||
this.url = this.router.url;
|
||||
@@ -97,6 +99,13 @@ export class AbstractItemUpdateComponent extends AbstractTrackableComponent impl
|
||||
this.initializeUpdates();
|
||||
}
|
||||
|
||||
setItem(item: Item) {
|
||||
this.item = item;
|
||||
this.itemPageRoute = getItemPageRoute(this.item);
|
||||
this.postItemInit();
|
||||
this.initializeUpdates();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (hasValue(this.itemUpdateSubscription)) {
|
||||
this.itemUpdateSubscription.unsubscribe();
|
||||
|
@@ -5,11 +5,18 @@
|
||||
<div class="pt-2">
|
||||
<ul class="nav nav-tabs justify-content-start">
|
||||
<li *ngFor="let page of pages" class="nav-item">
|
||||
<a class="nav-link"
|
||||
[ngClass]="{'active' : page === currentPage}"
|
||||
[routerLink]="['./' + page]">
|
||||
{{'item.edit.tabs.' + page + '.head' | translate}}
|
||||
<a *ngIf="(page.enabled | async)"
|
||||
class="nav-link"
|
||||
[ngClass]="{'active' : page.page === currentPage}"
|
||||
[routerLink]="['./' + page.page]">
|
||||
{{'item.edit.tabs.' + page.page + '.head' | translate}}
|
||||
</a>
|
||||
<span [ngbTooltip]="'item.edit.tabs.disabled.tooltip' | translate">
|
||||
<button *ngIf="!(page.enabled | async)"
|
||||
class="nav-link disabled">
|
||||
{{'item.edit.tabs.' + page.page + '.head' | translate}}
|
||||
</button>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-pane active">
|
||||
|
@@ -0,0 +1,107 @@
|
||||
import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
|
||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ActivatedRoute, ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
|
||||
import { EditItemPageComponent } from './edit-item-page.component';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
|
||||
describe('ItemPageComponent', () => {
|
||||
let comp: EditItemPageComponent;
|
||||
let fixture: ComponentFixture<EditItemPageComponent>;
|
||||
|
||||
class AcceptAllGuard implements CanActivate {
|
||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
|
||||
return observableOf(true);
|
||||
}
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:max-classes-per-file
|
||||
class AcceptNoneGuard implements CanActivate {
|
||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
|
||||
console.log('BLA');
|
||||
return observableOf(false);
|
||||
}
|
||||
}
|
||||
|
||||
const accesiblePages = ['accessible'];
|
||||
const inaccesiblePages = ['inaccessible', 'inaccessibleDoubleGuard'];
|
||||
const mockRoute = {
|
||||
snapshot: {
|
||||
firstChild: {
|
||||
routeConfig: {
|
||||
path: accesiblePages[0]
|
||||
}
|
||||
},
|
||||
routerState: {
|
||||
snapshot: undefined
|
||||
}
|
||||
},
|
||||
routeConfig: {
|
||||
children: [
|
||||
{
|
||||
path: accesiblePages[0],
|
||||
canActivate: [AcceptAllGuard]
|
||||
}, {
|
||||
path: inaccesiblePages[0],
|
||||
canActivate: [AcceptNoneGuard]
|
||||
}, {
|
||||
path: inaccesiblePages[1],
|
||||
canActivate: [AcceptAllGuard, AcceptNoneGuard]
|
||||
},
|
||||
]
|
||||
},
|
||||
data: observableOf({dso: createSuccessfulRemoteDataObject(new Item())})
|
||||
};
|
||||
|
||||
const mockRouter = {
|
||||
routerState: {
|
||||
snapshot: undefined
|
||||
},
|
||||
events: observableOf(undefined)
|
||||
};
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
})],
|
||||
declarations: [EditItemPageComponent],
|
||||
providers: [
|
||||
{ provide: ActivatedRoute, useValue: mockRoute },
|
||||
{ provide: Router, useValue: mockRouter },
|
||||
AcceptAllGuard,
|
||||
AcceptNoneGuard,
|
||||
],
|
||||
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(EditItemPageComponent, {
|
||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
fixture = TestBed.createComponent(EditItemPageComponent);
|
||||
comp = fixture.componentInstance;
|
||||
spyOn((comp as any).injector, 'get').and.callFake((a) => new a());
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
describe('ngOnInit', () => {
|
||||
it('should enable tabs that the user can activate', fakeAsync(() => {
|
||||
const enabledItems = fixture.debugElement.queryAll(By.css('a.nav-link'));
|
||||
expect(enabledItems.length).toBe(accesiblePages.length);
|
||||
}));
|
||||
|
||||
it('should disable tabs that the user can not activate', () => {
|
||||
const disabledItems = fixture.debugElement.queryAll(By.css('button.nav-link.disabled'));
|
||||
expect(disabledItems.length).toBe(inaccesiblePages.length);
|
||||
});
|
||||
});
|
||||
});
|
@@ -1,12 +1,13 @@
|
||||
import { fadeIn, fadeInOut } from '../../shared/animations/fade';
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ChangeDetectionStrategy, Component, Injector, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, CanActivate, Route, Router } from '@angular/router';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { Observable } from 'rxjs';
|
||||
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
import { getItemPageRoute } from '../item-page-routing-paths';
|
||||
import { GenericConstructor } from '../../core/shared/generic-constructor';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-edit-item-page',
|
||||
@@ -35,9 +36,9 @@ export class EditItemPageComponent implements OnInit {
|
||||
/**
|
||||
* All possible page outlet strings
|
||||
*/
|
||||
pages: string[];
|
||||
pages: { page: string, enabled: Observable<boolean> }[];
|
||||
|
||||
constructor(private route: ActivatedRoute, private router: Router) {
|
||||
constructor(private route: ActivatedRoute, private router: Router, private injector: Injector) {
|
||||
this.router.events.subscribe(() => {
|
||||
this.currentPage = this.route.snapshot.firstChild.routeConfig.path;
|
||||
});
|
||||
@@ -45,8 +46,20 @@ export class EditItemPageComponent implements OnInit {
|
||||
|
||||
ngOnInit(): void {
|
||||
this.pages = this.route.routeConfig.children
|
||||
.map((child: any) => child.path)
|
||||
.filter((path: string) => isNotEmpty(path)); // ignore reroutes
|
||||
.filter((child: Route) => isNotEmpty(child.path))
|
||||
.map((child: Route) => {
|
||||
let enabled = observableOf(true);
|
||||
if (isNotEmpty(child.canActivate)) {
|
||||
enabled = observableCombineLatest(child.canActivate.map((guardConstructor: GenericConstructor<CanActivate>) => {
|
||||
const guard: CanActivate = this.injector.get<CanActivate>(guardConstructor);
|
||||
return guard.canActivate(this.route.snapshot, this.router.routerState.snapshot);
|
||||
})
|
||||
).pipe(
|
||||
map((canActivateOutcomes: any[]) => canActivateOutcomes.every((e) => e === true))
|
||||
);
|
||||
}
|
||||
return { page: child.path, enabled: enabled };
|
||||
}); // ignore reroutes
|
||||
this.itemRD$ = this.route.data.pipe(map((data) => data.dso));
|
||||
}
|
||||
|
||||
|
@@ -22,15 +22,17 @@ import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit
|
||||
import { I18nBreadcrumbsService } from '../../core/breadcrumbs/i18n-breadcrumbs.service';
|
||||
import {
|
||||
ITEM_EDIT_AUTHORIZATIONS_PATH,
|
||||
ITEM_EDIT_MOVE_PATH,
|
||||
ITEM_EDIT_DELETE_PATH,
|
||||
ITEM_EDIT_PUBLIC_PATH,
|
||||
ITEM_EDIT_MOVE_PATH,
|
||||
ITEM_EDIT_PRIVATE_PATH,
|
||||
ITEM_EDIT_PUBLIC_PATH,
|
||||
ITEM_EDIT_REINSTATE_PATH,
|
||||
ITEM_EDIT_WITHDRAW_PATH
|
||||
} from './edit-item-page.routing-paths';
|
||||
import { ItemPageReinstateGuard } from './item-page-reinstate.guard';
|
||||
import { ItemPageWithdrawGuard } from './item-page-withdraw.guard';
|
||||
import { ItemPageEditMetadataGuard } from '../item-page-edit-metadata.guard';
|
||||
import { ItemPageAdministratorGuard } from '../item-page-administrator.guard';
|
||||
|
||||
/**
|
||||
* Routing module that handles the routing for the Edit Item page administrator functionality
|
||||
@@ -57,22 +59,26 @@ import { ItemPageWithdrawGuard } from './item-page-withdraw.guard';
|
||||
{
|
||||
path: 'status',
|
||||
component: ItemStatusComponent,
|
||||
data: { title: 'item.edit.tabs.status.title', showBreadcrumbs: true }
|
||||
data: { title: 'item.edit.tabs.status.title', showBreadcrumbs: true },
|
||||
canActivate: [ItemPageAdministratorGuard]
|
||||
},
|
||||
{
|
||||
path: 'bitstreams',
|
||||
component: ItemBitstreamsComponent,
|
||||
data: { title: 'item.edit.tabs.bitstreams.title', showBreadcrumbs: true }
|
||||
data: { title: 'item.edit.tabs.bitstreams.title', showBreadcrumbs: true },
|
||||
canActivate: [ItemPageAdministratorGuard]
|
||||
},
|
||||
{
|
||||
path: 'metadata',
|
||||
component: ItemMetadataComponent,
|
||||
data: { title: 'item.edit.tabs.metadata.title', showBreadcrumbs: true }
|
||||
data: { title: 'item.edit.tabs.metadata.title', showBreadcrumbs: true },
|
||||
canActivate: [ItemPageEditMetadataGuard]
|
||||
},
|
||||
{
|
||||
path: 'relationships',
|
||||
component: ItemRelationshipsComponent,
|
||||
data: { title: 'item.edit.tabs.relationships.title', showBreadcrumbs: true }
|
||||
data: { title: 'item.edit.tabs.relationships.title', showBreadcrumbs: true },
|
||||
canActivate: [ItemPageEditMetadataGuard]
|
||||
},
|
||||
/* TODO - uncomment & fix when view page exists
|
||||
{
|
||||
@@ -89,12 +95,14 @@ import { ItemPageWithdrawGuard } from './item-page-withdraw.guard';
|
||||
{
|
||||
path: 'versionhistory',
|
||||
component: ItemVersionHistoryComponent,
|
||||
data: { title: 'item.edit.tabs.versionhistory.title', showBreadcrumbs: true }
|
||||
data: { title: 'item.edit.tabs.versionhistory.title', showBreadcrumbs: true },
|
||||
canActivate: [ItemPageAdministratorGuard]
|
||||
},
|
||||
{
|
||||
path: 'mapper',
|
||||
component: ItemCollectionMapperComponent,
|
||||
data: { title: 'item.edit.tabs.item-mapper.title', showBreadcrumbs: true }
|
||||
data: { title: 'item.edit.tabs.item-mapper.title', showBreadcrumbs: true },
|
||||
canActivate: [ItemPageAdministratorGuard]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -165,7 +173,9 @@ import { ItemPageWithdrawGuard } from './item-page-withdraw.guard';
|
||||
ResourcePolicyResolver,
|
||||
ResourcePolicyTargetResolver,
|
||||
ItemPageReinstateGuard,
|
||||
ItemPageWithdrawGuard
|
||||
ItemPageWithdrawGuard,
|
||||
ItemPageAdministratorGuard,
|
||||
ItemPageEditMetadataGuard,
|
||||
]
|
||||
})
|
||||
export class EditItemPageRoutingModule {
|
||||
|
@@ -18,9 +18,8 @@ import { hasValue } from '../../shared/empty.util';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
|
||||
/**
|
||||
* This component renders a simple item page.
|
||||
* This component renders a full item page.
|
||||
* The route parameter 'id' is used to request the item it represents.
|
||||
* All fields of the item that should be displayed, are defined in its template.
|
||||
*/
|
||||
|
||||
@Component({
|
||||
|
25
src/app/+item-page/full/themed-full-item-page.component.ts
Normal file
25
src/app/+item-page/full/themed-full-item-page.component.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ThemedComponent } from '../../shared/theme-support/themed.component';
|
||||
import { FullItemPageComponent } from './full-item-page.component';
|
||||
|
||||
/**
|
||||
* Themed wrapper for FullItemPageComponent
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-themed-full-item-page',
|
||||
styleUrls: [],
|
||||
templateUrl: './../../shared/theme-support/themed.component.html',
|
||||
})
|
||||
export class ThemedFullItemPageComponent extends ThemedComponent<FullItemPageComponent> {
|
||||
protected getComponentName(): string {
|
||||
return 'FullItemPageComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../../themes/${themeName}/app/+item-page/full/full-item-page.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import(`./full-item-page.component`);
|
||||
}
|
||||
}
|
31
src/app/+item-page/item-page-edit-metadata.guard.ts
Normal file
31
src/app/+item-page/item-page-edit-metadata.guard.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
||||
import { ItemPageResolver } from './item-page.resolver';
|
||||
import { Item } from '../core/shared/item.model';
|
||||
import { DsoPageFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard';
|
||||
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 edit metadata rights
|
||||
*/
|
||||
export class ItemPageEditMetadataGuard extends DsoPageFeatureGuard<Item> {
|
||||
constructor(protected resolver: ItemPageResolver,
|
||||
protected authorizationService: AuthorizationDataService,
|
||||
protected router: Router,
|
||||
protected authService: AuthService) {
|
||||
super(resolver, authorizationService, router, authService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check edit metadata authorization rights
|
||||
*/
|
||||
getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
||||
return observableOf(FeatureID.CanEditMetadata);
|
||||
}
|
||||
}
|
@@ -1,18 +1,17 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { ItemPageComponent } from './simple/item-page.component';
|
||||
import { FullItemPageComponent } from './full/full-item-page.component';
|
||||
import { ItemPageResolver } from './item-page.resolver';
|
||||
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
||||
import { ItemBreadcrumbResolver } from '../core/breadcrumbs/item-breadcrumb.resolver';
|
||||
import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service';
|
||||
import { LinkService } from '../core/cache/builders/link.service';
|
||||
import { UploadBitstreamComponent } from './bitstreams/upload/upload-bitstream.component';
|
||||
import { UPLOAD_BITSTREAM_PATH, ITEM_EDIT_PATH } from './item-page-routing-paths';
|
||||
import { ITEM_EDIT_PATH, UPLOAD_BITSTREAM_PATH } from './item-page-routing-paths';
|
||||
import { ItemPageAdministratorGuard } from './item-page-administrator.guard';
|
||||
import { MenuItemType } from '../shared/menu/initial-menus-state';
|
||||
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
|
||||
import { ThemedItemPageComponent } from './simple/themed-item-page.component';
|
||||
import { ThemedFullItemPageComponent } from './full/themed-full-item-page.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -27,18 +26,17 @@ import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: ItemPageComponent,
|
||||
component: ThemedItemPageComponent,
|
||||
pathMatch: 'full',
|
||||
},
|
||||
{
|
||||
path: 'full',
|
||||
component: FullItemPageComponent,
|
||||
component: ThemedFullItemPageComponent,
|
||||
},
|
||||
{
|
||||
path: ITEM_EDIT_PATH,
|
||||
loadChildren: () => import('./edit-item-page/edit-item-page.module')
|
||||
.then((m) => m.EditItemPageModule),
|
||||
canActivate: [ItemPageAdministratorGuard]
|
||||
},
|
||||
{
|
||||
path: UPLOAD_BITSTREAM_PATH,
|
||||
@@ -68,7 +66,7 @@ import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
|
||||
ItemBreadcrumbResolver,
|
||||
DSOBreadcrumbsService,
|
||||
LinkService,
|
||||
ItemPageAdministratorGuard
|
||||
ItemPageAdministratorGuard,
|
||||
]
|
||||
|
||||
})
|
||||
|
@@ -25,6 +25,8 @@ import { AbstractIncrementalListComponent } from './simple/abstract-incremental-
|
||||
import { UntypedItemComponent } from './simple/item-types/untyped-item/untyped-item.component';
|
||||
import { JournalEntitiesModule } from '../entity-groups/journal-entities/journal-entities.module';
|
||||
import { ResearchEntitiesModule } from '../entity-groups/research-entities/research-entities.module';
|
||||
import { ThemedItemPageComponent } from './simple/themed-item-page.component';
|
||||
import { ThemedFullItemPageComponent } from './full/themed-full-item-page.component';
|
||||
|
||||
const ENTRY_COMPONENTS = [
|
||||
// put only entry components that use custom decorator
|
||||
@@ -34,7 +36,9 @@ const ENTRY_COMPONENTS = [
|
||||
|
||||
const DECLARATIONS = [
|
||||
ItemPageComponent,
|
||||
ThemedItemPageComponent,
|
||||
FullItemPageComponent,
|
||||
ThemedFullItemPageComponent,
|
||||
MetadataUriValuesComponent,
|
||||
ItemPageAuthorFieldComponent,
|
||||
ItemPageDateFieldComponent,
|
||||
|
27
src/app/+item-page/simple/themed-item-page.component.ts
Normal file
27
src/app/+item-page/simple/themed-item-page.component.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ThemedComponent } from '../../shared/theme-support/themed.component';
|
||||
import { ItemPageComponent } from './item-page.component';
|
||||
|
||||
/**
|
||||
* Themed wrapper for ItemPageComponent
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-themed-item-page',
|
||||
styleUrls: [],
|
||||
templateUrl: './../../shared/theme-support/themed.component.html',
|
||||
})
|
||||
|
||||
export class ThemedItemPageComponent extends ThemedComponent<ItemPageComponent> {
|
||||
protected getComponentName(): string {
|
||||
return 'ItemPageComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../../themes/${themeName}/app/+item-page/simple/item-page.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import(`./item-page.component`);
|
||||
}
|
||||
|
||||
}
|
@@ -1,14 +1,13 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { LoginPageComponent } from './login-page.component';
|
||||
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||
import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service';
|
||||
import { ThemedLoginPageComponent } from './themed-login-page.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild([
|
||||
{ path: '', pathMatch: 'full', component: LoginPageComponent, resolve: { breadcrumb: I18nBreadcrumbResolver }, data: { breadcrumbKey: 'login', title: 'login.title' } }
|
||||
{ path: '', pathMatch: 'full', component: ThemedLoginPageComponent, resolve: { breadcrumb: I18nBreadcrumbResolver }, data: { breadcrumbKey: 'login', title: 'login.title' } }
|
||||
])
|
||||
],
|
||||
providers: [
|
||||
|
@@ -3,6 +3,7 @@ import { NgModule } from '@angular/core';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { LoginPageComponent } from './login-page.component';
|
||||
import { LoginPageRoutingModule } from './login-page-routing.module';
|
||||
import { ThemedLoginPageComponent } from './themed-login-page.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -11,7 +12,8 @@ import { LoginPageRoutingModule } from './login-page-routing.module';
|
||||
SharedModule,
|
||||
],
|
||||
declarations: [
|
||||
LoginPageComponent
|
||||
LoginPageComponent,
|
||||
ThemedLoginPageComponent
|
||||
]
|
||||
})
|
||||
export class LoginPageModule {
|
||||
|
25
src/app/+login-page/themed-login-page.component.ts
Normal file
25
src/app/+login-page/themed-login-page.component.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ThemedComponent } from '../shared/theme-support/themed.component';
|
||||
import { LoginPageComponent } from './login-page.component';
|
||||
|
||||
/**
|
||||
* Themed wrapper for LoginPageComponent
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-themed-login-page',
|
||||
styleUrls: [],
|
||||
templateUrl: './../shared/theme-support/themed.component.html'
|
||||
})
|
||||
export class ThemedLoginPageComponent extends ThemedComponent<LoginPageComponent> {
|
||||
protected getComponentName(): string {
|
||||
return 'LoginPageComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../themes/${themeName}/app/+login-page/login-page.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import(`./login-page.component`);
|
||||
}
|
||||
}
|
@@ -1,8 +1,7 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { LogoutPageComponent } from './logout-page.component';
|
||||
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
||||
import { ThemedLogoutPageComponent } from './themed-logout-page.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -10,7 +9,7 @@ import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
||||
{
|
||||
canActivate: [AuthenticatedGuard],
|
||||
path: '',
|
||||
component: LogoutPageComponent,
|
||||
component: ThemedLogoutPageComponent,
|
||||
data: { title: 'logout.title' }
|
||||
}
|
||||
])
|
||||
|
@@ -3,6 +3,7 @@ import { NgModule } from '@angular/core';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { LogoutPageComponent } from './logout-page.component';
|
||||
import { LogoutPageRoutingModule } from './logout-page-routing.module';
|
||||
import { ThemedLogoutPageComponent } from './themed-logout-page.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -11,7 +12,8 @@ import { LogoutPageRoutingModule } from './logout-page-routing.module';
|
||||
SharedModule,
|
||||
],
|
||||
declarations: [
|
||||
LogoutPageComponent
|
||||
LogoutPageComponent,
|
||||
ThemedLogoutPageComponent
|
||||
]
|
||||
})
|
||||
export class LogoutPageModule {
|
||||
|
25
src/app/+logout-page/themed-logout-page.component.ts
Normal file
25
src/app/+logout-page/themed-logout-page.component.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ThemedComponent } from '../shared/theme-support/themed.component';
|
||||
import { LogoutPageComponent } from './logout-page.component';
|
||||
|
||||
/**
|
||||
* Themed wrapper for LogoutPageComponent
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-themed-logout-page',
|
||||
styleUrls: [],
|
||||
templateUrl: './../shared/theme-support/themed.component.html'
|
||||
})
|
||||
export class ThemedLogoutPageComponent extends ThemedComponent<LogoutPageComponent> {
|
||||
protected getComponentName(): string {
|
||||
return 'LogoutPageComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../themes/${themeName}/app/+logout-page/logout-page.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import(`./logout-page.component`);
|
||||
}
|
||||
}
|
@@ -1,8 +1,8 @@
|
||||
import { LookupGuard } from './lookup-guard';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, UrlSegment } from '@angular/router';
|
||||
import { ObjectNotFoundComponent } from './objectnotfound/objectnotfound.component';
|
||||
import { isNotEmpty } from '../shared/empty.util';
|
||||
import { ThemedObjectNotFoundComponent } from './objectnotfound/themed-objectnotfound.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -10,7 +10,7 @@ import { isNotEmpty } from '../shared/empty.util';
|
||||
{
|
||||
matcher: urlMatcher,
|
||||
canActivate: [LookupGuard],
|
||||
component: ObjectNotFoundComponent }
|
||||
component: ThemedObjectNotFoundComponent }
|
||||
])
|
||||
],
|
||||
providers: [
|
||||
|
@@ -4,6 +4,7 @@ import { SharedModule } from '../shared/shared.module';
|
||||
import { LookupRoutingModule } from './lookup-by-id-routing.module';
|
||||
import { ObjectNotFoundComponent } from './objectnotfound/objectnotfound.component';
|
||||
import { DsoRedirectDataService } from '../core/data/dso-redirect-data.service';
|
||||
import { ThemedObjectNotFoundComponent } from './objectnotfound/themed-objectnotfound.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -12,7 +13,8 @@ import { DsoRedirectDataService } from '../core/data/dso-redirect-data.service';
|
||||
SharedModule,
|
||||
],
|
||||
declarations: [
|
||||
ObjectNotFoundComponent
|
||||
ObjectNotFoundComponent,
|
||||
ThemedObjectNotFoundComponent
|
||||
],
|
||||
providers: [
|
||||
DsoRedirectDataService
|
||||
|
@@ -0,0 +1,26 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ThemedComponent } from '../../shared/theme-support/themed.component';
|
||||
import { ObjectNotFoundComponent } from './objectnotfound.component';
|
||||
|
||||
/**
|
||||
* Themed wrapper for ObjectNotFoundComponent
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-themed-objnotfound',
|
||||
styleUrls: [],
|
||||
templateUrl: '../../shared/theme-support/themed.component.html',
|
||||
})
|
||||
export class ThemedObjectNotFoundComponent extends ThemedComponent<ObjectNotFoundComponent> {
|
||||
protected getComponentName(): string {
|
||||
return 'ObjectNotFoundComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../../themes/${themeName}/app/+lookup-by-id/objectnotfound/objectnotfound.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import(`./objectnotfound.component`);
|
||||
}
|
||||
|
||||
}
|
@@ -1,15 +1,14 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { MyDSpacePageComponent } from './my-dspace-page.component';
|
||||
import { MyDSpaceGuard } from './my-dspace.guard';
|
||||
import { ThemedMyDSpacePageComponent } from './themed-my-dspace-page.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild([
|
||||
{
|
||||
path: '',
|
||||
component: MyDSpacePageComponent,
|
||||
component: ThemedMyDSpacePageComponent,
|
||||
data: { title: 'mydspace.title' },
|
||||
canActivate: [
|
||||
MyDSpaceGuard
|
||||
|
@@ -11,6 +11,15 @@ import { MyDSpaceGuard } from './my-dspace.guard';
|
||||
import { MyDSpaceConfigurationService } from './my-dspace-configuration.service';
|
||||
import { CollectionSelectorComponent } from './collection-selector/collection-selector.component';
|
||||
import { MyDspaceSearchModule } from './my-dspace-search.module';
|
||||
import { ThemedMyDSpacePageComponent } from './themed-my-dspace-page.component';
|
||||
|
||||
const DECLARATIONS = [
|
||||
MyDSpacePageComponent,
|
||||
ThemedMyDSpacePageComponent,
|
||||
MyDSpaceResultsComponent,
|
||||
MyDSpaceNewSubmissionComponent,
|
||||
CollectionSelectorComponent
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -19,16 +28,12 @@ import { MyDspaceSearchModule } from './my-dspace-search.module';
|
||||
MyDspacePageRoutingModule,
|
||||
MyDspaceSearchModule.withEntryComponents()
|
||||
],
|
||||
declarations: [
|
||||
MyDSpacePageComponent,
|
||||
MyDSpaceResultsComponent,
|
||||
MyDSpaceNewSubmissionComponent,
|
||||
CollectionSelectorComponent
|
||||
],
|
||||
declarations: DECLARATIONS,
|
||||
providers: [
|
||||
MyDSpaceGuard,
|
||||
MyDSpaceConfigurationService
|
||||
]
|
||||
],
|
||||
exports: DECLARATIONS,
|
||||
})
|
||||
|
||||
/**
|
||||
|
27
src/app/+my-dspace-page/themed-my-dspace-page.component.ts
Normal file
27
src/app/+my-dspace-page/themed-my-dspace-page.component.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ThemedComponent } from '../shared/theme-support/themed.component';
|
||||
import { MyDSpacePageComponent } from './my-dspace-page.component';
|
||||
|
||||
/**
|
||||
* Themed wrapper for MyDSpacePageComponent
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-themed-my-dspace-page',
|
||||
styleUrls: [],
|
||||
templateUrl: './../shared/theme-support/themed.component.html'
|
||||
})
|
||||
export class ThemedMyDSpacePageComponent extends ThemedComponent<MyDSpacePageComponent> {
|
||||
protected inAndOutputNames: (keyof MyDSpacePageComponent & keyof this)[];
|
||||
|
||||
protected getComponentName(): string {
|
||||
return 'MyDSpacePageComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../themes/${themeName}/app/+my-dspace-page/my-dspace-page.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import(`./my-dspace-page.component`);
|
||||
}
|
||||
}
|
@@ -2,11 +2,11 @@ import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { ConfigurationSearchPageGuard } from './configuration-search-page.guard';
|
||||
import { ConfigurationSearchPageComponent } from './configuration-search-page.component';
|
||||
import { SearchPageComponent } from './search-page.component';
|
||||
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||
import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service';
|
||||
import { SearchPageModule } from './search-page.module';
|
||||
import { ThemedSearchPageComponent } from './themed-search-page.component';
|
||||
import { ThemedConfigurationSearchPageComponent } from './themed-configuration-search-page.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -15,8 +15,8 @@ import { SearchPageModule } from './search-page.module';
|
||||
path: '',
|
||||
resolve: { breadcrumb: I18nBreadcrumbResolver }, data: { title: 'search.title', breadcrumbKey: 'search' },
|
||||
children: [
|
||||
{ path: '', component: SearchPageComponent },
|
||||
{ path: ':configuration', component: ConfigurationSearchPageComponent, canActivate: [ConfigurationSearchPageGuard] }
|
||||
{ path: '', component: ThemedSearchPageComponent },
|
||||
{ path: ':configuration', component: ThemedConfigurationSearchPageComponent, canActivate: [ConfigurationSearchPageGuard] }
|
||||
]
|
||||
}]
|
||||
)
|
||||
|
@@ -13,11 +13,13 @@ import { SearchFilterService } from '../core/shared/search/search-filter.service
|
||||
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
|
||||
import { JournalEntitiesModule } from '../entity-groups/journal-entities/journal-entities.module';
|
||||
import { ResearchEntitiesModule } from '../entity-groups/research-entities/research-entities.module';
|
||||
import { ThemedSearchPageComponent } from './themed-search-page.component';
|
||||
|
||||
const components = [
|
||||
SearchPageComponent,
|
||||
SearchComponent,
|
||||
SearchTrackerComponent
|
||||
SearchTrackerComponent,
|
||||
ThemedSearchPageComponent
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@@ -0,0 +1,72 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { ThemedComponent } from '../shared/theme-support/themed.component';
|
||||
import { ConfigurationSearchPageComponent } from './configuration-search-page.component';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Context } from '../core/shared/context.model';
|
||||
|
||||
/**
|
||||
* Themed wrapper for ConfigurationSearchPageComponent
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-themed-configuration-search-page',
|
||||
styleUrls: [],
|
||||
templateUrl: '../shared/theme-support/themed.component.html',
|
||||
})
|
||||
export class ThemedConfigurationSearchPageComponent extends ThemedComponent<ConfigurationSearchPageComponent> {
|
||||
/**
|
||||
* The configuration to use for the search options
|
||||
* If empty, the configuration will be determined by the route parameter called 'configuration'
|
||||
*/
|
||||
@Input() configuration: string;
|
||||
|
||||
/**
|
||||
* The actual query for the fixed filter.
|
||||
* If empty, the query will be determined by the route parameter called 'filter'
|
||||
*/
|
||||
@Input() fixedFilterQuery: string;
|
||||
|
||||
/**
|
||||
* True when the search component should show results on the current page
|
||||
*/
|
||||
@Input() inPlaceSearch = true;
|
||||
|
||||
/**
|
||||
* Whether or not the search bar should be visible
|
||||
*/
|
||||
@Input()
|
||||
searchEnabled = true;
|
||||
|
||||
/**
|
||||
* The width of the sidebar (bootstrap columns)
|
||||
*/
|
||||
@Input()
|
||||
sideBarWidth = 3;
|
||||
|
||||
/**
|
||||
* The currently applied configuration (determines title of search)
|
||||
*/
|
||||
@Input()
|
||||
configuration$: Observable<string>;
|
||||
|
||||
/**
|
||||
* The current context
|
||||
*/
|
||||
@Input()
|
||||
context: Context;
|
||||
|
||||
protected inAndOutputNames: (keyof ConfigurationSearchPageComponent & keyof this)[] =
|
||||
['configuration', 'fixedFilterQuery', 'inPlaceSearch', 'searchEnabled', 'sideBarWidth', 'configuration$', 'context'];
|
||||
|
||||
protected getComponentName(): string {
|
||||
return 'ConfigurationSearchPageComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../themes/${themeName}/app/+search-page/configuration-search-page.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import(`./configuration-search-page.component`);
|
||||
}
|
||||
|
||||
}
|
26
src/app/+search-page/themed-search-page.component.ts
Normal file
26
src/app/+search-page/themed-search-page.component.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ThemedComponent } from '../shared/theme-support/themed.component';
|
||||
import { SearchPageComponent } from './search-page.component';
|
||||
|
||||
/**
|
||||
* Themed wrapper for SearchPageComponent
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-themed-search-page',
|
||||
styleUrls: [],
|
||||
templateUrl: '../shared/theme-support/themed.component.html',
|
||||
})
|
||||
export class ThemedSearchPageComponent extends ThemedComponent<SearchPageComponent> {
|
||||
|
||||
protected getComponentName(): string {
|
||||
return 'SearchPageComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../themes/${themeName}/app/+search-page/search-page.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import(`./search-page.component`);
|
||||
}
|
||||
}
|
@@ -2,7 +2,7 @@ import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
||||
import { SubmissionSubmitComponent } from '../submission/submit/submission-submit.component';
|
||||
import { ThemedSubmissionSubmitComponent } from '../submission/submit/themed-submission-submit.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -11,7 +11,7 @@ import { SubmissionSubmitComponent } from '../submission/submit/submission-submi
|
||||
canActivate: [AuthenticatedGuard],
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
component: SubmissionSubmitComponent,
|
||||
component: ThemedSubmissionSubmitComponent,
|
||||
data: { title: 'submission.submit.title' }
|
||||
}
|
||||
])
|
||||
|
@@ -0,0 +1,26 @@
|
||||
import { WorkflowItemDeleteComponent } from './workflow-item-delete.component';
|
||||
import { ThemedComponent } from '../../shared/theme-support/themed.component';
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
/**
|
||||
* Themed wrapper for WorkflowItemDeleteComponent
|
||||
*/
|
||||
|
||||
@Component({
|
||||
selector: 'ds-themed-workflow-item-delete',
|
||||
styleUrls: [],
|
||||
templateUrl: './../../shared/theme-support/themed.component.html'
|
||||
})
|
||||
export class ThemedWorkflowItemDeleteComponent extends ThemedComponent<WorkflowItemDeleteComponent> {
|
||||
protected getComponentName(): string {
|
||||
return 'WorkflowItemDeleteComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../../themes/${themeName}/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import(`./workflow-item-delete.component`);
|
||||
}
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
import { ThemedComponent } from '../../shared/theme-support/themed.component';
|
||||
import { Component } from '@angular/core';
|
||||
import { WorkflowItemSendBackComponent } from './workflow-item-send-back.component';
|
||||
|
||||
/**
|
||||
* Themed wrapper for WorkflowItemActionPageComponent
|
||||
*/
|
||||
|
||||
@Component({
|
||||
selector: 'ds-themed-workflow-item-send-back',
|
||||
styleUrls: [],
|
||||
templateUrl: './../../shared/theme-support/themed.component.html'
|
||||
})
|
||||
export class ThemedWorkflowItemSendBackComponent extends ThemedComponent<WorkflowItemSendBackComponent> {
|
||||
protected getComponentName(): string {
|
||||
return 'WorkflowItemSendBackComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../../themes/${themeName}/app/+workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import(`./workflow-item-send-back.component`);
|
||||
}
|
||||
}
|
@@ -2,15 +2,11 @@ import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
||||
import { SubmissionEditComponent } from '../submission/edit/submission-edit.component';
|
||||
import { WorkflowItemDeleteComponent } from './workflow-item-delete/workflow-item-delete.component';
|
||||
import { WorkflowItemPageResolver } from './workflow-item-page.resolver';
|
||||
import { WorkflowItemSendBackComponent } from './workflow-item-send-back/workflow-item-send-back.component';
|
||||
import {
|
||||
WORKFLOW_ITEM_SEND_BACK_PATH,
|
||||
WORKFLOW_ITEM_DELETE_PATH,
|
||||
WORKFLOW_ITEM_EDIT_PATH
|
||||
} from './workflowitems-edit-page-routing-paths';
|
||||
import { WORKFLOW_ITEM_DELETE_PATH, WORKFLOW_ITEM_EDIT_PATH, WORKFLOW_ITEM_SEND_BACK_PATH } from './workflowitems-edit-page-routing-paths';
|
||||
import { ThemedSubmissionEditComponent } from '../submission/edit/themed-submission-edit.component';
|
||||
import { ThemedWorkflowItemDeleteComponent } from './workflow-item-delete/themed-workflow-item-delete.component';
|
||||
import { ThemedWorkflowItemSendBackComponent } from './workflow-item-send-back/themed-workflow-item-send-back.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -22,19 +18,19 @@ import {
|
||||
{
|
||||
canActivate: [AuthenticatedGuard],
|
||||
path: WORKFLOW_ITEM_EDIT_PATH,
|
||||
component: SubmissionEditComponent,
|
||||
component: ThemedSubmissionEditComponent,
|
||||
data: { title: 'submission.edit.title' }
|
||||
},
|
||||
{
|
||||
canActivate: [AuthenticatedGuard],
|
||||
path: WORKFLOW_ITEM_DELETE_PATH,
|
||||
component: WorkflowItemDeleteComponent,
|
||||
component: ThemedWorkflowItemDeleteComponent,
|
||||
data: { title: 'workflow-item.delete.title' }
|
||||
},
|
||||
{
|
||||
canActivate: [AuthenticatedGuard],
|
||||
path: WORKFLOW_ITEM_SEND_BACK_PATH,
|
||||
component: WorkflowItemSendBackComponent,
|
||||
component: ThemedWorkflowItemSendBackComponent,
|
||||
data: { title: 'workflow-item.send-back.title' }
|
||||
}
|
||||
]
|
||||
|
@@ -5,6 +5,8 @@ import { WorkflowItemsEditPageRoutingModule } from './workflowitems-edit-page-ro
|
||||
import { SubmissionModule } from '../submission/submission.module';
|
||||
import { WorkflowItemDeleteComponent } from './workflow-item-delete/workflow-item-delete.component';
|
||||
import { WorkflowItemSendBackComponent } from './workflow-item-send-back/workflow-item-send-back.component';
|
||||
import { ThemedWorkflowItemDeleteComponent } from './workflow-item-delete/themed-workflow-item-delete.component';
|
||||
import { ThemedWorkflowItemSendBackComponent } from './workflow-item-send-back/themed-workflow-item-send-back.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -13,7 +15,7 @@ import { WorkflowItemSendBackComponent } from './workflow-item-send-back/workflo
|
||||
SharedModule,
|
||||
SubmissionModule,
|
||||
],
|
||||
declarations: [WorkflowItemDeleteComponent, WorkflowItemSendBackComponent]
|
||||
declarations: [WorkflowItemDeleteComponent, ThemedWorkflowItemDeleteComponent, WorkflowItemSendBackComponent, ThemedWorkflowItemSendBackComponent]
|
||||
})
|
||||
/**
|
||||
* This module handles all modules that need to access the workflowitems edit page.
|
||||
|
@@ -2,7 +2,7 @@ import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
||||
import { SubmissionEditComponent } from '../submission/edit/submission-edit.component';
|
||||
import { ThemedSubmissionEditComponent } from '../submission/edit/themed-submission-edit.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -11,7 +11,7 @@ import { SubmissionEditComponent } from '../submission/edit/submission-edit.comp
|
||||
{
|
||||
canActivate: [AuthenticatedGuard],
|
||||
path: ':id/edit',
|
||||
component: SubmissionEditComponent,
|
||||
component: ThemedSubmissionEditComponent,
|
||||
data: { title: 'submission.edit.title' }
|
||||
}
|
||||
])
|
||||
|
@@ -2,7 +2,6 @@ import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { AuthBlockingGuard } from './core/auth/auth-blocking.guard';
|
||||
|
||||
import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
|
||||
import { AuthenticatedGuard } from './core/auth/authenticated.guard';
|
||||
import { SiteAdministratorGuard } from './core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
|
||||
import {
|
||||
@@ -23,7 +22,8 @@ import { PROCESS_MODULE_PATH } from './process-page/process-page-routing.paths';
|
||||
import { ReloadGuard } from './core/reload/reload.guard';
|
||||
import { EndUserAgreementCurrentUserGuard } from './core/end-user-agreement/end-user-agreement-current-user.guard';
|
||||
import { SiteRegisterGuard } from './core/data/feature-authorization/feature-authorization-guard/site-register.guard';
|
||||
import { ForbiddenComponent } from './forbidden/forbidden.component';
|
||||
import { ThemedPageNotFoundComponent } from './pagenotfound/themed-pagenotfound.component';
|
||||
import { ThemedForbiddenComponent } from './forbidden/themed-forbidden.component';
|
||||
import { GroupAdministratorGuard } from './core/data/feature-authorization/feature-authorization-guard/group-administrator.guard';
|
||||
|
||||
@NgModule({
|
||||
@@ -32,7 +32,7 @@ import { GroupAdministratorGuard } from './core/data/feature-authorization/featu
|
||||
path: '', canActivate: [AuthBlockingGuard],
|
||||
children: [
|
||||
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
||||
{ path: 'reload/:rnd', component: PageNotFoundComponent, pathMatch: 'full', canActivate: [ReloadGuard] },
|
||||
{ path: 'reload/:rnd', component: ThemedPageNotFoundComponent, pathMatch: 'full', canActivate: [ReloadGuard] },
|
||||
{
|
||||
path: 'home',
|
||||
loadChildren: () => import('./+home-page/home-page.module')
|
||||
@@ -175,7 +175,7 @@ import { GroupAdministratorGuard } from './core/data/feature-authorization/featu
|
||||
},
|
||||
{
|
||||
path: FORBIDDEN_PATH,
|
||||
component: ForbiddenComponent
|
||||
component: ThemedForbiddenComponent
|
||||
},
|
||||
{
|
||||
path: 'statistics',
|
||||
@@ -187,7 +187,7 @@ import { GroupAdministratorGuard } from './core/data/feature-authorization/featu
|
||||
loadChildren: () => import('./access-control/access-control.module').then((m) => m.AccessControlModule),
|
||||
canActivate: [GroupAdministratorGuard],
|
||||
},
|
||||
{ path: '**', pathMatch: 'full', component: PageNotFoundComponent },
|
||||
{ path: '**', pathMatch: 'full', component: ThemedPageNotFoundComponent },
|
||||
]}
|
||||
],{
|
||||
onSameUrlNavigation: 'reload',
|
||||
|
@@ -35,6 +35,7 @@ import { provideMockStore } from '@ngrx/store/testing';
|
||||
import { GoogleAnalyticsService } from './statistics/google-analytics.service';
|
||||
import { ThemeService } from './shared/theme-support/theme.service';
|
||||
import { getMockThemeService } from './shared/mocks/theme-service.mock';
|
||||
import { BreadcrumbsService } from './breadcrumbs/breadcrumbs.service';
|
||||
|
||||
let comp: AppComponent;
|
||||
let fixture: ComponentFixture<AppComponent>;
|
||||
@@ -45,47 +46,54 @@ const initialState = {
|
||||
|
||||
describe('App component', () => {
|
||||
|
||||
let breadcrumbsServiceSpy;
|
||||
|
||||
function getMockLocaleService(): LocaleService {
|
||||
return jasmine.createSpyObj('LocaleService', {
|
||||
setCurrentLanguageCode: jasmine.createSpy('setCurrentLanguageCode')
|
||||
});
|
||||
}
|
||||
|
||||
const defaultTestBedConf = {
|
||||
imports: [
|
||||
CommonModule,
|
||||
StoreModule.forRoot(authReducer, storeModuleConfig),
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
}),
|
||||
],
|
||||
declarations: [AppComponent], // declare the test component
|
||||
providers: [
|
||||
{ provide: NativeWindowService, useValue: new NativeWindowRef() },
|
||||
{ provide: MetadataService, useValue: new MetadataServiceMock() },
|
||||
{ provide: Angulartics2GoogleAnalytics, useValue: new AngularticsProviderMock() },
|
||||
{ provide: Angulartics2DSpace, useValue: new AngularticsProviderMock() },
|
||||
{ provide: AuthService, useValue: new AuthServiceMock() },
|
||||
{ provide: Router, useValue: new RouterMock() },
|
||||
{ provide: ActivatedRoute, useValue: new MockActivatedRoute() },
|
||||
{ provide: MenuService, useValue: menuService },
|
||||
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
|
||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
|
||||
{ provide: LocaleService, useValue: getMockLocaleService() },
|
||||
{ provide: ThemeService, useValue: getMockThemeService() },
|
||||
provideMockStore({ initialState }),
|
||||
AppComponent,
|
||||
RouteService
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
const getDefaultTestBedConf = () => {
|
||||
breadcrumbsServiceSpy = jasmine.createSpyObj(['listenForRouteChanges']);
|
||||
|
||||
return {
|
||||
imports: [
|
||||
CommonModule,
|
||||
StoreModule.forRoot(authReducer, storeModuleConfig),
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
}),
|
||||
],
|
||||
declarations: [AppComponent], // declare the test component
|
||||
providers: [
|
||||
{ provide: NativeWindowService, useValue: new NativeWindowRef() },
|
||||
{ provide: MetadataService, useValue: new MetadataServiceMock() },
|
||||
{ provide: Angulartics2GoogleAnalytics, useValue: new AngularticsProviderMock() },
|
||||
{ provide: Angulartics2DSpace, useValue: new AngularticsProviderMock() },
|
||||
{ provide: AuthService, useValue: new AuthServiceMock() },
|
||||
{ provide: Router, useValue: new RouterMock() },
|
||||
{ provide: ActivatedRoute, useValue: new MockActivatedRoute() },
|
||||
{ provide: MenuService, useValue: menuService },
|
||||
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
|
||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
|
||||
{ provide: LocaleService, useValue: getMockLocaleService() },
|
||||
{ provide: ThemeService, useValue: getMockThemeService() },
|
||||
{ provide: BreadcrumbsService, useValue: breadcrumbsServiceSpy },
|
||||
provideMockStore({ initialState }),
|
||||
AppComponent,
|
||||
RouteService
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
};
|
||||
};
|
||||
|
||||
// waitForAsync beforeEach
|
||||
beforeEach(waitForAsync(() => {
|
||||
return TestBed.configureTestingModule(defaultTestBedConf);
|
||||
return TestBed.configureTestingModule(getDefaultTestBedConf());
|
||||
}));
|
||||
|
||||
// synchronous beforeEach
|
||||
@@ -120,13 +128,19 @@ describe('App component', () => {
|
||||
|
||||
});
|
||||
|
||||
describe('the constructor', () => {
|
||||
it('should call breadcrumbsService.listenForRouteChanges', () => {
|
||||
expect(breadcrumbsServiceSpy.listenForRouteChanges).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when GoogleAnalyticsService is provided', () => {
|
||||
let googleAnalyticsSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
// NOTE: Cannot override providers once components have been compiled, so TestBed needs to be reset
|
||||
TestBed.resetTestingModule();
|
||||
TestBed.configureTestingModule(defaultTestBedConf);
|
||||
TestBed.configureTestingModule(getDefaultTestBedConf());
|
||||
googleAnalyticsSpy = jasmine.createSpyObj('googleAnalyticsService', [
|
||||
'addTrackingIdToPage',
|
||||
]);
|
||||
@@ -154,7 +168,7 @@ describe('App component', () => {
|
||||
beforeEach(() => {
|
||||
// NOTE: Cannot override providers once components have been compiled, so TestBed needs to be reset
|
||||
TestBed.resetTestingModule();
|
||||
TestBed.configureTestingModule(defaultTestBedConf);
|
||||
TestBed.configureTestingModule(getDefaultTestBedConf());
|
||||
TestBed.overrideProvider(ThemeService, {useValue: getMockThemeService('custom')});
|
||||
document = TestBed.inject(DOCUMENT);
|
||||
headSpy = jasmine.createSpyObj('head', ['appendChild']);
|
||||
|
@@ -36,6 +36,7 @@ import { DOCUMENT } from '@angular/common';
|
||||
import { ThemeService } from './shared/theme-support/theme.service';
|
||||
import { BASE_THEME_NAME } from './shared/theme-support/theme.constants';
|
||||
import { DEFAULT_THEME_CONFIG } from './shared/theme-support/theme.effects';
|
||||
import { BreadcrumbsService } from './breadcrumbs/breadcrumbs.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-app',
|
||||
@@ -73,6 +74,7 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||
private menuService: MenuService,
|
||||
private windowService: HostWindowService,
|
||||
private localeService: LocaleService,
|
||||
private breadcrumbsService: BreadcrumbsService,
|
||||
@Optional() private cookiesService: KlaroService,
|
||||
@Optional() private googleAnalyticsService: GoogleAnalyticsService,
|
||||
) {
|
||||
@@ -106,6 +108,7 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||
angulartics2DSpace.startTracking();
|
||||
|
||||
metadata.listenForRouteChange();
|
||||
breadcrumbsService.listenForRouteChanges();
|
||||
|
||||
if (environment.debug) {
|
||||
console.info(environment);
|
||||
|
@@ -46,6 +46,11 @@ import { XsrfInterceptor } from './core/xsrf/xsrf.interceptor';
|
||||
import { RootComponent } from './root/root.component';
|
||||
import { ThemedRootComponent } from './root/themed-root.component';
|
||||
import { ThemedEntryComponentModule } from '../themes/themed-entry-component.module';
|
||||
import { ThemedPageNotFoundComponent } from './pagenotfound/themed-pagenotfound.component';
|
||||
import { ThemedForbiddenComponent } from './forbidden/themed-forbidden.component';
|
||||
import { ThemedHeaderComponent } from './header/themed-header.component';
|
||||
import { ThemedFooterComponent } from './footer/themed-footer.component';
|
||||
import { ThemedBreadcrumbsComponent } from './breadcrumbs/themed-breadcrumbs.component';
|
||||
|
||||
export function getBase() {
|
||||
return environment.ui.nameSpace;
|
||||
@@ -127,17 +132,22 @@ const DECLARATIONS = [
|
||||
RootComponent,
|
||||
ThemedRootComponent,
|
||||
HeaderComponent,
|
||||
ThemedHeaderComponent,
|
||||
HeaderNavbarWrapperComponent,
|
||||
AdminSidebarComponent,
|
||||
AdminSidebarSectionComponent,
|
||||
ExpandableAdminSidebarSectionComponent,
|
||||
FooterComponent,
|
||||
ThemedFooterComponent,
|
||||
PageNotFoundComponent,
|
||||
ThemedPageNotFoundComponent,
|
||||
NotificationComponent,
|
||||
NotificationsBoardComponent,
|
||||
SearchNavbarComponent,
|
||||
BreadcrumbsComponent,
|
||||
ThemedBreadcrumbsComponent,
|
||||
ForbiddenComponent,
|
||||
ThemedForbiddenComponent,
|
||||
];
|
||||
|
||||
const EXPORTS = [
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { BreadcrumbsService } from '../../core/breadcrumbs/breadcrumbs.service';
|
||||
import { BreadcrumbsProviderService } from '../../core/breadcrumbs/breadcrumbsProviderService';
|
||||
|
||||
/**
|
||||
* Interface for breadcrumb configuration objects
|
||||
@@ -7,7 +7,7 @@ export interface BreadcrumbConfig<T> {
|
||||
/**
|
||||
* The service used to calculate the breadcrumb object
|
||||
*/
|
||||
provider: BreadcrumbsService<T>;
|
||||
provider: BreadcrumbsProviderService<T>;
|
||||
|
||||
/**
|
||||
* The key that is used to calculate the breadcrumb display value
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<ng-container *ngVar="(breadcrumbs$ | async) as breadcrumbs">
|
||||
<nav *ngIf="showBreadcrumbs" aria-label="breadcrumb">
|
||||
<nav *ngIf="(showBreadcrumbs$ | async)" aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<ng-container
|
||||
*ngTemplateOutlet="breadcrumbs?.length > 0 ? breadcrumb : activeBreadcrumb; context: {text: 'home.breadcrumbs', url: '/'}"></ng-container>
|
||||
|
@@ -1,115 +1,78 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { BreadcrumbsComponent } from './breadcrumbs.component';
|
||||
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { BreadcrumbsService } from './breadcrumbs.service';
|
||||
import { Breadcrumb } from './breadcrumb/breadcrumb.model';
|
||||
import { VarDirective } from '../shared/utils/var.directive';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
import { TranslateLoaderMock } from '../shared/testing/translate-loader.mock';
|
||||
import { BreadcrumbConfig } from './breadcrumb/breadcrumb-config.model';
|
||||
import { BreadcrumbsService } from '../core/breadcrumbs/breadcrumbs.service';
|
||||
import { Breadcrumb } from './breadcrumb/breadcrumb.model';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { VarDirective } from '../shared/utils/var.directive';
|
||||
import { getTestScheduler } from 'jasmine-marbles';
|
||||
|
||||
class TestBreadcrumbsService implements BreadcrumbsService<string> {
|
||||
getBreadcrumbs(key: string, url: string): Observable<Breadcrumb[]> {
|
||||
return observableOf([new Breadcrumb(key, url)]);
|
||||
}
|
||||
}
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||
import { DebugElement } from '@angular/core';
|
||||
|
||||
describe('BreadcrumbsComponent', () => {
|
||||
let component: BreadcrumbsComponent;
|
||||
let fixture: ComponentFixture<BreadcrumbsComponent>;
|
||||
let router: any;
|
||||
let route: any;
|
||||
let breadcrumbProvider;
|
||||
let breadcrumbConfigA: BreadcrumbConfig<string>;
|
||||
let breadcrumbConfigB: BreadcrumbConfig<string>;
|
||||
let expectedBreadcrumbs;
|
||||
let breadcrumbsServiceMock: BreadcrumbsService;
|
||||
|
||||
function init() {
|
||||
breadcrumbProvider = new TestBreadcrumbsService();
|
||||
const expectBreadcrumb = (listItem: DebugElement, text: string, url: string) => {
|
||||
const anchor = listItem.query(By.css('a'));
|
||||
|
||||
breadcrumbConfigA = { provider: breadcrumbProvider, key: 'example.path', url: 'example.com' };
|
||||
breadcrumbConfigB = { provider: breadcrumbProvider, key: 'another.path', url: 'another.com' };
|
||||
|
||||
route = {
|
||||
root: {
|
||||
snapshot: {
|
||||
data: { breadcrumb: breadcrumbConfigA },
|
||||
routeConfig: { resolve: { breadcrumb: {} } }
|
||||
},
|
||||
firstChild: {
|
||||
snapshot: {
|
||||
// Example without resolver should be ignored
|
||||
data: { breadcrumb: breadcrumbConfigA },
|
||||
},
|
||||
firstChild: {
|
||||
snapshot: {
|
||||
data: { breadcrumb: breadcrumbConfigB },
|
||||
routeConfig: { resolve: { breadcrumb: {} } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
expectedBreadcrumbs = [
|
||||
new Breadcrumb(breadcrumbConfigA.key, breadcrumbConfigA.url),
|
||||
new Breadcrumb(breadcrumbConfigB.key, breadcrumbConfigB.url)
|
||||
];
|
||||
|
||||
}
|
||||
if (url == null) {
|
||||
expect(anchor).toBeNull();
|
||||
expect(listItem.nativeElement.innerHTML).toEqual(text);
|
||||
} else {
|
||||
expect(anchor).toBeInstanceOf(DebugElement);
|
||||
expect(anchor.attributes.href).toEqual(url);
|
||||
expect(anchor.nativeElement.innerHTML).toEqual(text);
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
init();
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [BreadcrumbsComponent, VarDirective],
|
||||
imports: [RouterTestingModule.withRoutes([]), TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
}), NgbModule],
|
||||
providers: [
|
||||
{provide: ActivatedRoute, useValue: route}
|
||||
], schemas: [NO_ERRORS_SCHEMA]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
breadcrumbsServiceMock = {
|
||||
breadcrumbs$: observableOf([
|
||||
// NOTE: a root breadcrumb is automatically rendered
|
||||
new Breadcrumb('bc 1', 'example.com'),
|
||||
new Breadcrumb('bc 2', 'another.com'),
|
||||
]),
|
||||
showBreadcrumbs$: observableOf(true),
|
||||
} as BreadcrumbsService;
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
BreadcrumbsComponent,
|
||||
VarDirective,
|
||||
],
|
||||
imports: [
|
||||
RouterTestingModule.withRoutes([]),
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock,
|
||||
}
|
||||
}),
|
||||
],
|
||||
providers: [
|
||||
{ provide: BreadcrumbsService, useValue: breadcrumbsServiceMock },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(BreadcrumbsComponent);
|
||||
component = fixture.componentInstance;
|
||||
router = TestBed.inject(Router);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('ngOnInit', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(component, 'resolveBreadcrumbs').and.returnValue(observableOf([]));
|
||||
});
|
||||
|
||||
it('should call resolveBreadcrumb on init', () => {
|
||||
router.events = observableOf(new NavigationEnd(0, '', ''));
|
||||
component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.resolveBreadcrumbs).toHaveBeenCalledWith(route.root);
|
||||
});
|
||||
it('should render the breadcrumbs', () => {
|
||||
const breadcrumbs = fixture.debugElement.queryAll(By.css('.breadcrumb-item'));
|
||||
expect(breadcrumbs.length).toBe(3);
|
||||
expectBreadcrumb(breadcrumbs[0], 'home.breadcrumbs', '/');
|
||||
expectBreadcrumb(breadcrumbs[1], 'bc 1', '/example.com');
|
||||
expectBreadcrumb(breadcrumbs[2], 'bc 2', null);
|
||||
});
|
||||
|
||||
describe('resolveBreadcrumbs', () => {
|
||||
it('should return the correct breadcrumbs', () => {
|
||||
const breadcrumbs = component.resolveBreadcrumbs(route.root);
|
||||
getTestScheduler().expectObservable(breadcrumbs).toBe('(a|)', { a: expectedBreadcrumbs });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,9 +1,7 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
|
||||
import { Component } from '@angular/core';
|
||||
import { Breadcrumb } from './breadcrumb/breadcrumb.model';
|
||||
import { hasNoValue, hasValue, isUndefined } from '../shared/empty.util';
|
||||
import { filter, map, switchMap, tap } from 'rxjs/operators';
|
||||
import { combineLatest, Observable, of as observableOf } from 'rxjs';
|
||||
import { BreadcrumbsService } from './breadcrumbs.service';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
|
||||
/**
|
||||
* Component representing the breadcrumbs of a page
|
||||
@@ -13,7 +11,8 @@ import { combineLatest, Observable, of as observableOf } from 'rxjs';
|
||||
templateUrl: './breadcrumbs.component.html',
|
||||
styleUrls: ['./breadcrumbs.component.scss']
|
||||
})
|
||||
export class BreadcrumbsComponent implements OnInit {
|
||||
export class BreadcrumbsComponent {
|
||||
|
||||
/**
|
||||
* Observable of the list of breadcrumbs for this page
|
||||
*/
|
||||
@@ -22,61 +21,13 @@ export class BreadcrumbsComponent implements OnInit {
|
||||
/**
|
||||
* Whether or not to show breadcrumbs on this page
|
||||
*/
|
||||
showBreadcrumbs: boolean;
|
||||
showBreadcrumbs$: Observable<boolean>;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private router: Router
|
||||
private breadcrumbsService: BreadcrumbsService,
|
||||
) {
|
||||
this.breadcrumbs$ = breadcrumbsService.breadcrumbs$;
|
||||
this.showBreadcrumbs$ = breadcrumbsService.showBreadcrumbs$;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the breadcrumbs on init for this page
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.breadcrumbs$ = this.router.events.pipe(
|
||||
filter((e): e is NavigationEnd => e instanceof NavigationEnd),
|
||||
tap(() => this.reset()),
|
||||
switchMap(() => this.resolveBreadcrumbs(this.route.root)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that recursively resolves breadcrumbs
|
||||
* @param route The route to get the breadcrumb from
|
||||
*/
|
||||
resolveBreadcrumbs(route: ActivatedRoute): Observable<Breadcrumb[]> {
|
||||
const data = route.snapshot.data;
|
||||
const routeConfig = route.snapshot.routeConfig;
|
||||
|
||||
const last: boolean = hasNoValue(route.firstChild);
|
||||
if (last) {
|
||||
if (hasValue(data.showBreadcrumbs)) {
|
||||
this.showBreadcrumbs = data.showBreadcrumbs;
|
||||
} else if (isUndefined(data.breadcrumb)) {
|
||||
this.showBreadcrumbs = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
hasValue(data) && hasValue(data.breadcrumb) &&
|
||||
hasValue(routeConfig) && hasValue(routeConfig.resolve) && hasValue(routeConfig.resolve.breadcrumb)
|
||||
) {
|
||||
const { provider, key, url } = data.breadcrumb;
|
||||
if (!last) {
|
||||
return combineLatest(provider.getBreadcrumbs(key, url), this.resolveBreadcrumbs(route.firstChild))
|
||||
.pipe(map((crumbs) => [].concat.apply([], crumbs)));
|
||||
} else {
|
||||
return provider.getBreadcrumbs(key, url);
|
||||
}
|
||||
}
|
||||
return !last ? this.resolveBreadcrumbs(route.firstChild) : observableOf([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the state of the breadcrumbs
|
||||
*/
|
||||
reset() {
|
||||
this.showBreadcrumbs = true;
|
||||
}
|
||||
}
|
||||
|
171
src/app/breadcrumbs/breadcrumbs.service.spec.ts
Normal file
171
src/app/breadcrumbs/breadcrumbs.service.spec.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { BreadcrumbsService } from './breadcrumbs.service';
|
||||
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
|
||||
import { Observable, of as observableOf, Subject } from 'rxjs';
|
||||
import { BreadcrumbConfig } from './breadcrumb/breadcrumb-config.model';
|
||||
import { BreadcrumbsProviderService } from '../core/breadcrumbs/breadcrumbsProviderService';
|
||||
import { Breadcrumb } from './breadcrumb/breadcrumb.model';
|
||||
import { cold } from 'jasmine-marbles';
|
||||
|
||||
class TestBreadcrumbsService implements BreadcrumbsProviderService<string> {
|
||||
getBreadcrumbs(key: string, url: string): Observable<Breadcrumb[]> {
|
||||
return observableOf([new Breadcrumb(key, url)]);
|
||||
}
|
||||
}
|
||||
|
||||
describe('BreadcrumbsService', () => {
|
||||
let service: BreadcrumbsService;
|
||||
let routerEventsObs: Subject<any>;
|
||||
let routerMock: Router;
|
||||
let activatedRouteMock: Partial<ActivatedRoute>;
|
||||
let currentRootRoute: Partial<ActivatedRoute>;
|
||||
let breadcrumbProvider;
|
||||
let breadcrumbConfigA: BreadcrumbConfig<string>;
|
||||
let breadcrumbConfigB: BreadcrumbConfig<string>;
|
||||
|
||||
/**
|
||||
* Init breadcrumb variables, see beforeEach
|
||||
*/
|
||||
const initBreadcrumbs = () => {
|
||||
breadcrumbProvider = new TestBreadcrumbsService();
|
||||
breadcrumbConfigA = { provider: breadcrumbProvider, key: 'example.path', url: 'example.com' };
|
||||
breadcrumbConfigB = { provider: breadcrumbProvider, key: 'another.path', url: 'another.com' };
|
||||
};
|
||||
|
||||
const changeActivatedRoute = (newRootRoute: any) => {
|
||||
// update the ActivatedRoute that the service will receive
|
||||
currentRootRoute = newRootRoute;
|
||||
|
||||
// the pipeline of BreadcrumbsService#listenForRouteChanges needs a NavigationEnd event,
|
||||
// but the actual payload does not matter, since ActivatedRoute is mocked too.
|
||||
routerEventsObs.next(new NavigationEnd(0, '', ''));
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
initBreadcrumbs();
|
||||
|
||||
routerEventsObs = new Subject<any>();
|
||||
|
||||
// BreadcrumbsService uses Router#events
|
||||
routerMock = jasmine.createSpyObj([], {
|
||||
events: routerEventsObs,
|
||||
});
|
||||
|
||||
// BreadcrumbsService uses ActivatedRoute#root
|
||||
activatedRouteMock = {
|
||||
get root() {
|
||||
return currentRootRoute as ActivatedRoute;
|
||||
},
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: Router, useValue: routerMock },
|
||||
{ provide: ActivatedRoute, useValue: activatedRouteMock },
|
||||
],
|
||||
});
|
||||
service = TestBed.inject(BreadcrumbsService);
|
||||
|
||||
// this is done by AppComponent under regular circumstances
|
||||
service.listenForRouteChanges();
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('breadcrumbs$', () => {
|
||||
it('should return a breadcrumb corresponding to the current route', () => {
|
||||
const route1 = {
|
||||
snapshot: {
|
||||
data: { breadcrumb: breadcrumbConfigA },
|
||||
routeConfig: { resolve: { breadcrumb: {} } }
|
||||
}
|
||||
};
|
||||
|
||||
const expectation1 = [
|
||||
new Breadcrumb(breadcrumbConfigA.key, breadcrumbConfigA.url),
|
||||
];
|
||||
|
||||
changeActivatedRoute(route1);
|
||||
expect(service.breadcrumbs$).toBeObservable(cold('a', { a: expectation1 }));
|
||||
|
||||
const route2 = {
|
||||
snapshot: {
|
||||
data: { breadcrumb: breadcrumbConfigA },
|
||||
routeConfig: { resolve: { breadcrumb: {} } }
|
||||
},
|
||||
firstChild: {
|
||||
snapshot: {
|
||||
// Example without resolver should be ignored
|
||||
data: { breadcrumb: breadcrumbConfigA },
|
||||
},
|
||||
firstChild: {
|
||||
snapshot: {
|
||||
data: { breadcrumb: breadcrumbConfigB },
|
||||
routeConfig: { resolve: { breadcrumb: {} } }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const expectation2 = [
|
||||
new Breadcrumb(breadcrumbConfigA.key, breadcrumbConfigA.url),
|
||||
new Breadcrumb(breadcrumbConfigB.key, breadcrumbConfigB.url),
|
||||
];
|
||||
|
||||
changeActivatedRoute(route2);
|
||||
expect(service.breadcrumbs$).toBeObservable(cold('a', { a: expectation2 }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('showBreadcrumbs$', () => {
|
||||
describe('when the last part of the route has showBreadcrumbs in its data', () => {
|
||||
it('should return that value', () => {
|
||||
const route1 = {
|
||||
snapshot: {
|
||||
data: {
|
||||
breadcrumb: breadcrumbConfigA,
|
||||
showBreadcrumbs: false, // explicitly hide breadcrumbs
|
||||
},
|
||||
routeConfig: { resolve: { breadcrumb: {} } }
|
||||
}
|
||||
};
|
||||
|
||||
changeActivatedRoute(route1);
|
||||
expect(service.showBreadcrumbs$).toBeObservable(cold('a', { a: false }));
|
||||
|
||||
const route2 = {
|
||||
snapshot: {
|
||||
data: {
|
||||
breadcrumb: breadcrumbConfigA,
|
||||
showBreadcrumbs: true, // explicitly show breadcrumbs
|
||||
},
|
||||
routeConfig: { resolve: { breadcrumb: {} } }
|
||||
}
|
||||
};
|
||||
|
||||
changeActivatedRoute(route2);
|
||||
expect(service.showBreadcrumbs$).toBeObservable(cold('a', { a: true }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the last part of the route has no breadcrumb in its data', () => {
|
||||
it('should return false', () => {
|
||||
const route1 = {
|
||||
snapshot: {
|
||||
data: {
|
||||
// no breadcrumbs set - always hide
|
||||
},
|
||||
routeConfig: { resolve: { breadcrumb: {} } }
|
||||
}
|
||||
};
|
||||
|
||||
changeActivatedRoute(route1);
|
||||
expect(service.showBreadcrumbs$).toBeObservable(cold('a', { a: false }));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
80
src/app/breadcrumbs/breadcrumbs.service.ts
Normal file
80
src/app/breadcrumbs/breadcrumbs.service.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import {combineLatest, Observable, of as observableOf, ReplaySubject} from 'rxjs';
|
||||
import {Breadcrumb} from './breadcrumb/breadcrumb.model';
|
||||
import {ActivatedRoute, NavigationEnd, Router} from '@angular/router';
|
||||
import {filter, map, switchMap, tap} from 'rxjs/operators';
|
||||
import {hasNoValue, hasValue, isUndefined} from '../shared/empty.util';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class BreadcrumbsService {
|
||||
|
||||
/**
|
||||
* Observable of the list of breadcrumbs for this page
|
||||
*/
|
||||
breadcrumbs$: ReplaySubject<Breadcrumb[]> = new ReplaySubject(1);
|
||||
|
||||
/**
|
||||
* Whether or not to show breadcrumbs on this page
|
||||
*/
|
||||
showBreadcrumbs$: ReplaySubject<boolean> = new ReplaySubject(1);
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Called by {@link AppComponent#constructor} (i.e. before routing)
|
||||
* such that no routing events are missed.
|
||||
*/
|
||||
listenForRouteChanges() {
|
||||
// supply events to this.breadcrumbs$
|
||||
this.router.events.pipe(
|
||||
filter((e): e is NavigationEnd => e instanceof NavigationEnd),
|
||||
tap(() => this.reset()),
|
||||
switchMap(() => this.resolveBreadcrumbs(this.route.root)),
|
||||
).subscribe(this.breadcrumbs$);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that recursively resolves breadcrumbs
|
||||
* @param route The route to get the breadcrumb from
|
||||
*/
|
||||
private resolveBreadcrumbs(route: ActivatedRoute): Observable<Breadcrumb[]> {
|
||||
const data = route.snapshot.data;
|
||||
const routeConfig = route.snapshot.routeConfig;
|
||||
|
||||
const last: boolean = hasNoValue(route.firstChild);
|
||||
if (last) {
|
||||
if (hasValue(data.showBreadcrumbs)) {
|
||||
this.showBreadcrumbs$.next(data.showBreadcrumbs);
|
||||
} else if (isUndefined(data.breadcrumb)) {
|
||||
this.showBreadcrumbs$.next(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
hasValue(data) && hasValue(data.breadcrumb) &&
|
||||
hasValue(routeConfig) && hasValue(routeConfig.resolve) && hasValue(routeConfig.resolve.breadcrumb)
|
||||
) {
|
||||
const { provider, key, url } = data.breadcrumb;
|
||||
if (!last) {
|
||||
return combineLatest(provider.getBreadcrumbs(key, url), this.resolveBreadcrumbs(route.firstChild))
|
||||
.pipe(map((crumbs) => [].concat.apply([], crumbs)));
|
||||
} else {
|
||||
return provider.getBreadcrumbs(key, url);
|
||||
}
|
||||
}
|
||||
return !last ? this.resolveBreadcrumbs(route.firstChild) : observableOf([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the state of the breadcrumbs
|
||||
*/
|
||||
private reset() {
|
||||
this.showBreadcrumbs$.next(true);
|
||||
}
|
||||
|
||||
}
|
25
src/app/breadcrumbs/themed-breadcrumbs.component.ts
Normal file
25
src/app/breadcrumbs/themed-breadcrumbs.component.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ThemedComponent } from '../shared/theme-support/themed.component';
|
||||
import { BreadcrumbsComponent } from './breadcrumbs.component';
|
||||
|
||||
/**
|
||||
* Themed wrapper for BreadcrumbsComponent
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-themed-breadcrumbs',
|
||||
styleUrls: [],
|
||||
templateUrl: '../shared/theme-support/themed.component.html',
|
||||
})
|
||||
export class ThemedBreadcrumbsComponent extends ThemedComponent<BreadcrumbsComponent> {
|
||||
protected getComponentName(): string {
|
||||
return 'BreadcrumbsComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../themes/${themeName}/app/breadcrumbs/breadcrumbs.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import(`./breadcrumbs.component`);
|
||||
}
|
||||
}
|
@@ -4,7 +4,14 @@ import { SharedModule } from '../shared/shared.module';
|
||||
import { CommunityListPageComponent } from './community-list-page.component';
|
||||
import { CommunityListPageRoutingModule } from './community-list-page.routing.module';
|
||||
import { CommunityListComponent } from './community-list/community-list.component';
|
||||
import { ThemedCommunityListPageComponent } from './themed-community-list-page.component';
|
||||
|
||||
|
||||
const DECLARATIONS = [
|
||||
CommunityListPageComponent,
|
||||
CommunityListComponent,
|
||||
ThemedCommunityListPageComponent
|
||||
];
|
||||
/**
|
||||
* The page which houses a title and the community list, as described in community-list.component
|
||||
*/
|
||||
@@ -15,9 +22,11 @@ import { CommunityListComponent } from './community-list/community-list.componen
|
||||
CommunityListPageRoutingModule
|
||||
],
|
||||
declarations: [
|
||||
CommunityListPageComponent,
|
||||
CommunityListComponent
|
||||
]
|
||||
...DECLARATIONS
|
||||
],
|
||||
exports: [
|
||||
...DECLARATIONS,
|
||||
],
|
||||
})
|
||||
export class CommunityListPageModule {
|
||||
|
||||
|
@@ -2,8 +2,8 @@ import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { CdkTreeModule } from '@angular/cdk/tree';
|
||||
|
||||
import { CommunityListPageComponent } from './community-list-page.component';
|
||||
import { CommunityListService } from './community-list-service';
|
||||
import { ThemedCommunityListPageComponent } from './themed-community-list-page.component';
|
||||
|
||||
/**
|
||||
* RouterModule to help navigate to the page with the community list tree
|
||||
@@ -13,7 +13,7 @@ import { CommunityListService } from './community-list-service';
|
||||
RouterModule.forChild([
|
||||
{
|
||||
path: '',
|
||||
component: CommunityListPageComponent,
|
||||
component: ThemedCommunityListPageComponent,
|
||||
pathMatch: 'full',
|
||||
data: { title: 'communityList.tabTitle' }
|
||||
}
|
||||
|
@@ -0,0 +1,26 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ThemedComponent } from '../shared/theme-support/themed.component';
|
||||
import { CommunityListPageComponent } from './community-list-page.component';
|
||||
|
||||
/**
|
||||
* Themed wrapper for CommunityListPageComponent
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-themed-community-list-page',
|
||||
styleUrls: [],
|
||||
templateUrl: '../shared/theme-support/themed.component.html',
|
||||
})
|
||||
export class ThemedCommunityListPageComponent extends ThemedComponent<CommunityListPageComponent> {
|
||||
protected getComponentName(): string {
|
||||
return 'CommunityListPageComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../themes/${themeName}/app/community-list-page/community-list-page.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import(`./community-list-page.component`);
|
||||
}
|
||||
|
||||
}
|
@@ -4,7 +4,7 @@ import { Observable } from 'rxjs';
|
||||
/**
|
||||
* Service to calculate breadcrumbs for a single part of the route
|
||||
*/
|
||||
export interface BreadcrumbsService<T> {
|
||||
export interface BreadcrumbsProviderService<T> {
|
||||
|
||||
/**
|
||||
* Method to calculate the breadcrumbs for a part of the route
|
@@ -1,5 +1,5 @@
|
||||
import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model';
|
||||
import { BreadcrumbsService } from './breadcrumbs.service';
|
||||
import { BreadcrumbsProviderService } from './breadcrumbsProviderService';
|
||||
import { DSONameService } from './dso-name.service';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import { ChildHALResource } from '../shared/child-hal-resource.model';
|
||||
@@ -18,7 +18,7 @@ import { getDSORoute } from '../../app-routing-paths';
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DSOBreadcrumbsService implements BreadcrumbsService<ChildHALResource & DSpaceObject> {
|
||||
export class DSOBreadcrumbsService implements BreadcrumbsProviderService<ChildHALResource & DSpaceObject> {
|
||||
constructor(
|
||||
private linkService: LinkService,
|
||||
private dsoNameService: DSONameService
|
||||
|
@@ -45,7 +45,7 @@ describe(`DSONameService`, () => {
|
||||
}
|
||||
});
|
||||
|
||||
service = new DSONameService();
|
||||
service = new DSONameService({ instant: (a) => a } as any);
|
||||
});
|
||||
|
||||
describe(`getName`, () => {
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
/**
|
||||
* Returns a name for a {@link DSpaceObject} based
|
||||
@@ -11,6 +12,10 @@ import { DSpaceObject } from '../shared/dspace-object.model';
|
||||
})
|
||||
export class DSONameService {
|
||||
|
||||
constructor(private translateService: TranslateService) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Functions to generate the specific names.
|
||||
*
|
||||
@@ -29,7 +34,7 @@ export class DSONameService {
|
||||
},
|
||||
Default: (dso: DSpaceObject): string => {
|
||||
// If object doesn't have dc.title metadata use name property
|
||||
return dso.firstMetadataValue('dc.title') || dso.name;
|
||||
return dso.firstMetadataValue('dc.title') || dso.name || this.translateService.instant('dso.name.untitled');
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model';
|
||||
import { BreadcrumbsService } from './breadcrumbs.service';
|
||||
import { BreadcrumbsProviderService } from './breadcrumbsProviderService';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@@ -14,7 +14,7 @@ export const BREADCRUMB_MESSAGE_POSTFIX = '.breadcrumbs';
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class I18nBreadcrumbsService implements BreadcrumbsService<string> {
|
||||
export class I18nBreadcrumbsService implements BreadcrumbsProviderService<string> {
|
||||
|
||||
/**
|
||||
* Method to calculate the breadcrumbs
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
|
||||
import { cold } from 'jasmine-marbles';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
||||
@@ -13,16 +13,15 @@ import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { ComColDataService } from './comcol-data.service';
|
||||
import { CommunityDataService } from './community-data.service';
|
||||
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
||||
import { FindListOptions, GetRequest } from './request.models';
|
||||
import { RequestEntry } from './request.reducer';
|
||||
import { FindListOptions } from './request.models';
|
||||
import { RequestService } from './request.service';
|
||||
import {
|
||||
createFailedRemoteDataObject$,
|
||||
createNoContentRemoteDataObject$,
|
||||
createSuccessfulRemoteDataObject$
|
||||
createSuccessfulRemoteDataObject$,
|
||||
createFailedRemoteDataObject,
|
||||
createSuccessfulRemoteDataObject
|
||||
} from '../../shared/remote-data.utils';
|
||||
import { BitstreamDataService } from './bitstream-data.service';
|
||||
import { take } from 'rxjs/operators';
|
||||
|
||||
const LINK_NAME = 'test';
|
||||
|
||||
@@ -50,8 +49,8 @@ class TestService extends ComColDataService<any> {
|
||||
}
|
||||
}
|
||||
|
||||
// tslint:disable:no-shadowed-variable
|
||||
describe('ComColDataService', () => {
|
||||
let scheduler: TestScheduler;
|
||||
let service: TestService;
|
||||
let requestService: RequestService;
|
||||
let cds: CommunityDataService;
|
||||
@@ -59,6 +58,8 @@ describe('ComColDataService', () => {
|
||||
let halService: any = {};
|
||||
let bitstreamDataService: BitstreamDataService;
|
||||
let rdbService: RemoteDataBuildService;
|
||||
let testScheduler: TestScheduler;
|
||||
let topEndpoint: string;
|
||||
|
||||
const store = {} as Store<CoreState>;
|
||||
const notificationsService = {} as NotificationsService;
|
||||
@@ -69,17 +70,9 @@ describe('ComColDataService', () => {
|
||||
const options = Object.assign(new FindListOptions(), {
|
||||
scopeID: scopeID
|
||||
});
|
||||
const getRequestEntry$ = (successful: boolean) => {
|
||||
return observableOf({
|
||||
response: { isSuccessful: successful } as any
|
||||
} as RequestEntry);
|
||||
};
|
||||
|
||||
const communitiesEndpoint = 'https://rest.api/core/communities';
|
||||
const communityEndpoint = `${communitiesEndpoint}/${scopeID}`;
|
||||
const scopedEndpoint = `${communityEndpoint}/${LINK_NAME}`;
|
||||
const serviceEndpoint = `https://rest.api/core/${LINK_NAME}`;
|
||||
const authHeader = 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJlaWQiOiJhNjA4NmIzNC0zOTE4LTQ1YjctOGRkZC05MzI5YTcwMmEyNmEiLCJzZyI6W10sImV4cCI6MTUzNDk0MDcyNX0.RV5GAtiX6cpwBN77P_v16iG9ipeyiO7faNYSNMzq_sQ';
|
||||
|
||||
const mockHalService = {
|
||||
getEndpoint: (linkPath) => observableOf(communitiesEndpoint)
|
||||
@@ -98,8 +91,8 @@ describe('ComColDataService', () => {
|
||||
}
|
||||
|
||||
function initMockCommunityDataService(): CommunityDataService {
|
||||
return jasmine.createSpyObj('responseCache', {
|
||||
getEndpoint: hot('--a-', { a: communitiesEndpoint }),
|
||||
return jasmine.createSpyObj('cds', {
|
||||
getEndpoint: cold('--a-', { a: communitiesEndpoint }),
|
||||
getIDHref: communityEndpoint
|
||||
});
|
||||
}
|
||||
@@ -134,7 +127,15 @@ describe('ComColDataService', () => {
|
||||
);
|
||||
}
|
||||
|
||||
const initTestScheduler = (): TestScheduler => {
|
||||
return new TestScheduler((actual, expected) => {
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
topEndpoint = 'https://rest.api/core/communities/search/top';
|
||||
testScheduler = initTestScheduler();
|
||||
cds = initMockCommunityDataService();
|
||||
requestService = getMockRequestService();
|
||||
objectCache = initMockObjectCacheService();
|
||||
@@ -145,113 +146,94 @@ describe('ComColDataService', () => {
|
||||
});
|
||||
|
||||
describe('getBrowseEndpoint', () => {
|
||||
beforeEach(() => {
|
||||
scheduler = getTestScheduler();
|
||||
});
|
||||
|
||||
it('should send a new FindByIDRequest for the scope Community', () => {
|
||||
cds = initMockCommunityDataService();
|
||||
requestService = getMockRequestService(getRequestEntry$(true));
|
||||
objectCache = initMockObjectCacheService();
|
||||
service = initTestService();
|
||||
|
||||
const expected = new GetRequest(requestService.generateRequestId(), communityEndpoint);
|
||||
|
||||
scheduler.schedule(() => service.getBrowseEndpoint(options).subscribe());
|
||||
scheduler.flush();
|
||||
|
||||
expect(requestService.send).toHaveBeenCalledWith(expected, true);
|
||||
it(`should call createAndSendGetRequest with the scope Community's self link`, () => {
|
||||
testScheduler.run(({ cold, flush, expectObservable }) => {
|
||||
(cds.getEndpoint as jasmine.Spy).and.returnValue(cold('a', { a: communitiesEndpoint }));
|
||||
(rdbService.buildSingle as jasmine.Spy).and.returnValue(cold('a', { a: createFailedRemoteDataObject() }));
|
||||
spyOn(service as any, 'createAndSendGetRequest');
|
||||
service.getBrowseEndpoint(options);
|
||||
flush();
|
||||
expectObservable((service as any).createAndSendGetRequest.calls.argsFor(0)[0]).toBe('(a|)', { a: communityEndpoint });
|
||||
expect((service as any).createAndSendGetRequest.calls.argsFor(0)[1]).toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
describe('if the scope Community can\'t be found', () => {
|
||||
it('should throw an error', () => {
|
||||
const result = service.getBrowseEndpoint(options).pipe(take(1));
|
||||
const expected = cold('--#-', undefined, new Error(`The Community with scope ${scopeID} couldn't be retrieved`));
|
||||
|
||||
expect(result).toBeObservable(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cache refresh', () => {
|
||||
let communityWithoutParentHref;
|
||||
let data;
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(halService, 'getEndpoint').and.returnValue(observableOf('https://rest.api/core/communities/search/top'));
|
||||
});
|
||||
|
||||
describe('cache refreshed top level community', () => {
|
||||
beforeEach(() => {
|
||||
(rdbService.buildSingle as jasmine.Spy).and.returnValue(createNoContentRemoteDataObject$());
|
||||
data = {
|
||||
dso: Object.assign(new Community(), {
|
||||
metadata: [{
|
||||
key: 'dc.title',
|
||||
value: 'top level community'
|
||||
}]
|
||||
}),
|
||||
_links: {
|
||||
parentCommunity: {
|
||||
href: 'topLevel/parentCommunity'
|
||||
}
|
||||
}
|
||||
};
|
||||
communityWithoutParentHref = {
|
||||
dso: Object.assign(new Community(), {
|
||||
metadata: [{
|
||||
key: 'dc.title',
|
||||
value: 'top level community'
|
||||
}]
|
||||
}),
|
||||
_links: {}
|
||||
};
|
||||
});
|
||||
it('top level community cache refreshed', () => {
|
||||
scheduler.schedule(() => (service as any).refreshCache(data));
|
||||
scheduler.flush();
|
||||
expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith('https://rest.api/core/communities/search/top');
|
||||
});
|
||||
it('top level community without parent link, cache not refreshed', () => {
|
||||
scheduler.schedule(() => (service as any).refreshCache(communityWithoutParentHref));
|
||||
scheduler.flush();
|
||||
expect(requestService.setStaleByHrefSubstring).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('cache refreshed child community', () => {
|
||||
beforeEach(() => {
|
||||
const parentCommunity = Object.assign(new Community(), {
|
||||
uuid: 'a20da287-e174-466a-9926-f66as300d399',
|
||||
id: 'a20da287-e174-466a-9926-f66as300d399',
|
||||
metadata: [{
|
||||
key: 'dc.title',
|
||||
value: 'parent community'
|
||||
}],
|
||||
_links: {}
|
||||
});
|
||||
(rdbService.buildSingle as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(parentCommunity));
|
||||
data = {
|
||||
dso: Object.assign(new Community(), {
|
||||
metadata: [{
|
||||
key: 'dc.title',
|
||||
value: 'child community'
|
||||
}]
|
||||
}),
|
||||
_links: {
|
||||
parentCommunity: {
|
||||
href: 'child/parentCommunity'
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
it('child level community cache refreshed', () => {
|
||||
scheduler.schedule(() => (service as any).refreshCache(data));
|
||||
scheduler.flush();
|
||||
expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith('a20da287-e174-466a-9926-f66as300d399');
|
||||
testScheduler.run(({ cold, expectObservable }) => {
|
||||
// spies re-defined here to use the "cold" function from rxjs's TestScheduler
|
||||
// rather than the one imported from jasmine-marbles.
|
||||
// Mixing the two seems to lead to unpredictable results
|
||||
(cds.getEndpoint as jasmine.Spy).and.returnValue(cold('a', { a: communitiesEndpoint }));
|
||||
(rdbService.buildSingle as jasmine.Spy).and.returnValue(cold('a', { a: createFailedRemoteDataObject() }));
|
||||
const expectedError = new Error(`The Community with scope ${scopeID} couldn't be retrieved`);
|
||||
expectObservable(service.getBrowseEndpoint(options)).toBe('#', undefined, expectedError);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('cache refresh', () => {
|
||||
let communityWithoutParentHref;
|
||||
let communityWithParentHref;
|
||||
|
||||
beforeEach(() => {
|
||||
communityWithParentHref = {
|
||||
_links: {
|
||||
parentCommunity: {
|
||||
href: 'topLevel/parentCommunity'
|
||||
}
|
||||
}
|
||||
} as Community;
|
||||
communityWithoutParentHref = {
|
||||
_links: {}
|
||||
} as Community;
|
||||
});
|
||||
|
||||
describe('cache refreshed top level community', () => {
|
||||
it(`should refresh the top level community cache when the dso has a parent link that can't be resolved`, () => {
|
||||
testScheduler.run(({ flush, cold }) => {
|
||||
spyOn(halService, 'getEndpoint').and.returnValue(cold('a', { a: topEndpoint }));
|
||||
spyOn(service, 'findByHref').and.returnValue(cold('a', { a: createSuccessfulRemoteDataObject({}) }));
|
||||
service.refreshCache(communityWithParentHref);
|
||||
flush();
|
||||
expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith(topEndpoint);
|
||||
});
|
||||
});
|
||||
it(`shouldn't do anything when the dso doesn't have a parent link`, () => {
|
||||
testScheduler.run(({ flush, cold }) => {
|
||||
spyOn(halService, 'getEndpoint').and.returnValue(cold('a', { a: topEndpoint }));
|
||||
spyOn(service, 'findByHref').and.returnValue(cold('a', { a: createSuccessfulRemoteDataObject({}) }));
|
||||
service.refreshCache(communityWithoutParentHref);
|
||||
flush();
|
||||
expect(requestService.setStaleByHrefSubstring).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('cache refreshed child community', () => {
|
||||
let parentCommunity: Community;
|
||||
beforeEach(() => {
|
||||
parentCommunity = Object.assign(new Community(), {
|
||||
uuid: 'a20da287-e174-466a-9926-f66as300d399',
|
||||
id: 'a20da287-e174-466a-9926-f66as300d399',
|
||||
metadata: [{
|
||||
key: 'dc.title',
|
||||
value: 'parent community'
|
||||
}],
|
||||
_links: {}
|
||||
});
|
||||
});
|
||||
it('should refresh a specific cached community when the parent link can be resolved', () => {
|
||||
testScheduler.run(({ flush, cold }) => {
|
||||
spyOn(halService, 'getEndpoint').and.returnValue(cold('a', { a: topEndpoint }));
|
||||
spyOn(service, 'findByHref').and.returnValue(cold('a', { a: createSuccessfulRemoteDataObject(parentCommunity) }));
|
||||
service.refreshCache(communityWithParentHref);
|
||||
flush();
|
||||
expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith('a20da287-e174-466a-9926-f66as300d399');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -20,6 +20,9 @@ import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
||||
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
|
||||
import { RequestParam } from '../cache/models/request-param.model';
|
||||
import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { RemoteData } from './remote-data';
|
||||
import { RequestEntryState } from './request.reducer';
|
||||
|
||||
const endpoint = 'https://rest.api/core';
|
||||
|
||||
@@ -63,6 +66,10 @@ describe('DataService', () => {
|
||||
let comparator;
|
||||
let objectCache;
|
||||
let store;
|
||||
let selfLink;
|
||||
let linksToFollow;
|
||||
let testScheduler;
|
||||
let remoteDataMocks;
|
||||
|
||||
function initTestService(): TestService {
|
||||
requestService = getMockRequestService();
|
||||
@@ -81,6 +88,34 @@ describe('DataService', () => {
|
||||
}
|
||||
} as any;
|
||||
store = {} as Store<CoreState>;
|
||||
selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
||||
linksToFollow = [
|
||||
followLink('a'),
|
||||
followLink('b')
|
||||
];
|
||||
|
||||
testScheduler = new TestScheduler((actual, expected) => {
|
||||
// asserting the two objects are equal
|
||||
// e.g. using chai.
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
const timeStamp = new Date().getTime();
|
||||
const msToLive = 15 * 60 * 1000;
|
||||
const payload = { foo: 'bar' };
|
||||
const statusCodeSuccess = 200;
|
||||
const statusCodeError = 404;
|
||||
const errorMessage = 'not found';
|
||||
remoteDataMocks = {
|
||||
RequestPending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.RequestPending, undefined, undefined, undefined),
|
||||
ResponsePending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.ResponsePending, undefined, undefined, undefined),
|
||||
Success: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Success, undefined, payload, statusCodeSuccess),
|
||||
SuccessStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.SuccessStale, undefined, payload, statusCodeSuccess),
|
||||
Error: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Error, errorMessage, undefined, statusCodeError),
|
||||
ErrorStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.ErrorStale, errorMessage, undefined, statusCodeError),
|
||||
};
|
||||
|
||||
|
||||
return new TestService(
|
||||
requestService,
|
||||
rdbService,
|
||||
@@ -307,14 +342,12 @@ describe('DataService', () => {
|
||||
|
||||
describe('update', () => {
|
||||
let operations;
|
||||
let selfLink;
|
||||
let dso;
|
||||
let dso2;
|
||||
const name1 = 'random string';
|
||||
const name2 = 'another random string';
|
||||
beforeEach(() => {
|
||||
operations = [{ op: 'replace', path: '/0/value', value: name2 } as Operation];
|
||||
selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
||||
|
||||
dso = Object.assign(new DSpaceObject(), {
|
||||
_links: { self: { href: selfLink } },
|
||||
@@ -340,5 +373,452 @@ describe('DataService', () => {
|
||||
expect(objectCache.addPatch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe(`reRequestStaleRemoteData`, () => {
|
||||
let callback: jasmine.Spy<jasmine.Func>;
|
||||
|
||||
beforeEach(() => {
|
||||
callback = jasmine.createSpy();
|
||||
});
|
||||
|
||||
|
||||
describe(`when shouldReRequest is false`, () => {
|
||||
it(`shouldn't do anything`, () => {
|
||||
testScheduler.run(({ cold, expectObservable, flush }) => {
|
||||
const expected = 'a-b-c-d-e-f';
|
||||
const values = {
|
||||
a: remoteDataMocks.RequestPending,
|
||||
b: remoteDataMocks.ResponsePending,
|
||||
c: remoteDataMocks.Success,
|
||||
d: remoteDataMocks.SuccessStale,
|
||||
e: remoteDataMocks.Error,
|
||||
f: remoteDataMocks.ErrorStale,
|
||||
};
|
||||
|
||||
expectObservable((service as any).reRequestStaleRemoteData(false, callback)(cold(expected, values))).toBe(expected, values);
|
||||
// since the callback happens in a tap(), flush to ensure it has been executed
|
||||
flush();
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when shouldReRequest is true`, () => {
|
||||
it(`should call the callback for stale RemoteData objects, but still pass the source observable unmodified`, () => {
|
||||
testScheduler.run(({ cold, expectObservable, flush }) => {
|
||||
const expected = 'a-b';
|
||||
const values = {
|
||||
a: remoteDataMocks.SuccessStale,
|
||||
b: remoteDataMocks.ErrorStale,
|
||||
};
|
||||
|
||||
expectObservable((service as any).reRequestStaleRemoteData(true, callback)(cold(expected, values))).toBe(expected, values);
|
||||
// since the callback happens in a tap(), flush to ensure it has been executed
|
||||
flush();
|
||||
expect(callback).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
it(`should only call the callback for stale RemoteData objects if something is subscribed to it`, (done) => {
|
||||
testScheduler.run(({ cold, expectObservable }) => {
|
||||
const expected = 'a';
|
||||
const values = {
|
||||
a: remoteDataMocks.SuccessStale,
|
||||
};
|
||||
|
||||
const result$ = (service as any).reRequestStaleRemoteData(true, callback)(cold(expected, values));
|
||||
expectObservable(result$).toBe(expected, values);
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
result$.subscribe(() => {
|
||||
expect(callback).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it(`shouldn't do anything for RemoteData objects that aren't stale`, () => {
|
||||
testScheduler.run(({ cold, expectObservable, flush }) => {
|
||||
const expected = 'a-b-c-d';
|
||||
const values = {
|
||||
a: remoteDataMocks.RequestPending,
|
||||
b: remoteDataMocks.ResponsePending,
|
||||
c: remoteDataMocks.Success,
|
||||
d: remoteDataMocks.Error,
|
||||
};
|
||||
|
||||
expectObservable((service as any).reRequestStaleRemoteData(true, callback)(cold(expected, values))).toBe(expected, values);
|
||||
// since the callback happens in a tap(), flush to ensure it has been executed
|
||||
flush();
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe(`findByHref`, () => {
|
||||
beforeEach(() => {
|
||||
spyOn(service as any, 'createAndSendGetRequest').and.callFake((href$) => { href$.subscribe().unsubscribe(); });
|
||||
});
|
||||
|
||||
it(`should call buildHrefFromFindOptions with href and linksToFollow`, () => {
|
||||
testScheduler.run(({ cold }) => {
|
||||
spyOn(service, 'buildHrefFromFindOptions').and.returnValue(selfLink);
|
||||
spyOn(rdbService, 'buildSingle').and.returnValue(cold('a', { a: remoteDataMocks.Success }));
|
||||
spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataMocks.Success }));
|
||||
|
||||
service.findByHref(selfLink, true, true, ...linksToFollow);
|
||||
expect(service.buildHrefFromFindOptions).toHaveBeenCalledWith(selfLink, {}, [], ...linksToFollow);
|
||||
});
|
||||
});
|
||||
|
||||
it(`should call createAndSendGetRequest with the result from buildHrefFromFindOptions and useCachedVersionIfAvailable`, () => {
|
||||
testScheduler.run(({ cold, expectObservable }) => {
|
||||
spyOn(service, 'buildHrefFromFindOptions').and.returnValue('bingo!');
|
||||
spyOn(rdbService, 'buildSingle').and.returnValue(cold('a', { a: remoteDataMocks.Success }));
|
||||
spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataMocks.Success }));
|
||||
|
||||
service.findByHref(selfLink, true, true, ...linksToFollow);
|
||||
expect((service as any).createAndSendGetRequest).toHaveBeenCalledWith(jasmine.anything(), true);
|
||||
expectObservable(rdbService.buildSingle.calls.argsFor(0)[0]).toBe('(a|)', { a: 'bingo!' });
|
||||
|
||||
service.findByHref(selfLink, false, true, ...linksToFollow);
|
||||
expect((service as any).createAndSendGetRequest).toHaveBeenCalledWith(jasmine.anything(), false);
|
||||
expectObservable(rdbService.buildSingle.calls.argsFor(1)[0]).toBe('(a|)', { a: 'bingo!' });
|
||||
});
|
||||
});
|
||||
|
||||
it(`should call rdbService.buildSingle with the result from buildHrefFromFindOptions and linksToFollow`, () => {
|
||||
testScheduler.run(({ cold, expectObservable }) => {
|
||||
spyOn(service, 'buildHrefFromFindOptions').and.returnValue('bingo!');
|
||||
spyOn(rdbService, 'buildSingle').and.returnValue(cold('a', { a: remoteDataMocks.Success }));
|
||||
spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataMocks.Success }));
|
||||
|
||||
service.findByHref(selfLink, true, true, ...linksToFollow);
|
||||
expect(rdbService.buildSingle).toHaveBeenCalledWith(jasmine.anything() as any, ...linksToFollow);
|
||||
expectObservable(rdbService.buildSingle.calls.argsFor(0)[0]).toBe('(a|)', { a: 'bingo!' });
|
||||
});
|
||||
});
|
||||
|
||||
it(`should return a the output from reRequestStaleRemoteData`, () => {
|
||||
testScheduler.run(({ cold, expectObservable }) => {
|
||||
spyOn(service, 'buildHrefFromFindOptions').and.returnValue(selfLink);
|
||||
spyOn(rdbService, 'buildSingle').and.returnValue(cold('a', { a: remoteDataMocks.Success }));
|
||||
spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: 'bingo!' }));
|
||||
const expected = 'a';
|
||||
const values = {
|
||||
a: 'bingo!',
|
||||
};
|
||||
|
||||
expectObservable(service.findByHref(selfLink, true, true, ...linksToFollow)).toBe(expected, values);
|
||||
});
|
||||
});
|
||||
|
||||
it(`should call reRequestStaleRemoteData with reRequestOnStale and the exact same findByHref call as a callback`, () => {
|
||||
testScheduler.run(({ cold, expectObservable }) => {
|
||||
spyOn(service, 'buildHrefFromFindOptions').and.returnValue(selfLink);
|
||||
spyOn(rdbService, 'buildSingle').and.returnValue(cold('a', { a: remoteDataMocks.SuccessStale }));
|
||||
spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataMocks.SuccessStale }));
|
||||
|
||||
service.findByHref(selfLink, true, true, ...linksToFollow);
|
||||
expect((service as any).reRequestStaleRemoteData.calls.argsFor(0)[0]).toBeTrue();
|
||||
spyOn(service, 'findByHref').and.returnValue(cold('a', { a: remoteDataMocks.SuccessStale }));
|
||||
// prove that the spy we just added hasn't been called yet
|
||||
expect(service.findByHref).not.toHaveBeenCalled();
|
||||
// call the callback passed to reRequestStaleRemoteData
|
||||
(service as any).reRequestStaleRemoteData.calls.argsFor(0)[1]();
|
||||
// verify that findByHref _has_ been called now, with the same params as the original call
|
||||
expect(service.findByHref).toHaveBeenCalledWith(jasmine.anything(), true, true, ...linksToFollow);
|
||||
// ... except for selflink, which will have been turned in to an observable.
|
||||
expectObservable((service.findByHref as jasmine.Spy).calls.argsFor(0)[0]).toBe('(a|)', { a: selfLink });
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when useCachedVersionIfAvailable is true`, () => {
|
||||
beforeEach(() => {
|
||||
spyOn(service, 'buildHrefFromFindOptions').and.returnValue(selfLink);
|
||||
spyOn(service as any, 'reRequestStaleRemoteData').and.callFake(() => (source) => source);
|
||||
});
|
||||
|
||||
it(`should emit a cached completed RemoteData immediately, and keep emitting if it gets rerequested`, () => {
|
||||
testScheduler.run(({ cold, expectObservable }) => {
|
||||
spyOn(rdbService, 'buildSingle').and.returnValue(cold('a-b-c-d-e', {
|
||||
a: remoteDataMocks.Success,
|
||||
b: remoteDataMocks.RequestPending,
|
||||
c: remoteDataMocks.ResponsePending,
|
||||
d: remoteDataMocks.Success,
|
||||
e: remoteDataMocks.SuccessStale,
|
||||
}));
|
||||
const expected = 'a-b-c-d-e';
|
||||
const values = {
|
||||
a: remoteDataMocks.Success,
|
||||
b: remoteDataMocks.RequestPending,
|
||||
c: remoteDataMocks.ResponsePending,
|
||||
d: remoteDataMocks.Success,
|
||||
e: remoteDataMocks.SuccessStale,
|
||||
};
|
||||
|
||||
expectObservable(service.findByHref(selfLink, true, true, ...linksToFollow)).toBe(expected, values);
|
||||
});
|
||||
});
|
||||
|
||||
it(`should not emit a cached stale RemoteData, but only start emitting after the state first changes to RequestPending`, () => {
|
||||
testScheduler.run(({ cold, expectObservable }) => {
|
||||
spyOn(rdbService, 'buildSingle').and.returnValue(cold('a-b-c-d-e', {
|
||||
a: remoteDataMocks.SuccessStale,
|
||||
b: remoteDataMocks.RequestPending,
|
||||
c: remoteDataMocks.ResponsePending,
|
||||
d: remoteDataMocks.Success,
|
||||
e: remoteDataMocks.SuccessStale,
|
||||
}));
|
||||
const expected = '--b-c-d-e';
|
||||
const values = {
|
||||
b: remoteDataMocks.RequestPending,
|
||||
c: remoteDataMocks.ResponsePending,
|
||||
d: remoteDataMocks.Success,
|
||||
e: remoteDataMocks.SuccessStale,
|
||||
};
|
||||
|
||||
expectObservable(service.findByHref(selfLink, true, true, ...linksToFollow)).toBe(expected, values);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe(`when useCachedVersionIfAvailable is false`, () => {
|
||||
beforeEach(() => {
|
||||
spyOn(service, 'buildHrefFromFindOptions').and.returnValue(selfLink);
|
||||
spyOn(service as any, 'reRequestStaleRemoteData').and.callFake(() => (source) => source);
|
||||
});
|
||||
|
||||
|
||||
it(`should not emit a cached completed RemoteData, but only start emitting after the state first changes to RequestPending`, () => {
|
||||
testScheduler.run(({ cold, expectObservable }) => {
|
||||
spyOn(rdbService, 'buildSingle').and.returnValue(cold('a-b-c-d-e', {
|
||||
a: remoteDataMocks.Success,
|
||||
b: remoteDataMocks.RequestPending,
|
||||
c: remoteDataMocks.ResponsePending,
|
||||
d: remoteDataMocks.Success,
|
||||
e: remoteDataMocks.SuccessStale,
|
||||
}));
|
||||
const expected = '--b-c-d-e';
|
||||
const values = {
|
||||
b: remoteDataMocks.RequestPending,
|
||||
c: remoteDataMocks.ResponsePending,
|
||||
d: remoteDataMocks.Success,
|
||||
e: remoteDataMocks.SuccessStale,
|
||||
};
|
||||
|
||||
expectObservable(service.findByHref(selfLink, false, true, ...linksToFollow)).toBe(expected, values);
|
||||
});
|
||||
});
|
||||
|
||||
it(`should not emit a cached stale RemoteData, but only start emitting after the state first changes to RequestPending`, () => {
|
||||
testScheduler.run(({ cold, expectObservable }) => {
|
||||
spyOn(rdbService, 'buildSingle').and.returnValue(cold('a-b-c-d-e', {
|
||||
a: remoteDataMocks.SuccessStale,
|
||||
b: remoteDataMocks.RequestPending,
|
||||
c: remoteDataMocks.ResponsePending,
|
||||
d: remoteDataMocks.Success,
|
||||
e: remoteDataMocks.SuccessStale,
|
||||
}));
|
||||
const expected = '--b-c-d-e';
|
||||
const values = {
|
||||
b: remoteDataMocks.RequestPending,
|
||||
c: remoteDataMocks.ResponsePending,
|
||||
d: remoteDataMocks.Success,
|
||||
e: remoteDataMocks.SuccessStale,
|
||||
};
|
||||
|
||||
expectObservable(service.findByHref(selfLink, false, true, ...linksToFollow)).toBe(expected, values);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe(`findAllByHref`, () => {
|
||||
let findListOptions;
|
||||
beforeEach(() => {
|
||||
findListOptions = { currentPage: 5 };
|
||||
spyOn(service as any, 'createAndSendGetRequest').and.callFake((href$) => { href$.subscribe().unsubscribe(); });
|
||||
});
|
||||
|
||||
it(`should call buildHrefFromFindOptions with href and linksToFollow`, () => {
|
||||
testScheduler.run(({ cold }) => {
|
||||
spyOn(service, 'buildHrefFromFindOptions').and.returnValue(selfLink);
|
||||
spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataMocks.Success }));
|
||||
spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataMocks.Success }));
|
||||
|
||||
service.findAllByHref(selfLink, findListOptions, true, true, ...linksToFollow);
|
||||
expect(service.buildHrefFromFindOptions).toHaveBeenCalledWith(selfLink, findListOptions, [], ...linksToFollow);
|
||||
});
|
||||
});
|
||||
|
||||
it(`should call createAndSendGetRequest with the result from buildHrefFromFindOptions and useCachedVersionIfAvailable`, () => {
|
||||
testScheduler.run(({ cold, expectObservable }) => {
|
||||
spyOn(service, 'buildHrefFromFindOptions').and.returnValue('bingo!');
|
||||
spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataMocks.Success }));
|
||||
spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataMocks.Success }));
|
||||
|
||||
service.findAllByHref(selfLink, findListOptions, true, true, ...linksToFollow);
|
||||
expect((service as any).createAndSendGetRequest).toHaveBeenCalledWith(jasmine.anything(), true);
|
||||
expectObservable(rdbService.buildList.calls.argsFor(0)[0]).toBe('(a|)', { a: 'bingo!' });
|
||||
|
||||
service.findAllByHref(selfLink, findListOptions, false, true, ...linksToFollow);
|
||||
expect((service as any).createAndSendGetRequest).toHaveBeenCalledWith(jasmine.anything(), false);
|
||||
expectObservable(rdbService.buildList.calls.argsFor(1)[0]).toBe('(a|)', { a: 'bingo!' });
|
||||
});
|
||||
});
|
||||
|
||||
it(`should call rdbService.buildList with the result from buildHrefFromFindOptions and linksToFollow`, () => {
|
||||
testScheduler.run(({ cold, expectObservable }) => {
|
||||
spyOn(service, 'buildHrefFromFindOptions').and.returnValue('bingo!');
|
||||
spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataMocks.Success }));
|
||||
spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataMocks.Success }));
|
||||
|
||||
service.findAllByHref(selfLink, findListOptions, true, true, ...linksToFollow);
|
||||
expect(rdbService.buildList).toHaveBeenCalledWith(jasmine.anything() as any, ...linksToFollow);
|
||||
expectObservable(rdbService.buildList.calls.argsFor(0)[0]).toBe('(a|)', { a: 'bingo!' });
|
||||
});
|
||||
});
|
||||
|
||||
it(`should call reRequestStaleRemoteData with reRequestOnStale and the exact same findAllByHref call as a callback`, () => {
|
||||
testScheduler.run(({ cold, expectObservable }) => {
|
||||
spyOn(service, 'buildHrefFromFindOptions').and.returnValue('bingo!');
|
||||
spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataMocks.SuccessStale }));
|
||||
spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataMocks.SuccessStale }));
|
||||
|
||||
service.findAllByHref(selfLink, findListOptions, true, true, ...linksToFollow);
|
||||
expect((service as any).reRequestStaleRemoteData.calls.argsFor(0)[0]).toBeTrue();
|
||||
spyOn(service, 'findAllByHref').and.returnValue(cold('a', { a: remoteDataMocks.SuccessStale }));
|
||||
// prove that the spy we just added hasn't been called yet
|
||||
expect(service.findAllByHref).not.toHaveBeenCalled();
|
||||
// call the callback passed to reRequestStaleRemoteData
|
||||
(service as any).reRequestStaleRemoteData.calls.argsFor(0)[1]();
|
||||
// verify that findAllByHref _has_ been called now, with the same params as the original call
|
||||
expect(service.findAllByHref).toHaveBeenCalledWith(jasmine.anything(), findListOptions, true, true, ...linksToFollow);
|
||||
// ... except for selflink, which will have been turned in to an observable.
|
||||
expectObservable((service.findAllByHref as jasmine.Spy).calls.argsFor(0)[0]).toBe('(a|)', { a: selfLink });
|
||||
});
|
||||
});
|
||||
|
||||
it(`should return a the output from reRequestStaleRemoteData`, () => {
|
||||
testScheduler.run(({ cold, expectObservable }) => {
|
||||
spyOn(service, 'buildHrefFromFindOptions').and.returnValue(selfLink);
|
||||
spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataMocks.Success }));
|
||||
spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: 'bingo!' }));
|
||||
const expected = 'a';
|
||||
const values = {
|
||||
a: 'bingo!',
|
||||
};
|
||||
|
||||
expectObservable(service.findAllByHref(selfLink, findListOptions, true, true, ...linksToFollow)).toBe(expected, values);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when useCachedVersionIfAvailable is true`, () => {
|
||||
beforeEach(() => {
|
||||
spyOn(service, 'buildHrefFromFindOptions').and.returnValue(selfLink);
|
||||
spyOn(service as any, 'reRequestStaleRemoteData').and.callFake(() => (source) => source);
|
||||
});
|
||||
|
||||
it(`should emit a cached completed RemoteData immediately, and keep emitting if it gets rerequested`, () => {
|
||||
testScheduler.run(({ cold, expectObservable }) => {
|
||||
spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e', {
|
||||
a: remoteDataMocks.Success,
|
||||
b: remoteDataMocks.RequestPending,
|
||||
c: remoteDataMocks.ResponsePending,
|
||||
d: remoteDataMocks.Success,
|
||||
e: remoteDataMocks.SuccessStale,
|
||||
}));
|
||||
const expected = 'a-b-c-d-e';
|
||||
const values = {
|
||||
a: remoteDataMocks.Success,
|
||||
b: remoteDataMocks.RequestPending,
|
||||
c: remoteDataMocks.ResponsePending,
|
||||
d: remoteDataMocks.Success,
|
||||
e: remoteDataMocks.SuccessStale,
|
||||
};
|
||||
|
||||
expectObservable(service.findAllByHref(selfLink, findListOptions, true, true, ...linksToFollow)).toBe(expected, values);
|
||||
});
|
||||
});
|
||||
|
||||
it(`should not emit a cached stale RemoteData, but only start emitting after the state first changes to RequestPending`, () => {
|
||||
testScheduler.run(({ cold, expectObservable }) => {
|
||||
spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e', {
|
||||
a: remoteDataMocks.SuccessStale,
|
||||
b: remoteDataMocks.RequestPending,
|
||||
c: remoteDataMocks.ResponsePending,
|
||||
d: remoteDataMocks.Success,
|
||||
e: remoteDataMocks.SuccessStale,
|
||||
}));
|
||||
const expected = '--b-c-d-e';
|
||||
const values = {
|
||||
b: remoteDataMocks.RequestPending,
|
||||
c: remoteDataMocks.ResponsePending,
|
||||
d: remoteDataMocks.Success,
|
||||
e: remoteDataMocks.SuccessStale,
|
||||
};
|
||||
|
||||
expectObservable(service.findAllByHref(selfLink, findListOptions, true, true, ...linksToFollow)).toBe(expected, values);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe(`when useCachedVersionIfAvailable is false`, () => {
|
||||
beforeEach(() => {
|
||||
spyOn(service, 'buildHrefFromFindOptions').and.returnValue(selfLink);
|
||||
spyOn(service as any, 'reRequestStaleRemoteData').and.callFake(() => (source) => source);
|
||||
});
|
||||
|
||||
|
||||
it(`should not emit a cached completed RemoteData, but only start emitting after the state first changes to RequestPending`, () => {
|
||||
testScheduler.run(({ cold, expectObservable }) => {
|
||||
spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e', {
|
||||
a: remoteDataMocks.Success,
|
||||
b: remoteDataMocks.RequestPending,
|
||||
c: remoteDataMocks.ResponsePending,
|
||||
d: remoteDataMocks.Success,
|
||||
e: remoteDataMocks.SuccessStale,
|
||||
}));
|
||||
const expected = '--b-c-d-e';
|
||||
const values = {
|
||||
b: remoteDataMocks.RequestPending,
|
||||
c: remoteDataMocks.ResponsePending,
|
||||
d: remoteDataMocks.Success,
|
||||
e: remoteDataMocks.SuccessStale,
|
||||
};
|
||||
|
||||
expectObservable(service.findAllByHref(selfLink, findListOptions, false, true, ...linksToFollow)).toBe(expected, values);
|
||||
});
|
||||
});
|
||||
|
||||
it(`should not emit a cached stale RemoteData, but only start emitting after the state first changes to RequestPending`, () => {
|
||||
testScheduler.run(({ cold, expectObservable }) => {
|
||||
spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e', {
|
||||
a: remoteDataMocks.SuccessStale,
|
||||
b: remoteDataMocks.RequestPending,
|
||||
c: remoteDataMocks.ResponsePending,
|
||||
d: remoteDataMocks.Success,
|
||||
e: remoteDataMocks.SuccessStale,
|
||||
}));
|
||||
const expected = '--b-c-d-e';
|
||||
const values = {
|
||||
b: remoteDataMocks.RequestPending,
|
||||
c: remoteDataMocks.ResponsePending,
|
||||
d: remoteDataMocks.Success,
|
||||
e: remoteDataMocks.SuccessStale,
|
||||
};
|
||||
|
||||
expectObservable(service.findAllByHref(selfLink, findListOptions, false, true, ...linksToFollow)).toBe(expected, values);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
/* tslint:enable:max-classes-per-file */
|
||||
|
@@ -12,6 +12,7 @@ import {
|
||||
takeWhile,
|
||||
switchMap,
|
||||
tap,
|
||||
skipWhile,
|
||||
} from 'rxjs/operators';
|
||||
import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
|
||||
import { NotificationOptions } from '../../shared/notifications/models/notification-options.model';
|
||||
@@ -45,29 +46,6 @@ import { UpdateDataService } from './update-data.service';
|
||||
import { GenericConstructor } from '../shared/generic-constructor';
|
||||
import { NoContent } from '../shared/NoContent.model';
|
||||
|
||||
/**
|
||||
* An operator that will call the given function if the incoming RemoteData is stale and
|
||||
* shouldReRequest is true
|
||||
*
|
||||
* @param shouldReRequest Whether or not to call the re-request function if the RemoteData is stale
|
||||
* @param requestFn The function to call if the RemoteData is stale and shouldReRequest is
|
||||
* true
|
||||
*/
|
||||
export const reRequestStaleRemoteData = <T>(shouldReRequest: boolean, requestFn: () => Observable<RemoteData<T>>) =>
|
||||
(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> => {
|
||||
if (shouldReRequest === true) {
|
||||
return source.pipe(
|
||||
tap((remoteData: RemoteData<T>) => {
|
||||
if (hasValue(remoteData) && remoteData.isStale) {
|
||||
requestFn();
|
||||
}
|
||||
})
|
||||
);
|
||||
} else {
|
||||
return source;
|
||||
}
|
||||
};
|
||||
|
||||
export abstract class DataService<T extends CacheableObject> implements UpdateDataService<T> {
|
||||
protected abstract requestService: RequestService;
|
||||
protected abstract rdbService: RemoteDataBuildService;
|
||||
@@ -332,6 +310,30 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
|
||||
return this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||
}
|
||||
|
||||
/**
|
||||
* An operator that will call the given function if the incoming RemoteData is stale and
|
||||
* shouldReRequest is true
|
||||
*
|
||||
* @param shouldReRequest Whether or not to call the re-request function if the RemoteData is stale
|
||||
* @param requestFn The function to call if the RemoteData is stale and shouldReRequest is
|
||||
* true
|
||||
*/
|
||||
protected reRequestStaleRemoteData<O>(shouldReRequest: boolean, requestFn: () => Observable<RemoteData<O>>) {
|
||||
return (source: Observable<RemoteData<O>>): Observable<RemoteData<O>> => {
|
||||
if (shouldReRequest === true) {
|
||||
return source.pipe(
|
||||
tap((remoteData: RemoteData<O>) => {
|
||||
if (hasValue(remoteData) && remoteData.isStale) {
|
||||
requestFn();
|
||||
}
|
||||
})
|
||||
);
|
||||
} else {
|
||||
return source;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable of {@link RemoteData} of an object, based on an href, with a list of
|
||||
* {@link FollowLinkConfig}, to automatically resolve {@link HALLink}s of the object
|
||||
@@ -358,7 +360,12 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
|
||||
this.createAndSendGetRequest(requestHref$, useCachedVersionIfAvailable);
|
||||
|
||||
return this.rdbService.buildSingle<T>(requestHref$, ...linksToFollow).pipe(
|
||||
reRequestStaleRemoteData(reRequestOnStale, () =>
|
||||
// This skip ensures that if a stale object is present in the cache when you do a
|
||||
// call it isn't immediately returned, but we wait until the remote data for the new request
|
||||
// is created. If useCachedVersionIfAvailable is false it also ensures you don't get a
|
||||
// cached completed object
|
||||
skipWhile((rd: RemoteData<T>) => useCachedVersionIfAvailable ? rd.isStale : rd.hasCompleted),
|
||||
this.reRequestStaleRemoteData(reRequestOnStale, () =>
|
||||
this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow))
|
||||
);
|
||||
}
|
||||
@@ -390,7 +397,12 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
|
||||
this.createAndSendGetRequest(requestHref$, useCachedVersionIfAvailable);
|
||||
|
||||
return this.rdbService.buildList<T>(requestHref$, ...linksToFollow).pipe(
|
||||
reRequestStaleRemoteData(reRequestOnStale, () =>
|
||||
// This skip ensures that if a stale object is present in the cache when you do a
|
||||
// call it isn't immediately returned, but we wait until the remote data for the new request
|
||||
// is created. If useCachedVersionIfAvailable is false it also ensures you don't get a
|
||||
// cached completed object
|
||||
skipWhile((rd: RemoteData<PaginatedList<T>>) => useCachedVersionIfAvailable ? rd.isStale : rd.hasCompleted),
|
||||
this.reRequestStaleRemoteData(reRequestOnStale, () =>
|
||||
this.findAllByHref(href$, findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow))
|
||||
);
|
||||
}
|
||||
|
@@ -32,6 +32,8 @@ describe('DsoPageAdministratorGuard', () => {
|
||||
let authService: AuthService;
|
||||
let resolver: Resolve<RemoteData<any>>;
|
||||
let object: DSpaceObject;
|
||||
let route;
|
||||
let parentRoute;
|
||||
|
||||
function init() {
|
||||
object = {
|
||||
@@ -50,6 +52,16 @@ describe('DsoPageAdministratorGuard', () => {
|
||||
authService = jasmine.createSpyObj('authService', {
|
||||
isAuthenticated: observableOf(true)
|
||||
});
|
||||
parentRoute = {
|
||||
params: {
|
||||
id: '3e1a5327-dabb-41ff-af93-e6cab9d032f0'
|
||||
}
|
||||
};
|
||||
route = {
|
||||
params: {
|
||||
},
|
||||
parent: parentRoute
|
||||
};
|
||||
guard = new DsoPageFeatureGuardImpl(resolver, authorizationService, router, authService, undefined);
|
||||
}
|
||||
|
||||
@@ -59,10 +71,17 @@ describe('DsoPageAdministratorGuard', () => {
|
||||
|
||||
describe('getObjectUrl', () => {
|
||||
it('should return the resolved object\'s selflink', (done) => {
|
||||
guard.getObjectUrl(undefined, undefined).subscribe((selflink) => {
|
||||
guard.getObjectUrl(route, undefined).subscribe((selflink) => {
|
||||
expect(selflink).toEqual(object.self);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRouteWithDSOId', () => {
|
||||
it('should return the route that has the UUID of the DSO', () => {
|
||||
const foundRoute = (guard as any).getRouteWithDSOId(route);
|
||||
expect(foundRoute).toBe(parentRoute);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -7,6 +7,7 @@ import { map } from 'rxjs/operators';
|
||||
import { DSpaceObject } from '../../../shared/dspace-object.model';
|
||||
import { FeatureAuthorizationGuard } from './feature-authorization.guard';
|
||||
import { AuthService } from '../../../auth/auth.service';
|
||||
import { hasNoValue, hasValue } from '../../../../shared/empty.util';
|
||||
|
||||
/**
|
||||
* Abstract Guard for preventing unauthorized access to {@link DSpaceObject} pages that require rights for a specific feature
|
||||
@@ -24,9 +25,22 @@ export abstract class DsoPageFeatureGuard<T extends DSpaceObject> extends Featur
|
||||
* Check authorization rights for the object resolved using the provided resolver
|
||||
*/
|
||||
getObjectUrl(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<string> {
|
||||
return (this.resolver.resolve(route, state) as Observable<RemoteData<T>>).pipe(
|
||||
const routeWithObjectID = this.getRouteWithDSOId(route);
|
||||
return (this.resolver.resolve(routeWithObjectID, state) as Observable<RemoteData<T>>).pipe(
|
||||
getAllSucceededRemoteDataPayload(),
|
||||
map((dso) => dso.self)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to resolve resolve (parent) route that contains the UUID of the DSO
|
||||
* @param route The current route
|
||||
*/
|
||||
protected getRouteWithDSOId(route: ActivatedRouteSnapshot): ActivatedRouteSnapshot {
|
||||
let routeWithDSOId = route;
|
||||
while (hasNoValue(routeWithDSOId.params.id) && hasValue(routeWithDSOId.parent)) {
|
||||
routeWithDSOId = routeWithDSOId.parent;
|
||||
}
|
||||
return routeWithDSOId;
|
||||
}
|
||||
}
|
||||
|
@@ -22,6 +22,7 @@ import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
import { NoContent } from '../shared/NoContent.model';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import { Operation } from 'fast-json-patch';
|
||||
import { getFirstCompletedRemoteData } from '../shared/operators';
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
/**
|
||||
@@ -57,15 +58,23 @@ class DataServiceImpl extends ItemDataService {
|
||||
super(requestService, rdbService, store, bs, objectCache, halService, notificationsService, http, comparator, bundleService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the endpoint based on a collection
|
||||
* @param collectionID The ID of the collection to base the endpoint on
|
||||
*/
|
||||
public getCollectionEndpoint(collectionID: string): Observable<string> {
|
||||
return this.collectionService.getIDHrefObs(collectionID).pipe(
|
||||
switchMap((href: string) => this.halService.getEndpoint(this.collectionLinkPath, href))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the endpoint to be based on a collection
|
||||
* @param collectionID The ID of the collection to base the endpoint on
|
||||
*/
|
||||
private setCollectionEndpoint(collectionID: string) {
|
||||
this.collectionEndpoint = true;
|
||||
this.endpoint$ = this.collectionService.getIDHrefObs(collectionID).pipe(
|
||||
switchMap((href: string) => this.halService.getEndpoint(this.collectionLinkPath, href))
|
||||
);
|
||||
this.endpoint$ = this.getCollectionEndpoint(collectionID);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -130,6 +139,7 @@ class DataServiceImpl extends ItemDataService {
|
||||
deleteByCollectionID(item: Item, collectionID: string): Observable<boolean> {
|
||||
this.setRegularEndpoint();
|
||||
return super.delete(item.uuid).pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
map((response: RemoteData<NoContent>) => hasValue(response) && response.hasSucceeded)
|
||||
);
|
||||
}
|
||||
@@ -209,5 +219,13 @@ export class ItemTemplateDataService implements UpdateDataService<Item> {
|
||||
deleteByCollectionID(item: Item, collectionID: string): Observable<boolean> {
|
||||
return this.dataService.deleteByCollectionID(item, collectionID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the endpoint based on a collection
|
||||
* @param collectionID The ID of the collection to base the endpoint on
|
||||
*/
|
||||
getCollectionEndpoint(collectionID: string): Observable<string> {
|
||||
return this.dataService.getCollectionEndpoint(collectionID);
|
||||
}
|
||||
}
|
||||
/* tslint:enable:max-classes-per-file */
|
||||
|
@@ -110,7 +110,6 @@ export abstract class TasksService<T extends CacheableObject> extends DataServic
|
||||
find((href: string) => hasValue(href)),
|
||||
mergeMap((href) => this.findByHref(href, false, true).pipe(
|
||||
getAllCompletedRemoteData(),
|
||||
filter((rd: RemoteData<T>) => !rd.isSuccessStale),
|
||||
tap(() => this.requestService.setStaleByHrefSubstring(href)))
|
||||
)
|
||||
);
|
||||
|
@@ -1,11 +1,11 @@
|
||||
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
|
||||
<ds-truncatable [id]="dso.id">
|
||||
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer"
|
||||
[routerLink]="[itemPageRoute]" class="lead"
|
||||
[innerHTML]="firstMetadataValue('dc.title')"></a>
|
||||
[routerLink]="[itemPageRoute]" class="lead item-list-title"
|
||||
[innerHTML]="dsoTitle"></a>
|
||||
<span *ngIf="linkType == linkTypes.None"
|
||||
class="lead"
|
||||
[innerHTML]="firstMetadataValue('dc.title')"></span>
|
||||
class="lead item-list-title"
|
||||
[innerHTML]="dsoTitle"></span>
|
||||
<span class="text-muted">
|
||||
<ds-truncatable-part [id]="dso.id" [minLines]="1">
|
||||
<span *ngIf="dso.allMetadata(['publicationvolume.volumeNumber']).length > 0"
|
||||
|
@@ -7,6 +7,8 @@ import { JournalIssueSearchResultListElementComponent } from './journal-issue-se
|
||||
import { Item } from '../../../../../core/shared/item.model';
|
||||
import { TruncatePipe } from '../../../../../shared/utils/truncate.pipe';
|
||||
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
|
||||
|
||||
let journalIssueListElementComponent: JournalIssueSearchResultListElementComponent;
|
||||
let fixture: ComponentFixture<JournalIssueSearchResultListElementComponent>;
|
||||
@@ -60,7 +62,8 @@ describe('JournalIssueSearchResultListElementComponent', () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [JournalIssueSearchResultListElementComponent, TruncatePipe],
|
||||
providers: [
|
||||
{ provide: TruncatableService, useValue: {} }
|
||||
{ provide: TruncatableService, useValue: {} },
|
||||
{ provide: DSONameService, useClass: DSONameServiceMock }
|
||||
],
|
||||
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
|
@@ -1,11 +1,11 @@
|
||||
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
|
||||
<ds-truncatable [id]="dso.id">
|
||||
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer"
|
||||
[routerLink]="[itemPageRoute]" class="lead"
|
||||
[innerHTML]="firstMetadataValue('dc.title')"></a>
|
||||
[routerLink]="[itemPageRoute]" class="lead item-list-title"
|
||||
[innerHTML]="dsoTitle"></a>
|
||||
<span *ngIf="linkType == linkTypes.None"
|
||||
class="lead"
|
||||
[innerHTML]="firstMetadataValue('dc.title')"></span>
|
||||
class="lead item-list-title"
|
||||
[innerHTML]="dsoTitle"></span>
|
||||
<span class="text-muted">
|
||||
<ds-truncatable-part [id]="dso.id" [minLines]="1">
|
||||
<span *ngIf="dso.allMetadata(['journal.title']).length > 0"
|
||||
|
@@ -7,6 +7,8 @@ import { Item } from '../../../../../core/shared/item.model';
|
||||
import { JournalVolumeSearchResultListElementComponent } from './journal-volume-search-result-list-element.component';
|
||||
import { TruncatePipe } from '../../../../../shared/utils/truncate.pipe';
|
||||
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
|
||||
|
||||
let journalVolumeListElementComponent: JournalVolumeSearchResultListElementComponent;
|
||||
let fixture: ComponentFixture<JournalVolumeSearchResultListElementComponent>;
|
||||
@@ -59,7 +61,8 @@ describe('JournalVolumeSearchResultListElementComponent', () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [JournalVolumeSearchResultListElementComponent, TruncatePipe],
|
||||
providers: [
|
||||
{ provide: TruncatableService, useValue: {} }
|
||||
{ provide: TruncatableService, useValue: {} },
|
||||
{ provide: DSONameService, useClass: DSONameServiceMock },
|
||||
],
|
||||
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
|
@@ -1,11 +1,11 @@
|
||||
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
|
||||
<ds-truncatable [id]="dso.id">
|
||||
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer"
|
||||
[routerLink]="[itemPageRoute]" class="lead"
|
||||
[innerHTML]="firstMetadataValue('dc.title')"></a>
|
||||
[routerLink]="[itemPageRoute]" class="lead item-list-title"
|
||||
[innerHTML]="dsoTitle"></a>
|
||||
<span *ngIf="linkType == linkTypes.None"
|
||||
class="lead"
|
||||
[innerHTML]="firstMetadataValue('dc.title')"></span>
|
||||
class="lead item-list-title"
|
||||
[innerHTML]="dsoTitle"></span>
|
||||
<span class="text-muted">
|
||||
<ds-truncatable-part [id]="dso.id" [minLines]="1">
|
||||
<span *ngIf="dso.allMetadata(['creativeworkseries.issn']).length > 0"
|
||||
|
@@ -7,6 +7,8 @@ import { Item } from '../../../../../core/shared/item.model';
|
||||
import { TruncatePipe } from '../../../../../shared/utils/truncate.pipe';
|
||||
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||
import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model';
|
||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
|
||||
|
||||
let journalListElementComponent: JournalSearchResultListElementComponent;
|
||||
let fixture: ComponentFixture<JournalSearchResultListElementComponent>;
|
||||
@@ -55,7 +57,8 @@ describe('JournalSearchResultListElementComponent', () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [JournalSearchResultListElementComponent, TruncatePipe],
|
||||
providers: [
|
||||
{ provide: TruncatableService, useValue: {} }
|
||||
{ provide: TruncatableService, useValue: {} },
|
||||
{ provide: DSONameService, useClass: DSONameServiceMock },
|
||||
],
|
||||
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
|
@@ -7,6 +7,8 @@ import { Item } from '../../../../../core/shared/item.model';
|
||||
import { TruncatePipe } from '../../../../../shared/utils/truncate.pipe';
|
||||
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||
import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model';
|
||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
|
||||
|
||||
let orgUnitListElementComponent: OrgUnitSearchResultListElementComponent;
|
||||
let fixture: ComponentFixture<OrgUnitSearchResultListElementComponent>;
|
||||
@@ -53,7 +55,8 @@ describe('OrgUnitSearchResultListElementComponent', () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ OrgUnitSearchResultListElementComponent , TruncatePipe],
|
||||
providers: [
|
||||
{ provide: TruncatableService, useValue: {} }
|
||||
{ provide: TruncatableService, useValue: {} },
|
||||
{ provide: DSONameService, useClass: DSONameServiceMock }
|
||||
],
|
||||
|
||||
schemas: [ NO_ERRORS_SCHEMA ]
|
||||
|
@@ -7,6 +7,8 @@ import { PersonSearchResultListElementComponent } from './person-search-result-l
|
||||
import { Item } from '../../../../../core/shared/item.model';
|
||||
import { TruncatePipe } from '../../../../../shared/utils/truncate.pipe';
|
||||
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
|
||||
|
||||
let personListElementComponent: PersonSearchResultListElementComponent;
|
||||
let fixture: ComponentFixture<PersonSearchResultListElementComponent>;
|
||||
@@ -53,7 +55,8 @@ describe('PersonSearchResultListElementComponent', () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [PersonSearchResultListElementComponent, TruncatePipe],
|
||||
providers: [
|
||||
{ provide: TruncatableService, useValue: {} }
|
||||
{ provide: TruncatableService, useValue: {} },
|
||||
{ provide: DSONameService, useClass: DSONameServiceMock }
|
||||
],
|
||||
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
|
@@ -1,11 +1,11 @@
|
||||
<ds-truncatable [id]="dso.id">
|
||||
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
|
||||
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer"
|
||||
[routerLink]="[itemPageRoute]" class="lead"
|
||||
[innerHTML]="firstMetadataValue('dc.title')"></a>
|
||||
[routerLink]="[itemPageRoute]" class="lead item-list-title"
|
||||
[innerHTML]="dsoTitle"></a>
|
||||
<span *ngIf="linkType == linkTypes.None"
|
||||
class="lead"
|
||||
[innerHTML]="firstMetadataValue('dc.title')"></span>
|
||||
class="lead item-list-title"
|
||||
[innerHTML]="dsoTitle"></span>
|
||||
<!--<span class="text-muted">-->
|
||||
<!--<ds-truncatable-part [id]="dso.id" [minLines]="1">-->
|
||||
<!--<span *ngIf="dso.allMetadata(['project.identifier.status']).length > 0"-->
|
||||
|
@@ -6,6 +6,8 @@ import { ProjectSearchResultListElementComponent } from './project-search-result
|
||||
import { Item } from '../../../../../core/shared/item.model';
|
||||
import { TruncatePipe } from '../../../../../shared/utils/truncate.pipe';
|
||||
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
|
||||
|
||||
let projectListElementComponent: ProjectSearchResultListElementComponent;
|
||||
let fixture: ComponentFixture<ProjectSearchResultListElementComponent>;
|
||||
@@ -53,7 +55,8 @@ describe('ProjectSearchResultListElementComponent', () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ProjectSearchResultListElementComponent, TruncatePipe],
|
||||
providers: [
|
||||
{ provide: TruncatableService, useValue: {} }
|
||||
{ provide: TruncatableService, useValue: {} },
|
||||
{ provide: DSONameService, useClass: DSONameServiceMock }
|
||||
],
|
||||
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
|
@@ -9,6 +9,7 @@ import { isNotEmpty } from '../../../../../shared/empty.util';
|
||||
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||
import { LinkService } from '../../../../../core/cache/builders/link.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||
|
||||
@listableObjectComponent('PersonSearchResult', ViewMode.ListElement, Context.SideBarSearchModal)
|
||||
@listableObjectComponent('PersonSearchResult', ViewMode.ListElement, Context.SideBarSearchModalCurrent)
|
||||
@@ -23,8 +24,10 @@ import { TranslateService } from '@ngx-translate/core';
|
||||
export class PersonSidebarSearchListElementComponent extends SidebarSearchListElementComponent<ItemSearchResult, Item> {
|
||||
constructor(protected truncatableService: TruncatableService,
|
||||
protected linkService: LinkService,
|
||||
protected translateService: TranslateService) {
|
||||
super(truncatableService, linkService);
|
||||
protected translateService: TranslateService,
|
||||
protected dsoNameService: DSONameService
|
||||
) {
|
||||
super(truncatableService, linkService, dsoNameService);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -27,6 +27,8 @@ import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-
|
||||
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||
import { TruncatePipe } from '../../../../../shared/utils/truncate.pipe';
|
||||
import { OrgUnitSearchResultListSubmissionElementComponent } from './org-unit-search-result-list-submission-element.component';
|
||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
|
||||
|
||||
let personListElementComponent: OrgUnitSearchResultListSubmissionElementComponent;
|
||||
let fixture: ComponentFixture<OrgUnitSearchResultListSubmissionElementComponent>;
|
||||
@@ -115,6 +117,7 @@ describe('OrgUnitSearchResultListSubmissionElementComponent', () => {
|
||||
{ provide: DSOChangeAnalyzer, useValue: {} },
|
||||
{ provide: DefaultChangeAnalyzer, useValue: {} },
|
||||
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
|
||||
{ provide: DSONameService, useClass: DSONameServiceMock }
|
||||
],
|
||||
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
|
@@ -19,6 +19,7 @@ import { MetadataValue } from '../../../../../core/shared/metadata.models';
|
||||
import { ItemDataService } from '../../../../../core/data/item-data.service';
|
||||
import { SelectableListService } from '../../../../../shared/object-list/selectable-list/selectable-list.service';
|
||||
import { NameVariantModalComponent } from '../../name-variant-modal/name-variant-modal.component';
|
||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||
|
||||
@listableObjectComponent('OrgUnitSearchResult', ViewMode.ListElement, Context.EntitySearchModal)
|
||||
@listableObjectComponent('OrgUnitSearchResult', ViewMode.ListElement, Context.EntitySearchModalWithNameVariants)
|
||||
@@ -44,8 +45,10 @@ export class OrgUnitSearchResultListSubmissionElementComponent extends SearchRes
|
||||
private modalService: NgbModal,
|
||||
private itemDataService: ItemDataService,
|
||||
private bitstreamDataService: BitstreamDataService,
|
||||
private selectableListService: SelectableListService) {
|
||||
super(truncatableService);
|
||||
private selectableListService: SelectableListService,
|
||||
protected dsoNameService: DSONameService
|
||||
) {
|
||||
super(truncatableService, dsoNameService);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
|
@@ -19,6 +19,7 @@ import { NameVariantModalComponent } from '../../name-variant-modal/name-variant
|
||||
import { MetadataValue } from '../../../../../core/shared/metadata.models';
|
||||
import { ItemDataService } from '../../../../../core/data/item-data.service';
|
||||
import { SelectableListService } from '../../../../../shared/object-list/selectable-list/selectable-list.service';
|
||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||
|
||||
@listableObjectComponent('PersonSearchResult', ViewMode.ListElement, Context.EntitySearchModalWithNameVariants)
|
||||
@Component({
|
||||
@@ -42,8 +43,10 @@ export class PersonSearchResultListSubmissionElementComponent extends SearchResu
|
||||
private modalService: NgbModal,
|
||||
private itemDataService: ItemDataService,
|
||||
private bitstreamDataService: BitstreamDataService,
|
||||
private selectableListService: SelectableListService) {
|
||||
super(truncatableService);
|
||||
private selectableListService: SelectableListService,
|
||||
protected dsoNameService: DSONameService
|
||||
) {
|
||||
super(truncatableService, dsoNameService);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
|
25
src/app/footer/themed-footer.component.ts
Normal file
25
src/app/footer/themed-footer.component.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ThemedComponent } from '../shared/theme-support/themed.component';
|
||||
import { FooterComponent } from './footer.component';
|
||||
|
||||
/**
|
||||
* Themed wrapper for FooterComponent
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-themed-footer',
|
||||
styleUrls: ['footer.component.scss'],
|
||||
templateUrl: '../shared/theme-support/themed.component.html',
|
||||
})
|
||||
export class ThemedFooterComponent extends ThemedComponent<FooterComponent> {
|
||||
protected getComponentName(): string {
|
||||
return 'FooterComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../themes/${themeName}/app/footer/footer.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import(`./footer.component`);
|
||||
}
|
||||
}
|
26
src/app/forbidden/themed-forbidden.component.ts
Normal file
26
src/app/forbidden/themed-forbidden.component.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ThemedComponent } from '../shared/theme-support/themed.component';
|
||||
import { ForbiddenComponent } from './forbidden.component';
|
||||
|
||||
/**
|
||||
* Themed wrapper for ForbiddenComponent
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-themed-forbidden',
|
||||
styleUrls: [],
|
||||
templateUrl: '../shared/theme-support/themed.component.html',
|
||||
})
|
||||
export class ThemedForbiddenComponent extends ThemedComponent<ForbiddenComponent> {
|
||||
protected getComponentName(): string {
|
||||
return 'ForbiddenComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../themes/${themeName}/app/forbidden/forbidden.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import(`./forbidden.component`);
|
||||
}
|
||||
|
||||
}
|
@@ -2,6 +2,7 @@ import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-forgot-email',
|
||||
styleUrls: ['./forgot-email.component.scss'],
|
||||
templateUrl: './forgot-email.component.html'
|
||||
})
|
||||
/**
|
||||
|
@@ -0,0 +1,26 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ThemedComponent } from '../../shared/theme-support/themed.component';
|
||||
import { ForgotEmailComponent } from './forgot-email.component';
|
||||
|
||||
/**
|
||||
* Themed wrapper for ForgotEmailComponent
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-themed-forgot-email',
|
||||
styleUrls: [],
|
||||
templateUrl: './../../shared/theme-support/themed.component.html'
|
||||
})
|
||||
export class ThemedForgotEmailComponent extends ThemedComponent<ForgotEmailComponent> {
|
||||
protected getComponentName(): string {
|
||||
return 'ForgotEmailComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../../themes/${themeName}/app/forgot-password/forgot-password-email/forgot-email.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import(`./forgot-email.component`);
|
||||
}
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user