mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge branch 'master' into w2p-68729_List-version-history
Conflicts: src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts src/app/+item-page/item-page-routing.module.ts src/app/+item-page/item-page.resolver.ts src/app/core/shared/item.model.ts
This commit is contained in:
@@ -6,5 +6,7 @@ WORKDIR /app
|
|||||||
ADD . /app/
|
ADD . /app/
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
RUN yarn install
|
# We run yarn install with an increased network timeout (5min) to avoid "ESOCKETTIMEDOUT" errors from hub.docker.com
|
||||||
|
# See, for example https://github.com/yarnpkg/yarn/issues/5540
|
||||||
|
RUN yarn install --network-timeout 300000
|
||||||
CMD yarn run watch
|
CMD yarn run watch
|
||||||
|
@@ -140,7 +140,7 @@ module.exports = {
|
|||||||
}, {
|
}, {
|
||||||
code: 'nl',
|
code: 'nl',
|
||||||
label: 'Nederlands',
|
label: 'Nederlands',
|
||||||
active: false,
|
active: true,
|
||||||
}, {
|
}, {
|
||||||
code: 'pt',
|
code: 'pt',
|
||||||
label: 'Português',
|
label: 'Português',
|
||||||
|
@@ -196,6 +196,14 @@
|
|||||||
|
|
||||||
"browse.metadata.title": "Title",
|
"browse.metadata.title": "Title",
|
||||||
|
|
||||||
|
"browse.metadata.author.breadcrumbs": "Browse by Author",
|
||||||
|
|
||||||
|
"browse.metadata.dateissued.breadcrumbs": "Browse by Date",
|
||||||
|
|
||||||
|
"browse.metadata.subject.breadcrumbs": "Browse by Subject",
|
||||||
|
|
||||||
|
"browse.metadata.title.breadcrumbs": "Browse by Title",
|
||||||
|
|
||||||
"browse.startsWith.choose_start": "(Choose start)",
|
"browse.startsWith.choose_start": "(Choose start)",
|
||||||
|
|
||||||
"browse.startsWith.choose_year": "(Choose year)",
|
"browse.startsWith.choose_year": "(Choose year)",
|
||||||
@@ -237,7 +245,6 @@
|
|||||||
"browse.title": "Browsing {{ collection }} by {{ field }} {{ value }}",
|
"browse.title": "Browsing {{ collection }} by {{ field }} {{ value }}",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"chips.remove": "Remove chip",
|
"chips.remove": "Remove chip",
|
||||||
|
|
||||||
|
|
||||||
@@ -266,6 +273,8 @@
|
|||||||
|
|
||||||
"collection.edit.head": "Edit Collection",
|
"collection.edit.head": "Edit Collection",
|
||||||
|
|
||||||
|
"collection.edit.breadcrumbs": "Edit Collection",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"collection.edit.item-mapper.cancel": "Cancel",
|
"collection.edit.item-mapper.cancel": "Cancel",
|
||||||
@@ -450,6 +459,7 @@
|
|||||||
|
|
||||||
"community.edit.head": "Edit Community",
|
"community.edit.head": "Edit Community",
|
||||||
|
|
||||||
|
"community.edit.breadcrumbs": "Edit Community",
|
||||||
|
|
||||||
|
|
||||||
"community.edit.logo.label": "Community logo",
|
"community.edit.logo.label": "Community logo",
|
||||||
@@ -657,6 +667,8 @@
|
|||||||
|
|
||||||
"item.edit.head": "Edit Item",
|
"item.edit.head": "Edit Item",
|
||||||
|
|
||||||
|
"item.edit.breadcrumbs": "Edit Item",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"item.edit.item-mapper.buttons.add": "Map item to selected collections",
|
"item.edit.item-mapper.buttons.add": "Map item to selected collections",
|
||||||
@@ -1096,6 +1108,8 @@
|
|||||||
|
|
||||||
"login.title": "Login",
|
"login.title": "Login",
|
||||||
|
|
||||||
|
"login.breadcrumbs": "Login",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"logout.form.header": "Log out from DSpace",
|
"logout.form.header": "Log out from DSpace",
|
||||||
@@ -1494,6 +1508,7 @@
|
|||||||
|
|
||||||
"search.title": "DSpace Angular :: Search",
|
"search.title": "DSpace Angular :: Search",
|
||||||
|
|
||||||
|
"search.breadcrumbs": "Search",
|
||||||
|
|
||||||
|
|
||||||
"search.filters.applied.f.author": "Author",
|
"search.filters.applied.f.author": "Author",
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -230,10 +230,10 @@ describe('BitstreamFormatsComponent', () => {
|
|||||||
comp.deleteFormats();
|
comp.deleteFormats();
|
||||||
|
|
||||||
expect(bitstreamFormatService.clearBitStreamFormatRequests).toHaveBeenCalled();
|
expect(bitstreamFormatService.clearBitStreamFormatRequests).toHaveBeenCalled();
|
||||||
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat1);
|
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat1.id);
|
||||||
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat2);
|
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat2.id);
|
||||||
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat3);
|
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat3.id);
|
||||||
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat4);
|
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat4.id);
|
||||||
|
|
||||||
expect(notificationsServiceStub.success).toHaveBeenCalledWith('admin.registries.bitstream-formats.delete.success.head',
|
expect(notificationsServiceStub.success).toHaveBeenCalledWith('admin.registries.bitstream-formats.delete.success.head',
|
||||||
'admin.registries.bitstream-formats.delete.success.amount');
|
'admin.registries.bitstream-formats.delete.success.amount');
|
||||||
@@ -276,10 +276,10 @@ describe('BitstreamFormatsComponent', () => {
|
|||||||
comp.deleteFormats();
|
comp.deleteFormats();
|
||||||
|
|
||||||
expect(bitstreamFormatService.clearBitStreamFormatRequests).toHaveBeenCalled();
|
expect(bitstreamFormatService.clearBitStreamFormatRequests).toHaveBeenCalled();
|
||||||
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat1);
|
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat1.id);
|
||||||
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat2);
|
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat2.id);
|
||||||
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat3);
|
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat3.id);
|
||||||
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat4);
|
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat4.id);
|
||||||
|
|
||||||
expect(notificationsServiceStub.error).toHaveBeenCalledWith('admin.registries.bitstream-formats.delete.failure.head',
|
expect(notificationsServiceStub.error).toHaveBeenCalledWith('admin.registries.bitstream-formats.delete.failure.head',
|
||||||
'admin.registries.bitstream-formats.delete.failure.amount');
|
'admin.registries.bitstream-formats.delete.failure.amount');
|
||||||
|
@@ -64,7 +64,7 @@ export class BitstreamFormatsComponent implements OnInit {
|
|||||||
const tasks$ = [];
|
const tasks$ = [];
|
||||||
for (const format of formats) {
|
for (const format of formats) {
|
||||||
if (hasValue(format.id)) {
|
if (hasValue(format.id)) {
|
||||||
tasks$.push(this.bitstreamFormatService.delete(format));
|
tasks$.push(this.bitstreamFormatService.delete(format.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
zip(...tasks$).subscribe((results: boolean[]) => {
|
zip(...tasks$).subscribe((results: boolean[]) => {
|
||||||
|
@@ -20,7 +20,6 @@ import { Item } from '../../core/shared/item.model';
|
|||||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||||
import { Community } from '../../core/shared/community.model';
|
import { Community } from '../../core/shared/community.model';
|
||||||
import { MockRouter } from '../../shared/mocks/mock-router';
|
import { MockRouter } from '../../shared/mocks/mock-router';
|
||||||
import { ResourceType } from '../../core/shared/resource-type';
|
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils';
|
||||||
import { BrowseEntry } from '../../core/shared/browse-entry.model';
|
import { BrowseEntry } from '../../core/shared/browse-entry.model';
|
||||||
import { VarDirective } from '../../shared/utils/var.directive';
|
import { VarDirective } from '../../shared/utils/var.directive';
|
||||||
|
41
src/app/+browse-by/browse-by-dso-breadcrumb.resolver.ts
Normal file
41
src/app/+browse-by/browse-by-dso-breadcrumb.resolver.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Community } from '../core/shared/community.model';
|
||||||
|
import { DSpaceObjectDataService } from '../core/data/dspace-object-data.service';
|
||||||
|
import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service';
|
||||||
|
import { Collection } from '../core/shared/collection.model';
|
||||||
|
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { BreadcrumbConfig } from '../breadcrumbs/breadcrumb/breadcrumb-config.model';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { getRemoteDataPayload, getSucceededRemoteData } from '../core/shared/operators';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
import { hasValue } from '../shared/empty.util';
|
||||||
|
import { getDSOPath } from '../app-routing.module';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The class that resolves the BreadcrumbConfig object for a DSpaceObject on a browse by page
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class BrowseByDSOBreadcrumbResolver {
|
||||||
|
constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: DSpaceObjectDataService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method for resolving a breadcrumb config object
|
||||||
|
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
|
||||||
|
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
|
||||||
|
* @returns BreadcrumbConfig object
|
||||||
|
*/
|
||||||
|
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<BreadcrumbConfig<Community | Collection>> {
|
||||||
|
const uuid = route.queryParams.scope;
|
||||||
|
if (hasValue(uuid)) {
|
||||||
|
return this.dataService.findById(uuid).pipe(
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
map((object: Community | Collection) => {
|
||||||
|
return { provider: this.breadcrumbService, key: object, url: getDSOPath(object) };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
@@ -37,24 +37,24 @@ export class BrowseByGuard implements CanActivate {
|
|||||||
return dsoAndMetadata$.pipe(
|
return dsoAndMetadata$.pipe(
|
||||||
map((dsoRD) => {
|
map((dsoRD) => {
|
||||||
const name = dsoRD.payload.name;
|
const name = dsoRD.payload.name;
|
||||||
route.data = this.createData(title, id, metadataField, name, metadataTranslated, value);
|
route.data = this.createData(title, id, metadataField, name, metadataTranslated, value, route);
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
route.data = this.createData(title, id, metadataField, '', metadataTranslated, value);
|
route.data = this.createData(title, id, metadataField, '', metadataTranslated, value, route);
|
||||||
return observableOf(true);
|
return observableOf(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private createData(title, id, metadataField, collection, field, value) {
|
private createData(title, id, metadataField, collection, field, value, route) {
|
||||||
return {
|
return Object.assign({}, route.data, {
|
||||||
title: title,
|
title: title,
|
||||||
id: id,
|
id: id,
|
||||||
metadataField: metadataField,
|
metadataField: metadataField,
|
||||||
collection: collection,
|
collection: collection,
|
||||||
field: field,
|
field: field,
|
||||||
value: hasValue(value) ? `"${value}"` : ''
|
value: hasValue(value) ? `"${value}"` : ''
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
28
src/app/+browse-by/browse-by-i18n-breadcrumb.resolver.ts
Normal file
28
src/app/+browse-by/browse-by-i18n-breadcrumb.resolver.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service';
|
||||||
|
import { BreadcrumbConfig } from '../breadcrumbs/breadcrumb/breadcrumb-config.model';
|
||||||
|
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class resolves a BreadcrumbConfig object with an i18n key string for a route
|
||||||
|
* It adds the metadata field of the current browse-by page
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class BrowseByI18nBreadcrumbResolver extends I18nBreadcrumbResolver {
|
||||||
|
constructor(protected breadcrumbService: I18nBreadcrumbsService) {
|
||||||
|
super(breadcrumbService);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method for resolving a browse-by i18n breadcrumb configuration object
|
||||||
|
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
|
||||||
|
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
|
||||||
|
* @returns BreadcrumbConfig object for a browse-by page
|
||||||
|
*/
|
||||||
|
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): BreadcrumbConfig<string> {
|
||||||
|
const extendedBreadcrumbKey = route.data.breadcrumbKey + '.' + route.params.id;
|
||||||
|
route.data = Object.assign({}, route.data, { breadcrumbKey: extendedBreadcrumbKey });
|
||||||
|
return super.resolve(route, state);
|
||||||
|
}
|
||||||
|
}
|
@@ -2,12 +2,29 @@ import { RouterModule } from '@angular/router';
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { BrowseByGuard } from './browse-by-guard';
|
import { BrowseByGuard } from './browse-by-guard';
|
||||||
import { BrowseBySwitcherComponent } from './+browse-by-switcher/browse-by-switcher.component';
|
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';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forChild([
|
RouterModule.forChild([
|
||||||
{ path: ':id', component: BrowseBySwitcherComponent, canActivate: [BrowseByGuard], data: { title: 'browse.title' } }
|
{
|
||||||
])
|
path: '',
|
||||||
|
resolve: { breadcrumb: BrowseByDSOBreadcrumbResolver },
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: ':id',
|
||||||
|
component: BrowseBySwitcherComponent,
|
||||||
|
canActivate: [BrowseByGuard],
|
||||||
|
resolve: { breadcrumb: BrowseByI18nBreadcrumbResolver },
|
||||||
|
data: { title: 'browse.title', breadcrumbKey: 'browse.metadata' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}])
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
BrowseByI18nBreadcrumbResolver,
|
||||||
|
BrowseByDSOBreadcrumbResolver
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class BrowseByRoutingModule {
|
export class BrowseByRoutingModule {
|
||||||
|
@@ -83,7 +83,7 @@ describe('CollectionItemMapperComponent', () => {
|
|||||||
const itemDataServiceStub = {
|
const itemDataServiceStub = {
|
||||||
mapToCollection: () => of(new RestResponse(true, 200, 'OK'))
|
mapToCollection: () => of(new RestResponse(true, 200, 'OK'))
|
||||||
};
|
};
|
||||||
const activatedRouteStub = new ActivatedRouteStub({}, { collection: mockCollectionRD });
|
const activatedRouteStub = new ActivatedRouteStub({}, { dso: mockCollectionRD });
|
||||||
const translateServiceStub = {
|
const translateServiceStub = {
|
||||||
get: () => of('test-message of collection ' + mockCollection.name),
|
get: () => of('test-message of collection ' + mockCollection.name),
|
||||||
onLangChange: new EventEmitter(),
|
onLangChange: new EventEmitter(),
|
||||||
|
@@ -102,7 +102,7 @@ export class CollectionItemMapperComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.collectionRD$ = this.route.data.pipe(map((data) => data.collection)).pipe(getSucceededRemoteData()) as Observable<RemoteData<Collection>>;
|
this.collectionRD$ = this.route.data.pipe(map((data) => data.dso)).pipe(getSucceededRemoteData()) as Observable<RemoteData<Collection>>;
|
||||||
this.searchOptions$ = this.searchConfigService.paginatedSearchOptions;
|
this.searchOptions$ = this.searchConfigService.paginatedSearchOptions;
|
||||||
this.loadItemLists();
|
this.loadItemLists();
|
||||||
}
|
}
|
||||||
|
@@ -10,6 +10,9 @@ import { DeleteCollectionPageComponent } from './delete-collection-page/delete-c
|
|||||||
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
||||||
import { getCollectionModulePath } from '../app-routing.module';
|
import { getCollectionModulePath } from '../app-routing.module';
|
||||||
import { CollectionItemMapperComponent } from './collection-item-mapper/collection-item-mapper.component';
|
import { CollectionItemMapperComponent } from './collection-item-mapper/collection-item-mapper.component';
|
||||||
|
import { CollectionBreadcrumbResolver } from '../core/breadcrumbs/collection-breadcrumb.resolver';
|
||||||
|
import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service';
|
||||||
|
import { LinkService } from '../core/cache/builders/link.service';
|
||||||
|
|
||||||
export const COLLECTION_PARENT_PARAMETER = 'parent';
|
export const COLLECTION_PARENT_PARAMETER = 'parent';
|
||||||
|
|
||||||
@@ -18,7 +21,7 @@ export function getCollectionPageRoute(collectionId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getCollectionEditPath(id: string) {
|
export function getCollectionEditPath(id: string) {
|
||||||
return new URLCombiner(getCollectionModulePath(), COLLECTION_EDIT_PATH.replace(/:id/, id)).toString()
|
return new URLCombiner(getCollectionModulePath(), id, COLLECTION_EDIT_PATH).toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCollectionCreatePath() {
|
export function getCollectionCreatePath() {
|
||||||
@@ -26,51 +29,54 @@ export function getCollectionCreatePath() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const COLLECTION_CREATE_PATH = 'create';
|
const COLLECTION_CREATE_PATH = 'create';
|
||||||
const COLLECTION_EDIT_PATH = ':id/edit';
|
const COLLECTION_EDIT_PATH = 'edit';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forChild([
|
RouterModule.forChild([
|
||||||
|
{
|
||||||
|
path: ':id',
|
||||||
|
resolve: {
|
||||||
|
dso: CollectionPageResolver,
|
||||||
|
breadcrumb: CollectionBreadcrumbResolver
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: COLLECTION_EDIT_PATH,
|
||||||
|
loadChildren: './edit-collection-page/edit-collection-page.module#EditCollectionPageModule',
|
||||||
|
canActivate: [AuthenticatedGuard]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'delete',
|
||||||
|
pathMatch: 'full',
|
||||||
|
component: DeleteCollectionPageComponent,
|
||||||
|
canActivate: [AuthenticatedGuard],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: CollectionPageComponent,
|
||||||
|
pathMatch: 'full',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/edit/mapper',
|
||||||
|
component: CollectionItemMapperComponent,
|
||||||
|
pathMatch: 'full',
|
||||||
|
canActivate: [AuthenticatedGuard]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: COLLECTION_CREATE_PATH,
|
path: COLLECTION_CREATE_PATH,
|
||||||
component: CreateCollectionPageComponent,
|
component: CreateCollectionPageComponent,
|
||||||
canActivate: [AuthenticatedGuard, CreateCollectionPageGuard]
|
canActivate: [AuthenticatedGuard, CreateCollectionPageGuard]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: COLLECTION_EDIT_PATH,
|
|
||||||
loadChildren: './edit-collection-page/edit-collection-page.module#EditCollectionPageModule',
|
|
||||||
canActivate: [AuthenticatedGuard]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ':id/delete',
|
|
||||||
pathMatch: 'full',
|
|
||||||
component: DeleteCollectionPageComponent,
|
|
||||||
canActivate: [AuthenticatedGuard],
|
|
||||||
resolve: {
|
|
||||||
dso: CollectionPageResolver
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ':id',
|
|
||||||
component: CollectionPageComponent,
|
|
||||||
pathMatch: 'full',
|
|
||||||
resolve: {
|
|
||||||
collection: CollectionPageResolver
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ':id/edit/mapper',
|
|
||||||
component: CollectionItemMapperComponent,
|
|
||||||
pathMatch: 'full',
|
|
||||||
resolve: {
|
|
||||||
collection: CollectionPageResolver
|
|
||||||
},
|
|
||||||
canActivate: [AuthenticatedGuard]
|
|
||||||
}
|
|
||||||
])
|
])
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
CollectionPageResolver,
|
CollectionPageResolver,
|
||||||
|
CollectionBreadcrumbResolver,
|
||||||
|
DSOBreadcrumbsService,
|
||||||
|
LinkService,
|
||||||
CreateCollectionPageGuard
|
CreateCollectionPageGuard
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
@@ -62,7 +62,7 @@ export class CollectionPageComponent implements OnInit {
|
|||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.collectionRD$ = this.route.data.pipe(
|
this.collectionRD$ = this.route.data.pipe(
|
||||||
map((data) => data.collection as RemoteData<Collection>),
|
map((data) => data.dso as RemoteData<Collection>),
|
||||||
redirectToPageNotFoundOn404(this.router),
|
redirectToPageNotFoundOn404(this.router),
|
||||||
take(1)
|
take(1)
|
||||||
);
|
);
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { EditCollectionPageComponent } from './edit-collection-page.component';
|
import { EditCollectionPageComponent } from './edit-collection-page.component';
|
||||||
import { CollectionPageResolver } from '../collection-page.resolver';
|
|
||||||
import { CollectionMetadataComponent } from './collection-metadata/collection-metadata.component';
|
import { CollectionMetadataComponent } from './collection-metadata/collection-metadata.component';
|
||||||
import { CollectionRolesComponent } from './collection-roles/collection-roles.component';
|
import { CollectionRolesComponent } from './collection-roles/collection-roles.component';
|
||||||
import { CollectionSourceComponent } from './collection-source/collection-source.component';
|
import { CollectionSourceComponent } from './collection-source/collection-source.component';
|
||||||
import { CollectionCurateComponent } from './collection-curate/collection-curate.component';
|
import { CollectionCurateComponent } from './collection-curate/collection-curate.component';
|
||||||
|
import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Routing module that handles the routing for the Edit Collection page administrator functionality
|
* Routing module that handles the routing for the Edit Collection page administrator functionality
|
||||||
@@ -15,10 +15,11 @@ import { CollectionCurateComponent } from './collection-curate/collection-curate
|
|||||||
RouterModule.forChild([
|
RouterModule.forChild([
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: EditCollectionPageComponent,
|
|
||||||
resolve: {
|
resolve: {
|
||||||
dso: CollectionPageResolver
|
breadcrumb: I18nBreadcrumbResolver
|
||||||
},
|
},
|
||||||
|
data: { breadcrumbKey: 'collection.edit' },
|
||||||
|
component: EditCollectionPageComponent,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
@@ -30,30 +31,28 @@ import { CollectionCurateComponent } from './collection-curate/collection-curate
|
|||||||
component: CollectionMetadataComponent,
|
component: CollectionMetadataComponent,
|
||||||
data: {
|
data: {
|
||||||
title: 'collection.edit.tabs.metadata.title',
|
title: 'collection.edit.tabs.metadata.title',
|
||||||
hideReturnButton: true
|
hideReturnButton: true,
|
||||||
|
showBreadcrumbs: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'roles',
|
path: 'roles',
|
||||||
component: CollectionRolesComponent,
|
component: CollectionRolesComponent,
|
||||||
data: { title: 'collection.edit.tabs.roles.title' }
|
data: { title: 'collection.edit.tabs.roles.title', showBreadcrumbs: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'source',
|
path: 'source',
|
||||||
component: CollectionSourceComponent,
|
component: CollectionSourceComponent,
|
||||||
data: { title: 'collection.edit.tabs.source.title' }
|
data: { title: 'collection.edit.tabs.source.title', showBreadcrumbs: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'curate',
|
path: 'curate',
|
||||||
component: CollectionCurateComponent,
|
component: CollectionCurateComponent,
|
||||||
data: { title: 'collection.edit.tabs.curate.title' }
|
data: { title: 'collection.edit.tabs.curate.title', showBreadcrumbs: true }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
],
|
|
||||||
providers: [
|
|
||||||
CollectionPageResolver,
|
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class EditCollectionPageRoutingModule {
|
export class EditCollectionPageRoutingModule {
|
||||||
|
@@ -9,6 +9,9 @@ import { CreateCommunityPageGuard } from './create-community-page/create-communi
|
|||||||
import { DeleteCommunityPageComponent } from './delete-community-page/delete-community-page.component';
|
import { DeleteCommunityPageComponent } from './delete-community-page/delete-community-page.component';
|
||||||
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
||||||
import { getCommunityModulePath } from '../app-routing.module';
|
import { getCommunityModulePath } from '../app-routing.module';
|
||||||
|
import { CommunityBreadcrumbResolver } from '../core/breadcrumbs/community-breadcrumb.resolver';
|
||||||
|
import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service';
|
||||||
|
import { LinkService } from '../core/cache/builders/link.service';
|
||||||
|
|
||||||
export const COMMUNITY_PARENT_PARAMETER = 'parent';
|
export const COMMUNITY_PARENT_PARAMETER = 'parent';
|
||||||
|
|
||||||
@@ -17,7 +20,7 @@ export function getCommunityPageRoute(communityId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getCommunityEditPath(id: string) {
|
export function getCommunityEditPath(id: string) {
|
||||||
return new URLCombiner(getCommunityModulePath(), COMMUNITY_EDIT_PATH.replace(/:id/, id)).toString()
|
return new URLCombiner(getCommunityModulePath(), id, COMMUNITY_EDIT_PATH).toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCommunityCreatePath() {
|
export function getCommunityCreatePath() {
|
||||||
@@ -25,42 +28,48 @@ export function getCommunityCreatePath() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const COMMUNITY_CREATE_PATH = 'create';
|
const COMMUNITY_CREATE_PATH = 'create';
|
||||||
const COMMUNITY_EDIT_PATH = ':id/edit';
|
const COMMUNITY_EDIT_PATH = 'edit';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forChild([
|
RouterModule.forChild([
|
||||||
|
{
|
||||||
|
path: ':id',
|
||||||
|
resolve: {
|
||||||
|
dso: CommunityPageResolver,
|
||||||
|
breadcrumb: CommunityBreadcrumbResolver
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: COMMUNITY_EDIT_PATH,
|
||||||
|
loadChildren: './edit-community-page/edit-community-page.module#EditCommunityPageModule',
|
||||||
|
canActivate: [AuthenticatedGuard]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'delete',
|
||||||
|
pathMatch: 'full',
|
||||||
|
component: DeleteCommunityPageComponent,
|
||||||
|
canActivate: [AuthenticatedGuard],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: CommunityPageComponent,
|
||||||
|
pathMatch: 'full',
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: COMMUNITY_CREATE_PATH,
|
path: COMMUNITY_CREATE_PATH,
|
||||||
component: CreateCommunityPageComponent,
|
component: CreateCommunityPageComponent,
|
||||||
canActivate: [AuthenticatedGuard, CreateCommunityPageGuard]
|
canActivate: [AuthenticatedGuard, CreateCommunityPageGuard]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: COMMUNITY_EDIT_PATH,
|
|
||||||
loadChildren: './edit-community-page/edit-community-page.module#EditCommunityPageModule',
|
|
||||||
canActivate: [AuthenticatedGuard]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ':id/delete',
|
|
||||||
pathMatch: 'full',
|
|
||||||
component: DeleteCommunityPageComponent,
|
|
||||||
canActivate: [AuthenticatedGuard],
|
|
||||||
resolve: {
|
|
||||||
dso: CommunityPageResolver
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ':id',
|
|
||||||
component: CommunityPageComponent,
|
|
||||||
pathMatch: 'full',
|
|
||||||
resolve: {
|
|
||||||
community: CommunityPageResolver
|
|
||||||
}
|
|
||||||
}
|
|
||||||
])
|
])
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
CommunityPageResolver,
|
CommunityPageResolver,
|
||||||
|
CommunityBreadcrumbResolver,
|
||||||
|
DSOBreadcrumbsService,
|
||||||
|
LinkService,
|
||||||
CreateCommunityPageGuard
|
CreateCommunityPageGuard
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
@@ -46,7 +46,7 @@ export class CommunityPageComponent implements OnInit {
|
|||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.communityRD$ = this.route.data.pipe(
|
this.communityRD$ = this.route.data.pipe(
|
||||||
map((data) => data.community as RemoteData<Community>),
|
map((data) => data.dso as RemoteData<Community>),
|
||||||
redirectToPageNotFoundOn404(this.router)
|
redirectToPageNotFoundOn404(this.router)
|
||||||
);
|
);
|
||||||
this.logoRD$ = this.communityRD$.pipe(
|
this.logoRD$ = this.communityRD$.pipe(
|
||||||
|
@@ -5,6 +5,7 @@ import { NgModule } from '@angular/core';
|
|||||||
import { CommunityMetadataComponent } from './community-metadata/community-metadata.component';
|
import { CommunityMetadataComponent } from './community-metadata/community-metadata.component';
|
||||||
import { CommunityRolesComponent } from './community-roles/community-roles.component';
|
import { CommunityRolesComponent } from './community-roles/community-roles.component';
|
||||||
import { CommunityCurateComponent } from './community-curate/community-curate.component';
|
import { CommunityCurateComponent } from './community-curate/community-curate.component';
|
||||||
|
import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Routing module that handles the routing for the Edit Community page administrator functionality
|
* Routing module that handles the routing for the Edit Community page administrator functionality
|
||||||
@@ -14,10 +15,11 @@ import { CommunityCurateComponent } from './community-curate/community-curate.co
|
|||||||
RouterModule.forChild([
|
RouterModule.forChild([
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: EditCommunityPageComponent,
|
|
||||||
resolve: {
|
resolve: {
|
||||||
dso: CommunityPageResolver
|
breadcrumb: I18nBreadcrumbResolver
|
||||||
},
|
},
|
||||||
|
data: { breadcrumbKey: 'community.edit' },
|
||||||
|
component: EditCommunityPageComponent,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
@@ -29,26 +31,24 @@ import { CommunityCurateComponent } from './community-curate/community-curate.co
|
|||||||
component: CommunityMetadataComponent,
|
component: CommunityMetadataComponent,
|
||||||
data: {
|
data: {
|
||||||
title: 'community.edit.tabs.metadata.title',
|
title: 'community.edit.tabs.metadata.title',
|
||||||
hideReturnButton: true
|
hideReturnButton: true,
|
||||||
|
showBreadcrumbs: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'roles',
|
path: 'roles',
|
||||||
component: CommunityRolesComponent,
|
component: CommunityRolesComponent,
|
||||||
data: { title: 'community.edit.tabs.roles.title' }
|
data: { title: 'community.edit.tabs.roles.title', showBreadcrumbs: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'curate',
|
path: 'curate',
|
||||||
component: CommunityCurateComponent,
|
component: CommunityCurateComponent,
|
||||||
data: { title: 'community.edit.tabs.curate.title' }
|
data: { title: 'community.edit.tabs.curate.title', showBreadcrumbs: true }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
],
|
],
|
||||||
providers: [
|
|
||||||
CommunityPageResolver,
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
export class EditCommunityPageRoutingModule {
|
export class EditCommunityPageRoutingModule {
|
||||||
|
|
||||||
|
@@ -13,6 +13,7 @@ import { ItemBitstreamsComponent } from './item-bitstreams/item-bitstreams.compo
|
|||||||
import { ItemCollectionMapperComponent } from './item-collection-mapper/item-collection-mapper.component';
|
import { ItemCollectionMapperComponent } from './item-collection-mapper/item-collection-mapper.component';
|
||||||
import { ItemMoveComponent } from './item-move/item-move.component';
|
import { ItemMoveComponent } from './item-move/item-move.component';
|
||||||
import { ItemRelationshipsComponent } from './item-relationships/item-relationships.component';
|
import { ItemRelationshipsComponent } from './item-relationships/item-relationships.component';
|
||||||
|
import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||||
import { ItemVersionHistoryComponent } from './item-version-history/item-version-history.component';
|
import { ItemVersionHistoryComponent } from './item-version-history/item-version-history.component';
|
||||||
|
|
||||||
export function getItemEditVersionHistoryPath() {
|
export function getItemEditVersionHistoryPath() {
|
||||||
@@ -35,112 +36,92 @@ const ITEM_EDIT_VERSION_HISTORY = 'versionhistory';
|
|||||||
RouterModule.forChild([
|
RouterModule.forChild([
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: EditItemPageComponent,
|
|
||||||
resolve: {
|
resolve: {
|
||||||
item: ItemPageResolver
|
breadcrumb: I18nBreadcrumbResolver
|
||||||
},
|
},
|
||||||
|
data: { breadcrumbKey: 'item.edit' },
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
redirectTo: 'status',
|
component: EditItemPageComponent,
|
||||||
pathMatch: 'full'
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
redirectTo: 'status',
|
||||||
|
pathMatch: 'full'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'status',
|
||||||
|
component: ItemStatusComponent,
|
||||||
|
data: { title: 'item.edit.tabs.status.title', showBreadcrumbs: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'bitstreams',
|
||||||
|
component: ItemBitstreamsComponent,
|
||||||
|
data: { title: 'item.edit.tabs.bitstreams.title', showBreadcrumbs: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'metadata',
|
||||||
|
component: ItemMetadataComponent,
|
||||||
|
data: { title: 'item.edit.tabs.metadata.title', showBreadcrumbs: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'relationships',
|
||||||
|
component: ItemRelationshipsComponent,
|
||||||
|
data: { title: 'item.edit.tabs.relationships.title', showBreadcrumbs: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'view',
|
||||||
|
/* TODO - change when view page exists */
|
||||||
|
component: ItemBitstreamsComponent,
|
||||||
|
data: { title: 'item.edit.tabs.view.title', showBreadcrumbs: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'curate',
|
||||||
|
/* TODO - change when curate page exists */
|
||||||
|
component: ItemBitstreamsComponent,
|
||||||
|
data: { title: 'item.edit.tabs.curate.title', showBreadcrumbs: true }
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'status',
|
path: 'mapper',
|
||||||
component: ItemStatusComponent,
|
component: ItemCollectionMapperComponent,
|
||||||
data: { title: 'item.edit.tabs.status.title' }
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'bitstreams',
|
path: ITEM_EDIT_WITHDRAW_PATH,
|
||||||
component: ItemBitstreamsComponent,
|
component: ItemWithdrawComponent,
|
||||||
data: { title: 'item.edit.tabs.bitstreams.title' }
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'metadata',
|
path: ITEM_EDIT_REINSTATE_PATH,
|
||||||
component: ItemMetadataComponent,
|
component: ItemReinstateComponent,
|
||||||
data: { title: 'item.edit.tabs.metadata.title' }
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'relationships',
|
path: ITEM_EDIT_PRIVATE_PATH,
|
||||||
component: ItemRelationshipsComponent,
|
component: ItemPrivateComponent,
|
||||||
data: { title: 'item.edit.tabs.relationships.title' }
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'view',
|
path: ITEM_EDIT_PUBLIC_PATH,
|
||||||
/* TODO - change when view page exists */
|
component: ItemPublicComponent,
|
||||||
component: ItemBitstreamsComponent,
|
|
||||||
data: { title: 'item.edit.tabs.view.title' }
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'curate',
|
path: ITEM_EDIT_DELETE_PATH,
|
||||||
/* TODO - change when curate page exists */
|
component: ItemDeleteComponent,
|
||||||
component: ItemBitstreamsComponent,
|
|
||||||
data: { title: 'item.edit.tabs.curate.title' }
|
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: ITEM_EDIT_MOVE_PATH,
|
||||||
|
component: ItemMoveComponent,
|
||||||
|
data: { title: 'item.edit.move.title' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ITEM_EDIT_VERSION_HISTORY,
|
||||||
|
component: ItemVersionHistoryComponent,
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'mapper',
|
|
||||||
component: ItemCollectionMapperComponent,
|
|
||||||
resolve: {
|
|
||||||
item: ItemPageResolver
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ITEM_EDIT_WITHDRAW_PATH,
|
|
||||||
component: ItemWithdrawComponent,
|
|
||||||
resolve: {
|
|
||||||
item: ItemPageResolver
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ITEM_EDIT_REINSTATE_PATH,
|
|
||||||
component: ItemReinstateComponent,
|
|
||||||
resolve: {
|
|
||||||
item: ItemPageResolver
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ITEM_EDIT_PRIVATE_PATH,
|
|
||||||
component: ItemPrivateComponent,
|
|
||||||
resolve: {
|
|
||||||
item: ItemPageResolver
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ITEM_EDIT_PUBLIC_PATH,
|
|
||||||
component: ItemPublicComponent,
|
|
||||||
resolve: {
|
|
||||||
item: ItemPageResolver
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ITEM_EDIT_DELETE_PATH,
|
|
||||||
component: ItemDeleteComponent,
|
|
||||||
resolve: {
|
|
||||||
item: ItemPageResolver
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ITEM_EDIT_MOVE_PATH,
|
|
||||||
component: ItemMoveComponent,
|
|
||||||
data: { title: 'item.edit.move.title' },
|
|
||||||
resolve: {
|
|
||||||
item: ItemPageResolver
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ITEM_EDIT_VERSION_HISTORY,
|
|
||||||
component: ItemVersionHistoryComponent,
|
|
||||||
resolve: {
|
|
||||||
item: ItemPageResolver
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
],
|
],
|
||||||
providers: [
|
providers: []
|
||||||
ItemPageResolver,
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
export class EditItemPageRoutingModule {
|
export class EditItemPageRoutingModule {
|
||||||
|
|
||||||
|
@@ -220,7 +220,7 @@ describe('ItemDeleteComponent', () => {
|
|||||||
spyOn(comp, 'notify');
|
spyOn(comp, 'notify');
|
||||||
comp.performAction();
|
comp.performAction();
|
||||||
expect(mockItemDataService.delete)
|
expect(mockItemDataService.delete)
|
||||||
.toHaveBeenCalledWith(mockItem, types.filter((type) => typesSelection[type]).map((type) => type.id));
|
.toHaveBeenCalledWith(mockItem.id, types.filter((type) => typesSelection[type]).map((type) => type.id));
|
||||||
expect(comp.notify).toHaveBeenCalled();
|
expect(comp.notify).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -312,7 +312,7 @@ export class ItemDeleteComponent
|
|||||||
)
|
)
|
||||||
),
|
),
|
||||||
).subscribe((types) => {
|
).subscribe((types) => {
|
||||||
this.itemDataService.delete(this.item, types).pipe(first()).subscribe(
|
this.itemDataService.delete(this.item.id, types).pipe(first()).subscribe(
|
||||||
(succeeded: boolean) => {
|
(succeeded: boolean) => {
|
||||||
this.notify(succeeded);
|
this.notify(succeeded);
|
||||||
}
|
}
|
||||||
@@ -322,7 +322,7 @@ export class ItemDeleteComponent
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* When the item is successfully delete, navigate to the homepage, otherwise navigate back to the item edit page
|
* When the item is successfully delete, navigate to the homepage, otherwise navigate back to the item edit page
|
||||||
* @param response
|
* @param succeeded
|
||||||
*/
|
*/
|
||||||
notify(succeeded: boolean) {
|
notify(succeeded: boolean) {
|
||||||
if (succeeded) {
|
if (succeeded) {
|
||||||
|
@@ -7,19 +7,23 @@ import { ItemPageResolver } from './item-page.resolver';
|
|||||||
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
||||||
import { getItemModulePath } from '../app-routing.module';
|
import { getItemModulePath } from '../app-routing.module';
|
||||||
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
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 { getItemEditVersionHistoryPath } from './edit-item-page/edit-item-page.routing.module';
|
import { getItemEditVersionHistoryPath } from './edit-item-page/edit-item-page.routing.module';
|
||||||
|
|
||||||
export function getItemPageRoute(itemId: string) {
|
export function getItemPageRoute(itemId: string) {
|
||||||
return new URLCombiner(getItemModulePath(), itemId).toString();
|
return new URLCombiner(getItemModulePath(), itemId).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getItemEditPath(id: string) {
|
export function getItemEditPath(id: string) {
|
||||||
return new URLCombiner(getItemModulePath(),ITEM_EDIT_PATH.replace(/:id/, id)).toString()
|
return new URLCombiner(getItemModulePath(), id, ITEM_EDIT_PATH).toString()
|
||||||
}
|
}
|
||||||
export function getFullItemEditVersionHistoryPath(id: string) {
|
export function getFullItemEditVersionHistoryPath(id: string) {
|
||||||
return new URLCombiner(getItemModulePath(),ITEM_EDIT_VERSION_HISTORY_PATH.replace(/:id/, id)).toString()
|
return new URLCombiner(getItemModulePath(), id, ITEM_EDIT_VERSION_HISTORY_PATH).toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
const ITEM_EDIT_PATH = ':id/edit';
|
const ITEM_EDIT_PATH = 'edit';
|
||||||
const ITEM_EDIT_VERSION_HISTORY_PATH = `${ITEM_EDIT_PATH}/${getItemEditVersionHistoryPath()}`;
|
const ITEM_EDIT_VERSION_HISTORY_PATH = `${ITEM_EDIT_PATH}/${getItemEditVersionHistoryPath()}`;
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@@ -27,29 +31,36 @@ const ITEM_EDIT_VERSION_HISTORY_PATH = `${ITEM_EDIT_PATH}/${getItemEditVersionHi
|
|||||||
RouterModule.forChild([
|
RouterModule.forChild([
|
||||||
{
|
{
|
||||||
path: ':id',
|
path: ':id',
|
||||||
component: ItemPageComponent,
|
|
||||||
pathMatch: 'full',
|
|
||||||
resolve: {
|
resolve: {
|
||||||
item: ItemPageResolver
|
item: ItemPageResolver,
|
||||||
}
|
breadcrumb: ItemBreadcrumbResolver
|
||||||
},
|
},
|
||||||
{
|
children: [
|
||||||
path: ':id/full',
|
{
|
||||||
component: FullItemPageComponent,
|
path: '',
|
||||||
resolve: {
|
component: ItemPageComponent,
|
||||||
item: ItemPageResolver
|
pathMatch: 'full',
|
||||||
}
|
},
|
||||||
},
|
{
|
||||||
{
|
path: 'full',
|
||||||
path: ITEM_EDIT_PATH,
|
component: FullItemPageComponent,
|
||||||
loadChildren: './edit-item-page/edit-item-page.module#EditItemPageModule',
|
},
|
||||||
canActivate: [AuthenticatedGuard]
|
{
|
||||||
},
|
path: ITEM_EDIT_PATH,
|
||||||
|
loadChildren: './edit-item-page/edit-item-page.module#EditItemPageModule',
|
||||||
|
canActivate: [AuthenticatedGuard]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
])
|
])
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
ItemPageResolver,
|
ItemPageResolver,
|
||||||
|
ItemBreadcrumbResolver,
|
||||||
|
DSOBreadcrumbsService,
|
||||||
|
LinkService
|
||||||
]
|
]
|
||||||
|
|
||||||
})
|
})
|
||||||
export class ItemPageRoutingModule {
|
export class ItemPageRoutingModule {
|
||||||
|
|
||||||
|
@@ -28,7 +28,7 @@ export class ItemPageResolver implements Resolve<RemoteData<Item>> {
|
|||||||
followLink('owningCollection'),
|
followLink('owningCollection'),
|
||||||
followLink('bundles'),
|
followLink('bundles'),
|
||||||
followLink('relationships'),
|
followLink('relationships'),
|
||||||
followLink('version', undefined, followLink('versionhistory'))
|
followLink('version', undefined, followLink('versionhistory')),
|
||||||
).pipe(
|
).pipe(
|
||||||
find((RD) => hasValue(RD.error) || RD.hasSucceeded),
|
find((RD) => hasValue(RD.error) || RD.hasSucceeded),
|
||||||
);
|
);
|
||||||
|
@@ -2,12 +2,14 @@ import { NgModule } from '@angular/core';
|
|||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
import { LoginPageComponent } from './login-page.component';
|
import { LoginPageComponent } from './login-page.component';
|
||||||
|
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forChild([
|
RouterModule.forChild([
|
||||||
{ path: '', pathMatch: 'full', component: LoginPageComponent, data: { title: 'login.title' } }
|
{ path: '', pathMatch: 'full', component: LoginPageComponent, resolve: { breadcrumb: I18nBreadcrumbResolver }, data: { breadcrumbKey: 'login', title: 'login.title' } }
|
||||||
])
|
])
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class LoginPageRoutingModule { }
|
export class LoginPageRoutingModule {
|
||||||
|
}
|
||||||
|
@@ -19,7 +19,7 @@
|
|||||||
<ds-search-labels [inPlaceSearch]="inPlaceSearch"></ds-search-labels>
|
<ds-search-labels [inPlaceSearch]="inPlaceSearch"></ds-search-labels>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div id="search-body"
|
<div id="search-body"
|
||||||
class="row-offcanvas row-offcanvas-left"
|
class="row-offcanvas row-offcanvas-left w-100"
|
||||||
[@pushInOut]="(isSidebarCollapsed() | async) ? 'collapsed' : 'expanded'">
|
[@pushInOut]="(isSidebarCollapsed() | async) ? 'collapsed' : 'expanded'">
|
||||||
<ds-search-sidebar *ngIf="(isXsOrSm$ | async)" class="col-12"
|
<ds-search-sidebar *ngIf="(isXsOrSm$ | async)" class="col-12"
|
||||||
id="search-sidebar-sm"
|
id="search-sidebar-sm"
|
||||||
|
@@ -4,13 +4,24 @@ import { RouterModule } from '@angular/router';
|
|||||||
import { ConfigurationSearchPageGuard } from './configuration-search-page.guard';
|
import { ConfigurationSearchPageGuard } from './configuration-search-page.guard';
|
||||||
import { ConfigurationSearchPageComponent } from './configuration-search-page.component';
|
import { ConfigurationSearchPageComponent } from './configuration-search-page.component';
|
||||||
import { SearchPageComponent } from './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';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forChild([
|
RouterModule.forChild([{
|
||||||
{ path: '', component: SearchPageComponent, data: { title: 'search.title' } },
|
path: '',
|
||||||
{ path: ':configuration', component: ConfigurationSearchPageComponent, canActivate: [ConfigurationSearchPageGuard]}
|
resolve: { breadcrumb: I18nBreadcrumbResolver }, data: { title: 'search.title', breadcrumbKey: 'search' },
|
||||||
])
|
children: [
|
||||||
|
{ path: '', component: SearchPageComponent },
|
||||||
|
{ path: ':configuration', component: ConfigurationSearchPageComponent, canActivate: [ConfigurationSearchPageGuard] }
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
)
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
I18nBreadcrumbResolver,
|
||||||
|
I18nBreadcrumbsService
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class SearchPageRoutingModule {
|
export class SearchPageRoutingModule {
|
||||||
|
@@ -3,30 +3,56 @@ import { RouterModule } from '@angular/router';
|
|||||||
|
|
||||||
import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
|
import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
|
||||||
import { AuthenticatedGuard } from './core/auth/authenticated.guard';
|
import { AuthenticatedGuard } from './core/auth/authenticated.guard';
|
||||||
|
import { Breadcrumb } from './breadcrumbs/breadcrumb/breadcrumb.model';
|
||||||
|
import { DSpaceObject } from './core/shared/dspace-object.model';
|
||||||
|
import { Community } from './core/shared/community.model';
|
||||||
|
import { getCommunityPageRoute } from './+community-page/community-page-routing.module';
|
||||||
|
import { Collection } from './core/shared/collection.model';
|
||||||
|
import { Item } from './core/shared/item.model';
|
||||||
|
import { getItemPageRoute } from './+item-page/item-page-routing.module';
|
||||||
|
import { getCollectionPageRoute } from './+collection-page/collection-page-routing.module';
|
||||||
|
import { BrowseByDSOBreadcrumbResolver } from './+browse-by/browse-by-dso-breadcrumb.resolver';
|
||||||
|
|
||||||
const ITEM_MODULE_PATH = 'items';
|
const ITEM_MODULE_PATH = 'items';
|
||||||
|
|
||||||
export function getItemModulePath() {
|
export function getItemModulePath() {
|
||||||
return `/${ITEM_MODULE_PATH}`;
|
return `/${ITEM_MODULE_PATH}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const COLLECTION_MODULE_PATH = 'collections';
|
const COLLECTION_MODULE_PATH = 'collections';
|
||||||
|
|
||||||
export function getCollectionModulePath() {
|
export function getCollectionModulePath() {
|
||||||
return `/${COLLECTION_MODULE_PATH}`;
|
return `/${COLLECTION_MODULE_PATH}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const COMMUNITY_MODULE_PATH = 'communities';
|
const COMMUNITY_MODULE_PATH = 'communities';
|
||||||
|
|
||||||
export function getCommunityModulePath() {
|
export function getCommunityModulePath() {
|
||||||
return `/${COMMUNITY_MODULE_PATH}`;
|
return `/${COMMUNITY_MODULE_PATH}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ADMIN_MODULE_PATH = 'admin';
|
const ADMIN_MODULE_PATH = 'admin';
|
||||||
|
|
||||||
export function getAdminModulePath() {
|
export function getAdminModulePath() {
|
||||||
return `/${ADMIN_MODULE_PATH}`;
|
return `/${ADMIN_MODULE_PATH}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getDSOPath(dso: DSpaceObject): string {
|
||||||
|
switch ((dso as any).type) {
|
||||||
|
case Community.type.value:
|
||||||
|
return getCommunityPageRoute(dso.uuid);
|
||||||
|
case Collection.type.value:
|
||||||
|
return getCollectionPageRoute(dso.uuid);
|
||||||
|
case Item.type.value:
|
||||||
|
return getItemPageRoute(dso.uuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forRoot([
|
RouterModule.forRoot([
|
||||||
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
||||||
{ path: 'home', loadChildren: './+home-page/home-page.module#HomePageModule' },
|
{ path: 'home', loadChildren: './+home-page/home-page.module#HomePageModule', data: { showBreadcrumbs: false } },
|
||||||
{ path: 'community-list', loadChildren: './community-list-page/community-list-page.module#CommunityListPageModule' },
|
{ path: 'community-list', loadChildren: './community-list-page/community-list-page.module#CommunityListPageModule' },
|
||||||
{ path: 'id', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule' },
|
{ path: 'id', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule' },
|
||||||
{ path: 'handle', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule' },
|
{ path: 'handle', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule' },
|
||||||
@@ -35,7 +61,7 @@ export function getAdminModulePath() {
|
|||||||
{ path: ITEM_MODULE_PATH, loadChildren: './+item-page/item-page.module#ItemPageModule' },
|
{ path: ITEM_MODULE_PATH, loadChildren: './+item-page/item-page.module#ItemPageModule' },
|
||||||
{ path: 'mydspace', loadChildren: './+my-dspace-page/my-dspace-page.module#MyDSpacePageModule', canActivate: [AuthenticatedGuard] },
|
{ path: 'mydspace', loadChildren: './+my-dspace-page/my-dspace-page.module#MyDSpacePageModule', canActivate: [AuthenticatedGuard] },
|
||||||
{ path: 'search', loadChildren: './+search-page/search-page.module#SearchPageModule' },
|
{ path: 'search', loadChildren: './+search-page/search-page.module#SearchPageModule' },
|
||||||
{ path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule' },
|
{ path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule'},
|
||||||
{ path: ADMIN_MODULE_PATH, loadChildren: './+admin/admin.module#AdminModule', canActivate: [AuthenticatedGuard] },
|
{ path: ADMIN_MODULE_PATH, loadChildren: './+admin/admin.module#AdminModule', canActivate: [AuthenticatedGuard] },
|
||||||
{ path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' },
|
{ path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' },
|
||||||
{ path: 'logout', loadChildren: './+logout-page/logout-page.module#LogoutPageModule' },
|
{ path: 'logout', loadChildren: './+logout-page/logout-page.module#LogoutPageModule' },
|
||||||
@@ -45,7 +71,7 @@ export function getAdminModulePath() {
|
|||||||
{ path: '**', pathMatch: 'full', component: PageNotFoundComponent },
|
{ path: '**', pathMatch: 'full', component: PageNotFoundComponent },
|
||||||
])
|
])
|
||||||
],
|
],
|
||||||
exports: [RouterModule]
|
exports: [RouterModule],
|
||||||
})
|
})
|
||||||
export class AppRoutingModule {
|
export class AppRoutingModule {
|
||||||
|
|
||||||
|
@@ -10,6 +10,10 @@
|
|||||||
[options]="config.notifications">
|
[options]="config.notifications">
|
||||||
</ds-notifications-board>
|
</ds-notifications-board>
|
||||||
<main class="main-content">
|
<main class="main-content">
|
||||||
|
<div class="container">
|
||||||
|
<ds-breadcrumbs></ds-breadcrumbs>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="container" *ngIf="isLoading$ | async">
|
<div class="container" *ngIf="isLoading$ | async">
|
||||||
<ds-loading message="{{'loading.default' | translate}}"></ds-loading>
|
<ds-loading message="{{'loading.default' | translate}}"></ds-loading>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -3,12 +3,11 @@ import { HttpClientModule } from '@angular/common/http';
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
|
|
||||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
import { EffectsModule } from '@ngrx/effects';
|
import { EffectsModule } from '@ngrx/effects';
|
||||||
import { RouterStateSerializer, StoreRouterConnectingModule } from '@ngrx/router-store';
|
import { RouterStateSerializer, StoreRouterConnectingModule } from '@ngrx/router-store';
|
||||||
import { META_REDUCERS, MetaReducer, StoreModule, USER_PROVIDED_META_REDUCERS } from '@ngrx/store';
|
import { MetaReducer, StoreModule, USER_PROVIDED_META_REDUCERS } from '@ngrx/store';
|
||||||
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
|
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
|
||||||
|
import { DYNAMIC_MATCHER_PROVIDERS } from '@ng-dynamic-forms/core';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to';
|
import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to';
|
||||||
|
|
||||||
@@ -21,7 +20,7 @@ import { AppRoutingModule } from './app-routing.module';
|
|||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
|
|
||||||
import { appEffects } from './app.effects';
|
import { appEffects } from './app.effects';
|
||||||
import { appMetaReducers, debugMetaReducers, universalMetaReducer } from './app.metareducers';
|
import { appMetaReducers, debugMetaReducers } from './app.metareducers';
|
||||||
import { appReducers, AppState } from './app.reducer';
|
import { appReducers, AppState } from './app.reducer';
|
||||||
|
|
||||||
import { CoreModule } from './core/core.module';
|
import { CoreModule } from './core/core.module';
|
||||||
@@ -39,6 +38,7 @@ import { DSpaceRouterStateSerializer } from './shared/ngrx/dspace-router-state-s
|
|||||||
import { NotificationComponent } from './shared/notifications/notification/notification.component';
|
import { NotificationComponent } from './shared/notifications/notification/notification.component';
|
||||||
import { NotificationsBoardComponent } from './shared/notifications/notifications-board/notifications-board.component';
|
import { NotificationsBoardComponent } from './shared/notifications/notifications-board/notifications-board.component';
|
||||||
import { SharedModule } from './shared/shared.module';
|
import { SharedModule } from './shared/shared.module';
|
||||||
|
import { BreadcrumbsComponent } from './breadcrumbs/breadcrumbs.component';
|
||||||
|
|
||||||
export function getConfig() {
|
export function getConfig() {
|
||||||
return ENV_CONFIG;
|
return ENV_CONFIG;
|
||||||
@@ -97,7 +97,8 @@ const PROVIDERS = [
|
|||||||
provide: RouterStateSerializer,
|
provide: RouterStateSerializer,
|
||||||
useClass: DSpaceRouterStateSerializer
|
useClass: DSpaceRouterStateSerializer
|
||||||
},
|
},
|
||||||
ClientCookieService
|
ClientCookieService,
|
||||||
|
...DYNAMIC_MATCHER_PROVIDERS,
|
||||||
];
|
];
|
||||||
|
|
||||||
const DECLARATIONS = [
|
const DECLARATIONS = [
|
||||||
@@ -128,6 +129,7 @@ const EXPORTS = [
|
|||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
...DECLARATIONS,
|
...DECLARATIONS,
|
||||||
|
BreadcrumbsComponent,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
...EXPORTS
|
...EXPORTS
|
||||||
|
21
src/app/breadcrumbs/breadcrumb/breadcrumb-config.model.ts
Normal file
21
src/app/breadcrumbs/breadcrumb/breadcrumb-config.model.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { BreadcrumbsService } from '../../core/breadcrumbs/breadcrumbs.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for breadcrumb configuration objects
|
||||||
|
*/
|
||||||
|
export interface BreadcrumbConfig<T> {
|
||||||
|
/**
|
||||||
|
* The service used to calculate the breadcrumb object
|
||||||
|
*/
|
||||||
|
provider: BreadcrumbsService<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The key that is used to calculate the breadcrumb display value
|
||||||
|
*/
|
||||||
|
key: T;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The url of the breadcrumb
|
||||||
|
*/
|
||||||
|
url?: string;
|
||||||
|
}
|
15
src/app/breadcrumbs/breadcrumb/breadcrumb.model.ts
Normal file
15
src/app/breadcrumbs/breadcrumb/breadcrumb.model.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Class representing a single breadcrumb
|
||||||
|
*/
|
||||||
|
export class Breadcrumb {
|
||||||
|
constructor(
|
||||||
|
/**
|
||||||
|
* The display value of the breadcrumb
|
||||||
|
*/
|
||||||
|
public text: string,
|
||||||
|
/**
|
||||||
|
* The optional url of the breadcrumb
|
||||||
|
*/
|
||||||
|
public url?: string) {
|
||||||
|
}
|
||||||
|
}
|
17
src/app/breadcrumbs/breadcrumbs.component.html
Normal file
17
src/app/breadcrumbs/breadcrumbs.component.html
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<nav *ngIf="showBreadcrumbs" aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<ng-container *ngTemplateOutlet="breadcrumbs.length > 0 ? breadcrumb : activeBreadcrumb; context: {text: 'Home', url: '/'}"></ng-container>
|
||||||
|
<ng-container *ngFor="let bc of breadcrumbs; let last = last;">
|
||||||
|
<ng-container *ngTemplateOutlet="!last ? breadcrumb : activeBreadcrumb; context: bc"></ng-container>
|
||||||
|
</ng-container>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<ng-template #breadcrumb let-text="text" let-url="url">
|
||||||
|
<li class="breadcrumb-item"><a [routerLink]="url">{{text | translate}}</a></li>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template #activeBreadcrumb let-text="text" >
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">{{text | translate}}</li>
|
||||||
|
</ng-template>
|
||||||
|
|
0
src/app/breadcrumbs/breadcrumbs.component.scss
Normal file
0
src/app/breadcrumbs/breadcrumbs.component.scss
Normal file
111
src/app/breadcrumbs/breadcrumbs.component.spec.ts
Normal file
111
src/app/breadcrumbs/breadcrumbs.component.spec.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } 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 { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { MockTranslateLoader } from '../shared/testing/mock-translate-loader';
|
||||||
|
import { BreadcrumbConfig } from './breadcrumb/breadcrumb-config.model';
|
||||||
|
import { BreadcrumbsService } from '../core/breadcrumbs/breadcrumbs.service';
|
||||||
|
import { Breadcrumb } from './breadcrumb/breadcrumb.model';
|
||||||
|
import { getTestScheduler } from 'jasmine-marbles';
|
||||||
|
|
||||||
|
class TestBreadcrumbsService implements BreadcrumbsService<string> {
|
||||||
|
getBreadcrumbs(key: string, url: string): Observable<Breadcrumb[]> {
|
||||||
|
return observableOf([new Breadcrumb(key, url)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
breadcrumbProvider = new TestBreadcrumbsService();
|
||||||
|
|
||||||
|
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)
|
||||||
|
]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [BreadcrumbsComponent],
|
||||||
|
imports: [RouterTestingModule.withRoutes([]), TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: MockTranslateLoader
|
||||||
|
}
|
||||||
|
})],
|
||||||
|
providers: [
|
||||||
|
{ provide: ActivatedRoute, useValue: route }
|
||||||
|
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(BreadcrumbsComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
router = TestBed.get(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();
|
||||||
|
expect(component.resolveBreadcrumbs).toHaveBeenCalledWith(route.root);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('resolveBreadcrumbs', () => {
|
||||||
|
it('should return the correct breadcrumbs', () => {
|
||||||
|
const breadcrumbs = component.resolveBreadcrumbs(route.root);
|
||||||
|
getTestScheduler().expectObservable(breadcrumbs).toBe('(a|)', { a: expectedBreadcrumbs })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
100
src/app/breadcrumbs/breadcrumbs.component.ts
Normal file
100
src/app/breadcrumbs/breadcrumbs.component.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
|
||||||
|
import { Breadcrumb } from './breadcrumb/breadcrumb.model';
|
||||||
|
import { hasNoValue, hasValue, isNotUndefined, isUndefined } from '../shared/empty.util';
|
||||||
|
import { filter, map, switchMap, tap } from 'rxjs/operators';
|
||||||
|
import { combineLatest, Observable, Subscription, of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component representing the breadcrumbs of a page
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-breadcrumbs',
|
||||||
|
templateUrl: './breadcrumbs.component.html',
|
||||||
|
styleUrls: ['./breadcrumbs.component.scss']
|
||||||
|
})
|
||||||
|
export class BreadcrumbsComponent implements OnInit, OnDestroy {
|
||||||
|
/**
|
||||||
|
* List of breadcrumbs for this page
|
||||||
|
*/
|
||||||
|
breadcrumbs: Breadcrumb[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not to show breadcrumbs on this page
|
||||||
|
*/
|
||||||
|
showBreadcrumbs: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscription to unsubscribe from on destroy
|
||||||
|
*/
|
||||||
|
subscription: Subscription;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private router: Router
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the breadcrumbs on init for this page
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.subscription = this.router.events.pipe(
|
||||||
|
filter((e): e is NavigationEnd => e instanceof NavigationEnd),
|
||||||
|
tap(() => this.reset()),
|
||||||
|
switchMap(() => this.resolveBreadcrumbs(this.route.root))
|
||||||
|
).subscribe((breadcrumbs) => {
|
||||||
|
this.breadcrumbs = breadcrumbs;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribe from subscription
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
if (hasValue(this.subscription)) {
|
||||||
|
this.subscription.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the state of the breadcrumbs
|
||||||
|
*/
|
||||||
|
reset() {
|
||||||
|
this.breadcrumbs = [];
|
||||||
|
this.showBreadcrumbs = true;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,7 +1,6 @@
|
|||||||
import { Observable, of as observableOf, throwError as observableThrowError } from 'rxjs';
|
import { Observable, of as observableOf, throwError as observableThrowError } from 'rxjs';
|
||||||
import { distinctUntilChanged, filter, map, mergeMap, tap } from 'rxjs/operators';
|
import { distinctUntilChanged, filter, map, mergeMap, tap } from 'rxjs/operators';
|
||||||
import { Inject, Injectable } from '@angular/core';
|
import { Inject, Injectable } from '@angular/core';
|
||||||
import { EPersonDataService } from '../eperson/eperson-data.service';
|
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { GLOBAL_CONFIG } from '../../../config';
|
import { GLOBAL_CONFIG } from '../../../config';
|
||||||
|
@@ -1,9 +1,7 @@
|
|||||||
// import @ngrx
|
// import @ngrx
|
||||||
import { Action } from '@ngrx/store';
|
import { Action } from '@ngrx/store';
|
||||||
|
|
||||||
// import type function
|
// import type function
|
||||||
import { type } from '../../shared/ngrx/type';
|
import { type } from '../../shared/ngrx/type';
|
||||||
|
|
||||||
// import models
|
// import models
|
||||||
import { EPerson } from '../eperson/models/eperson.model';
|
import { EPerson } from '../eperson/models/eperson.model';
|
||||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||||
@@ -31,6 +29,9 @@ export const AuthActionTypes = {
|
|||||||
REGISTRATION_ERROR: type('dspace/auth/REGISTRATION_ERROR'),
|
REGISTRATION_ERROR: type('dspace/auth/REGISTRATION_ERROR'),
|
||||||
REGISTRATION_SUCCESS: type('dspace/auth/REGISTRATION_SUCCESS'),
|
REGISTRATION_SUCCESS: type('dspace/auth/REGISTRATION_SUCCESS'),
|
||||||
SET_REDIRECT_URL: type('dspace/auth/SET_REDIRECT_URL'),
|
SET_REDIRECT_URL: type('dspace/auth/SET_REDIRECT_URL'),
|
||||||
|
RETRIEVE_AUTHENTICATED_EPERSON: type('dspace/auth/RETRIEVE_AUTHENTICATED_EPERSON'),
|
||||||
|
RETRIEVE_AUTHENTICATED_EPERSON_SUCCESS: type('dspace/auth/RETRIEVE_AUTHENTICATED_EPERSON_SUCCESS'),
|
||||||
|
RETRIEVE_AUTHENTICATED_EPERSON_ERROR: type('dspace/auth/RETRIEVE_AUTHENTICATED_EPERSON_ERROR'),
|
||||||
};
|
};
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
@@ -76,11 +77,11 @@ export class AuthenticatedSuccessAction implements Action {
|
|||||||
payload: {
|
payload: {
|
||||||
authenticated: boolean;
|
authenticated: boolean;
|
||||||
authToken: AuthTokenInfo;
|
authToken: AuthTokenInfo;
|
||||||
user: EPerson
|
userHref: string
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(authenticated: boolean, authToken: AuthTokenInfo, user: EPerson) {
|
constructor(authenticated: boolean, authToken: AuthTokenInfo, userHref: string) {
|
||||||
this.payload = { authenticated, authToken, user };
|
this.payload = { authenticated, authToken, userHref };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,6 +323,47 @@ export class SetRedirectUrlAction implements Action {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the authenticated eperson.
|
||||||
|
* @class RetrieveAuthenticatedEpersonAction
|
||||||
|
* @implements {Action}
|
||||||
|
*/
|
||||||
|
export class RetrieveAuthenticatedEpersonAction implements Action {
|
||||||
|
public type: string = AuthActionTypes.RETRIEVE_AUTHENTICATED_EPERSON;
|
||||||
|
payload: string;
|
||||||
|
|
||||||
|
constructor(user: string) {
|
||||||
|
this.payload = user ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the authenticated eperson in the state.
|
||||||
|
* @class RetrieveAuthenticatedEpersonSuccessAction
|
||||||
|
* @implements {Action}
|
||||||
|
*/
|
||||||
|
export class RetrieveAuthenticatedEpersonSuccessAction implements Action {
|
||||||
|
public type: string = AuthActionTypes.RETRIEVE_AUTHENTICATED_EPERSON_SUCCESS;
|
||||||
|
payload: EPerson;
|
||||||
|
|
||||||
|
constructor(user: EPerson) {
|
||||||
|
this.payload = user ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the authenticated eperson in the state.
|
||||||
|
* @class RetrieveAuthenticatedEpersonSuccessAction
|
||||||
|
* @implements {Action}
|
||||||
|
*/
|
||||||
|
export class RetrieveAuthenticatedEpersonErrorAction implements Action {
|
||||||
|
public type: string = AuthActionTypes.RETRIEVE_AUTHENTICATED_EPERSON_ERROR;
|
||||||
|
payload: Error;
|
||||||
|
|
||||||
|
constructor(payload: Error) {
|
||||||
|
this.payload = payload ;
|
||||||
|
}
|
||||||
|
}
|
||||||
/* tslint:enable:max-classes-per-file */
|
/* tslint:enable:max-classes-per-file */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -343,4 +385,8 @@ export type AuthActions
|
|||||||
| RegistrationErrorAction
|
| RegistrationErrorAction
|
||||||
| RegistrationSuccessAction
|
| RegistrationSuccessAction
|
||||||
| AddAuthenticationMessageAction
|
| AddAuthenticationMessageAction
|
||||||
| ResetAuthenticationMessagesAction;
|
| ResetAuthenticationMessagesAction
|
||||||
|
| RetrieveAuthenticatedEpersonAction
|
||||||
|
| RetrieveAuthenticatedEpersonErrorAction
|
||||||
|
| RetrieveAuthenticatedEpersonSuccessAction
|
||||||
|
| SetRedirectUrlAction;
|
||||||
|
@@ -18,12 +18,14 @@ import {
|
|||||||
LogOutErrorAction,
|
LogOutErrorAction,
|
||||||
LogOutSuccessAction,
|
LogOutSuccessAction,
|
||||||
RefreshTokenErrorAction,
|
RefreshTokenErrorAction,
|
||||||
RefreshTokenSuccessAction
|
RefreshTokenSuccessAction,
|
||||||
|
RetrieveAuthenticatedEpersonAction,
|
||||||
|
RetrieveAuthenticatedEpersonErrorAction,
|
||||||
|
RetrieveAuthenticatedEpersonSuccessAction
|
||||||
} from './auth.actions';
|
} from './auth.actions';
|
||||||
import { AuthServiceStub } from '../../shared/testing/auth-service-stub';
|
import { AuthServiceStub } from '../../shared/testing/auth-service-stub';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { AuthState } from './auth.reducer';
|
import { AuthState } from './auth.reducer';
|
||||||
|
|
||||||
import { EPersonMock } from '../../shared/testing/eperson-mock';
|
import { EPersonMock } from '../../shared/testing/eperson-mock';
|
||||||
|
|
||||||
describe('AuthEffects', () => {
|
describe('AuthEffects', () => {
|
||||||
@@ -42,13 +44,14 @@ describe('AuthEffects', () => {
|
|||||||
authServiceStub = new AuthServiceStub();
|
authServiceStub = new AuthServiceStub();
|
||||||
token = authServiceStub.getToken();
|
token = authServiceStub.getToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
init();
|
init();
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
providers: [
|
providers: [
|
||||||
AuthEffects,
|
AuthEffects,
|
||||||
{provide: AuthService, useValue: authServiceStub},
|
{ provide: AuthService, useValue: authServiceStub },
|
||||||
{provide: Store, useValue: store},
|
{ provide: Store, useValue: store },
|
||||||
provideMockActions(() => actions),
|
provideMockActions(() => actions),
|
||||||
// other providers
|
// other providers
|
||||||
],
|
],
|
||||||
@@ -63,11 +66,11 @@ describe('AuthEffects', () => {
|
|||||||
actions = hot('--a-', {
|
actions = hot('--a-', {
|
||||||
a: {
|
a: {
|
||||||
type: AuthActionTypes.AUTHENTICATE,
|
type: AuthActionTypes.AUTHENTICATE,
|
||||||
payload: {email: 'user', password: 'password'}
|
payload: { email: 'user', password: 'password' }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const expected = cold('--b-', {b: new AuthenticationSuccessAction(token)});
|
const expected = cold('--b-', { b: new AuthenticationSuccessAction(token) });
|
||||||
|
|
||||||
expect(authEffects.authenticate$).toBeObservable(expected);
|
expect(authEffects.authenticate$).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
@@ -80,11 +83,11 @@ describe('AuthEffects', () => {
|
|||||||
actions = hot('--a-', {
|
actions = hot('--a-', {
|
||||||
a: {
|
a: {
|
||||||
type: AuthActionTypes.AUTHENTICATE,
|
type: AuthActionTypes.AUTHENTICATE,
|
||||||
payload: {email: 'user', password: 'wrongpassword'}
|
payload: { email: 'user', password: 'wrongpassword' }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const expected = cold('--b-', {b: new AuthenticationErrorAction(new Error('Message Error test'))});
|
const expected = cold('--b-', { b: new AuthenticationErrorAction(new Error('Message Error test')) });
|
||||||
|
|
||||||
expect(authEffects.authenticate$).toBeObservable(expected);
|
expect(authEffects.authenticate$).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
@@ -94,9 +97,9 @@ describe('AuthEffects', () => {
|
|||||||
describe('authenticateSuccess$', () => {
|
describe('authenticateSuccess$', () => {
|
||||||
|
|
||||||
it('should return a AUTHENTICATED action in response to a AUTHENTICATE_SUCCESS action', () => {
|
it('should return a AUTHENTICATED action in response to a AUTHENTICATE_SUCCESS action', () => {
|
||||||
actions = hot('--a-', {a: {type: AuthActionTypes.AUTHENTICATE_SUCCESS, payload: token}});
|
actions = hot('--a-', { a: { type: AuthActionTypes.AUTHENTICATE_SUCCESS, payload: token } });
|
||||||
|
|
||||||
const expected = cold('--b-', {b: new AuthenticatedAction(token)});
|
const expected = cold('--b-', { b: new AuthenticatedAction(token) });
|
||||||
|
|
||||||
expect(authEffects.authenticateSuccess$).toBeObservable(expected);
|
expect(authEffects.authenticateSuccess$).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
@@ -106,9 +109,9 @@ describe('AuthEffects', () => {
|
|||||||
|
|
||||||
describe('when token is valid', () => {
|
describe('when token is valid', () => {
|
||||||
it('should return a AUTHENTICATED_SUCCESS action in response to a AUTHENTICATED action', () => {
|
it('should return a AUTHENTICATED_SUCCESS action in response to a AUTHENTICATED action', () => {
|
||||||
actions = hot('--a-', {a: {type: AuthActionTypes.AUTHENTICATED, payload: token}});
|
actions = hot('--a-', { a: { type: AuthActionTypes.AUTHENTICATED, payload: token } });
|
||||||
|
|
||||||
const expected = cold('--b-', {b: new AuthenticatedSuccessAction(true, token, EPersonMock)});
|
const expected = cold('--b-', { b: new AuthenticatedSuccessAction(true, token, EPersonMock._links.self.href) });
|
||||||
|
|
||||||
expect(authEffects.authenticated$).toBeObservable(expected);
|
expect(authEffects.authenticated$).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
@@ -118,23 +121,42 @@ describe('AuthEffects', () => {
|
|||||||
it('should return a AUTHENTICATED_ERROR action in response to a AUTHENTICATED action', () => {
|
it('should return a AUTHENTICATED_ERROR action in response to a AUTHENTICATED action', () => {
|
||||||
spyOn((authEffects as any).authService, 'authenticatedUser').and.returnValue(observableThrow(new Error('Message Error test')));
|
spyOn((authEffects as any).authService, 'authenticatedUser').and.returnValue(observableThrow(new Error('Message Error test')));
|
||||||
|
|
||||||
actions = hot('--a-', {a: {type: AuthActionTypes.AUTHENTICATED, payload: token}});
|
actions = hot('--a-', { a: { type: AuthActionTypes.AUTHENTICATED, payload: token } });
|
||||||
|
|
||||||
const expected = cold('--b-', {b: new AuthenticatedErrorAction(new Error('Message Error test'))});
|
const expected = cold('--b-', { b: new AuthenticatedErrorAction(new Error('Message Error test')) });
|
||||||
|
|
||||||
expect(authEffects.authenticated$).toBeObservable(expected);
|
expect(authEffects.authenticated$).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('authenticatedSuccess$', () => {
|
||||||
|
|
||||||
|
it('should return a RETRIEVE_AUTHENTICATED_EPERSON action in response to a AUTHENTICATED_SUCCESS action', () => {
|
||||||
|
actions = hot('--a-', {
|
||||||
|
a: {
|
||||||
|
type: AuthActionTypes.AUTHENTICATED_SUCCESS, payload: {
|
||||||
|
authenticated: true,
|
||||||
|
authToken: token,
|
||||||
|
userHref: EPersonMock._links.self.href
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const expected = cold('--b-', { b: new RetrieveAuthenticatedEpersonAction(EPersonMock._links.self.href) });
|
||||||
|
|
||||||
|
expect(authEffects.authenticatedSuccess$).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('checkToken$', () => {
|
describe('checkToken$', () => {
|
||||||
|
|
||||||
describe('when check token succeeded', () => {
|
describe('when check token succeeded', () => {
|
||||||
it('should return a AUTHENTICATED action in response to a CHECK_AUTHENTICATION_TOKEN action', () => {
|
it('should return a AUTHENTICATED action in response to a CHECK_AUTHENTICATION_TOKEN action', () => {
|
||||||
|
|
||||||
actions = hot('--a-', {a: {type: AuthActionTypes.CHECK_AUTHENTICATION_TOKEN}});
|
actions = hot('--a-', { a: { type: AuthActionTypes.CHECK_AUTHENTICATION_TOKEN } });
|
||||||
|
|
||||||
const expected = cold('--b-', {b: new AuthenticatedAction(token)});
|
const expected = cold('--b-', { b: new AuthenticatedAction(token) });
|
||||||
|
|
||||||
expect(authEffects.checkToken$).toBeObservable(expected);
|
expect(authEffects.checkToken$).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
@@ -144,23 +166,53 @@ describe('AuthEffects', () => {
|
|||||||
it('should return a CHECK_AUTHENTICATION_TOKEN_ERROR action in response to a CHECK_AUTHENTICATION_TOKEN action', () => {
|
it('should return a CHECK_AUTHENTICATION_TOKEN_ERROR action in response to a CHECK_AUTHENTICATION_TOKEN action', () => {
|
||||||
spyOn((authEffects as any).authService, 'hasValidAuthenticationToken').and.returnValue(observableThrow(''));
|
spyOn((authEffects as any).authService, 'hasValidAuthenticationToken').and.returnValue(observableThrow(''));
|
||||||
|
|
||||||
actions = hot('--a-', {a: {type: AuthActionTypes.CHECK_AUTHENTICATION_TOKEN, payload: token}});
|
actions = hot('--a-', { a: { type: AuthActionTypes.CHECK_AUTHENTICATION_TOKEN, payload: token } });
|
||||||
|
|
||||||
const expected = cold('--b-', {b: new CheckAuthenticationTokenErrorAction()});
|
const expected = cold('--b-', { b: new CheckAuthenticationTokenErrorAction() });
|
||||||
|
|
||||||
expect(authEffects.checkToken$).toBeObservable(expected);
|
expect(authEffects.checkToken$).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('retrieveAuthenticatedEperson$', () => {
|
||||||
|
|
||||||
|
describe('when request is successful', () => {
|
||||||
|
it('should return a RETRIEVE_AUTHENTICATED_EPERSON_SUCCESS action in response to a RETRIEVE_AUTHENTICATED_EPERSON action', () => {
|
||||||
|
actions = hot('--a-', {
|
||||||
|
a: {
|
||||||
|
type: AuthActionTypes.RETRIEVE_AUTHENTICATED_EPERSON,
|
||||||
|
payload: EPersonMock._links.self.href
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const expected = cold('--b-', { b: new RetrieveAuthenticatedEpersonSuccessAction(EPersonMock) });
|
||||||
|
|
||||||
|
expect(authEffects.retrieveAuthenticatedEperson$).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when request is not successful', () => {
|
||||||
|
it('should return a RETRIEVE_AUTHENTICATED_EPERSON_ERROR action in response to a RETRIEVE_AUTHENTICATED_EPERSON action', () => {
|
||||||
|
spyOn((authEffects as any).authService, 'retrieveAuthenticatedUserByHref').and.returnValue(observableThrow(new Error('Message Error test')));
|
||||||
|
|
||||||
|
actions = hot('--a-', { a: { type: AuthActionTypes.RETRIEVE_AUTHENTICATED_EPERSON, payload: token } });
|
||||||
|
|
||||||
|
const expected = cold('--b-', { b: new RetrieveAuthenticatedEpersonErrorAction(new Error('Message Error test')) });
|
||||||
|
|
||||||
|
expect(authEffects.retrieveAuthenticatedEperson$).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('refreshToken$', () => {
|
describe('refreshToken$', () => {
|
||||||
|
|
||||||
describe('when refresh token succeeded', () => {
|
describe('when refresh token succeeded', () => {
|
||||||
it('should return a REFRESH_TOKEN_SUCCESS action in response to a REFRESH_TOKEN action', () => {
|
it('should return a REFRESH_TOKEN_SUCCESS action in response to a REFRESH_TOKEN action', () => {
|
||||||
|
|
||||||
actions = hot('--a-', {a: {type: AuthActionTypes.REFRESH_TOKEN}});
|
actions = hot('--a-', { a: { type: AuthActionTypes.REFRESH_TOKEN } });
|
||||||
|
|
||||||
const expected = cold('--b-', {b: new RefreshTokenSuccessAction(token)});
|
const expected = cold('--b-', { b: new RefreshTokenSuccessAction(token) });
|
||||||
|
|
||||||
expect(authEffects.refreshToken$).toBeObservable(expected);
|
expect(authEffects.refreshToken$).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
@@ -170,9 +222,9 @@ describe('AuthEffects', () => {
|
|||||||
it('should return a REFRESH_TOKEN_ERROR action in response to a REFRESH_TOKEN action', () => {
|
it('should return a REFRESH_TOKEN_ERROR action in response to a REFRESH_TOKEN action', () => {
|
||||||
spyOn((authEffects as any).authService, 'refreshAuthenticationToken').and.returnValue(observableThrow(''));
|
spyOn((authEffects as any).authService, 'refreshAuthenticationToken').and.returnValue(observableThrow(''));
|
||||||
|
|
||||||
actions = hot('--a-', {a: {type: AuthActionTypes.REFRESH_TOKEN, payload: token}});
|
actions = hot('--a-', { a: { type: AuthActionTypes.REFRESH_TOKEN, payload: token } });
|
||||||
|
|
||||||
const expected = cold('--b-', {b: new RefreshTokenErrorAction()});
|
const expected = cold('--b-', { b: new RefreshTokenErrorAction() });
|
||||||
|
|
||||||
expect(authEffects.refreshToken$).toBeObservable(expected);
|
expect(authEffects.refreshToken$).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
@@ -184,9 +236,9 @@ describe('AuthEffects', () => {
|
|||||||
describe('when refresh token succeeded', () => {
|
describe('when refresh token succeeded', () => {
|
||||||
it('should return a LOG_OUT_SUCCESS action in response to a LOG_OUT action', () => {
|
it('should return a LOG_OUT_SUCCESS action in response to a LOG_OUT action', () => {
|
||||||
|
|
||||||
actions = hot('--a-', {a: {type: AuthActionTypes.LOG_OUT}});
|
actions = hot('--a-', { a: { type: AuthActionTypes.LOG_OUT } });
|
||||||
|
|
||||||
const expected = cold('--b-', {b: new LogOutSuccessAction()});
|
const expected = cold('--b-', { b: new LogOutSuccessAction() });
|
||||||
|
|
||||||
expect(authEffects.logOut$).toBeObservable(expected);
|
expect(authEffects.logOut$).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
@@ -196,9 +248,9 @@ describe('AuthEffects', () => {
|
|||||||
it('should return a REFRESH_TOKEN_ERROR action in response to a LOG_OUT action', () => {
|
it('should return a REFRESH_TOKEN_ERROR action in response to a LOG_OUT action', () => {
|
||||||
spyOn((authEffects as any).authService, 'logout').and.returnValue(observableThrow(new Error('Message Error test')));
|
spyOn((authEffects as any).authService, 'logout').and.returnValue(observableThrow(new Error('Message Error test')));
|
||||||
|
|
||||||
actions = hot('--a-', {a: {type: AuthActionTypes.LOG_OUT, payload: token}});
|
actions = hot('--a-', { a: { type: AuthActionTypes.LOG_OUT, payload: token } });
|
||||||
|
|
||||||
const expected = cold('--b-', {b: new LogOutErrorAction(new Error('Message Error test'))});
|
const expected = cold('--b-', { b: new LogOutErrorAction(new Error('Message Error test')) });
|
||||||
|
|
||||||
expect(authEffects.logOut$).toBeObservable(expected);
|
expect(authEffects.logOut$).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
|
@@ -26,7 +26,10 @@ import {
|
|||||||
RefreshTokenSuccessAction,
|
RefreshTokenSuccessAction,
|
||||||
RegistrationAction,
|
RegistrationAction,
|
||||||
RegistrationErrorAction,
|
RegistrationErrorAction,
|
||||||
RegistrationSuccessAction
|
RegistrationSuccessAction,
|
||||||
|
RetrieveAuthenticatedEpersonAction,
|
||||||
|
RetrieveAuthenticatedEpersonErrorAction,
|
||||||
|
RetrieveAuthenticatedEpersonSuccessAction
|
||||||
} from './auth.actions';
|
} from './auth.actions';
|
||||||
import { EPerson } from '../eperson/models/eperson.model';
|
import { EPerson } from '../eperson/models/eperson.model';
|
||||||
import { AuthStatus } from './models/auth-status.model';
|
import { AuthStatus } from './models/auth-status.model';
|
||||||
@@ -66,11 +69,17 @@ export class AuthEffects {
|
|||||||
ofType(AuthActionTypes.AUTHENTICATED),
|
ofType(AuthActionTypes.AUTHENTICATED),
|
||||||
switchMap((action: AuthenticatedAction) => {
|
switchMap((action: AuthenticatedAction) => {
|
||||||
return this.authService.authenticatedUser(action.payload).pipe(
|
return this.authService.authenticatedUser(action.payload).pipe(
|
||||||
map((user: EPerson) => new AuthenticatedSuccessAction((user !== null), action.payload, user)),
|
map((userHref: string) => new AuthenticatedSuccessAction((userHref !== null), action.payload, userHref)),
|
||||||
catchError((error) => observableOf(new AuthenticatedErrorAction(error))),);
|
catchError((error) => observableOf(new AuthenticatedErrorAction(error))),);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@Effect()
|
||||||
|
public authenticatedSuccess$: Observable<Action> = this.actions$.pipe(
|
||||||
|
ofType(AuthActionTypes.AUTHENTICATED_SUCCESS),
|
||||||
|
map((action: AuthenticatedSuccessAction) => new RetrieveAuthenticatedEpersonAction(action.payload.userHref))
|
||||||
|
);
|
||||||
|
|
||||||
// It means "reacts to this action but don't send another"
|
// It means "reacts to this action but don't send another"
|
||||||
@Effect({ dispatch: false })
|
@Effect({ dispatch: false })
|
||||||
public authenticatedError$: Observable<Action> = this.actions$.pipe(
|
public authenticatedError$: Observable<Action> = this.actions$.pipe(
|
||||||
@@ -78,6 +87,16 @@ export class AuthEffects {
|
|||||||
tap((action: LogOutSuccessAction) => this.authService.removeToken())
|
tap((action: LogOutSuccessAction) => this.authService.removeToken())
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@Effect()
|
||||||
|
public retrieveAuthenticatedEperson$: Observable<Action> = this.actions$.pipe(
|
||||||
|
ofType(AuthActionTypes.RETRIEVE_AUTHENTICATED_EPERSON),
|
||||||
|
switchMap((action: RetrieveAuthenticatedEpersonAction) => {
|
||||||
|
return this.authService.retrieveAuthenticatedUserByHref(action.payload).pipe(
|
||||||
|
map((user: EPerson) => new RetrieveAuthenticatedEpersonSuccessAction(user)),
|
||||||
|
catchError((error) => observableOf(new RetrieveAuthenticatedEpersonErrorAction(error))));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
@Effect()
|
@Effect()
|
||||||
public checkToken$: Observable<Action> = this.actions$.pipe(ofType(AuthActionTypes.CHECK_AUTHENTICATION_TOKEN),
|
public checkToken$: Observable<Action> = this.actions$.pipe(ofType(AuthActionTypes.CHECK_AUTHENTICATION_TOKEN),
|
||||||
switchMap(() => {
|
switchMap(() => {
|
||||||
|
@@ -17,7 +17,7 @@ import {
|
|||||||
RefreshTokenAction,
|
RefreshTokenAction,
|
||||||
RefreshTokenErrorAction,
|
RefreshTokenErrorAction,
|
||||||
RefreshTokenSuccessAction,
|
RefreshTokenSuccessAction,
|
||||||
ResetAuthenticationMessagesAction,
|
ResetAuthenticationMessagesAction, RetrieveAuthenticatedEpersonErrorAction, RetrieveAuthenticatedEpersonSuccessAction,
|
||||||
SetRedirectUrlAction
|
SetRedirectUrlAction
|
||||||
} from './auth.actions';
|
} from './auth.actions';
|
||||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||||
@@ -107,16 +107,15 @@ describe('authReducer', () => {
|
|||||||
loading: true,
|
loading: true,
|
||||||
info: undefined
|
info: undefined
|
||||||
};
|
};
|
||||||
const action = new AuthenticatedSuccessAction(true, mockTokenInfo, EPersonMock);
|
const action = new AuthenticatedSuccessAction(true, mockTokenInfo, EPersonMock._links.self.href);
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
state = {
|
state = {
|
||||||
authenticated: true,
|
authenticated: true,
|
||||||
authToken: mockTokenInfo,
|
authToken: mockTokenInfo,
|
||||||
loaded: true,
|
loaded: false,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
loading: false,
|
loading: true,
|
||||||
info: undefined,
|
info: undefined
|
||||||
user: EPersonMock
|
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -242,6 +241,50 @@ describe('authReducer', () => {
|
|||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should properly set the state, in response to a RETRIEVE_AUTHENTICATED_EPERSON_SUCCESS action', () => {
|
||||||
|
initialState = {
|
||||||
|
authenticated: true,
|
||||||
|
authToken: mockTokenInfo,
|
||||||
|
loaded: false,
|
||||||
|
error: undefined,
|
||||||
|
loading: true,
|
||||||
|
info: undefined
|
||||||
|
};
|
||||||
|
const action = new RetrieveAuthenticatedEpersonSuccessAction(EPersonMock);
|
||||||
|
const newState = authReducer(initialState, action);
|
||||||
|
state = {
|
||||||
|
authenticated: true,
|
||||||
|
authToken: mockTokenInfo,
|
||||||
|
loaded: true,
|
||||||
|
error: undefined,
|
||||||
|
loading: false,
|
||||||
|
info: undefined,
|
||||||
|
user: EPersonMock
|
||||||
|
};
|
||||||
|
expect(newState).toEqual(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly set the state, in response to a RETRIEVE_AUTHENTICATED_EPERSON_ERROR action', () => {
|
||||||
|
initialState = {
|
||||||
|
authenticated: false,
|
||||||
|
loaded: false,
|
||||||
|
error: undefined,
|
||||||
|
loading: true,
|
||||||
|
info: undefined
|
||||||
|
};
|
||||||
|
const action = new RetrieveAuthenticatedEpersonErrorAction(mockError);
|
||||||
|
const newState = authReducer(initialState, action);
|
||||||
|
state = {
|
||||||
|
authenticated: false,
|
||||||
|
authToken: undefined,
|
||||||
|
error: 'Test error message',
|
||||||
|
loaded: true,
|
||||||
|
loading: false,
|
||||||
|
info: undefined
|
||||||
|
};
|
||||||
|
expect(newState).toEqual(state);
|
||||||
|
});
|
||||||
|
|
||||||
it('should properly set the state, in response to a REFRESH_TOKEN action', () => {
|
it('should properly set the state, in response to a REFRESH_TOKEN action', () => {
|
||||||
initialState = {
|
initialState = {
|
||||||
authenticated: true,
|
authenticated: true,
|
||||||
|
@@ -8,7 +8,7 @@ import {
|
|||||||
LogOutErrorAction,
|
LogOutErrorAction,
|
||||||
RedirectWhenAuthenticationIsRequiredAction,
|
RedirectWhenAuthenticationIsRequiredAction,
|
||||||
RedirectWhenTokenExpiredAction,
|
RedirectWhenTokenExpiredAction,
|
||||||
RefreshTokenSuccessAction,
|
RefreshTokenSuccessAction, RetrieveAuthenticatedEpersonSuccessAction,
|
||||||
SetRedirectUrlAction
|
SetRedirectUrlAction
|
||||||
} from './auth.actions';
|
} from './auth.actions';
|
||||||
// import models
|
// import models
|
||||||
@@ -80,6 +80,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
|||||||
});
|
});
|
||||||
|
|
||||||
case AuthActionTypes.AUTHENTICATED_ERROR:
|
case AuthActionTypes.AUTHENTICATED_ERROR:
|
||||||
|
case AuthActionTypes.RETRIEVE_AUTHENTICATED_EPERSON_ERROR:
|
||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
authToken: undefined,
|
authToken: undefined,
|
||||||
@@ -91,12 +92,16 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
|||||||
case AuthActionTypes.AUTHENTICATED_SUCCESS:
|
case AuthActionTypes.AUTHENTICATED_SUCCESS:
|
||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
authenticated: true,
|
authenticated: true,
|
||||||
authToken: (action as AuthenticatedSuccessAction).payload.authToken,
|
authToken: (action as AuthenticatedSuccessAction).payload.authToken
|
||||||
|
});
|
||||||
|
|
||||||
|
case AuthActionTypes.RETRIEVE_AUTHENTICATED_EPERSON_SUCCESS:
|
||||||
|
return Object.assign({}, state, {
|
||||||
loaded: true,
|
loaded: true,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
user: (action as AuthenticatedSuccessAction).payload.user
|
user: (action as RetrieveAuthenticatedEpersonSuccessAction).payload
|
||||||
});
|
});
|
||||||
|
|
||||||
case AuthActionTypes.AUTHENTICATE_ERROR:
|
case AuthActionTypes.AUTHENTICATE_ERROR:
|
||||||
|
@@ -5,14 +5,12 @@ import { ActivatedRoute, Router } from '@angular/router';
|
|||||||
import { Store, StoreModule } from '@ngrx/store';
|
import { Store, StoreModule } from '@ngrx/store';
|
||||||
import { REQUEST } from '@nguniversal/express-engine/tokens';
|
import { REQUEST } from '@nguniversal/express-engine/tokens';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import { LinkService } from '../cache/builders/link.service';
|
|
||||||
|
|
||||||
import { authReducer, AuthState } from './auth.reducer';
|
import { authReducer, AuthState } from './auth.reducer';
|
||||||
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
|
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { RouterStub } from '../../shared/testing/router-stub';
|
import { RouterStub } from '../../shared/testing/router-stub';
|
||||||
import { ActivatedRouteStub } from '../../shared/testing/active-router-stub';
|
import { ActivatedRouteStub } from '../../shared/testing/active-router-stub';
|
||||||
|
|
||||||
import { CookieService } from '../services/cookie.service';
|
import { CookieService } from '../services/cookie.service';
|
||||||
import { AuthRequestServiceStub } from '../../shared/testing/auth-request-service-stub';
|
import { AuthRequestServiceStub } from '../../shared/testing/auth-request-service-stub';
|
||||||
import { AuthRequestService } from './auth-request.service';
|
import { AuthRequestService } from './auth-request.service';
|
||||||
@@ -23,12 +21,21 @@ import { EPersonMock } from '../../shared/testing/eperson-mock';
|
|||||||
import { AppState } from '../../app.reducer';
|
import { AppState } from '../../app.reducer';
|
||||||
import { ClientCookieService } from '../services/client-cookie.service';
|
import { ClientCookieService } from '../services/client-cookie.service';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-data-build.service';
|
|
||||||
import { routeServiceStub } from '../../shared/testing/route-service-stub';
|
import { routeServiceStub } from '../../shared/testing/route-service-stub';
|
||||||
import { RouteService } from '../services/route.service';
|
import { RouteService } from '../services/route.service';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { RemoteData } from '../data/remote-data';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils';
|
||||||
|
import { EPersonDataService } from '../eperson/eperson-data.service';
|
||||||
|
|
||||||
describe('AuthService test', () => {
|
describe('AuthService test', () => {
|
||||||
|
|
||||||
|
const mockEpersonDataService: any = {
|
||||||
|
findByHref(href: string): Observable<RemoteData<EPerson>> {
|
||||||
|
return createSuccessfulRemoteDataObject$(EPersonMock);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let mockStore: Store<AuthState>;
|
let mockStore: Store<AuthState>;
|
||||||
let authService: AuthService;
|
let authService: AuthService;
|
||||||
let routeServiceMock: RouteService;
|
let routeServiceMock: RouteService;
|
||||||
@@ -62,7 +69,7 @@ describe('AuthService test', () => {
|
|||||||
linkService = {
|
linkService = {
|
||||||
resolveLinks: {}
|
resolveLinks: {}
|
||||||
};
|
};
|
||||||
spyOn(linkService, 'resolveLinks').and.returnValue({authenticated: true, eperson: observableOf({payload: {}})});
|
spyOn(linkService, 'resolveLinks').and.returnValue({ authenticated: true, eperson: observableOf({ payload: {} }) });
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +90,7 @@ describe('AuthService test', () => {
|
|||||||
{ provide: RouteService, useValue: routeServiceStub },
|
{ provide: RouteService, useValue: routeServiceStub },
|
||||||
{ provide: ActivatedRoute, useValue: routeStub },
|
{ provide: ActivatedRoute, useValue: routeStub },
|
||||||
{ provide: Store, useValue: mockStore },
|
{ provide: Store, useValue: mockStore },
|
||||||
{ provide: LinkService, useValue: linkService },
|
{ provide: EPersonDataService, useValue: mockEpersonDataService },
|
||||||
CookieService,
|
CookieService,
|
||||||
AuthService
|
AuthService
|
||||||
],
|
],
|
||||||
@@ -101,8 +108,14 @@ describe('AuthService test', () => {
|
|||||||
expect(authService.authenticate.bind(null, 'user', 'passwordwrong')).toThrow();
|
expect(authService.authenticate.bind(null, 'user', 'passwordwrong')).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the authenticated user object when user token is valid', () => {
|
it('should return the authenticated user href when user token is valid', () => {
|
||||||
authService.authenticatedUser(new AuthTokenInfo('test_token')).subscribe((user: EPerson) => {
|
authService.authenticatedUser(new AuthTokenInfo('test_token')).subscribe((userHref: string) => {
|
||||||
|
expect(userHref).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the authenticated user', () => {
|
||||||
|
authService.retrieveAuthenticatedUserByHref(EPersonMock._links.self.href).subscribe((user: EPerson) => {
|
||||||
expect(user).toBeDefined();
|
expect(user).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -159,7 +172,7 @@ describe('AuthService test', () => {
|
|||||||
(state as any).core = Object.create({});
|
(state as any).core = Object.create({});
|
||||||
(state as any).core.auth = authenticatedState;
|
(state as any).core.auth = authenticatedState;
|
||||||
});
|
});
|
||||||
authService = new AuthService({}, window, undefined, authReqService, router, routeService, cookieService, store, linkService);
|
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should return true when user is logged in', () => {
|
it('should return true when user is logged in', () => {
|
||||||
@@ -221,7 +234,7 @@ describe('AuthService test', () => {
|
|||||||
(state as any).core = Object.create({});
|
(state as any).core = Object.create({});
|
||||||
(state as any).core.auth = authenticatedState;
|
(state as any).core.auth = authenticatedState;
|
||||||
});
|
});
|
||||||
authService = new AuthService({}, window, undefined, authReqService, router, routeService, cookieService, store, linkService);
|
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store);
|
||||||
storage = (authService as any).storage;
|
storage = (authService as any).storage;
|
||||||
routeServiceMock = TestBed.get(RouteService);
|
routeServiceMock = TestBed.get(RouteService);
|
||||||
routerStub = TestBed.get(Router);
|
routerStub = TestBed.get(Router);
|
||||||
@@ -250,7 +263,7 @@ describe('AuthService test', () => {
|
|||||||
expect(storage.remove).toHaveBeenCalled();
|
expect(storage.remove).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it ('should set redirect url to previous page', () => {
|
it('should set redirect url to previous page', () => {
|
||||||
spyOn(routeServiceMock, 'getHistory').and.callThrough();
|
spyOn(routeServiceMock, 'getHistory').and.callThrough();
|
||||||
spyOn(routerStub, 'navigateByUrl');
|
spyOn(routerStub, 'navigateByUrl');
|
||||||
authService.redirectAfterLoginSuccess(true);
|
authService.redirectAfterLoginSuccess(true);
|
||||||
@@ -258,7 +271,7 @@ describe('AuthService test', () => {
|
|||||||
expect(routerStub.navigateByUrl).toHaveBeenCalledWith('/collection/123');
|
expect(routerStub.navigateByUrl).toHaveBeenCalledWith('/collection/123');
|
||||||
});
|
});
|
||||||
|
|
||||||
it ('should set redirect url to current page', () => {
|
it('should set redirect url to current page', () => {
|
||||||
spyOn(routeServiceMock, 'getHistory').and.callThrough();
|
spyOn(routeServiceMock, 'getHistory').and.callThrough();
|
||||||
spyOn(routerStub, 'navigateByUrl');
|
spyOn(routerStub, 'navigateByUrl');
|
||||||
authService.redirectAfterLoginSuccess(false);
|
authService.redirectAfterLoginSuccess(false);
|
||||||
@@ -266,7 +279,7 @@ describe('AuthService test', () => {
|
|||||||
expect(routerStub.navigateByUrl).toHaveBeenCalledWith('/home');
|
expect(routerStub.navigateByUrl).toHaveBeenCalledWith('/home');
|
||||||
});
|
});
|
||||||
|
|
||||||
it ('should redirect to / and not to /login', () => {
|
it('should redirect to / and not to /login', () => {
|
||||||
spyOn(routeServiceMock, 'getHistory').and.returnValue(observableOf(['/login', '/login']));
|
spyOn(routeServiceMock, 'getHistory').and.returnValue(observableOf(['/login', '/login']));
|
||||||
spyOn(routerStub, 'navigateByUrl');
|
spyOn(routerStub, 'navigateByUrl');
|
||||||
authService.redirectAfterLoginSuccess(true);
|
authService.redirectAfterLoginSuccess(true);
|
||||||
@@ -274,7 +287,7 @@ describe('AuthService test', () => {
|
|||||||
expect(routerStub.navigateByUrl).toHaveBeenCalledWith('/');
|
expect(routerStub.navigateByUrl).toHaveBeenCalledWith('/');
|
||||||
});
|
});
|
||||||
|
|
||||||
it ('should redirect to / when no redirect url is found', () => {
|
it('should redirect to / when no redirect url is found', () => {
|
||||||
spyOn(routeServiceMock, 'getHistory').and.returnValue(observableOf(['']));
|
spyOn(routeServiceMock, 'getHistory').and.returnValue(observableOf(['']));
|
||||||
spyOn(routerStub, 'navigateByUrl');
|
spyOn(routerStub, 'navigateByUrl');
|
||||||
authService.redirectAfterLoginSuccess(true);
|
authService.redirectAfterLoginSuccess(true);
|
||||||
|
@@ -4,12 +4,10 @@ import { HttpHeaders } from '@angular/common/http';
|
|||||||
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
|
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
|
||||||
|
|
||||||
import { Observable, of as observableOf } from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
import { distinctUntilChanged, filter, map, startWith, switchMap, take, withLatestFrom } from 'rxjs/operators';
|
import { distinctUntilChanged, filter, map, startWith, take, withLatestFrom } from 'rxjs/operators';
|
||||||
import { RouterReducerState } from '@ngrx/router-store';
|
import { RouterReducerState } from '@ngrx/router-store';
|
||||||
import { select, Store } from '@ngrx/store';
|
import { select, Store } from '@ngrx/store';
|
||||||
import { CookieAttributes } from 'js-cookie';
|
import { CookieAttributes } from 'js-cookie';
|
||||||
import { followLink } from '../../shared/utils/follow-link-config.model';
|
|
||||||
import { LinkService } from '../cache/builders/link.service';
|
|
||||||
|
|
||||||
import { EPerson } from '../eperson/models/eperson.model';
|
import { EPerson } from '../eperson/models/eperson.model';
|
||||||
import { AuthRequestService } from './auth-request.service';
|
import { AuthRequestService } from './auth-request.service';
|
||||||
@@ -24,6 +22,8 @@ import { ResetAuthenticationMessagesAction, SetRedirectUrlAction } from './auth.
|
|||||||
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
|
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
|
||||||
import { Base64EncodeUrl } from '../../shared/utils/encode-decode.util';
|
import { Base64EncodeUrl } from '../../shared/utils/encode-decode.util';
|
||||||
import { RouteService } from '../services/route.service';
|
import { RouteService } from '../services/route.service';
|
||||||
|
import { EPersonDataService } from '../eperson/eperson-data.service';
|
||||||
|
import { getFirstSucceededRemoteDataPayload } from '../shared/operators';
|
||||||
|
|
||||||
export const LOGIN_ROUTE = '/login';
|
export const LOGIN_ROUTE = '/login';
|
||||||
export const LOGOUT_ROUTE = '/logout';
|
export const LOGOUT_ROUTE = '/logout';
|
||||||
@@ -44,13 +44,13 @@ export class AuthService {
|
|||||||
|
|
||||||
constructor(@Inject(REQUEST) protected req: any,
|
constructor(@Inject(REQUEST) protected req: any,
|
||||||
@Inject(NativeWindowService) protected _window: NativeWindowRef,
|
@Inject(NativeWindowService) protected _window: NativeWindowRef,
|
||||||
protected authRequestService: AuthRequestService,
|
|
||||||
@Optional() @Inject(RESPONSE) private response: any,
|
@Optional() @Inject(RESPONSE) private response: any,
|
||||||
|
protected authRequestService: AuthRequestService,
|
||||||
|
protected epersonService: EPersonDataService,
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
protected routeService: RouteService,
|
protected routeService: RouteService,
|
||||||
protected storage: CookieService,
|
protected storage: CookieService,
|
||||||
protected store: Store<AppState>,
|
protected store: Store<AppState>
|
||||||
protected linkService: LinkService
|
|
||||||
) {
|
) {
|
||||||
this.store.pipe(
|
this.store.pipe(
|
||||||
select(isAuthenticated),
|
select(isAuthenticated),
|
||||||
@@ -123,10 +123,10 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the authenticated user
|
* Returns the href link to authenticated user
|
||||||
* @returns {User}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
public authenticatedUser(token: AuthTokenInfo): Observable<EPerson> {
|
public authenticatedUser(token: AuthTokenInfo): Observable<string> {
|
||||||
// Determine if the user has an existing auth session on the server
|
// Determine if the user has an existing auth session on the server
|
||||||
const options: HttpOptions = Object.create({});
|
const options: HttpOptions = Object.create({});
|
||||||
let headers = new HttpHeaders();
|
let headers = new HttpHeaders();
|
||||||
@@ -134,16 +134,25 @@ export class AuthService {
|
|||||||
headers = headers.append('Authorization', `Bearer ${token.accessToken}`);
|
headers = headers.append('Authorization', `Bearer ${token.accessToken}`);
|
||||||
options.headers = headers;
|
options.headers = headers;
|
||||||
return this.authRequestService.getRequest('status', options).pipe(
|
return this.authRequestService.getRequest('status', options).pipe(
|
||||||
map((status) => this.linkService.resolveLinks(status, followLink<AuthStatus>('eperson'))),
|
map((status: AuthStatus) => {
|
||||||
switchMap((status: AuthStatus) => {
|
|
||||||
if (status.authenticated) {
|
if (status.authenticated) {
|
||||||
return status.eperson.pipe(map((eperson) => eperson.payload));
|
return status._links.eperson.href;
|
||||||
} else {
|
} else {
|
||||||
throw(new Error('Not authenticated'));
|
throw(new Error('Not authenticated'));
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the authenticated user
|
||||||
|
* @returns {User}
|
||||||
|
*/
|
||||||
|
public retrieveAuthenticatedUserByHref(userHref: string): Observable<EPerson> {
|
||||||
|
return this.epersonService.findByHref(userHref).pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if token is present into browser storage and is valid. (NB Check is done only on SSR)
|
* Checks if token is present into browser storage and is valid. (NB Check is done only on SSR)
|
||||||
*/
|
*/
|
||||||
|
@@ -2,11 +2,9 @@ import { HttpHeaders } from '@angular/common/http';
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { filter, map, switchMap, take } from 'rxjs/operators';
|
import { filter, map, take } from 'rxjs/operators';
|
||||||
import { isNotEmpty } from '../../shared/empty.util';
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
import { followLink } from '../../shared/utils/follow-link-config.model';
|
|
||||||
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||||
import { EPerson } from '../eperson/models/eperson.model';
|
|
||||||
import { CheckAuthenticationTokenAction } from './auth.actions';
|
import { CheckAuthenticationTokenAction } from './auth.actions';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { AuthStatus } from './models/auth-status.model';
|
import { AuthStatus } from './models/auth-status.model';
|
||||||
@@ -22,7 +20,7 @@ export class ServerAuthService extends AuthService {
|
|||||||
* Returns the authenticated user
|
* Returns the authenticated user
|
||||||
* @returns {User}
|
* @returns {User}
|
||||||
*/
|
*/
|
||||||
public authenticatedUser(token: AuthTokenInfo): Observable<EPerson> {
|
public authenticatedUser(token: AuthTokenInfo): Observable<string> {
|
||||||
// Determine if the user has an existing auth session on the server
|
// Determine if the user has an existing auth session on the server
|
||||||
const options: HttpOptions = Object.create({});
|
const options: HttpOptions = Object.create({});
|
||||||
let headers = new HttpHeaders();
|
let headers = new HttpHeaders();
|
||||||
@@ -35,10 +33,9 @@ export class ServerAuthService extends AuthService {
|
|||||||
|
|
||||||
options.headers = headers;
|
options.headers = headers;
|
||||||
return this.authRequestService.getRequest('status', options).pipe(
|
return this.authRequestService.getRequest('status', options).pipe(
|
||||||
map((status) => this.linkService.resolveLinks(status, followLink<AuthStatus>('eperson'))),
|
map((status: AuthStatus) => {
|
||||||
switchMap((status: AuthStatus) => {
|
|
||||||
if (status.authenticated) {
|
if (status.authenticated) {
|
||||||
return status.eperson.pipe(map((eperson) => eperson.payload));
|
return status._links.eperson.href;
|
||||||
} else {
|
} else {
|
||||||
throw(new Error('Not authenticated'));
|
throw(new Error('Not authenticated'));
|
||||||
}
|
}
|
||||||
|
15
src/app/core/breadcrumbs/breadcrumbs.service.ts
Normal file
15
src/app/core/breadcrumbs/breadcrumbs.service.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to calculate breadcrumbs for a single part of the route
|
||||||
|
*/
|
||||||
|
export interface BreadcrumbsService<T> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to calculate the breadcrumbs for a part of the route
|
||||||
|
* @param key The key used to resolve the breadcrumb
|
||||||
|
* @param url The url to use as a link for this breadcrumb
|
||||||
|
*/
|
||||||
|
getBreadcrumbs(key: T, url: string): Observable<Breadcrumb[]>;
|
||||||
|
}
|
29
src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts
Normal file
29
src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { DSOBreadcrumbsService } from './dso-breadcrumbs.service';
|
||||||
|
import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver';
|
||||||
|
import { Collection } from '../shared/collection.model';
|
||||||
|
import { CollectionDataService } from '../data/collection-data.service';
|
||||||
|
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The class that resolves the BreadcrumbConfig object for a Collection
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class CollectionBreadcrumbResolver extends DSOBreadcrumbResolver<Collection> {
|
||||||
|
constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: CollectionDataService) {
|
||||||
|
super(breadcrumbService, dataService);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method that returns the follow links to already resolve
|
||||||
|
* The self links defined in this list are expected to be requested somewhere in the near future
|
||||||
|
* Requesting them as embeds will limit the number of requests
|
||||||
|
*/
|
||||||
|
get followLinks(): Array<FollowLinkConfig<Collection>> {
|
||||||
|
return [
|
||||||
|
followLink('parentCommunity', undefined,
|
||||||
|
followLink('parentCommunity')
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
27
src/app/core/breadcrumbs/community-breadcrumb.resolver.ts
Normal file
27
src/app/core/breadcrumbs/community-breadcrumb.resolver.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { DSOBreadcrumbsService } from './dso-breadcrumbs.service';
|
||||||
|
import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver';
|
||||||
|
import { CommunityDataService } from '../data/community-data.service';
|
||||||
|
import { Community } from '../shared/community.model';
|
||||||
|
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The class that resolves the BreadcrumbConfig object for a Community
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class CommunityBreadcrumbResolver extends DSOBreadcrumbResolver<Community> {
|
||||||
|
constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: CommunityDataService) {
|
||||||
|
super(breadcrumbService, dataService);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method that returns the follow links to already resolve
|
||||||
|
* The self links defined in this list are expected to be requested somewhere in the near future
|
||||||
|
* Requesting them as embeds will limit the number of requests
|
||||||
|
*/
|
||||||
|
get followLinks(): Array<FollowLinkConfig<Community>> {
|
||||||
|
return [
|
||||||
|
followLink('parentCommunity')
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
35
src/app/core/breadcrumbs/dso-breadcrumb.resolver.spec.ts
Normal file
35
src/app/core/breadcrumbs/dso-breadcrumb.resolver.spec.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver';
|
||||||
|
import { Collection } from '../shared/collection.model';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils';
|
||||||
|
import { getTestScheduler } from 'jasmine-marbles';
|
||||||
|
import { CollectionBreadcrumbResolver } from './collection-breadcrumb.resolver';
|
||||||
|
|
||||||
|
describe('DSOBreadcrumbResolver', () => {
|
||||||
|
describe('resolve', () => {
|
||||||
|
let resolver: DSOBreadcrumbResolver<Collection>;
|
||||||
|
let collectionService: any;
|
||||||
|
let dsoBreadcrumbService: any;
|
||||||
|
let testCollection: Collection;
|
||||||
|
let uuid;
|
||||||
|
let breadcrumbUrl;
|
||||||
|
let currentUrl;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
uuid = '1234-65487-12354-1235';
|
||||||
|
breadcrumbUrl = '/collections/' + uuid;
|
||||||
|
currentUrl = breadcrumbUrl + '/edit';
|
||||||
|
testCollection = Object.assign(new Collection(), { uuid });
|
||||||
|
dsoBreadcrumbService = {};
|
||||||
|
collectionService = {
|
||||||
|
findById: (id: string) => createSuccessfulRemoteDataObject$(testCollection)
|
||||||
|
};
|
||||||
|
resolver = new CollectionBreadcrumbResolver(dsoBreadcrumbService, collectionService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve a breadcrumb config for the correct DSO', () => {
|
||||||
|
const resolvedConfig = resolver.resolve({ params: { id: uuid } } as any, { url: currentUrl } as any);
|
||||||
|
const expectedConfig = { provider: dsoBreadcrumbService, key: testCollection, url: breadcrumbUrl };
|
||||||
|
getTestScheduler().expectObservable(resolvedConfig).toBe('(a|)', { a: expectedConfig})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
46
src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts
Normal file
46
src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { DSOBreadcrumbsService } from './dso-breadcrumbs.service';
|
||||||
|
import { DataService } from '../data/data.service';
|
||||||
|
import { getRemoteDataPayload, getSucceededRemoteData } from '../shared/operators';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||||
|
import { ChildHALResource } from '../shared/child-hal-resource.model';
|
||||||
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The class that resolves the BreadcrumbConfig object for a DSpaceObject
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export abstract class DSOBreadcrumbResolver<T extends ChildHALResource & DSpaceObject> implements Resolve<BreadcrumbConfig<T>> {
|
||||||
|
constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: DataService<T>) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method for resolving a breadcrumb config object
|
||||||
|
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
|
||||||
|
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
|
||||||
|
* @returns BreadcrumbConfig object
|
||||||
|
*/
|
||||||
|
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<BreadcrumbConfig<T>> {
|
||||||
|
const uuid = route.params.id;
|
||||||
|
return this.dataService.findById(uuid, ...this.followLinks).pipe(
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
map((object: T) => {
|
||||||
|
const fullPath = state.url;
|
||||||
|
const url = fullPath.substr(0, fullPath.indexOf(uuid)) + uuid;
|
||||||
|
return { provider: this.breadcrumbService, key: object, url: url };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method that returns the follow links to already resolve
|
||||||
|
* The self links defined in this list are expected to be requested somewhere in the near future
|
||||||
|
* Requesting them as embeds will limit the number of requests
|
||||||
|
*/
|
||||||
|
abstract get followLinks(): Array<FollowLinkConfig<T>>;
|
||||||
|
}
|
122
src/app/core/breadcrumbs/dso-breadcrumbs.service.spec.ts
Normal file
122
src/app/core/breadcrumbs/dso-breadcrumbs.service.spec.ts
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import { async, TestBed } from '@angular/core/testing';
|
||||||
|
import { DSOBreadcrumbsService } from './dso-breadcrumbs.service';
|
||||||
|
import { getMockLinkService } from '../../shared/mocks/mock-link-service';
|
||||||
|
import { LinkService } from '../cache/builders/link.service';
|
||||||
|
import { Item } from '../shared/item.model';
|
||||||
|
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils';
|
||||||
|
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { Community } from '../shared/community.model';
|
||||||
|
import { Collection } from '../shared/collection.model';
|
||||||
|
import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model';
|
||||||
|
import { getTestScheduler } from 'jasmine-marbles';
|
||||||
|
import { getDSOPath } from '../../app-routing.module';
|
||||||
|
import { DSONameService } from './dso-name.service';
|
||||||
|
|
||||||
|
describe('DSOBreadcrumbsService', () => {
|
||||||
|
let service: DSOBreadcrumbsService;
|
||||||
|
let linkService: any;
|
||||||
|
let testItem;
|
||||||
|
let testCollection;
|
||||||
|
let testCommunity;
|
||||||
|
|
||||||
|
let itemPath;
|
||||||
|
let collectionPath;
|
||||||
|
let communityPath;
|
||||||
|
|
||||||
|
let itemUUID;
|
||||||
|
let collectionUUID;
|
||||||
|
let communityUUID;
|
||||||
|
|
||||||
|
let dsoNameService;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
itemPath = '/items/';
|
||||||
|
collectionPath = '/collection/';
|
||||||
|
communityPath = '/community/';
|
||||||
|
|
||||||
|
itemUUID = '04dd18fc-03f9-4b9a-9304-ed7c313686d3';
|
||||||
|
collectionUUID = '91dfa5b5-5440-4fb4-b869-02610342f886';
|
||||||
|
communityUUID = '6c0bfa6b-ce82-4bf4-a2a8-fd7682c567e8';
|
||||||
|
|
||||||
|
testCommunity = Object.assign(new Community(),
|
||||||
|
{
|
||||||
|
type: 'community',
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [{value: 'community'}]
|
||||||
|
},
|
||||||
|
uuid: communityUUID,
|
||||||
|
parentCommunity: observableOf(Object.assign(createSuccessfulRemoteDataObject(undefined), { statusCode: 204 })),
|
||||||
|
|
||||||
|
_links: {
|
||||||
|
parentCommunity: 'site',
|
||||||
|
self: communityPath + communityUUID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
testCollection = Object.assign(new Collection(),
|
||||||
|
{
|
||||||
|
type: 'collection',
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [{value: 'collection'}]
|
||||||
|
},
|
||||||
|
uuid: collectionUUID,
|
||||||
|
parentCommunity: createSuccessfulRemoteDataObject$(testCommunity),
|
||||||
|
_links: {
|
||||||
|
parentCommunity: communityPath + communityUUID,
|
||||||
|
self: communityPath + collectionUUID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
testItem = Object.assign(new Item(),
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [{value: 'item'}]
|
||||||
|
},
|
||||||
|
uuid: itemUUID,
|
||||||
|
owningCollection: createSuccessfulRemoteDataObject$(testCollection),
|
||||||
|
_links: {
|
||||||
|
owningCollection: collectionPath + collectionUUID,
|
||||||
|
self: itemPath + itemUUID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
dsoNameService = { getName: (dso) => getName(dso) }
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
{ provide: LinkService, useValue: getMockLinkService() },
|
||||||
|
{ provide: DSONameService, useValue: dsoNameService }
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
linkService = TestBed.get(LinkService);
|
||||||
|
linkService.resolveLink.and.callFake((object, link) => object);
|
||||||
|
service = new DSOBreadcrumbsService(linkService, dsoNameService);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getBreadcrumbs', () => {
|
||||||
|
it('should return the breadcrumbs based on an Item', () => {
|
||||||
|
const breadcrumbs = service.getBreadcrumbs(testItem, testItem._links.self);
|
||||||
|
const expectedCrumbs = [
|
||||||
|
new Breadcrumb(getName(testCommunity), getDSOPath(testCommunity)),
|
||||||
|
new Breadcrumb(getName(testCollection), getDSOPath(testCollection)),
|
||||||
|
new Breadcrumb(getName(testItem), getDSOPath(testItem)),
|
||||||
|
];
|
||||||
|
getTestScheduler().expectObservable(breadcrumbs).toBe('(a|)', { a: expectedCrumbs });
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
function getName(dso: DSpaceObject): string {
|
||||||
|
return dso.metadata['dc.title'][0].value
|
||||||
|
}
|
||||||
|
});
|
50
src/app/core/breadcrumbs/dso-breadcrumbs.service.ts
Normal file
50
src/app/core/breadcrumbs/dso-breadcrumbs.service.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model';
|
||||||
|
import { BreadcrumbsService } from './breadcrumbs.service';
|
||||||
|
import { DSONameService } from './dso-name.service';
|
||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
import { ChildHALResource } from '../shared/child-hal-resource.model';
|
||||||
|
import { LinkService } from '../cache/builders/link.service';
|
||||||
|
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||||
|
import { followLink } from '../../shared/utils/follow-link-config.model';
|
||||||
|
import { find, map, switchMap } from 'rxjs/operators';
|
||||||
|
import { getDSOPath } from '../../app-routing.module';
|
||||||
|
import { RemoteData } from '../data/remote-data';
|
||||||
|
import { hasValue } from '../../shared/empty.util';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to calculate DSpaceObject breadcrumbs for a single part of the route
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class DSOBreadcrumbsService implements BreadcrumbsService<ChildHALResource & DSpaceObject> {
|
||||||
|
constructor(
|
||||||
|
private linkService: LinkService,
|
||||||
|
private dsoNameService: DSONameService
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to recursively calculate the breadcrumbs
|
||||||
|
* This method returns the name and url of the key and all its parent DSO's recursively, top down
|
||||||
|
* @param key The key (a DSpaceObject) used to resolve the breadcrumb
|
||||||
|
* @param url The url to use as a link for this breadcrumb
|
||||||
|
*/
|
||||||
|
getBreadcrumbs(key: ChildHALResource & DSpaceObject, url: string): Observable<Breadcrumb[]> {
|
||||||
|
const label = this.dsoNameService.getName(key);
|
||||||
|
const crumb = new Breadcrumb(label, url);
|
||||||
|
const propertyName = key.getParentLinkKey();
|
||||||
|
return this.linkService.resolveLink(key, followLink(propertyName))[propertyName].pipe(
|
||||||
|
find((parentRD: RemoteData<ChildHALResource & DSpaceObject>) => parentRD.hasSucceeded || parentRD.statusCode === 204),
|
||||||
|
switchMap((parentRD: RemoteData<ChildHALResource & DSpaceObject>) => {
|
||||||
|
if (hasValue(parentRD.payload)) {
|
||||||
|
const parent = parentRD.payload;
|
||||||
|
return this.getBreadcrumbs(parent, getDSOPath(parent))
|
||||||
|
}
|
||||||
|
return observableOf([]);
|
||||||
|
|
||||||
|
}),
|
||||||
|
map((breadcrumbs: Breadcrumb[]) => [...breadcrumbs, crumb])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
116
src/app/core/breadcrumbs/dso-name.service.spec.ts
Normal file
116
src/app/core/breadcrumbs/dso-name.service.spec.ts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import { ListableObject } from '../../shared/object-collection/shared/listable-object.model';
|
||||||
|
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||||
|
import { GenericConstructor } from '../shared/generic-constructor';
|
||||||
|
import { Item } from '../shared/item.model';
|
||||||
|
import { MetadataValueFilter } from '../shared/metadata.models';
|
||||||
|
import { DSONameService } from './dso-name.service';
|
||||||
|
|
||||||
|
describe(`DSONameService`, () => {
|
||||||
|
let service: DSONameService;
|
||||||
|
let mockPersonName: string;
|
||||||
|
let mockPerson: DSpaceObject;
|
||||||
|
let mockOrgUnitName: string;
|
||||||
|
let mockOrgUnit: DSpaceObject;
|
||||||
|
let mockDSOName: string;
|
||||||
|
let mockDSO: DSpaceObject;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockPersonName = 'Doe, John';
|
||||||
|
mockPerson = Object.assign(new DSpaceObject(), {
|
||||||
|
firstMetadataValue(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): string {
|
||||||
|
return mockPersonName
|
||||||
|
},
|
||||||
|
getRenderTypes(): Array<string | GenericConstructor<ListableObject>> {
|
||||||
|
return ['Person', Item, DSpaceObject];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mockOrgUnitName = 'Molecular Spectroscopy';
|
||||||
|
mockOrgUnit = Object.assign(new DSpaceObject(), {
|
||||||
|
firstMetadataValue(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): string {
|
||||||
|
return mockOrgUnitName
|
||||||
|
},
|
||||||
|
getRenderTypes(): Array<string | GenericConstructor<ListableObject>> {
|
||||||
|
return ['OrgUnit', Item, DSpaceObject];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mockDSOName = 'Lorem Ipsum';
|
||||||
|
mockDSO = Object.assign(new DSpaceObject(), {
|
||||||
|
firstMetadataValue(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): string {
|
||||||
|
return mockDSOName
|
||||||
|
},
|
||||||
|
getRenderTypes(): Array<string | GenericConstructor<ListableObject>> {
|
||||||
|
return [DSpaceObject];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
service = new DSONameService();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(`getName`, () => {
|
||||||
|
it(`should use the Person factory for Person entities`, () => {
|
||||||
|
spyOn((service as any).factories, 'Person').and.returnValue('Bingo!');
|
||||||
|
|
||||||
|
const result = service.getName(mockPerson);
|
||||||
|
|
||||||
|
expect((service as any).factories.Person).toHaveBeenCalledWith(mockPerson);
|
||||||
|
expect(result).toBe('Bingo!');
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should use the OrgUnit factory for OrgUnit entities`, () => {
|
||||||
|
spyOn((service as any).factories, 'OrgUnit').and.returnValue('Bingo!');
|
||||||
|
|
||||||
|
const result = service.getName(mockOrgUnit);
|
||||||
|
|
||||||
|
expect((service as any).factories.OrgUnit).toHaveBeenCalledWith(mockOrgUnit);
|
||||||
|
expect(result).toBe('Bingo!');
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should use the Default factory for regular DSpaceObjects`, () => {
|
||||||
|
spyOn((service as any).factories, 'Default').and.returnValue('Bingo!');
|
||||||
|
|
||||||
|
const result = service.getName(mockDSO);
|
||||||
|
|
||||||
|
expect((service as any).factories.Default).toHaveBeenCalledWith(mockDSO);
|
||||||
|
expect(result).toBe('Bingo!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(`factories.Person`, () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(mockPerson, 'firstMetadataValue').and.returnValues(...mockPersonName.split(', '));
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should return 'person.familyName, person.givenName'`, () => {
|
||||||
|
const result = (service as any).factories.Person(mockPerson);
|
||||||
|
expect(result).toBe(mockPersonName);
|
||||||
|
expect(mockPerson.firstMetadataValue).toHaveBeenCalledWith('person.familyName');
|
||||||
|
expect(mockPerson.firstMetadataValue).toHaveBeenCalledWith('person.givenName');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(`factories.OrgUnit`, () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(mockOrgUnit, 'firstMetadataValue').and.callThrough();
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should return 'organization.legalName'`, () => {
|
||||||
|
const result = (service as any).factories.OrgUnit(mockOrgUnit);
|
||||||
|
expect(result).toBe(mockOrgUnitName);
|
||||||
|
expect(mockOrgUnit.firstMetadataValue).toHaveBeenCalledWith('organization.legalName');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(`factories.Default`, () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(mockDSO, 'firstMetadataValue').and.callThrough();
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should return 'dc.title'`, () => {
|
||||||
|
const result = (service as any).factories.Default(mockDSO);
|
||||||
|
expect(result).toBe(mockDSOName);
|
||||||
|
expect(mockDSO.firstMetadataValue).toHaveBeenCalledWith('dc.title');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
53
src/app/core/breadcrumbs/dso-name.service.ts
Normal file
53
src/app/core/breadcrumbs/dso-name.service.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { hasValue } from '../../shared/empty.util';
|
||||||
|
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a name for a {@link DSpaceObject} based
|
||||||
|
* on its render types.
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class DSONameService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Functions to generate the specific names.
|
||||||
|
*
|
||||||
|
* If this list ever expands it will probably be worth it to
|
||||||
|
* refactor this using decorators for specific entity types,
|
||||||
|
* or perhaps by using a dedicated model for each entity type
|
||||||
|
*
|
||||||
|
* With only two exceptions those solutions seem overkill for now.
|
||||||
|
*/
|
||||||
|
private factories = {
|
||||||
|
Person: (dso: DSpaceObject): string => {
|
||||||
|
return `${dso.firstMetadataValue('person.familyName')}, ${dso.firstMetadataValue('person.givenName')}`;
|
||||||
|
},
|
||||||
|
OrgUnit: (dso: DSpaceObject): string => {
|
||||||
|
return dso.firstMetadataValue('organization.legalName');
|
||||||
|
},
|
||||||
|
Default: (dso: DSpaceObject): string => {
|
||||||
|
return dso.firstMetadataValue('dc.title');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the name for the given {@link DSpaceObject}
|
||||||
|
*
|
||||||
|
* @param dso The {@link DSpaceObject} you want a name for
|
||||||
|
*/
|
||||||
|
getName(dso: DSpaceObject): string {
|
||||||
|
const types = dso.getRenderTypes();
|
||||||
|
const match = types
|
||||||
|
.filter((type) => typeof type === 'string')
|
||||||
|
.find((type: string) => Object.keys(this.factories).includes(type)) as string;
|
||||||
|
|
||||||
|
if (hasValue(match)) {
|
||||||
|
return this.factories[match](dso);
|
||||||
|
} else {
|
||||||
|
return this.factories.Default(dso);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
28
src/app/core/breadcrumbs/i18n-breadcrumb.resolver.spec.ts
Normal file
28
src/app/core/breadcrumbs/i18n-breadcrumb.resolver.spec.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { I18nBreadcrumbResolver } from './i18n-breadcrumb.resolver';
|
||||||
|
|
||||||
|
describe('I18nBreadcrumbResolver', () => {
|
||||||
|
describe('resolve', () => {
|
||||||
|
let resolver: I18nBreadcrumbResolver;
|
||||||
|
let i18nBreadcrumbService: any;
|
||||||
|
let i18nKey: string;
|
||||||
|
let path: string;
|
||||||
|
beforeEach(() => {
|
||||||
|
i18nKey = 'example.key';
|
||||||
|
path = 'rest.com/path/to/breadcrumb';
|
||||||
|
i18nBreadcrumbService = {};
|
||||||
|
resolver = new I18nBreadcrumbResolver(i18nBreadcrumbService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve the breadcrumb config', () => {
|
||||||
|
const resolvedConfig = resolver.resolve({ data: { breadcrumbKey: i18nKey }, url: [path] } as any, {} as any);
|
||||||
|
const expectedConfig = { provider: i18nBreadcrumbService, key: i18nKey, url: path };
|
||||||
|
expect(resolvedConfig).toEqual(expectedConfig);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve throw an error when no breadcrumbKey is defined', () => {
|
||||||
|
expect(() => {
|
||||||
|
resolver.resolve({ data: {} } as any, undefined)
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
29
src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts
Normal file
29
src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { I18nBreadcrumbsService } from './i18n-breadcrumbs.service';
|
||||||
|
import { hasNoValue } from '../../shared/empty.util';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The class that resolves a BreadcrumbConfig object with an i18n key string for a route
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class I18nBreadcrumbResolver implements Resolve<BreadcrumbConfig<string>> {
|
||||||
|
constructor(protected breadcrumbService: I18nBreadcrumbsService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method for resolving an I18n breadcrumb configuration object
|
||||||
|
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
|
||||||
|
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
|
||||||
|
* @returns BreadcrumbConfig object
|
||||||
|
*/
|
||||||
|
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): BreadcrumbConfig<string> {
|
||||||
|
const key = route.data.breadcrumbKey;
|
||||||
|
if (hasNoValue(key)) {
|
||||||
|
throw new Error('You provided an i18nBreadcrumbResolver for url \"' + route.url + '\" but no breadcrumbKey in the route\'s data')
|
||||||
|
}
|
||||||
|
const fullPath = route.url.join('');
|
||||||
|
return { provider: this.breadcrumbService, key: key, url: fullPath };
|
||||||
|
}
|
||||||
|
}
|
31
src/app/core/breadcrumbs/i18n-breadcrumbs.service.spec.ts
Normal file
31
src/app/core/breadcrumbs/i18n-breadcrumbs.service.spec.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { async, TestBed } from '@angular/core/testing';
|
||||||
|
import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model';
|
||||||
|
import { getTestScheduler } from 'jasmine-marbles';
|
||||||
|
import { BREADCRUMB_MESSAGE_POSTFIX, I18nBreadcrumbsService } from './i18n-breadcrumbs.service';
|
||||||
|
|
||||||
|
describe('I18nBreadcrumbsService', () => {
|
||||||
|
let service: I18nBreadcrumbsService;
|
||||||
|
let exampleString;
|
||||||
|
let exampleURL;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
exampleString = 'example.string';
|
||||||
|
exampleURL = 'example.com';
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service = new I18nBreadcrumbsService();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getBreadcrumbs', () => {
|
||||||
|
it('should return a breadcrumb based on a string by adding the postfix', () => {
|
||||||
|
const breadcrumbs = service.getBreadcrumbs(exampleString, exampleURL);
|
||||||
|
getTestScheduler().expectObservable(breadcrumbs).toBe('(a|)', { a: [new Breadcrumb(exampleString + BREADCRUMB_MESSAGE_POSTFIX, exampleURL)] });
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
25
src/app/core/breadcrumbs/i18n-breadcrumbs.service.ts
Normal file
25
src/app/core/breadcrumbs/i18n-breadcrumbs.service.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model';
|
||||||
|
import { BreadcrumbsService } from './breadcrumbs.service';
|
||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The postfix for i18n breadcrumbs
|
||||||
|
*/
|
||||||
|
export const BREADCRUMB_MESSAGE_POSTFIX = '.breadcrumbs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to calculate i18n breadcrumbs for a single part of the route
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class I18nBreadcrumbsService implements BreadcrumbsService<string> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to calculate the breadcrumbs
|
||||||
|
* @param key The key used to resolve the breadcrumb
|
||||||
|
* @param url The url to use as a link for this breadcrumb
|
||||||
|
*/
|
||||||
|
getBreadcrumbs(key: string, url: string): Observable<Breadcrumb[]> {
|
||||||
|
return observableOf([new Breadcrumb(key + BREADCRUMB_MESSAGE_POSTFIX, url)]);
|
||||||
|
}
|
||||||
|
}
|
32
src/app/core/breadcrumbs/item-breadcrumb.resolver.ts
Normal file
32
src/app/core/breadcrumbs/item-breadcrumb.resolver.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { DSOBreadcrumbsService } from './dso-breadcrumbs.service';
|
||||||
|
import { ItemDataService } from '../data/item-data.service';
|
||||||
|
import { Item } from '../shared/item.model';
|
||||||
|
import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver';
|
||||||
|
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The class that resolves the BreadcrumbConfig object for an Item
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class ItemBreadcrumbResolver extends DSOBreadcrumbResolver<Item> {
|
||||||
|
constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: ItemDataService) {
|
||||||
|
super(breadcrumbService, dataService);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method that returns the follow links to already resolve
|
||||||
|
* The self links defined in this list are expected to be requested somewhere in the near future
|
||||||
|
* Requesting them as embeds will limit the number of requests
|
||||||
|
*/
|
||||||
|
get followLinks(): Array<FollowLinkConfig<Item>> {
|
||||||
|
return [
|
||||||
|
followLink('owningCollection', undefined,
|
||||||
|
followLink('parentCommunity', undefined,
|
||||||
|
followLink('parentCommunity'))
|
||||||
|
),
|
||||||
|
followLink('bundles'),
|
||||||
|
followLink('relationships')
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@@ -96,13 +96,14 @@ export class RemoteDataBuildService {
|
|||||||
const responsePending = hasValue(reqEntry) && hasValue(reqEntry.responsePending) ? reqEntry.responsePending : false;
|
const responsePending = hasValue(reqEntry) && hasValue(reqEntry.responsePending) ? reqEntry.responsePending : false;
|
||||||
let isSuccessful: boolean;
|
let isSuccessful: boolean;
|
||||||
let error: RemoteDataError;
|
let error: RemoteDataError;
|
||||||
if (hasValue(reqEntry) && hasValue(reqEntry.response)) {
|
const response = reqEntry ? reqEntry.response : undefined;
|
||||||
isSuccessful = reqEntry.response.isSuccessful;
|
if (hasValue(response)) {
|
||||||
const errorMessage = isSuccessful === false ? (reqEntry.response as ErrorResponse).errorMessage : undefined;
|
isSuccessful = response.isSuccessful;
|
||||||
|
const errorMessage = isSuccessful === false ? (response as ErrorResponse).errorMessage : undefined;
|
||||||
if (hasValue(errorMessage)) {
|
if (hasValue(errorMessage)) {
|
||||||
error = new RemoteDataError(
|
error = new RemoteDataError(
|
||||||
(reqEntry.response as ErrorResponse).statusCode,
|
response.statusCode,
|
||||||
(reqEntry.response as ErrorResponse).statusText,
|
response.statusText,
|
||||||
errorMessage
|
errorMessage
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -112,7 +113,9 @@ export class RemoteDataBuildService {
|
|||||||
responsePending,
|
responsePending,
|
||||||
isSuccessful,
|
isSuccessful,
|
||||||
error,
|
error,
|
||||||
payload
|
payload,
|
||||||
|
hasValue(response) ? response.statusCode : undefined
|
||||||
|
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@@ -3,7 +3,7 @@ import { Injectable } from '@angular/core';
|
|||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { Observable } from 'rxjs/internal/Observable';
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
import { map, switchMap } from 'rxjs/operators';
|
import { map, switchMap } from 'rxjs/operators';
|
||||||
import { hasValue } from '../../shared/empty.util';
|
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
import { dataService } from '../cache/builders/build-decorators';
|
import { dataService } from '../cache/builders/build-decorators';
|
||||||
@@ -72,7 +72,7 @@ export class BitstreamDataService extends DataService<Bitstream> {
|
|||||||
public getThumbnailFor(item: Item): Observable<RemoteData<Bitstream>> {
|
public getThumbnailFor(item: Item): Observable<RemoteData<Bitstream>> {
|
||||||
return this.bundleService.findByItemAndName(item, 'THUMBNAIL').pipe(
|
return this.bundleService.findByItemAndName(item, 'THUMBNAIL').pipe(
|
||||||
switchMap((bundleRD: RemoteData<Bundle>) => {
|
switchMap((bundleRD: RemoteData<Bundle>) => {
|
||||||
if (hasValue(bundleRD.payload)) {
|
if (isNotEmpty(bundleRD.payload)) {
|
||||||
return this.findAllByBundle(bundleRD.payload, { elementsPerPage: 1 }).pipe(
|
return this.findAllByBundle(bundleRD.payload, { elementsPerPage: 1 }).pipe(
|
||||||
map((bitstreamRD: RemoteData<PaginatedList<Bitstream>>) => {
|
map((bitstreamRD: RemoteData<PaginatedList<Bitstream>>) => {
|
||||||
if (hasValue(bitstreamRD.payload) && hasValue(bitstreamRD.payload.page)) {
|
if (hasValue(bitstreamRD.payload) && hasValue(bitstreamRD.payload.page)) {
|
||||||
@@ -108,7 +108,7 @@ export class BitstreamDataService extends DataService<Bitstream> {
|
|||||||
public getMatchingThumbnail(item: Item, bitstreamInOriginal: Bitstream): Observable<RemoteData<Bitstream>> {
|
public getMatchingThumbnail(item: Item, bitstreamInOriginal: Bitstream): Observable<RemoteData<Bitstream>> {
|
||||||
return this.bundleService.findByItemAndName(item, 'THUMBNAIL').pipe(
|
return this.bundleService.findByItemAndName(item, 'THUMBNAIL').pipe(
|
||||||
switchMap((bundleRD: RemoteData<Bundle>) => {
|
switchMap((bundleRD: RemoteData<Bundle>) => {
|
||||||
if (hasValue(bundleRD.payload)) {
|
if (isNotEmpty(bundleRD.payload)) {
|
||||||
return this.findAllByBundle(bundleRD.payload, { elementsPerPage: Number.MAX_SAFE_INTEGER }).pipe(
|
return this.findAllByBundle(bundleRD.payload, { elementsPerPage: Number.MAX_SAFE_INTEGER }).pipe(
|
||||||
map((bitstreamRD: RemoteData<PaginatedList<Bitstream>>) => {
|
map((bitstreamRD: RemoteData<PaginatedList<Bitstream>>) => {
|
||||||
if (hasValue(bitstreamRD.payload) && hasValue(bitstreamRD.payload.page)) {
|
if (hasValue(bitstreamRD.payload) && hasValue(bitstreamRD.payload.page)) {
|
||||||
|
@@ -282,7 +282,7 @@ describe('BitstreamFormatDataService', () => {
|
|||||||
format.id = 'format-id';
|
format.id = 'format-id';
|
||||||
|
|
||||||
const expected = cold('(b|)', {b: true});
|
const expected = cold('(b|)', {b: true});
|
||||||
const result = service.delete(format);
|
const result = service.delete(format.id);
|
||||||
|
|
||||||
expect(result).toBeObservable(expected);
|
expect(result).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
|
@@ -154,19 +154,19 @@ export class BitstreamFormatDataService extends DataService<BitstreamFormat> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete an existing DSpace Object on the server
|
* Delete an existing DSpace Object on the server
|
||||||
* @param format The DSpace Object to be removed
|
* @param formatID The DSpace Object'id to be removed
|
||||||
* Return an observable that emits true when the deletion was successful, false when it failed
|
* Return an observable that emits true when the deletion was successful, false when it failed
|
||||||
*/
|
*/
|
||||||
delete(format: BitstreamFormat): Observable<boolean> {
|
delete(formatID: string): Observable<boolean> {
|
||||||
const requestId = this.requestService.generateRequestId();
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
|
||||||
const hrefObs = this.halService.getEndpoint(this.linkPath).pipe(
|
const hrefObs = this.halService.getEndpoint(this.linkPath).pipe(
|
||||||
map((endpoint: string) => this.getIDHref(endpoint, format.id)));
|
map((endpoint: string) => this.getIDHref(endpoint, formatID)));
|
||||||
|
|
||||||
hrefObs.pipe(
|
hrefObs.pipe(
|
||||||
find((href: string) => hasValue(href)),
|
find((href: string) => hasValue(href)),
|
||||||
map((href: string) => {
|
map((href: string) => {
|
||||||
const request = new DeleteByIDRequest(requestId, href, format.id);
|
const request = new DeleteByIDRequest(requestId, href, formatID);
|
||||||
this.requestService.configure(request);
|
this.requestService.configure(request);
|
||||||
})
|
})
|
||||||
).subscribe();
|
).subscribe();
|
||||||
|
@@ -152,7 +152,11 @@ export abstract class DataService<T extends CacheableObject> {
|
|||||||
/**
|
/**
|
||||||
* Returns {@link RemoteData} of all object with a list of {@link FollowLinkConfig}, to indicate which embedded
|
* Returns {@link RemoteData} of all object with a list of {@link FollowLinkConfig}, to indicate which embedded
|
||||||
* info should be added to the objects
|
* info should be added to the objects
|
||||||
|
*
|
||||||
|
* @param options Find list options object
|
||||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||||
|
* @return {Observable<RemoteData<PaginatedList<T>>>}
|
||||||
|
* Return an observable that emits object list
|
||||||
*/
|
*/
|
||||||
findAll(options: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<T>>): Observable<RemoteData<PaginatedList<T>>> {
|
findAll(options: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<T>>): Observable<RemoteData<PaginatedList<T>>> {
|
||||||
return this.findList(this.getFindAllHref(options), options, ...linksToFollow);
|
return this.findList(this.getFindAllHref(options), options, ...linksToFollow);
|
||||||
@@ -162,6 +166,7 @@ export abstract class DataService<T extends CacheableObject> {
|
|||||||
* Returns an observable of {@link RemoteData} of an object, based on href observable,
|
* Returns an observable of {@link RemoteData} of an object, based on href observable,
|
||||||
* with a list of {@link FollowLinkConfig}, to automatically resolve {@link HALLink}s of the object
|
* with a list of {@link FollowLinkConfig}, to automatically resolve {@link HALLink}s of the object
|
||||||
* @param href$ Observable of href of object we want to retrieve
|
* @param href$ Observable of href of object we want to retrieve
|
||||||
|
* @param options Find list options object
|
||||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||||
*/
|
*/
|
||||||
protected findList(href$, options: FindListOptions, ...linksToFollow: Array<FollowLinkConfig<T>>) {
|
protected findList(href$, options: FindListOptions, ...linksToFollow: Array<FollowLinkConfig<T>>) {
|
||||||
@@ -231,6 +236,7 @@ export abstract class DataService<T extends CacheableObject> {
|
|||||||
* Returns a list of observables of {@link RemoteData} of objects, based on an href, with a list of {@link FollowLinkConfig},
|
* Returns a list of observables of {@link RemoteData} of objects, based on an href, with a list of {@link FollowLinkConfig},
|
||||||
* to automatically resolve {@link HALLink}s of the object
|
* to automatically resolve {@link HALLink}s of the object
|
||||||
* @param href The url of object we want to retrieve
|
* @param href The url of object we want to retrieve
|
||||||
|
* @param findListOptions Find list options object
|
||||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||||
*/
|
*/
|
||||||
findAllByHref(href: string, findListOptions: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<T>>): Observable<RemoteData<PaginatedList<T>>> {
|
findAllByHref(href: string, findListOptions: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<T>>): Observable<RemoteData<PaginatedList<T>>> {
|
||||||
@@ -259,6 +265,7 @@ export abstract class DataService<T extends CacheableObject> {
|
|||||||
*
|
*
|
||||||
* @param searchMethod The search method for the object
|
* @param searchMethod The search method for the object
|
||||||
* @param options The [[FindListOptions]] object
|
* @param options The [[FindListOptions]] object
|
||||||
|
* @param linksToFollow The array of [[FollowLinkConfig]]
|
||||||
* @return {Observable<RemoteData<PaginatedList<T>>}
|
* @return {Observable<RemoteData<PaginatedList<T>>}
|
||||||
* Return an observable that emits response from the server
|
* Return an observable that emits response from the server
|
||||||
*/
|
*/
|
||||||
@@ -367,16 +374,16 @@ export abstract class DataService<T extends CacheableObject> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete an existing DSpace Object on the server
|
* Delete an existing DSpace Object on the server
|
||||||
* @param dso The DSpace Object to be removed
|
* @param dsoID The DSpace Object' id to be removed
|
||||||
* @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual
|
* @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual
|
||||||
* metadata should be saved as real metadata
|
* metadata should be saved as real metadata
|
||||||
* @return an observable that emits true when the deletion was successful, false when it failed
|
* @return an observable that emits true when the deletion was successful, false when it failed
|
||||||
*/
|
*/
|
||||||
delete(dso: T, copyVirtualMetadata?: string[]): Observable<boolean> {
|
delete(dsoID: string, copyVirtualMetadata?: string[]): Observable<boolean> {
|
||||||
const requestId = this.requestService.generateRequestId();
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
|
||||||
const hrefObs = this.halService.getEndpoint(this.linkPath).pipe(
|
const hrefObs = this.halService.getEndpoint(this.linkPath).pipe(
|
||||||
map((endpoint: string) => this.getIDHref(endpoint, dso.uuid)));
|
map((endpoint: string) => this.getIDHref(endpoint, dsoID)));
|
||||||
|
|
||||||
hrefObs.pipe(
|
hrefObs.pipe(
|
||||||
find((href: string) => hasValue(href)),
|
find((href: string) => hasValue(href)),
|
||||||
@@ -388,7 +395,7 @@ export abstract class DataService<T extends CacheableObject> {
|
|||||||
+ id
|
+ id
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const request = new DeleteByIDRequest(requestId, href, dso.uuid);
|
const request = new DeleteByIDRequest(requestId, href, dsoID);
|
||||||
this.requestService.configure(request);
|
this.requestService.configure(request);
|
||||||
})
|
})
|
||||||
).subscribe();
|
).subscribe();
|
||||||
|
@@ -17,7 +17,8 @@ export class RemoteData<T> {
|
|||||||
private responsePending?: boolean,
|
private responsePending?: boolean,
|
||||||
private isSuccessful?: boolean,
|
private isSuccessful?: boolean,
|
||||||
public error?: RemoteDataError,
|
public error?: RemoteDataError,
|
||||||
public payload?: T
|
public payload?: T,
|
||||||
|
public statusCode?: number,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -18,6 +18,8 @@ import { RemoteData } from '../data/remote-data';
|
|||||||
import { PaginatedList } from '../data/paginated-list';
|
import { PaginatedList } from '../data/paginated-list';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
|
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
|
||||||
|
import { dataService } from '../cache/builders/build-decorators';
|
||||||
|
import { GROUP } from './models/group.resource-type';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides methods to retrieve eperson group resources.
|
* Provides methods to retrieve eperson group resources.
|
||||||
@@ -25,6 +27,7 @@ import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
|
|||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
|
@dataService(GROUP)
|
||||||
export class GroupDataService extends DataService<Group> {
|
export class GroupDataService extends DataService<Group> {
|
||||||
protected linkPath = 'groups';
|
protected linkPath = 'groups';
|
||||||
protected browseEndpoint = '';
|
protected browseEndpoint = '';
|
||||||
|
@@ -10,6 +10,7 @@ import { catchError, distinctUntilKeyChanged, filter, first, map, take } from 'r
|
|||||||
|
|
||||||
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
|
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
|
||||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { DSONameService } from '../breadcrumbs/dso-name.service';
|
||||||
import { CacheableObject } from '../cache/object-cache.reducer';
|
import { CacheableObject } from '../cache/object-cache.reducer';
|
||||||
import { BitstreamDataService } from '../data/bitstream-data.service';
|
import { BitstreamDataService } from '../data/bitstream-data.service';
|
||||||
import { BitstreamFormatDataService } from '../data/bitstream-format-data.service';
|
import { BitstreamFormatDataService } from '../data/bitstream-format-data.service';
|
||||||
@@ -35,6 +36,7 @@ export class MetadataService {
|
|||||||
private translate: TranslateService,
|
private translate: TranslateService,
|
||||||
private meta: Meta,
|
private meta: Meta,
|
||||||
private title: Title,
|
private title: Title,
|
||||||
|
private dsoNameService: DSONameService,
|
||||||
private bitstreamDataService: BitstreamDataService,
|
private bitstreamDataService: BitstreamDataService,
|
||||||
private bitstreamFormatDataService: BitstreamFormatDataService,
|
private bitstreamFormatDataService: BitstreamFormatDataService,
|
||||||
@Inject(GLOBAL_CONFIG) private envConfig: GlobalConfig
|
@Inject(GLOBAL_CONFIG) private envConfig: GlobalConfig
|
||||||
@@ -154,7 +156,7 @@ export class MetadataService {
|
|||||||
* Add <meta name="title" ... > to the <head>
|
* Add <meta name="title" ... > to the <head>
|
||||||
*/
|
*/
|
||||||
private setTitleTag(): void {
|
private setTitleTag(): void {
|
||||||
const value = this.getMetaTagValue('dc.title');
|
const value = this.dsoNameService.getName(this.currentObject.getValue());
|
||||||
this.addMetaTag('title', value);
|
this.addMetaTag('title', value);
|
||||||
this.title.setTitle(value);
|
this.title.setTitle(value);
|
||||||
}
|
}
|
||||||
|
12
src/app/core/shared/child-hal-resource.model.ts
Normal file
12
src/app/core/shared/child-hal-resource.model.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { HALResource } from './hal-resource.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for HALResources with a parent object link
|
||||||
|
*/
|
||||||
|
export interface ChildHALResource extends HALResource {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the key of the parent link
|
||||||
|
*/
|
||||||
|
getParentLinkKey(): keyof this['_links'];
|
||||||
|
}
|
@@ -12,10 +12,13 @@ import { License } from './license.model';
|
|||||||
import { LICENSE } from './license.resource-type';
|
import { LICENSE } from './license.resource-type';
|
||||||
import { ResourcePolicy } from './resource-policy.model';
|
import { ResourcePolicy } from './resource-policy.model';
|
||||||
import { RESOURCE_POLICY } from './resource-policy.resource-type';
|
import { RESOURCE_POLICY } from './resource-policy.resource-type';
|
||||||
|
import { COMMUNITY } from './community.resource-type';
|
||||||
|
import { Community } from './community.model';
|
||||||
|
import { ChildHALResource } from './child-hal-resource.model';
|
||||||
|
|
||||||
@typedObject
|
@typedObject
|
||||||
@inheritSerialization(DSpaceObject)
|
@inheritSerialization(DSpaceObject)
|
||||||
export class Collection extends DSpaceObject {
|
export class Collection extends DSpaceObject implements ChildHALResource {
|
||||||
static type = COLLECTION;
|
static type = COLLECTION;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,6 +38,7 @@ export class Collection extends DSpaceObject {
|
|||||||
itemtemplate: HALLink;
|
itemtemplate: HALLink;
|
||||||
defaultAccessConditions: HALLink;
|
defaultAccessConditions: HALLink;
|
||||||
logo: HALLink;
|
logo: HALLink;
|
||||||
|
parentCommunity: HALLink;
|
||||||
self: HALLink;
|
self: HALLink;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -59,6 +63,13 @@ export class Collection extends DSpaceObject {
|
|||||||
@link(RESOURCE_POLICY, true)
|
@link(RESOURCE_POLICY, true)
|
||||||
defaultAccessConditions?: Observable<RemoteData<PaginatedList<ResourcePolicy>>>;
|
defaultAccessConditions?: Observable<RemoteData<PaginatedList<ResourcePolicy>>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Community that is a direct parent of this Collection
|
||||||
|
* Will be undefined unless the parent community HALLink has been resolved.
|
||||||
|
*/
|
||||||
|
@link(COMMUNITY, false)
|
||||||
|
parentCommunity?: Observable<RemoteData<Community>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The introductory text of this Collection
|
* The introductory text of this Collection
|
||||||
* Corresponds to the metadata field dc.description
|
* Corresponds to the metadata field dc.description
|
||||||
@@ -98,4 +109,8 @@ export class Collection extends DSpaceObject {
|
|||||||
get sidebarText(): string {
|
get sidebarText(): string {
|
||||||
return this.firstMetadataValue('dc.description.tableofcontents');
|
return this.firstMetadataValue('dc.description.tableofcontents');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getParentLinkKey(): keyof this['_links'] {
|
||||||
|
return 'parentCommunity';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -10,10 +10,11 @@ import { COLLECTION } from './collection.resource-type';
|
|||||||
import { COMMUNITY } from './community.resource-type';
|
import { COMMUNITY } from './community.resource-type';
|
||||||
import { DSpaceObject } from './dspace-object.model';
|
import { DSpaceObject } from './dspace-object.model';
|
||||||
import { HALLink } from './hal-link.model';
|
import { HALLink } from './hal-link.model';
|
||||||
|
import { ChildHALResource } from './child-hal-resource.model';
|
||||||
|
|
||||||
@typedObject
|
@typedObject
|
||||||
@inheritSerialization(DSpaceObject)
|
@inheritSerialization(DSpaceObject)
|
||||||
export class Community extends DSpaceObject {
|
export class Community extends DSpaceObject implements ChildHALResource {
|
||||||
static type = COMMUNITY;
|
static type = COMMUNITY;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -30,6 +31,7 @@ export class Community extends DSpaceObject {
|
|||||||
collections: HALLink;
|
collections: HALLink;
|
||||||
logo: HALLink;
|
logo: HALLink;
|
||||||
subcommunities: HALLink;
|
subcommunities: HALLink;
|
||||||
|
parentCommunity: HALLink;
|
||||||
self: HALLink;
|
self: HALLink;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -54,6 +56,13 @@ export class Community extends DSpaceObject {
|
|||||||
@link(COMMUNITY, true)
|
@link(COMMUNITY, true)
|
||||||
subcommunities?: Observable<RemoteData<PaginatedList<Community>>>;
|
subcommunities?: Observable<RemoteData<PaginatedList<Community>>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Community that is a direct parent of this Community
|
||||||
|
* Will be undefined unless the parent community HALLink has been resolved.
|
||||||
|
*/
|
||||||
|
@link(COMMUNITY, false)
|
||||||
|
parentCommunity?: Observable<RemoteData<Community>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The introductory text of this Community
|
* The introductory text of this Community
|
||||||
* Corresponds to the metadata field dc.description
|
* Corresponds to the metadata field dc.description
|
||||||
@@ -85,4 +94,8 @@ export class Community extends DSpaceObject {
|
|||||||
get sidebarText(): string {
|
get sidebarText(): string {
|
||||||
return this.firstMetadataValue('dc.description.tableofcontents');
|
return this.firstMetadataValue('dc.description.tableofcontents');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getParentLinkKey(): keyof this['_links'] {
|
||||||
|
return 'parentCommunity';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -69,6 +69,7 @@ export class DSpaceObject extends ListableObject implements CacheableObject {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The name for this DSpaceObject
|
* The name for this DSpaceObject
|
||||||
|
* @deprecated use {@link DSONameService} instead
|
||||||
*/
|
*/
|
||||||
get name(): string {
|
get name(): string {
|
||||||
return (isUndefined(this._name)) ? this.firstMetadataValue('dc.title') : this._name;
|
return (isUndefined(this._name)) ? this.firstMetadataValue('dc.title') : this._name;
|
||||||
|
@@ -17,6 +17,7 @@ import { HALLink } from './hal-link.model';
|
|||||||
import { Relationship } from './item-relationships/relationship.model';
|
import { Relationship } from './item-relationships/relationship.model';
|
||||||
import { RELATIONSHIP } from './item-relationships/relationship.resource-type';
|
import { RELATIONSHIP } from './item-relationships/relationship.resource-type';
|
||||||
import { ITEM } from './item.resource-type';
|
import { ITEM } from './item.resource-type';
|
||||||
|
import { ChildHALResource } from './child-hal-resource.model';
|
||||||
import { Version } from './version.model';
|
import { Version } from './version.model';
|
||||||
import { VERSION } from './version.resource-type';
|
import { VERSION } from './version.resource-type';
|
||||||
|
|
||||||
@@ -25,7 +26,7 @@ import { VERSION } from './version.resource-type';
|
|||||||
*/
|
*/
|
||||||
@typedObject
|
@typedObject
|
||||||
@inheritSerialization(DSpaceObject)
|
@inheritSerialization(DSpaceObject)
|
||||||
export class Item extends DSpaceObject {
|
export class Item extends DSpaceObject implements ChildHALResource {
|
||||||
static type = ITEM;
|
static type = ITEM;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -110,4 +111,8 @@ export class Item extends DSpaceObject {
|
|||||||
}
|
}
|
||||||
return [entityType, ...super.getRenderTypes()];
|
return [entityType, ...super.getRenderTypes()];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getParentLinkKey(): keyof this['_links'] {
|
||||||
|
return 'owningCollection';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,7 @@ import { combineLatest as observableCombineLatest, Observable, of as observableO
|
|||||||
import { Injectable, OnDestroy } from '@angular/core';
|
import { Injectable, OnDestroy } from '@angular/core';
|
||||||
import { NavigationExtras, Router } from '@angular/router';
|
import { NavigationExtras, Router } from '@angular/router';
|
||||||
import { first, map, switchMap, tap } from 'rxjs/operators';
|
import { first, map, switchMap, tap } from 'rxjs/operators';
|
||||||
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
import { followLink, FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
|
||||||
import { LinkService } from '../../cache/builders/link.service';
|
import { LinkService } from '../../cache/builders/link.service';
|
||||||
import { FacetConfigSuccessResponse, FacetValueSuccessResponse, SearchSuccessResponse } from '../../cache/response.models';
|
import { FacetConfigSuccessResponse, FacetValueSuccessResponse, SearchSuccessResponse } from '../../cache/response.models';
|
||||||
import { PaginatedList } from '../../data/paginated-list';
|
import { PaginatedList } from '../../data/paginated-list';
|
||||||
@@ -107,10 +107,11 @@ export class SearchService implements OnDestroy {
|
|||||||
* Method to retrieve a paginated list of search results from the server
|
* Method to retrieve a paginated list of search results from the server
|
||||||
* @param {PaginatedSearchOptions} searchOptions The configuration necessary to perform this search
|
* @param {PaginatedSearchOptions} searchOptions The configuration necessary to perform this search
|
||||||
* @param responseMsToLive The amount of milliseconds for the response to live in cache
|
* @param responseMsToLive The amount of milliseconds for the response to live in cache
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||||
* @returns {Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>>} Emits a paginated list with all search results found
|
* @returns {Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>>} Emits a paginated list with all search results found
|
||||||
*/
|
*/
|
||||||
search(searchOptions?: PaginatedSearchOptions, responseMsToLive?: number): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> {
|
search<T extends DSpaceObject>(searchOptions?: PaginatedSearchOptions, responseMsToLive?: number, ...linksToFollow: Array<FollowLinkConfig<T>>): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> {
|
||||||
return this.getPaginatedResults(this.searchEntries(searchOptions));
|
return this.getPaginatedResults<T>(this.searchEntries(searchOptions), ...linksToFollow);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -151,9 +152,10 @@ export class SearchService implements OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* Method to convert the parsed responses into a paginated list of search results
|
* Method to convert the parsed responses into a paginated list of search results
|
||||||
* @param searchEntries: The request entries from the search method
|
* @param searchEntries: The request entries from the search method
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||||
* @returns {Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>>} Emits a paginated list with all search results found
|
* @returns {Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>>} Emits a paginated list with all search results found
|
||||||
*/
|
*/
|
||||||
getPaginatedResults(searchEntries: Observable<{ searchOptions: PaginatedSearchOptions, requestEntry: RequestEntry }>): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> {
|
getPaginatedResults<T extends DSpaceObject>(searchEntries: Observable<{ searchOptions: PaginatedSearchOptions, requestEntry: RequestEntry }>, ...linksToFollow: Array<FollowLinkConfig<T>>): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> {
|
||||||
const requestEntryObs: Observable<RequestEntry> = searchEntries.pipe(
|
const requestEntryObs: Observable<RequestEntry> = searchEntries.pipe(
|
||||||
map((entry) => entry.requestEntry),
|
map((entry) => entry.requestEntry),
|
||||||
);
|
);
|
||||||
@@ -174,7 +176,7 @@ export class SearchService implements OnDestroy {
|
|||||||
}),
|
}),
|
||||||
// Send a request for each item to ensure fresh cache
|
// Send a request for each item to ensure fresh cache
|
||||||
tap((reqs: RestRequest[]) => reqs.forEach((req: RestRequest) => this.requestService.configure(req))),
|
tap((reqs: RestRequest[]) => reqs.forEach((req: RestRequest) => this.requestService.configure(req))),
|
||||||
map((reqs: RestRequest[]) => reqs.map((req: RestRequest) => this.rdb.buildSingle(req.href))),
|
map((reqs: RestRequest[]) => reqs.map((req: RestRequest) => this.rdb.buildSingle(req.href, ...linksToFollow))),
|
||||||
switchMap((input: Array<Observable<RemoteData<DSpaceObject>>>) => this.rdb.aggregate(input)),
|
switchMap((input: Array<Observable<RemoteData<DSpaceObject>>>) => this.rdb.aggregate(input)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import { inheritSerialization } from 'cerialize';
|
import { inheritSerialization } from 'cerialize';
|
||||||
import { typedObject } from '../../cache/builders/build-decorators';
|
import { inheritLinkAnnotations, typedObject } from '../../cache/builders/build-decorators';
|
||||||
import { DSpaceObject } from '../../shared/dspace-object.model';
|
|
||||||
import { CLAIMED_TASK } from './claimed-task-object.resource-type';
|
import { CLAIMED_TASK } from './claimed-task-object.resource-type';
|
||||||
import { TaskObject } from './task-object.model';
|
import { TaskObject } from './task-object.model';
|
||||||
|
|
||||||
@@ -8,7 +7,8 @@ import { TaskObject } from './task-object.model';
|
|||||||
* A model class for a ClaimedTask.
|
* A model class for a ClaimedTask.
|
||||||
*/
|
*/
|
||||||
@typedObject
|
@typedObject
|
||||||
@inheritSerialization(DSpaceObject)
|
@inheritSerialization(TaskObject)
|
||||||
|
@inheritLinkAnnotations(TaskObject)
|
||||||
export class ClaimedTask extends TaskObject {
|
export class ClaimedTask extends TaskObject {
|
||||||
static type = CLAIMED_TASK;
|
static type = CLAIMED_TASK;
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { inheritSerialization } from 'cerialize';
|
import { inheritSerialization } from 'cerialize';
|
||||||
import { typedObject } from '../../cache/builders/build-decorators';
|
import { inheritLinkAnnotations, typedObject } from '../../cache/builders/build-decorators';
|
||||||
import { POOL_TASK } from './pool-task-object.resource-type';
|
import { POOL_TASK } from './pool-task-object.resource-type';
|
||||||
import { TaskObject } from './task-object.model';
|
import { TaskObject } from './task-object.model';
|
||||||
|
|
||||||
@@ -8,6 +8,7 @@ import { TaskObject } from './task-object.model';
|
|||||||
*/
|
*/
|
||||||
@typedObject
|
@typedObject
|
||||||
@inheritSerialization(TaskObject)
|
@inheritSerialization(TaskObject)
|
||||||
|
@inheritLinkAnnotations(TaskObject)
|
||||||
export class PoolTask extends TaskObject {
|
export class PoolTask extends TaskObject {
|
||||||
static type = POOL_TASK;
|
static type = POOL_TASK;
|
||||||
}
|
}
|
||||||
|
@@ -12,6 +12,7 @@ import { DSpaceObject } from '../../shared/dspace-object.model';
|
|||||||
import { HALLink } from '../../shared/hal-link.model';
|
import { HALLink } from '../../shared/hal-link.model';
|
||||||
import { WorkflowItem } from '../../submission/models/workflowitem.model';
|
import { WorkflowItem } from '../../submission/models/workflowitem.model';
|
||||||
import { TASK_OBJECT } from './task-object.resource-type';
|
import { TASK_OBJECT } from './task-object.resource-type';
|
||||||
|
import { WORKFLOWITEM } from '../../eperson/models/workflowitem.resource-type';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An abstract model class for a TaskObject.
|
* An abstract model class for a TaskObject.
|
||||||
@@ -45,7 +46,7 @@ export class TaskObject extends DSpaceObject implements CacheableObject {
|
|||||||
@deserialize
|
@deserialize
|
||||||
_links: {
|
_links: {
|
||||||
self: HALLink;
|
self: HALLink;
|
||||||
eperson: HALLink;
|
owner: HALLink;
|
||||||
group: HALLink;
|
group: HALLink;
|
||||||
workflowitem: HALLink;
|
workflowitem: HALLink;
|
||||||
};
|
};
|
||||||
@@ -54,7 +55,7 @@ export class TaskObject extends DSpaceObject implements CacheableObject {
|
|||||||
* The EPerson for this task
|
* The EPerson for this task
|
||||||
* Will be undefined unless the eperson {@link HALLink} has been resolved.
|
* Will be undefined unless the eperson {@link HALLink} has been resolved.
|
||||||
*/
|
*/
|
||||||
@link(EPERSON)
|
@link(EPERSON, false, 'owner')
|
||||||
eperson?: Observable<RemoteData<EPerson>>;
|
eperson?: Observable<RemoteData<EPerson>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -68,7 +69,7 @@ export class TaskObject extends DSpaceObject implements CacheableObject {
|
|||||||
* The WorkflowItem for this task
|
* The WorkflowItem for this task
|
||||||
* Will be undefined unless the workflowitem {@link HALLink} has been resolved.
|
* Will be undefined unless the workflowitem {@link HALLink} has been resolved.
|
||||||
*/
|
*/
|
||||||
@link(WorkflowItem.type)
|
@link(WORKFLOWITEM)
|
||||||
workflowitem?: Observable<RemoteData<WorkflowItem>> | WorkflowItem;
|
workflowitem?: Observable<RemoteData<WorkflowItem>> | WorkflowItem;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -23,7 +23,7 @@ describe('OrgUnitItemMetadataListElementComponent', () => {
|
|||||||
declarations: [OrgUnitItemMetadataListElementComponent],
|
declarations: [OrgUnitItemMetadataListElementComponent],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).overrideComponent(OrgUnitItemMetadataListElementComponent, {
|
}).overrideComponent(OrgUnitItemMetadataListElementComponent, {
|
||||||
// set: { changeDetection: ChangeDetectionStrategy.Default }
|
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@@ -29,12 +29,12 @@ describe('PersonItemMetadataListElementComponent', () => {
|
|||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(PersonItemMetadataListElementComponent);
|
fixture = TestBed.createComponent(PersonItemMetadataListElementComponent);
|
||||||
comp = fixture.componentInstance;
|
comp = fixture.componentInstance;
|
||||||
comp.metadataRepresentation = mockItemMetadataRepresentation;
|
comp.metadataRepresentation = mockItemMetadataRepresentation;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
}));
|
});
|
||||||
|
|
||||||
it('should show the person\'s name as a link', () => {
|
it('should show the person\'s name as a link', () => {
|
||||||
const linkText = fixture.debugElement.query(By.css('a')).nativeElement.textContent;
|
const linkText = fixture.debugElement.query(By.css('a')).nativeElement.textContent;
|
||||||
|
@@ -125,7 +125,7 @@ describe('DeleteComColPageComponent', () => {
|
|||||||
it('should call delete on the data service', () => {
|
it('should call delete on the data service', () => {
|
||||||
comp.onConfirm(data1);
|
comp.onConfirm(data1);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(dsoDataService.delete).toHaveBeenCalledWith(data1);
|
expect(dsoDataService.delete).toHaveBeenCalledWith(data1.id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -43,7 +43,7 @@ export class DeleteComColPageComponent<TDomain extends DSpaceObject> implements
|
|||||||
* Deletes an existing DSO and redirects to the home page afterwards, showing a notification that states whether or not the deletion was successful
|
* Deletes an existing DSO and redirects to the home page afterwards, showing a notification that states whether or not the deletion was successful
|
||||||
*/
|
*/
|
||||||
onConfirm(dso: TDomain) {
|
onConfirm(dso: TDomain) {
|
||||||
this.dsoDataService.delete(dso)
|
this.dsoDataService.delete(dso.id)
|
||||||
.pipe(first())
|
.pipe(first())
|
||||||
.subscribe((success: boolean) => {
|
.subscribe((success: boolean) => {
|
||||||
if (success) {
|
if (success) {
|
||||||
|
@@ -12,7 +12,7 @@ import { filter, map, startWith, tap } from 'rxjs/operators';
|
|||||||
import { getCollectionPageRoute } from '../../+collection-page/collection-page-routing.module';
|
import { getCollectionPageRoute } from '../../+collection-page/collection-page-routing.module';
|
||||||
import { getCommunityPageRoute } from '../../+community-page/community-page-routing.module';
|
import { getCommunityPageRoute } from '../../+community-page/community-page-routing.module';
|
||||||
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
|
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
|
||||||
import { Router, ActivatedRoute, RouterModule, UrlSegment } from '@angular/router';
|
import { Router, ActivatedRoute, RouterModule, UrlSegment, Params } from '@angular/router';
|
||||||
import { BrowseByTypeConfig } from '../../../config/browse-by-type-config.interface';
|
import { BrowseByTypeConfig } from '../../../config/browse-by-type-config.interface';
|
||||||
import { hasValue } from '../empty.util';
|
import { hasValue } from '../empty.util';
|
||||||
|
|
||||||
@@ -76,9 +76,8 @@ export class ComcolPageBrowseByComponent implements OnInit {
|
|||||||
}, ...this.allOptions ];
|
}, ...this.allOptions ];
|
||||||
}
|
}
|
||||||
|
|
||||||
this.currentOptionId$ = this.route.url.pipe(
|
this.currentOptionId$ = this.route.params.pipe(
|
||||||
filter((urlSegments: UrlSegment[]) => hasValue(urlSegments)),
|
map((params: Params) => params.id)
|
||||||
map((urlSegments: UrlSegment[]) => urlSegments[urlSegments.length - 1].path)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -49,7 +49,7 @@ export abstract class FieldParser {
|
|||||||
label: this.configData.label,
|
label: this.configData.label,
|
||||||
initialCount: this.getInitArrayIndex(),
|
initialCount: this.getInitArrayIndex(),
|
||||||
notRepeatable: !this.configData.repeatable,
|
notRepeatable: !this.configData.repeatable,
|
||||||
required: isNotEmpty(this.configData.mandatory),
|
required: JSON.parse( this.configData.mandatory),
|
||||||
groupFactory: () => {
|
groupFactory: () => {
|
||||||
let model;
|
let model;
|
||||||
if ((arrayCounter === 0)) {
|
if ((arrayCounter === 0)) {
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
<a [class.disabled]="!(object.workflowitem | async)?.hasSucceeded"
|
<a [class.disabled]="!(object.workflowitem | async)?.hasSucceeded"
|
||||||
class="btn btn-primary mt-1 mb-3"
|
class="btn btn-primary mt-1 mb-3"
|
||||||
ngbTooltip="{{'submission.workflow.tasks.claimed.edit_help' | translate}}"
|
ngbTooltip="{{'submission.workflow.tasks.claimed.edit_help' | translate}}"
|
||||||
[routerLink]="['/workflowitems/' + (object.workflowitem | async)?.payload.id + '/edit']"
|
[routerLink]="['/workflowitems/' + (object?.workflowitem | async)?.payload?.id + '/edit']"
|
||||||
role="button">
|
role="button">
|
||||||
<i class="fa fa-edit"></i> {{'submission.workflow.tasks.claimed.edit' | translate}}
|
<i class="fa fa-edit"></i> {{'submission.workflow.tasks.claimed.edit' | translate}}
|
||||||
</a>
|
</a>
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { ChangeDetectionStrategy, Injector, NO_ERRORS_SCHEMA } from '@angular/core';
|
import { ChangeDetectionStrategy, Injector, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { async, ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing';
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
|
|
||||||
@@ -141,7 +141,7 @@ describe('WorkspaceitemActionsComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
fixture.whenStable().then(() => {
|
fixture.whenStable().then(() => {
|
||||||
expect(mockDataService.delete).toHaveBeenCalledWith(mockObject);
|
expect(mockDataService.delete).toHaveBeenCalledWith(mockObject.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Component, Injector, Input, OnDestroy } from '@angular/core';
|
import { Component, Injector, Input } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
import { BehaviorSubject } from 'rxjs';
|
import { BehaviorSubject } from 'rxjs';
|
||||||
@@ -62,7 +62,7 @@ export class WorkspaceitemActionsComponent extends MyDSpaceActionsComponent<Work
|
|||||||
(result) => {
|
(result) => {
|
||||||
if (result === 'ok') {
|
if (result === 'ok') {
|
||||||
this.processingDelete$.next(true);
|
this.processingDelete$.next(true);
|
||||||
this.objectDataService.delete(this.object)
|
this.objectDataService.delete(this.object.id)
|
||||||
.subscribe((response: boolean) => {
|
.subscribe((response: boolean) => {
|
||||||
this.processingDelete$.next(false);
|
this.processingDelete$.next(false);
|
||||||
this.handleActionResponse(response);
|
this.handleActionResponse(response);
|
||||||
|
@@ -1,8 +1,10 @@
|
|||||||
<ds-item-detail-preview *ngIf="workflowitem"
|
<ng-container *ngVar="(workflowitemRD$ | async)?.payload as workflowitem">
|
||||||
[item]="(workflowitem.item | async)?.payload"
|
<ds-item-detail-preview *ngIf="workflowitem"
|
||||||
[object]="object"
|
[item]="(workflowitem.item | async)?.payload"
|
||||||
[showSubmitter]="showSubmitter"
|
[object]="object"
|
||||||
[status]="status">
|
[showSubmitter]="showSubmitter"
|
||||||
</ds-item-detail-preview>
|
[status]="status">
|
||||||
|
</ds-item-detail-preview>
|
||||||
|
|
||||||
<ds-claimed-task-actions *ngIf="workflowitem" [object]="dso"></ds-claimed-task-actions>
|
<ds-claimed-task-actions *ngIf="workflowitem" [object]="dso"></ds-claimed-task-actions>
|
||||||
|
</ng-container>
|
||||||
|
@@ -11,6 +11,9 @@ import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspa
|
|||||||
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
|
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
|
||||||
import { createSuccessfulRemoteDataObject } from '../../../testing/utils';
|
import { createSuccessfulRemoteDataObject } from '../../../testing/utils';
|
||||||
import { ClaimedTaskSearchResult } from '../../../object-collection/shared/claimed-task-search-result.model';
|
import { ClaimedTaskSearchResult } from '../../../object-collection/shared/claimed-task-search-result.model';
|
||||||
|
import { VarDirective } from '../../../utils/var.directive';
|
||||||
|
import { getMockLinkService } from '../../../mocks/mock-link-service';
|
||||||
|
import { LinkService } from '../../../../core/cache/builders/link.service';
|
||||||
|
|
||||||
let component: ClaimedTaskSearchResultDetailElementComponent;
|
let component: ClaimedTaskSearchResultDetailElementComponent;
|
||||||
let fixture: ComponentFixture<ClaimedTaskSearchResultDetailElementComponent>;
|
let fixture: ComponentFixture<ClaimedTaskSearchResultDetailElementComponent>;
|
||||||
@@ -53,12 +56,16 @@ const rdItem = createSuccessfulRemoteDataObject(item);
|
|||||||
const workflowitem = Object.assign(new WorkflowItem(), { item: observableOf(rdItem) });
|
const workflowitem = Object.assign(new WorkflowItem(), { item: observableOf(rdItem) });
|
||||||
const rdWorkflowitem = createSuccessfulRemoteDataObject(workflowitem);
|
const rdWorkflowitem = createSuccessfulRemoteDataObject(workflowitem);
|
||||||
mockResultObject.indexableObject = Object.assign(new ClaimedTask(), { workflowitem: observableOf(rdWorkflowitem) });
|
mockResultObject.indexableObject = Object.assign(new ClaimedTask(), { workflowitem: observableOf(rdWorkflowitem) });
|
||||||
|
const linkService = getMockLinkService();
|
||||||
|
|
||||||
describe('ClaimedTaskSearchResultDetailElementComponent', () => {
|
describe('ClaimedTaskSearchResultDetailElementComponent', () => {
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [NoopAnimationsModule],
|
imports: [NoopAnimationsModule],
|
||||||
declarations: [ClaimedTaskSearchResultDetailElementComponent],
|
declarations: [ClaimedTaskSearchResultDetailElementComponent, VarDirective],
|
||||||
|
providers: [
|
||||||
|
{ provide: LinkService, useValue: linkService }
|
||||||
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).overrideComponent(ClaimedTaskSearchResultDetailElementComponent, {
|
}).overrideComponent(ClaimedTaskSearchResultDetailElementComponent, {
|
||||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||||
@@ -75,8 +82,12 @@ describe('ClaimedTaskSearchResultDetailElementComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should init item properly', () => {
|
it('should init workflowitem properly', (done) => {
|
||||||
expect(component.workflowitem).toEqual(workflowitem);
|
component.workflowitemRD$.subscribe((workflowitemRD) => {
|
||||||
|
expect(linkService.resolveLink).toHaveBeenCalled();
|
||||||
|
expect(workflowitemRD.payload).toEqual(workflowitem);
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have properly status', () => {
|
it('should have properly status', () => {
|
||||||
|
@@ -1,17 +1,17 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { find } from 'rxjs/operators';
|
|
||||||
|
|
||||||
import { RemoteData } from '../../../../core/data/remote-data';
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||||
import { isNotUndefined } from '../../../empty.util';
|
|
||||||
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
|
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
|
||||||
import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model';
|
import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model';
|
||||||
import { SearchResultDetailElementComponent } from '../search-result-detail-element.component';
|
import { SearchResultDetailElementComponent } from '../search-result-detail-element.component';
|
||||||
import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
|
import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
|
||||||
import { listableObjectComponent } from '../../../object-collection/shared/listable-object/listable-object.decorator';
|
import { listableObjectComponent } from '../../../object-collection/shared/listable-object/listable-object.decorator';
|
||||||
import { ClaimedTaskSearchResult } from '../../../object-collection/shared/claimed-task-search-result.model';
|
import { ClaimedTaskSearchResult } from '../../../object-collection/shared/claimed-task-search-result.model';
|
||||||
|
import { followLink } from '../../../utils/follow-link-config.model';
|
||||||
|
import { LinkService } from '../../../../core/cache/builders/link.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders claimed task object for the search result in the detail view.
|
* This component renders claimed task object for the search result in the detail view.
|
||||||
@@ -38,25 +38,24 @@ export class ClaimedTaskSearchResultDetailElementComponent extends SearchResultD
|
|||||||
/**
|
/**
|
||||||
* The workflowitem object that belonging to the result object
|
* The workflowitem object that belonging to the result object
|
||||||
*/
|
*/
|
||||||
public workflowitem: WorkflowItem;
|
public workflowitemRD$: Observable<RemoteData<WorkflowItem>>;
|
||||||
|
|
||||||
|
constructor(protected linkService: LinkService) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize all instance variables
|
* Initialize all instance variables
|
||||||
*/
|
*/
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
this.initWorkflowItem(this.dso.workflowitem as Observable<RemoteData<WorkflowItem>>);
|
this.linkService.resolveLink(this.dso, followLink(
|
||||||
}
|
'workflowitem',
|
||||||
|
null,
|
||||||
/**
|
followLink('item', null, followLink('bundles')),
|
||||||
* Retrieve workflow item from result object
|
followLink('submitter')
|
||||||
*/
|
));
|
||||||
initWorkflowItem(wfi$: Observable<RemoteData<WorkflowItem>>) {
|
this.workflowitemRD$ = this.dso.workflowitem as Observable<RemoteData<WorkflowItem>>;
|
||||||
wfi$.pipe(
|
|
||||||
find((rd: RemoteData<WorkflowItem>) => (rd.hasSucceeded && isNotUndefined(rd.payload)))
|
|
||||||
).subscribe((rd: RemoteData<WorkflowItem>) => {
|
|
||||||
this.workflowitem = rd.payload;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -10,9 +10,9 @@
|
|||||||
<div class="row mb-1">
|
<div class="row mb-1">
|
||||||
<div class="col-xs-12 col-md-4">
|
<div class="col-xs-12 col-md-4">
|
||||||
<ds-metadata-field-wrapper>
|
<ds-metadata-field-wrapper>
|
||||||
<ds-thumbnail [thumbnail]="thumbnail$ | async"></ds-thumbnail>
|
<ds-thumbnail [thumbnail]="getThumbnail() | async"></ds-thumbnail>
|
||||||
</ds-metadata-field-wrapper>
|
</ds-metadata-field-wrapper>
|
||||||
<ng-container *ngVar="(bitstreams$ | async) as bitstreams">
|
<ng-container *ngVar="(getFiles() | async) as bitstreams">
|
||||||
<ds-metadata-field-wrapper [label]="('item.page.files' | translate)">
|
<ds-metadata-field-wrapper [label]="('item.page.files' | translate)">
|
||||||
<div *ngIf="bitstreams?.length > 0" class="file-section">
|
<div *ngIf="bitstreams?.length > 0" class="file-section">
|
||||||
<button class="btn btn-link" *ngFor="let file of bitstreams; let last=last;" (click)="downloadBitstreamFile(file?.uuid)">
|
<button class="btn btn-link" *ngFor="let file of bitstreams; let last=last;" (click)="downloadBitstreamFile(file?.uuid)">
|
||||||
|
@@ -5,8 +5,8 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
|||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
import { of as observableOf } from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
import { Observable } from 'rxjs/internal/Observable';
|
|
||||||
import { RemoteDataBuildService } from '../../../../core/cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../../../../core/cache/builders/remote-data-build.service';
|
||||||
import { ObjectCacheService } from '../../../../core/cache/object-cache.service';
|
import { ObjectCacheService } from '../../../../core/cache/object-cache.service';
|
||||||
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
|
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
|
||||||
@@ -28,7 +28,6 @@ import { HALEndpointServiceStub } from '../../../testing/hal-endpoint-service-st
|
|||||||
import { createSuccessfulRemoteDataObject$ } from '../../../testing/utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../../testing/utils';
|
||||||
import { FileSizePipe } from '../../../utils/file-size-pipe';
|
import { FileSizePipe } from '../../../utils/file-size-pipe';
|
||||||
import { FollowLinkConfig } from '../../../utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../../utils/follow-link-config.model';
|
||||||
|
|
||||||
import { TruncatePipe } from '../../../utils/truncate.pipe';
|
import { TruncatePipe } from '../../../utils/truncate.pipe';
|
||||||
import { VarDirective } from '../../../utils/var.directive';
|
import { VarDirective } from '../../../utils/var.directive';
|
||||||
import { ItemDetailPreviewFieldComponent } from './item-detail-preview-field/item-detail-preview-field.component';
|
import { ItemDetailPreviewFieldComponent } from './item-detail-preview-field/item-detail-preview-field.component';
|
||||||
@@ -127,8 +126,17 @@ describe('ItemDetailPreviewComponent', () => {
|
|||||||
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should init thumbnail and bitstreams on init', () => {
|
it('should get item thumbnail', (done) => {
|
||||||
expect(component.thumbnail$).toBeDefined();
|
component.getThumbnail().subscribe((thumbnail) => {
|
||||||
expect(component.bitstreams$).toBeDefined();
|
expect(thumbnail).toBeDefined();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get item bitstreams', (done) => {
|
||||||
|
component.getFiles().subscribe((bitstreams) => {
|
||||||
|
expect(bitstreams).toBeDefined();
|
||||||
|
done();
|
||||||
|
})
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,17 +1,13 @@
|
|||||||
import { Component, Input } from '@angular/core';
|
import { Component, Input } from '@angular/core';
|
||||||
|
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { first, map } from 'rxjs/operators';
|
import { first } from 'rxjs/operators';
|
||||||
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
|
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
|
||||||
import { RemoteData } from '../../../../core/data/remote-data';
|
|
||||||
|
|
||||||
import { Item } from '../../../../core/shared/item.model';
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
import {
|
import {
|
||||||
getAllSucceededRemoteListPayload,
|
|
||||||
getFirstSucceededRemoteDataPayload,
|
getFirstSucceededRemoteDataPayload,
|
||||||
getFirstSucceededRemoteListPayload,
|
getFirstSucceededRemoteListPayload
|
||||||
getRemoteDataPayload,
|
|
||||||
getSucceededRemoteData
|
|
||||||
} from '../../../../core/shared/operators';
|
} from '../../../../core/shared/operators';
|
||||||
import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
|
import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
|
||||||
import { fadeInOut } from '../../../animations/fade';
|
import { fadeInOut } from '../../../animations/fade';
|
||||||
@@ -71,20 +67,13 @@ export class ItemDetailPreviewComponent {
|
|||||||
*
|
*
|
||||||
* @param {FileService} fileService
|
* @param {FileService} fileService
|
||||||
* @param {HALEndpointService} halService
|
* @param {HALEndpointService} halService
|
||||||
|
* @param {BitstreamDataService} bitstreamDataService
|
||||||
*/
|
*/
|
||||||
constructor(private fileService: FileService,
|
constructor(private fileService: FileService,
|
||||||
private halService: HALEndpointService,
|
private halService: HALEndpointService,
|
||||||
private bitstreamDataService: BitstreamDataService) {
|
private bitstreamDataService: BitstreamDataService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize all instance variables
|
|
||||||
*/
|
|
||||||
ngOnInit() {
|
|
||||||
this.thumbnail$ = this.getThumbnail();
|
|
||||||
this.bitstreams$ = this.getFiles();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform bitstream download
|
* Perform bitstream download
|
||||||
*/
|
*/
|
||||||
@@ -98,14 +87,14 @@ export class ItemDetailPreviewComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO refactor this method to return RemoteData, and the template to deal with loading and errors
|
// TODO refactor this method to return RemoteData, and the template to deal with loading and errors
|
||||||
getThumbnail(): Observable<Bitstream> {
|
public getThumbnail(): Observable<Bitstream> {
|
||||||
return this.bitstreamDataService.getThumbnailFor(this.item).pipe(
|
return this.bitstreamDataService.getThumbnailFor(this.item).pipe(
|
||||||
getFirstSucceededRemoteDataPayload()
|
getFirstSucceededRemoteDataPayload()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO refactor this method to return RemoteData, and the template to deal with loading and errors
|
// TODO refactor this method to return RemoteData, and the template to deal with loading and errors
|
||||||
getFiles(): Observable<Bitstream[]> {
|
public getFiles(): Observable<Bitstream[]> {
|
||||||
return this.bitstreamDataService
|
return this.bitstreamDataService
|
||||||
.findAllByItemAndBundleName(this.item, 'ORIGINAL', { elementsPerPage: Number.MAX_SAFE_INTEGER })
|
.findAllByItemAndBundleName(this.item, 'ORIGINAL', { elementsPerPage: Number.MAX_SAFE_INTEGER })
|
||||||
.pipe(
|
.pipe(
|
||||||
|
@@ -1,7 +1,9 @@
|
|||||||
<ds-item-detail-preview *ngIf="workflowitem"
|
<ng-container *ngVar="(workflowitemRD$ | async)?.payload as workflowitem">
|
||||||
[item]="(workflowitem.item | async)?.payload"
|
<ds-item-detail-preview *ngIf="workflowitem"
|
||||||
[object]="object"
|
[item]="(workflowitem?.item | async)?.payload"
|
||||||
[showSubmitter]="showSubmitter"
|
[object]="object"
|
||||||
[status]="status"></ds-item-detail-preview>
|
[showSubmitter]="showSubmitter"
|
||||||
|
[status]="status"></ds-item-detail-preview>
|
||||||
|
|
||||||
<ds-pool-task-actions *ngIf="workflowitem" [object]="dso"></ds-pool-task-actions>
|
<ds-pool-task-actions *ngIf="workflowitem" [object]="dso"></ds-pool-task-actions>
|
||||||
|
</ng-container>
|
||||||
|
@@ -11,6 +11,9 @@ import { WorkflowItem } from '../../../../core/submission/models/workflowitem.mo
|
|||||||
import { createSuccessfulRemoteDataObject } from '../../../testing/utils';
|
import { createSuccessfulRemoteDataObject } from '../../../testing/utils';
|
||||||
import { PoolSearchResultDetailElementComponent } from './pool-search-result-detail-element.component';
|
import { PoolSearchResultDetailElementComponent } from './pool-search-result-detail-element.component';
|
||||||
import { PoolTaskSearchResult } from '../../../object-collection/shared/pool-task-search-result.model';
|
import { PoolTaskSearchResult } from '../../../object-collection/shared/pool-task-search-result.model';
|
||||||
|
import { VarDirective } from '../../../utils/var.directive';
|
||||||
|
import { LinkService } from '../../../../core/cache/builders/link.service';
|
||||||
|
import { getMockLinkService } from '../../../mocks/mock-link-service';
|
||||||
|
|
||||||
let component: PoolSearchResultDetailElementComponent;
|
let component: PoolSearchResultDetailElementComponent;
|
||||||
let fixture: ComponentFixture<PoolSearchResultDetailElementComponent>;
|
let fixture: ComponentFixture<PoolSearchResultDetailElementComponent>;
|
||||||
@@ -53,15 +56,17 @@ const rdItem = createSuccessfulRemoteDataObject(item);
|
|||||||
const workflowitem = Object.assign(new WorkflowItem(), { item: observableOf(rdItem) });
|
const workflowitem = Object.assign(new WorkflowItem(), { item: observableOf(rdItem) });
|
||||||
const rdWorkflowitem = createSuccessfulRemoteDataObject(workflowitem);
|
const rdWorkflowitem = createSuccessfulRemoteDataObject(workflowitem);
|
||||||
mockResultObject.indexableObject = Object.assign(new PoolTask(), { workflowitem: observableOf(rdWorkflowitem) });
|
mockResultObject.indexableObject = Object.assign(new PoolTask(), { workflowitem: observableOf(rdWorkflowitem) });
|
||||||
|
const linkService = getMockLinkService();
|
||||||
|
|
||||||
describe('PoolSearchResultDetailElementComponent', () => {
|
describe('PoolSearchResultDetailElementComponent', () => {
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [NoopAnimationsModule],
|
imports: [NoopAnimationsModule],
|
||||||
declarations: [PoolSearchResultDetailElementComponent],
|
declarations: [PoolSearchResultDetailElementComponent, VarDirective],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: 'objectElementProvider', useValue: (mockResultObject) },
|
{ provide: 'objectElementProvider', useValue: (mockResultObject) },
|
||||||
{ provide: 'indexElementProvider', useValue: (compIndex) }
|
{ provide: 'indexElementProvider', useValue: (compIndex) },
|
||||||
|
{ provide: LinkService, useValue: linkService }
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).overrideComponent(PoolSearchResultDetailElementComponent, {
|
}).overrideComponent(PoolSearchResultDetailElementComponent, {
|
||||||
@@ -79,8 +84,12 @@ describe('PoolSearchResultDetailElementComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should init item properly', () => {
|
it('should init workflowitem properly', (done) => {
|
||||||
expect(component.workflowitem).toEqual(workflowitem);
|
component.workflowitemRD$.subscribe((workflowitemRD) => {
|
||||||
|
expect(linkService.resolveLink).toHaveBeenCalled();
|
||||||
|
expect(workflowitemRD.payload).toEqual(workflowitem);
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have properly status', () => {
|
it('should have properly status', () => {
|
||||||
|
@@ -1,9 +1,7 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { find } from 'rxjs/operators';
|
|
||||||
import { RemoteData } from '../../../../core/data/remote-data';
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
import { isNotUndefined } from '../../../empty.util';
|
|
||||||
import { PoolTask } from '../../../../core/tasks/models/pool-task-object.model';
|
import { PoolTask } from '../../../../core/tasks/models/pool-task-object.model';
|
||||||
import { SearchResultDetailElementComponent } from '../search-result-detail-element.component';
|
import { SearchResultDetailElementComponent } from '../search-result-detail-element.component';
|
||||||
import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
|
import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
|
||||||
@@ -11,6 +9,8 @@ import { WorkflowItem } from '../../../../core/submission/models/workflowitem.mo
|
|||||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||||
import { listableObjectComponent } from '../../../object-collection/shared/listable-object/listable-object.decorator';
|
import { listableObjectComponent } from '../../../object-collection/shared/listable-object/listable-object.decorator';
|
||||||
import { PoolTaskSearchResult } from '../../../object-collection/shared/pool-task-search-result.model';
|
import { PoolTaskSearchResult } from '../../../object-collection/shared/pool-task-search-result.model';
|
||||||
|
import { followLink } from '../../../utils/follow-link-config.model';
|
||||||
|
import { LinkService } from '../../../../core/cache/builders/link.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders pool task object for the search result in the detail view.
|
* This component renders pool task object for the search result in the detail view.
|
||||||
@@ -37,25 +37,24 @@ export class PoolSearchResultDetailElementComponent extends SearchResultDetailEl
|
|||||||
/**
|
/**
|
||||||
* The workflowitem object that belonging to the result object
|
* The workflowitem object that belonging to the result object
|
||||||
*/
|
*/
|
||||||
public workflowitem: WorkflowItem;
|
public workflowitemRD$: Observable<RemoteData<WorkflowItem>>;
|
||||||
|
|
||||||
|
constructor(protected linkService: LinkService) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize all instance variables
|
* Initialize all instance variables
|
||||||
*/
|
*/
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
this.initWorkflowItem(this.dso.workflowitem as Observable<RemoteData<WorkflowItem>>);
|
this.linkService.resolveLink(this.dso, followLink(
|
||||||
}
|
'workflowitem',
|
||||||
|
null,
|
||||||
/**
|
followLink('item', null, followLink('bundles')),
|
||||||
* Retrieve workflowitem from result object
|
followLink('submitter')
|
||||||
*/
|
));
|
||||||
initWorkflowItem(wfi$: Observable<RemoteData<WorkflowItem>>) {
|
this.workflowitemRD$ = this.dso.workflowitem as Observable<RemoteData<WorkflowItem>>;
|
||||||
wfi$.pipe(
|
|
||||||
find((rd: RemoteData<WorkflowItem>) => (rd.hasSucceeded && isNotUndefined(rd.payload)))
|
|
||||||
).subscribe((rd: RemoteData<WorkflowItem>) => {
|
|
||||||
this.workflowitem = rd.payload;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,10 @@
|
|||||||
<ds-item-list-preview *ngIf="workflowitem"
|
<ng-container *ngVar="(workflowitemRD$ | async)?.payload as workflowitem">
|
||||||
[item]="(workflowitem.item | async)?.payload"
|
<ds-item-list-preview *ngIf="workflowitem"
|
||||||
[object]="object"
|
[item]="(workflowitem?.item | async)?.payload"
|
||||||
[showSubmitter]="showSubmitter"
|
[object]="object"
|
||||||
[status]="status"></ds-item-list-preview>
|
[showSubmitter]="showSubmitter"
|
||||||
|
[status]="status"></ds-item-list-preview>
|
||||||
|
|
||||||
|
<ds-claimed-task-actions *ngIf="workflowitem" [object]="dso"></ds-claimed-task-actions>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<ds-claimed-task-actions *ngIf="workflowitem" [object]="dso"></ds-claimed-task-actions>
|
|
||||||
|
@@ -12,6 +12,9 @@ import { WorkflowItem } from '../../../../core/submission/models/workflowitem.mo
|
|||||||
import { createSuccessfulRemoteDataObject } from '../../../testing/utils';
|
import { createSuccessfulRemoteDataObject } from '../../../testing/utils';
|
||||||
import { ClaimedTaskSearchResult } from '../../../object-collection/shared/claimed-task-search-result.model';
|
import { ClaimedTaskSearchResult } from '../../../object-collection/shared/claimed-task-search-result.model';
|
||||||
import { TruncatableService } from '../../../truncatable/truncatable.service';
|
import { TruncatableService } from '../../../truncatable/truncatable.service';
|
||||||
|
import { VarDirective } from '../../../utils/var.directive';
|
||||||
|
import { LinkService } from '../../../../core/cache/builders/link.service';
|
||||||
|
import { getMockLinkService } from '../../../mocks/mock-link-service';
|
||||||
|
|
||||||
let component: ClaimedSearchResultListElementComponent;
|
let component: ClaimedSearchResultListElementComponent;
|
||||||
let fixture: ComponentFixture<ClaimedSearchResultListElementComponent>;
|
let fixture: ComponentFixture<ClaimedSearchResultListElementComponent>;
|
||||||
@@ -54,14 +57,16 @@ const rdItem = createSuccessfulRemoteDataObject(item);
|
|||||||
const workflowitem = Object.assign(new WorkflowItem(), { item: observableOf(rdItem) });
|
const workflowitem = Object.assign(new WorkflowItem(), { item: observableOf(rdItem) });
|
||||||
const rdWorkflowitem = createSuccessfulRemoteDataObject(workflowitem);
|
const rdWorkflowitem = createSuccessfulRemoteDataObject(workflowitem);
|
||||||
mockResultObject.indexableObject = Object.assign(new ClaimedTask(), { workflowitem: observableOf(rdWorkflowitem) });
|
mockResultObject.indexableObject = Object.assign(new ClaimedTask(), { workflowitem: observableOf(rdWorkflowitem) });
|
||||||
|
const linkService = getMockLinkService();
|
||||||
|
|
||||||
describe('ClaimedSearchResultListElementComponent', () => {
|
describe('ClaimedSearchResultListElementComponent', () => {
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [NoopAnimationsModule],
|
imports: [NoopAnimationsModule],
|
||||||
declarations: [ClaimedSearchResultListElementComponent],
|
declarations: [ClaimedSearchResultListElementComponent, VarDirective],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: TruncatableService, useValue: {} },
|
{ provide: TruncatableService, useValue: {} },
|
||||||
|
{ provide: LinkService, useValue: linkService }
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).overrideComponent(ClaimedSearchResultListElementComponent, {
|
}).overrideComponent(ClaimedSearchResultListElementComponent, {
|
||||||
@@ -79,8 +84,12 @@ describe('ClaimedSearchResultListElementComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should init item properly', () => {
|
it('should init workflowitem properly', (done) => {
|
||||||
expect(component.workflowitem).toEqual(workflowitem);
|
component.workflowitemRD$.subscribe((workflowitemRD) => {
|
||||||
|
expect(linkService.resolveLink).toHaveBeenCalled();
|
||||||
|
expect(workflowitemRD.payload).toEqual(workflowitem);
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have properly status', () => {
|
it('should have properly status', () => {
|
||||||
|
@@ -2,17 +2,18 @@ import { Component } from '@angular/core';
|
|||||||
import { Location, LocationStrategy, PathLocationStrategy } from '@angular/common';
|
import { Location, LocationStrategy, PathLocationStrategy } from '@angular/common';
|
||||||
|
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { find } from 'rxjs/operators';
|
|
||||||
|
|
||||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||||
import { RemoteData } from '../../../../core/data/remote-data';
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
import { isNotUndefined } from '../../../empty.util';
|
|
||||||
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
|
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
|
||||||
import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model';
|
import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model';
|
||||||
import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
|
import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
|
||||||
import { listableObjectComponent } from '../../../object-collection/shared/listable-object/listable-object.decorator';
|
import { listableObjectComponent } from '../../../object-collection/shared/listable-object/listable-object.decorator';
|
||||||
import { ClaimedTaskSearchResult } from '../../../object-collection/shared/claimed-task-search-result.model';
|
import { ClaimedTaskSearchResult } from '../../../object-collection/shared/claimed-task-search-result.model';
|
||||||
import { SearchResultListElementComponent } from '../../search-result-list-element/search-result-list-element.component';
|
import { SearchResultListElementComponent } from '../../search-result-list-element/search-result-list-element.component';
|
||||||
|
import { followLink } from '../../../utils/follow-link-config.model';
|
||||||
|
import { LinkService } from '../../../../core/cache/builders/link.service';
|
||||||
|
import { TruncatableService } from '../../../truncatable/truncatable.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders claimed task object for the search result in the list view.
|
* This component renders claimed task object for the search result in the list view.
|
||||||
@@ -40,24 +41,26 @@ export class ClaimedSearchResultListElementComponent extends SearchResultListEle
|
|||||||
/**
|
/**
|
||||||
* The workflowitem object that belonging to the result object
|
* The workflowitem object that belonging to the result object
|
||||||
*/
|
*/
|
||||||
public workflowitem: WorkflowItem;
|
public workflowitemRD$: Observable<RemoteData<WorkflowItem>>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected linkService: LinkService,
|
||||||
|
protected truncatableService: TruncatableService
|
||||||
|
) {
|
||||||
|
super(truncatableService);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize all instance variables
|
* Initialize all instance variables
|
||||||
*/
|
*/
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
this.initWorkflowItem(this.dso.workflowitem as Observable<RemoteData<WorkflowItem>>);
|
this.linkService.resolveLink(this.dso, followLink(
|
||||||
}
|
'workflowitem',
|
||||||
|
null,
|
||||||
/**
|
followLink('item'),
|
||||||
* Retrieve workflowitem from result object
|
followLink('submitter')
|
||||||
*/
|
));
|
||||||
initWorkflowItem(wfi$: Observable<RemoteData<WorkflowItem>>) {
|
this.workflowitemRD$ = this.dso.workflowitem as Observable<RemoteData<WorkflowItem>>;
|
||||||
wfi$.pipe(
|
|
||||||
find((rd: RemoteData<WorkflowItem>) => (rd.hasSucceeded && isNotUndefined(rd.payload)))
|
|
||||||
).subscribe((rd: RemoteData<WorkflowItem>) => {
|
|
||||||
this.workflowitem = rd.payload;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,9 @@
|
|||||||
<ds-item-list-preview *ngIf="workflowitem"
|
<ng-container *ngVar="(workflowitemRD$ | async)?.payload as workflowitem">
|
||||||
[item]="(workflowitem.item | async)?.payload"
|
<ds-item-list-preview *ngIf="workflowitem"
|
||||||
[object]="object"
|
[item]="(workflowitem?.item | async)?.payload"
|
||||||
[showSubmitter]="showSubmitter"
|
[object]="object"
|
||||||
[status]="status"></ds-item-list-preview>
|
[showSubmitter]="showSubmitter"
|
||||||
|
[status]="status"></ds-item-list-preview>
|
||||||
|
|
||||||
<ds-pool-task-actions [object]="dso"></ds-pool-task-actions>
|
<ds-pool-task-actions *ngIf="workflowitem" [object]="dso"></ds-pool-task-actions>
|
||||||
|
</ng-container>
|
||||||
|
@@ -12,6 +12,9 @@ import { WorkflowItem } from '../../../../core/submission/models/workflowitem.mo
|
|||||||
import { createSuccessfulRemoteDataObject } from '../../../testing/utils';
|
import { createSuccessfulRemoteDataObject } from '../../../testing/utils';
|
||||||
import { PoolTaskSearchResult } from '../../../object-collection/shared/pool-task-search-result.model';
|
import { PoolTaskSearchResult } from '../../../object-collection/shared/pool-task-search-result.model';
|
||||||
import { TruncatableService } from '../../../truncatable/truncatable.service';
|
import { TruncatableService } from '../../../truncatable/truncatable.service';
|
||||||
|
import { VarDirective } from '../../../utils/var.directive';
|
||||||
|
import { LinkService } from '../../../../core/cache/builders/link.service';
|
||||||
|
import { getMockLinkService } from '../../../mocks/mock-link-service';
|
||||||
|
|
||||||
let component: PoolSearchResultListElementComponent;
|
let component: PoolSearchResultListElementComponent;
|
||||||
let fixture: ComponentFixture<PoolSearchResultListElementComponent>;
|
let fixture: ComponentFixture<PoolSearchResultListElementComponent>;
|
||||||
@@ -54,14 +57,16 @@ const rdItem = createSuccessfulRemoteDataObject(item);
|
|||||||
const workflowitem = Object.assign(new WorkflowItem(), { item: observableOf(rdItem) });
|
const workflowitem = Object.assign(new WorkflowItem(), { item: observableOf(rdItem) });
|
||||||
const rdWorkflowitem = createSuccessfulRemoteDataObject(workflowitem);
|
const rdWorkflowitem = createSuccessfulRemoteDataObject(workflowitem);
|
||||||
mockResultObject.indexableObject = Object.assign(new PoolTask(), { workflowitem: observableOf(rdWorkflowitem) });
|
mockResultObject.indexableObject = Object.assign(new PoolTask(), { workflowitem: observableOf(rdWorkflowitem) });
|
||||||
|
const linkService = getMockLinkService();
|
||||||
|
|
||||||
describe('PoolSearchResultListElementComponent', () => {
|
describe('PoolSearchResultListElementComponent', () => {
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [NoopAnimationsModule],
|
imports: [NoopAnimationsModule],
|
||||||
declarations: [PoolSearchResultListElementComponent],
|
declarations: [PoolSearchResultListElementComponent, VarDirective],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: TruncatableService, useValue: {} },
|
{ provide: TruncatableService, useValue: {} },
|
||||||
|
{ provide: LinkService, useValue: linkService }
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).overrideComponent(PoolSearchResultListElementComponent, {
|
}).overrideComponent(PoolSearchResultListElementComponent, {
|
||||||
@@ -79,8 +84,12 @@ describe('PoolSearchResultListElementComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should init item properly', () => {
|
it('should init workflowitem properly', (done) => {
|
||||||
expect(component.workflowitem).toEqual(workflowitem);
|
component.workflowitemRD$.subscribe((workflowitemRD) => {
|
||||||
|
expect(linkService.resolveLink).toHaveBeenCalled();
|
||||||
|
expect(workflowitemRD.payload).toEqual(workflowitem);
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have properly status', () => {
|
it('should have properly status', () => {
|
||||||
|
@@ -1,11 +1,9 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { find } from 'rxjs/operators';
|
|
||||||
|
|
||||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||||
import { RemoteData } from '../../../../core/data/remote-data';
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
import { isNotUndefined } from '../../../empty.util';
|
|
||||||
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
|
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
|
||||||
import { PoolTask } from '../../../../core/tasks/models/pool-task-object.model';
|
import { PoolTask } from '../../../../core/tasks/models/pool-task-object.model';
|
||||||
import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
|
import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
|
||||||
@@ -13,6 +11,8 @@ import { listableObjectComponent } from '../../../object-collection/shared/lista
|
|||||||
import { PoolTaskSearchResult } from '../../../object-collection/shared/pool-task-search-result.model';
|
import { PoolTaskSearchResult } from '../../../object-collection/shared/pool-task-search-result.model';
|
||||||
import { SearchResultListElementComponent } from '../../search-result-list-element/search-result-list-element.component';
|
import { SearchResultListElementComponent } from '../../search-result-list-element/search-result-list-element.component';
|
||||||
import { TruncatableService } from '../../../truncatable/truncatable.service';
|
import { TruncatableService } from '../../../truncatable/truncatable.service';
|
||||||
|
import { followLink } from '../../../utils/follow-link-config.model';
|
||||||
|
import { LinkService } from '../../../../core/cache/builders/link.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders pool task object for the search result in the list view.
|
* This component renders pool task object for the search result in the list view.
|
||||||
@@ -39,14 +39,17 @@ export class PoolSearchResultListElementComponent extends SearchResultListElemen
|
|||||||
/**
|
/**
|
||||||
* The workflowitem object that belonging to the result object
|
* The workflowitem object that belonging to the result object
|
||||||
*/
|
*/
|
||||||
public workflowitem: WorkflowItem;
|
public workflowitemRD$: Observable<RemoteData<WorkflowItem>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The index of this list element
|
* The index of this list element
|
||||||
*/
|
*/
|
||||||
public index: number;
|
public index: number;
|
||||||
|
|
||||||
constructor(protected truncatableService: TruncatableService) {
|
constructor(
|
||||||
|
protected linkService: LinkService,
|
||||||
|
protected truncatableService: TruncatableService
|
||||||
|
) {
|
||||||
super(truncatableService);
|
super(truncatableService);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,17 +58,12 @@ export class PoolSearchResultListElementComponent extends SearchResultListElemen
|
|||||||
*/
|
*/
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
this.initWorkflowItem(this.dso.workflowitem as Observable<RemoteData<WorkflowItem>>);
|
this.linkService.resolveLink(this.dso, followLink(
|
||||||
}
|
'workflowitem',
|
||||||
|
null,
|
||||||
/**
|
followLink('item'),
|
||||||
* Retrieve workflowitem from result object
|
followLink('submitter')
|
||||||
*/
|
));
|
||||||
initWorkflowItem(wfi$: Observable<RemoteData<WorkflowItem>>) {
|
this.workflowitemRD$ = this.dso.workflowitem as Observable<RemoteData<WorkflowItem>>;
|
||||||
wfi$.pipe(
|
|
||||||
find((rd: RemoteData<WorkflowItem>) => (rd.hasSucceeded && isNotUndefined(rd.payload)))
|
|
||||||
).subscribe((rd: RemoteData<WorkflowItem>) => {
|
|
||||||
this.workflowitem = rd.payload;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user