mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +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'
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'new_item_version',
|
||||
parentID: 'new',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.new_item_version',
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
// TODO: enable this menu item once the feature has been implemented
|
||||
// {
|
||||
// id: 'new_item_version',
|
||||
// parentID: 'new',
|
||||
// active: false,
|
||||
// visible: true,
|
||||
// model: {
|
||||
// type: MenuItemType.LINK,
|
||||
// text: 'menu.section.new_item_version',
|
||||
// link: ''
|
||||
// } as LinkMenuItemModel,
|
||||
// },
|
||||
|
||||
/* Edit */
|
||||
{
|
||||
@@ -243,47 +244,35 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
} 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 */
|
||||
{
|
||||
id: 'statistics_task',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.statistics_task',
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
icon: 'chart-bar',
|
||||
index: 8
|
||||
},
|
||||
// TODO: enable this menu item once the feature has been implemented
|
||||
// {
|
||||
// id: 'statistics_task',
|
||||
// active: false,
|
||||
// visible: true,
|
||||
// model: {
|
||||
// type: MenuItemType.LINK,
|
||||
// text: 'menu.section.statistics_task',
|
||||
// link: ''
|
||||
// } as LinkMenuItemModel,
|
||||
// icon: 'chart-bar',
|
||||
// index: 8
|
||||
// },
|
||||
|
||||
/* Control Panel */
|
||||
{
|
||||
id: 'control_panel',
|
||||
active: false,
|
||||
visible: isSiteAdmin,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.control_panel',
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
icon: 'cogs',
|
||||
index: 9
|
||||
},
|
||||
// TODO: enable this menu item once the feature has been implemented
|
||||
// {
|
||||
// id: 'control_panel',
|
||||
// active: false,
|
||||
// visible: isSiteAdmin,
|
||||
// model: {
|
||||
// type: MenuItemType.LINK,
|
||||
// text: 'menu.section.control_panel',
|
||||
// link: ''
|
||||
// } as LinkMenuItemModel,
|
||||
// icon: 'cogs',
|
||||
// index: 9
|
||||
// },
|
||||
|
||||
/* Processes */
|
||||
{
|
||||
@@ -324,42 +313,45 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
index: 3,
|
||||
shouldPersistOnRouteChange: true
|
||||
},
|
||||
{
|
||||
id: 'export_community',
|
||||
parentID: 'export',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.export_community',
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
shouldPersistOnRouteChange: true
|
||||
},
|
||||
{
|
||||
id: 'export_collection',
|
||||
parentID: 'export',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.export_collection',
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
shouldPersistOnRouteChange: true
|
||||
},
|
||||
{
|
||||
id: 'export_item',
|
||||
parentID: 'export',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.export_item',
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
shouldPersistOnRouteChange: true
|
||||
},
|
||||
// TODO: enable this menu item once the feature has been implemented
|
||||
// {
|
||||
// id: 'export_community',
|
||||
// parentID: 'export',
|
||||
// active: false,
|
||||
// visible: true,
|
||||
// model: {
|
||||
// type: MenuItemType.LINK,
|
||||
// text: 'menu.section.export_community',
|
||||
// link: ''
|
||||
// } as LinkMenuItemModel,
|
||||
// shouldPersistOnRouteChange: true
|
||||
// },
|
||||
// TODO: enable this menu item once the feature has been implemented
|
||||
// {
|
||||
// id: 'export_collection',
|
||||
// parentID: 'export',
|
||||
// active: false,
|
||||
// visible: true,
|
||||
// model: {
|
||||
// type: MenuItemType.LINK,
|
||||
// text: 'menu.section.export_collection',
|
||||
// link: ''
|
||||
// } as LinkMenuItemModel,
|
||||
// shouldPersistOnRouteChange: true
|
||||
// },
|
||||
// TODO: enable this menu item once the feature has been implemented
|
||||
// {
|
||||
// id: 'export_item',
|
||||
// parentID: 'export',
|
||||
// active: false,
|
||||
// visible: true,
|
||||
// model: {
|
||||
// type: MenuItemType.LINK,
|
||||
// text: 'menu.section.export_item',
|
||||
// link: ''
|
||||
// } as LinkMenuItemModel,
|
||||
// shouldPersistOnRouteChange: true
|
||||
// },
|
||||
];
|
||||
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, menuSection));
|
||||
|
||||
@@ -406,17 +398,18 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
icon: 'file-import',
|
||||
index: 2
|
||||
},
|
||||
{
|
||||
id: 'import_batch',
|
||||
parentID: 'import',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.import_batch',
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
}
|
||||
// TODO: enable this menu item once the feature has been implemented
|
||||
// {
|
||||
// id: 'import_batch',
|
||||
// parentID: 'import',
|
||||
// active: false,
|
||||
// visible: true,
|
||||
// model: {
|
||||
// type: MenuItemType.LINK,
|
||||
// text: 'menu.section.import_batch',
|
||||
// link: ''
|
||||
// } as LinkMenuItemModel,
|
||||
// }
|
||||
];
|
||||
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, {
|
||||
shouldPersistOnRouteChange: true
|
||||
@@ -563,17 +556,18 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
link: '/access-control/groups'
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'access_control_authorizations',
|
||||
parentID: 'access_control',
|
||||
active: false,
|
||||
visible: authorized,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.access_control_authorizations',
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
// TODO: enable this menu item once the feature has been implemented
|
||||
// {
|
||||
// id: 'access_control_authorizations',
|
||||
// parentID: 'access_control',
|
||||
// active: false,
|
||||
// visible: authorized,
|
||||
// model: {
|
||||
// type: MenuItemType.LINK,
|
||||
// text: 'menu.section.access_control_authorizations',
|
||||
// link: ''
|
||||
// } as LinkMenuItemModel,
|
||||
// },
|
||||
{
|
||||
id: 'access_control',
|
||||
active: false,
|
||||
|
@@ -1,4 +1,6 @@
|
||||
import { hasNoValue } from '../../shared/empty.util';
|
||||
import { InjectionToken } from '@angular/core';
|
||||
import { GenericConstructor } from '../../core/shared/generic-constructor';
|
||||
|
||||
export enum BrowseByType {
|
||||
Title = 'title',
|
||||
@@ -8,6 +10,11 @@ export enum BrowseByType {
|
||||
|
||||
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();
|
||||
|
||||
/**
|
||||
|
@@ -2,12 +2,11 @@ import { BrowseBySwitcherComponent } from './browse-by-switcher.component';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import * as decorator from './browse-by-decorator';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
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 fixture: ComponentFixture<BrowseBySwitcherComponent>;
|
||||
|
||||
@@ -23,7 +22,8 @@ xdescribe('BrowseBySwitcherComponent', () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [BrowseBySwitcherComponent],
|
||||
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]
|
||||
}).compileComponents();
|
||||
@@ -32,7 +32,6 @@ xdescribe('BrowseBySwitcherComponent', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
fixture = TestBed.createComponent(BrowseBySwitcherComponent);
|
||||
comp = fixture.componentInstance;
|
||||
spyOnProperty(decorator, 'getComponentByBrowseByType').and.returnValue(createSpy('getComponentByItemType'));
|
||||
}));
|
||||
|
||||
types.forEach((type) => {
|
||||
@@ -43,7 +42,7 @@ xdescribe('BrowseBySwitcherComponent', () => {
|
||||
});
|
||||
|
||||
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 { Observable } from 'rxjs';
|
||||
import { BrowseByTypeConfig } from '../../../config/browse-by-type-config.interface';
|
||||
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 { GenericConstructor } from '../../core/shared/generic-constructor';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-browse-by-switcher',
|
||||
@@ -20,7 +21,8 @@ export class BrowseBySwitcherComponent implements OnInit {
|
||||
*/
|
||||
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;
|
||||
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 { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
||||
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 { 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
|
||||
*/
|
||||
export class CollectionPageAdministratorGuard extends DsoPageFeatureGuard<Collection> {
|
||||
export class CollectionPageAdministratorGuard extends DsoPageSingleFeatureGuard<Collection> {
|
||||
constructor(protected resolver: CollectionPageResolver,
|
||||
protected authorizationService: AuthorizationDataService,
|
||||
protected router: Router,
|
||||
|
@@ -4,7 +4,7 @@ import { Community } from '../core/shared/community.model';
|
||||
import { CommunityPageResolver } from './community-page.resolver';
|
||||
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
||||
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 { 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
|
||||
*/
|
||||
export class CommunityPageAdministratorGuard extends DsoPageFeatureGuard<Community> {
|
||||
export class CommunityPageAdministratorGuard extends DsoPageSingleFeatureGuard<Community> {
|
||||
constructor(protected resolver: CommunityPageResolver,
|
||||
protected authorizationService: AuthorizationDataService,
|
||||
protected router: Router,
|
||||
|
@@ -31,8 +31,13 @@ import {
|
||||
} from './edit-item-page.routing-paths';
|
||||
import { ItemPageReinstateGuard } from './item-page-reinstate.guard';
|
||||
import { ItemPageWithdrawGuard } from './item-page-withdraw.guard';
|
||||
import { ItemPageEditMetadataGuard } from '../item-page-edit-metadata.guard';
|
||||
import { ItemPageMetadataGuard } from './item-page-metadata.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
|
||||
@@ -60,25 +65,25 @@ import { ItemPageAdministratorGuard } from '../item-page-administrator.guard';
|
||||
path: 'status',
|
||||
component: ItemStatusComponent,
|
||||
data: { title: 'item.edit.tabs.status.title', showBreadcrumbs: true },
|
||||
canActivate: [ItemPageAdministratorGuard]
|
||||
canActivate: [ItemPageStatusGuard]
|
||||
},
|
||||
{
|
||||
path: 'bitstreams',
|
||||
component: ItemBitstreamsComponent,
|
||||
data: { title: 'item.edit.tabs.bitstreams.title', showBreadcrumbs: true },
|
||||
canActivate: [ItemPageAdministratorGuard]
|
||||
canActivate: [ItemPageBitstreamsGuard]
|
||||
},
|
||||
{
|
||||
path: 'metadata',
|
||||
component: ItemMetadataComponent,
|
||||
data: { title: 'item.edit.tabs.metadata.title', showBreadcrumbs: true },
|
||||
canActivate: [ItemPageEditMetadataGuard]
|
||||
canActivate: [ItemPageMetadataGuard]
|
||||
},
|
||||
{
|
||||
path: 'relationships',
|
||||
component: ItemRelationshipsComponent,
|
||||
data: { title: 'item.edit.tabs.relationships.title', showBreadcrumbs: true },
|
||||
canActivate: [ItemPageEditMetadataGuard]
|
||||
canActivate: [ItemPageRelationshipsGuard]
|
||||
},
|
||||
/* TODO - uncomment & fix when view page exists
|
||||
{
|
||||
@@ -96,13 +101,13 @@ import { ItemPageAdministratorGuard } from '../item-page-administrator.guard';
|
||||
path: 'versionhistory',
|
||||
component: ItemVersionHistoryComponent,
|
||||
data: { title: 'item.edit.tabs.versionhistory.title', showBreadcrumbs: true },
|
||||
canActivate: [ItemPageAdministratorGuard]
|
||||
canActivate: [ItemPageVersionHistoryGuard]
|
||||
},
|
||||
{
|
||||
path: 'mapper',
|
||||
component: ItemCollectionMapperComponent,
|
||||
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,
|
||||
ItemPageWithdrawGuard,
|
||||
ItemPageAdministratorGuard,
|
||||
ItemPageEditMetadataGuard,
|
||||
ItemPageMetadataGuard,
|
||||
ItemPageStatusGuard,
|
||||
ItemPageBitstreamsGuard,
|
||||
ItemPageRelationshipsGuard,
|
||||
ItemPageVersionHistoryGuard,
|
||||
ItemPageCollectionMapperGuard,
|
||||
]
|
||||
})
|
||||
export class EditItemPageRoutingModule {
|
||||
|
@@ -3,13 +3,15 @@
|
||||
{{'item.edit.tabs.status.buttons.' + operation.operationKey + '.label' | translate}}
|
||||
</span>
|
||||
</div>
|
||||
<div *ngIf="!operation.disabled" class="col-9 float-left action-button">
|
||||
<a class="btn btn-outline-primary" [routerLink]="operation.operationUrl">
|
||||
{{'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate}}
|
||||
</a>
|
||||
</div>
|
||||
<div *ngIf="operation.disabled" class="col-9 float-left action-button">
|
||||
<span class="btn btn-danger">
|
||||
{{'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate}}
|
||||
<div class="col-9 float-left action-button">
|
||||
<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}}
|
||||
</button>
|
||||
</span>
|
||||
<span *ngIf="!operation.authorized" [ngbTooltip]="'item.edit.tabs.status.buttons.unauthorized' | translate">
|
||||
<button class="btn btn-outline-primary" [disabled]="true">
|
||||
{{'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate}}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
|
@@ -28,19 +28,19 @@ describe('ItemOperationComponent', () => {
|
||||
});
|
||||
|
||||
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');
|
||||
const link = fixture.debugElement.query(By.css('a')).nativeElement;
|
||||
expect(link.href).toContain('url1');
|
||||
expect(link.textContent).toContain('item.edit.tabs.status.buttons.key1.button');
|
||||
const button = fixture.debugElement.query(By.css('button')).nativeElement;
|
||||
expect(button.textContent).toContain('item.edit.tabs.status.buttons.key1.button');
|
||||
});
|
||||
it('should render disabled operation row', () => {
|
||||
itemOperation.setDisabled(true);
|
||||
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');
|
||||
const span2 = fixture.debugElement.query(By.css('span.btn-danger')).nativeElement;
|
||||
expect(span2.textContent).toContain('item.edit.tabs.status.buttons.key1.button');
|
||||
const button = fixture.debugElement.query(By.css('button')).nativeElement;
|
||||
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
|
||||
* when performing the action and an option to disable the operation.
|
||||
@@ -7,11 +9,15 @@ export class ItemOperation {
|
||||
operationKey: string;
|
||||
operationUrl: string;
|
||||
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.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 { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
||||
import { ItemPageResolver } from './item-page.resolver';
|
||||
import { Item } from '../core/shared/item.model';
|
||||
import { DsoPageFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard';
|
||||
import { 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';
|
||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
|
||||
@Injectable({
|
||||
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
|
||||
*/
|
||||
export class ItemPageEditMetadataGuard extends DsoPageFeatureGuard<Item> {
|
||||
export class ItemPageMetadataGuard extends DsoPageSingleFeatureGuard<Item> {
|
||||
constructor(protected resolver: ItemPageResolver,
|
||||
protected authorizationService: AuthorizationDataService,
|
||||
protected router: Router,
|
@@ -1,5 +1,5 @@
|
||||
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 { ItemPageResolver } from '../item-page.resolver';
|
||||
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
|
||||
*/
|
||||
export class ItemPageReinstateGuard extends DsoPageFeatureGuard<Item> {
|
||||
export class ItemPageReinstateGuard extends DsoPageSingleFeatureGuard<Item> {
|
||||
constructor(protected resolver: ItemPageResolver,
|
||||
protected authorizationService: AuthorizationDataService,
|
||||
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 { Injectable } from '@angular/core';
|
||||
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
|
||||
*/
|
||||
export class ItemPageWithdrawGuard extends DsoPageFeatureGuard<Item> {
|
||||
export class ItemPageWithdrawGuard extends DsoPageSingleFeatureGuard<Item> {
|
||||
constructor(protected resolver: ItemPageResolver,
|
||||
protected authorizationService: AuthorizationDataService,
|
||||
protected router: Router,
|
||||
|
@@ -18,7 +18,7 @@ import { RelationshipType } from '../../../../core/shared/item-relationships/rel
|
||||
import {
|
||||
getAllSucceededRemoteData,
|
||||
getRemoteDataPayload,
|
||||
getFirstSucceededRemoteData,
|
||||
getFirstSucceededRemoteData, getFirstSucceededRemoteDataPayload,
|
||||
} from '../../../../core/shared/operators';
|
||||
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';
|
||||
@@ -29,6 +29,7 @@ import { SearchResult } from '../../../../shared/search/search-result.model';
|
||||
import { followLink } from '../../../../shared/utils/follow-link-config.model';
|
||||
import { PaginatedList } from '../../../../core/data/paginated-list.model';
|
||||
import { RemoteData } from '../../../../core/data/remote-data';
|
||||
import { Collection } from '../../../../core/shared/collection.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-edit-relationship-list',
|
||||
@@ -146,6 +147,11 @@ export class EditRelationshipListComponent implements OnInit {
|
||||
modalComp.repeatable = true;
|
||||
modalComp.listId = this.listId;
|
||||
modalComp.item = this.item;
|
||||
this.item.owningCollection.pipe(
|
||||
getFirstSucceededRemoteDataPayload()
|
||||
).subscribe((collection: Collection) => {
|
||||
modalComp.collection = collection;
|
||||
});
|
||||
modalComp.select = (...selectableObjects: SearchResult<Item>[]) => {
|
||||
selectableObjects.forEach((searchResult) => {
|
||||
const relatedItem: Item = searchResult.indexableObject;
|
||||
|
@@ -3,8 +3,8 @@ import { fadeIn, fadeInOut } from '../../../shared/animations/fade';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { ItemOperation } from '../item-operation/itemOperation.model';
|
||||
import { distinctUntilChanged, first, map } from 'rxjs/operators';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { distinctUntilChanged, first, map, mergeMap, toArray } from 'rxjs/operators';
|
||||
import { BehaviorSubject, Observable, from as observableFrom } from 'rxjs';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { getItemEditRoute, getItemPageRoute } from '../../item-page-routing-paths';
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
@@ -78,42 +78,36 @@ export class ItemStatusComponent implements OnInit {
|
||||
The value is supposed to be a href for the button
|
||||
*/
|
||||
const operations = [];
|
||||
operations.push(new ItemOperation('authorizations', this.getCurrentUrl(item) + '/authorizations'));
|
||||
operations.push(new ItemOperation('mappedCollections', this.getCurrentUrl(item) + '/mapper'));
|
||||
operations.push(undefined);
|
||||
// Store the index of the "withdraw" or "reinstate" operation, because it's added asynchronously
|
||||
const indexOfWithdrawReinstate = operations.length - 1;
|
||||
if (item.isDiscoverable) {
|
||||
operations.push(new ItemOperation('private', this.getCurrentUrl(item) + '/private'));
|
||||
operations.push(new ItemOperation('authorizations', this.getCurrentUrl(item) + '/authorizations', FeatureID.CanManagePolicies, true));
|
||||
operations.push(new ItemOperation('mappedCollections', this.getCurrentUrl(item) + '/mapper', FeatureID.CanManageMappings, true));
|
||||
if (item.isWithdrawn) {
|
||||
operations.push(new ItemOperation('reinstate', this.getCurrentUrl(item) + '/reinstate', FeatureID.ReinstateItem, true));
|
||||
} 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'));
|
||||
operations.push(new ItemOperation('move', this.getCurrentUrl(item) + '/move'));
|
||||
if (item.isDiscoverable) {
|
||||
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);
|
||||
|
||||
if (item.isWithdrawn) {
|
||||
this.authorizationService.isAuthorized(FeatureID.ReinstateItem, item.self).pipe(distinctUntilChanged()).subscribe((authorized) => {
|
||||
const newOperations = [...this.operations$.value];
|
||||
if (authorized) {
|
||||
newOperations[indexOfWithdrawReinstate] = new ItemOperation('reinstate', this.getCurrentUrl(item) + '/reinstate');
|
||||
observableFrom(operations).pipe(
|
||||
mergeMap((operation) => {
|
||||
if (hasValue(operation.featureID)) {
|
||||
return this.authorizationService.isAuthorized(operation.featureID, item.self).pipe(
|
||||
distinctUntilChanged(),
|
||||
map((authorized) => new ItemOperation(operation.operationKey, operation.operationUrl, operation.featureID, !authorized, authorized))
|
||||
);
|
||||
} 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(
|
||||
getAllSucceededRemoteDataPayload(),
|
||||
|
@@ -3,7 +3,7 @@ import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/ro
|
||||
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
||||
import { ItemPageResolver } from './item-page.resolver';
|
||||
import { Item } from '../core/shared/item.model';
|
||||
import { DsoPageFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard';
|
||||
import { 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';
|
||||
@@ -14,7 +14,7 @@ import { AuthService } from '../core/auth/auth.service';
|
||||
/**
|
||||
* 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,
|
||||
protected authorizationService: AuthorizationDataService,
|
||||
protected router: Router,
|
||||
|
@@ -9,6 +9,12 @@ import {
|
||||
getFirstSucceededRemoteData
|
||||
} from '../../../../core/shared/operators';
|
||||
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
|
||||
|
@@ -27,7 +27,10 @@ describe('MyDSpaceConfigurationService', () => {
|
||||
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', {
|
||||
getQueryParameterValue: observableOf(value1),
|
||||
|
@@ -6,6 +6,8 @@
|
||||
[configurationList]="(configurationList$ | async)"
|
||||
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
|
||||
[viewModeList]="viewModeList"
|
||||
[searchOptions]="(searchOptions$ | async)"
|
||||
[sortOptions]="(sortOptions$ | async)"
|
||||
[refreshFilters]="refreshFilters.asObservable()"
|
||||
[inPlaceSearch]="inPlaceSearch"></ds-search-sidebar>
|
||||
<div class="col-12 col-md-9">
|
||||
@@ -28,6 +30,8 @@
|
||||
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
|
||||
(toggleSidebar)="closeSidebar()"
|
||||
[ngClass]="{'active': !(isSidebarCollapsed() | async)}"
|
||||
[searchOptions]="(searchOptions$ | async)"
|
||||
[sortOptions]="(sortOptions$ | async)"
|
||||
[refreshFilters]="refreshFilters.asObservable()"
|
||||
[inPlaceSearch]="inPlaceSearch">
|
||||
</ds-search-sidebar>
|
||||
|
@@ -45,6 +45,7 @@ describe('MyDSpacePageComponent', () => {
|
||||
pagination.id = 'mydspace-results-pagination';
|
||||
pagination.currentPage = 1;
|
||||
pagination.pageSize = 10;
|
||||
const sortOption = { name: 'score', sortOrder: 'DESC', metadata: null };
|
||||
const sort: SortOptions = new SortOptions('score', SortDirection.DESC);
|
||||
const mockResults = createSuccessfulRemoteDataObject$(['test', 'data']);
|
||||
const searchServiceStub = jasmine.createSpyObj('SearchService', {
|
||||
@@ -52,7 +53,8 @@ describe('MyDSpacePageComponent', () => {
|
||||
getEndpoint: observableOf('discover/search/objects'),
|
||||
getSearchLink: '/mydspace',
|
||||
getScopes: observableOf(['test-scope']),
|
||||
setServiceOptions: {}
|
||||
setServiceOptions: {},
|
||||
getSearchConfigurationFor: createSuccessfulRemoteDataObject$({ sortOptions: [sortOption]})
|
||||
});
|
||||
const configurationParam = 'default';
|
||||
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';
|
||||
|
||||
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 { 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 { SearchResult } from '../shared/search/search-result.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 SEARCH_CONFIG_SERVICE: InjectionToken<SearchConfigurationService> = new InjectionToken<SearchConfigurationService>('searchConfigurationService');
|
||||
@@ -71,6 +73,11 @@ export class MyDSpacePageComponent implements OnInit {
|
||||
*/
|
||||
searchOptions$: Observable<PaginatedSearchOptions>;
|
||||
|
||||
/**
|
||||
* The current available sort options
|
||||
*/
|
||||
sortOptions$: Observable<SortOptions[]>;
|
||||
|
||||
/**
|
||||
* The current relevant scopes
|
||||
*/
|
||||
@@ -109,7 +116,8 @@ export class MyDSpacePageComponent implements OnInit {
|
||||
constructor(private service: SearchService,
|
||||
private sidebarService: SidebarService,
|
||||
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.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>
|
||||
<ds-search-sidebar id="search-sidebar" *ngIf="!(isXsOrSm$ | async)"
|
||||
[resultCount]="(resultsRD$ | async)?.payload?.totalElements"
|
||||
[searchOptions]="(searchOptions$ | async)"
|
||||
[sortOptions]="(sortOptions$ | async)"
|
||||
[inPlaceSearch]="inPlaceSearch"></ds-search-sidebar>
|
||||
<ds-search-sidebar id="search-sidebar-sm" *ngIf="(isXsOrSm$ | async)"
|
||||
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
|
||||
(toggleSidebar)="closeSidebar()"
|
||||
>
|
||||
[searchOptions]="(searchOptions$ | async)"
|
||||
[sortOptions]="(sortOptions$ | async)"
|
||||
(toggleSidebar)="closeSidebar()">
|
||||
</ds-search-sidebar>
|
||||
</ng-template>
|
||||
|
||||
|
@@ -40,12 +40,14 @@ const pagination: PaginationComponentOptions = new PaginationComponentOptions();
|
||||
pagination.id = 'search-results-pagination';
|
||||
pagination.currentPage = 1;
|
||||
pagination.pageSize = 10;
|
||||
const sortOption = { name: 'score', sortOrder: 'DESC', metadata: null };
|
||||
const sort: SortOptions = new SortOptions('score', SortDirection.DESC);
|
||||
const mockResults = createSuccessfulRemoteDataObject$(['test', 'data']);
|
||||
const searchServiceStub = jasmine.createSpyObj('SearchService', {
|
||||
search: mockResults,
|
||||
getSearchLink: '/search',
|
||||
getScopes: observableOf(['test-scope'])
|
||||
getScopes: observableOf(['test-scope']),
|
||||
getSearchConfigurationFor: createSuccessfulRemoteDataObject$({ sortOptions: [sortOption]})
|
||||
});
|
||||
const configurationParam = 'default';
|
||||
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 { 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 { RemoteData } from '../core/data/remote-data';
|
||||
import { DSpaceObject } from '../core/shared/dspace-object.model';
|
||||
import { pushInOut } from '../shared/animations/push';
|
||||
import { HostWindowService } from '../shared/host-window.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 { RouteService } from '../core/services/route.service';
|
||||
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 { SearchService } from '../core/shared/search/search.service';
|
||||
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 { SortOptions } from '../core/cache/models/sort-options.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-search',
|
||||
@@ -47,6 +48,11 @@ export class SearchComponent implements OnInit {
|
||||
*/
|
||||
searchOptions$: Observable<PaginatedSearchOptions>;
|
||||
|
||||
/**
|
||||
* The current available sort options
|
||||
*/
|
||||
sortOptions$: Observable<SortOptions[]>;
|
||||
|
||||
/**
|
||||
* The current relevant scopes
|
||||
*/
|
||||
@@ -129,9 +135,15 @@ export class SearchComponent implements OnInit {
|
||||
this.scopeListRD$ = this.searchConfigService.getCurrentScope('').pipe(
|
||||
switchMap((scopeId) => this.service.getScopes(scopeId))
|
||||
);
|
||||
if (!isNotEmpty(this.configuration$)) {
|
||||
this.configuration$ = this.routeService.getRouteParameterValue('configuration');
|
||||
if (isEmpty(this.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')
|
||||
ngOnDestroy(): void {
|
||||
this.onCancel();
|
||||
this.groupDataService.cancelEditGroup();
|
||||
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,
|
||||
getResourceTypeValueFor
|
||||
} 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');
|
||||
|
||||
|
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 { HALResource } from '../../shared/hal-resource.model';
|
||||
import { ResourceType } from '../../shared/resource-type';
|
||||
import * as decorators from './build-decorators';
|
||||
import { LinkService } from './link.service';
|
||||
|
||||
const spyOnFunction = <T>(obj: T, func: keyof T) => {
|
||||
const spy = jasmine.createSpy(func as string);
|
||||
spyOnProperty(obj, func, 'get').and.returnValue(spy);
|
||||
|
||||
return spy;
|
||||
};
|
||||
import { DATA_SERVICE_FACTORY, LINK_DEFINITION_FACTORY, LINK_DEFINITION_MAP_FACTORY } from './build-decorators';
|
||||
import { isEmpty } from 'rxjs/operators';
|
||||
|
||||
const TEST_MODEL = new ResourceType('testmodel');
|
||||
let result: any;
|
||||
@@ -51,7 +45,7 @@ let testDataService: TestDataService;
|
||||
|
||||
let testModel: TestModel;
|
||||
|
||||
xdescribe('LinkService', () => {
|
||||
describe('LinkService', () => {
|
||||
let service: LinkService;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -76,6 +70,30 @@ xdescribe('LinkService', () => {
|
||||
providers: [LinkService, {
|
||||
provide: 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);
|
||||
@@ -84,12 +102,6 @@ xdescribe('LinkService', () => {
|
||||
describe('resolveLink', () => {
|
||||
describe(`when the linkdefinition concerns a single object`, () => {
|
||||
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')));
|
||||
});
|
||||
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`, () => {
|
||||
beforeEach(() => {
|
||||
spyOnFunction(decorators, 'getLinkDefinition').and.returnValue({
|
||||
((service as any).getLinkDefinition as jasmine.Spy).and.returnValue({
|
||||
resourceType: TEST_MODEL,
|
||||
linkName: 'predecessor',
|
||||
propertyName: 'predecessor',
|
||||
isList: true
|
||||
});
|
||||
spyOnFunction(decorators, 'getDataServiceFor').and.returnValue(TestDataService);
|
||||
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', () => {
|
||||
@@ -113,21 +124,15 @@ xdescribe('LinkService', () => {
|
||||
});
|
||||
describe('either way', () => {
|
||||
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')));
|
||||
});
|
||||
|
||||
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', () => {
|
||||
expect(decorators.getDataServiceFor).toHaveBeenCalledWith(TEST_MODEL);
|
||||
expect((service as any).getDataServiceFor).toHaveBeenCalledWith(TEST_MODEL);
|
||||
});
|
||||
|
||||
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`, () => {
|
||||
beforeEach(() => {
|
||||
spyOnFunction(decorators, 'getLinkDefinition').and.returnValue(undefined);
|
||||
((service as any).getLinkDefinition as jasmine.Spy).and.returnValue(undefined);
|
||||
});
|
||||
it('should throw an error', () => {
|
||||
expect(() => {
|
||||
@@ -151,12 +156,7 @@ xdescribe('LinkService', () => {
|
||||
|
||||
describe(`when there is no dataservice for the resourcetype in the link`, () => {
|
||||
beforeEach(() => {
|
||||
spyOnFunction(decorators, 'getLinkDefinition').and.returnValue({
|
||||
resourceType: TEST_MODEL,
|
||||
linkName: 'predecessor',
|
||||
propertyName: 'predecessor'
|
||||
});
|
||||
spyOnFunction(decorators, 'getDataServiceFor').and.returnValue(undefined);
|
||||
((service as any).getDataServiceFor as jasmine.Spy).and.returnValue(undefined);
|
||||
});
|
||||
it('should throw an error', () => {
|
||||
expect(() => {
|
||||
@@ -188,18 +188,6 @@ xdescribe('LinkService', () => {
|
||||
beforeEach(() => {
|
||||
testModel.predecessor = 'predecessor 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', () => {
|
||||
@@ -231,16 +219,10 @@ xdescribe('LinkService', () => {
|
||||
}
|
||||
}
|
||||
});
|
||||
spyOnFunction(decorators, 'getDataServiceFor').and.returnValue(TestDataService);
|
||||
});
|
||||
|
||||
describe('resolving the available link', () => {
|
||||
beforeEach(() => {
|
||||
spyOnFunction(decorators, 'getLinkDefinition').and.returnValue({
|
||||
resourceType: TEST_MODEL,
|
||||
linkName: 'predecessor',
|
||||
propertyName: 'predecessor'
|
||||
});
|
||||
result = service.resolveLinks(testModel, followLink('predecessor'));
|
||||
});
|
||||
|
||||
@@ -251,7 +233,7 @@ xdescribe('LinkService', () => {
|
||||
|
||||
describe('resolving the missing link', () => {
|
||||
beforeEach(() => {
|
||||
spyOnFunction(decorators, 'getLinkDefinition').and.returnValue({
|
||||
((service as any).getLinkDefinition as jasmine.Spy).and.returnValue({
|
||||
resourceType: TEST_MODEL,
|
||||
linkName: 'successor',
|
||||
propertyName: 'successor'
|
||||
@@ -259,8 +241,11 @@ xdescribe('LinkService', () => {
|
||||
result = service.resolveLinks(testModel, followLink('successor'));
|
||||
});
|
||||
|
||||
it('should return the model with no resolved link', () => {
|
||||
expect(result.successor).toBeUndefined();
|
||||
it('should resolve to an empty observable', (done) => {
|
||||
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 { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
|
||||
import { GenericConstructor } from '../../shared/generic-constructor';
|
||||
import { HALResource } from '../../shared/hal-resource.model';
|
||||
import {
|
||||
getDataServiceFor,
|
||||
getLinkDefinition,
|
||||
getLinkDefinitions,
|
||||
DATA_SERVICE_FACTORY,
|
||||
LINK_DEFINITION_FACTORY,
|
||||
LINK_DEFINITION_MAP_FACTORY,
|
||||
LinkDefinition
|
||||
} from './build-decorators';
|
||||
import { RemoteData } from '../../data/remote-data';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { EMPTY } from 'rxjs';
|
||||
import { ResourceType } from '../../shared/resource-type';
|
||||
|
||||
/**
|
||||
* A Service to handle the resolving and removing
|
||||
@@ -24,6 +25,9 @@ export class LinkService {
|
||||
|
||||
constructor(
|
||||
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
|
||||
*/
|
||||
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)) {
|
||||
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 {
|
||||
const provider = getDataServiceFor(matchingLinkDef.resourceType);
|
||||
const provider = this.getDataServiceFor(matchingLinkDef.resourceType);
|
||||
|
||||
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`);
|
||||
@@ -104,7 +108,7 @@ export class LinkService {
|
||||
*/
|
||||
public removeResolvedLinks<T extends HALResource>(model: T): T {
|
||||
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)) {
|
||||
linkDefs.forEach((linkDef: LinkDefinition<T>) => {
|
||||
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 { RootDataService } from './data/root-data.service';
|
||||
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
|
||||
@@ -340,6 +341,7 @@ export const models =
|
||||
Registration,
|
||||
UsageReport,
|
||||
Root,
|
||||
SearchConfig
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@@ -8,8 +8,11 @@ import { Registration } from '../shared/registration.model';
|
||||
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
|
||||
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
|
||||
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
|
||||
describe('EpersonRegistrationService', () => {
|
||||
let testScheduler;
|
||||
|
||||
let service: EpersonRegistrationService;
|
||||
let requestService: RequestService;
|
||||
|
||||
@@ -29,6 +32,12 @@ describe('EpersonRegistrationService', () => {
|
||||
rd = createSuccessfulRemoteDataObject(registrationWithUser);
|
||||
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', {
|
||||
generateRequestId: 'request-id',
|
||||
send: {},
|
||||
@@ -36,7 +45,8 @@ describe('EpersonRegistrationService', () => {
|
||||
{ a: Object.assign(new RequestEntry(), { response: new RestResponse(true, 200, 'Success') }) })
|
||||
});
|
||||
rdbService = jasmine.createSpyObj('rdbService', {
|
||||
buildFromRequestUUID: observableOf(rd)
|
||||
buildSingle: observableOf(rd),
|
||||
buildFromRequestUUID: observableOf(rd),
|
||||
});
|
||||
service = new EpersonRegistrationService(
|
||||
requestService,
|
||||
@@ -86,8 +96,28 @@ describe('EpersonRegistrationService', () => {
|
||||
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 { GetRequest, PostRequest } from './request.models';
|
||||
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 { Registration } from '../shared/registration.model';
|
||||
import { getFirstCompletedRemoteData, getFirstSucceededRemoteData } from '../shared/operators';
|
||||
@@ -60,9 +60,9 @@ export class EpersonRegistrationService {
|
||||
|
||||
const requestId = this.requestService.generateRequestId();
|
||||
|
||||
const hrefObs = this.getRegistrationEndpoint();
|
||||
const href$ = this.getRegistrationEndpoint();
|
||||
|
||||
hrefObs.pipe(
|
||||
href$.pipe(
|
||||
find((href: string) => hasValue(href)),
|
||||
map((href: string) => {
|
||||
const request = new PostRequest(requestId, href, registration);
|
||||
@@ -82,27 +82,28 @@ export class EpersonRegistrationService {
|
||||
searchByToken(token: string): Observable<Registration> {
|
||||
const requestId = this.requestService.generateRequestId();
|
||||
|
||||
const hrefObs = this.getTokenSearchEndpoint(token);
|
||||
|
||||
hrefObs.pipe(
|
||||
const href$ = this.getTokenSearchEndpoint(token).pipe(
|
||||
find((href: string) => hasValue(href)),
|
||||
map((href: string) => {
|
||||
const request = new GetRequest(requestId, href);
|
||||
Object.assign(request, {
|
||||
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||
return RegistrationResponseParsingService;
|
||||
}
|
||||
});
|
||||
this.requestService.send(request, true);
|
||||
})
|
||||
).subscribe();
|
||||
);
|
||||
|
||||
return this.rdbService.buildFromRequestUUID<Registration>(requestId).pipe(
|
||||
href$.subscribe((href: string) => {
|
||||
const request = new GetRequest(requestId, href);
|
||||
Object.assign(request, {
|
||||
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||
return RegistrationResponseParsingService;
|
||||
}
|
||||
});
|
||||
this.requestService.send(request, true);
|
||||
});
|
||||
|
||||
return this.rdbService.buildSingle<Registration>(href$).pipe(
|
||||
skipWhile((rd: RemoteData<Registration>) => rd.isStale),
|
||||
getFirstSucceededRemoteData(),
|
||||
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 { FeatureAuthorizationGuard } from './feature-authorization.guard';
|
||||
import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard';
|
||||
import { AuthorizationDataService } from '../authorization-data.service';
|
||||
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||
import { AuthService } from '../../../auth/auth.service';
|
||||
@@ -13,7 +13,7 @@ import { FeatureID } from '../feature-id';
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class CollectionAdministratorGuard extends FeatureAuthorizationGuard {
|
||||
export class CollectionAdministratorGuard extends SingleFeatureAuthorizationGuard {
|
||||
constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) {
|
||||
super(authorizationService, router, authService);
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
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 { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||
import { AuthService } from '../../../auth/auth.service';
|
||||
@@ -13,7 +13,7 @@ import { FeatureID } from '../feature-id';
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class CommunityAdministratorGuard extends FeatureAuthorizationGuard {
|
||||
export class CommunityAdministratorGuard extends SingleFeatureAuthorizationGuard {
|
||||
constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) {
|
||||
super(authorizationService, router, authService);
|
||||
}
|
||||
|
@@ -4,14 +4,14 @@ 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 { DsoPageFeatureGuard } from './dso-page-feature.guard';
|
||||
import { DsoPageSingleFeatureGuard } from './dso-page-single-feature.guard';
|
||||
import { FeatureID } from '../feature-id';
|
||||
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>>,
|
||||
protected authorizationService: AuthorizationDataService,
|
||||
protected router: Router,
|
||||
@@ -25,8 +25,8 @@ class DsoPageFeatureGuardImpl extends DsoPageFeatureGuard<any> {
|
||||
}
|
||||
}
|
||||
|
||||
describe('DsoPageAdministratorGuard', () => {
|
||||
let guard: DsoPageFeatureGuard<any>;
|
||||
describe('DsoPageSingleFeatureGuard', () => {
|
||||
let guard: DsoPageSingleFeatureGuard<any>;
|
||||
let authorizationService: AuthorizationDataService;
|
||||
let router: Router;
|
||||
let authService: AuthService;
|
||||
@@ -62,7 +62,7 @@ describe('DsoPageAdministratorGuard', () => {
|
||||
},
|
||||
parent: parentRoute
|
||||
};
|
||||
guard = new DsoPageFeatureGuardImpl(resolver, authorizationService, router, authService, undefined);
|
||||
guard = new DsoPageSingleFeatureGuardImpl(resolver, authorizationService, router, authService, undefined);
|
||||
}
|
||||
|
||||
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 { map } from 'rxjs/operators';
|
||||
import { DSpaceObject } from '../../../shared/dspace-object.model';
|
||||
import { FeatureAuthorizationGuard } from './feature-authorization.guard';
|
||||
import { AuthService } from '../../../auth/auth.service';
|
||||
import { hasNoValue, hasValue } from '../../../../shared/empty.util';
|
||||
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
|
||||
*/
|
||||
export abstract class DsoPageFeatureGuard<T extends DSpaceObject> extends FeatureAuthorizationGuard {
|
||||
export abstract class DsoPageSomeFeatureGuard<T extends DSpaceObject> extends SomeFeatureAuthorizationGuard {
|
||||
constructor(protected resolver: Resolve<RemoteData<T>>,
|
||||
protected authorizationService: AuthorizationDataService,
|
||||
protected router: Router,
|
@@ -1,5 +1,5 @@
|
||||
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 { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||
import { AuthService } from '../../../auth/auth.service';
|
||||
@@ -13,7 +13,7 @@ import { FeatureID } from '../feature-id';
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class GroupAdministratorGuard extends FeatureAuthorizationGuard {
|
||||
export class GroupAdministratorGuard extends SingleFeatureAuthorizationGuard {
|
||||
constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: 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 { FeatureID } from '../feature-id';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
@@ -6,10 +6,10 @@ import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/ro
|
||||
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
|
||||
*/
|
||||
class FeatureAuthorizationGuardImpl extends FeatureAuthorizationGuard {
|
||||
class SingleFeatureAuthorizationGuardImpl extends SingleFeatureAuthorizationGuard {
|
||||
constructor(protected authorizationService: AuthorizationDataService,
|
||||
protected router: Router,
|
||||
protected authService: AuthService,
|
||||
@@ -32,8 +32,8 @@ class FeatureAuthorizationGuardImpl extends FeatureAuthorizationGuard {
|
||||
}
|
||||
}
|
||||
|
||||
describe('FeatureAuthorizationGuard', () => {
|
||||
let guard: FeatureAuthorizationGuard;
|
||||
describe('SingleFeatureAuthorizationGuard', () => {
|
||||
let guard: SingleFeatureAuthorizationGuard;
|
||||
let authorizationService: AuthorizationDataService;
|
||||
let router: Router;
|
||||
let authService: AuthService;
|
||||
@@ -56,7 +56,7 @@ describe('FeatureAuthorizationGuard', () => {
|
||||
authService = jasmine.createSpyObj('authService', {
|
||||
isAuthenticated: observableOf(true)
|
||||
});
|
||||
guard = new FeatureAuthorizationGuardImpl(authorizationService, router, authService, featureId, objectUrl, ePersonUuid);
|
||||
guard = new SingleFeatureAuthorizationGuardImpl(authorizationService, router, authService, featureId, objectUrl, ePersonUuid);
|
||||
}
|
||||
|
||||
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 { FeatureAuthorizationGuard } from './feature-authorization.guard';
|
||||
import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard';
|
||||
import { FeatureID } from '../feature-id';
|
||||
import { AuthorizationDataService } from '../authorization-data.service';
|
||||
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||
@@ -13,7 +13,7 @@ import { AuthService } from '../../../auth/auth.service';
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class SiteAdministratorGuard extends FeatureAuthorizationGuard {
|
||||
export class SiteAdministratorGuard extends SingleFeatureAuthorizationGuard {
|
||||
constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: 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 { AuthorizationDataService } from '../authorization-data.service';
|
||||
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||
@@ -13,7 +13,7 @@ import { AuthService } from '../../../auth/auth.service';
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class SiteRegisterGuard extends FeatureAuthorizationGuard {
|
||||
export class SiteRegisterGuard extends SingleFeatureAuthorizationGuard {
|
||||
constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: 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 { FeatureID } from '../feature-id';
|
||||
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 { AuthService } from '../../../auth/auth.service';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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 list of features and/or object.
|
||||
*/
|
||||
export abstract class FeatureAuthorizationGuard implements CanActivate {
|
||||
export abstract class SomeFeatureAuthorizationGuard implements CanActivate {
|
||||
constructor(protected authorizationService: AuthorizationDataService,
|
||||
protected router: Router,
|
||||
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
|
||||
*/
|
||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
|
||||
return observableCombineLatest(this.getFeatureID(route, state), this.getObjectUrl(route, state), this.getEPersonUuid(route, state)).pipe(
|
||||
switchMap(([featureID, objectUrl, ePersonUuid]) => this.authorizationService.isAuthorized(featureID, objectUrl, ePersonUuid)),
|
||||
returnForbiddenUrlTreeOrLoginOnFalse(this.router, this.authService, state.url)
|
||||
return observableCombineLatest(this.getFeatureIDs(route, state), this.getObjectUrl(route, state), this.getEPersonUuid(route, state)).pipe(
|
||||
switchMap(([featureIDs, objectUrl, ePersonUuid]) =>
|
||||
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
|
||||
* Override this method to define a feature
|
||||
* The features to check authorization for
|
||||
* 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
|
@@ -13,4 +13,11 @@ export enum FeatureID {
|
||||
IsCollectionAdmin = 'isCollectionAdmin',
|
||||
IsCommunityAdmin = 'isCommunityAdmin',
|
||||
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 * as ItemRelationshipsUtils from '../../+item-page/simple/item-types/shared/item-relationships-utils';
|
||||
import { followLink } from '../../shared/utils/follow-link-config.model';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
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 { getMockRemoteDataBuildServiceHrefMap } from '../../shared/mocks/remote-data-build.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 requestService: RequestService;
|
||||
|
||||
@@ -132,7 +131,8 @@ xdescribe('RelationshipService', () => {
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
jasmine.createSpy('paginatedRelationsToItems').and.returnValue((v) => v),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -195,8 +195,6 @@ xdescribe('RelationshipService', () => {
|
||||
|
||||
const rd$ = createSuccessfulRemoteDataObject$(relationsList);
|
||||
spyOn(service, 'getItemRelationshipsByLabel').and.returnValue(rd$);
|
||||
|
||||
spyOnOperator(ItemRelationshipsUtils, 'paginatedRelationsToItems').and.returnValue((v) => v);
|
||||
});
|
||||
|
||||
it('should call getItemRelationshipsByLabel with the correct params', (done) => {
|
||||
@@ -225,7 +223,7 @@ xdescribe('RelationshipService', () => {
|
||||
mockLabel,
|
||||
mockOptions
|
||||
).subscribe((result) => {
|
||||
expect(ItemRelationshipsUtils.paginatedRelationsToItems).toHaveBeenCalledWith(mockItem.uuid);
|
||||
expect((service as any).paginatedRelationsToItems).toHaveBeenCalledWith(mockItem.uuid);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
@@ -1,11 +1,10 @@
|
||||
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 { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||
import { distinctUntilChanged, filter, map, mergeMap, startWith, switchMap, take, tap } from 'rxjs/operators';
|
||||
import {
|
||||
compareArraysUsingIds,
|
||||
paginatedRelationsToItems,
|
||||
compareArraysUsingIds, PAGINATED_RELATIONS_TO_ITEMS_OPERATOR,
|
||||
relationsToItems
|
||||
} from '../../+item-page/simple/item-types/shared/item-relationships-utils';
|
||||
import { AppState, keySelector } from '../../app.reducer';
|
||||
@@ -87,7 +86,8 @@ export class RelationshipService extends DataService<Relationship> {
|
||||
protected notificationsService: NotificationsService,
|
||||
protected http: HttpClient,
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -254,7 +254,7 @@ export class RelationshipService extends DataService<Relationship> {
|
||||
* @param options
|
||||
*/
|
||||
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 { 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 { SearchResult } from '../../shared/search/search-result.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 { getEndUserAgreementPath } from '../../info/info-routing-paths';
|
||||
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
|
||||
@@ -201,10 +207,23 @@ export const redirectOn4xx = <T>(router: Router, authService: AuthService) =>
|
||||
*/
|
||||
export const returnForbiddenUrlTreeOrLoginOnFalse = (router: Router, authService: AuthService, redirectUrl: string) =>
|
||||
(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(
|
||||
map(([authorized, authenticated]: [boolean, boolean]) => {
|
||||
if (authorized) {
|
||||
return authorized;
|
||||
map(([authorizedList, authenticated]: [boolean[], boolean]) => {
|
||||
if (authorizedList.some((b: boolean) => b === true)) {
|
||||
return true;
|
||||
} else {
|
||||
if (authenticated) {
|
||||
return router.parseUrl(getForbiddenRoute());
|
||||
|
@@ -23,7 +23,10 @@ describe('SearchConfigurationService', () => {
|
||||
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', {
|
||||
getQueryParameterValue: observableOf(value1),
|
||||
|
@@ -1,8 +1,15 @@
|
||||
import { Injectable, OnDestroy } from '@angular/core';
|
||||
import { ActivatedRoute, Params } from '@angular/router';
|
||||
|
||||
import { BehaviorSubject, combineLatest as observableCombineLatest, merge as observableMerge, Observable, Subscription } from 'rxjs';
|
||||
import { filter, map, startWith } from 'rxjs/operators';
|
||||
import {
|
||||
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 { SearchOptions } from '../../../shared/search/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 { SortDirection, SortOptions } from '../../cache/models/sort-options.model';
|
||||
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 { 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';
|
||||
|
||||
/**
|
||||
@@ -168,7 +181,7 @@ export class SearchConfigurationService implements OnDestroy {
|
||||
if (hasNoValue(filters.find((f) => f.key === realKey))) {
|
||||
const min = filterParams[realKey + '.min'] ? filterParams[realKey + '.min'][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 {
|
||||
filters.push(new SearchFilter(key, filterParams[key]));
|
||||
@@ -194,6 +207,60 @@ export class SearchConfigurationService implements OnDestroy {
|
||||
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
|
||||
* @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);
|
||||
});
|
||||
});
|
||||
|
||||
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 { FacetConfigResponse } from '../../../shared/search/facet-config-response.model';
|
||||
import { FacetValues } from '../../../shared/search/facet-values.model';
|
||||
import { SearchConfig } from './search-filters/search-config.model';
|
||||
import { PaginationService } from '../../pagination/pagination.service';
|
||||
import { SearchConfigurationService } from './search-configuration.service';
|
||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||
@@ -46,6 +47,12 @@ import { PaginationComponentOptions } from '../../../shared/pagination/paginatio
|
||||
*/
|
||||
@Injectable()
|
||||
export class SearchService implements OnDestroy {
|
||||
|
||||
/**
|
||||
* Endpoint link path for retrieving search configurations
|
||||
*/
|
||||
private configurationLinkPath = 'discover/search';
|
||||
|
||||
/**
|
||||
* Endpoint link path for retrieving general search results
|
||||
*/
|
||||
@@ -229,6 +236,24 @@ export class SearchService implements OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
private getConfigUrl(url: string, scope?: string, configurationName?: string) {
|
||||
const args: string[] = [];
|
||||
|
||||
if (isNotEmpty(scope)) {
|
||||
args.push(`scope=${scope}`);
|
||||
}
|
||||
|
||||
if (isNotEmpty(configurationName)) {
|
||||
args.push(`configuration=${configurationName}`);
|
||||
}
|
||||
|
||||
if (isNotEmpty(args)) {
|
||||
url = new URLCombiner(url, `?${args.join('&')}`).toString();
|
||||
}
|
||||
|
||||
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
|
||||
@@ -237,33 +262,17 @@ export class SearchService implements OnDestroy {
|
||||
*/
|
||||
getConfig(scope?: string, configurationName?: string): Observable<RemoteData<SearchFilterConfig[]>> {
|
||||
const href$ = this.halService.getEndpoint(this.facetLinkPathPrefix).pipe(
|
||||
map((url: string) => {
|
||||
const args: string[] = [];
|
||||
|
||||
if (isNotEmpty(scope)) {
|
||||
args.push(`scope=${scope}`);
|
||||
}
|
||||
|
||||
if (isNotEmpty(configurationName)) {
|
||||
args.push(`configuration=${configurationName}`);
|
||||
}
|
||||
|
||||
if (isNotEmpty(args)) {
|
||||
url = new URLCombiner(url, `?${args.join('&')}`).toString();
|
||||
}
|
||||
|
||||
return url;
|
||||
}),
|
||||
map((url: string) => this.getConfigUrl(url, scope, configurationName)),
|
||||
);
|
||||
|
||||
href$.pipe(take(1)).subscribe((url: string) => {
|
||||
let request = new this.request(this.requestService.generateRequestId(), url);
|
||||
request = Object.assign(request, {
|
||||
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||
return FacetConfigResponseParsingService;
|
||||
}
|
||||
});
|
||||
this.requestService.send(request, true);
|
||||
let request = new this.request(this.requestService.generateRequestId(), url);
|
||||
request = Object.assign(request, {
|
||||
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||
return FacetConfigResponseParsingService;
|
||||
}
|
||||
});
|
||||
this.requestService.send(request, true);
|
||||
});
|
||||
|
||||
return this.rdb.buildFromHref(href$).pipe(
|
||||
@@ -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
|
||||
*/
|
||||
|
@@ -11,6 +11,7 @@ import { Store } from '@ngrx/store';
|
||||
import { CoreState } from '../../core/core.reducers';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-forgot-password-form',
|
||||
@@ -70,7 +71,9 @@ export class ForgotPasswordFormComponent {
|
||||
*/
|
||||
submit() {
|
||||
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) {
|
||||
this.notificationsService.success(
|
||||
this.translateService.instant(this.NOTIFICATIONS_PREFIX + '.success.title'),
|
||||
|
@@ -5,3 +5,7 @@
|
||||
position: sticky;
|
||||
}
|
||||
}
|
||||
|
||||
:host {
|
||||
z-index: var(--ds-nav-z-index);
|
||||
}
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { BrowserKlaroService, COOKIE_MDFIELD } from './browser-klaro.service';
|
||||
import { getMockTranslateService } from '../mocks/translate.service.mock';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
@@ -13,7 +12,7 @@ import { getTestScheduler } from 'jasmine-marbles';
|
||||
import { MetadataValue } from '../../core/shared/metadata.models';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
xdescribe('BrowserKlaroService', () => {
|
||||
describe('BrowserKlaroService', () => {
|
||||
let translateService;
|
||||
let ePersonService;
|
||||
let authService;
|
||||
@@ -81,7 +80,7 @@ xdescribe('BrowserKlaroService', () => {
|
||||
}
|
||||
}
|
||||
},
|
||||
apps: [{
|
||||
services: [{
|
||||
name: appName,
|
||||
purposes: [purpose]
|
||||
}],
|
||||
|
@@ -21,8 +21,6 @@ import { createPaginatedList } from '../../../../testing/utils.test';
|
||||
import { ExternalSourceService } from '../../../../../core/data/external-source.service';
|
||||
import { LookupRelationService } from '../../../../../core/data/lookup-relation.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 { Collection } from '../../../../../core/shared/collection.model';
|
||||
|
||||
@@ -46,8 +44,6 @@ describe('DsDynamicLookupRelationModalComponent', () => {
|
||||
let lookupRelationService;
|
||||
let rdbService;
|
||||
let submissionId;
|
||||
let submissionService;
|
||||
let submissionObjectDataService;
|
||||
|
||||
const externalSources = [
|
||||
Object.assign(new ExternalSource(), {
|
||||
@@ -99,12 +95,6 @@ describe('DsDynamicLookupRelationModalComponent', () => {
|
||||
rdbService = jasmine.createSpyObj('rdbService', {
|
||||
aggregate: createSuccessfulRemoteDataObject$(externalSources)
|
||||
});
|
||||
submissionService = jasmine.createSpyObj('SubmissionService', {
|
||||
dispatchSave: jasmine.createSpy('dispatchSave')
|
||||
});
|
||||
submissionObjectDataService = jasmine.createSpyObj('SubmissionObjectDataService', {
|
||||
findById: createSuccessfulRemoteDataObject$(testWSI)
|
||||
});
|
||||
submissionId = '1234';
|
||||
}
|
||||
|
||||
@@ -129,8 +119,6 @@ describe('DsDynamicLookupRelationModalComponent', () => {
|
||||
},
|
||||
{ provide: RelationshipTypeService, useValue: {} },
|
||||
{ provide: RemoteDataBuildService, useValue: rdbService },
|
||||
{ provide: SubmissionService, useValue: submissionService },
|
||||
{ provide: SubmissionObjectDataService, useValue: submissionObjectDataService },
|
||||
{
|
||||
provide: Store, useValue: {
|
||||
// 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 { Item } from '../../../../../core/shared/item.model';
|
||||
import {
|
||||
getAllSucceededRemoteData,
|
||||
getAllSucceededRemoteDataPayload,
|
||||
getRemoteDataPayload
|
||||
} from '../../../../../core/shared/operators';
|
||||
import { AddRelationshipAction, RemoveRelationshipAction, UpdateRelationshipNameVariantAction } from './relationship.actions';
|
||||
AddRelationshipAction, RemoveRelationshipAction, UpdateRelationshipNameVariantAction,
|
||||
} from './relationship.actions';
|
||||
import { RelationshipService } from '../../../../../core/data/relationship.service';
|
||||
import { RelationshipTypeService } from '../../../../../core/data/relationship-type.service';
|
||||
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 { Router } from '@angular/router';
|
||||
import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service';
|
||||
import { followLink } from '../../../../utils/follow-link-config.model';
|
||||
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';
|
||||
import { getAllSucceededRemoteDataPayload } from '../../../../../core/shared/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-dynamic-lookup-relation-modal',
|
||||
@@ -122,10 +114,6 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
|
||||
*/
|
||||
totalExternal$: Observable<number[]>;
|
||||
|
||||
/**
|
||||
* List of subscriptions to unsubscribe from
|
||||
*/
|
||||
private subs: Subscription[] = [];
|
||||
|
||||
constructor(
|
||||
public modal: NgbActiveModal,
|
||||
@@ -136,17 +124,14 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
|
||||
private lookupRelationService: LookupRelationService,
|
||||
private searchConfigService: SearchConfigurationService,
|
||||
private rdbService: RemoteDataBuildService,
|
||||
private submissionService: SubmissionService,
|
||||
private submissionObjectService: SubmissionObjectDataService,
|
||||
private zone: NgZone,
|
||||
private store: Store<AppState>,
|
||||
private router: Router
|
||||
private router: Router,
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.setItem();
|
||||
this.selection$ = this.selectableListService
|
||||
.getSelectableList(this.listId)
|
||||
.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
|
||||
* @param sri The search result to track name variants for
|
||||
@@ -279,8 +246,5 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
|
||||
ngOnDestroy() {
|
||||
this.router.navigate([], {});
|
||||
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 { BehaviorSubject, Observable, of as observableOf } from 'rxjs';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { provideMockActions } from '@ngrx/effects/testing';
|
||||
import { Store } from '@ngrx/store';
|
||||
|
||||
import { RelationshipEffects } from './relationship.effects';
|
||||
import { AddRelationshipAction, RelationshipActionTypes, RemoveRelationshipAction } from './relationship.actions';
|
||||
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 { TranslateService } from '@ngx-translate/core';
|
||||
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', () => {
|
||||
let relationEffects: RelationshipEffects;
|
||||
@@ -51,7 +51,6 @@ describe('RelationshipEffects', () => {
|
||||
let notificationsService;
|
||||
let translateService;
|
||||
let selectableListService;
|
||||
let testScheduler: TestScheduler;
|
||||
|
||||
function init() {
|
||||
testUUID1 = '20e24c2f-a00a-467c-bdee-c929e79bf08d';
|
||||
@@ -131,6 +130,7 @@ describe('RelationshipEffects', () => {
|
||||
{ provide: NotificationsService, useValue: notificationsService },
|
||||
{ provide: TranslateService, useValue: translateService },
|
||||
{ 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);
|
||||
spyOn((relationEffects as any), 'addRelationship').and.stub();
|
||||
spyOn((relationEffects as any), 'removeRelationship').and.stub();
|
||||
testScheduler = new TestScheduler((actual, expected) => {
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapLastActions$', () => {
|
||||
@@ -151,15 +148,13 @@ describe('RelationshipEffects', () => {
|
||||
let action;
|
||||
|
||||
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');
|
||||
actions = hot('--a-|', { a: action });
|
||||
expectObservable(relationEffects.mapLastActions$).toBe('--b-|', { b: undefined });
|
||||
flush();
|
||||
// TODO check following expectations with the implementation
|
||||
// expect((relationEffects as any).initialActionMap[identifier]).toBe(action.type);
|
||||
// expect((relationEffects as any).debounceMap[identifier].value).toBe(action.type);
|
||||
});
|
||||
action = new AddRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234');
|
||||
actions = hot('--a-|', { a: action });
|
||||
const expected = cold('--b-|', { b: undefined });
|
||||
expect(relationEffects.mapLastActions$).toBeObservable(expected);
|
||||
|
||||
expect((relationEffects as any).initialActionMap[identifier]).toBe(action.type);
|
||||
expect((relationEffects as any).debounceMap[identifier].value).toBe(action.type);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -172,14 +167,14 @@ describe('RelationshipEffects', () => {
|
||||
});
|
||||
|
||||
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');
|
||||
actions = hot('--a-|', { a: action });
|
||||
expectObservable(relationEffects.mapLastActions$).toBe('--b-|', { b: undefined });
|
||||
flush();
|
||||
expect((relationEffects as any).initialActionMap[identifier]).toBe(testActionType);
|
||||
expect((relationEffects as any).debounceMap[identifier].value).toBe(action.type);
|
||||
});
|
||||
action = new AddRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234');
|
||||
actions = hot('--a-|', { a: action });
|
||||
|
||||
const expected = cold('--b-|', { b: undefined });
|
||||
expect(relationEffects.mapLastActions$).toBeObservable(expected);
|
||||
|
||||
expect((relationEffects as any).initialActionMap[identifier]).toBe(testActionType);
|
||||
expect((relationEffects as any).debounceMap[identifier].value).toBe(action.type);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -188,30 +183,26 @@ describe('RelationshipEffects', () => {
|
||||
describe('When the last value in the debounceMap is also an ADD_RELATIONSHIP action', () => {
|
||||
beforeEach(() => {
|
||||
(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', () => {
|
||||
testScheduler.run(({ hot, expectObservable, flush }) => {
|
||||
action = new AddRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234');
|
||||
actions = hot('--a-|', { a: action });
|
||||
expectObservable(relationEffects.mapLastActions$).toBe('--b-|', { b: undefined });
|
||||
flush();
|
||||
expect((relationEffects as any).addRelationship).toHaveBeenCalledWith(leftItem, rightItem, relationshipType.leftwardType, '1234', undefined);
|
||||
});
|
||||
action = new AddRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234');
|
||||
actions = hot('--a-|', { a: action });
|
||||
const expected = cold('--b-|', { b: undefined });
|
||||
expect(relationEffects.mapLastActions$).toBeObservable(expected);
|
||||
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', () => {
|
||||
|
||||
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 actionb = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234');
|
||||
actions = hot('--ab-|', { a: actiona, b: actionb });
|
||||
expectObservable(relationEffects.mapLastActions$).toBe('--bb-|', { b: undefined });
|
||||
flush();
|
||||
expect((relationEffects as any).addRelationship).not.toHaveBeenCalled();
|
||||
expect((relationEffects as any).removeRelationship).not.toHaveBeenCalled();
|
||||
});
|
||||
const actiona = new AddRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234');
|
||||
const actionb = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234');
|
||||
actions = hot('--ab-|', { a: actiona, b: actionb });
|
||||
const expected = cold('--bb-|', { b: undefined });
|
||||
expect(relationEffects.mapLastActions$).toBeObservable(expected);
|
||||
expect((relationEffects as any).addRelationship).not.toHaveBeenCalled();
|
||||
expect((relationEffects as any).removeRelationship).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -222,15 +213,13 @@ describe('RelationshipEffects', () => {
|
||||
let action;
|
||||
|
||||
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');
|
||||
actions = hot('--a-|', { a: action });
|
||||
expectObservable(relationEffects.mapLastActions$).toBe('--b-|', { b: undefined });
|
||||
flush();
|
||||
// TODO check following expectations with the implementation
|
||||
// expect((relationEffects as any).initialActionMap[identifier]).toBe(action.type);
|
||||
// expect((relationEffects as any).debounceMap[identifier].value).toBe(action.type);
|
||||
});
|
||||
action = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234');
|
||||
actions = hot('--a-|', { a: action });
|
||||
const expected = cold('--b-|', { b: undefined });
|
||||
expect(relationEffects.mapLastActions$).toBeObservable(expected);
|
||||
|
||||
expect((relationEffects as any).initialActionMap[identifier]).toBe(action.type);
|
||||
expect((relationEffects as any).debounceMap[identifier].value).toBe(action.type);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -238,20 +227,19 @@ describe('RelationshipEffects', () => {
|
||||
let action;
|
||||
const testActionType = 'TEST_TYPE';
|
||||
beforeEach(() => {
|
||||
((relationEffects as any).debounceTime as jasmine.Spy).and.returnValue((v) => v);
|
||||
(relationEffects as any).initialActionMap[identifier] = 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', () => {
|
||||
testScheduler.run(({ hot, expectObservable, flush }) => {
|
||||
action = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234');
|
||||
actions = hot('--a-|', { a: action });
|
||||
expectObservable(relationEffects.mapLastActions$).toBe('--b-|', { b: undefined });
|
||||
flush();
|
||||
// TODO check following expectations with the implementation
|
||||
// expect((relationEffects as any).initialActionMap[identifier]).toBe(testActionType);
|
||||
// expect((relationEffects as any).debounceMap[identifier].value).toBe(action.type);
|
||||
});
|
||||
action = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234');
|
||||
actions = hot('--a-|', { a: action });
|
||||
const expected = cold('--b-|', { b: undefined });
|
||||
expect(relationEffects.mapLastActions$).toBeObservable(expected);
|
||||
|
||||
expect((relationEffects as any).initialActionMap[identifier]).toBe(testActionType);
|
||||
expect((relationEffects as any).debounceMap[identifier].value).toBe(action.type);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -259,32 +247,28 @@ describe('RelationshipEffects', () => {
|
||||
let action;
|
||||
describe('When the last value in the debounceMap is also an REMOVE_RELATIONSHIP action', () => {
|
||||
beforeEach(() => {
|
||||
((relationEffects as any).debounceTime as jasmine.Spy).and.returnValue((v) => v);
|
||||
(relationEffects as any).initialActionMap[identifier] = RelationshipActionTypes.REMOVE_RELATIONSHIP;
|
||||
});
|
||||
|
||||
it('should call removeRelationship on the effect', () => {
|
||||
testScheduler.run(({ hot, expectObservable, flush }) => {
|
||||
action = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234');
|
||||
actions = hot('a', { a: action });
|
||||
expectObservable(relationEffects.mapLastActions$).toBe('b', { b: undefined });
|
||||
flush();
|
||||
expect((relationEffects as any).removeRelationship).toHaveBeenCalledWith(leftItem, rightItem, relationshipType.leftwardType, '1234',);
|
||||
});
|
||||
action = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234');
|
||||
actions = hot('--a-|', { a: action });
|
||||
const expected = cold('--b-|', { b: undefined });
|
||||
expect(relationEffects.mapLastActions$).toBeObservable(expected);
|
||||
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', () => {
|
||||
|
||||
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 actiona = new AddRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234');
|
||||
actions = hot('--ab-|', { a: actiona, b: actionb });
|
||||
expectObservable(relationEffects.mapLastActions$).toBe('--bb-|', { b: undefined });
|
||||
flush();
|
||||
expect((relationEffects as any).addRelationship).not.toHaveBeenCalled();
|
||||
expect((relationEffects as any).removeRelationship).not.toHaveBeenCalled();
|
||||
});
|
||||
const actionb = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234');
|
||||
const actiona = new AddRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234');
|
||||
actions = hot('--ab-|', { a: actiona, b: actionb });
|
||||
const expected = cold('--bb-|', { b: undefined });
|
||||
expect(relationEffects.mapLastActions$).toBeObservable(expected);
|
||||
expect((relationEffects as any).addRelationship).not.toHaveBeenCalled();
|
||||
expect((relationEffects as any).removeRelationship).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
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 { RelationshipService } from '../../../../../core/data/relationship.service';
|
||||
import {
|
||||
getRemoteDataPayload,
|
||||
getFirstSucceededRemoteData
|
||||
getFirstSucceededRemoteData, DEBOUNCE_TIME_OPERATOR
|
||||
} from '../../../../../core/shared/operators';
|
||||
import {
|
||||
AddRelationshipAction,
|
||||
@@ -71,7 +71,7 @@ export class RelationshipEffects {
|
||||
this.initialActionMap[identifier] = action.type;
|
||||
this.debounceMap[identifier] = new BehaviorSubject<string>(action.type);
|
||||
this.debounceMap[identifier].pipe(
|
||||
debounceTime(DEBOUNCE_TIME),
|
||||
this.debounceTime(DEBOUNCE_TIME),
|
||||
take(1)
|
||||
).subscribe(
|
||||
(type) => {
|
||||
@@ -159,6 +159,7 @@ export class RelationshipEffects {
|
||||
private notificationsService: NotificationsService,
|
||||
private translateService: TranslateService,
|
||||
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 { ChangeDetectionStrategy, ComponentFactoryResolver, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { Context } from '../../core/shared/context.model';
|
||||
import {
|
||||
MetadataRepresentation,
|
||||
MetadataRepresentationType
|
||||
} from '../../core/shared/metadata-representation/metadata-representation.model';
|
||||
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 * 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 testContext = Context.Search;
|
||||
@@ -29,16 +29,30 @@ class TestType implements MetadataRepresentation {
|
||||
}
|
||||
}
|
||||
|
||||
xdescribe('MetadataRepresentationLoaderComponent', () => {
|
||||
describe('MetadataRepresentationLoaderComponent', () => {
|
||||
let comp: MetadataRepresentationLoaderComponent;
|
||||
let fixture: ComponentFixture<MetadataRepresentationLoaderComponent>;
|
||||
let themeService: ThemeService;
|
||||
const themeName = 'test-theme';
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
themeService = jasmine.createSpyObj('themeService', {
|
||||
getThemeName: themeName,
|
||||
});
|
||||
TestBed.configureTestingModule({
|
||||
imports: [],
|
||||
declarations: [MetadataRepresentationLoaderComponent, PlainTextMetadataListElementComponent, MetadataRepresentationDirective],
|
||||
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, {
|
||||
set: {
|
||||
changeDetection: ChangeDetectionStrategy.Default,
|
||||
@@ -53,15 +67,12 @@ xdescribe('MetadataRepresentationLoaderComponent', () => {
|
||||
|
||||
comp.mdRepresentation = new TestType();
|
||||
comp.context = testContext;
|
||||
|
||||
spyOnExported(metadataRepresentationDecorator, 'getMetadataRepresentationComponent').and.returnValue(PlainTextMetadataListElementComponent);
|
||||
fixture.detectChanges();
|
||||
|
||||
}));
|
||||
|
||||
describe('When the component is rendered', () => {
|
||||
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 { MetadataRepresentation } from '../../core/shared/metadata-representation/metadata-representation.model';
|
||||
import { getMetadataRepresentationComponent } from './metadata-representation.decorator';
|
||||
import { Component, ComponentFactoryResolver, Inject, Input, OnInit, ViewChild } from '@angular/core';
|
||||
import {
|
||||
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 { GenericConstructor } from '../../core/shared/generic-constructor';
|
||||
import { MetadataRepresentationListElementComponent } from '../object-list/metadata-representation-list-element/metadata-representation-list-element.component';
|
||||
@@ -45,7 +48,8 @@ export class MetadataRepresentationLoaderComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
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}
|
||||
*/
|
||||
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 { hasNoValue, hasValue } from '../empty.util';
|
||||
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();
|
||||
|
||||
|
@@ -97,8 +97,14 @@ export class EndpointMockingRestService extends DspaceRestService {
|
||||
* the mock response if there is one, undefined otherwise
|
||||
*/
|
||||
private getMockData(urlStr: string): any {
|
||||
const url = new URL(urlStr);
|
||||
const key = url.pathname.slice(environment.rest.nameSpace.length);
|
||||
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);
|
||||
key = url.pathname.slice(environment.rest.nameSpace.length);
|
||||
}
|
||||
if (this.mockResponseMap.has(key)) {
|
||||
// parse and stringify to clone the object to ensure that any changes made
|
||||
// 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 mockPublicationResponse from './mock-publication-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> {}
|
||||
|
||||
@@ -16,4 +20,8 @@ export const mockResponseMap: ResponseMapMock = new Map([
|
||||
// [ '/config/submissionforms/traditionalpageone', mockSubmissionResponse ]
|
||||
// [ '/api/pid/find', mockPublicationResponse ],
|
||||
// [ '/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;
|
||||
const sortOptions = new SortOptions('test.field', SortDirection.DESC);
|
||||
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 scope = '0fde1ecb-82cc-425a-b600-ac3576d76b47';
|
||||
const baseUrl = 'www.rest.com';
|
||||
@@ -19,7 +24,8 @@ describe('PaginatedSearchOptions', () => {
|
||||
filters: filters,
|
||||
query: query,
|
||||
scope: scope,
|
||||
dsoTypes: [DSpaceObjectType.ITEM]
|
||||
dsoTypes: [DSpaceObjectType.ITEM],
|
||||
fixedFilter: fixedFilter,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,12 +37,14 @@ describe('PaginatedSearchOptions', () => {
|
||||
'sort=test.field,DESC&' +
|
||||
'page=0&' +
|
||||
'size=40&' +
|
||||
'query=search query&' +
|
||||
'f.fixed=1234%2C5678,equals&' +
|
||||
'query=search%20query&' +
|
||||
'scope=0fde1ecb-82cc-425a-b600-ac3576d76b47&' +
|
||||
'dsoType=ITEM&' +
|
||||
'f.test=value&' +
|
||||
'f.example=another value&' +
|
||||
'f.example=second value'
|
||||
'f.example=another%20value&' +
|
||||
'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
|
||||
*/
|
||||
closed = true;
|
||||
closed: boolean;
|
||||
|
||||
/**
|
||||
* Emits true when the filter is currently collapsed in the store
|
||||
|
@@ -8,18 +8,16 @@
|
||||
|
||||
::ng-deep
|
||||
{
|
||||
--ds-slider-handle-width: 18px;
|
||||
|
||||
html:not([dir=rtl]) .noUi-horizontal .noUi-handle {
|
||||
right: calc(var(--ds-slider-handle-width) / -2);
|
||||
}
|
||||
.noUi-horizontal .noUi-handle {
|
||||
width: var(--ds-slider-handle-width);
|
||||
&:before {
|
||||
left: calc(calc(calc(var(--ds-slider-handle-width) - 2) / 2) - 2);
|
||||
left: calc(((var(--ds-slider-handle-width) - 2px) / 2) - 2px);
|
||||
}
|
||||
&:after {
|
||||
left: calc(calc(calc(var(--ds-slider-handle-width) - 2) / 2) + 2);
|
||||
left: calc(((var(--ds-slider-handle-width) - 2px) / 2) + 2px);
|
||||
}
|
||||
&:focus {
|
||||
outline: none;
|
||||
|
@@ -56,7 +56,7 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple
|
||||
/**
|
||||
* Fallback maximum for the range
|
||||
*/
|
||||
max = 2018;
|
||||
max = new Date().getFullYear();
|
||||
|
||||
/**
|
||||
* 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';
|
||||
|
||||
describe('SearchOptions', () => {
|
||||
let options: PaginatedSearchOptions;
|
||||
const filters = [new SearchFilter('f.test', ['value']), new SearchFilter('f.example', ['another value', 'second value'])];
|
||||
let options: SearchOptions;
|
||||
|
||||
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 scope = '0fde1ecb-82cc-425a-b600-ac3576d76b47';
|
||||
const baseUrl = 'www.rest.com';
|
||||
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', () => {
|
||||
@@ -18,12 +30,14 @@ describe('SearchOptions', () => {
|
||||
it('should generate a string with all parameters that are present', () => {
|
||||
const outcome = options.toRestUrl(baseUrl);
|
||||
expect(outcome).toEqual('www.rest.com?' +
|
||||
'query=search query&' +
|
||||
'f.fixed=1234%2C5678,equals&' +
|
||||
'query=search%20query&' +
|
||||
'scope=0fde1ecb-82cc-425a-b600-ac3576d76b47&' +
|
||||
'dsoType=ITEM&' +
|
||||
'f.test=value&' +
|
||||
'f.example=another value&' +
|
||||
'f.example=second value'
|
||||
'f.example=another%20value&' +
|
||||
'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 { SearchFilter } from './search-filter.model';
|
||||
import { DSpaceObjectType } from '../../core/shared/dspace-object-type.model';
|
||||
@@ -13,10 +13,15 @@ export class SearchOptions {
|
||||
scope?: string;
|
||||
query?: string;
|
||||
dsoTypes?: DSpaceObjectType[];
|
||||
filters?: any;
|
||||
fixedFilter?: any;
|
||||
filters?: SearchFilter[];
|
||||
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.scope = options.scope;
|
||||
this.query = options.query;
|
||||
@@ -33,27 +38,27 @@ export class SearchOptions {
|
||||
*/
|
||||
toRestUrl(url: string, args: string[] = []): string {
|
||||
if (isNotEmpty(this.configuration)) {
|
||||
args.push(`configuration=${this.configuration}`);
|
||||
args.push(`configuration=${encodeURIComponent(this.configuration)}`);
|
||||
}
|
||||
if (isNotEmpty(this.fixedFilter)) {
|
||||
args.push(this.fixedFilter);
|
||||
args.push(this.encodedFixedFilter);
|
||||
}
|
||||
if (isNotEmpty(this.query)) {
|
||||
args.push(`query=${this.query}`);
|
||||
args.push(`query=${encodeURIComponent(this.query)}`);
|
||||
}
|
||||
if (isNotEmpty(this.scope)) {
|
||||
args.push(`scope=${this.scope}`);
|
||||
args.push(`scope=${encodeURIComponent(this.scope)}`);
|
||||
}
|
||||
if (isNotEmpty(this.dsoTypes)) {
|
||||
this.dsoTypes.forEach((dsoType: string) => {
|
||||
args.push(`dsoType=${dsoType}`);
|
||||
args.push(`dsoType=${encodeURIComponent(dsoType)}`);
|
||||
});
|
||||
}
|
||||
if (isNotEmpty(this.filters)) {
|
||||
this.filters.forEach((filter: SearchFilter) => {
|
||||
filter.values.forEach((value) => {
|
||||
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;
|
||||
}
|
||||
|
||||
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>
|
||||
<div class="result-order-settings">
|
||||
<ds-sidebar-dropdown
|
||||
@@ -7,7 +7,7 @@
|
||||
[label]="'search.sidebar.settings.sort-by'"
|
||||
(change)="reloadOrder($event)"
|
||||
>
|
||||
<option *ngFor="let sortOption of searchOptionPossibilities"
|
||||
<option *ngFor="let sortOption of sortOptions"
|
||||
[value]="sortOption.field + ',' + sortOption.direction.toString()"
|
||||
[selected]="sortOption.field === config?.sort.field && sortOption.direction === (config?.sort.direction)? 'selected': null">
|
||||
{{'sorting.' + sortOption.field + '.' + sortOption.direction | translate}}
|
||||
|
@@ -12,7 +12,6 @@ import { EnumKeysPipe } from '../../utils/enum-keys-pipe';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { SearchFilterService } from '../../../core/shared/search/search-filter.service';
|
||||
import { VarDirective } from '../../utils/var.directive';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { SEARCH_CONFIG_SERVICE } from '../../../+my-dspace-page/my-dspace-page.component';
|
||||
import { SidebarService } from '../../sidebar/sidebar.service';
|
||||
import { SidebarServiceStub } from '../../testing/sidebar-service.stub';
|
||||
@@ -91,7 +90,7 @@ describe('SearchSettingsComponent', () => {
|
||||
provide: SEARCH_CONFIG_SERVICE,
|
||||
useValue: {
|
||||
paginatedSearchOptions: observableOf(paginatedSearchOptions),
|
||||
getCurrentScope: observableOf('test-id')
|
||||
getCurrentScope: observableOf('test-id'),
|
||||
}
|
||||
},
|
||||
],
|
||||
@@ -103,6 +102,14 @@ describe('SearchSettingsComponent', () => {
|
||||
fixture = TestBed.createComponent(SearchSettingsComponent);
|
||||
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
|
||||
fixture.detectChanges();
|
||||
searchServiceObject = (comp as any).service;
|
||||
@@ -111,34 +118,24 @@ describe('SearchSettingsComponent', () => {
|
||||
|
||||
});
|
||||
|
||||
it('it should show the order settings with the respective selectable options', (done) => {
|
||||
(comp as any).searchOptions$.pipe(take(1)).subscribe((options) => {
|
||||
fixture.detectChanges();
|
||||
const orderSetting = fixture.debugElement.query(By.css('div.result-order-settings'));
|
||||
expect(orderSetting).toBeDefined();
|
||||
const childElements = orderSetting.queryAll(By.css('option'));
|
||||
expect(childElements.length).toEqual(comp.searchOptionPossibilities.length);
|
||||
done();
|
||||
});
|
||||
it('it should show the order settings with the respective selectable options', () => {
|
||||
fixture.detectChanges();
|
||||
const orderSetting = fixture.debugElement.query(By.css('div.result-order-settings'));
|
||||
expect(orderSetting).toBeDefined();
|
||||
const childElements = orderSetting.queryAll(By.css('option'));
|
||||
expect(childElements.length).toEqual(comp.sortOptions.length);
|
||||
});
|
||||
|
||||
it('it should show the size settings', (done) => {
|
||||
(comp as any).searchOptions$.pipe(take(1)).subscribe((options) => {
|
||||
fixture.detectChanges();
|
||||
const pageSizeSetting = fixture.debugElement.query(By.css('page-size-settings'));
|
||||
expect(pageSizeSetting).toBeDefined();
|
||||
done();
|
||||
}
|
||||
);
|
||||
it('it should show the size settings', () => {
|
||||
fixture.detectChanges();
|
||||
const pageSizeSetting = fixture.debugElement.query(By.css('page-size-settings'));
|
||||
expect(pageSizeSetting).toBeDefined();
|
||||
});
|
||||
|
||||
it('should have the proper order value selected by default', (done) => {
|
||||
(comp as any).searchOptions$.pipe(take(1)).subscribe((options) => {
|
||||
fixture.detectChanges();
|
||||
const orderSetting = fixture.debugElement.query(By.css('div.result-order-settings'));
|
||||
const childElementToBeSelected = orderSetting.query(By.css('option[value="0"][selected="selected"]'));
|
||||
expect(childElementToBeSelected).toBeDefined();
|
||||
done();
|
||||
});
|
||||
it('should have the proper order value selected by default', () => {
|
||||
fixture.detectChanges();
|
||||
const orderSetting = fixture.debugElement.query(By.css('div.result-order-settings'));
|
||||
const childElementToBeSelected = orderSetting.query(By.css('option[value="score,DESC"][selected="selected"]'));
|
||||
expect(childElementToBeSelected).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
@@ -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 { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { PaginatedSearchOptions } from '../paginated-search-options.model';
|
||||
import { Observable } from 'rxjs';
|
||||
import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service';
|
||||
import { SEARCH_CONFIG_SERVICE } from '../../../+my-dspace-page/my-dspace-page.component';
|
||||
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.
|
||||
*/
|
||||
export class SearchSettingsComponent implements OnInit {
|
||||
export class SearchSettingsComponent {
|
||||
|
||||
/**
|
||||
* The configuration for the current paginated search results
|
||||
*/
|
||||
searchOptions$: Observable<PaginatedSearchOptions>;
|
||||
@Input() searchOptions: PaginatedSearchOptions;
|
||||
|
||||
/**
|
||||
* 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,
|
||||
private route: ActivatedRoute,
|
||||
@@ -35,13 +35,6 @@ export class SearchSettingsComponent implements OnInit {
|
||||
@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
|
||||
* @param {Event} event Change event containing the sort direction and sort field
|
||||
|
@@ -12,7 +12,7 @@
|
||||
<div class="sidebar-content">
|
||||
<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-settings></ds-search-settings>
|
||||
<ds-search-settings [searchOptions]="searchOptions" [sortOptions]="sortOptions"></ds-search-settings>
|
||||
</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 { 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.
|
||||
@@ -45,6 +47,16 @@ export class SearchSidebarComponent {
|
||||
*/
|
||||
@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.
|
||||
*/
|
||||
|
@@ -1520,7 +1520,7 @@
|
||||
|
||||
"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",
|
||||
@@ -1765,6 +1765,8 @@
|
||||
|
||||
"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.label": "Withdraw item from the repository",
|
||||
@@ -3030,6 +3032,8 @@
|
||||
"search.results.empty": "Your search returned no results.",
|
||||
|
||||
|
||||
"default.search.results.head": "Search Results",
|
||||
|
||||
|
||||
"search.sidebar.close": "Back to results",
|
||||
|
||||
@@ -3063,8 +3067,21 @@
|
||||
|
||||
"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",
|
||||
|
@@ -78,4 +78,5 @@
|
||||
--ds-breadcrumb-link-active-color: #{darken($cyan, 30%)};
|
||||
|
||||
--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"
|
||||
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
|
||||
|
||||
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.4.0:
|
||||
version "4.11.9"
|
||||
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828"
|
||||
integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==
|
||||
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9:
|
||||
version "4.12.0"
|
||||
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88"
|
||||
integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==
|
||||
|
||||
bn.js@^5.0.0, bn.js@^5.1.1:
|
||||
version "5.1.3"
|
||||
@@ -2750,7 +2750,7 @@ braces@^3.0.1, braces@^3.0.2, braces@~3.0.2:
|
||||
dependencies:
|
||||
fill-range "^7.0.1"
|
||||
|
||||
brorand@^1.0.1:
|
||||
brorand@^1.0.1, brorand@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
|
||||
integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
|
||||
@@ -4695,17 +4695,17 @@ element-resize-detector@^1.2.1:
|
||||
batch-processor "1.0.0"
|
||||
|
||||
elliptic@^6.5.3:
|
||||
version "6.5.3"
|
||||
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6"
|
||||
integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==
|
||||
version "6.5.4"
|
||||
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb"
|
||||
integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==
|
||||
dependencies:
|
||||
bn.js "^4.4.0"
|
||||
brorand "^1.0.1"
|
||||
bn.js "^4.11.9"
|
||||
brorand "^1.1.0"
|
||||
hash.js "^1.0.0"
|
||||
hmac-drbg "^1.0.0"
|
||||
inherits "^2.0.1"
|
||||
minimalistic-assert "^1.0.0"
|
||||
minimalistic-crypto-utils "^1.0.0"
|
||||
hmac-drbg "^1.0.1"
|
||||
inherits "^2.0.4"
|
||||
minimalistic-assert "^1.0.1"
|
||||
minimalistic-crypto-utils "^1.0.1"
|
||||
|
||||
emoji-regex@^7.0.1:
|
||||
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"
|
||||
integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==
|
||||
|
||||
hmac-drbg@^1.0.0:
|
||||
hmac-drbg@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
|
||||
integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=
|
||||
@@ -11659,9 +11659,9 @@ sshpk@^1.7.0:
|
||||
tweetnacl "~0.14.0"
|
||||
|
||||
ssri@^6.0.0, ssri@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8"
|
||||
integrity sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==
|
||||
version "6.0.2"
|
||||
resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.2.tgz#157939134f20464e7301ddba3e90ffa8f7728ac5"
|
||||
integrity sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==
|
||||
dependencies:
|
||||
figgy-pudding "^3.5.1"
|
||||
|
||||
|
Reference in New Issue
Block a user