mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge branch 'main' into iiif-mirador
This commit is contained in:
@@ -179,17 +179,18 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
link: '/processes/new'
|
link: '/processes/new'
|
||||||
} as LinkMenuItemModel,
|
} as LinkMenuItemModel,
|
||||||
},
|
},
|
||||||
{
|
// TODO: enable this menu item once the feature has been implemented
|
||||||
id: 'new_item_version',
|
// {
|
||||||
parentID: 'new',
|
// id: 'new_item_version',
|
||||||
active: false,
|
// parentID: 'new',
|
||||||
visible: true,
|
// active: false,
|
||||||
model: {
|
// visible: true,
|
||||||
type: MenuItemType.LINK,
|
// model: {
|
||||||
text: 'menu.section.new_item_version',
|
// type: MenuItemType.LINK,
|
||||||
link: ''
|
// text: 'menu.section.new_item_version',
|
||||||
} as LinkMenuItemModel,
|
// link: ''
|
||||||
},
|
// } as LinkMenuItemModel,
|
||||||
|
// },
|
||||||
|
|
||||||
/* Edit */
|
/* Edit */
|
||||||
{
|
{
|
||||||
@@ -243,47 +244,35 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
} as OnClickMenuItemModel,
|
} as OnClickMenuItemModel,
|
||||||
},
|
},
|
||||||
|
|
||||||
/* Curation tasks */
|
|
||||||
{
|
|
||||||
id: 'curation_tasks',
|
|
||||||
active: false,
|
|
||||||
visible: isCollectionAdmin,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.LINK,
|
|
||||||
text: 'menu.section.curation_task',
|
|
||||||
link: ''
|
|
||||||
} as LinkMenuItemModel,
|
|
||||||
icon: 'filter',
|
|
||||||
index: 7
|
|
||||||
},
|
|
||||||
|
|
||||||
/* Statistics */
|
/* Statistics */
|
||||||
{
|
// TODO: enable this menu item once the feature has been implemented
|
||||||
id: 'statistics_task',
|
// {
|
||||||
active: false,
|
// id: 'statistics_task',
|
||||||
visible: true,
|
// active: false,
|
||||||
model: {
|
// visible: true,
|
||||||
type: MenuItemType.LINK,
|
// model: {
|
||||||
text: 'menu.section.statistics_task',
|
// type: MenuItemType.LINK,
|
||||||
link: ''
|
// text: 'menu.section.statistics_task',
|
||||||
} as LinkMenuItemModel,
|
// link: ''
|
||||||
icon: 'chart-bar',
|
// } as LinkMenuItemModel,
|
||||||
index: 8
|
// icon: 'chart-bar',
|
||||||
},
|
// index: 8
|
||||||
|
// },
|
||||||
|
|
||||||
/* Control Panel */
|
/* Control Panel */
|
||||||
{
|
// TODO: enable this menu item once the feature has been implemented
|
||||||
id: 'control_panel',
|
// {
|
||||||
active: false,
|
// id: 'control_panel',
|
||||||
visible: isSiteAdmin,
|
// active: false,
|
||||||
model: {
|
// visible: isSiteAdmin,
|
||||||
type: MenuItemType.LINK,
|
// model: {
|
||||||
text: 'menu.section.control_panel',
|
// type: MenuItemType.LINK,
|
||||||
link: ''
|
// text: 'menu.section.control_panel',
|
||||||
} as LinkMenuItemModel,
|
// link: ''
|
||||||
icon: 'cogs',
|
// } as LinkMenuItemModel,
|
||||||
index: 9
|
// icon: 'cogs',
|
||||||
},
|
// index: 9
|
||||||
|
// },
|
||||||
|
|
||||||
/* Processes */
|
/* Processes */
|
||||||
{
|
{
|
||||||
@@ -324,42 +313,45 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
index: 3,
|
index: 3,
|
||||||
shouldPersistOnRouteChange: true
|
shouldPersistOnRouteChange: true
|
||||||
},
|
},
|
||||||
{
|
// TODO: enable this menu item once the feature has been implemented
|
||||||
id: 'export_community',
|
// {
|
||||||
parentID: 'export',
|
// id: 'export_community',
|
||||||
active: false,
|
// parentID: 'export',
|
||||||
visible: true,
|
// active: false,
|
||||||
model: {
|
// visible: true,
|
||||||
type: MenuItemType.LINK,
|
// model: {
|
||||||
text: 'menu.section.export_community',
|
// type: MenuItemType.LINK,
|
||||||
link: ''
|
// text: 'menu.section.export_community',
|
||||||
} as LinkMenuItemModel,
|
// link: ''
|
||||||
shouldPersistOnRouteChange: true
|
// } as LinkMenuItemModel,
|
||||||
},
|
// shouldPersistOnRouteChange: true
|
||||||
{
|
// },
|
||||||
id: 'export_collection',
|
// TODO: enable this menu item once the feature has been implemented
|
||||||
parentID: 'export',
|
// {
|
||||||
active: false,
|
// id: 'export_collection',
|
||||||
visible: true,
|
// parentID: 'export',
|
||||||
model: {
|
// active: false,
|
||||||
type: MenuItemType.LINK,
|
// visible: true,
|
||||||
text: 'menu.section.export_collection',
|
// model: {
|
||||||
link: ''
|
// type: MenuItemType.LINK,
|
||||||
} as LinkMenuItemModel,
|
// text: 'menu.section.export_collection',
|
||||||
shouldPersistOnRouteChange: true
|
// link: ''
|
||||||
},
|
// } as LinkMenuItemModel,
|
||||||
{
|
// shouldPersistOnRouteChange: true
|
||||||
id: 'export_item',
|
// },
|
||||||
parentID: 'export',
|
// TODO: enable this menu item once the feature has been implemented
|
||||||
active: false,
|
// {
|
||||||
visible: true,
|
// id: 'export_item',
|
||||||
model: {
|
// parentID: 'export',
|
||||||
type: MenuItemType.LINK,
|
// active: false,
|
||||||
text: 'menu.section.export_item',
|
// visible: true,
|
||||||
link: ''
|
// model: {
|
||||||
} as LinkMenuItemModel,
|
// type: MenuItemType.LINK,
|
||||||
shouldPersistOnRouteChange: true
|
// text: 'menu.section.export_item',
|
||||||
},
|
// link: ''
|
||||||
|
// } as LinkMenuItemModel,
|
||||||
|
// shouldPersistOnRouteChange: true
|
||||||
|
// },
|
||||||
];
|
];
|
||||||
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, menuSection));
|
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, menuSection));
|
||||||
|
|
||||||
@@ -406,17 +398,18 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
icon: 'file-import',
|
icon: 'file-import',
|
||||||
index: 2
|
index: 2
|
||||||
},
|
},
|
||||||
{
|
// TODO: enable this menu item once the feature has been implemented
|
||||||
id: 'import_batch',
|
// {
|
||||||
parentID: 'import',
|
// id: 'import_batch',
|
||||||
active: false,
|
// parentID: 'import',
|
||||||
visible: true,
|
// active: false,
|
||||||
model: {
|
// visible: true,
|
||||||
type: MenuItemType.LINK,
|
// model: {
|
||||||
text: 'menu.section.import_batch',
|
// type: MenuItemType.LINK,
|
||||||
link: ''
|
// text: 'menu.section.import_batch',
|
||||||
} as LinkMenuItemModel,
|
// link: ''
|
||||||
}
|
// } as LinkMenuItemModel,
|
||||||
|
// }
|
||||||
];
|
];
|
||||||
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, {
|
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, {
|
||||||
shouldPersistOnRouteChange: true
|
shouldPersistOnRouteChange: true
|
||||||
@@ -563,17 +556,18 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
link: '/access-control/groups'
|
link: '/access-control/groups'
|
||||||
} as LinkMenuItemModel,
|
} as LinkMenuItemModel,
|
||||||
},
|
},
|
||||||
{
|
// TODO: enable this menu item once the feature has been implemented
|
||||||
id: 'access_control_authorizations',
|
// {
|
||||||
parentID: 'access_control',
|
// id: 'access_control_authorizations',
|
||||||
active: false,
|
// parentID: 'access_control',
|
||||||
visible: authorized,
|
// active: false,
|
||||||
model: {
|
// visible: authorized,
|
||||||
type: MenuItemType.LINK,
|
// model: {
|
||||||
text: 'menu.section.access_control_authorizations',
|
// type: MenuItemType.LINK,
|
||||||
link: ''
|
// text: 'menu.section.access_control_authorizations',
|
||||||
} as LinkMenuItemModel,
|
// link: ''
|
||||||
},
|
// } as LinkMenuItemModel,
|
||||||
|
// },
|
||||||
{
|
{
|
||||||
id: 'access_control',
|
id: 'access_control',
|
||||||
active: false,
|
active: false,
|
||||||
|
@@ -1,4 +1,6 @@
|
|||||||
import { hasNoValue } from '../../shared/empty.util';
|
import { hasNoValue } from '../../shared/empty.util';
|
||||||
|
import { InjectionToken } from '@angular/core';
|
||||||
|
import { GenericConstructor } from '../../core/shared/generic-constructor';
|
||||||
|
|
||||||
export enum BrowseByType {
|
export enum BrowseByType {
|
||||||
Title = 'title',
|
Title = 'title',
|
||||||
@@ -8,6 +10,11 @@ export enum BrowseByType {
|
|||||||
|
|
||||||
export const DEFAULT_BROWSE_BY_TYPE = BrowseByType.Metadata;
|
export const DEFAULT_BROWSE_BY_TYPE = BrowseByType.Metadata;
|
||||||
|
|
||||||
|
export const BROWSE_BY_COMPONENT_FACTORY = new InjectionToken<(browseByType) => GenericConstructor<any>>('getComponentByBrowseByType', {
|
||||||
|
providedIn: 'root',
|
||||||
|
factory: () => getComponentByBrowseByType
|
||||||
|
});
|
||||||
|
|
||||||
const map = new Map();
|
const map = new Map();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -2,12 +2,11 @@ import { BrowseBySwitcherComponent } from './browse-by-switcher.component';
|
|||||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import * as decorator from './browse-by-decorator';
|
|
||||||
import { BehaviorSubject } from 'rxjs';
|
import { BehaviorSubject } from 'rxjs';
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
import createSpy = jasmine.createSpy;
|
import { BROWSE_BY_COMPONENT_FACTORY } from './browse-by-decorator';
|
||||||
|
|
||||||
xdescribe('BrowseBySwitcherComponent', () => {
|
describe('BrowseBySwitcherComponent', () => {
|
||||||
let comp: BrowseBySwitcherComponent;
|
let comp: BrowseBySwitcherComponent;
|
||||||
let fixture: ComponentFixture<BrowseBySwitcherComponent>;
|
let fixture: ComponentFixture<BrowseBySwitcherComponent>;
|
||||||
|
|
||||||
@@ -23,7 +22,8 @@ xdescribe('BrowseBySwitcherComponent', () => {
|
|||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [BrowseBySwitcherComponent],
|
declarations: [BrowseBySwitcherComponent],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: ActivatedRoute, useValue: activatedRouteStub }
|
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||||
|
{ provide: BROWSE_BY_COMPONENT_FACTORY, useValue: jasmine.createSpy('getComponentByBrowseByType').and.returnValue(null) }
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
@@ -32,7 +32,6 @@ xdescribe('BrowseBySwitcherComponent', () => {
|
|||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
fixture = TestBed.createComponent(BrowseBySwitcherComponent);
|
fixture = TestBed.createComponent(BrowseBySwitcherComponent);
|
||||||
comp = fixture.componentInstance;
|
comp = fixture.componentInstance;
|
||||||
spyOnProperty(decorator, 'getComponentByBrowseByType').and.returnValue(createSpy('getComponentByItemType'));
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
types.forEach((type) => {
|
types.forEach((type) => {
|
||||||
@@ -43,7 +42,7 @@ xdescribe('BrowseBySwitcherComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it(`should call getComponentByBrowseByType with type "${type.type}"`, () => {
|
it(`should call getComponentByBrowseByType with type "${type.type}"`, () => {
|
||||||
expect(decorator.getComponentByBrowseByType).toHaveBeenCalledWith(type.type);
|
expect((comp as any).getComponentByBrowseByType).toHaveBeenCalledWith(type.type);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,10 +1,11 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, Inject, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { BrowseByTypeConfig } from '../../../config/browse-by-type-config.interface';
|
import { BrowseByTypeConfig } from '../../../config/browse-by-type-config.interface';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
import { getComponentByBrowseByType } from './browse-by-decorator';
|
import { BROWSE_BY_COMPONENT_FACTORY } from './browse-by-decorator';
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
|
import { GenericConstructor } from '../../core/shared/generic-constructor';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-browse-by-switcher',
|
selector: 'ds-browse-by-switcher',
|
||||||
@@ -20,7 +21,8 @@ export class BrowseBySwitcherComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
browseByComponent: Observable<any>;
|
browseByComponent: Observable<any>;
|
||||||
|
|
||||||
public constructor(protected route: ActivatedRoute) {
|
public constructor(protected route: ActivatedRoute,
|
||||||
|
@Inject(BROWSE_BY_COMPONENT_FACTORY) private getComponentByBrowseByType: (browseByType) => GenericConstructor<any>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -32,7 +34,7 @@ export class BrowseBySwitcherComponent implements OnInit {
|
|||||||
const id = params.id;
|
const id = params.id;
|
||||||
return environment.browseBy.types.find((config: BrowseByTypeConfig) => config.id === id);
|
return environment.browseBy.types.find((config: BrowseByTypeConfig) => config.id === id);
|
||||||
}),
|
}),
|
||||||
map((config: BrowseByTypeConfig) => getComponentByBrowseByType(config.type))
|
map((config: BrowseByTypeConfig) => this.getComponentByBrowseByType(config.type))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -4,7 +4,7 @@ import { Collection } from '../core/shared/collection.model';
|
|||||||
import { CollectionPageResolver } from './collection-page.resolver';
|
import { CollectionPageResolver } from './collection-page.resolver';
|
||||||
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
||||||
import { Observable, of as observableOf } from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
import { DsoPageFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard';
|
import { DsoPageSingleFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard';
|
||||||
import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
||||||
import { AuthService } from '../core/auth/auth.service';
|
import { AuthService } from '../core/auth/auth.service';
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ import { AuthService } from '../core/auth/auth.service';
|
|||||||
/**
|
/**
|
||||||
* Guard for preventing unauthorized access to certain {@link Collection} pages requiring administrator rights
|
* Guard for preventing unauthorized access to certain {@link Collection} pages requiring administrator rights
|
||||||
*/
|
*/
|
||||||
export class CollectionPageAdministratorGuard extends DsoPageFeatureGuard<Collection> {
|
export class CollectionPageAdministratorGuard extends DsoPageSingleFeatureGuard<Collection> {
|
||||||
constructor(protected resolver: CollectionPageResolver,
|
constructor(protected resolver: CollectionPageResolver,
|
||||||
protected authorizationService: AuthorizationDataService,
|
protected authorizationService: AuthorizationDataService,
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
|
@@ -4,7 +4,7 @@ import { Community } from '../core/shared/community.model';
|
|||||||
import { CommunityPageResolver } from './community-page.resolver';
|
import { CommunityPageResolver } from './community-page.resolver';
|
||||||
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
||||||
import { Observable, of as observableOf } from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
import { DsoPageFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard';
|
import { DsoPageSingleFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard';
|
||||||
import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
||||||
import { AuthService } from '../core/auth/auth.service';
|
import { AuthService } from '../core/auth/auth.service';
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ import { AuthService } from '../core/auth/auth.service';
|
|||||||
/**
|
/**
|
||||||
* Guard for preventing unauthorized access to certain {@link Community} pages requiring administrator rights
|
* Guard for preventing unauthorized access to certain {@link Community} pages requiring administrator rights
|
||||||
*/
|
*/
|
||||||
export class CommunityPageAdministratorGuard extends DsoPageFeatureGuard<Community> {
|
export class CommunityPageAdministratorGuard extends DsoPageSingleFeatureGuard<Community> {
|
||||||
constructor(protected resolver: CommunityPageResolver,
|
constructor(protected resolver: CommunityPageResolver,
|
||||||
protected authorizationService: AuthorizationDataService,
|
protected authorizationService: AuthorizationDataService,
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
|
@@ -31,8 +31,13 @@ import {
|
|||||||
} from './edit-item-page.routing-paths';
|
} from './edit-item-page.routing-paths';
|
||||||
import { ItemPageReinstateGuard } from './item-page-reinstate.guard';
|
import { ItemPageReinstateGuard } from './item-page-reinstate.guard';
|
||||||
import { ItemPageWithdrawGuard } from './item-page-withdraw.guard';
|
import { ItemPageWithdrawGuard } from './item-page-withdraw.guard';
|
||||||
import { ItemPageEditMetadataGuard } from '../item-page-edit-metadata.guard';
|
import { ItemPageMetadataGuard } from './item-page-metadata.guard';
|
||||||
import { ItemPageAdministratorGuard } from '../item-page-administrator.guard';
|
import { ItemPageAdministratorGuard } from '../item-page-administrator.guard';
|
||||||
|
import { ItemPageStatusGuard } from './item-page-status.guard';
|
||||||
|
import { ItemPageBitstreamsGuard } from './item-page-bitstreams.guard';
|
||||||
|
import { ItemPageRelationshipsGuard } from './item-page-relationships.guard';
|
||||||
|
import { ItemPageVersionHistoryGuard } from './item-page-version-history.guard';
|
||||||
|
import { ItemPageCollectionMapperGuard } from './item-page-collection-mapper.guard';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Routing module that handles the routing for the Edit Item page administrator functionality
|
* Routing module that handles the routing for the Edit Item page administrator functionality
|
||||||
@@ -60,25 +65,25 @@ import { ItemPageAdministratorGuard } from '../item-page-administrator.guard';
|
|||||||
path: 'status',
|
path: 'status',
|
||||||
component: ItemStatusComponent,
|
component: ItemStatusComponent,
|
||||||
data: { title: 'item.edit.tabs.status.title', showBreadcrumbs: true },
|
data: { title: 'item.edit.tabs.status.title', showBreadcrumbs: true },
|
||||||
canActivate: [ItemPageAdministratorGuard]
|
canActivate: [ItemPageStatusGuard]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'bitstreams',
|
path: 'bitstreams',
|
||||||
component: ItemBitstreamsComponent,
|
component: ItemBitstreamsComponent,
|
||||||
data: { title: 'item.edit.tabs.bitstreams.title', showBreadcrumbs: true },
|
data: { title: 'item.edit.tabs.bitstreams.title', showBreadcrumbs: true },
|
||||||
canActivate: [ItemPageAdministratorGuard]
|
canActivate: [ItemPageBitstreamsGuard]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'metadata',
|
path: 'metadata',
|
||||||
component: ItemMetadataComponent,
|
component: ItemMetadataComponent,
|
||||||
data: { title: 'item.edit.tabs.metadata.title', showBreadcrumbs: true },
|
data: { title: 'item.edit.tabs.metadata.title', showBreadcrumbs: true },
|
||||||
canActivate: [ItemPageEditMetadataGuard]
|
canActivate: [ItemPageMetadataGuard]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'relationships',
|
path: 'relationships',
|
||||||
component: ItemRelationshipsComponent,
|
component: ItemRelationshipsComponent,
|
||||||
data: { title: 'item.edit.tabs.relationships.title', showBreadcrumbs: true },
|
data: { title: 'item.edit.tabs.relationships.title', showBreadcrumbs: true },
|
||||||
canActivate: [ItemPageEditMetadataGuard]
|
canActivate: [ItemPageRelationshipsGuard]
|
||||||
},
|
},
|
||||||
/* TODO - uncomment & fix when view page exists
|
/* TODO - uncomment & fix when view page exists
|
||||||
{
|
{
|
||||||
@@ -96,13 +101,13 @@ import { ItemPageAdministratorGuard } from '../item-page-administrator.guard';
|
|||||||
path: 'versionhistory',
|
path: 'versionhistory',
|
||||||
component: ItemVersionHistoryComponent,
|
component: ItemVersionHistoryComponent,
|
||||||
data: { title: 'item.edit.tabs.versionhistory.title', showBreadcrumbs: true },
|
data: { title: 'item.edit.tabs.versionhistory.title', showBreadcrumbs: true },
|
||||||
canActivate: [ItemPageAdministratorGuard]
|
canActivate: [ItemPageVersionHistoryGuard]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'mapper',
|
path: 'mapper',
|
||||||
component: ItemCollectionMapperComponent,
|
component: ItemCollectionMapperComponent,
|
||||||
data: { title: 'item.edit.tabs.item-mapper.title', showBreadcrumbs: true },
|
data: { title: 'item.edit.tabs.item-mapper.title', showBreadcrumbs: true },
|
||||||
canActivate: [ItemPageAdministratorGuard]
|
canActivate: [ItemPageCollectionMapperGuard]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -175,7 +180,12 @@ import { ItemPageAdministratorGuard } from '../item-page-administrator.guard';
|
|||||||
ItemPageReinstateGuard,
|
ItemPageReinstateGuard,
|
||||||
ItemPageWithdrawGuard,
|
ItemPageWithdrawGuard,
|
||||||
ItemPageAdministratorGuard,
|
ItemPageAdministratorGuard,
|
||||||
ItemPageEditMetadataGuard,
|
ItemPageMetadataGuard,
|
||||||
|
ItemPageStatusGuard,
|
||||||
|
ItemPageBitstreamsGuard,
|
||||||
|
ItemPageRelationshipsGuard,
|
||||||
|
ItemPageVersionHistoryGuard,
|
||||||
|
ItemPageCollectionMapperGuard,
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class EditItemPageRoutingModule {
|
export class EditItemPageRoutingModule {
|
||||||
|
@@ -3,13 +3,15 @@
|
|||||||
{{'item.edit.tabs.status.buttons.' + operation.operationKey + '.label' | translate}}
|
{{'item.edit.tabs.status.buttons.' + operation.operationKey + '.label' | translate}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="!operation.disabled" class="col-9 float-left action-button">
|
<div class="col-9 float-left action-button">
|
||||||
<a class="btn btn-outline-primary" [routerLink]="operation.operationUrl">
|
<span *ngIf="operation.authorized">
|
||||||
|
<button class="btn btn-outline-primary" [disabled]="operation.disabled" [routerLink]="operation.operationUrl">
|
||||||
{{'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate}}
|
{{'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate}}
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</span>
|
||||||
<div *ngIf="operation.disabled" class="col-9 float-left action-button">
|
<span *ngIf="!operation.authorized" [ngbTooltip]="'item.edit.tabs.status.buttons.unauthorized' | translate">
|
||||||
<span class="btn btn-danger">
|
<button class="btn btn-outline-primary" [disabled]="true">
|
||||||
{{'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate}}
|
{{'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate}}
|
||||||
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -28,19 +28,19 @@ describe('ItemOperationComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should render operation row', () => {
|
it('should render operation row', () => {
|
||||||
const span = fixture.debugElement.query(By.css('span')).nativeElement;
|
const span = fixture.debugElement.query(By.css('.action-label span')).nativeElement;
|
||||||
expect(span.textContent).toContain('item.edit.tabs.status.buttons.key1.label');
|
expect(span.textContent).toContain('item.edit.tabs.status.buttons.key1.label');
|
||||||
const link = fixture.debugElement.query(By.css('a')).nativeElement;
|
const button = fixture.debugElement.query(By.css('button')).nativeElement;
|
||||||
expect(link.href).toContain('url1');
|
expect(button.textContent).toContain('item.edit.tabs.status.buttons.key1.button');
|
||||||
expect(link.textContent).toContain('item.edit.tabs.status.buttons.key1.button');
|
|
||||||
});
|
});
|
||||||
it('should render disabled operation row', () => {
|
it('should render disabled operation row', () => {
|
||||||
itemOperation.setDisabled(true);
|
itemOperation.setDisabled(true);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const span = fixture.debugElement.query(By.css('span')).nativeElement;
|
const span = fixture.debugElement.query(By.css('.action-label span')).nativeElement;
|
||||||
expect(span.textContent).toContain('item.edit.tabs.status.buttons.key1.label');
|
expect(span.textContent).toContain('item.edit.tabs.status.buttons.key1.label');
|
||||||
const span2 = fixture.debugElement.query(By.css('span.btn-danger')).nativeElement;
|
const button = fixture.debugElement.query(By.css('button')).nativeElement;
|
||||||
expect(span2.textContent).toContain('item.edit.tabs.status.buttons.key1.button');
|
expect(button.disabled).toBeTrue();
|
||||||
|
expect(button.textContent).toContain('item.edit.tabs.status.buttons.key1.button');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents an item operation used on the edit item page with a key, an operation URL to which will be navigated
|
* Represents an item operation used on the edit item page with a key, an operation URL to which will be navigated
|
||||||
* when performing the action and an option to disable the operation.
|
* when performing the action and an option to disable the operation.
|
||||||
@@ -7,11 +9,15 @@ export class ItemOperation {
|
|||||||
operationKey: string;
|
operationKey: string;
|
||||||
operationUrl: string;
|
operationUrl: string;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
|
authorized: boolean;
|
||||||
|
featureID: FeatureID;
|
||||||
|
|
||||||
constructor(operationKey: string, operationUrl: string) {
|
constructor(operationKey: string, operationUrl: string, featureID?: FeatureID, disabled = false, authorized = true) {
|
||||||
this.operationKey = operationKey;
|
this.operationKey = operationKey;
|
||||||
this.operationUrl = operationUrl;
|
this.operationUrl = operationUrl;
|
||||||
this.setDisabled(false);
|
this.featureID = featureID;
|
||||||
|
this.authorized = authorized;
|
||||||
|
this.setDisabled(disabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -0,0 +1,31 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard';
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { ItemPageResolver } from '../item-page.resolver';
|
||||||
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Guard for preventing unauthorized access to certain {@link Item} pages requiring manage bitstreams rights
|
||||||
|
*/
|
||||||
|
export class ItemPageBitstreamsGuard extends DsoPageSingleFeatureGuard<Item> {
|
||||||
|
constructor(protected resolver: ItemPageResolver,
|
||||||
|
protected authorizationService: AuthorizationDataService,
|
||||||
|
protected router: Router,
|
||||||
|
protected authService: AuthService) {
|
||||||
|
super(resolver, authorizationService, router, authService);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check manage bitstreams authorization rights
|
||||||
|
*/
|
||||||
|
getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
||||||
|
return observableOf(FeatureID.CanManageBitstreamBundles);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,31 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { ItemPageResolver } from '../item-page.resolver';
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard';
|
||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Guard for preventing unauthorized access to certain {@link Item} pages requiring manage mappings rights
|
||||||
|
*/
|
||||||
|
export class ItemPageCollectionMapperGuard extends DsoPageSingleFeatureGuard<Item> {
|
||||||
|
constructor(protected resolver: ItemPageResolver,
|
||||||
|
protected authorizationService: AuthorizationDataService,
|
||||||
|
protected router: Router,
|
||||||
|
protected authService: AuthService) {
|
||||||
|
super(resolver, authorizationService, router, authService);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check manage mappings authorization rights
|
||||||
|
*/
|
||||||
|
getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
||||||
|
return observableOf(FeatureID.CanManageMappings);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,12 +1,12 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
import { ItemPageResolver } from './item-page.resolver';
|
import { ItemPageResolver } from '../item-page.resolver';
|
||||||
import { Item } from '../core/shared/item.model';
|
import { Item } from '../../core/shared/item.model';
|
||||||
import { DsoPageFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard';
|
import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard';
|
||||||
import { Observable, of as observableOf } from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
import { AuthService } from '../core/auth/auth.service';
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@@ -14,7 +14,7 @@ import { AuthService } from '../core/auth/auth.service';
|
|||||||
/**
|
/**
|
||||||
* Guard for preventing unauthorized access to certain {@link Item} pages requiring edit metadata rights
|
* Guard for preventing unauthorized access to certain {@link Item} pages requiring edit metadata rights
|
||||||
*/
|
*/
|
||||||
export class ItemPageEditMetadataGuard extends DsoPageFeatureGuard<Item> {
|
export class ItemPageMetadataGuard extends DsoPageSingleFeatureGuard<Item> {
|
||||||
constructor(protected resolver: ItemPageResolver,
|
constructor(protected resolver: ItemPageResolver,
|
||||||
protected authorizationService: AuthorizationDataService,
|
protected authorizationService: AuthorizationDataService,
|
||||||
protected router: Router,
|
protected router: Router,
|
@@ -1,5 +1,5 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { DsoPageFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard';
|
import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard';
|
||||||
import { Item } from '../../core/shared/item.model';
|
import { Item } from '../../core/shared/item.model';
|
||||||
import { ItemPageResolver } from '../item-page.resolver';
|
import { ItemPageResolver } from '../item-page.resolver';
|
||||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
@@ -14,7 +14,7 @@ import { AuthService } from '../../core/auth/auth.service';
|
|||||||
/**
|
/**
|
||||||
* Guard for preventing unauthorized access to certain {@link Item} pages requiring reinstate rights
|
* Guard for preventing unauthorized access to certain {@link Item} pages requiring reinstate rights
|
||||||
*/
|
*/
|
||||||
export class ItemPageReinstateGuard extends DsoPageFeatureGuard<Item> {
|
export class ItemPageReinstateGuard extends DsoPageSingleFeatureGuard<Item> {
|
||||||
constructor(protected resolver: ItemPageResolver,
|
constructor(protected resolver: ItemPageResolver,
|
||||||
protected authorizationService: AuthorizationDataService,
|
protected authorizationService: AuthorizationDataService,
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
|
@@ -0,0 +1,31 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { ItemPageResolver } from '../item-page.resolver';
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard';
|
||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Guard for preventing unauthorized access to certain {@link Item} pages requiring manage relationships rights
|
||||||
|
*/
|
||||||
|
export class ItemPageRelationshipsGuard extends DsoPageSingleFeatureGuard<Item> {
|
||||||
|
constructor(protected resolver: ItemPageResolver,
|
||||||
|
protected authorizationService: AuthorizationDataService,
|
||||||
|
protected router: Router,
|
||||||
|
protected authService: AuthService) {
|
||||||
|
super(resolver, authorizationService, router, authService);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check manage relationships authorization rights
|
||||||
|
*/
|
||||||
|
getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
||||||
|
return observableOf(FeatureID.CanManageRelationships);
|
||||||
|
}
|
||||||
|
}
|
32
src/app/+item-page/edit-item-page/item-page-status.guard.ts
Normal file
32
src/app/+item-page/edit-item-page/item-page-status.guard.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { ItemPageResolver } from '../item-page.resolver';
|
||||||
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
import { DsoPageSomeFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Guard for preventing unauthorized access to certain {@link Item} pages requiring any of the rights required for
|
||||||
|
* the status page
|
||||||
|
*/
|
||||||
|
export class ItemPageStatusGuard extends DsoPageSomeFeatureGuard<Item> {
|
||||||
|
constructor(protected resolver: ItemPageResolver,
|
||||||
|
protected authorizationService: AuthorizationDataService,
|
||||||
|
protected router: Router,
|
||||||
|
protected authService: AuthService) {
|
||||||
|
super(resolver, authorizationService, router, authService);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check authorization rights
|
||||||
|
*/
|
||||||
|
getFeatureIDs(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID[]> {
|
||||||
|
return observableOf([FeatureID.CanManageMappings, FeatureID.WithdrawItem, FeatureID.ReinstateItem, FeatureID.CanManagePolicies, FeatureID.CanMakePrivate, FeatureID.CanDelete, FeatureID.CanMove]);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,31 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { ItemPageResolver } from '../item-page.resolver';
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard';
|
||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Guard for preventing unauthorized access to certain {@link Item} pages requiring manage versions rights
|
||||||
|
*/
|
||||||
|
export class ItemPageVersionHistoryGuard extends DsoPageSingleFeatureGuard<Item> {
|
||||||
|
constructor(protected resolver: ItemPageResolver,
|
||||||
|
protected authorizationService: AuthorizationDataService,
|
||||||
|
protected router: Router,
|
||||||
|
protected authService: AuthService) {
|
||||||
|
super(resolver, authorizationService, router, authService);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check manage versions authorization rights
|
||||||
|
*/
|
||||||
|
getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
||||||
|
return observableOf(FeatureID.CanManageVersions);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
import { DsoPageFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard';
|
import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard';
|
||||||
import { Item } from '../../core/shared/item.model';
|
import { Item } from '../../core/shared/item.model';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { ItemPageResolver } from '../item-page.resolver';
|
import { ItemPageResolver } from '../item-page.resolver';
|
||||||
@@ -14,7 +14,7 @@ import { AuthService } from '../../core/auth/auth.service';
|
|||||||
/**
|
/**
|
||||||
* Guard for preventing unauthorized access to certain {@link Item} pages requiring withdraw rights
|
* Guard for preventing unauthorized access to certain {@link Item} pages requiring withdraw rights
|
||||||
*/
|
*/
|
||||||
export class ItemPageWithdrawGuard extends DsoPageFeatureGuard<Item> {
|
export class ItemPageWithdrawGuard extends DsoPageSingleFeatureGuard<Item> {
|
||||||
constructor(protected resolver: ItemPageResolver,
|
constructor(protected resolver: ItemPageResolver,
|
||||||
protected authorizationService: AuthorizationDataService,
|
protected authorizationService: AuthorizationDataService,
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
|
@@ -18,7 +18,7 @@ import { RelationshipType } from '../../../../core/shared/item-relationships/rel
|
|||||||
import {
|
import {
|
||||||
getAllSucceededRemoteData,
|
getAllSucceededRemoteData,
|
||||||
getRemoteDataPayload,
|
getRemoteDataPayload,
|
||||||
getFirstSucceededRemoteData,
|
getFirstSucceededRemoteData, getFirstSucceededRemoteDataPayload,
|
||||||
} from '../../../../core/shared/operators';
|
} from '../../../../core/shared/operators';
|
||||||
import { ItemType } from '../../../../core/shared/item-relationships/item-type.model';
|
import { ItemType } from '../../../../core/shared/item-relationships/item-type.model';
|
||||||
import { DsDynamicLookupRelationModalComponent } from '../../../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component';
|
import { DsDynamicLookupRelationModalComponent } from '../../../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component';
|
||||||
@@ -29,6 +29,7 @@ import { SearchResult } from '../../../../shared/search/search-result.model';
|
|||||||
import { followLink } from '../../../../shared/utils/follow-link-config.model';
|
import { followLink } from '../../../../shared/utils/follow-link-config.model';
|
||||||
import { PaginatedList } from '../../../../core/data/paginated-list.model';
|
import { PaginatedList } from '../../../../core/data/paginated-list.model';
|
||||||
import { RemoteData } from '../../../../core/data/remote-data';
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
|
import { Collection } from '../../../../core/shared/collection.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-edit-relationship-list',
|
selector: 'ds-edit-relationship-list',
|
||||||
@@ -146,6 +147,11 @@ export class EditRelationshipListComponent implements OnInit {
|
|||||||
modalComp.repeatable = true;
|
modalComp.repeatable = true;
|
||||||
modalComp.listId = this.listId;
|
modalComp.listId = this.listId;
|
||||||
modalComp.item = this.item;
|
modalComp.item = this.item;
|
||||||
|
this.item.owningCollection.pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload()
|
||||||
|
).subscribe((collection: Collection) => {
|
||||||
|
modalComp.collection = collection;
|
||||||
|
});
|
||||||
modalComp.select = (...selectableObjects: SearchResult<Item>[]) => {
|
modalComp.select = (...selectableObjects: SearchResult<Item>[]) => {
|
||||||
selectableObjects.forEach((searchResult) => {
|
selectableObjects.forEach((searchResult) => {
|
||||||
const relatedItem: Item = searchResult.indexableObject;
|
const relatedItem: Item = searchResult.indexableObject;
|
||||||
|
@@ -3,8 +3,8 @@ import { fadeIn, fadeInOut } from '../../../shared/animations/fade';
|
|||||||
import { Item } from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { ItemOperation } from '../item-operation/itemOperation.model';
|
import { ItemOperation } from '../item-operation/itemOperation.model';
|
||||||
import { distinctUntilChanged, first, map } from 'rxjs/operators';
|
import { distinctUntilChanged, first, map, mergeMap, toArray } from 'rxjs/operators';
|
||||||
import { BehaviorSubject, Observable } from 'rxjs';
|
import { BehaviorSubject, Observable, from as observableFrom } from 'rxjs';
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import { getItemEditRoute, getItemPageRoute } from '../../item-page-routing-paths';
|
import { getItemEditRoute, getItemPageRoute } from '../../item-page-routing-paths';
|
||||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||||
@@ -78,42 +78,36 @@ export class ItemStatusComponent implements OnInit {
|
|||||||
The value is supposed to be a href for the button
|
The value is supposed to be a href for the button
|
||||||
*/
|
*/
|
||||||
const operations = [];
|
const operations = [];
|
||||||
operations.push(new ItemOperation('authorizations', this.getCurrentUrl(item) + '/authorizations'));
|
operations.push(new ItemOperation('authorizations', this.getCurrentUrl(item) + '/authorizations', FeatureID.CanManagePolicies, true));
|
||||||
operations.push(new ItemOperation('mappedCollections', this.getCurrentUrl(item) + '/mapper'));
|
operations.push(new ItemOperation('mappedCollections', this.getCurrentUrl(item) + '/mapper', FeatureID.CanManageMappings, true));
|
||||||
operations.push(undefined);
|
if (item.isWithdrawn) {
|
||||||
// Store the index of the "withdraw" or "reinstate" operation, because it's added asynchronously
|
operations.push(new ItemOperation('reinstate', this.getCurrentUrl(item) + '/reinstate', FeatureID.ReinstateItem, true));
|
||||||
const indexOfWithdrawReinstate = operations.length - 1;
|
|
||||||
if (item.isDiscoverable) {
|
|
||||||
operations.push(new ItemOperation('private', this.getCurrentUrl(item) + '/private'));
|
|
||||||
} else {
|
} else {
|
||||||
operations.push(new ItemOperation('public', this.getCurrentUrl(item) + '/public'));
|
operations.push(new ItemOperation('withdraw', this.getCurrentUrl(item) + '/withdraw', FeatureID.WithdrawItem, true));
|
||||||
}
|
}
|
||||||
operations.push(new ItemOperation('delete', this.getCurrentUrl(item) + '/delete'));
|
if (item.isDiscoverable) {
|
||||||
operations.push(new ItemOperation('move', this.getCurrentUrl(item) + '/move'));
|
operations.push(new ItemOperation('private', this.getCurrentUrl(item) + '/private', FeatureID.CanMakePrivate, true));
|
||||||
|
} else {
|
||||||
|
operations.push(new ItemOperation('public', this.getCurrentUrl(item) + '/public', FeatureID.CanMakePrivate, true));
|
||||||
|
}
|
||||||
|
operations.push(new ItemOperation('delete', this.getCurrentUrl(item) + '/delete', FeatureID.CanDelete, true));
|
||||||
|
operations.push(new ItemOperation('move', this.getCurrentUrl(item) + '/move', FeatureID.CanMove, true));
|
||||||
|
|
||||||
this.operations$.next(operations);
|
this.operations$.next(operations);
|
||||||
|
|
||||||
if (item.isWithdrawn) {
|
observableFrom(operations).pipe(
|
||||||
this.authorizationService.isAuthorized(FeatureID.ReinstateItem, item.self).pipe(distinctUntilChanged()).subscribe((authorized) => {
|
mergeMap((operation) => {
|
||||||
const newOperations = [...this.operations$.value];
|
if (hasValue(operation.featureID)) {
|
||||||
if (authorized) {
|
return this.authorizationService.isAuthorized(operation.featureID, item.self).pipe(
|
||||||
newOperations[indexOfWithdrawReinstate] = new ItemOperation('reinstate', this.getCurrentUrl(item) + '/reinstate');
|
distinctUntilChanged(),
|
||||||
|
map((authorized) => new ItemOperation(operation.operationKey, operation.operationUrl, operation.featureID, !authorized, authorized))
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
newOperations[indexOfWithdrawReinstate] = undefined;
|
return [operation];
|
||||||
}
|
|
||||||
this.operations$.next(newOperations);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.authorizationService.isAuthorized(FeatureID.WithdrawItem, item.self).pipe(distinctUntilChanged()).subscribe((authorized) => {
|
|
||||||
const newOperations = [...this.operations$.value];
|
|
||||||
if (authorized) {
|
|
||||||
newOperations[indexOfWithdrawReinstate] = new ItemOperation('withdraw', this.getCurrentUrl(item) + '/withdraw');
|
|
||||||
} else {
|
|
||||||
newOperations[indexOfWithdrawReinstate] = undefined;
|
|
||||||
}
|
|
||||||
this.operations$.next(newOperations);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
}),
|
||||||
|
toArray()
|
||||||
|
).subscribe((ops) => this.operations$.next(ops));
|
||||||
});
|
});
|
||||||
this.itemPageRoute$ = this.itemRD$.pipe(
|
this.itemPageRoute$ = this.itemRD$.pipe(
|
||||||
getAllSucceededRemoteDataPayload(),
|
getAllSucceededRemoteDataPayload(),
|
||||||
|
@@ -3,7 +3,7 @@ import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/ro
|
|||||||
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
||||||
import { ItemPageResolver } from './item-page.resolver';
|
import { ItemPageResolver } from './item-page.resolver';
|
||||||
import { Item } from '../core/shared/item.model';
|
import { Item } from '../core/shared/item.model';
|
||||||
import { DsoPageFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard';
|
import { DsoPageSingleFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard';
|
||||||
import { Observable, of as observableOf } from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
||||||
import { AuthService } from '../core/auth/auth.service';
|
import { AuthService } from '../core/auth/auth.service';
|
||||||
@@ -14,7 +14,7 @@ import { AuthService } from '../core/auth/auth.service';
|
|||||||
/**
|
/**
|
||||||
* Guard for preventing unauthorized access to certain {@link Item} pages requiring administrator rights
|
* Guard for preventing unauthorized access to certain {@link Item} pages requiring administrator rights
|
||||||
*/
|
*/
|
||||||
export class ItemPageAdministratorGuard extends DsoPageFeatureGuard<Item> {
|
export class ItemPageAdministratorGuard extends DsoPageSingleFeatureGuard<Item> {
|
||||||
constructor(protected resolver: ItemPageResolver,
|
constructor(protected resolver: ItemPageResolver,
|
||||||
protected authorizationService: AuthorizationDataService,
|
protected authorizationService: AuthorizationDataService,
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
|
@@ -9,6 +9,12 @@ import {
|
|||||||
getFirstSucceededRemoteData
|
getFirstSucceededRemoteData
|
||||||
} from '../../../../core/shared/operators';
|
} from '../../../../core/shared/operators';
|
||||||
import { hasValue } from '../../../../shared/empty.util';
|
import { hasValue } from '../../../../shared/empty.util';
|
||||||
|
import { InjectionToken } from '@angular/core';
|
||||||
|
|
||||||
|
export const PAGINATED_RELATIONS_TO_ITEMS_OPERATOR = new InjectionToken<(thisId: string) => (source: Observable<RemoteData<PaginatedList<Relationship>>>) => Observable<RemoteData<PaginatedList<Item>>>>('paginatedRelationsToItems', {
|
||||||
|
providedIn: 'root',
|
||||||
|
factory: () => paginatedRelationsToItems
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Operator for comparing arrays using a mapping function
|
* Operator for comparing arrays using a mapping function
|
||||||
|
@@ -27,7 +27,10 @@ describe('MyDSpaceConfigurationService', () => {
|
|||||||
scope: ''
|
scope: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
const backendFilters = [new SearchFilter('f.namedresourcetype', ['another value']), new SearchFilter('f.dateSubmitted', ['[2013 TO 2018]'])];
|
const backendFilters = [
|
||||||
|
new SearchFilter('f.namedresourcetype', ['another value']),
|
||||||
|
new SearchFilter('f.dateSubmitted', ['[2013 TO 2018]'], 'equals')
|
||||||
|
];
|
||||||
|
|
||||||
const spy = jasmine.createSpyObj('RouteService', {
|
const spy = jasmine.createSpyObj('RouteService', {
|
||||||
getQueryParameterValue: observableOf(value1),
|
getQueryParameterValue: observableOf(value1),
|
||||||
|
@@ -6,6 +6,8 @@
|
|||||||
[configurationList]="(configurationList$ | async)"
|
[configurationList]="(configurationList$ | async)"
|
||||||
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
|
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
|
||||||
[viewModeList]="viewModeList"
|
[viewModeList]="viewModeList"
|
||||||
|
[searchOptions]="(searchOptions$ | async)"
|
||||||
|
[sortOptions]="(sortOptions$ | async)"
|
||||||
[refreshFilters]="refreshFilters.asObservable()"
|
[refreshFilters]="refreshFilters.asObservable()"
|
||||||
[inPlaceSearch]="inPlaceSearch"></ds-search-sidebar>
|
[inPlaceSearch]="inPlaceSearch"></ds-search-sidebar>
|
||||||
<div class="col-12 col-md-9">
|
<div class="col-12 col-md-9">
|
||||||
@@ -28,6 +30,8 @@
|
|||||||
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
|
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
|
||||||
(toggleSidebar)="closeSidebar()"
|
(toggleSidebar)="closeSidebar()"
|
||||||
[ngClass]="{'active': !(isSidebarCollapsed() | async)}"
|
[ngClass]="{'active': !(isSidebarCollapsed() | async)}"
|
||||||
|
[searchOptions]="(searchOptions$ | async)"
|
||||||
|
[sortOptions]="(sortOptions$ | async)"
|
||||||
[refreshFilters]="refreshFilters.asObservable()"
|
[refreshFilters]="refreshFilters.asObservable()"
|
||||||
[inPlaceSearch]="inPlaceSearch">
|
[inPlaceSearch]="inPlaceSearch">
|
||||||
</ds-search-sidebar>
|
</ds-search-sidebar>
|
||||||
|
@@ -45,6 +45,7 @@ describe('MyDSpacePageComponent', () => {
|
|||||||
pagination.id = 'mydspace-results-pagination';
|
pagination.id = 'mydspace-results-pagination';
|
||||||
pagination.currentPage = 1;
|
pagination.currentPage = 1;
|
||||||
pagination.pageSize = 10;
|
pagination.pageSize = 10;
|
||||||
|
const sortOption = { name: 'score', sortOrder: 'DESC', metadata: null };
|
||||||
const sort: SortOptions = new SortOptions('score', SortDirection.DESC);
|
const sort: SortOptions = new SortOptions('score', SortDirection.DESC);
|
||||||
const mockResults = createSuccessfulRemoteDataObject$(['test', 'data']);
|
const mockResults = createSuccessfulRemoteDataObject$(['test', 'data']);
|
||||||
const searchServiceStub = jasmine.createSpyObj('SearchService', {
|
const searchServiceStub = jasmine.createSpyObj('SearchService', {
|
||||||
@@ -52,7 +53,8 @@ describe('MyDSpacePageComponent', () => {
|
|||||||
getEndpoint: observableOf('discover/search/objects'),
|
getEndpoint: observableOf('discover/search/objects'),
|
||||||
getSearchLink: '/mydspace',
|
getSearchLink: '/mydspace',
|
||||||
getScopes: observableOf(['test-scope']),
|
getScopes: observableOf(['test-scope']),
|
||||||
setServiceOptions: {}
|
setServiceOptions: {},
|
||||||
|
getSearchConfigurationFor: createSuccessfulRemoteDataObject$({ sortOptions: [sortOption]})
|
||||||
});
|
});
|
||||||
const configurationParam = 'default';
|
const configurationParam = 'default';
|
||||||
const queryParam = 'test query';
|
const queryParam = 'test query';
|
||||||
@@ -188,4 +190,24 @@ describe('MyDSpacePageComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when stable', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have initialized the sortOptions$ observable', (done) => {
|
||||||
|
|
||||||
|
comp.sortOptions$.subscribe((sortOptions) => {
|
||||||
|
|
||||||
|
expect(sortOptions.length).toEqual(2);
|
||||||
|
expect(sortOptions[0]).toEqual(new SortOptions('score', SortDirection.ASC));
|
||||||
|
expect(sortOptions[1]).toEqual(new SortOptions('score', SortDirection.DESC));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -8,7 +8,7 @@ import {
|
|||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
|
||||||
import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs';
|
import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs';
|
||||||
import { map, switchMap, tap, } from 'rxjs/operators';
|
import { map, switchMap, tap } from 'rxjs/operators';
|
||||||
|
|
||||||
import { PaginatedList } from '../core/data/paginated-list.model';
|
import { PaginatedList } from '../core/data/paginated-list.model';
|
||||||
import { RemoteData } from '../core/data/remote-data';
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
@@ -29,6 +29,8 @@ import { ViewMode } from '../core/shared/view-mode.model';
|
|||||||
import { MyDSpaceRequest } from '../core/data/request.models';
|
import { MyDSpaceRequest } from '../core/data/request.models';
|
||||||
import { SearchResult } from '../shared/search/search-result.model';
|
import { SearchResult } from '../shared/search/search-result.model';
|
||||||
import { Context } from '../core/shared/context.model';
|
import { Context } from '../core/shared/context.model';
|
||||||
|
import { SortOptions } from '../core/cache/models/sort-options.model';
|
||||||
|
import { RouteService } from '../core/services/route.service';
|
||||||
|
|
||||||
export const MYDSPACE_ROUTE = '/mydspace';
|
export const MYDSPACE_ROUTE = '/mydspace';
|
||||||
export const SEARCH_CONFIG_SERVICE: InjectionToken<SearchConfigurationService> = new InjectionToken<SearchConfigurationService>('searchConfigurationService');
|
export const SEARCH_CONFIG_SERVICE: InjectionToken<SearchConfigurationService> = new InjectionToken<SearchConfigurationService>('searchConfigurationService');
|
||||||
@@ -71,6 +73,11 @@ export class MyDSpacePageComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
searchOptions$: Observable<PaginatedSearchOptions>;
|
searchOptions$: Observable<PaginatedSearchOptions>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current available sort options
|
||||||
|
*/
|
||||||
|
sortOptions$: Observable<SortOptions[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current relevant scopes
|
* The current relevant scopes
|
||||||
*/
|
*/
|
||||||
@@ -109,7 +116,8 @@ export class MyDSpacePageComponent implements OnInit {
|
|||||||
constructor(private service: SearchService,
|
constructor(private service: SearchService,
|
||||||
private sidebarService: SidebarService,
|
private sidebarService: SidebarService,
|
||||||
private windowService: HostWindowService,
|
private windowService: HostWindowService,
|
||||||
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: MyDSpaceConfigurationService) {
|
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: MyDSpaceConfigurationService,
|
||||||
|
private routeService: RouteService) {
|
||||||
this.isXsOrSm$ = this.windowService.isXsOrSm();
|
this.isXsOrSm$ = this.windowService.isXsOrSm();
|
||||||
this.service.setServiceOptions(MyDSpaceResponseParsingService, MyDSpaceRequest);
|
this.service.setServiceOptions(MyDSpaceResponseParsingService, MyDSpaceRequest);
|
||||||
}
|
}
|
||||||
@@ -151,6 +159,12 @@ export class MyDSpacePageComponent implements OnInit {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const configuration$ = this.searchConfigService.getCurrentConfiguration('workspace');
|
||||||
|
const searchConfig$ = this.searchConfigService.getConfigurationSearchConfigObservable(configuration$, this.service);
|
||||||
|
|
||||||
|
this.sortOptions$ = this.searchConfigService.getConfigurationSortOptionsObservable(searchConfig$);
|
||||||
|
this.searchConfigService.initializeSortOptionsFromConfiguration(searchConfig$);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -31,11 +31,14 @@
|
|||||||
<ng-template #sidebarContent>
|
<ng-template #sidebarContent>
|
||||||
<ds-search-sidebar id="search-sidebar" *ngIf="!(isXsOrSm$ | async)"
|
<ds-search-sidebar id="search-sidebar" *ngIf="!(isXsOrSm$ | async)"
|
||||||
[resultCount]="(resultsRD$ | async)?.payload?.totalElements"
|
[resultCount]="(resultsRD$ | async)?.payload?.totalElements"
|
||||||
|
[searchOptions]="(searchOptions$ | async)"
|
||||||
|
[sortOptions]="(sortOptions$ | async)"
|
||||||
[inPlaceSearch]="inPlaceSearch"></ds-search-sidebar>
|
[inPlaceSearch]="inPlaceSearch"></ds-search-sidebar>
|
||||||
<ds-search-sidebar id="search-sidebar-sm" *ngIf="(isXsOrSm$ | async)"
|
<ds-search-sidebar id="search-sidebar-sm" *ngIf="(isXsOrSm$ | async)"
|
||||||
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
|
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
|
||||||
(toggleSidebar)="closeSidebar()"
|
[searchOptions]="(searchOptions$ | async)"
|
||||||
>
|
[sortOptions]="(sortOptions$ | async)"
|
||||||
|
(toggleSidebar)="closeSidebar()">
|
||||||
</ds-search-sidebar>
|
</ds-search-sidebar>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
|
@@ -40,12 +40,14 @@ const pagination: PaginationComponentOptions = new PaginationComponentOptions();
|
|||||||
pagination.id = 'search-results-pagination';
|
pagination.id = 'search-results-pagination';
|
||||||
pagination.currentPage = 1;
|
pagination.currentPage = 1;
|
||||||
pagination.pageSize = 10;
|
pagination.pageSize = 10;
|
||||||
|
const sortOption = { name: 'score', sortOrder: 'DESC', metadata: null };
|
||||||
const sort: SortOptions = new SortOptions('score', SortDirection.DESC);
|
const sort: SortOptions = new SortOptions('score', SortDirection.DESC);
|
||||||
const mockResults = createSuccessfulRemoteDataObject$(['test', 'data']);
|
const mockResults = createSuccessfulRemoteDataObject$(['test', 'data']);
|
||||||
const searchServiceStub = jasmine.createSpyObj('SearchService', {
|
const searchServiceStub = jasmine.createSpyObj('SearchService', {
|
||||||
search: mockResults,
|
search: mockResults,
|
||||||
getSearchLink: '/search',
|
getSearchLink: '/search',
|
||||||
getScopes: observableOf(['test-scope'])
|
getScopes: observableOf(['test-scope']),
|
||||||
|
getSearchConfigurationFor: createSuccessfulRemoteDataObject$({ sortOptions: [sortOption]})
|
||||||
});
|
});
|
||||||
const configurationParam = 'default';
|
const configurationParam = 'default';
|
||||||
const queryParam = 'test query';
|
const queryParam = 'test query';
|
||||||
@@ -181,4 +183,24 @@ describe('SearchComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when stable', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have initialized the sortOptions$ observable', (done) => {
|
||||||
|
|
||||||
|
comp.sortOptions$.subscribe((sortOptions) => {
|
||||||
|
|
||||||
|
expect(sortOptions.length).toEqual(2);
|
||||||
|
expect(sortOptions[0]).toEqual(new SortOptions('score', SortDirection.ASC));
|
||||||
|
expect(sortOptions[1]).toEqual(new SortOptions('score', SortDirection.DESC));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,13 +1,13 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angular/core';
|
||||||
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
|
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
|
||||||
import { startWith, switchMap, } from 'rxjs/operators';
|
import { startWith, switchMap } from 'rxjs/operators';
|
||||||
import { PaginatedList } from '../core/data/paginated-list.model';
|
import { PaginatedList } from '../core/data/paginated-list.model';
|
||||||
import { RemoteData } from '../core/data/remote-data';
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
import { DSpaceObject } from '../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../core/shared/dspace-object.model';
|
||||||
import { pushInOut } from '../shared/animations/push';
|
import { pushInOut } from '../shared/animations/push';
|
||||||
import { HostWindowService } from '../shared/host-window.service';
|
import { HostWindowService } from '../shared/host-window.service';
|
||||||
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||||
import { hasValue, isNotEmpty } from '../shared/empty.util';
|
import { hasValue, isEmpty } from '../shared/empty.util';
|
||||||
import { getFirstSucceededRemoteData } from '../core/shared/operators';
|
import { getFirstSucceededRemoteData } from '../core/shared/operators';
|
||||||
import { RouteService } from '../core/services/route.service';
|
import { RouteService } from '../core/services/route.service';
|
||||||
import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component';
|
import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component';
|
||||||
@@ -16,8 +16,9 @@ import { SearchResult } from '../shared/search/search-result.model';
|
|||||||
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
|
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
|
||||||
import { SearchService } from '../core/shared/search/search.service';
|
import { SearchService } from '../core/shared/search/search.service';
|
||||||
import { currentPath } from '../shared/utils/route.utils';
|
import { currentPath } from '../shared/utils/route.utils';
|
||||||
import { Router } from '@angular/router';
|
import { Router} from '@angular/router';
|
||||||
import { Context } from '../core/shared/context.model';
|
import { Context } from '../core/shared/context.model';
|
||||||
|
import { SortOptions } from '../core/cache/models/sort-options.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-search',
|
selector: 'ds-search',
|
||||||
@@ -47,6 +48,11 @@ export class SearchComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
searchOptions$: Observable<PaginatedSearchOptions>;
|
searchOptions$: Observable<PaginatedSearchOptions>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current available sort options
|
||||||
|
*/
|
||||||
|
sortOptions$: Observable<SortOptions[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current relevant scopes
|
* The current relevant scopes
|
||||||
*/
|
*/
|
||||||
@@ -129,9 +135,15 @@ export class SearchComponent implements OnInit {
|
|||||||
this.scopeListRD$ = this.searchConfigService.getCurrentScope('').pipe(
|
this.scopeListRD$ = this.searchConfigService.getCurrentScope('').pipe(
|
||||||
switchMap((scopeId) => this.service.getScopes(scopeId))
|
switchMap((scopeId) => this.service.getScopes(scopeId))
|
||||||
);
|
);
|
||||||
if (!isNotEmpty(this.configuration$)) {
|
if (isEmpty(this.configuration$)) {
|
||||||
this.configuration$ = this.routeService.getRouteParameterValue('configuration');
|
this.configuration$ = this.searchConfigService.getCurrentConfiguration('default');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const searchConfig$ = this.searchConfigService.getConfigurationSearchConfigObservable(this.configuration$, this.service);
|
||||||
|
|
||||||
|
this.sortOptions$ = this.searchConfigService.getConfigurationSortOptionsObservable(searchConfig$);
|
||||||
|
this.searchConfigService.initializeSortOptionsFromConfiguration(searchConfig$);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -210,4 +210,11 @@ describe('GroupFormComponent', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('ngOnDestroy', () => {
|
||||||
|
it('does NOT call router.navigate', () => {
|
||||||
|
component.ngOnDestroy();
|
||||||
|
expect(router.navigate).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -405,7 +405,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
@HostListener('window:beforeunload')
|
@HostListener('window:beforeunload')
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.onCancel();
|
this.groupDataService.cancelEditGroup();
|
||||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
14
src/app/core/cache/builders/build-decorators.ts
vendored
14
src/app/core/cache/builders/build-decorators.ts
vendored
@@ -8,6 +8,20 @@ import {
|
|||||||
TypedObject,
|
TypedObject,
|
||||||
getResourceTypeValueFor
|
getResourceTypeValueFor
|
||||||
} from '../object-cache.reducer';
|
} from '../object-cache.reducer';
|
||||||
|
import { InjectionToken } from '@angular/core';
|
||||||
|
|
||||||
|
export const DATA_SERVICE_FACTORY = new InjectionToken<(resourceType: ResourceType) => GenericConstructor<any>>('getDataServiceFor', {
|
||||||
|
providedIn: 'root',
|
||||||
|
factory: () => getDataServiceFor
|
||||||
|
});
|
||||||
|
export const LINK_DEFINITION_FACTORY = new InjectionToken<<T extends HALResource>(source: GenericConstructor<T>, linkName: keyof T['_links']) => LinkDefinition<T>>('getLinkDefinition', {
|
||||||
|
providedIn: 'root',
|
||||||
|
factory: () => getLinkDefinition
|
||||||
|
});
|
||||||
|
export const LINK_DEFINITION_MAP_FACTORY = new InjectionToken<<T extends HALResource>(source: GenericConstructor<T>) => Map<keyof T['_links'], LinkDefinition<T>>>('getLinkDefinitions', {
|
||||||
|
providedIn: 'root',
|
||||||
|
factory: () => getLinkDefinitions
|
||||||
|
});
|
||||||
|
|
||||||
const resolvedLinkKey = Symbol('resolvedLink');
|
const resolvedLinkKey = Symbol('resolvedLink');
|
||||||
|
|
||||||
|
91
src/app/core/cache/builders/link.service.spec.ts
vendored
91
src/app/core/cache/builders/link.service.spec.ts
vendored
@@ -5,15 +5,9 @@ import { FindListOptions } from '../../data/request.models';
|
|||||||
import { HALLink } from '../../shared/hal-link.model';
|
import { HALLink } from '../../shared/hal-link.model';
|
||||||
import { HALResource } from '../../shared/hal-resource.model';
|
import { HALResource } from '../../shared/hal-resource.model';
|
||||||
import { ResourceType } from '../../shared/resource-type';
|
import { ResourceType } from '../../shared/resource-type';
|
||||||
import * as decorators from './build-decorators';
|
|
||||||
import { LinkService } from './link.service';
|
import { LinkService } from './link.service';
|
||||||
|
import { DATA_SERVICE_FACTORY, LINK_DEFINITION_FACTORY, LINK_DEFINITION_MAP_FACTORY } from './build-decorators';
|
||||||
const spyOnFunction = <T>(obj: T, func: keyof T) => {
|
import { isEmpty } from 'rxjs/operators';
|
||||||
const spy = jasmine.createSpy(func as string);
|
|
||||||
spyOnProperty(obj, func, 'get').and.returnValue(spy);
|
|
||||||
|
|
||||||
return spy;
|
|
||||||
};
|
|
||||||
|
|
||||||
const TEST_MODEL = new ResourceType('testmodel');
|
const TEST_MODEL = new ResourceType('testmodel');
|
||||||
let result: any;
|
let result: any;
|
||||||
@@ -51,7 +45,7 @@ let testDataService: TestDataService;
|
|||||||
|
|
||||||
let testModel: TestModel;
|
let testModel: TestModel;
|
||||||
|
|
||||||
xdescribe('LinkService', () => {
|
describe('LinkService', () => {
|
||||||
let service: LinkService;
|
let service: LinkService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -76,6 +70,30 @@ xdescribe('LinkService', () => {
|
|||||||
providers: [LinkService, {
|
providers: [LinkService, {
|
||||||
provide: TestDataService,
|
provide: TestDataService,
|
||||||
useValue: testDataService
|
useValue: testDataService
|
||||||
|
}, {
|
||||||
|
provide: DATA_SERVICE_FACTORY,
|
||||||
|
useValue: jasmine.createSpy('getDataServiceFor').and.returnValue(TestDataService),
|
||||||
|
}, {
|
||||||
|
provide: LINK_DEFINITION_FACTORY,
|
||||||
|
useValue: jasmine.createSpy('getLinkDefinition').and.returnValue({
|
||||||
|
resourceType: TEST_MODEL,
|
||||||
|
linkName: 'predecessor',
|
||||||
|
propertyName: 'predecessor'
|
||||||
|
}),
|
||||||
|
}, {
|
||||||
|
provide: LINK_DEFINITION_MAP_FACTORY,
|
||||||
|
useValue: jasmine.createSpy('getLinkDefinitions').and.returnValue([
|
||||||
|
{
|
||||||
|
resourceType: TEST_MODEL,
|
||||||
|
linkName: 'predecessor',
|
||||||
|
propertyName: 'predecessor',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
resourceType: TEST_MODEL,
|
||||||
|
linkName: 'successor',
|
||||||
|
propertyName: 'successor',
|
||||||
|
}
|
||||||
|
]),
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
service = TestBed.inject(LinkService);
|
service = TestBed.inject(LinkService);
|
||||||
@@ -84,12 +102,6 @@ xdescribe('LinkService', () => {
|
|||||||
describe('resolveLink', () => {
|
describe('resolveLink', () => {
|
||||||
describe(`when the linkdefinition concerns a single object`, () => {
|
describe(`when the linkdefinition concerns a single object`, () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOnFunction(decorators, 'getLinkDefinition').and.returnValue({
|
|
||||||
resourceType: TEST_MODEL,
|
|
||||||
linkName: 'predecessor',
|
|
||||||
propertyName: 'predecessor'
|
|
||||||
});
|
|
||||||
spyOnFunction(decorators, 'getDataServiceFor').and.returnValue(TestDataService);
|
|
||||||
service.resolveLink(testModel, followLink('predecessor', {}, true, true, true, followLink('successor')));
|
service.resolveLink(testModel, followLink('predecessor', {}, true, true, true, followLink('successor')));
|
||||||
});
|
});
|
||||||
it('should call dataservice.findByHref with the correct href and nested links', () => {
|
it('should call dataservice.findByHref with the correct href and nested links', () => {
|
||||||
@@ -98,13 +110,12 @@ xdescribe('LinkService', () => {
|
|||||||
});
|
});
|
||||||
describe(`when the linkdefinition concerns a list`, () => {
|
describe(`when the linkdefinition concerns a list`, () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOnFunction(decorators, 'getLinkDefinition').and.returnValue({
|
((service as any).getLinkDefinition as jasmine.Spy).and.returnValue({
|
||||||
resourceType: TEST_MODEL,
|
resourceType: TEST_MODEL,
|
||||||
linkName: 'predecessor',
|
linkName: 'predecessor',
|
||||||
propertyName: 'predecessor',
|
propertyName: 'predecessor',
|
||||||
isList: true
|
isList: true
|
||||||
});
|
});
|
||||||
spyOnFunction(decorators, 'getDataServiceFor').and.returnValue(TestDataService);
|
|
||||||
service.resolveLink(testModel, followLink('predecessor', { some: 'options ' } as any, true, true, true, followLink('successor')));
|
service.resolveLink(testModel, followLink('predecessor', { some: 'options ' } as any, true, true, true, followLink('successor')));
|
||||||
});
|
});
|
||||||
it('should call dataservice.findAllByHref with the correct href, findListOptions, and nested links', () => {
|
it('should call dataservice.findAllByHref with the correct href, findListOptions, and nested links', () => {
|
||||||
@@ -113,21 +124,15 @@ xdescribe('LinkService', () => {
|
|||||||
});
|
});
|
||||||
describe('either way', () => {
|
describe('either way', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOnFunction(decorators, 'getLinkDefinition').and.returnValue({
|
|
||||||
resourceType: TEST_MODEL,
|
|
||||||
linkName: 'predecessor',
|
|
||||||
propertyName: 'predecessor'
|
|
||||||
});
|
|
||||||
spyOnFunction(decorators, 'getDataServiceFor').and.returnValue(TestDataService);
|
|
||||||
result = service.resolveLink(testModel, followLink('predecessor', {}, true, true, true, followLink('successor')));
|
result = service.resolveLink(testModel, followLink('predecessor', {}, true, true, true, followLink('successor')));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call getLinkDefinition with the correct model and link', () => {
|
it('should call getLinkDefinition with the correct model and link', () => {
|
||||||
expect(decorators.getLinkDefinition).toHaveBeenCalledWith(testModel.constructor as any, 'predecessor');
|
expect((service as any).getLinkDefinition).toHaveBeenCalledWith(testModel.constructor as any, 'predecessor');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call getDataServiceFor with the correct resource type', () => {
|
it('should call getDataServiceFor with the correct resource type', () => {
|
||||||
expect(decorators.getDataServiceFor).toHaveBeenCalledWith(TEST_MODEL);
|
expect((service as any).getDataServiceFor).toHaveBeenCalledWith(TEST_MODEL);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the model with the resolved link', () => {
|
it('should return the model with the resolved link', () => {
|
||||||
@@ -140,7 +145,7 @@ xdescribe('LinkService', () => {
|
|||||||
|
|
||||||
describe(`when the specified link doesn't exist on the model's class`, () => {
|
describe(`when the specified link doesn't exist on the model's class`, () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOnFunction(decorators, 'getLinkDefinition').and.returnValue(undefined);
|
((service as any).getLinkDefinition as jasmine.Spy).and.returnValue(undefined);
|
||||||
});
|
});
|
||||||
it('should throw an error', () => {
|
it('should throw an error', () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
@@ -151,12 +156,7 @@ xdescribe('LinkService', () => {
|
|||||||
|
|
||||||
describe(`when there is no dataservice for the resourcetype in the link`, () => {
|
describe(`when there is no dataservice for the resourcetype in the link`, () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOnFunction(decorators, 'getLinkDefinition').and.returnValue({
|
((service as any).getDataServiceFor as jasmine.Spy).and.returnValue(undefined);
|
||||||
resourceType: TEST_MODEL,
|
|
||||||
linkName: 'predecessor',
|
|
||||||
propertyName: 'predecessor'
|
|
||||||
});
|
|
||||||
spyOnFunction(decorators, 'getDataServiceFor').and.returnValue(undefined);
|
|
||||||
});
|
});
|
||||||
it('should throw an error', () => {
|
it('should throw an error', () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
@@ -188,18 +188,6 @@ xdescribe('LinkService', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
testModel.predecessor = 'predecessor value' as any;
|
testModel.predecessor = 'predecessor value' as any;
|
||||||
testModel.successor = 'successor value' as any;
|
testModel.successor = 'successor value' as any;
|
||||||
spyOnFunction(decorators, 'getLinkDefinitions').and.returnValue([
|
|
||||||
{
|
|
||||||
resourceType: TEST_MODEL,
|
|
||||||
linkName: 'predecessor',
|
|
||||||
propertyName: 'predecessor',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
resourceType: TEST_MODEL,
|
|
||||||
linkName: 'successor',
|
|
||||||
propertyName: 'successor',
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return a new version of the object without any resolved links', () => {
|
it('should return a new version of the object without any resolved links', () => {
|
||||||
@@ -231,16 +219,10 @@ xdescribe('LinkService', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
spyOnFunction(decorators, 'getDataServiceFor').and.returnValue(TestDataService);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('resolving the available link', () => {
|
describe('resolving the available link', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOnFunction(decorators, 'getLinkDefinition').and.returnValue({
|
|
||||||
resourceType: TEST_MODEL,
|
|
||||||
linkName: 'predecessor',
|
|
||||||
propertyName: 'predecessor'
|
|
||||||
});
|
|
||||||
result = service.resolveLinks(testModel, followLink('predecessor'));
|
result = service.resolveLinks(testModel, followLink('predecessor'));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -251,7 +233,7 @@ xdescribe('LinkService', () => {
|
|||||||
|
|
||||||
describe('resolving the missing link', () => {
|
describe('resolving the missing link', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOnFunction(decorators, 'getLinkDefinition').and.returnValue({
|
((service as any).getLinkDefinition as jasmine.Spy).and.returnValue({
|
||||||
resourceType: TEST_MODEL,
|
resourceType: TEST_MODEL,
|
||||||
linkName: 'successor',
|
linkName: 'successor',
|
||||||
propertyName: 'successor'
|
propertyName: 'successor'
|
||||||
@@ -259,8 +241,11 @@ xdescribe('LinkService', () => {
|
|||||||
result = service.resolveLinks(testModel, followLink('successor'));
|
result = service.resolveLinks(testModel, followLink('successor'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the model with no resolved link', () => {
|
it('should resolve to an empty observable', (done) => {
|
||||||
expect(result.successor).toBeUndefined();
|
result.successor.pipe(isEmpty()).subscribe((v) => {
|
||||||
|
expect(v).toEqual(true);
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
18
src/app/core/cache/builders/link.service.ts
vendored
18
src/app/core/cache/builders/link.service.ts
vendored
@@ -1,17 +1,18 @@
|
|||||||
import { Injectable, Injector } from '@angular/core';
|
import { Inject, Injectable, Injector } from '@angular/core';
|
||||||
import { hasNoValue, hasValue, isNotEmpty } from '../../../shared/empty.util';
|
import { hasNoValue, hasValue, isNotEmpty } from '../../../shared/empty.util';
|
||||||
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
|
||||||
import { GenericConstructor } from '../../shared/generic-constructor';
|
import { GenericConstructor } from '../../shared/generic-constructor';
|
||||||
import { HALResource } from '../../shared/hal-resource.model';
|
import { HALResource } from '../../shared/hal-resource.model';
|
||||||
import {
|
import {
|
||||||
getDataServiceFor,
|
DATA_SERVICE_FACTORY,
|
||||||
getLinkDefinition,
|
LINK_DEFINITION_FACTORY,
|
||||||
getLinkDefinitions,
|
LINK_DEFINITION_MAP_FACTORY,
|
||||||
LinkDefinition
|
LinkDefinition
|
||||||
} from './build-decorators';
|
} from './build-decorators';
|
||||||
import { RemoteData } from '../../data/remote-data';
|
import { RemoteData } from '../../data/remote-data';
|
||||||
import { Observable } from 'rxjs/internal/Observable';
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
import { EMPTY } from 'rxjs';
|
import { EMPTY } from 'rxjs';
|
||||||
|
import { ResourceType } from '../../shared/resource-type';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Service to handle the resolving and removing
|
* A Service to handle the resolving and removing
|
||||||
@@ -24,6 +25,9 @@ export class LinkService {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected parentInjector: Injector,
|
protected parentInjector: Injector,
|
||||||
|
@Inject(DATA_SERVICE_FACTORY) private getDataServiceFor: (resourceType: ResourceType) => GenericConstructor<any>,
|
||||||
|
@Inject(LINK_DEFINITION_FACTORY) private getLinkDefinition: <T extends HALResource>(source: GenericConstructor<T>, linkName: keyof T['_links']) => LinkDefinition<T>,
|
||||||
|
@Inject(LINK_DEFINITION_MAP_FACTORY) private getLinkDefinitions: <T extends HALResource>(source: GenericConstructor<T>) => Map<keyof T['_links'], LinkDefinition<T>>,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,12 +53,12 @@ export class LinkService {
|
|||||||
* @param linkToFollow the {@link FollowLinkConfig} to resolve
|
* @param linkToFollow the {@link FollowLinkConfig} to resolve
|
||||||
*/
|
*/
|
||||||
public resolveLinkWithoutAttaching<T extends HALResource, U extends HALResource>(model, linkToFollow: FollowLinkConfig<T>): Observable<RemoteData<U>> {
|
public resolveLinkWithoutAttaching<T extends HALResource, U extends HALResource>(model, linkToFollow: FollowLinkConfig<T>): Observable<RemoteData<U>> {
|
||||||
const matchingLinkDef = getLinkDefinition(model.constructor, linkToFollow.name);
|
const matchingLinkDef = this.getLinkDefinition(model.constructor, linkToFollow.name);
|
||||||
|
|
||||||
if (hasNoValue(matchingLinkDef)) {
|
if (hasNoValue(matchingLinkDef)) {
|
||||||
throw new Error(`followLink('${linkToFollow.name}') was used for a ${model.constructor.name}, but there is no property on ${model.constructor.name} models with an @link() for ${linkToFollow.name}`);
|
throw new Error(`followLink('${linkToFollow.name}') was used for a ${model.constructor.name}, but there is no property on ${model.constructor.name} models with an @link() for ${linkToFollow.name}`);
|
||||||
} else {
|
} else {
|
||||||
const provider = getDataServiceFor(matchingLinkDef.resourceType);
|
const provider = this.getDataServiceFor(matchingLinkDef.resourceType);
|
||||||
|
|
||||||
if (hasNoValue(provider)) {
|
if (hasNoValue(provider)) {
|
||||||
throw new Error(`The @link() for ${linkToFollow.name} on ${model.constructor.name} models uses the resource type ${matchingLinkDef.resourceType.value.toUpperCase()}, but there is no service with an @dataService(${matchingLinkDef.resourceType.value.toUpperCase()}) annotation in order to retrieve it`);
|
throw new Error(`The @link() for ${linkToFollow.name} on ${model.constructor.name} models uses the resource type ${matchingLinkDef.resourceType.value.toUpperCase()}, but there is no service with an @dataService(${matchingLinkDef.resourceType.value.toUpperCase()}) annotation in order to retrieve it`);
|
||||||
@@ -104,7 +108,7 @@ export class LinkService {
|
|||||||
*/
|
*/
|
||||||
public removeResolvedLinks<T extends HALResource>(model: T): T {
|
public removeResolvedLinks<T extends HALResource>(model: T): T {
|
||||||
const result = Object.assign(new (model.constructor as GenericConstructor<T>)(), model);
|
const result = Object.assign(new (model.constructor as GenericConstructor<T>)(), model);
|
||||||
const linkDefs = getLinkDefinitions(model.constructor as GenericConstructor<T>);
|
const linkDefs = this.getLinkDefinitions(model.constructor as GenericConstructor<T>);
|
||||||
if (isNotEmpty(linkDefs)) {
|
if (isNotEmpty(linkDefs)) {
|
||||||
linkDefs.forEach((linkDef: LinkDefinition<T>) => {
|
linkDefs.forEach((linkDef: LinkDefinition<T>) => {
|
||||||
result[linkDef.propertyName] = undefined;
|
result[linkDef.propertyName] = undefined;
|
||||||
|
@@ -161,6 +161,7 @@ import { ShortLivedToken } from './auth/models/short-lived-token.model';
|
|||||||
import { UsageReport } from './statistics/models/usage-report.model';
|
import { UsageReport } from './statistics/models/usage-report.model';
|
||||||
import { RootDataService } from './data/root-data.service';
|
import { RootDataService } from './data/root-data.service';
|
||||||
import { Root } from './data/root.model';
|
import { Root } from './data/root.model';
|
||||||
|
import { SearchConfig } from './shared/search/search-filters/search-config.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When not in production, endpoint responses can be mocked for testing purposes
|
* When not in production, endpoint responses can be mocked for testing purposes
|
||||||
@@ -340,6 +341,7 @@ export const models =
|
|||||||
Registration,
|
Registration,
|
||||||
UsageReport,
|
UsageReport,
|
||||||
Root,
|
Root,
|
||||||
|
SearchConfig
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@@ -8,8 +8,11 @@ import { Registration } from '../shared/registration.model';
|
|||||||
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
|
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
|
||||||
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
|
||||||
import { of as observableOf } from 'rxjs/internal/observable/of';
|
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||||
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
|
|
||||||
describe('EpersonRegistrationService', () => {
|
describe('EpersonRegistrationService', () => {
|
||||||
|
let testScheduler;
|
||||||
|
|
||||||
let service: EpersonRegistrationService;
|
let service: EpersonRegistrationService;
|
||||||
let requestService: RequestService;
|
let requestService: RequestService;
|
||||||
|
|
||||||
@@ -29,6 +32,12 @@ describe('EpersonRegistrationService', () => {
|
|||||||
rd = createSuccessfulRemoteDataObject(registrationWithUser);
|
rd = createSuccessfulRemoteDataObject(registrationWithUser);
|
||||||
halService = new HALEndpointServiceStub('rest-url');
|
halService = new HALEndpointServiceStub('rest-url');
|
||||||
|
|
||||||
|
testScheduler = new TestScheduler((actual, expected) => {
|
||||||
|
// asserting the two objects are equal
|
||||||
|
// e.g. using chai.
|
||||||
|
expect(actual).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
requestService = jasmine.createSpyObj('requestService', {
|
requestService = jasmine.createSpyObj('requestService', {
|
||||||
generateRequestId: 'request-id',
|
generateRequestId: 'request-id',
|
||||||
send: {},
|
send: {},
|
||||||
@@ -36,7 +45,8 @@ describe('EpersonRegistrationService', () => {
|
|||||||
{ a: Object.assign(new RequestEntry(), { response: new RestResponse(true, 200, 'Success') }) })
|
{ a: Object.assign(new RequestEntry(), { response: new RestResponse(true, 200, 'Success') }) })
|
||||||
});
|
});
|
||||||
rdbService = jasmine.createSpyObj('rdbService', {
|
rdbService = jasmine.createSpyObj('rdbService', {
|
||||||
buildFromRequestUUID: observableOf(rd)
|
buildSingle: observableOf(rd),
|
||||||
|
buildFromRequestUUID: observableOf(rd),
|
||||||
});
|
});
|
||||||
service = new EpersonRegistrationService(
|
service = new EpersonRegistrationService(
|
||||||
requestService,
|
requestService,
|
||||||
@@ -86,8 +96,28 @@ describe('EpersonRegistrationService', () => {
|
|||||||
user: registrationWithUser.user
|
user: registrationWithUser.user
|
||||||
})
|
})
|
||||||
}));
|
}));
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// tslint:disable:no-shadowed-variable
|
||||||
|
it('should use cached responses and /registrations/search/findByToken?', () => {
|
||||||
|
testScheduler.run(({ cold, expectObservable }) => {
|
||||||
|
rdbService.buildSingle.and.returnValue(cold('a', { a: rd }));
|
||||||
|
|
||||||
|
service.searchByToken('test-token');
|
||||||
|
|
||||||
|
expect(requestService.send).toHaveBeenCalledWith(
|
||||||
|
jasmine.objectContaining({
|
||||||
|
uuid: 'request-id', method: 'GET',
|
||||||
|
href: 'rest-url/registrations/search/findByToken?token=test-token',
|
||||||
|
}), true
|
||||||
|
);
|
||||||
|
expectObservable(rdbService.buildSingle.calls.argsFor(0)[0]).toBe('(a|)', {
|
||||||
|
a: 'rest-url/registrations/search/findByToken?token=test-token'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
/**/
|
||||||
|
@@ -3,7 +3,7 @@ import { RequestService } from './request.service';
|
|||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { GetRequest, PostRequest } from './request.models';
|
import { GetRequest, PostRequest } from './request.models';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { filter, find, map, take } from 'rxjs/operators';
|
import { filter, find, map, skipWhile } from 'rxjs/operators';
|
||||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||||
import { Registration } from '../shared/registration.model';
|
import { Registration } from '../shared/registration.model';
|
||||||
import { getFirstCompletedRemoteData, getFirstSucceededRemoteData } from '../shared/operators';
|
import { getFirstCompletedRemoteData, getFirstSucceededRemoteData } from '../shared/operators';
|
||||||
@@ -60,9 +60,9 @@ export class EpersonRegistrationService {
|
|||||||
|
|
||||||
const requestId = this.requestService.generateRequestId();
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
|
||||||
const hrefObs = this.getRegistrationEndpoint();
|
const href$ = this.getRegistrationEndpoint();
|
||||||
|
|
||||||
hrefObs.pipe(
|
href$.pipe(
|
||||||
find((href: string) => hasValue(href)),
|
find((href: string) => hasValue(href)),
|
||||||
map((href: string) => {
|
map((href: string) => {
|
||||||
const request = new PostRequest(requestId, href, registration);
|
const request = new PostRequest(requestId, href, registration);
|
||||||
@@ -82,11 +82,11 @@ export class EpersonRegistrationService {
|
|||||||
searchByToken(token: string): Observable<Registration> {
|
searchByToken(token: string): Observable<Registration> {
|
||||||
const requestId = this.requestService.generateRequestId();
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
|
||||||
const hrefObs = this.getTokenSearchEndpoint(token);
|
const href$ = this.getTokenSearchEndpoint(token).pipe(
|
||||||
|
|
||||||
hrefObs.pipe(
|
|
||||||
find((href: string) => hasValue(href)),
|
find((href: string) => hasValue(href)),
|
||||||
map((href: string) => {
|
);
|
||||||
|
|
||||||
|
href$.subscribe((href: string) => {
|
||||||
const request = new GetRequest(requestId, href);
|
const request = new GetRequest(requestId, href);
|
||||||
Object.assign(request, {
|
Object.assign(request, {
|
||||||
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||||
@@ -94,15 +94,16 @@ export class EpersonRegistrationService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.requestService.send(request, true);
|
this.requestService.send(request, true);
|
||||||
})
|
});
|
||||||
).subscribe();
|
|
||||||
|
|
||||||
return this.rdbService.buildFromRequestUUID<Registration>(requestId).pipe(
|
return this.rdbService.buildSingle<Registration>(href$).pipe(
|
||||||
|
skipWhile((rd: RemoteData<Registration>) => rd.isStale),
|
||||||
getFirstSucceededRemoteData(),
|
getFirstSucceededRemoteData(),
|
||||||
map((restResponse: RemoteData<Registration>) => {
|
map((restResponse: RemoteData<Registration>) => {
|
||||||
return Object.assign(new Registration(), {email: restResponse.payload.email, token: token, user: restResponse.payload.user});
|
return Object.assign(new Registration(), {
|
||||||
|
email: restResponse.payload.email, token: token, user: restResponse.payload.user
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
take(1),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { FeatureAuthorizationGuard } from './feature-authorization.guard';
|
import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard';
|
||||||
import { AuthorizationDataService } from '../authorization-data.service';
|
import { AuthorizationDataService } from '../authorization-data.service';
|
||||||
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
import { AuthService } from '../../../auth/auth.service';
|
import { AuthService } from '../../../auth/auth.service';
|
||||||
@@ -13,7 +13,7 @@ import { FeatureID } from '../feature-id';
|
|||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class CollectionAdministratorGuard extends FeatureAuthorizationGuard {
|
export class CollectionAdministratorGuard extends SingleFeatureAuthorizationGuard {
|
||||||
constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) {
|
constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) {
|
||||||
super(authorizationService, router, authService);
|
super(authorizationService, router, authService);
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { FeatureAuthorizationGuard } from './feature-authorization.guard';
|
import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard';
|
||||||
import { AuthorizationDataService } from '../authorization-data.service';
|
import { AuthorizationDataService } from '../authorization-data.service';
|
||||||
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
import { AuthService } from '../../../auth/auth.service';
|
import { AuthService } from '../../../auth/auth.service';
|
||||||
@@ -13,7 +13,7 @@ import { FeatureID } from '../feature-id';
|
|||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class CommunityAdministratorGuard extends FeatureAuthorizationGuard {
|
export class CommunityAdministratorGuard extends SingleFeatureAuthorizationGuard {
|
||||||
constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) {
|
constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) {
|
||||||
super(authorizationService, router, authService);
|
super(authorizationService, router, authService);
|
||||||
}
|
}
|
||||||
|
@@ -4,14 +4,14 @@ import { RemoteData } from '../../remote-data';
|
|||||||
import { Observable, of as observableOf } from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
|
||||||
import { DSpaceObject } from '../../../shared/dspace-object.model';
|
import { DSpaceObject } from '../../../shared/dspace-object.model';
|
||||||
import { DsoPageFeatureGuard } from './dso-page-feature.guard';
|
import { DsoPageSingleFeatureGuard } from './dso-page-single-feature.guard';
|
||||||
import { FeatureID } from '../feature-id';
|
import { FeatureID } from '../feature-id';
|
||||||
import { AuthService } from '../../../auth/auth.service';
|
import { AuthService } from '../../../auth/auth.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test implementation of abstract class DsoPageAdministratorGuard
|
* Test implementation of abstract class DsoPageSingleFeatureGuard
|
||||||
*/
|
*/
|
||||||
class DsoPageFeatureGuardImpl extends DsoPageFeatureGuard<any> {
|
class DsoPageSingleFeatureGuardImpl extends DsoPageSingleFeatureGuard<any> {
|
||||||
constructor(protected resolver: Resolve<RemoteData<any>>,
|
constructor(protected resolver: Resolve<RemoteData<any>>,
|
||||||
protected authorizationService: AuthorizationDataService,
|
protected authorizationService: AuthorizationDataService,
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
@@ -25,8 +25,8 @@ class DsoPageFeatureGuardImpl extends DsoPageFeatureGuard<any> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('DsoPageAdministratorGuard', () => {
|
describe('DsoPageSingleFeatureGuard', () => {
|
||||||
let guard: DsoPageFeatureGuard<any>;
|
let guard: DsoPageSingleFeatureGuard<any>;
|
||||||
let authorizationService: AuthorizationDataService;
|
let authorizationService: AuthorizationDataService;
|
||||||
let router: Router;
|
let router: Router;
|
||||||
let authService: AuthService;
|
let authService: AuthService;
|
||||||
@@ -62,7 +62,7 @@ describe('DsoPageAdministratorGuard', () => {
|
|||||||
},
|
},
|
||||||
parent: parentRoute
|
parent: parentRoute
|
||||||
};
|
};
|
||||||
guard = new DsoPageFeatureGuardImpl(resolver, authorizationService, router, authService, undefined);
|
guard = new DsoPageSingleFeatureGuardImpl(resolver, authorizationService, router, authService, undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
@@ -0,0 +1,27 @@
|
|||||||
|
import { DSpaceObject } from '../../../shared/dspace-object.model';
|
||||||
|
import { DsoPageSomeFeatureGuard } from './dso-page-some-feature.guard';
|
||||||
|
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { FeatureID } from '../feature-id';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract Guard for preventing unauthorized access to {@link DSpaceObject} pages that require rights for a specific feature
|
||||||
|
* This guard utilizes a resolver to retrieve the relevant object to check authorizations for
|
||||||
|
*/
|
||||||
|
export abstract class DsoPageSingleFeatureGuard<T extends DSpaceObject> extends DsoPageSomeFeatureGuard<T> {
|
||||||
|
/**
|
||||||
|
* The features to check authorization for
|
||||||
|
*/
|
||||||
|
getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID[]> {
|
||||||
|
return this.getFeatureID(route, state).pipe(
|
||||||
|
map((featureID) => [featureID]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of feature to check authorization for
|
||||||
|
* Override this method to define a feature
|
||||||
|
*/
|
||||||
|
abstract getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID>;
|
||||||
|
}
|
@@ -0,0 +1,87 @@
|
|||||||
|
import { AuthorizationDataService } from '../authorization-data.service';
|
||||||
|
import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { RemoteData } from '../../remote-data';
|
||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
|
||||||
|
import { DSpaceObject } from '../../../shared/dspace-object.model';
|
||||||
|
import { FeatureID } from '../feature-id';
|
||||||
|
import { AuthService } from '../../../auth/auth.service';
|
||||||
|
import { DsoPageSomeFeatureGuard } from './dso-page-some-feature.guard';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test implementation of abstract class DsoPageSomeFeatureGuard
|
||||||
|
*/
|
||||||
|
class DsoPageSomeFeatureGuardImpl extends DsoPageSomeFeatureGuard<any> {
|
||||||
|
constructor(protected resolver: Resolve<RemoteData<any>>,
|
||||||
|
protected authorizationService: AuthorizationDataService,
|
||||||
|
protected router: Router,
|
||||||
|
protected authService: AuthService,
|
||||||
|
protected featureIDs: FeatureID[]) {
|
||||||
|
super(resolver, authorizationService, router, authService);
|
||||||
|
}
|
||||||
|
|
||||||
|
getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID[]> {
|
||||||
|
return observableOf(this.featureIDs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('DsoPageSomeFeatureGuard', () => {
|
||||||
|
let guard: DsoPageSomeFeatureGuard<any>;
|
||||||
|
let authorizationService: AuthorizationDataService;
|
||||||
|
let router: Router;
|
||||||
|
let authService: AuthService;
|
||||||
|
let resolver: Resolve<RemoteData<any>>;
|
||||||
|
let object: DSpaceObject;
|
||||||
|
let route;
|
||||||
|
let parentRoute;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
object = {
|
||||||
|
self: 'test-selflink'
|
||||||
|
} as DSpaceObject;
|
||||||
|
|
||||||
|
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||||
|
isAuthorized: observableOf(true)
|
||||||
|
});
|
||||||
|
router = jasmine.createSpyObj('router', {
|
||||||
|
parseUrl: {}
|
||||||
|
});
|
||||||
|
resolver = jasmine.createSpyObj('resolver', {
|
||||||
|
resolve: createSuccessfulRemoteDataObject$(object)
|
||||||
|
});
|
||||||
|
authService = jasmine.createSpyObj('authService', {
|
||||||
|
isAuthenticated: observableOf(true)
|
||||||
|
});
|
||||||
|
parentRoute = {
|
||||||
|
params: {
|
||||||
|
id: '3e1a5327-dabb-41ff-af93-e6cab9d032f0'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
route = {
|
||||||
|
params: {
|
||||||
|
},
|
||||||
|
parent: parentRoute
|
||||||
|
};
|
||||||
|
guard = new DsoPageSomeFeatureGuardImpl(resolver, authorizationService, router, authService, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
init();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getObjectUrl', () => {
|
||||||
|
it('should return the resolved object\'s selflink', (done) => {
|
||||||
|
guard.getObjectUrl(route, undefined).subscribe((selflink) => {
|
||||||
|
expect(selflink).toEqual(object.self);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getRouteWithDSOId', () => {
|
||||||
|
it('should return the route that has the UUID of the DSO', () => {
|
||||||
|
const foundRoute = (guard as any).getRouteWithDSOId(route);
|
||||||
|
expect(foundRoute).toBe(parentRoute);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -5,15 +5,15 @@ import { Observable } from 'rxjs';
|
|||||||
import { getAllSucceededRemoteDataPayload } from '../../../shared/operators';
|
import { getAllSucceededRemoteDataPayload } from '../../../shared/operators';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
import { DSpaceObject } from '../../../shared/dspace-object.model';
|
import { DSpaceObject } from '../../../shared/dspace-object.model';
|
||||||
import { FeatureAuthorizationGuard } from './feature-authorization.guard';
|
|
||||||
import { AuthService } from '../../../auth/auth.service';
|
import { AuthService } from '../../../auth/auth.service';
|
||||||
import { hasNoValue, hasValue } from '../../../../shared/empty.util';
|
import { hasNoValue, hasValue } from '../../../../shared/empty.util';
|
||||||
|
import { SomeFeatureAuthorizationGuard } from './some-feature-authorization.guard';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract Guard for preventing unauthorized access to {@link DSpaceObject} pages that require rights for a specific feature
|
* Abstract Guard for preventing unauthorized access to {@link DSpaceObject} pages that require rights for any specific feature in a list
|
||||||
* This guard utilizes a resolver to retrieve the relevant object to check authorizations for
|
* This guard utilizes a resolver to retrieve the relevant object to check authorizations for
|
||||||
*/
|
*/
|
||||||
export abstract class DsoPageFeatureGuard<T extends DSpaceObject> extends FeatureAuthorizationGuard {
|
export abstract class DsoPageSomeFeatureGuard<T extends DSpaceObject> extends SomeFeatureAuthorizationGuard {
|
||||||
constructor(protected resolver: Resolve<RemoteData<T>>,
|
constructor(protected resolver: Resolve<RemoteData<T>>,
|
||||||
protected authorizationService: AuthorizationDataService,
|
protected authorizationService: AuthorizationDataService,
|
||||||
protected router: Router,
|
protected router: Router,
|
@@ -1,5 +1,5 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { FeatureAuthorizationGuard } from './feature-authorization.guard';
|
import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard';
|
||||||
import { AuthorizationDataService } from '../authorization-data.service';
|
import { AuthorizationDataService } from '../authorization-data.service';
|
||||||
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
import { AuthService } from '../../../auth/auth.service';
|
import { AuthService } from '../../../auth/auth.service';
|
||||||
@@ -13,7 +13,7 @@ import { FeatureID } from '../feature-id';
|
|||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class GroupAdministratorGuard extends FeatureAuthorizationGuard {
|
export class GroupAdministratorGuard extends SingleFeatureAuthorizationGuard {
|
||||||
constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) {
|
constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) {
|
||||||
super(authorizationService, router, authService);
|
super(authorizationService, router, authService);
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { FeatureAuthorizationGuard } from './feature-authorization.guard';
|
import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard';
|
||||||
import { AuthorizationDataService } from '../authorization-data.service';
|
import { AuthorizationDataService } from '../authorization-data.service';
|
||||||
import { FeatureID } from '../feature-id';
|
import { FeatureID } from '../feature-id';
|
||||||
import { Observable, of as observableOf } from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
@@ -6,10 +6,10 @@ import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/ro
|
|||||||
import { AuthService } from '../../../auth/auth.service';
|
import { AuthService } from '../../../auth/auth.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test implementation of abstract class FeatureAuthorizationGuard
|
* Test implementation of abstract class SingleFeatureAuthorizationGuard
|
||||||
* Provide the return values of the overwritten getters as constructor arguments
|
* Provide the return values of the overwritten getters as constructor arguments
|
||||||
*/
|
*/
|
||||||
class FeatureAuthorizationGuardImpl extends FeatureAuthorizationGuard {
|
class SingleFeatureAuthorizationGuardImpl extends SingleFeatureAuthorizationGuard {
|
||||||
constructor(protected authorizationService: AuthorizationDataService,
|
constructor(protected authorizationService: AuthorizationDataService,
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
protected authService: AuthService,
|
protected authService: AuthService,
|
||||||
@@ -32,8 +32,8 @@ class FeatureAuthorizationGuardImpl extends FeatureAuthorizationGuard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('FeatureAuthorizationGuard', () => {
|
describe('SingleFeatureAuthorizationGuard', () => {
|
||||||
let guard: FeatureAuthorizationGuard;
|
let guard: SingleFeatureAuthorizationGuard;
|
||||||
let authorizationService: AuthorizationDataService;
|
let authorizationService: AuthorizationDataService;
|
||||||
let router: Router;
|
let router: Router;
|
||||||
let authService: AuthService;
|
let authService: AuthService;
|
||||||
@@ -56,7 +56,7 @@ describe('FeatureAuthorizationGuard', () => {
|
|||||||
authService = jasmine.createSpyObj('authService', {
|
authService = jasmine.createSpyObj('authService', {
|
||||||
isAuthenticated: observableOf(true)
|
isAuthenticated: observableOf(true)
|
||||||
});
|
});
|
||||||
guard = new FeatureAuthorizationGuardImpl(authorizationService, router, authService, featureId, objectUrl, ePersonUuid);
|
guard = new SingleFeatureAuthorizationGuardImpl(authorizationService, router, authService, featureId, objectUrl, ePersonUuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
@@ -0,0 +1,27 @@
|
|||||||
|
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { FeatureID } from '../feature-id';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { map} from 'rxjs/operators';
|
||||||
|
import { SomeFeatureAuthorizationGuard } from './some-feature-authorization.guard';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract Guard for preventing unauthorized activating and loading of routes when a user
|
||||||
|
* doesn't have authorized rights on a specific feature and/or object.
|
||||||
|
* Override the desired getters in the parent class for checking specific authorization on a feature and/or object.
|
||||||
|
*/
|
||||||
|
export abstract class SingleFeatureAuthorizationGuard extends SomeFeatureAuthorizationGuard {
|
||||||
|
/**
|
||||||
|
* The features to check authorization for
|
||||||
|
*/
|
||||||
|
getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID[]> {
|
||||||
|
return this.getFeatureID(route, state).pipe(
|
||||||
|
map((featureID) => [featureID]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of feature to check authorization for
|
||||||
|
* Override this method to define a feature
|
||||||
|
*/
|
||||||
|
abstract getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID>;
|
||||||
|
}
|
@@ -1,5 +1,5 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { FeatureAuthorizationGuard } from './feature-authorization.guard';
|
import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard';
|
||||||
import { FeatureID } from '../feature-id';
|
import { FeatureID } from '../feature-id';
|
||||||
import { AuthorizationDataService } from '../authorization-data.service';
|
import { AuthorizationDataService } from '../authorization-data.service';
|
||||||
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
@@ -13,7 +13,7 @@ import { AuthService } from '../../../auth/auth.service';
|
|||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class SiteAdministratorGuard extends FeatureAuthorizationGuard {
|
export class SiteAdministratorGuard extends SingleFeatureAuthorizationGuard {
|
||||||
constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) {
|
constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) {
|
||||||
super(authorizationService, router, authService);
|
super(authorizationService, router, authService);
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { FeatureAuthorizationGuard } from './feature-authorization.guard';
|
import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { AuthorizationDataService } from '../authorization-data.service';
|
import { AuthorizationDataService } from '../authorization-data.service';
|
||||||
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
@@ -13,7 +13,7 @@ import { AuthService } from '../../../auth/auth.service';
|
|||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class SiteRegisterGuard extends FeatureAuthorizationGuard {
|
export class SiteRegisterGuard extends SingleFeatureAuthorizationGuard {
|
||||||
constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) {
|
constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) {
|
||||||
super(authorizationService, router, authService);
|
super(authorizationService, router, authService);
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,110 @@
|
|||||||
|
import { AuthorizationDataService } from '../authorization-data.service';
|
||||||
|
import { FeatureID } from '../feature-id';
|
||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { AuthService } from '../../../auth/auth.service';
|
||||||
|
import { SomeFeatureAuthorizationGuard } from './some-feature-authorization.guard';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test implementation of abstract class SomeFeatureAuthorizationGuard
|
||||||
|
* Provide the return values of the overwritten getters as constructor arguments
|
||||||
|
*/
|
||||||
|
class SomeFeatureAuthorizationGuardImpl extends SomeFeatureAuthorizationGuard {
|
||||||
|
constructor(protected authorizationService: AuthorizationDataService,
|
||||||
|
protected router: Router,
|
||||||
|
protected authService: AuthService,
|
||||||
|
protected featureIds: FeatureID[],
|
||||||
|
protected objectUrl: string,
|
||||||
|
protected ePersonUuid: string) {
|
||||||
|
super(authorizationService, router, authService);
|
||||||
|
}
|
||||||
|
|
||||||
|
getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID[]> {
|
||||||
|
return observableOf(this.featureIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
getObjectUrl(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<string> {
|
||||||
|
return observableOf(this.objectUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
getEPersonUuid(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<string> {
|
||||||
|
return observableOf(this.ePersonUuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('SomeFeatureAuthorizationGuard', () => {
|
||||||
|
let guard: SomeFeatureAuthorizationGuard;
|
||||||
|
let authorizationService: AuthorizationDataService;
|
||||||
|
let router: Router;
|
||||||
|
let authService: AuthService;
|
||||||
|
|
||||||
|
let featureIds: FeatureID[];
|
||||||
|
let authorizedFeatureIds: FeatureID[];
|
||||||
|
let objectUrl: string;
|
||||||
|
let ePersonUuid: string;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
featureIds = [FeatureID.LoginOnBehalfOf, FeatureID.CanDelete];
|
||||||
|
authorizedFeatureIds = [];
|
||||||
|
objectUrl = 'fake-object-url';
|
||||||
|
ePersonUuid = 'fake-eperson-uuid';
|
||||||
|
|
||||||
|
authorizationService = Object.assign({
|
||||||
|
isAuthorized(featureId?: FeatureID): Observable<boolean> {
|
||||||
|
return observableOf(authorizedFeatureIds.indexOf(featureId) > -1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
router = jasmine.createSpyObj('router', {
|
||||||
|
parseUrl: {}
|
||||||
|
});
|
||||||
|
authService = jasmine.createSpyObj('authService', {
|
||||||
|
isAuthenticated: observableOf(true)
|
||||||
|
});
|
||||||
|
guard = new SomeFeatureAuthorizationGuardImpl(authorizationService, router, authService, featureIds, objectUrl, ePersonUuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
init();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('canActivate', () => {
|
||||||
|
describe('when the user isn\'t authorized', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
authorizedFeatureIds = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not return true', (done) => {
|
||||||
|
guard.canActivate(undefined, { url: 'current-url' } as any).subscribe((result) => {
|
||||||
|
expect(result).not.toEqual(true);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the user is authorized for at least one of the guard\'s features', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
authorizedFeatureIds = [featureIds[0]];
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true', (done) => {
|
||||||
|
guard.canActivate(undefined, { url: 'current-url' } as any).subscribe((result) => {
|
||||||
|
expect(result).toEqual(true);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the user is authorized for all of the guard\'s features', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
authorizedFeatureIds = featureIds;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true', (done) => {
|
||||||
|
guard.canActivate(undefined, { url: 'current-url' } as any).subscribe((result) => {
|
||||||
|
expect(result).toEqual(true);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -2,16 +2,16 @@ import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTr
|
|||||||
import { AuthorizationDataService } from '../authorization-data.service';
|
import { AuthorizationDataService } from '../authorization-data.service';
|
||||||
import { FeatureID } from '../feature-id';
|
import { FeatureID } from '../feature-id';
|
||||||
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
|
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
|
||||||
import { returnForbiddenUrlTreeOrLoginOnFalse } from '../../../shared/operators';
|
import { returnForbiddenUrlTreeOrLoginOnAllFalse } from '../../../shared/operators';
|
||||||
import { switchMap } from 'rxjs/operators';
|
import { switchMap } from 'rxjs/operators';
|
||||||
import { AuthService } from '../../../auth/auth.service';
|
import { AuthService } from '../../../auth/auth.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract Guard for preventing unauthorized activating and loading of routes when a user
|
* Abstract Guard for preventing unauthorized activating and loading of routes when a user
|
||||||
* doesn't have authorized rights on a specific feature and/or object.
|
* doesn't have authorized rights on any of the specified features and/or object.
|
||||||
* Override the desired getters in the parent class for checking specific authorization on a feature and/or object.
|
* Override the desired getters in the parent class for checking specific authorization on a list of features and/or object.
|
||||||
*/
|
*/
|
||||||
export abstract class FeatureAuthorizationGuard implements CanActivate {
|
export abstract class SomeFeatureAuthorizationGuard implements CanActivate {
|
||||||
constructor(protected authorizationService: AuthorizationDataService,
|
constructor(protected authorizationService: AuthorizationDataService,
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
protected authService: AuthService) {
|
protected authService: AuthService) {
|
||||||
@@ -22,17 +22,19 @@ export abstract class FeatureAuthorizationGuard implements CanActivate {
|
|||||||
* Redirect the user to the unauthorized page when he/she's not authorized for the given feature
|
* Redirect the user to the unauthorized page when he/she's not authorized for the given feature
|
||||||
*/
|
*/
|
||||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
|
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
|
||||||
return observableCombineLatest(this.getFeatureID(route, state), this.getObjectUrl(route, state), this.getEPersonUuid(route, state)).pipe(
|
return observableCombineLatest(this.getFeatureIDs(route, state), this.getObjectUrl(route, state), this.getEPersonUuid(route, state)).pipe(
|
||||||
switchMap(([featureID, objectUrl, ePersonUuid]) => this.authorizationService.isAuthorized(featureID, objectUrl, ePersonUuid)),
|
switchMap(([featureIDs, objectUrl, ePersonUuid]) =>
|
||||||
returnForbiddenUrlTreeOrLoginOnFalse(this.router, this.authService, state.url)
|
observableCombineLatest(...featureIDs.map((featureID) => this.authorizationService.isAuthorized(featureID, objectUrl, ePersonUuid)))
|
||||||
|
),
|
||||||
|
returnForbiddenUrlTreeOrLoginOnAllFalse(this.router, this.authService, state.url)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The type of feature to check authorization for
|
* The features to check authorization for
|
||||||
* Override this method to define a feature
|
* Override this method to define a list of features
|
||||||
*/
|
*/
|
||||||
abstract getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID>;
|
abstract getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The URL of the object to check if the user has authorized rights for
|
* The URL of the object to check if the user has authorized rights for
|
@@ -13,4 +13,11 @@ export enum FeatureID {
|
|||||||
IsCollectionAdmin = 'isCollectionAdmin',
|
IsCollectionAdmin = 'isCollectionAdmin',
|
||||||
IsCommunityAdmin = 'isCommunityAdmin',
|
IsCommunityAdmin = 'isCommunityAdmin',
|
||||||
CanDownload = 'canDownload',
|
CanDownload = 'canDownload',
|
||||||
|
CanManageVersions = 'canManageVersions',
|
||||||
|
CanManageBitstreamBundles = 'canManageBitstreamBundles',
|
||||||
|
CanManageRelationships = 'canManageRelationships',
|
||||||
|
CanManageMappings = 'canManageMappings',
|
||||||
|
CanManagePolicies = 'canManagePolicies',
|
||||||
|
CanMakePrivate = 'canMakePrivate',
|
||||||
|
CanMove = 'canMove',
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import * as ItemRelationshipsUtils from '../../+item-page/simple/item-types/shared/item-relationships-utils';
|
|
||||||
import { followLink } from '../../shared/utils/follow-link-config.model';
|
import { followLink } from '../../shared/utils/follow-link-config.model';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { RelationshipType } from '../shared/item-relationships/relationship-type.model';
|
import { RelationshipType } from '../shared/item-relationships/relationship-type.model';
|
||||||
@@ -15,9 +14,9 @@ import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-servic
|
|||||||
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
import { getMockRemoteDataBuildServiceHrefMap } from '../../shared/mocks/remote-data-build.service.mock';
|
import { getMockRemoteDataBuildServiceHrefMap } from '../../shared/mocks/remote-data-build.service.mock';
|
||||||
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
||||||
import { createPaginatedList, spyOnOperator } from '../../shared/testing/utils.test';
|
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||||
|
|
||||||
xdescribe('RelationshipService', () => {
|
describe('RelationshipService', () => {
|
||||||
let service: RelationshipService;
|
let service: RelationshipService;
|
||||||
let requestService: RequestService;
|
let requestService: RequestService;
|
||||||
|
|
||||||
@@ -132,7 +131,8 @@ xdescribe('RelationshipService', () => {
|
|||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null
|
null,
|
||||||
|
jasmine.createSpy('paginatedRelationsToItems').and.returnValue((v) => v),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,8 +195,6 @@ xdescribe('RelationshipService', () => {
|
|||||||
|
|
||||||
const rd$ = createSuccessfulRemoteDataObject$(relationsList);
|
const rd$ = createSuccessfulRemoteDataObject$(relationsList);
|
||||||
spyOn(service, 'getItemRelationshipsByLabel').and.returnValue(rd$);
|
spyOn(service, 'getItemRelationshipsByLabel').and.returnValue(rd$);
|
||||||
|
|
||||||
spyOnOperator(ItemRelationshipsUtils, 'paginatedRelationsToItems').and.returnValue((v) => v);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call getItemRelationshipsByLabel with the correct params', (done) => {
|
it('should call getItemRelationshipsByLabel with the correct params', (done) => {
|
||||||
@@ -225,7 +223,7 @@ xdescribe('RelationshipService', () => {
|
|||||||
mockLabel,
|
mockLabel,
|
||||||
mockOptions
|
mockOptions
|
||||||
).subscribe((result) => {
|
).subscribe((result) => {
|
||||||
expect(ItemRelationshipsUtils.paginatedRelationsToItems).toHaveBeenCalledWith(mockItem.uuid);
|
expect((service as any).paginatedRelationsToItems).toHaveBeenCalledWith(mockItem.uuid);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,11 +1,10 @@
|
|||||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||||
import { Injectable } from '@angular/core';
|
import { Inject, Injectable } from '@angular/core';
|
||||||
import { MemoizedSelector, select, Store } from '@ngrx/store';
|
import { MemoizedSelector, select, Store } from '@ngrx/store';
|
||||||
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||||
import { distinctUntilChanged, filter, map, mergeMap, startWith, switchMap, take, tap } from 'rxjs/operators';
|
import { distinctUntilChanged, filter, map, mergeMap, startWith, switchMap, take, tap } from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
compareArraysUsingIds,
|
compareArraysUsingIds, PAGINATED_RELATIONS_TO_ITEMS_OPERATOR,
|
||||||
paginatedRelationsToItems,
|
|
||||||
relationsToItems
|
relationsToItems
|
||||||
} from '../../+item-page/simple/item-types/shared/item-relationships-utils';
|
} from '../../+item-page/simple/item-types/shared/item-relationships-utils';
|
||||||
import { AppState, keySelector } from '../../app.reducer';
|
import { AppState, keySelector } from '../../app.reducer';
|
||||||
@@ -87,7 +86,8 @@ export class RelationshipService extends DataService<Relationship> {
|
|||||||
protected notificationsService: NotificationsService,
|
protected notificationsService: NotificationsService,
|
||||||
protected http: HttpClient,
|
protected http: HttpClient,
|
||||||
protected comparator: DefaultChangeAnalyzer<Relationship>,
|
protected comparator: DefaultChangeAnalyzer<Relationship>,
|
||||||
protected appStore: Store<AppState>) {
|
protected appStore: Store<AppState>,
|
||||||
|
@Inject(PAGINATED_RELATIONS_TO_ITEMS_OPERATOR) private paginatedRelationsToItems: (thisId: string) => (source: Observable<RemoteData<PaginatedList<Relationship>>>) => Observable<RemoteData<PaginatedList<Item>>>) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,7 +254,7 @@ export class RelationshipService extends DataService<Relationship> {
|
|||||||
* @param options
|
* @param options
|
||||||
*/
|
*/
|
||||||
getRelatedItemsByLabel(item: Item, label: string, options?: FindListOptions): Observable<RemoteData<PaginatedList<Item>>> {
|
getRelatedItemsByLabel(item: Item, label: string, options?: FindListOptions): Observable<RemoteData<PaginatedList<Item>>> {
|
||||||
return this.getItemRelationshipsByLabel(item, label, options, true, true, followLink('leftItem'), followLink('rightItem'), followLink('relationshipType')).pipe(paginatedRelationsToItems(item.uuid));
|
return this.getItemRelationshipsByLabel(item, label, options, true, true, followLink('leftItem'), followLink('rightItem'), followLink('relationshipType')).pipe(this.paginatedRelationsToItems(item.uuid));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { Router, UrlTree } from '@angular/router';
|
import { Router, UrlTree } from '@angular/router';
|
||||||
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||||
import { filter, find, map, mergeMap, switchMap, take, takeWhile, tap } from 'rxjs/operators';
|
import { debounceTime, filter, find, map, mergeMap, switchMap, take, takeWhile, tap } from 'rxjs/operators';
|
||||||
import { hasNoValue, hasValue, hasValueOperator, isNotEmpty } from '../../shared/empty.util';
|
import { hasNoValue, hasValue, hasValueOperator, isNotEmpty } from '../../shared/empty.util';
|
||||||
import { SearchResult } from '../../shared/search/search-result.model';
|
import { SearchResult } from '../../shared/search/search-result.model';
|
||||||
import { PaginatedList } from '../data/paginated-list.model';
|
import { PaginatedList } from '../data/paginated-list.model';
|
||||||
@@ -15,6 +15,12 @@ import { DSpaceObject } from './dspace-object.model';
|
|||||||
import { getForbiddenRoute, getPageNotFoundRoute } from '../../app-routing-paths';
|
import { getForbiddenRoute, getPageNotFoundRoute } from '../../app-routing-paths';
|
||||||
import { getEndUserAgreementPath } from '../../info/info-routing-paths';
|
import { getEndUserAgreementPath } from '../../info/info-routing-paths';
|
||||||
import { AuthService } from '../auth/auth.service';
|
import { AuthService } from '../auth/auth.service';
|
||||||
|
import { InjectionToken } from '@angular/core';
|
||||||
|
|
||||||
|
export const DEBOUNCE_TIME_OPERATOR = new InjectionToken<<T>(dueTime: number) => (source: Observable<T>) => Observable<T>>('debounceTime', {
|
||||||
|
providedIn: 'root',
|
||||||
|
factory: () => debounceTime
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This file contains custom RxJS operators that can be used in multiple places
|
* This file contains custom RxJS operators that can be used in multiple places
|
||||||
@@ -201,10 +207,23 @@ export const redirectOn4xx = <T>(router: Router, authService: AuthService) =>
|
|||||||
*/
|
*/
|
||||||
export const returnForbiddenUrlTreeOrLoginOnFalse = (router: Router, authService: AuthService, redirectUrl: string) =>
|
export const returnForbiddenUrlTreeOrLoginOnFalse = (router: Router, authService: AuthService, redirectUrl: string) =>
|
||||||
(source: Observable<boolean>): Observable<boolean | UrlTree> =>
|
(source: Observable<boolean>): Observable<boolean | UrlTree> =>
|
||||||
|
source.pipe(
|
||||||
|
map((authorized) => [authorized]),
|
||||||
|
returnForbiddenUrlTreeOrLoginOnAllFalse(router, authService, redirectUrl),
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Operator that returns a UrlTree to a forbidden page or the login page when the booleans received are all false
|
||||||
|
* @param router The router used to navigate to a forbidden page
|
||||||
|
* @param authService The AuthService used to determine whether or not the user is logged in
|
||||||
|
* @param redirectUrl The URL to redirect back to after logging in
|
||||||
|
*/
|
||||||
|
export const returnForbiddenUrlTreeOrLoginOnAllFalse = (router: Router, authService: AuthService, redirectUrl: string) =>
|
||||||
|
(source: Observable<boolean[]>): Observable<boolean | UrlTree> =>
|
||||||
observableCombineLatest(source, authService.isAuthenticated()).pipe(
|
observableCombineLatest(source, authService.isAuthenticated()).pipe(
|
||||||
map(([authorized, authenticated]: [boolean, boolean]) => {
|
map(([authorizedList, authenticated]: [boolean[], boolean]) => {
|
||||||
if (authorized) {
|
if (authorizedList.some((b: boolean) => b === true)) {
|
||||||
return authorized;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
if (authenticated) {
|
if (authenticated) {
|
||||||
return router.parseUrl(getForbiddenRoute());
|
return router.parseUrl(getForbiddenRoute());
|
||||||
|
@@ -23,7 +23,10 @@ describe('SearchConfigurationService', () => {
|
|||||||
scope: ''
|
scope: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
const backendFilters = [new SearchFilter('f.author', ['another value']), new SearchFilter('f.date', ['[2013 TO 2018]'])];
|
const backendFilters = [
|
||||||
|
new SearchFilter('f.author', ['another value']),
|
||||||
|
new SearchFilter('f.date', ['[2013 TO 2018]'], 'equals')
|
||||||
|
];
|
||||||
|
|
||||||
const routeService = jasmine.createSpyObj('RouteService', {
|
const routeService = jasmine.createSpyObj('RouteService', {
|
||||||
getQueryParameterValue: observableOf(value1),
|
getQueryParameterValue: observableOf(value1),
|
||||||
|
@@ -1,8 +1,15 @@
|
|||||||
import { Injectable, OnDestroy } from '@angular/core';
|
import { Injectable, OnDestroy } from '@angular/core';
|
||||||
import { ActivatedRoute, Params } from '@angular/router';
|
import { ActivatedRoute, Params } from '@angular/router';
|
||||||
|
|
||||||
import { BehaviorSubject, combineLatest as observableCombineLatest, merge as observableMerge, Observable, Subscription } from 'rxjs';
|
import {
|
||||||
import { filter, map, startWith } from 'rxjs/operators';
|
BehaviorSubject,
|
||||||
|
combineLatest,
|
||||||
|
combineLatest as observableCombineLatest,
|
||||||
|
merge as observableMerge,
|
||||||
|
Observable,
|
||||||
|
Subscription
|
||||||
|
} from 'rxjs';
|
||||||
|
import { distinctUntilChanged, filter, map, startWith, switchMap, take } from 'rxjs/operators';
|
||||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||||
import { SearchOptions } from '../../../shared/search/search-options.model';
|
import { SearchOptions } from '../../../shared/search/search-options.model';
|
||||||
import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model';
|
import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model';
|
||||||
@@ -11,9 +18,15 @@ import { RemoteData } from '../../data/remote-data';
|
|||||||
import { DSpaceObjectType } from '../dspace-object-type.model';
|
import { DSpaceObjectType } from '../dspace-object-type.model';
|
||||||
import { SortDirection, SortOptions } from '../../cache/models/sort-options.model';
|
import { SortDirection, SortOptions } from '../../cache/models/sort-options.model';
|
||||||
import { RouteService } from '../../services/route.service';
|
import { RouteService } from '../../services/route.service';
|
||||||
import { getFirstSucceededRemoteData } from '../operators';
|
import {
|
||||||
|
getAllSucceededRemoteDataPayload,
|
||||||
|
getFirstSucceededRemoteData
|
||||||
|
} from '../operators';
|
||||||
import { hasNoValue, hasValue, isNotEmpty, isNotEmptyOperator } from '../../../shared/empty.util';
|
import { hasNoValue, hasValue, isNotEmpty, isNotEmptyOperator } from '../../../shared/empty.util';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||||
|
import { SearchConfig } from './search-filters/search-config.model';
|
||||||
|
import { SearchService } from './search.service';
|
||||||
|
import { of } from 'rxjs/internal/observable/of';
|
||||||
import { PaginationService } from '../../pagination/pagination.service';
|
import { PaginationService } from '../../pagination/pagination.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -168,7 +181,7 @@ export class SearchConfigurationService implements OnDestroy {
|
|||||||
if (hasNoValue(filters.find((f) => f.key === realKey))) {
|
if (hasNoValue(filters.find((f) => f.key === realKey))) {
|
||||||
const min = filterParams[realKey + '.min'] ? filterParams[realKey + '.min'][0] : '*';
|
const min = filterParams[realKey + '.min'] ? filterParams[realKey + '.min'][0] : '*';
|
||||||
const max = filterParams[realKey + '.max'] ? filterParams[realKey + '.max'][0] : '*';
|
const max = filterParams[realKey + '.max'] ? filterParams[realKey + '.max'][0] : '*';
|
||||||
filters.push(new SearchFilter(realKey, ['[' + min + ' TO ' + max + ']']));
|
filters.push(new SearchFilter(realKey, ['[' + min + ' TO ' + max + ']'], 'equals'));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
filters.push(new SearchFilter(key, filterParams[key]));
|
filters.push(new SearchFilter(key, filterParams[key]));
|
||||||
@@ -194,6 +207,60 @@ export class SearchConfigurationService implements OnDestroy {
|
|||||||
return this.routeService.getQueryParamsWithPrefix('f.');
|
return this.routeService.getQueryParamsWithPrefix('f.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an observable of SearchConfig every time the configuration$ stream emits.
|
||||||
|
* @param configuration$
|
||||||
|
* @param service
|
||||||
|
*/
|
||||||
|
getConfigurationSearchConfigObservable(configuration$: Observable<string>, service: SearchService): Observable<SearchConfig> {
|
||||||
|
return configuration$.pipe(
|
||||||
|
distinctUntilChanged(),
|
||||||
|
switchMap((configuration) => service.getSearchConfigurationFor(null, configuration)),
|
||||||
|
getAllSucceededRemoteDataPayload());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every time searchConfig change (after a configuration change) it update the navigation with the default sort option
|
||||||
|
* and emit the new paginateSearchOptions value.
|
||||||
|
* @param configuration$
|
||||||
|
* @param service
|
||||||
|
*/
|
||||||
|
initializeSortOptionsFromConfiguration(searchConfig$: Observable<SearchConfig>) {
|
||||||
|
const subscription = searchConfig$.pipe(switchMap((searchConfig) => combineLatest([
|
||||||
|
of(searchConfig),
|
||||||
|
this.paginatedSearchOptions.pipe(take(1))
|
||||||
|
]))).subscribe(([searchConfig, searchOptions]) => {
|
||||||
|
const field = searchConfig.sortOptions[0].name;
|
||||||
|
const direction = searchConfig.sortOptions[0].sortOrder.toLowerCase() === SortDirection.ASC.toLowerCase() ? SortDirection.ASC : SortDirection.DESC;
|
||||||
|
const updateValue = Object.assign(new PaginatedSearchOptions({}), searchOptions, {
|
||||||
|
sort: new SortOptions(field, direction)
|
||||||
|
});
|
||||||
|
this.paginationService.updateRoute(this.paginationID,
|
||||||
|
{
|
||||||
|
sortDirection: updateValue.sort.direction,
|
||||||
|
sortField: updateValue.sort.field,
|
||||||
|
});
|
||||||
|
this.paginatedSearchOptions.next(updateValue);
|
||||||
|
});
|
||||||
|
this.subs.push(subscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an observable of available SortOptions[] every time the searchConfig$ stream emits.
|
||||||
|
* @param searchConfig$
|
||||||
|
* @param service
|
||||||
|
*/
|
||||||
|
getConfigurationSortOptionsObservable(searchConfig$: Observable<SearchConfig>): Observable<SortOptions[]> {
|
||||||
|
return searchConfig$.pipe(map((searchConfig) => {
|
||||||
|
const sortOptions = [];
|
||||||
|
searchConfig.sortOptions.forEach(sortOption => {
|
||||||
|
sortOptions.push(new SortOptions(sortOption.name, SortDirection.ASC));
|
||||||
|
sortOptions.push(new SortOptions(sortOption.name, SortDirection.DESC));
|
||||||
|
});
|
||||||
|
return sortOptions;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets up a subscription to all necessary parameters to make sure the searchOptions emits a new value every time they update
|
* Sets up a subscription to all necessary parameters to make sure the searchOptions emits a new value every time they update
|
||||||
* @param {SearchOptions} defaults Default values for when no parameters are available
|
* @param {SearchOptions} defaults Default values for when no parameters are available
|
||||||
|
@@ -0,0 +1,76 @@
|
|||||||
|
import { autoserialize, deserialize } from 'cerialize';
|
||||||
|
|
||||||
|
import { SEARCH_CONFIG } from './search-config.resource-type';
|
||||||
|
import { typedObject } from '../../../cache/builders/build-decorators';
|
||||||
|
import { CacheableObject } from '../../../cache/object-cache.reducer';
|
||||||
|
import { HALLink } from '../../hal-link.model';
|
||||||
|
import { ResourceType } from '../../resource-type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The configuration for a search
|
||||||
|
*/
|
||||||
|
@typedObject
|
||||||
|
export class SearchConfig implements CacheableObject {
|
||||||
|
static type = SEARCH_CONFIG;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id of this search configuration.
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The configured filters.
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
filters: FilterConfig[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The configured sort options.
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
sortOptions: SortOption[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The object type.
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
type: ResourceType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link HALLink}s for this Item
|
||||||
|
*/
|
||||||
|
@deserialize
|
||||||
|
_links: {
|
||||||
|
facets: HALLink;
|
||||||
|
objects: HALLink;
|
||||||
|
self: HALLink;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface to model filter's configuration.
|
||||||
|
*/
|
||||||
|
export interface FilterConfig {
|
||||||
|
filter: string;
|
||||||
|
hasFacets: boolean;
|
||||||
|
operators: OperatorConfig[];
|
||||||
|
openByDefault: boolean;
|
||||||
|
pageSize: number;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface to model sort option's configuration.
|
||||||
|
*/
|
||||||
|
export interface SortOption {
|
||||||
|
name: string;
|
||||||
|
sortOrder: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface to model operator's configuration.
|
||||||
|
*/
|
||||||
|
export interface OperatorConfig {
|
||||||
|
operator: string;
|
||||||
|
}
|
@@ -0,0 +1,9 @@
|
|||||||
|
import {ResourceType} from '../../resource-type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The resource type for SearchConfig
|
||||||
|
*
|
||||||
|
* Needs to be in a separate file to prevent circular
|
||||||
|
* dependencies in webpack.
|
||||||
|
*/
|
||||||
|
export const SEARCH_CONFIG = new ResourceType('discover');
|
@@ -240,5 +240,55 @@ describe('SearchService', () => {
|
|||||||
expect((searchService as any).requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({ href: requestUrl }), true);
|
expect((searchService as any).requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({ href: requestUrl }), true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when getSearchConfigurationFor is called without a scope', () => {
|
||||||
|
const endPoint = 'http://endpoint.com/test/config';
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(observableOf(endPoint));
|
||||||
|
spyOn((searchService as any).rdb, 'buildFromHref').and.callThrough();
|
||||||
|
/* tslint:disable:no-empty */
|
||||||
|
searchService.getSearchConfigurationFor(null).subscribe((t) => {
|
||||||
|
}); // subscribe to make sure all methods are called
|
||||||
|
/* tslint:enable:no-empty */
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call getEndpoint on the halService', () => {
|
||||||
|
expect((searchService as any).halService.getEndpoint).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send out the request on the request service', () => {
|
||||||
|
expect((searchService as any).requestService.send).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call send containing a request with the correct request url', () => {
|
||||||
|
expect((searchService as any).requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({ href: endPoint }), true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when getSearchConfigurationFor is called with a scope', () => {
|
||||||
|
const endPoint = 'http://endpoint.com/test/config';
|
||||||
|
const scope = 'test';
|
||||||
|
const requestUrl = endPoint + '?scope=' + scope;
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(observableOf(endPoint));
|
||||||
|
/* tslint:disable:no-empty */
|
||||||
|
searchService.getSearchConfigurationFor(scope).subscribe((t) => {
|
||||||
|
}); // subscribe to make sure all methods are called
|
||||||
|
/* tslint:enable:no-empty */
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call getEndpoint on the halService', () => {
|
||||||
|
expect((searchService as any).halService.getEndpoint).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send out the request on the request service', () => {
|
||||||
|
expect((searchService as any).requestService.send).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call send containing a request with the correct request url', () => {
|
||||||
|
expect((searchService as any).requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({ href: requestUrl }), true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -37,6 +37,7 @@ import { ListableObject } from '../../../shared/object-collection/shared/listabl
|
|||||||
import { getSearchResultFor } from '../../../shared/search/search-result-element-decorator';
|
import { getSearchResultFor } from '../../../shared/search/search-result-element-decorator';
|
||||||
import { FacetConfigResponse } from '../../../shared/search/facet-config-response.model';
|
import { FacetConfigResponse } from '../../../shared/search/facet-config-response.model';
|
||||||
import { FacetValues } from '../../../shared/search/facet-values.model';
|
import { FacetValues } from '../../../shared/search/facet-values.model';
|
||||||
|
import { SearchConfig } from './search-filters/search-config.model';
|
||||||
import { PaginationService } from '../../pagination/pagination.service';
|
import { PaginationService } from '../../pagination/pagination.service';
|
||||||
import { SearchConfigurationService } from './search-configuration.service';
|
import { SearchConfigurationService } from './search-configuration.service';
|
||||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||||
@@ -46,6 +47,12 @@ import { PaginationComponentOptions } from '../../../shared/pagination/paginatio
|
|||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SearchService implements OnDestroy {
|
export class SearchService implements OnDestroy {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Endpoint link path for retrieving search configurations
|
||||||
|
*/
|
||||||
|
private configurationLinkPath = 'discover/search';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Endpoint link path for retrieving general search results
|
* Endpoint link path for retrieving general search results
|
||||||
*/
|
*/
|
||||||
@@ -229,15 +236,7 @@ export class SearchService implements OnDestroy {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private getConfigUrl(url: string, scope?: string, configurationName?: string) {
|
||||||
* Request the filter configuration for a given scope or the whole repository
|
|
||||||
* @param {string} scope UUID of the object for which config the filter config is requested, when no scope is provided the configuration for the whole repository is loaded
|
|
||||||
* @param {string} configurationName the name of the configuration
|
|
||||||
* @returns {Observable<RemoteData<SearchFilterConfig[]>>} The found filter configuration
|
|
||||||
*/
|
|
||||||
getConfig(scope?: string, configurationName?: string): Observable<RemoteData<SearchFilterConfig[]>> {
|
|
||||||
const href$ = this.halService.getEndpoint(this.facetLinkPathPrefix).pipe(
|
|
||||||
map((url: string) => {
|
|
||||||
const args: string[] = [];
|
const args: string[] = [];
|
||||||
|
|
||||||
if (isNotEmpty(scope)) {
|
if (isNotEmpty(scope)) {
|
||||||
@@ -253,7 +252,17 @@ export class SearchService implements OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return url;
|
return url;
|
||||||
}),
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request the filter configuration for a given scope or the whole repository
|
||||||
|
* @param {string} scope UUID of the object for which config the filter config is requested, when no scope is provided the configuration for the whole repository is loaded
|
||||||
|
* @param {string} configurationName the name of the configuration
|
||||||
|
* @returns {Observable<RemoteData<SearchFilterConfig[]>>} The found filter configuration
|
||||||
|
*/
|
||||||
|
getConfig(scope?: string, configurationName?: string): Observable<RemoteData<SearchFilterConfig[]>> {
|
||||||
|
const href$ = this.halService.getEndpoint(this.facetLinkPathPrefix).pipe(
|
||||||
|
map((url: string) => this.getConfigUrl(url, scope, configurationName)),
|
||||||
);
|
);
|
||||||
|
|
||||||
href$.pipe(take(1)).subscribe((url: string) => {
|
href$.pipe(take(1)).subscribe((url: string) => {
|
||||||
@@ -398,6 +407,25 @@ export class SearchService implements OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request the search configuration for a given scope or the whole repository
|
||||||
|
* @param {string} scope UUID of the object for which config the filter config is requested, when no scope is provided the configuration for the whole repository is loaded
|
||||||
|
* @param {string} configurationName the name of the configuration
|
||||||
|
* @returns {Observable<RemoteData<SearchConfig[]>>} The found configuration
|
||||||
|
*/
|
||||||
|
getSearchConfigurationFor(scope?: string, configurationName?: string ): Observable<RemoteData<SearchConfig>> {
|
||||||
|
const href$ = this.halService.getEndpoint(this.configurationLinkPath).pipe(
|
||||||
|
map((url: string) => this.getConfigUrl(url, scope, configurationName)),
|
||||||
|
);
|
||||||
|
|
||||||
|
href$.pipe(take(1)).subscribe((url: string) => {
|
||||||
|
const request = new this.request(this.requestService.generateRequestId(), url);
|
||||||
|
this.requestService.send(request, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.rdb.buildFromHref(href$);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {string} The base path to the search page
|
* @returns {string} The base path to the search page
|
||||||
*/
|
*/
|
||||||
|
@@ -11,6 +11,7 @@ import { Store } from '@ngrx/store';
|
|||||||
import { CoreState } from '../../core/core.reducers';
|
import { CoreState } from '../../core/core.reducers';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { EPerson } from '../../core/eperson/models/eperson.model';
|
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||||
|
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-forgot-password-form',
|
selector: 'ds-forgot-password-form',
|
||||||
@@ -70,7 +71,9 @@ export class ForgotPasswordFormComponent {
|
|||||||
*/
|
*/
|
||||||
submit() {
|
submit() {
|
||||||
if (!this.isInValid) {
|
if (!this.isInValid) {
|
||||||
this.ePersonDataService.patchPasswordWithToken(this.user, this.token, this.password).subscribe((response: RemoteData<EPerson>) => {
|
this.ePersonDataService.patchPasswordWithToken(this.user, this.token, this.password).pipe(
|
||||||
|
getFirstCompletedRemoteData()
|
||||||
|
).subscribe((response: RemoteData<EPerson>) => {
|
||||||
if (response.hasSucceeded) {
|
if (response.hasSucceeded) {
|
||||||
this.notificationsService.success(
|
this.notificationsService.success(
|
||||||
this.translateService.instant(this.NOTIFICATIONS_PREFIX + '.success.title'),
|
this.translateService.instant(this.NOTIFICATIONS_PREFIX + '.success.title'),
|
||||||
|
@@ -5,3 +5,7 @@
|
|||||||
position: sticky;
|
position: sticky;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host {
|
||||||
|
z-index: var(--ds-nav-z-index);
|
||||||
|
}
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { BrowserKlaroService, COOKIE_MDFIELD } from './browser-klaro.service';
|
import { BrowserKlaroService, COOKIE_MDFIELD } from './browser-klaro.service';
|
||||||
import { getMockTranslateService } from '../mocks/translate.service.mock';
|
import { getMockTranslateService } from '../mocks/translate.service.mock';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
@@ -13,7 +12,7 @@ import { getTestScheduler } from 'jasmine-marbles';
|
|||||||
import { MetadataValue } from '../../core/shared/metadata.models';
|
import { MetadataValue } from '../../core/shared/metadata.models';
|
||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
|
|
||||||
xdescribe('BrowserKlaroService', () => {
|
describe('BrowserKlaroService', () => {
|
||||||
let translateService;
|
let translateService;
|
||||||
let ePersonService;
|
let ePersonService;
|
||||||
let authService;
|
let authService;
|
||||||
@@ -81,7 +80,7 @@ xdescribe('BrowserKlaroService', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
apps: [{
|
services: [{
|
||||||
name: appName,
|
name: appName,
|
||||||
purposes: [purpose]
|
purposes: [purpose]
|
||||||
}],
|
}],
|
||||||
|
@@ -21,8 +21,6 @@ import { createPaginatedList } from '../../../../testing/utils.test';
|
|||||||
import { ExternalSourceService } from '../../../../../core/data/external-source.service';
|
import { ExternalSourceService } from '../../../../../core/data/external-source.service';
|
||||||
import { LookupRelationService } from '../../../../../core/data/lookup-relation.service';
|
import { LookupRelationService } from '../../../../../core/data/lookup-relation.service';
|
||||||
import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service';
|
||||||
import { SubmissionService } from '../../../../../submission/submission.service';
|
|
||||||
import { SubmissionObjectDataService } from '../../../../../core/submission/submission-object-data.service';
|
|
||||||
import { WorkspaceItem } from '../../../../../core/submission/models/workspaceitem.model';
|
import { WorkspaceItem } from '../../../../../core/submission/models/workspaceitem.model';
|
||||||
import { Collection } from '../../../../../core/shared/collection.model';
|
import { Collection } from '../../../../../core/shared/collection.model';
|
||||||
|
|
||||||
@@ -46,8 +44,6 @@ describe('DsDynamicLookupRelationModalComponent', () => {
|
|||||||
let lookupRelationService;
|
let lookupRelationService;
|
||||||
let rdbService;
|
let rdbService;
|
||||||
let submissionId;
|
let submissionId;
|
||||||
let submissionService;
|
|
||||||
let submissionObjectDataService;
|
|
||||||
|
|
||||||
const externalSources = [
|
const externalSources = [
|
||||||
Object.assign(new ExternalSource(), {
|
Object.assign(new ExternalSource(), {
|
||||||
@@ -99,12 +95,6 @@ describe('DsDynamicLookupRelationModalComponent', () => {
|
|||||||
rdbService = jasmine.createSpyObj('rdbService', {
|
rdbService = jasmine.createSpyObj('rdbService', {
|
||||||
aggregate: createSuccessfulRemoteDataObject$(externalSources)
|
aggregate: createSuccessfulRemoteDataObject$(externalSources)
|
||||||
});
|
});
|
||||||
submissionService = jasmine.createSpyObj('SubmissionService', {
|
|
||||||
dispatchSave: jasmine.createSpy('dispatchSave')
|
|
||||||
});
|
|
||||||
submissionObjectDataService = jasmine.createSpyObj('SubmissionObjectDataService', {
|
|
||||||
findById: createSuccessfulRemoteDataObject$(testWSI)
|
|
||||||
});
|
|
||||||
submissionId = '1234';
|
submissionId = '1234';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,8 +119,6 @@ describe('DsDynamicLookupRelationModalComponent', () => {
|
|||||||
},
|
},
|
||||||
{ provide: RelationshipTypeService, useValue: {} },
|
{ provide: RelationshipTypeService, useValue: {} },
|
||||||
{ provide: RemoteDataBuildService, useValue: rdbService },
|
{ provide: RemoteDataBuildService, useValue: rdbService },
|
||||||
{ provide: SubmissionService, useValue: submissionService },
|
|
||||||
{ provide: SubmissionObjectDataService, useValue: submissionObjectDataService },
|
|
||||||
{
|
{
|
||||||
provide: Store, useValue: {
|
provide: Store, useValue: {
|
||||||
// tslint:disable-next-line:no-empty
|
// tslint:disable-next-line:no-empty
|
||||||
|
@@ -12,11 +12,8 @@ import { RelationshipOptions } from '../../models/relationship-options.model';
|
|||||||
import { SearchResult } from '../../../../search/search-result.model';
|
import { SearchResult } from '../../../../search/search-result.model';
|
||||||
import { Item } from '../../../../../core/shared/item.model';
|
import { Item } from '../../../../../core/shared/item.model';
|
||||||
import {
|
import {
|
||||||
getAllSucceededRemoteData,
|
AddRelationshipAction, RemoveRelationshipAction, UpdateRelationshipNameVariantAction,
|
||||||
getAllSucceededRemoteDataPayload,
|
} from './relationship.actions';
|
||||||
getRemoteDataPayload
|
|
||||||
} from '../../../../../core/shared/operators';
|
|
||||||
import { AddRelationshipAction, RemoveRelationshipAction, UpdateRelationshipNameVariantAction } from './relationship.actions';
|
|
||||||
import { RelationshipService } from '../../../../../core/data/relationship.service';
|
import { RelationshipService } from '../../../../../core/data/relationship.service';
|
||||||
import { RelationshipTypeService } from '../../../../../core/data/relationship-type.service';
|
import { RelationshipTypeService } from '../../../../../core/data/relationship-type.service';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
@@ -27,12 +24,7 @@ import { ExternalSource } from '../../../../../core/shared/external-source.model
|
|||||||
import { ExternalSourceService } from '../../../../../core/data/external-source.service';
|
import { ExternalSourceService } from '../../../../../core/data/external-source.service';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service';
|
||||||
import { followLink } from '../../../../utils/follow-link-config.model';
|
import { getAllSucceededRemoteDataPayload } from '../../../../../core/shared/operators';
|
||||||
import { SubmissionObject } from '../../../../../core/submission/models/submission-object.model';
|
|
||||||
import { Collection } from '../../../../../core/shared/collection.model';
|
|
||||||
import { SubmissionService } from '../../../../../submission/submission.service';
|
|
||||||
import { SubmissionObjectDataService } from '../../../../../core/submission/submission-object-data.service';
|
|
||||||
import { RemoteData } from '../../../../../core/data/remote-data';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-dynamic-lookup-relation-modal',
|
selector: 'ds-dynamic-lookup-relation-modal',
|
||||||
@@ -122,10 +114,6 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
|
|||||||
*/
|
*/
|
||||||
totalExternal$: Observable<number[]>;
|
totalExternal$: Observable<number[]>;
|
||||||
|
|
||||||
/**
|
|
||||||
* List of subscriptions to unsubscribe from
|
|
||||||
*/
|
|
||||||
private subs: Subscription[] = [];
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public modal: NgbActiveModal,
|
public modal: NgbActiveModal,
|
||||||
@@ -136,17 +124,14 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
|
|||||||
private lookupRelationService: LookupRelationService,
|
private lookupRelationService: LookupRelationService,
|
||||||
private searchConfigService: SearchConfigurationService,
|
private searchConfigService: SearchConfigurationService,
|
||||||
private rdbService: RemoteDataBuildService,
|
private rdbService: RemoteDataBuildService,
|
||||||
private submissionService: SubmissionService,
|
|
||||||
private submissionObjectService: SubmissionObjectDataService,
|
|
||||||
private zone: NgZone,
|
private zone: NgZone,
|
||||||
private store: Store<AppState>,
|
private store: Store<AppState>,
|
||||||
private router: Router
|
private router: Router,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.setItem();
|
|
||||||
this.selection$ = this.selectableListService
|
this.selection$ = this.selectableListService
|
||||||
.getSelectableList(this.listId)
|
.getSelectableList(this.listId)
|
||||||
.pipe(map((listState: SelectableListState) => hasValue(listState) && hasValue(listState.selection) ? listState.selection : []));
|
.pipe(map((listState: SelectableListState) => hasValue(listState) && hasValue(listState.selection) ? listState.selection : []));
|
||||||
@@ -206,24 +191,6 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize this.item$ based on this.model.submissionId
|
|
||||||
*/
|
|
||||||
private setItem() {
|
|
||||||
const submissionObject$ = this.submissionObjectService
|
|
||||||
.findById(this.submissionId, true, true, followLink('item'), followLink('collection')).pipe(
|
|
||||||
getAllSucceededRemoteData(),
|
|
||||||
getRemoteDataPayload()
|
|
||||||
);
|
|
||||||
|
|
||||||
const item$ = submissionObject$.pipe(switchMap((submissionObject: SubmissionObject) => (submissionObject.item as Observable<RemoteData<Item>>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload())));
|
|
||||||
const collection$ = submissionObject$.pipe(switchMap((submissionObject: SubmissionObject) => (submissionObject.collection as Observable<RemoteData<Collection>>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload())));
|
|
||||||
|
|
||||||
this.subs.push(item$.subscribe((item) => this.item = item));
|
|
||||||
this.subs.push(collection$.subscribe((collection) => this.collection = collection));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a subscription updating relationships with name variants
|
* Add a subscription updating relationships with name variants
|
||||||
* @param sri The search result to track name variants for
|
* @param sri The search result to track name variants for
|
||||||
@@ -279,8 +246,5 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
|
|||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
this.router.navigate([], {});
|
this.router.navigate([], {});
|
||||||
Object.values(this.subMap).forEach((subscription) => subscription.unsubscribe());
|
Object.values(this.subMap).forEach((subscription) => subscription.unsubscribe());
|
||||||
this.subs
|
|
||||||
.filter((sub) => hasValue(sub))
|
|
||||||
.forEach((sub) => sub.unsubscribe());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,10 +1,7 @@
|
|||||||
import { TestBed, waitForAsync } from '@angular/core/testing';
|
import { TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
|
||||||
import { BehaviorSubject, Observable, of as observableOf } from 'rxjs';
|
import { BehaviorSubject, Observable, of as observableOf } from 'rxjs';
|
||||||
import { TestScheduler } from 'rxjs/testing';
|
|
||||||
import { provideMockActions } from '@ngrx/effects/testing';
|
import { provideMockActions } from '@ngrx/effects/testing';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
|
|
||||||
import { RelationshipEffects } from './relationship.effects';
|
import { RelationshipEffects } from './relationship.effects';
|
||||||
import { AddRelationshipAction, RelationshipActionTypes, RemoveRelationshipAction } from './relationship.actions';
|
import { AddRelationshipAction, RelationshipActionTypes, RemoveRelationshipAction } from './relationship.actions';
|
||||||
import { Item } from '../../../../../core/shared/item.model';
|
import { Item } from '../../../../../core/shared/item.model';
|
||||||
@@ -23,6 +20,9 @@ import { RequestService } from '../../../../../core/data/request.service';
|
|||||||
import { NotificationsService } from '../../../../notifications/notifications.service';
|
import { NotificationsService } from '../../../../notifications/notifications.service';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { SelectableListService } from '../../../../object-list/selectable-list/selectable-list.service';
|
import { SelectableListService } from '../../../../object-list/selectable-list/selectable-list.service';
|
||||||
|
import { cold, hot } from 'jasmine-marbles';
|
||||||
|
import { DEBOUNCE_TIME_OPERATOR } from '../../../../../core/shared/operators';
|
||||||
|
import { last } from 'rxjs/operators';
|
||||||
|
|
||||||
describe('RelationshipEffects', () => {
|
describe('RelationshipEffects', () => {
|
||||||
let relationEffects: RelationshipEffects;
|
let relationEffects: RelationshipEffects;
|
||||||
@@ -51,7 +51,6 @@ describe('RelationshipEffects', () => {
|
|||||||
let notificationsService;
|
let notificationsService;
|
||||||
let translateService;
|
let translateService;
|
||||||
let selectableListService;
|
let selectableListService;
|
||||||
let testScheduler: TestScheduler;
|
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
testUUID1 = '20e24c2f-a00a-467c-bdee-c929e79bf08d';
|
testUUID1 = '20e24c2f-a00a-467c-bdee-c929e79bf08d';
|
||||||
@@ -131,6 +130,7 @@ describe('RelationshipEffects', () => {
|
|||||||
{ provide: NotificationsService, useValue: notificationsService },
|
{ provide: NotificationsService, useValue: notificationsService },
|
||||||
{ provide: TranslateService, useValue: translateService },
|
{ provide: TranslateService, useValue: translateService },
|
||||||
{ provide: SelectableListService, useValue: selectableListService },
|
{ provide: SelectableListService, useValue: selectableListService },
|
||||||
|
{ provide: DEBOUNCE_TIME_OPERATOR, useValue: jasmine.createSpy('debounceTime').and.returnValue((v) => v.pipe(last())) },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
@@ -140,9 +140,6 @@ describe('RelationshipEffects', () => {
|
|||||||
identifier = (relationEffects as any).createIdentifier(leftItem, rightItem, relationshipType.leftwardType);
|
identifier = (relationEffects as any).createIdentifier(leftItem, rightItem, relationshipType.leftwardType);
|
||||||
spyOn((relationEffects as any), 'addRelationship').and.stub();
|
spyOn((relationEffects as any), 'addRelationship').and.stub();
|
||||||
spyOn((relationEffects as any), 'removeRelationship').and.stub();
|
spyOn((relationEffects as any), 'removeRelationship').and.stub();
|
||||||
testScheduler = new TestScheduler((actual, expected) => {
|
|
||||||
expect(actual).toEqual(expected);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('mapLastActions$', () => {
|
describe('mapLastActions$', () => {
|
||||||
@@ -151,15 +148,13 @@ describe('RelationshipEffects', () => {
|
|||||||
let action;
|
let action;
|
||||||
|
|
||||||
it('should set the current value debounceMap and the value of the initialActionMap to ADD_RELATIONSHIP', () => {
|
it('should set the current value debounceMap and the value of the initialActionMap to ADD_RELATIONSHIP', () => {
|
||||||
testScheduler.run(({ hot, expectObservable, flush }) => {
|
|
||||||
action = new AddRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234');
|
action = new AddRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234');
|
||||||
actions = hot('--a-|', { a: action });
|
actions = hot('--a-|', { a: action });
|
||||||
expectObservable(relationEffects.mapLastActions$).toBe('--b-|', { b: undefined });
|
const expected = cold('--b-|', { b: undefined });
|
||||||
flush();
|
expect(relationEffects.mapLastActions$).toBeObservable(expected);
|
||||||
// TODO check following expectations with the implementation
|
|
||||||
// expect((relationEffects as any).initialActionMap[identifier]).toBe(action.type);
|
expect((relationEffects as any).initialActionMap[identifier]).toBe(action.type);
|
||||||
// expect((relationEffects as any).debounceMap[identifier].value).toBe(action.type);
|
expect((relationEffects as any).debounceMap[identifier].value).toBe(action.type);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -172,65 +167,59 @@ describe('RelationshipEffects', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should set the current value debounceMap to ADD_RELATIONSHIP but not change the value of the initialActionMap', () => {
|
it('should set the current value debounceMap to ADD_RELATIONSHIP but not change the value of the initialActionMap', () => {
|
||||||
testScheduler.run(({ hot, expectObservable, flush }) => {
|
|
||||||
action = new AddRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234');
|
action = new AddRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234');
|
||||||
actions = hot('--a-|', { a: action });
|
actions = hot('--a-|', { a: action });
|
||||||
expectObservable(relationEffects.mapLastActions$).toBe('--b-|', { b: undefined });
|
|
||||||
flush();
|
const expected = cold('--b-|', { b: undefined });
|
||||||
|
expect(relationEffects.mapLastActions$).toBeObservable(expected);
|
||||||
|
|
||||||
expect((relationEffects as any).initialActionMap[identifier]).toBe(testActionType);
|
expect((relationEffects as any).initialActionMap[identifier]).toBe(testActionType);
|
||||||
expect((relationEffects as any).debounceMap[identifier].value).toBe(action.type);
|
expect((relationEffects as any).debounceMap[identifier].value).toBe(action.type);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe('When the initialActionMap contains an ADD_RELATIONSHIP action', () => {
|
describe('When the initialActionMap contains an ADD_RELATIONSHIP action', () => {
|
||||||
let action;
|
let action;
|
||||||
describe('When the last value in the debounceMap is also an ADD_RELATIONSHIP action', () => {
|
describe('When the last value in the debounceMap is also an ADD_RELATIONSHIP action', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
(relationEffects as any).initialActionMap[identifier] = RelationshipActionTypes.ADD_RELATIONSHIP;
|
(relationEffects as any).initialActionMap[identifier] = RelationshipActionTypes.ADD_RELATIONSHIP;
|
||||||
|
((relationEffects as any).debounceTime as jasmine.Spy).and.returnValue((v) => v);
|
||||||
});
|
});
|
||||||
it('should call addRelationship on the effect', () => {
|
it('should call addRelationship on the effect', () => {
|
||||||
testScheduler.run(({ hot, expectObservable, flush }) => {
|
|
||||||
action = new AddRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234');
|
action = new AddRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234');
|
||||||
actions = hot('--a-|', { a: action });
|
actions = hot('--a-|', { a: action });
|
||||||
expectObservable(relationEffects.mapLastActions$).toBe('--b-|', { b: undefined });
|
const expected = cold('--b-|', { b: undefined });
|
||||||
flush();
|
expect(relationEffects.mapLastActions$).toBeObservable(expected);
|
||||||
expect((relationEffects as any).addRelationship).toHaveBeenCalledWith(leftItem, rightItem, relationshipType.leftwardType, '1234', undefined);
|
expect((relationEffects as any).addRelationship).toHaveBeenCalledWith(leftItem, rightItem, relationshipType.leftwardType, '1234', undefined);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe('When the last value in the debounceMap is instead a REMOVE_RELATIONSHIP action', () => {
|
describe('When the last value in the debounceMap is instead a REMOVE_RELATIONSHIP action', () => {
|
||||||
|
|
||||||
it('should <b>not</b> call removeRelationship or addRelationship on the effect', () => {
|
it('should <b>not</b> call removeRelationship or addRelationship on the effect', () => {
|
||||||
testScheduler.run(({ hot, expectObservable, flush }) => {
|
|
||||||
const actiona = new AddRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234');
|
const actiona = new AddRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234');
|
||||||
const actionb = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234');
|
const actionb = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234');
|
||||||
actions = hot('--ab-|', { a: actiona, b: actionb });
|
actions = hot('--ab-|', { a: actiona, b: actionb });
|
||||||
expectObservable(relationEffects.mapLastActions$).toBe('--bb-|', { b: undefined });
|
const expected = cold('--bb-|', { b: undefined });
|
||||||
flush();
|
expect(relationEffects.mapLastActions$).toBeObservable(expected);
|
||||||
expect((relationEffects as any).addRelationship).not.toHaveBeenCalled();
|
expect((relationEffects as any).addRelationship).not.toHaveBeenCalled();
|
||||||
expect((relationEffects as any).removeRelationship).not.toHaveBeenCalled();
|
expect((relationEffects as any).removeRelationship).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe('When an REMOVE_RELATIONSHIP action is triggered', () => {
|
describe('When an REMOVE_RELATIONSHIP action is triggered', () => {
|
||||||
describe('When it\'s the first time for this identifier', () => {
|
describe('When it\'s the first time for this identifier', () => {
|
||||||
let action;
|
let action;
|
||||||
|
|
||||||
it('should set the current value debounceMap and the value of the initialActionMap to REMOVE_RELATIONSHIP', () => {
|
it('should set the current value debounceMap and the value of the initialActionMap to REMOVE_RELATIONSHIP', () => {
|
||||||
testScheduler.run(({ hot, expectObservable, flush }) => {
|
|
||||||
action = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234');
|
action = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234');
|
||||||
actions = hot('--a-|', { a: action });
|
actions = hot('--a-|', { a: action });
|
||||||
expectObservable(relationEffects.mapLastActions$).toBe('--b-|', { b: undefined });
|
const expected = cold('--b-|', { b: undefined });
|
||||||
flush();
|
expect(relationEffects.mapLastActions$).toBeObservable(expected);
|
||||||
// TODO check following expectations with the implementation
|
|
||||||
// expect((relationEffects as any).initialActionMap[identifier]).toBe(action.type);
|
expect((relationEffects as any).initialActionMap[identifier]).toBe(action.type);
|
||||||
// expect((relationEffects as any).debounceMap[identifier].value).toBe(action.type);
|
expect((relationEffects as any).debounceMap[identifier].value).toBe(action.type);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -238,20 +227,19 @@ describe('RelationshipEffects', () => {
|
|||||||
let action;
|
let action;
|
||||||
const testActionType = 'TEST_TYPE';
|
const testActionType = 'TEST_TYPE';
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
((relationEffects as any).debounceTime as jasmine.Spy).and.returnValue((v) => v);
|
||||||
(relationEffects as any).initialActionMap[identifier] = testActionType;
|
(relationEffects as any).initialActionMap[identifier] = testActionType;
|
||||||
(relationEffects as any).debounceMap[identifier] = new BehaviorSubject<string>(testActionType);
|
(relationEffects as any).debounceMap[identifier] = new BehaviorSubject<string>(testActionType);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set the current value debounceMap to REMOVE_RELATIONSHIP but not change the value of the initialActionMap', () => {
|
it('should set the current value debounceMap to REMOVE_RELATIONSHIP but not change the value of the initialActionMap', () => {
|
||||||
testScheduler.run(({ hot, expectObservable, flush }) => {
|
|
||||||
action = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234');
|
action = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234');
|
||||||
actions = hot('--a-|', { a: action });
|
actions = hot('--a-|', { a: action });
|
||||||
expectObservable(relationEffects.mapLastActions$).toBe('--b-|', { b: undefined });
|
const expected = cold('--b-|', { b: undefined });
|
||||||
flush();
|
expect(relationEffects.mapLastActions$).toBeObservable(expected);
|
||||||
// TODO check following expectations with the implementation
|
|
||||||
// expect((relationEffects as any).initialActionMap[identifier]).toBe(testActionType);
|
expect((relationEffects as any).initialActionMap[identifier]).toBe(testActionType);
|
||||||
// expect((relationEffects as any).debounceMap[identifier].value).toBe(action.type);
|
expect((relationEffects as any).debounceMap[identifier].value).toBe(action.type);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -259,29 +247,26 @@ describe('RelationshipEffects', () => {
|
|||||||
let action;
|
let action;
|
||||||
describe('When the last value in the debounceMap is also an REMOVE_RELATIONSHIP action', () => {
|
describe('When the last value in the debounceMap is also an REMOVE_RELATIONSHIP action', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
((relationEffects as any).debounceTime as jasmine.Spy).and.returnValue((v) => v);
|
||||||
(relationEffects as any).initialActionMap[identifier] = RelationshipActionTypes.REMOVE_RELATIONSHIP;
|
(relationEffects as any).initialActionMap[identifier] = RelationshipActionTypes.REMOVE_RELATIONSHIP;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call removeRelationship on the effect', () => {
|
it('should call removeRelationship on the effect', () => {
|
||||||
testScheduler.run(({ hot, expectObservable, flush }) => {
|
|
||||||
action = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234');
|
action = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234');
|
||||||
actions = hot('a', { a: action });
|
actions = hot('--a-|', { a: action });
|
||||||
expectObservable(relationEffects.mapLastActions$).toBe('b', { b: undefined });
|
const expected = cold('--b-|', { b: undefined });
|
||||||
flush();
|
expect(relationEffects.mapLastActions$).toBeObservable(expected);
|
||||||
expect((relationEffects as any).removeRelationship).toHaveBeenCalledWith(leftItem, rightItem, relationshipType.leftwardType, '1234',);
|
expect((relationEffects as any).removeRelationship).toHaveBeenCalledWith(leftItem, rightItem, relationshipType.leftwardType, '1234',);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe('When the last value in the debounceMap is instead a ADD_RELATIONSHIP action', () => {
|
describe('When the last value in the debounceMap is instead a ADD_RELATIONSHIP action', () => {
|
||||||
|
|
||||||
it('should <b>not</b> call addRelationship or removeRelationship on the effect', () => {
|
it('should <b>not</b> call addRelationship or removeRelationship on the effect', () => {
|
||||||
testScheduler.run(({ hot, expectObservable, flush }) => {
|
|
||||||
const actionb = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234');
|
const actionb = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234');
|
||||||
const actiona = new AddRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234');
|
const actiona = new AddRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234');
|
||||||
actions = hot('--ab-|', { a: actiona, b: actionb });
|
actions = hot('--ab-|', { a: actiona, b: actionb });
|
||||||
expectObservable(relationEffects.mapLastActions$).toBe('--bb-|', { b: undefined });
|
const expected = cold('--bb-|', { b: undefined });
|
||||||
flush();
|
expect(relationEffects.mapLastActions$).toBeObservable(expected);
|
||||||
expect((relationEffects as any).addRelationship).not.toHaveBeenCalled();
|
expect((relationEffects as any).addRelationship).not.toHaveBeenCalled();
|
||||||
expect((relationEffects as any).removeRelationship).not.toHaveBeenCalled();
|
expect((relationEffects as any).removeRelationship).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -289,5 +274,4 @@ describe('RelationshipEffects', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Inject, Injectable } from '@angular/core';
|
||||||
import { Actions, Effect, ofType } from '@ngrx/effects';
|
import { Actions, Effect, ofType } from '@ngrx/effects';
|
||||||
import { debounceTime, filter, map, mergeMap, switchMap, take } from 'rxjs/operators';
|
import { filter, map, mergeMap, switchMap, take } from 'rxjs/operators';
|
||||||
import { BehaviorSubject, Observable } from 'rxjs';
|
import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
import { RelationshipService } from '../../../../../core/data/relationship.service';
|
import { RelationshipService } from '../../../../../core/data/relationship.service';
|
||||||
import {
|
import {
|
||||||
getRemoteDataPayload,
|
getRemoteDataPayload,
|
||||||
getFirstSucceededRemoteData
|
getFirstSucceededRemoteData, DEBOUNCE_TIME_OPERATOR
|
||||||
} from '../../../../../core/shared/operators';
|
} from '../../../../../core/shared/operators';
|
||||||
import {
|
import {
|
||||||
AddRelationshipAction,
|
AddRelationshipAction,
|
||||||
@@ -71,7 +71,7 @@ export class RelationshipEffects {
|
|||||||
this.initialActionMap[identifier] = action.type;
|
this.initialActionMap[identifier] = action.type;
|
||||||
this.debounceMap[identifier] = new BehaviorSubject<string>(action.type);
|
this.debounceMap[identifier] = new BehaviorSubject<string>(action.type);
|
||||||
this.debounceMap[identifier].pipe(
|
this.debounceMap[identifier].pipe(
|
||||||
debounceTime(DEBOUNCE_TIME),
|
this.debounceTime(DEBOUNCE_TIME),
|
||||||
take(1)
|
take(1)
|
||||||
).subscribe(
|
).subscribe(
|
||||||
(type) => {
|
(type) => {
|
||||||
@@ -159,6 +159,7 @@ export class RelationshipEffects {
|
|||||||
private notificationsService: NotificationsService,
|
private notificationsService: NotificationsService,
|
||||||
private translateService: TranslateService,
|
private translateService: TranslateService,
|
||||||
private selectableListService: SelectableListService,
|
private selectableListService: SelectableListService,
|
||||||
|
@Inject(DEBOUNCE_TIME_OPERATOR) private debounceTime: <T>(dueTime: number) => (source: Observable<T>) => Observable<T>,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,15 +1,15 @@
|
|||||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
import { ChangeDetectionStrategy, ComponentFactoryResolver, NO_ERRORS_SCHEMA } from '@angular/core';
|
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { Context } from '../../core/shared/context.model';
|
import { Context } from '../../core/shared/context.model';
|
||||||
import {
|
import {
|
||||||
MetadataRepresentation,
|
MetadataRepresentation,
|
||||||
MetadataRepresentationType
|
MetadataRepresentationType
|
||||||
} from '../../core/shared/metadata-representation/metadata-representation.model';
|
} from '../../core/shared/metadata-representation/metadata-representation.model';
|
||||||
import { MetadataRepresentationLoaderComponent } from './metadata-representation-loader.component';
|
import { MetadataRepresentationLoaderComponent } from './metadata-representation-loader.component';
|
||||||
import { PlainTextMetadataListElementComponent } from '../object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component';
|
|
||||||
import { spyOnExported } from '../testing/utils.test';
|
|
||||||
import { MetadataRepresentationDirective } from './metadata-representation.directive';
|
import { MetadataRepresentationDirective } from './metadata-representation.directive';
|
||||||
import * as metadataRepresentationDecorator from './metadata-representation.decorator';
|
import { METADATA_REPRESENTATION_COMPONENT_FACTORY } from './metadata-representation.decorator';
|
||||||
|
import { ThemeService } from '../theme-support/theme.service';
|
||||||
|
import { PlainTextMetadataListElementComponent } from '../object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component';
|
||||||
|
|
||||||
const testType = 'TestType';
|
const testType = 'TestType';
|
||||||
const testContext = Context.Search;
|
const testContext = Context.Search;
|
||||||
@@ -29,16 +29,30 @@ class TestType implements MetadataRepresentation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
xdescribe('MetadataRepresentationLoaderComponent', () => {
|
describe('MetadataRepresentationLoaderComponent', () => {
|
||||||
let comp: MetadataRepresentationLoaderComponent;
|
let comp: MetadataRepresentationLoaderComponent;
|
||||||
let fixture: ComponentFixture<MetadataRepresentationLoaderComponent>;
|
let fixture: ComponentFixture<MetadataRepresentationLoaderComponent>;
|
||||||
|
let themeService: ThemeService;
|
||||||
|
const themeName = 'test-theme';
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
|
themeService = jasmine.createSpyObj('themeService', {
|
||||||
|
getThemeName: themeName,
|
||||||
|
});
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [],
|
imports: [],
|
||||||
declarations: [MetadataRepresentationLoaderComponent, PlainTextMetadataListElementComponent, MetadataRepresentationDirective],
|
declarations: [MetadataRepresentationLoaderComponent, PlainTextMetadataListElementComponent, MetadataRepresentationDirective],
|
||||||
schemas: [NO_ERRORS_SCHEMA],
|
schemas: [NO_ERRORS_SCHEMA],
|
||||||
providers: [ComponentFactoryResolver]
|
providers: [
|
||||||
|
{
|
||||||
|
provide: METADATA_REPRESENTATION_COMPONENT_FACTORY,
|
||||||
|
useValue: jasmine.createSpy('getMetadataRepresentationComponent').and.returnValue(PlainTextMetadataListElementComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: ThemeService,
|
||||||
|
useValue: themeService,
|
||||||
|
}
|
||||||
|
]
|
||||||
}).overrideComponent(MetadataRepresentationLoaderComponent, {
|
}).overrideComponent(MetadataRepresentationLoaderComponent, {
|
||||||
set: {
|
set: {
|
||||||
changeDetection: ChangeDetectionStrategy.Default,
|
changeDetection: ChangeDetectionStrategy.Default,
|
||||||
@@ -53,15 +67,12 @@ xdescribe('MetadataRepresentationLoaderComponent', () => {
|
|||||||
|
|
||||||
comp.mdRepresentation = new TestType();
|
comp.mdRepresentation = new TestType();
|
||||||
comp.context = testContext;
|
comp.context = testContext;
|
||||||
|
|
||||||
spyOnExported(metadataRepresentationDecorator, 'getMetadataRepresentationComponent').and.returnValue(PlainTextMetadataListElementComponent);
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('When the component is rendered', () => {
|
describe('When the component is rendered', () => {
|
||||||
it('should call the getMetadataRepresentationComponent function with the right entity type, representation type and context', () => {
|
it('should call the getMetadataRepresentationComponent function with the right entity type, representation type and context', () => {
|
||||||
expect(metadataRepresentationDecorator.getMetadataRepresentationComponent).toHaveBeenCalledWith(testType, testRepresentationType, testContext);
|
expect((comp as any).getMetadataRepresentationComponent).toHaveBeenCalledWith(testType, testRepresentationType, testContext, themeName);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,6 +1,9 @@
|
|||||||
import { Component, ComponentFactoryResolver, Input, OnInit, ViewChild } from '@angular/core';
|
import { Component, ComponentFactoryResolver, Inject, Input, OnInit, ViewChild } from '@angular/core';
|
||||||
import { MetadataRepresentation } from '../../core/shared/metadata-representation/metadata-representation.model';
|
import {
|
||||||
import { getMetadataRepresentationComponent } from './metadata-representation.decorator';
|
MetadataRepresentation,
|
||||||
|
MetadataRepresentationType
|
||||||
|
} from '../../core/shared/metadata-representation/metadata-representation.model';
|
||||||
|
import { METADATA_REPRESENTATION_COMPONENT_FACTORY } from './metadata-representation.decorator';
|
||||||
import { Context } from '../../core/shared/context.model';
|
import { Context } from '../../core/shared/context.model';
|
||||||
import { GenericConstructor } from '../../core/shared/generic-constructor';
|
import { GenericConstructor } from '../../core/shared/generic-constructor';
|
||||||
import { MetadataRepresentationListElementComponent } from '../object-list/metadata-representation-list-element/metadata-representation-list-element.component';
|
import { MetadataRepresentationListElementComponent } from '../object-list/metadata-representation-list-element/metadata-representation-list-element.component';
|
||||||
@@ -45,7 +48,8 @@ export class MetadataRepresentationLoaderComponent implements OnInit {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private componentFactoryResolver: ComponentFactoryResolver,
|
private componentFactoryResolver: ComponentFactoryResolver,
|
||||||
private themeService: ThemeService
|
private themeService: ThemeService,
|
||||||
|
@Inject(METADATA_REPRESENTATION_COMPONENT_FACTORY) private getMetadataRepresentationComponent: (entityType: string, mdRepresentationType: MetadataRepresentationType, context: Context, theme: string) => GenericConstructor<any>,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +72,6 @@ export class MetadataRepresentationLoaderComponent implements OnInit {
|
|||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
private getComponent(): GenericConstructor<MetadataRepresentationListElementComponent> {
|
private getComponent(): GenericConstructor<MetadataRepresentationListElementComponent> {
|
||||||
return getMetadataRepresentationComponent(this.mdRepresentation.itemType, this.mdRepresentation.representationType, this.context, this.themeService.getThemeName());
|
return this.getMetadataRepresentationComponent(this.mdRepresentation.itemType, this.mdRepresentation.representationType, this.context, this.themeService.getThemeName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,13 @@
|
|||||||
import { MetadataRepresentationType } from '../../core/shared/metadata-representation/metadata-representation.model';
|
import { MetadataRepresentationType } from '../../core/shared/metadata-representation/metadata-representation.model';
|
||||||
import { hasNoValue, hasValue } from '../empty.util';
|
import { hasNoValue, hasValue } from '../empty.util';
|
||||||
import { Context } from '../../core/shared/context.model';
|
import { Context } from '../../core/shared/context.model';
|
||||||
|
import { InjectionToken } from '@angular/core';
|
||||||
|
import { GenericConstructor } from '../../core/shared/generic-constructor';
|
||||||
|
|
||||||
|
export const METADATA_REPRESENTATION_COMPONENT_FACTORY = new InjectionToken<(entityType: string, mdRepresentationType: MetadataRepresentationType, context: Context, theme: string) => GenericConstructor<any>>('getMetadataRepresentationComponent', {
|
||||||
|
providedIn: 'root',
|
||||||
|
factory: () => getMetadataRepresentationComponent
|
||||||
|
});
|
||||||
|
|
||||||
export const map = new Map();
|
export const map = new Map();
|
||||||
|
|
||||||
|
@@ -97,8 +97,14 @@ export class EndpointMockingRestService extends DspaceRestService {
|
|||||||
* the mock response if there is one, undefined otherwise
|
* the mock response if there is one, undefined otherwise
|
||||||
*/
|
*/
|
||||||
private getMockData(urlStr: string): any {
|
private getMockData(urlStr: string): any {
|
||||||
|
let key;
|
||||||
|
if (this.mockResponseMap.has(urlStr)) {
|
||||||
|
key = urlStr;
|
||||||
|
} else {
|
||||||
|
// didn't find an exact match for the url, try to match only the endpoint without namespace and parameters
|
||||||
const url = new URL(urlStr);
|
const url = new URL(urlStr);
|
||||||
const key = url.pathname.slice(environment.rest.nameSpace.length);
|
key = url.pathname.slice(environment.rest.nameSpace.length);
|
||||||
|
}
|
||||||
if (this.mockResponseMap.has(key)) {
|
if (this.mockResponseMap.has(key)) {
|
||||||
// parse and stringify to clone the object to ensure that any changes made
|
// parse and stringify to clone the object to ensure that any changes made
|
||||||
// to it afterwards don't affect future calls
|
// to it afterwards don't affect future calls
|
||||||
|
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"_embedded": {
|
||||||
|
"authorizations": [
|
||||||
|
{
|
||||||
|
"id": "cd824a61-95be-4e16-bccd-51fea26707d0_canManageBitstreams_core.item_96715576-3748-4761-ad45-001646632963",
|
||||||
|
"type": "authorization",
|
||||||
|
"_links": {
|
||||||
|
"eperson": {
|
||||||
|
"href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageBitstreams_core.item_96715576-3748-4761-ad45-001646632963/eperson"
|
||||||
|
},
|
||||||
|
"feature": {
|
||||||
|
"href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageBitstreams_core.item_96715576-3748-4761-ad45-001646632963/feature"
|
||||||
|
},
|
||||||
|
"object": {
|
||||||
|
"href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageBitstreams_core.item_96715576-3748-4761-ad45-001646632963/object"
|
||||||
|
},
|
||||||
|
"self": {
|
||||||
|
"href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageBitstreams_core.item_96715576-3748-4761-ad45-001646632963"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_embedded": {
|
||||||
|
"feature": {
|
||||||
|
"id": "canManageBitstreams",
|
||||||
|
"description": "It can be used to verify if the bitstreams of the specified objects can be managed",
|
||||||
|
"type": "feature",
|
||||||
|
"resourcetypes": [
|
||||||
|
"core.item",
|
||||||
|
"core.bundle"
|
||||||
|
],
|
||||||
|
"_links": {
|
||||||
|
"self": {
|
||||||
|
"href": "https://api7.dspace.org/server/api/authz/features/canManageBitstreams"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"_links": {
|
||||||
|
"self": {
|
||||||
|
"href": "https://api7.dspace.org/server/api/authz/authorizations/search/object?uri=https://api7.dspace.org/server/api/core/items/96715576-3748-4761-ad45-001646632963&feature=canManageBitstreams"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"size": 20,
|
||||||
|
"totalElements": 1,
|
||||||
|
"totalPages": 1,
|
||||||
|
"number": 0
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"_embedded": {
|
||||||
|
"authorizations": [
|
||||||
|
{
|
||||||
|
"id": "cd824a61-95be-4e16-bccd-51fea26707d0_canManageMappings_core.item_e98b0f27-5c19-49a0-960d-eb6ad5287067",
|
||||||
|
"type": "authorization",
|
||||||
|
"_links": {
|
||||||
|
"eperson": {
|
||||||
|
"href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageMappings_core.item_e98b0f27-5c19-49a0-960d-eb6ad5287067/eperson"
|
||||||
|
},
|
||||||
|
"feature": {
|
||||||
|
"href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageMappings_core.item_e98b0f27-5c19-49a0-960d-eb6ad5287067/feature"
|
||||||
|
},
|
||||||
|
"object": {
|
||||||
|
"href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageMappings_core.item_e98b0f27-5c19-49a0-960d-eb6ad5287067/object"
|
||||||
|
},
|
||||||
|
"self": {
|
||||||
|
"href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageMappings_core.item_e98b0f27-5c19-49a0-960d-eb6ad5287067"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_embedded": {
|
||||||
|
"feature": {
|
||||||
|
"id": "canManageMappings",
|
||||||
|
"description": "It can be used to verify if the mappings of the specified objects can be managed",
|
||||||
|
"type": "feature",
|
||||||
|
"resourcetypes": [
|
||||||
|
"core.item"
|
||||||
|
],
|
||||||
|
"_links": {
|
||||||
|
"self": {
|
||||||
|
"href": "https://api7.dspace.org/server/api/authz/features/canManageMappings"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"_links": {
|
||||||
|
"self": {
|
||||||
|
"href": "https://api7.dspace.org/server/api/authz/authorizations/search/object?uri=https://api7.dspace.org/server/api/core/items/e98b0f27-5c19-49a0-960d-eb6ad5287067&feature=canManageMappings"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"size": 20,
|
||||||
|
"totalElements": 1,
|
||||||
|
"totalPages": 1,
|
||||||
|
"number": 0
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"_embedded": {
|
||||||
|
"authorizations": [
|
||||||
|
{
|
||||||
|
"id": "cd824a61-95be-4e16-bccd-51fea26707d0_canManageRelationships_core.item_047556d1-3d01-4c53-bc68-0cee7ad7ed4e",
|
||||||
|
"type": "authorization",
|
||||||
|
"_links": {
|
||||||
|
"eperson": {
|
||||||
|
"href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageRelationships_core.item_047556d1-3d01-4c53-bc68-0cee7ad7ed4e/eperson"
|
||||||
|
},
|
||||||
|
"feature": {
|
||||||
|
"href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageRelationships_core.item_047556d1-3d01-4c53-bc68-0cee7ad7ed4e/feature"
|
||||||
|
},
|
||||||
|
"object": {
|
||||||
|
"href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageRelationships_core.item_047556d1-3d01-4c53-bc68-0cee7ad7ed4e/object"
|
||||||
|
},
|
||||||
|
"self": {
|
||||||
|
"href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageRelationships_core.item_047556d1-3d01-4c53-bc68-0cee7ad7ed4e"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_embedded": {
|
||||||
|
"feature": {
|
||||||
|
"id": "canManageRelationships",
|
||||||
|
"description": "It can be used to verify if the relationships of the specified objects can be managed",
|
||||||
|
"type": "feature",
|
||||||
|
"resourcetypes": [
|
||||||
|
"core.item"
|
||||||
|
],
|
||||||
|
"_links": {
|
||||||
|
"self": {
|
||||||
|
"href": "https://api7.dspace.org/server/api/authz/features/canManageRelationships"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"_links": {
|
||||||
|
"self": {
|
||||||
|
"href": "https://api7.dspace.org/server/api/authz/authorizations/search/object?uri=https://api7.dspace.org/server/api/core/items/047556d1-3d01-4c53-bc68-0cee7ad7ed4e&feature=canManageRelationships"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"size": 20,
|
||||||
|
"totalElements": 1,
|
||||||
|
"totalPages": 1,
|
||||||
|
"number": 0
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"_embedded": {
|
||||||
|
"authorizations": [
|
||||||
|
{
|
||||||
|
"id": "cd824a61-95be-4e16-bccd-51fea26707d0_canManageVersions_core.item_e98b0f27-5c19-49a0-960d-eb6ad5287067",
|
||||||
|
"type": "authorization",
|
||||||
|
"_links": {
|
||||||
|
"eperson": {
|
||||||
|
"href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageVersions_core.item_e98b0f27-5c19-49a0-960d-eb6ad5287067/eperson"
|
||||||
|
},
|
||||||
|
"feature": {
|
||||||
|
"href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageVersions_core.item_e98b0f27-5c19-49a0-960d-eb6ad5287067/feature"
|
||||||
|
},
|
||||||
|
"object": {
|
||||||
|
"href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageVersions_core.item_e98b0f27-5c19-49a0-960d-eb6ad5287067/object"
|
||||||
|
},
|
||||||
|
"self": {
|
||||||
|
"href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageVersions_core.item_e98b0f27-5c19-49a0-960d-eb6ad5287067"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_embedded": {
|
||||||
|
"feature": {
|
||||||
|
"id": "canManageVersions",
|
||||||
|
"description": "It can be used to verify if the versions of the specified objects can be managed",
|
||||||
|
"type": "feature",
|
||||||
|
"resourcetypes": [
|
||||||
|
"core.item"
|
||||||
|
],
|
||||||
|
"_links": {
|
||||||
|
"self": {
|
||||||
|
"href": "https://api7.dspace.org/server/api/authz/features/canManageVersions"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"_links": {
|
||||||
|
"self": {
|
||||||
|
"href": "https://api7.dspace.org/server/api/authz/authorizations/search/object?uri=https://api7.dspace.org/server/api/core/items/e98b0f27-5c19-49a0-960d-eb6ad5287067&feature=canManageVersions"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"size": 20,
|
||||||
|
"totalElements": 1,
|
||||||
|
"totalPages": 1,
|
||||||
|
"number": 0
|
||||||
|
}
|
||||||
|
}
|
@@ -2,6 +2,10 @@ import { InjectionToken } from '@angular/core';
|
|||||||
// import mockSubmissionResponse from './mock-submission-response.json';
|
// import mockSubmissionResponse from './mock-submission-response.json';
|
||||||
// import mockPublicationResponse from './mock-publication-response.json';
|
// import mockPublicationResponse from './mock-publication-response.json';
|
||||||
// import mockUntypedItemResponse from './mock-untyped-item-response.json';
|
// import mockUntypedItemResponse from './mock-untyped-item-response.json';
|
||||||
|
import mockFeatureItemCanManageBitstreamsResponse from './mock-feature-item-can-manage-bitstreams-response.json';
|
||||||
|
import mockFeatureItemCanManageRelationshipsResponse from './mock-feature-item-can-manage-relationships-response.json';
|
||||||
|
import mockFeatureItemCanManageVersionsResponse from './mock-feature-item-can-manage-versions-response.json';
|
||||||
|
import mockFeatureItemCanManageMappingsResponse from './mock-feature-item-can-manage-mappings-response.json';
|
||||||
|
|
||||||
export class ResponseMapMock extends Map<string, any> {}
|
export class ResponseMapMock extends Map<string, any> {}
|
||||||
|
|
||||||
@@ -16,4 +20,8 @@ export const mockResponseMap: ResponseMapMock = new Map([
|
|||||||
// [ '/config/submissionforms/traditionalpageone', mockSubmissionResponse ]
|
// [ '/config/submissionforms/traditionalpageone', mockSubmissionResponse ]
|
||||||
// [ '/api/pid/find', mockPublicationResponse ],
|
// [ '/api/pid/find', mockPublicationResponse ],
|
||||||
// [ '/api/pid/find', mockUntypedItemResponse ],
|
// [ '/api/pid/find', mockUntypedItemResponse ],
|
||||||
|
[ 'https://api7.dspace.org/server/api/authz/authorizations/search/object?uri=https://api7.dspace.org/server/api/core/items/96715576-3748-4761-ad45-001646632963&feature=canManageBitstreams&embed=feature', mockFeatureItemCanManageBitstreamsResponse ],
|
||||||
|
[ 'https://api7.dspace.org/server/api/authz/authorizations/search/object?uri=https://api7.dspace.org/server/api/core/items/047556d1-3d01-4c53-bc68-0cee7ad7ed4e&feature=canManageRelationships&embed=feature', mockFeatureItemCanManageRelationshipsResponse ],
|
||||||
|
[ 'https://api7.dspace.org/server/api/authz/authorizations/search/object?uri=https://api7.dspace.org/server/api/core/items/e98b0f27-5c19-49a0-960d-eb6ad5287067&feature=canManageVersions&embed=feature', mockFeatureItemCanManageVersionsResponse ],
|
||||||
|
[ 'https://api7.dspace.org/server/api/authz/authorizations/search/object?uri=https://api7.dspace.org/server/api/core/items/e98b0f27-5c19-49a0-960d-eb6ad5287067&feature=canManageMappings&embed=feature', mockFeatureItemCanManageMappingsResponse ],
|
||||||
]);
|
]);
|
||||||
|
@@ -8,7 +8,12 @@ describe('PaginatedSearchOptions', () => {
|
|||||||
let options: PaginatedSearchOptions;
|
let options: PaginatedSearchOptions;
|
||||||
const sortOptions = new SortOptions('test.field', SortDirection.DESC);
|
const sortOptions = new SortOptions('test.field', SortDirection.DESC);
|
||||||
const pageOptions = Object.assign(new PaginationComponentOptions(), { pageSize: 40, page: 1 });
|
const pageOptions = Object.assign(new PaginationComponentOptions(), { pageSize: 40, page: 1 });
|
||||||
const filters = [new SearchFilter('f.test', ['value']), new SearchFilter('f.example', ['another value', 'second value'])];
|
const filters = [
|
||||||
|
new SearchFilter('f.test', ['value']),
|
||||||
|
new SearchFilter('f.example', ['another value', 'second value']), // should be split into two arguments, spaces should be URI-encoded
|
||||||
|
new SearchFilter('f.range', ['[2002 TO 2021]'], 'equals'), // value should be URI-encoded, ',equals' should not
|
||||||
|
];
|
||||||
|
const fixedFilter = 'f.fixed=1234,5678,equals'; // '=' and ',equals' should not be URI-encoded
|
||||||
const query = 'search query';
|
const query = 'search query';
|
||||||
const scope = '0fde1ecb-82cc-425a-b600-ac3576d76b47';
|
const scope = '0fde1ecb-82cc-425a-b600-ac3576d76b47';
|
||||||
const baseUrl = 'www.rest.com';
|
const baseUrl = 'www.rest.com';
|
||||||
@@ -19,7 +24,8 @@ describe('PaginatedSearchOptions', () => {
|
|||||||
filters: filters,
|
filters: filters,
|
||||||
query: query,
|
query: query,
|
||||||
scope: scope,
|
scope: scope,
|
||||||
dsoTypes: [DSpaceObjectType.ITEM]
|
dsoTypes: [DSpaceObjectType.ITEM],
|
||||||
|
fixedFilter: fixedFilter,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -31,12 +37,14 @@ describe('PaginatedSearchOptions', () => {
|
|||||||
'sort=test.field,DESC&' +
|
'sort=test.field,DESC&' +
|
||||||
'page=0&' +
|
'page=0&' +
|
||||||
'size=40&' +
|
'size=40&' +
|
||||||
'query=search query&' +
|
'f.fixed=1234%2C5678,equals&' +
|
||||||
|
'query=search%20query&' +
|
||||||
'scope=0fde1ecb-82cc-425a-b600-ac3576d76b47&' +
|
'scope=0fde1ecb-82cc-425a-b600-ac3576d76b47&' +
|
||||||
'dsoType=ITEM&' +
|
'dsoType=ITEM&' +
|
||||||
'f.test=value&' +
|
'f.test=value&' +
|
||||||
'f.example=another value&' +
|
'f.example=another%20value&' +
|
||||||
'f.example=second value'
|
'f.example=second%20value&' +
|
||||||
|
'f.range=%5B2002%20TO%202021%5D,equals'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -35,7 +35,7 @@ export class SearchFilterComponent implements OnInit {
|
|||||||
/**
|
/**
|
||||||
* True when the filter is 100% collapsed in the UI
|
* True when the filter is 100% collapsed in the UI
|
||||||
*/
|
*/
|
||||||
closed = true;
|
closed: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emits true when the filter is currently collapsed in the store
|
* Emits true when the filter is currently collapsed in the store
|
||||||
|
@@ -8,18 +8,16 @@
|
|||||||
|
|
||||||
::ng-deep
|
::ng-deep
|
||||||
{
|
{
|
||||||
--ds-slider-handle-width: 18px;
|
|
||||||
|
|
||||||
html:not([dir=rtl]) .noUi-horizontal .noUi-handle {
|
html:not([dir=rtl]) .noUi-horizontal .noUi-handle {
|
||||||
right: calc(var(--ds-slider-handle-width) / -2);
|
right: calc(var(--ds-slider-handle-width) / -2);
|
||||||
}
|
}
|
||||||
.noUi-horizontal .noUi-handle {
|
.noUi-horizontal .noUi-handle {
|
||||||
width: var(--ds-slider-handle-width);
|
width: var(--ds-slider-handle-width);
|
||||||
&:before {
|
&:before {
|
||||||
left: calc(calc(calc(var(--ds-slider-handle-width) - 2) / 2) - 2);
|
left: calc(((var(--ds-slider-handle-width) - 2px) / 2) - 2px);
|
||||||
}
|
}
|
||||||
&:after {
|
&:after {
|
||||||
left: calc(calc(calc(var(--ds-slider-handle-width) - 2) / 2) + 2);
|
left: calc(((var(--ds-slider-handle-width) - 2px) / 2) + 2px);
|
||||||
}
|
}
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
|
@@ -56,7 +56,7 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple
|
|||||||
/**
|
/**
|
||||||
* Fallback maximum for the range
|
* Fallback maximum for the range
|
||||||
*/
|
*/
|
||||||
max = 2018;
|
max = new Date().getFullYear();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current range of the filter
|
* The current range of the filter
|
||||||
|
@@ -4,13 +4,25 @@ import { SearchFilter } from './search-filter.model';
|
|||||||
import { DSpaceObjectType } from '../../core/shared/dspace-object-type.model';
|
import { DSpaceObjectType } from '../../core/shared/dspace-object-type.model';
|
||||||
|
|
||||||
describe('SearchOptions', () => {
|
describe('SearchOptions', () => {
|
||||||
let options: PaginatedSearchOptions;
|
let options: SearchOptions;
|
||||||
const filters = [new SearchFilter('f.test', ['value']), new SearchFilter('f.example', ['another value', 'second value'])];
|
|
||||||
|
const filters = [
|
||||||
|
new SearchFilter('f.test', ['value']),
|
||||||
|
new SearchFilter('f.example', ['another value', 'second value']), // should be split into two arguments, spaces should be URI-encoded
|
||||||
|
new SearchFilter('f.range', ['[2002 TO 2021]'], 'equals'), // value should be URI-encoded, ',equals' should not
|
||||||
|
];
|
||||||
|
const fixedFilter = 'f.fixed=1234,5678,equals'; // '=' and ',equals' should not be URI-encoded
|
||||||
const query = 'search query';
|
const query = 'search query';
|
||||||
const scope = '0fde1ecb-82cc-425a-b600-ac3576d76b47';
|
const scope = '0fde1ecb-82cc-425a-b600-ac3576d76b47';
|
||||||
const baseUrl = 'www.rest.com';
|
const baseUrl = 'www.rest.com';
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
options = new SearchOptions({ filters: filters, query: query, scope: scope, dsoTypes: [DSpaceObjectType.ITEM] });
|
options = new SearchOptions({
|
||||||
|
filters: filters,
|
||||||
|
query: query,
|
||||||
|
scope: scope,
|
||||||
|
dsoTypes: [DSpaceObjectType.ITEM],
|
||||||
|
fixedFilter: fixedFilter,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when toRestUrl is called', () => {
|
describe('when toRestUrl is called', () => {
|
||||||
@@ -18,12 +30,14 @@ describe('SearchOptions', () => {
|
|||||||
it('should generate a string with all parameters that are present', () => {
|
it('should generate a string with all parameters that are present', () => {
|
||||||
const outcome = options.toRestUrl(baseUrl);
|
const outcome = options.toRestUrl(baseUrl);
|
||||||
expect(outcome).toEqual('www.rest.com?' +
|
expect(outcome).toEqual('www.rest.com?' +
|
||||||
'query=search query&' +
|
'f.fixed=1234%2C5678,equals&' +
|
||||||
|
'query=search%20query&' +
|
||||||
'scope=0fde1ecb-82cc-425a-b600-ac3576d76b47&' +
|
'scope=0fde1ecb-82cc-425a-b600-ac3576d76b47&' +
|
||||||
'dsoType=ITEM&' +
|
'dsoType=ITEM&' +
|
||||||
'f.test=value&' +
|
'f.test=value&' +
|
||||||
'f.example=another value&' +
|
'f.example=another%20value&' +
|
||||||
'f.example=second value'
|
'f.example=second%20value&' +
|
||||||
|
'f.range=%5B2002%20TO%202021%5D,equals'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { isNotEmpty } from '../empty.util';
|
import { hasValue, isNotEmpty } from '../empty.util';
|
||||||
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
||||||
import { SearchFilter } from './search-filter.model';
|
import { SearchFilter } from './search-filter.model';
|
||||||
import { DSpaceObjectType } from '../../core/shared/dspace-object-type.model';
|
import { DSpaceObjectType } from '../../core/shared/dspace-object-type.model';
|
||||||
@@ -13,10 +13,15 @@ export class SearchOptions {
|
|||||||
scope?: string;
|
scope?: string;
|
||||||
query?: string;
|
query?: string;
|
||||||
dsoTypes?: DSpaceObjectType[];
|
dsoTypes?: DSpaceObjectType[];
|
||||||
filters?: any;
|
filters?: SearchFilter[];
|
||||||
fixedFilter?: any;
|
fixedFilter?: string;
|
||||||
|
|
||||||
constructor(options: {configuration?: string, scope?: string, query?: string, dsoTypes?: DSpaceObjectType[], filters?: SearchFilter[], fixedFilter?: any}) {
|
constructor(
|
||||||
|
options: {
|
||||||
|
configuration?: string, scope?: string, query?: string, dsoTypes?: DSpaceObjectType[], filters?: SearchFilter[],
|
||||||
|
fixedFilter?: string
|
||||||
|
}
|
||||||
|
) {
|
||||||
this.configuration = options.configuration;
|
this.configuration = options.configuration;
|
||||||
this.scope = options.scope;
|
this.scope = options.scope;
|
||||||
this.query = options.query;
|
this.query = options.query;
|
||||||
@@ -33,27 +38,27 @@ export class SearchOptions {
|
|||||||
*/
|
*/
|
||||||
toRestUrl(url: string, args: string[] = []): string {
|
toRestUrl(url: string, args: string[] = []): string {
|
||||||
if (isNotEmpty(this.configuration)) {
|
if (isNotEmpty(this.configuration)) {
|
||||||
args.push(`configuration=${this.configuration}`);
|
args.push(`configuration=${encodeURIComponent(this.configuration)}`);
|
||||||
}
|
}
|
||||||
if (isNotEmpty(this.fixedFilter)) {
|
if (isNotEmpty(this.fixedFilter)) {
|
||||||
args.push(this.fixedFilter);
|
args.push(this.encodedFixedFilter);
|
||||||
}
|
}
|
||||||
if (isNotEmpty(this.query)) {
|
if (isNotEmpty(this.query)) {
|
||||||
args.push(`query=${this.query}`);
|
args.push(`query=${encodeURIComponent(this.query)}`);
|
||||||
}
|
}
|
||||||
if (isNotEmpty(this.scope)) {
|
if (isNotEmpty(this.scope)) {
|
||||||
args.push(`scope=${this.scope}`);
|
args.push(`scope=${encodeURIComponent(this.scope)}`);
|
||||||
}
|
}
|
||||||
if (isNotEmpty(this.dsoTypes)) {
|
if (isNotEmpty(this.dsoTypes)) {
|
||||||
this.dsoTypes.forEach((dsoType: string) => {
|
this.dsoTypes.forEach((dsoType: string) => {
|
||||||
args.push(`dsoType=${dsoType}`);
|
args.push(`dsoType=${encodeURIComponent(dsoType)}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (isNotEmpty(this.filters)) {
|
if (isNotEmpty(this.filters)) {
|
||||||
this.filters.forEach((filter: SearchFilter) => {
|
this.filters.forEach((filter: SearchFilter) => {
|
||||||
filter.values.forEach((value) => {
|
filter.values.forEach((value) => {
|
||||||
const filterValue = value.includes(',') ? `${value}` : value + (filter.operator ? ',' + filter.operator : '');
|
const filterValue = value.includes(',') ? `${value}` : value + (filter.operator ? ',' + filter.operator : '');
|
||||||
args.push(`${filter.key}=${filterValue}`);
|
args.push(`${filter.key}=${this.encodeFilterQueryValue(filterValue)}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -62,4 +67,28 @@ export class SearchOptions {
|
|||||||
}
|
}
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get encodedFixedFilter(): string {
|
||||||
|
// expected format: 'arg=value'
|
||||||
|
// -> split the query agument into (arg=)(value) and only encode 'value'
|
||||||
|
const match = this.fixedFilter.match(/^([^=]+=)(.+)$/);
|
||||||
|
|
||||||
|
if (hasValue(match)) {
|
||||||
|
return match[1] + this.encodeFilterQueryValue(match[2]);
|
||||||
|
} else {
|
||||||
|
return this.encodeFilterQueryValue(this.fixedFilter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
encodeFilterQueryValue(filterQueryValue: string): string {
|
||||||
|
// expected format: 'value' or 'value,operator'
|
||||||
|
// -> split into (value)(,operator) and only encode 'value'
|
||||||
|
const match = filterQueryValue.match(/^(.*)(,\w+)$/);
|
||||||
|
|
||||||
|
if (hasValue(match)) {
|
||||||
|
return encodeURIComponent(match[1]) + match[2];
|
||||||
|
} else {
|
||||||
|
return encodeURIComponent(filterQueryValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
<ng-container *ngVar="(searchOptions$ | async) as config">
|
<ng-container *ngVar="searchOptions as config">
|
||||||
<h3>{{ 'search.sidebar.settings.title' | translate}}</h3>
|
<h3>{{ 'search.sidebar.settings.title' | translate}}</h3>
|
||||||
<div class="result-order-settings">
|
<div class="result-order-settings">
|
||||||
<ds-sidebar-dropdown
|
<ds-sidebar-dropdown
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
[label]="'search.sidebar.settings.sort-by'"
|
[label]="'search.sidebar.settings.sort-by'"
|
||||||
(change)="reloadOrder($event)"
|
(change)="reloadOrder($event)"
|
||||||
>
|
>
|
||||||
<option *ngFor="let sortOption of searchOptionPossibilities"
|
<option *ngFor="let sortOption of sortOptions"
|
||||||
[value]="sortOption.field + ',' + sortOption.direction.toString()"
|
[value]="sortOption.field + ',' + sortOption.direction.toString()"
|
||||||
[selected]="sortOption.field === config?.sort.field && sortOption.direction === (config?.sort.direction)? 'selected': null">
|
[selected]="sortOption.field === config?.sort.field && sortOption.direction === (config?.sort.direction)? 'selected': null">
|
||||||
{{'sorting.' + sortOption.field + '.' + sortOption.direction | translate}}
|
{{'sorting.' + sortOption.field + '.' + sortOption.direction | translate}}
|
||||||
|
@@ -12,7 +12,6 @@ import { EnumKeysPipe } from '../../utils/enum-keys-pipe';
|
|||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { SearchFilterService } from '../../../core/shared/search/search-filter.service';
|
import { SearchFilterService } from '../../../core/shared/search/search-filter.service';
|
||||||
import { VarDirective } from '../../utils/var.directive';
|
import { VarDirective } from '../../utils/var.directive';
|
||||||
import { take } from 'rxjs/operators';
|
|
||||||
import { SEARCH_CONFIG_SERVICE } from '../../../+my-dspace-page/my-dspace-page.component';
|
import { SEARCH_CONFIG_SERVICE } from '../../../+my-dspace-page/my-dspace-page.component';
|
||||||
import { SidebarService } from '../../sidebar/sidebar.service';
|
import { SidebarService } from '../../sidebar/sidebar.service';
|
||||||
import { SidebarServiceStub } from '../../testing/sidebar-service.stub';
|
import { SidebarServiceStub } from '../../testing/sidebar-service.stub';
|
||||||
@@ -91,7 +90,7 @@ describe('SearchSettingsComponent', () => {
|
|||||||
provide: SEARCH_CONFIG_SERVICE,
|
provide: SEARCH_CONFIG_SERVICE,
|
||||||
useValue: {
|
useValue: {
|
||||||
paginatedSearchOptions: observableOf(paginatedSearchOptions),
|
paginatedSearchOptions: observableOf(paginatedSearchOptions),
|
||||||
getCurrentScope: observableOf('test-id')
|
getCurrentScope: observableOf('test-id'),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -103,6 +102,14 @@ describe('SearchSettingsComponent', () => {
|
|||||||
fixture = TestBed.createComponent(SearchSettingsComponent);
|
fixture = TestBed.createComponent(SearchSettingsComponent);
|
||||||
comp = fixture.componentInstance;
|
comp = fixture.componentInstance;
|
||||||
|
|
||||||
|
comp.sortOptions = [
|
||||||
|
new SortOptions('score', SortDirection.DESC),
|
||||||
|
new SortOptions('dc.title', SortDirection.ASC),
|
||||||
|
new SortOptions('dc.title', SortDirection.DESC)
|
||||||
|
];
|
||||||
|
|
||||||
|
comp.searchOptions = paginatedSearchOptions;
|
||||||
|
|
||||||
// SearchPageComponent test instance
|
// SearchPageComponent test instance
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
searchServiceObject = (comp as any).service;
|
searchServiceObject = (comp as any).service;
|
||||||
@@ -111,34 +118,24 @@ describe('SearchSettingsComponent', () => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('it should show the order settings with the respective selectable options', (done) => {
|
it('it should show the order settings with the respective selectable options', () => {
|
||||||
(comp as any).searchOptions$.pipe(take(1)).subscribe((options) => {
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
const orderSetting = fixture.debugElement.query(By.css('div.result-order-settings'));
|
const orderSetting = fixture.debugElement.query(By.css('div.result-order-settings'));
|
||||||
expect(orderSetting).toBeDefined();
|
expect(orderSetting).toBeDefined();
|
||||||
const childElements = orderSetting.queryAll(By.css('option'));
|
const childElements = orderSetting.queryAll(By.css('option'));
|
||||||
expect(childElements.length).toEqual(comp.searchOptionPossibilities.length);
|
expect(childElements.length).toEqual(comp.sortOptions.length);
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('it should show the size settings', (done) => {
|
it('it should show the size settings', () => {
|
||||||
(comp as any).searchOptions$.pipe(take(1)).subscribe((options) => {
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
const pageSizeSetting = fixture.debugElement.query(By.css('page-size-settings'));
|
const pageSizeSetting = fixture.debugElement.query(By.css('page-size-settings'));
|
||||||
expect(pageSizeSetting).toBeDefined();
|
expect(pageSizeSetting).toBeDefined();
|
||||||
done();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have the proper order value selected by default', (done) => {
|
it('should have the proper order value selected by default', () => {
|
||||||
(comp as any).searchOptions$.pipe(take(1)).subscribe((options) => {
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
const orderSetting = fixture.debugElement.query(By.css('div.result-order-settings'));
|
const orderSetting = fixture.debugElement.query(By.css('div.result-order-settings'));
|
||||||
const childElementToBeSelected = orderSetting.query(By.css('option[value="0"][selected="selected"]'));
|
const childElementToBeSelected = orderSetting.query(By.css('option[value="score,DESC"][selected="selected"]'));
|
||||||
expect(childElementToBeSelected).toBeDefined();
|
expect(childElementToBeSelected).toBeDefined();
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,9 +1,8 @@
|
|||||||
import { Component, Inject, OnInit } from '@angular/core';
|
import { Component, Inject, Input } from '@angular/core';
|
||||||
import { SearchService } from '../../../core/shared/search/search.service';
|
import { SearchService } from '../../../core/shared/search/search.service';
|
||||||
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
|
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { PaginatedSearchOptions } from '../paginated-search-options.model';
|
import { PaginatedSearchOptions } from '../paginated-search-options.model';
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service';
|
import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service';
|
||||||
import { SEARCH_CONFIG_SERVICE } from '../../../+my-dspace-page/my-dspace-page.component';
|
import { SEARCH_CONFIG_SERVICE } from '../../../+my-dspace-page/my-dspace-page.component';
|
||||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||||
@@ -17,16 +16,17 @@ import { PaginationService } from '../../../core/pagination/pagination.service';
|
|||||||
/**
|
/**
|
||||||
* This component represents the part of the search sidebar that contains the general search settings.
|
* This component represents the part of the search sidebar that contains the general search settings.
|
||||||
*/
|
*/
|
||||||
export class SearchSettingsComponent implements OnInit {
|
export class SearchSettingsComponent {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The configuration for the current paginated search results
|
* The configuration for the current paginated search results
|
||||||
*/
|
*/
|
||||||
searchOptions$: Observable<PaginatedSearchOptions>;
|
@Input() searchOptions: PaginatedSearchOptions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All sort options that are shown in the settings
|
* All sort options that are shown in the settings
|
||||||
*/
|
*/
|
||||||
searchOptionPossibilities = [new SortOptions('score', SortDirection.DESC), new SortOptions('dc.title', SortDirection.ASC), new SortOptions('dc.title', SortDirection.DESC)];
|
@Input() sortOptions: SortOptions[];
|
||||||
|
|
||||||
constructor(private service: SearchService,
|
constructor(private service: SearchService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
@@ -35,13 +35,6 @@ export class SearchSettingsComponent implements OnInit {
|
|||||||
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigurationService: SearchConfigurationService) {
|
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigurationService: SearchConfigurationService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize paginated search options
|
|
||||||
*/
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.searchOptions$ = this.searchConfigurationService.paginatedSearchOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to change the current sort field and direction
|
* Method to change the current sort field and direction
|
||||||
* @param {Event} event Change event containing the sort direction and sort field
|
* @param {Event} event Change event containing the sort direction and sort field
|
||||||
|
@@ -12,7 +12,7 @@
|
|||||||
<div class="sidebar-content">
|
<div class="sidebar-content">
|
||||||
<ds-search-switch-configuration [inPlaceSearch]="inPlaceSearch" *ngIf="configurationList" [configurationList]="configurationList"></ds-search-switch-configuration>
|
<ds-search-switch-configuration [inPlaceSearch]="inPlaceSearch" *ngIf="configurationList" [configurationList]="configurationList"></ds-search-switch-configuration>
|
||||||
<ds-search-filters [refreshFilters]="refreshFilters" [inPlaceSearch]="inPlaceSearch"></ds-search-filters>
|
<ds-search-filters [refreshFilters]="refreshFilters" [inPlaceSearch]="inPlaceSearch"></ds-search-filters>
|
||||||
<ds-search-settings></ds-search-settings>
|
<ds-search-settings [searchOptions]="searchOptions" [sortOptions]="sortOptions"></ds-search-settings>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -2,6 +2,8 @@ import { Component, EventEmitter, Input, Output } from '@angular/core';
|
|||||||
|
|
||||||
import { SearchConfigurationOption } from '../search-switch-configuration/search-configuration-option.model';
|
import { SearchConfigurationOption } from '../search-switch-configuration/search-configuration-option.model';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
import { PaginatedSearchOptions } from '../paginated-search-options.model';
|
||||||
|
import { SortOptions } from '../../../core/cache/models/sort-options.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders a simple item page.
|
* This component renders a simple item page.
|
||||||
@@ -45,6 +47,16 @@ export class SearchSidebarComponent {
|
|||||||
*/
|
*/
|
||||||
@Input() inPlaceSearch;
|
@Input() inPlaceSearch;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The configuration for the current paginated search results
|
||||||
|
*/
|
||||||
|
@Input() searchOptions: PaginatedSearchOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All sort options that are shown in the settings
|
||||||
|
*/
|
||||||
|
@Input() sortOptions: SortOptions[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emits when the search filters values may be stale, and so they must be refreshed.
|
* Emits when the search filters values may be stale, and so they must be refreshed.
|
||||||
*/
|
*/
|
||||||
|
@@ -1520,7 +1520,7 @@
|
|||||||
|
|
||||||
"item.edit.breadcrumbs": "Edit Item",
|
"item.edit.breadcrumbs": "Edit Item",
|
||||||
|
|
||||||
"item.edit.tabs.disabled.tooltip": "You don't have permission to access this tab",
|
"item.edit.tabs.disabled.tooltip": "You're not authorized to access this tab",
|
||||||
|
|
||||||
|
|
||||||
"item.edit.tabs.mapper.head": "Collection Mapper",
|
"item.edit.tabs.mapper.head": "Collection Mapper",
|
||||||
@@ -1765,6 +1765,8 @@
|
|||||||
|
|
||||||
"item.edit.tabs.status.buttons.reinstate.label": "Reinstate item into the repository",
|
"item.edit.tabs.status.buttons.reinstate.label": "Reinstate item into the repository",
|
||||||
|
|
||||||
|
"item.edit.tabs.status.buttons.unauthorized": "You're not authorized to perform this action",
|
||||||
|
|
||||||
"item.edit.tabs.status.buttons.withdraw.button": "Withdraw...",
|
"item.edit.tabs.status.buttons.withdraw.button": "Withdraw...",
|
||||||
|
|
||||||
"item.edit.tabs.status.buttons.withdraw.label": "Withdraw item from the repository",
|
"item.edit.tabs.status.buttons.withdraw.label": "Withdraw item from the repository",
|
||||||
@@ -3030,6 +3032,8 @@
|
|||||||
"search.results.empty": "Your search returned no results.",
|
"search.results.empty": "Your search returned no results.",
|
||||||
|
|
||||||
|
|
||||||
|
"default.search.results.head": "Search Results",
|
||||||
|
|
||||||
|
|
||||||
"search.sidebar.close": "Back to results",
|
"search.sidebar.close": "Back to results",
|
||||||
|
|
||||||
@@ -3063,8 +3067,21 @@
|
|||||||
|
|
||||||
"sorting.dc.title.DESC": "Title Descending",
|
"sorting.dc.title.DESC": "Title Descending",
|
||||||
|
|
||||||
"sorting.score.DESC": "Relevance",
|
"sorting.score.ASC": "Least Relevant",
|
||||||
|
|
||||||
|
"sorting.score.DESC": "Most Relevant",
|
||||||
|
|
||||||
|
"sorting.dc.date.issued.ASC": "Date Issued Ascending",
|
||||||
|
|
||||||
|
"sorting.dc.date.issued.DESC": "Date Issued Descending",
|
||||||
|
|
||||||
|
"sorting.dc.date.accessioned.ASC": "Accessioned Date Ascending",
|
||||||
|
|
||||||
|
"sorting.dc.date.accessioned.DESC": "Accessioned Date Descending",
|
||||||
|
|
||||||
|
"sorting.lastModified.ASC": "Last modified Ascending",
|
||||||
|
|
||||||
|
"sorting.lastModified.DESC": "Last modified Descending",
|
||||||
|
|
||||||
|
|
||||||
"statistics.title": "Statistics",
|
"statistics.title": "Statistics",
|
||||||
|
@@ -78,4 +78,5 @@
|
|||||||
--ds-breadcrumb-link-active-color: #{darken($cyan, 30%)};
|
--ds-breadcrumb-link-active-color: #{darken($cyan, 30%)};
|
||||||
|
|
||||||
--ds-slider-color: #{$green};
|
--ds-slider-color: #{$green};
|
||||||
|
--ds-slider-handle-width: 18px;
|
||||||
}
|
}
|
||||||
|
38
yarn.lock
38
yarn.lock
@@ -2657,10 +2657,10 @@ bluebird@^3.5.1, bluebird@^3.5.3, bluebird@^3.5.5:
|
|||||||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
||||||
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
|
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
|
||||||
|
|
||||||
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.4.0:
|
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9:
|
||||||
version "4.11.9"
|
version "4.12.0"
|
||||||
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828"
|
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88"
|
||||||
integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==
|
integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==
|
||||||
|
|
||||||
bn.js@^5.0.0, bn.js@^5.1.1:
|
bn.js@^5.0.0, bn.js@^5.1.1:
|
||||||
version "5.1.3"
|
version "5.1.3"
|
||||||
@@ -2750,7 +2750,7 @@ braces@^3.0.1, braces@^3.0.2, braces@~3.0.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
fill-range "^7.0.1"
|
fill-range "^7.0.1"
|
||||||
|
|
||||||
brorand@^1.0.1:
|
brorand@^1.0.1, brorand@^1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
|
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
|
||||||
integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
|
integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
|
||||||
@@ -4695,17 +4695,17 @@ element-resize-detector@^1.2.1:
|
|||||||
batch-processor "1.0.0"
|
batch-processor "1.0.0"
|
||||||
|
|
||||||
elliptic@^6.5.3:
|
elliptic@^6.5.3:
|
||||||
version "6.5.3"
|
version "6.5.4"
|
||||||
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6"
|
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb"
|
||||||
integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==
|
integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
bn.js "^4.4.0"
|
bn.js "^4.11.9"
|
||||||
brorand "^1.0.1"
|
brorand "^1.1.0"
|
||||||
hash.js "^1.0.0"
|
hash.js "^1.0.0"
|
||||||
hmac-drbg "^1.0.0"
|
hmac-drbg "^1.0.1"
|
||||||
inherits "^2.0.1"
|
inherits "^2.0.4"
|
||||||
minimalistic-assert "^1.0.0"
|
minimalistic-assert "^1.0.1"
|
||||||
minimalistic-crypto-utils "^1.0.0"
|
minimalistic-crypto-utils "^1.0.1"
|
||||||
|
|
||||||
emoji-regex@^7.0.1:
|
emoji-regex@^7.0.1:
|
||||||
version "7.0.3"
|
version "7.0.3"
|
||||||
@@ -5892,7 +5892,7 @@ hex-color-regex@^1.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
|
resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
|
||||||
integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==
|
integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==
|
||||||
|
|
||||||
hmac-drbg@^1.0.0:
|
hmac-drbg@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
|
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
|
||||||
integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=
|
integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=
|
||||||
@@ -7909,7 +7909,7 @@ minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
|
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
|
||||||
integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
|
integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
|
||||||
|
|
||||||
minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
|
minimalistic-crypto-utils@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
|
resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
|
||||||
integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=
|
integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=
|
||||||
@@ -11659,9 +11659,9 @@ sshpk@^1.7.0:
|
|||||||
tweetnacl "~0.14.0"
|
tweetnacl "~0.14.0"
|
||||||
|
|
||||||
ssri@^6.0.0, ssri@^6.0.1:
|
ssri@^6.0.0, ssri@^6.0.1:
|
||||||
version "6.0.1"
|
version "6.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8"
|
resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.2.tgz#157939134f20464e7301ddba3e90ffa8f7728ac5"
|
||||||
integrity sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==
|
integrity sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
figgy-pudding "^3.5.1"
|
figgy-pudding "^3.5.1"
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user