mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge branch 'DSpace:main' into patch-21-squashed
This commit is contained in:
@@ -18,6 +18,11 @@ describe('Edit Item > Edit Metadata tab', () => {
|
|||||||
// <ds-edit-item-page> tag must be loaded
|
// <ds-edit-item-page> tag must be loaded
|
||||||
cy.get('ds-edit-item-page').should('be.visible');
|
cy.get('ds-edit-item-page').should('be.visible');
|
||||||
|
|
||||||
|
// wait for all the ds-dso-edit-metadata-value components to be rendered
|
||||||
|
cy.get('ds-dso-edit-metadata-value div[role="row"]').each(($row: HTMLDivElement) => {
|
||||||
|
cy.wrap($row).find('div[role="cell"]').should('be.visible');
|
||||||
|
});
|
||||||
|
|
||||||
// Analyze <ds-edit-item-page> for accessibility issues
|
// Analyze <ds-edit-item-page> for accessibility issues
|
||||||
testA11y('ds-edit-item-page');
|
testA11y('ds-edit-item-page');
|
||||||
});
|
});
|
||||||
|
@@ -137,7 +137,7 @@ describe('New Submission page', () => {
|
|||||||
|
|
||||||
// Upload our DSpace logo via drag & drop onto submission form
|
// Upload our DSpace logo via drag & drop onto submission form
|
||||||
// cy.get('div#section_upload')
|
// cy.get('div#section_upload')
|
||||||
cy.get('div.ds-document-drop-zone').selectFile('src/assets/images/dspace-logo.png', {
|
cy.get('div.ds-document-drop-zone').selectFile('src/assets/images/dspace-logo.svg', {
|
||||||
action: 'drag-drop',
|
action: 'drag-drop',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -1,16 +1,13 @@
|
|||||||
import { AbstractControl } from '@angular/forms';
|
import { AbstractControl } from '@angular/forms';
|
||||||
import {
|
import { Route } from '@angular/router';
|
||||||
mapToCanActivate,
|
|
||||||
Route,
|
|
||||||
} from '@angular/router';
|
|
||||||
import {
|
import {
|
||||||
DYNAMIC_ERROR_MESSAGES_MATCHER,
|
DYNAMIC_ERROR_MESSAGES_MATCHER,
|
||||||
DynamicErrorMessagesMatcher,
|
DynamicErrorMessagesMatcher,
|
||||||
} from '@ng-dynamic-forms/core';
|
} from '@ng-dynamic-forms/core';
|
||||||
|
|
||||||
import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||||
import { GroupAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/group-administrator.guard';
|
import { groupAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/group-administrator.guard';
|
||||||
import { SiteAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
|
import { siteAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
|
||||||
import {
|
import {
|
||||||
EPERSON_PATH,
|
EPERSON_PATH,
|
||||||
GROUP_PATH,
|
GROUP_PATH,
|
||||||
@@ -20,7 +17,7 @@ import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.co
|
|||||||
import { EPersonFormComponent } from './epeople-registry/eperson-form/eperson-form.component';
|
import { EPersonFormComponent } from './epeople-registry/eperson-form/eperson-form.component';
|
||||||
import { EPersonResolver } from './epeople-registry/eperson-resolver.service';
|
import { EPersonResolver } from './epeople-registry/eperson-resolver.service';
|
||||||
import { GroupFormComponent } from './group-registry/group-form/group-form.component';
|
import { GroupFormComponent } from './group-registry/group-form/group-form.component';
|
||||||
import { GroupPageGuard } from './group-registry/group-page.guard';
|
import { groupPageGuard } from './group-registry/group-page.guard';
|
||||||
import { GroupsRegistryComponent } from './group-registry/groups-registry.component';
|
import { GroupsRegistryComponent } from './group-registry/groups-registry.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,7 +25,7 @@ import { GroupsRegistryComponent } from './group-registry/groups-registry.compon
|
|||||||
*/
|
*/
|
||||||
export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher =
|
export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher =
|
||||||
(control: AbstractControl, model: any, hasFocus: boolean) => {
|
(control: AbstractControl, model: any, hasFocus: boolean) => {
|
||||||
return (control.touched && !hasFocus) || (control.errors?.emailTaken && hasFocus);
|
return ( control.touched && !hasFocus ) || ( control.errors?.emailTaken && hasFocus );
|
||||||
};
|
};
|
||||||
|
|
||||||
const providers = [
|
const providers = [
|
||||||
@@ -46,7 +43,7 @@ export const ROUTES: Route[] = [
|
|||||||
},
|
},
|
||||||
providers,
|
providers,
|
||||||
data: { title: 'admin.access-control.epeople.title', breadcrumbKey: 'admin.access-control.epeople' },
|
data: { title: 'admin.access-control.epeople.title', breadcrumbKey: 'admin.access-control.epeople' },
|
||||||
canActivate: mapToCanActivate([SiteAdministratorGuard]),
|
canActivate: [siteAdministratorGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `${EPERSON_PATH}/create`,
|
path: `${EPERSON_PATH}/create`,
|
||||||
@@ -56,7 +53,7 @@ export const ROUTES: Route[] = [
|
|||||||
},
|
},
|
||||||
providers,
|
providers,
|
||||||
data: { title: 'admin.access-control.epeople.add.title', breadcrumbKey: 'admin.access-control.epeople.add' },
|
data: { title: 'admin.access-control.epeople.add.title', breadcrumbKey: 'admin.access-control.epeople.add' },
|
||||||
canActivate: mapToCanActivate([SiteAdministratorGuard]),
|
canActivate: [siteAdministratorGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `${EPERSON_PATH}/:id/edit`,
|
path: `${EPERSON_PATH}/:id/edit`,
|
||||||
@@ -67,7 +64,7 @@ export const ROUTES: Route[] = [
|
|||||||
},
|
},
|
||||||
providers,
|
providers,
|
||||||
data: { title: 'admin.access-control.epeople.edit.title', breadcrumbKey: 'admin.access-control.epeople.edit' },
|
data: { title: 'admin.access-control.epeople.edit.title', breadcrumbKey: 'admin.access-control.epeople.edit' },
|
||||||
canActivate: mapToCanActivate([SiteAdministratorGuard]),
|
canActivate: [siteAdministratorGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: GROUP_PATH,
|
path: GROUP_PATH,
|
||||||
@@ -77,7 +74,7 @@ export const ROUTES: Route[] = [
|
|||||||
},
|
},
|
||||||
providers,
|
providers,
|
||||||
data: { title: 'admin.access-control.groups.title', breadcrumbKey: 'admin.access-control.groups' },
|
data: { title: 'admin.access-control.groups.title', breadcrumbKey: 'admin.access-control.groups' },
|
||||||
canActivate: mapToCanActivate([GroupAdministratorGuard]),
|
canActivate: [groupAdministratorGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `${GROUP_PATH}/create`,
|
path: `${GROUP_PATH}/create`,
|
||||||
@@ -90,7 +87,7 @@ export const ROUTES: Route[] = [
|
|||||||
title: 'admin.access-control.groups.title.addGroup',
|
title: 'admin.access-control.groups.title.addGroup',
|
||||||
breadcrumbKey: 'admin.access-control.groups.addGroup',
|
breadcrumbKey: 'admin.access-control.groups.addGroup',
|
||||||
},
|
},
|
||||||
canActivate: mapToCanActivate([GroupAdministratorGuard]),
|
canActivate: [groupAdministratorGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `${GROUP_PATH}/:groupId/edit`,
|
path: `${GROUP_PATH}/:groupId/edit`,
|
||||||
@@ -103,7 +100,7 @@ export const ROUTES: Route[] = [
|
|||||||
title: 'admin.access-control.groups.title.singleGroup',
|
title: 'admin.access-control.groups.title.singleGroup',
|
||||||
breadcrumbKey: 'admin.access-control.groups.singleGroup',
|
breadcrumbKey: 'admin.access-control.groups.singleGroup',
|
||||||
},
|
},
|
||||||
canActivate: mapToCanActivate([GroupPageGuard]),
|
canActivate: [groupPageGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'bulk-access',
|
path: 'bulk-access',
|
||||||
@@ -112,6 +109,6 @@ export const ROUTES: Route[] = [
|
|||||||
breadcrumb: i18nBreadcrumbResolver,
|
breadcrumb: i18nBreadcrumbResolver,
|
||||||
},
|
},
|
||||||
data: { title: 'admin.access-control.bulk-access.title', breadcrumbKey: 'admin.access-control.bulk-access' },
|
data: { title: 'admin.access-control.bulk-access.title', breadcrumbKey: 'admin.access-control.bulk-access' },
|
||||||
canActivate: mapToCanActivate([SiteAdministratorGuard]),
|
canActivate: [siteAdministratorGuard],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@@ -20,25 +20,33 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let eperson of (ePeopleMembersOfGroup | async)?.page">
|
<tr *ngFor="let epersonDTO of (ePeopleMembersOfGroup | async)?.page">
|
||||||
<td class="align-middle">{{eperson.id}}</td>
|
<td class="align-middle">{{epersonDTO.eperson.id}}</td>
|
||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
<a [routerLink]="getEPersonEditRoute(eperson.id)">
|
<a [routerLink]="getEPersonEditRoute(epersonDTO.eperson.id)">
|
||||||
{{ dsoNameService.getName(eperson) }}
|
{{ dsoNameService.getName(epersonDTO.eperson) }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
{{messagePrefix + '.table.email' | translate}}: {{ eperson.email ? eperson.email : '-' }}<br/>
|
{{messagePrefix + '.table.email' | translate}}: {{ epersonDTO.eperson.email ? epersonDTO.eperson.email : '-' }}<br/>
|
||||||
{{messagePrefix + '.table.netid' | translate}}: {{ eperson.netid ? eperson.netid : '-' }}
|
{{messagePrefix + '.table.netid' | translate}}: {{ epersonDTO.eperson.netid ? epersonDTO.eperson.netid : '-' }}
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
<div class="btn-group edit-field">
|
<div class="btn-group edit-field">
|
||||||
<button (click)="deleteMemberFromGroup(eperson)"
|
<button (click)="deleteMemberFromGroup(epersonDTO.eperson)"
|
||||||
|
*ngIf="epersonDTO.ableToDelete"
|
||||||
[disabled]="actionConfig.remove.disabled"
|
[disabled]="actionConfig.remove.disabled"
|
||||||
[ngClass]="['btn btn-sm', actionConfig.remove.css]"
|
[ngClass]="['btn btn-sm', actionConfig.remove.css]"
|
||||||
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(eperson) } }}">
|
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(epersonDTO.eperson) } }}">
|
||||||
<i [ngClass]="actionConfig.remove.icon"></i>
|
<i [ngClass]="actionConfig.remove.icon"></i>
|
||||||
</button>
|
</button>
|
||||||
|
<button *ngIf="!epersonDTO.ableToDelete"
|
||||||
|
(click)="addMemberToGroup(epersonDTO.eperson)"
|
||||||
|
[disabled]="actionConfig.add.disabled"
|
||||||
|
[ngClass]="['btn btn-sm', actionConfig.add.css]"
|
||||||
|
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(epersonDTO.eperson) } }}">
|
||||||
|
<i [ngClass]="actionConfig.add.icon"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@@ -222,13 +222,13 @@ describe('MembersListComponent', () => {
|
|||||||
|
|
||||||
describe('if first delete button is pressed', () => {
|
describe('if first delete button is pressed', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
spyOn(component, 'search').and.callThrough();
|
||||||
const deleteButton: DebugElement = fixture.debugElement.query(By.css('#ePeopleMembersOfGroup tbody .fa-trash-alt'));
|
const deleteButton: DebugElement = fixture.debugElement.query(By.css('#ePeopleMembersOfGroup tbody .fa-trash-alt'));
|
||||||
deleteButton.nativeElement.click();
|
deleteButton.nativeElement.click();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
it('then no ePerson remains as a member of the active group.', () => {
|
it('should trigger the search to add the user back to the search table', () => {
|
||||||
const epersonsFound = fixture.debugElement.queryAll(By.css('#ePeopleMembersOfGroup tbody tr'));
|
expect(component.search).toHaveBeenCalled();
|
||||||
expect(epersonsFound.length).toEqual(0);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -264,13 +264,13 @@ describe('MembersListComponent', () => {
|
|||||||
|
|
||||||
describe('if first add button is pressed', () => {
|
describe('if first add button is pressed', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
spyOn(component, 'search').and.callThrough();
|
||||||
const addButton: DebugElement = fixture.debugElement.query(By.css('#epersonsSearch tbody .fa-plus'));
|
const addButton: DebugElement = fixture.debugElement.query(By.css('#epersonsSearch tbody .fa-plus'));
|
||||||
addButton.nativeElement.click();
|
addButton.nativeElement.click();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
it('then all (two) ePersons are member of the active group. No non-members left', () => {
|
it('should trigger the search to remove the user from the search table', () => {
|
||||||
epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr'));
|
expect(component.search).toHaveBeenCalled();
|
||||||
expect(epersonsFound.length).toEqual(0);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -24,21 +24,29 @@ import {
|
|||||||
} from '@ngx-translate/core';
|
} from '@ngx-translate/core';
|
||||||
import {
|
import {
|
||||||
BehaviorSubject,
|
BehaviorSubject,
|
||||||
|
combineLatest as observableCombineLatest,
|
||||||
Observable,
|
Observable,
|
||||||
|
ObservedValueOf,
|
||||||
|
of as observableOf,
|
||||||
Subscription,
|
Subscription,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import {
|
import {
|
||||||
|
defaultIfEmpty,
|
||||||
map,
|
map,
|
||||||
switchMap,
|
switchMap,
|
||||||
take,
|
take,
|
||||||
} from 'rxjs/operators';
|
} from 'rxjs/operators';
|
||||||
|
|
||||||
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
|
||||||
import { PaginatedList } from '../../../../core/data/paginated-list.model';
|
import {
|
||||||
|
buildPaginatedList,
|
||||||
|
PaginatedList,
|
||||||
|
} from '../../../../core/data/paginated-list.model';
|
||||||
import { RemoteData } from '../../../../core/data/remote-data';
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
|
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
|
||||||
import { GroupDataService } from '../../../../core/eperson/group-data.service';
|
import { GroupDataService } from '../../../../core/eperson/group-data.service';
|
||||||
import { EPerson } from '../../../../core/eperson/models/eperson.model';
|
import { EPerson } from '../../../../core/eperson/models/eperson.model';
|
||||||
|
import { EpersonDtoModel } from '../../../../core/eperson/models/eperson-dto.model';
|
||||||
import { Group } from '../../../../core/eperson/models/group.model';
|
import { Group } from '../../../../core/eperson/models/group.model';
|
||||||
import { PaginationService } from '../../../../core/pagination/pagination.service';
|
import { PaginationService } from '../../../../core/pagination/pagination.service';
|
||||||
import {
|
import {
|
||||||
@@ -137,7 +145,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* List of EPeople members of currently active group being edited
|
* List of EPeople members of currently active group being edited
|
||||||
*/
|
*/
|
||||||
ePeopleMembersOfGroup: BehaviorSubject<PaginatedList<EPerson>> = new BehaviorSubject<PaginatedList<EPerson>>(undefined);
|
ePeopleMembersOfGroup: BehaviorSubject<PaginatedList<EpersonDtoModel>> = new BehaviorSubject(undefined);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pagination config used to display the list of EPeople that are result of EPeople search
|
* Pagination config used to display the list of EPeople that are result of EPeople search
|
||||||
@@ -226,10 +234,35 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
return rd;
|
return rd;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
getRemoteDataPayload())
|
switchMap((epersonListRD: RemoteData<PaginatedList<EPerson>>) => {
|
||||||
.subscribe((paginatedListOfEPersons: PaginatedList<EPerson>) => {
|
const dtos$ = observableCombineLatest([...epersonListRD.payload.page.map((member: EPerson) => {
|
||||||
this.ePeopleMembersOfGroup.next(paginatedListOfEPersons);
|
const dto$: Observable<EpersonDtoModel> = observableCombineLatest(
|
||||||
}));
|
this.isMemberOfGroup(member), (isMember: ObservedValueOf<Observable<boolean>>) => {
|
||||||
|
const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel();
|
||||||
|
epersonDtoModel.eperson = member;
|
||||||
|
epersonDtoModel.ableToDelete = isMember;
|
||||||
|
return epersonDtoModel;
|
||||||
|
});
|
||||||
|
return dto$;
|
||||||
|
})]);
|
||||||
|
return dtos$.pipe(defaultIfEmpty([]), map((dtos: EpersonDtoModel[]) => {
|
||||||
|
return buildPaginatedList(epersonListRD.payload.pageInfo, dtos);
|
||||||
|
}));
|
||||||
|
}),
|
||||||
|
).subscribe((paginatedListOfDTOs: PaginatedList<EpersonDtoModel>) => {
|
||||||
|
this.ePeopleMembersOfGroup.next(paginatedListOfDTOs);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We always return true since this is only used by the top section (which represents all the users part of the group
|
||||||
|
* in {@link MembersListComponent})
|
||||||
|
*
|
||||||
|
* @param possibleMember EPerson that is a possible member (being tested) of the group currently being edited
|
||||||
|
*/
|
||||||
|
isMemberOfGroup(possibleMember: EPerson): Observable<boolean> {
|
||||||
|
return observableOf(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,14 +1,24 @@
|
|||||||
|
import {
|
||||||
|
TestBed,
|
||||||
|
waitForAsync,
|
||||||
|
} from '@angular/core/testing';
|
||||||
import {
|
import {
|
||||||
ActivatedRouteSnapshot,
|
ActivatedRouteSnapshot,
|
||||||
Router,
|
Router,
|
||||||
|
UrlTree,
|
||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
import { of as observableOf } from 'rxjs';
|
import {
|
||||||
|
Observable,
|
||||||
|
of as observableOf,
|
||||||
|
} from 'rxjs';
|
||||||
|
|
||||||
import { AuthService } from '../../core/auth/auth.service';
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
|
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
|
||||||
import { GroupPageGuard } from './group-page.guard';
|
import { groupPageGuard } from './group-page.guard';
|
||||||
|
|
||||||
|
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; // Increase timeout to 10 seconds
|
||||||
|
|
||||||
describe('GroupPageGuard', () => {
|
describe('GroupPageGuard', () => {
|
||||||
const groupsEndpointUrl = 'https://test.org/api/eperson/groups';
|
const groupsEndpointUrl = 'https://test.org/api/eperson/groups';
|
||||||
@@ -20,42 +30,54 @@ describe('GroupPageGuard', () => {
|
|||||||
},
|
},
|
||||||
} as unknown as ActivatedRouteSnapshot;
|
} as unknown as ActivatedRouteSnapshot;
|
||||||
|
|
||||||
let guard: GroupPageGuard;
|
|
||||||
let halEndpointService: HALEndpointService;
|
let halEndpointService: HALEndpointService;
|
||||||
let authorizationService: AuthorizationDataService;
|
let authorizationService: AuthorizationDataService;
|
||||||
let router: Router;
|
let router: Router;
|
||||||
let authService: AuthService;
|
let authService: AuthService;
|
||||||
|
|
||||||
beforeEach(() => {
|
function init() {
|
||||||
halEndpointService = jasmine.createSpyObj(['getEndpoint']);
|
halEndpointService = jasmine.createSpyObj(['getEndpoint']);
|
||||||
(halEndpointService as any).getEndpoint.and.returnValue(observableOf(groupsEndpointUrl));
|
( halEndpointService as any ).getEndpoint.and.returnValue(observableOf(groupsEndpointUrl));
|
||||||
|
|
||||||
authorizationService = jasmine.createSpyObj(['isAuthorized']);
|
authorizationService = jasmine.createSpyObj(['isAuthorized']);
|
||||||
// NOTE: value is set in beforeEach
|
// NOTE: value is set in beforeEach
|
||||||
|
|
||||||
router = jasmine.createSpyObj(['parseUrl']);
|
router = jasmine.createSpyObj(['parseUrl']);
|
||||||
(router as any).parseUrl.and.returnValue = {};
|
( router as any ).parseUrl.and.returnValue = {};
|
||||||
|
|
||||||
authService = jasmine.createSpyObj(['isAuthenticated']);
|
authService = jasmine.createSpyObj(['isAuthenticated']);
|
||||||
(authService as any).isAuthenticated.and.returnValue(observableOf(true));
|
( authService as any ).isAuthenticated.and.returnValue(observableOf(true));
|
||||||
|
|
||||||
guard = new GroupPageGuard(halEndpointService, authorizationService, router, authService);
|
TestBed.configureTestingModule({
|
||||||
});
|
providers: [
|
||||||
|
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||||
|
{ provide: Router, useValue: router },
|
||||||
|
{ provide: AuthService, useValue: authService },
|
||||||
|
{ provide: HALEndpointService, useValue: halEndpointService },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
init();
|
||||||
|
}));
|
||||||
|
|
||||||
it('should be created', () => {
|
it('should be created', () => {
|
||||||
expect(guard).toBeTruthy();
|
expect(groupPageGuard).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('canActivate', () => {
|
describe('canActivate', () => {
|
||||||
describe('when the current user can manage the group', () => {
|
describe('when the current user can manage the group', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
(authorizationService as any).isAuthorized.and.returnValue(observableOf(true));
|
( authorizationService as any ).isAuthorized.and.returnValue(observableOf(true));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return true', (done) => {
|
it('should return true', (done) => {
|
||||||
guard.canActivate(
|
const result$ = TestBed.runInInjectionContext(() => {
|
||||||
routeSnapshotWithGroupId, { url: 'current-url' } as any,
|
return groupPageGuard()(routeSnapshotWithGroupId, { url: 'current-url' } as any);
|
||||||
).subscribe((result) => {
|
}) as Observable<boolean | UrlTree>;
|
||||||
|
|
||||||
|
result$.subscribe((result) => {
|
||||||
expect(authorizationService.isAuthorized).toHaveBeenCalledWith(
|
expect(authorizationService.isAuthorized).toHaveBeenCalledWith(
|
||||||
FeatureID.CanManageGroup, groupEndpointUrl, undefined,
|
FeatureID.CanManageGroup, groupEndpointUrl, undefined,
|
||||||
);
|
);
|
||||||
@@ -71,15 +93,18 @@ describe('GroupPageGuard', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not return true', (done) => {
|
it('should not return true', (done) => {
|
||||||
guard.canActivate(
|
const result$ = TestBed.runInInjectionContext(() => {
|
||||||
routeSnapshotWithGroupId, { url: 'current-url' } as any,
|
return groupPageGuard()(routeSnapshotWithGroupId, { url: 'current-url' } as any);
|
||||||
).subscribe((result) => {
|
}) as Observable<boolean | UrlTree>;
|
||||||
|
|
||||||
|
result$.subscribe((result) => {
|
||||||
expect(authorizationService.isAuthorized).toHaveBeenCalledWith(
|
expect(authorizationService.isAuthorized).toHaveBeenCalledWith(
|
||||||
FeatureID.CanManageGroup, groupEndpointUrl, undefined,
|
FeatureID.CanManageGroup, groupEndpointUrl, undefined,
|
||||||
);
|
);
|
||||||
expect(result).not.toBeTrue();
|
expect(result).not.toBeTrue();
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { inject } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
ActivatedRouteSnapshot,
|
ActivatedRouteSnapshot,
|
||||||
Router,
|
CanActivateFn,
|
||||||
RouterStateSnapshot,
|
RouterStateSnapshot,
|
||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
import {
|
import {
|
||||||
@@ -10,34 +10,29 @@ import {
|
|||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
import { AuthService } from '../../core/auth/auth.service';
|
import {
|
||||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
someFeatureAuthorizationGuard,
|
||||||
import { SomeFeatureAuthorizationGuard } from '../../core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard';
|
StringGuardParamFn,
|
||||||
|
} from '../../core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard';
|
||||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
|
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
|
||||||
|
|
||||||
@Injectable({
|
const defaultGroupPageGetObjectUrl: StringGuardParamFn = (
|
||||||
providedIn: 'root',
|
route: ActivatedRouteSnapshot,
|
||||||
})
|
state: RouterStateSnapshot,
|
||||||
export class GroupPageGuard extends SomeFeatureAuthorizationGuard {
|
): Observable<string> => {
|
||||||
|
const halEndpointService = inject(HALEndpointService);
|
||||||
|
const groupsEndpoint = 'groups';
|
||||||
|
|
||||||
protected groupsEndpoint = 'groups';
|
return halEndpointService.getEndpoint(groupsEndpoint).pipe(
|
||||||
|
map(groupsUrl => `${groupsUrl}/${route?.params?.groupId}`),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
constructor(protected halEndpointService: HALEndpointService,
|
export const groupPageGuard = (
|
||||||
protected authorizationService: AuthorizationDataService,
|
getObjectUrl = defaultGroupPageGetObjectUrl,
|
||||||
protected router: Router,
|
getEPersonUuid?: StringGuardParamFn,
|
||||||
protected authService: AuthService) {
|
): CanActivateFn => someFeatureAuthorizationGuard(
|
||||||
super(authorizationService, router, authService);
|
() => observableOf([FeatureID.CanManageGroup]),
|
||||||
}
|
getObjectUrl,
|
||||||
|
getEPersonUuid);
|
||||||
getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID[]> {
|
|
||||||
return observableOf([FeatureID.CanManageGroup]);
|
|
||||||
}
|
|
||||||
|
|
||||||
getObjectUrl(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<string> {
|
|
||||||
return this.halEndpointService.getEndpoint(this.groupsEndpoint).pipe(
|
|
||||||
map(groupsUrl => `${groupsUrl}/${route?.params?.groupId}`),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
@@ -1,18 +1,15 @@
|
|||||||
import {
|
import { Route } from '@angular/router';
|
||||||
mapToCanActivate,
|
|
||||||
Route,
|
|
||||||
} from '@angular/router';
|
|
||||||
|
|
||||||
import { i18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
|
import { i18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||||
import { notifyInfoGuard } from '../../core/coar-notify/notify-info/notify-info.guard';
|
import { notifyInfoGuard } from '../../core/coar-notify/notify-info/notify-info.guard';
|
||||||
import { SiteAdministratorGuard } from '../../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
|
import { siteAdministratorGuard } from '../../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
|
||||||
import { AdminNotifyDashboardComponent } from './admin-notify-dashboard.component';
|
import { AdminNotifyDashboardComponent } from './admin-notify-dashboard.component';
|
||||||
import { AdminNotifyIncomingComponent } from './admin-notify-logs/admin-notify-incoming/admin-notify-incoming.component';
|
import { AdminNotifyIncomingComponent } from './admin-notify-logs/admin-notify-incoming/admin-notify-incoming.component';
|
||||||
import { AdminNotifyOutgoingComponent } from './admin-notify-logs/admin-notify-outgoing/admin-notify-outgoing.component';
|
import { AdminNotifyOutgoingComponent } from './admin-notify-logs/admin-notify-outgoing/admin-notify-outgoing.component';
|
||||||
|
|
||||||
export const ROUTES: Route[] = [
|
export const ROUTES: Route[] = [
|
||||||
{
|
{
|
||||||
canActivate: [...mapToCanActivate([SiteAdministratorGuard]), notifyInfoGuard],
|
canActivate: [siteAdministratorGuard, notifyInfoGuard],
|
||||||
path: '',
|
path: '',
|
||||||
resolve: {
|
resolve: {
|
||||||
breadcrumb: i18nBreadcrumbResolver,
|
breadcrumb: i18nBreadcrumbResolver,
|
||||||
@@ -30,7 +27,7 @@ export const ROUTES: Route[] = [
|
|||||||
breadcrumb: i18nBreadcrumbResolver,
|
breadcrumb: i18nBreadcrumbResolver,
|
||||||
},
|
},
|
||||||
component: AdminNotifyIncomingComponent,
|
component: AdminNotifyIncomingComponent,
|
||||||
canActivate: [...mapToCanActivate([SiteAdministratorGuard]), notifyInfoGuard],
|
canActivate: [siteAdministratorGuard, notifyInfoGuard],
|
||||||
data: {
|
data: {
|
||||||
title: 'admin.notify.dashboard.page.title',
|
title: 'admin.notify.dashboard.page.title',
|
||||||
breadcrumbKey: 'admin.notify.dashboard',
|
breadcrumbKey: 'admin.notify.dashboard',
|
||||||
@@ -42,7 +39,7 @@ export const ROUTES: Route[] = [
|
|||||||
breadcrumb: i18nBreadcrumbResolver,
|
breadcrumb: i18nBreadcrumbResolver,
|
||||||
},
|
},
|
||||||
component: AdminNotifyOutgoingComponent,
|
component: AdminNotifyOutgoingComponent,
|
||||||
canActivate: [...mapToCanActivate([SiteAdministratorGuard]), notifyInfoGuard],
|
canActivate: [siteAdministratorGuard, notifyInfoGuard],
|
||||||
data: {
|
data: {
|
||||||
title: 'admin.notify.dashboard.page.title',
|
title: 'admin.notify.dashboard.page.title',
|
||||||
breadcrumbKey: 'admin.notify.dashboard',
|
breadcrumbKey: 'admin.notify.dashboard',
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
InMemoryScrollingOptions,
|
InMemoryScrollingOptions,
|
||||||
mapToCanActivate,
|
|
||||||
Route,
|
Route,
|
||||||
RouterConfigOptions,
|
RouterConfigOptions,
|
||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
@@ -26,12 +25,12 @@ import { COLLECTION_MODULE_PATH } from './collection-page/collection-page-routin
|
|||||||
import { COMMUNITY_MODULE_PATH } from './community-page/community-page-routing-paths';
|
import { COMMUNITY_MODULE_PATH } from './community-page/community-page-routing-paths';
|
||||||
import { authBlockingGuard } from './core/auth/auth-blocking.guard';
|
import { authBlockingGuard } from './core/auth/auth-blocking.guard';
|
||||||
import { authenticatedGuard } from './core/auth/authenticated.guard';
|
import { authenticatedGuard } from './core/auth/authenticated.guard';
|
||||||
import { GroupAdministratorGuard } from './core/data/feature-authorization/feature-authorization-guard/group-administrator.guard';
|
import { groupAdministratorGuard } from './core/data/feature-authorization/feature-authorization-guard/group-administrator.guard';
|
||||||
import { SiteAdministratorGuard } from './core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
|
import { siteAdministratorGuard } from './core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
|
||||||
import { SiteRegisterGuard } from './core/data/feature-authorization/feature-authorization-guard/site-register.guard';
|
import { siteRegisterGuard } from './core/data/feature-authorization/feature-authorization-guard/site-register.guard';
|
||||||
import { EndUserAgreementCurrentUserGuard } from './core/end-user-agreement/end-user-agreement-current-user.guard';
|
import { endUserAgreementCurrentUserGuard } from './core/end-user-agreement/end-user-agreement-current-user.guard';
|
||||||
import { reloadGuard } from './core/reload/reload.guard';
|
import { reloadGuard } from './core/reload/reload.guard';
|
||||||
import { ForgotPasswordCheckGuard } from './core/rest-property/forgot-password-check-guard.guard';
|
import { forgotPasswordCheckGuard } from './core/rest-property/forgot-password-check-guard.guard';
|
||||||
import { ServerCheckGuard } from './core/server-check/server-check.guard';
|
import { ServerCheckGuard } from './core/server-check/server-check.guard';
|
||||||
import { ThemedForbiddenComponent } from './forbidden/themed-forbidden.component';
|
import { ThemedForbiddenComponent } from './forbidden/themed-forbidden.component';
|
||||||
import { ITEM_MODULE_PATH } from './item-page/item-page-routing-paths';
|
import { ITEM_MODULE_PATH } from './item-page/item-page-routing-paths';
|
||||||
@@ -66,105 +65,105 @@ export const APP_ROUTES: Route[] = [
|
|||||||
.then((m) => m.ROUTES),
|
.then((m) => m.ROUTES),
|
||||||
data: { showBreadcrumbs: false },
|
data: { showBreadcrumbs: false },
|
||||||
providers: [provideSuggestionNotificationsState()],
|
providers: [provideSuggestionNotificationsState()],
|
||||||
canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]),
|
canActivate: [endUserAgreementCurrentUserGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'community-list',
|
path: 'community-list',
|
||||||
loadChildren: () => import('./community-list-page/community-list-page-routes')
|
loadChildren: () => import('./community-list-page/community-list-page-routes')
|
||||||
.then((m) => m.ROUTES),
|
.then((m) => m.ROUTES),
|
||||||
canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]),
|
canActivate: [endUserAgreementCurrentUserGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'id',
|
path: 'id',
|
||||||
loadChildren: () => import('./lookup-by-id/lookup-by-id-routes')
|
loadChildren: () => import('./lookup-by-id/lookup-by-id-routes')
|
||||||
.then((m) => m.ROUTES),
|
.then((m) => m.ROUTES),
|
||||||
canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]),
|
canActivate: [endUserAgreementCurrentUserGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'handle',
|
path: 'handle',
|
||||||
loadChildren: () => import('./lookup-by-id/lookup-by-id-routes')
|
loadChildren: () => import('./lookup-by-id/lookup-by-id-routes')
|
||||||
.then((m) => m.ROUTES),
|
.then((m) => m.ROUTES),
|
||||||
canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]),
|
canActivate: [endUserAgreementCurrentUserGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: REGISTER_PATH,
|
path: REGISTER_PATH,
|
||||||
loadChildren: () => import('./register-page/register-page-routes')
|
loadChildren: () => import('./register-page/register-page-routes')
|
||||||
.then((m) => m.ROUTES),
|
.then((m) => m.ROUTES),
|
||||||
canActivate: mapToCanActivate([SiteRegisterGuard]),
|
canActivate: [siteRegisterGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: FORGOT_PASSWORD_PATH,
|
path: FORGOT_PASSWORD_PATH,
|
||||||
loadChildren: () => import('./forgot-password/forgot-password-routes')
|
loadChildren: () => import('./forgot-password/forgot-password-routes')
|
||||||
.then((m) => m.ROUTES),
|
.then((m) => m.ROUTES),
|
||||||
canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard, ForgotPasswordCheckGuard]),
|
canActivate: [endUserAgreementCurrentUserGuard, forgotPasswordCheckGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: COMMUNITY_MODULE_PATH,
|
path: COMMUNITY_MODULE_PATH,
|
||||||
loadChildren: () => import('./community-page/community-page-routes')
|
loadChildren: () => import('./community-page/community-page-routes')
|
||||||
.then((m) => m.ROUTES),
|
.then((m) => m.ROUTES),
|
||||||
canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]),
|
canActivate: [endUserAgreementCurrentUserGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: COLLECTION_MODULE_PATH,
|
path: COLLECTION_MODULE_PATH,
|
||||||
loadChildren: () => import('./collection-page/collection-page-routes')
|
loadChildren: () => import('./collection-page/collection-page-routes')
|
||||||
.then((m) => m.ROUTES),
|
.then((m) => m.ROUTES),
|
||||||
canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]),
|
canActivate: [endUserAgreementCurrentUserGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ITEM_MODULE_PATH,
|
path: ITEM_MODULE_PATH,
|
||||||
loadChildren: () => import('./item-page/item-page-routes')
|
loadChildren: () => import('./item-page/item-page-routes')
|
||||||
.then((m) => m.ROUTES),
|
.then((m) => m.ROUTES),
|
||||||
canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]),
|
canActivate: [endUserAgreementCurrentUserGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'entities/:entity-type',
|
path: 'entities/:entity-type',
|
||||||
loadChildren: () => import('./item-page/item-page-routes')
|
loadChildren: () => import('./item-page/item-page-routes')
|
||||||
.then((m) => m.ROUTES),
|
.then((m) => m.ROUTES),
|
||||||
canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]),
|
canActivate: [endUserAgreementCurrentUserGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: LEGACY_BITSTREAM_MODULE_PATH,
|
path: LEGACY_BITSTREAM_MODULE_PATH,
|
||||||
loadChildren: () => import('./bitstream-page/bitstream-page-routes')
|
loadChildren: () => import('./bitstream-page/bitstream-page-routes')
|
||||||
.then((m) => m.ROUTES),
|
.then((m) => m.ROUTES),
|
||||||
canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]),
|
canActivate: [endUserAgreementCurrentUserGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: BITSTREAM_MODULE_PATH,
|
path: BITSTREAM_MODULE_PATH,
|
||||||
loadChildren: () => import('./bitstream-page/bitstream-page-routes')
|
loadChildren: () => import('./bitstream-page/bitstream-page-routes')
|
||||||
.then((m) => m.ROUTES),
|
.then((m) => m.ROUTES),
|
||||||
canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]),
|
canActivate: [endUserAgreementCurrentUserGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'mydspace',
|
path: 'mydspace',
|
||||||
loadChildren: () => import('./my-dspace-page/my-dspace-page-routes')
|
loadChildren: () => import('./my-dspace-page/my-dspace-page-routes')
|
||||||
.then((m) => m.ROUTES),
|
.then((m) => m.ROUTES),
|
||||||
providers: [provideSuggestionNotificationsState()],
|
providers: [provideSuggestionNotificationsState()],
|
||||||
canActivate: [authenticatedGuard, ...mapToCanActivate([EndUserAgreementCurrentUserGuard])],
|
canActivate: [authenticatedGuard, endUserAgreementCurrentUserGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'search',
|
path: 'search',
|
||||||
loadChildren: () => import('./search-page/search-page-routes')
|
loadChildren: () => import('./search-page/search-page-routes')
|
||||||
.then((m) => m.ROUTES),
|
.then((m) => m.ROUTES),
|
||||||
canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]),
|
canActivate: [endUserAgreementCurrentUserGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'browse',
|
path: 'browse',
|
||||||
loadChildren: () => import('./browse-by/browse-by-page-routes')
|
loadChildren: () => import('./browse-by/browse-by-page-routes')
|
||||||
.then((m) => m.ROUTES),
|
.then((m) => m.ROUTES),
|
||||||
canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]),
|
canActivate: [endUserAgreementCurrentUserGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ADMIN_MODULE_PATH,
|
path: ADMIN_MODULE_PATH,
|
||||||
loadChildren: () => import('./admin/admin-routes')
|
loadChildren: () => import('./admin/admin-routes')
|
||||||
.then((m) => m.ROUTES),
|
.then((m) => m.ROUTES),
|
||||||
canActivate: mapToCanActivate([SiteAdministratorGuard, EndUserAgreementCurrentUserGuard]),
|
canActivate: [siteAdministratorGuard, endUserAgreementCurrentUserGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: NOTIFICATIONS_MODULE_PATH,
|
path: NOTIFICATIONS_MODULE_PATH,
|
||||||
loadChildren: () => import('./quality-assurance-notifications-pages/notifications-pages-routes')
|
loadChildren: () => import('./quality-assurance-notifications-pages/notifications-pages-routes')
|
||||||
.then((m) => m.ROUTES),
|
.then((m) => m.ROUTES),
|
||||||
providers: [provideSuggestionNotificationsState()],
|
providers: [provideSuggestionNotificationsState()],
|
||||||
canActivate: [authenticatedGuard, ...mapToCanActivate([EndUserAgreementCurrentUserGuard])],
|
canActivate: [authenticatedGuard, endUserAgreementCurrentUserGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'login',
|
path: 'login',
|
||||||
@@ -181,47 +180,47 @@ export const APP_ROUTES: Route[] = [
|
|||||||
loadChildren: () => import('./submit-page/submit-page-routes')
|
loadChildren: () => import('./submit-page/submit-page-routes')
|
||||||
.then((m) => m.ROUTES),
|
.then((m) => m.ROUTES),
|
||||||
providers: [provideSubmissionState()],
|
providers: [provideSubmissionState()],
|
||||||
canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]),
|
canActivate: [endUserAgreementCurrentUserGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'import-external',
|
path: 'import-external',
|
||||||
loadChildren: () => import('./import-external-page/import-external-page-routes')
|
loadChildren: () => import('./import-external-page/import-external-page-routes')
|
||||||
.then((m) => m.ROUTES),
|
.then((m) => m.ROUTES),
|
||||||
canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]),
|
canActivate: [endUserAgreementCurrentUserGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'workspaceitems',
|
path: 'workspaceitems',
|
||||||
loadChildren: () => import('./workspaceitems-edit-page/workspaceitems-edit-page-routes')
|
loadChildren: () => import('./workspaceitems-edit-page/workspaceitems-edit-page-routes')
|
||||||
.then((m) => m.ROUTES),
|
.then((m) => m.ROUTES),
|
||||||
providers: [provideSubmissionState()],
|
providers: [provideSubmissionState()],
|
||||||
canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]),
|
canActivate: [endUserAgreementCurrentUserGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: WORKFLOW_ITEM_MODULE_PATH,
|
path: WORKFLOW_ITEM_MODULE_PATH,
|
||||||
providers: [provideSubmissionState()],
|
providers: [provideSubmissionState()],
|
||||||
loadChildren: () => import('./workflowitems-edit-page/workflowitems-edit-page-routes')
|
loadChildren: () => import('./workflowitems-edit-page/workflowitems-edit-page-routes')
|
||||||
.then((m) => m.ROUTES),
|
.then((m) => m.ROUTES),
|
||||||
canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]),
|
canActivate: [endUserAgreementCurrentUserGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: PROFILE_MODULE_PATH,
|
path: PROFILE_MODULE_PATH,
|
||||||
loadChildren: () => import('./profile-page/profile-page-routes')
|
loadChildren: () => import('./profile-page/profile-page-routes')
|
||||||
.then((m) => m.ROUTES),
|
.then((m) => m.ROUTES),
|
||||||
providers: [provideSuggestionNotificationsState()],
|
providers: [provideSuggestionNotificationsState()],
|
||||||
canActivate: [authenticatedGuard, ...mapToCanActivate([EndUserAgreementCurrentUserGuard])],
|
canActivate: [authenticatedGuard, endUserAgreementCurrentUserGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: PROCESS_MODULE_PATH,
|
path: PROCESS_MODULE_PATH,
|
||||||
loadChildren: () => import('./process-page/process-page-routes')
|
loadChildren: () => import('./process-page/process-page-routes')
|
||||||
.then((m) => m.ROUTES),
|
.then((m) => m.ROUTES),
|
||||||
canActivate: [authenticatedGuard, ...mapToCanActivate([EndUserAgreementCurrentUserGuard])],
|
canActivate: [authenticatedGuard, endUserAgreementCurrentUserGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: SUGGESTION_MODULE_PATH,
|
path: SUGGESTION_MODULE_PATH,
|
||||||
loadChildren: () => import('./suggestions-page/suggestions-page-routes')
|
loadChildren: () => import('./suggestions-page/suggestions-page-routes')
|
||||||
.then((m) => m.ROUTES),
|
.then((m) => m.ROUTES),
|
||||||
providers: [provideSuggestionNotificationsState()],
|
providers: [provideSuggestionNotificationsState()],
|
||||||
canActivate: [authenticatedGuard, ...mapToCanActivate([EndUserAgreementCurrentUserGuard])],
|
canActivate: [authenticatedGuard, endUserAgreementCurrentUserGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: INFO_MODULE_PATH,
|
path: INFO_MODULE_PATH,
|
||||||
@@ -230,7 +229,7 @@ export const APP_ROUTES: Route[] = [
|
|||||||
{
|
{
|
||||||
path: REQUEST_COPY_MODULE_PATH,
|
path: REQUEST_COPY_MODULE_PATH,
|
||||||
loadChildren: () => import('./request-copy/request-copy-routes').then((m) => m.ROUTES),
|
loadChildren: () => import('./request-copy/request-copy-routes').then((m) => m.ROUTES),
|
||||||
canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]),
|
canActivate: [endUserAgreementCurrentUserGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: FORBIDDEN_PATH,
|
path: FORBIDDEN_PATH,
|
||||||
@@ -240,7 +239,7 @@ export const APP_ROUTES: Route[] = [
|
|||||||
path: 'statistics',
|
path: 'statistics',
|
||||||
loadChildren: () => import('./statistics-page/statistics-page-routes')
|
loadChildren: () => import('./statistics-page/statistics-page-routes')
|
||||||
.then((m) => m.ROUTES),
|
.then((m) => m.ROUTES),
|
||||||
canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]),
|
canActivate: [endUserAgreementCurrentUserGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: HEALTH_PAGE_PATH,
|
path: HEALTH_PAGE_PATH,
|
||||||
@@ -250,7 +249,7 @@ export const APP_ROUTES: Route[] = [
|
|||||||
{
|
{
|
||||||
path: ACCESS_CONTROL_MODULE_PATH,
|
path: ACCESS_CONTROL_MODULE_PATH,
|
||||||
loadChildren: () => import('./access-control/access-control-routes').then((m) => m.ROUTES),
|
loadChildren: () => import('./access-control/access-control-routes').then((m) => m.ROUTES),
|
||||||
canActivate: mapToCanActivate([GroupAdministratorGuard, EndUserAgreementCurrentUserGuard]),
|
canActivate: [groupAdministratorGuard, endUserAgreementCurrentUserGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'subscriptions',
|
path: 'subscriptions',
|
||||||
|
@@ -32,6 +32,7 @@ import {
|
|||||||
Observable,
|
Observable,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import {
|
import {
|
||||||
|
delay,
|
||||||
distinctUntilChanged,
|
distinctUntilChanged,
|
||||||
take,
|
take,
|
||||||
withLatestFrom,
|
withLatestFrom,
|
||||||
@@ -136,7 +137,10 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit() {
|
ngAfterViewInit() {
|
||||||
this.router.events.subscribe((event) => {
|
this.router.events.pipe(
|
||||||
|
// delay(0) to prevent "Expression has changed after it was checked" errors
|
||||||
|
delay(0),
|
||||||
|
).subscribe((event) => {
|
||||||
if (event instanceof NavigationStart) {
|
if (event instanceof NavigationStart) {
|
||||||
distinctNext(this.isRouteLoading$, true);
|
distinctNext(this.isRouteLoading$, true);
|
||||||
} else if (
|
} else if (
|
||||||
|
@@ -0,0 +1,81 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import {
|
||||||
|
Router,
|
||||||
|
UrlTree,
|
||||||
|
} from '@angular/router';
|
||||||
|
import {
|
||||||
|
Observable,
|
||||||
|
of as observableOf,
|
||||||
|
} from 'rxjs';
|
||||||
|
import { AuthService } from 'src/app/core/auth/auth.service';
|
||||||
|
import { AuthorizationDataService } from 'src/app/core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { FeatureID } from 'src/app/core/data/feature-authorization/feature-id';
|
||||||
|
|
||||||
|
import { BitstreamDataService } from '../core/data/bitstream-data.service';
|
||||||
|
import { Bitstream } from '../core/shared/bitstream.model';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
|
||||||
|
import { bitstreamPageAuthorizationsGuard } from './bitstream-page-authorizations.guard';
|
||||||
|
|
||||||
|
describe('bitstreamPageAuthorizationsGuard', () => {
|
||||||
|
let authorizationService: AuthorizationDataService;
|
||||||
|
let authService: AuthService;
|
||||||
|
let router: Router;
|
||||||
|
let route;
|
||||||
|
let parentRoute;
|
||||||
|
let bitstreamService: BitstreamDataService;
|
||||||
|
let bitstream: Bitstream;
|
||||||
|
let uuid = '1234-abcdef-54321-fedcba';
|
||||||
|
let bitstreamSelfLink = 'test.url/1234-abcdef-54321-fedcba';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||||
|
isAuthorized: observableOf(true),
|
||||||
|
});
|
||||||
|
router = jasmine.createSpyObj('router', {
|
||||||
|
parseUrl: {},
|
||||||
|
navigateByUrl: undefined,
|
||||||
|
});
|
||||||
|
authService = jasmine.createSpyObj('authService', {
|
||||||
|
isAuthenticated: observableOf(true),
|
||||||
|
});
|
||||||
|
|
||||||
|
parentRoute = {
|
||||||
|
params: {
|
||||||
|
id: '3e1a5327-dabb-41ff-af93-e6cab9d032f0',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
route = {
|
||||||
|
params: {},
|
||||||
|
parent: parentRoute,
|
||||||
|
};
|
||||||
|
bitstream = new Bitstream();
|
||||||
|
bitstream.uuid = uuid;
|
||||||
|
bitstream._links = { self: { href: bitstreamSelfLink } } as any;
|
||||||
|
bitstreamService = jasmine.createSpyObj('bitstreamService', { findById: createSuccessfulRemoteDataObject$(bitstream) });
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||||
|
{ provide: Router, useValue: router },
|
||||||
|
{ provide: AuthService, useValue: authService },
|
||||||
|
{ provide: BitstreamDataService, useValue: bitstreamService },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call authorizationService.isAuthorized with the appropriate arguments', (done) => {
|
||||||
|
const result$ = TestBed.runInInjectionContext(() => {
|
||||||
|
return bitstreamPageAuthorizationsGuard(route, { url: 'current-url' } as any);
|
||||||
|
}) as Observable<boolean | UrlTree>;
|
||||||
|
|
||||||
|
result$.subscribe((result) => {
|
||||||
|
expect(authorizationService.isAuthorized).toHaveBeenCalledWith(
|
||||||
|
FeatureID.CanManagePolicies,
|
||||||
|
bitstreamSelfLink,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,16 @@
|
|||||||
|
import { CanActivateFn } from '@angular/router';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
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 { bitstreamPageResolver } from './bitstream-page.resolver';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Guard for preventing unauthorized access to certain {@link Bitstream} pages requiring specific authorizations.
|
||||||
|
* Checks authorization rights for managing policies.
|
||||||
|
*/
|
||||||
|
export const bitstreamPageAuthorizationsGuard: CanActivateFn =
|
||||||
|
dsoPageSingleFeatureGuard(
|
||||||
|
() => bitstreamPageResolver,
|
||||||
|
() => observableOf(FeatureID.CanManagePolicies),
|
||||||
|
);
|
@@ -10,8 +10,9 @@ import { resourcePolicyTargetResolver } from '../shared/resource-policies/resolv
|
|||||||
import { BitstreamAuthorizationsComponent } from './bitstream-authorizations/bitstream-authorizations.component';
|
import { BitstreamAuthorizationsComponent } from './bitstream-authorizations/bitstream-authorizations.component';
|
||||||
import { BitstreamDownloadPageComponent } from './bitstream-download-page/bitstream-download-page.component';
|
import { BitstreamDownloadPageComponent } from './bitstream-download-page/bitstream-download-page.component';
|
||||||
import { bitstreamPageResolver } from './bitstream-page.resolver';
|
import { bitstreamPageResolver } from './bitstream-page.resolver';
|
||||||
|
import { bitstreamPageAuthorizationsGuard } from './bitstream-page-authorizations.guard';
|
||||||
import { ThemedEditBitstreamPageComponent } from './edit-bitstream-page/themed-edit-bitstream-page.component';
|
import { ThemedEditBitstreamPageComponent } from './edit-bitstream-page/themed-edit-bitstream-page.component';
|
||||||
import { legacyBitstreamUrlResolver } from './legacy-bitstream-url.resolver';
|
import { legacyBitstreamURLRedirectGuard } from './legacy-bitstream-url-redirect.guard';
|
||||||
|
|
||||||
const EDIT_BITSTREAM_PATH = ':id/edit';
|
const EDIT_BITSTREAM_PATH = ':id/edit';
|
||||||
const EDIT_BITSTREAM_AUTHORIZATIONS_PATH = ':id/authorizations';
|
const EDIT_BITSTREAM_AUTHORIZATIONS_PATH = ':id/authorizations';
|
||||||
@@ -23,18 +24,12 @@ export const ROUTES: Route[] = [
|
|||||||
{
|
{
|
||||||
// Resolve XMLUI bitstream download URLs
|
// Resolve XMLUI bitstream download URLs
|
||||||
path: 'handle/:prefix/:suffix/:filename',
|
path: 'handle/:prefix/:suffix/:filename',
|
||||||
component: BitstreamDownloadPageComponent,
|
canActivate: [legacyBitstreamURLRedirectGuard],
|
||||||
resolve: {
|
|
||||||
bitstream: legacyBitstreamUrlResolver,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Resolve JSPUI bitstream download URLs
|
// Resolve JSPUI bitstream download URLs
|
||||||
path: ':prefix/:suffix/:sequence_id/:filename',
|
path: ':prefix/:suffix/:sequence_id/:filename',
|
||||||
component: BitstreamDownloadPageComponent,
|
canActivate: [legacyBitstreamURLRedirectGuard],
|
||||||
resolve: {
|
|
||||||
bitstream: legacyBitstreamUrlResolver,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Resolve angular bitstream download URLs
|
// Resolve angular bitstream download URLs
|
||||||
@@ -55,6 +50,7 @@ export const ROUTES: Route[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: EDIT_BITSTREAM_AUTHORIZATIONS_PATH,
|
path: EDIT_BITSTREAM_AUTHORIZATIONS_PATH,
|
||||||
|
canActivate: [bitstreamPageAuthorizationsGuard],
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'create',
|
path: 'create',
|
||||||
|
@@ -0,0 +1,158 @@
|
|||||||
|
import { cold } from 'jasmine-marbles';
|
||||||
|
import { EMPTY } from 'rxjs';
|
||||||
|
|
||||||
|
import { PAGE_NOT_FOUND_PATH } from '../app-routing-paths';
|
||||||
|
import { BitstreamDataService } from '../core/data/bitstream-data.service';
|
||||||
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
|
import { RequestEntryState } from '../core/data/request-entry-state.model';
|
||||||
|
import { BrowserHardRedirectService } from '../core/services/browser-hard-redirect.service';
|
||||||
|
import { HardRedirectService } from '../core/services/hard-redirect.service';
|
||||||
|
import { Bitstream } from '../core/shared/bitstream.model';
|
||||||
|
import { RouterStub } from '../shared/testing/router.stub';
|
||||||
|
import { legacyBitstreamURLRedirectGuard } from './legacy-bitstream-url-redirect.guard';
|
||||||
|
|
||||||
|
describe('legacyBitstreamURLRedirectGuard', () => {
|
||||||
|
let resolver: any;
|
||||||
|
let bitstreamDataService: BitstreamDataService;
|
||||||
|
let remoteDataMocks: { [type: string]: RemoteData<Bitstream> };
|
||||||
|
let route;
|
||||||
|
let state;
|
||||||
|
let hardRedirectService: HardRedirectService;
|
||||||
|
let router: RouterStub;
|
||||||
|
|
||||||
|
let bitstream: Bitstream;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
route = {
|
||||||
|
params: {},
|
||||||
|
queryParams: {},
|
||||||
|
};
|
||||||
|
router = new RouterStub();
|
||||||
|
hardRedirectService = new BrowserHardRedirectService(window.location);
|
||||||
|
state = {};
|
||||||
|
bitstream = Object.assign(new Bitstream(), {
|
||||||
|
uuid: 'bitstream-id',
|
||||||
|
});
|
||||||
|
remoteDataMocks = {
|
||||||
|
RequestPending: new RemoteData(undefined, 0, 0, RequestEntryState.RequestPending, undefined, undefined, undefined),
|
||||||
|
ResponsePending: new RemoteData(undefined, 0, 0, RequestEntryState.ResponsePending, undefined, undefined, undefined),
|
||||||
|
Success: new RemoteData(0, 0, 0, RequestEntryState.Success, undefined, bitstream, 200),
|
||||||
|
NoContent: new RemoteData(0, 0, 0, RequestEntryState.Success, undefined, undefined, 204),
|
||||||
|
Error: new RemoteData(0, 0, 0, RequestEntryState.Error, 'Internal server error', undefined, 500),
|
||||||
|
};
|
||||||
|
bitstreamDataService = {
|
||||||
|
findByItemHandle: () => undefined,
|
||||||
|
} as any;
|
||||||
|
resolver = legacyBitstreamURLRedirectGuard;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(`resolve`, () => {
|
||||||
|
describe(`For JSPUI-style URLs`, () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(bitstreamDataService, 'findByItemHandle').and.returnValue(EMPTY);
|
||||||
|
route = Object.assign({}, route, {
|
||||||
|
params: {
|
||||||
|
prefix: '123456789',
|
||||||
|
suffix: '1234',
|
||||||
|
filename: 'some-file.pdf',
|
||||||
|
sequence_id: '5',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it(`should call findByItemHandle with the handle, sequence id, and filename from the route`, () => {
|
||||||
|
resolver(route, state, bitstreamDataService, hardRedirectService, router);
|
||||||
|
expect(bitstreamDataService.findByItemHandle).toHaveBeenCalledWith(
|
||||||
|
`${route.params.prefix}/${route.params.suffix}`,
|
||||||
|
route.params.sequence_id,
|
||||||
|
route.params.filename,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(`For XMLUI-style URLs`, () => {
|
||||||
|
describe(`when there is a sequenceId query parameter`, () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(bitstreamDataService, 'findByItemHandle').and.returnValue(EMPTY);
|
||||||
|
route = Object.assign({}, route, {
|
||||||
|
params: {
|
||||||
|
prefix: '123456789',
|
||||||
|
suffix: '1234',
|
||||||
|
filename: 'some-file.pdf',
|
||||||
|
},
|
||||||
|
queryParams: {
|
||||||
|
sequenceId: '5',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it(`should call findByItemHandle with the handle and filename from the route, and the sequence ID from the queryParams`, () => {
|
||||||
|
resolver(route, state, bitstreamDataService, hardRedirectService, router);
|
||||||
|
expect(bitstreamDataService.findByItemHandle).toHaveBeenCalledWith(
|
||||||
|
`${route.params.prefix}/${route.params.suffix}`,
|
||||||
|
route.queryParams.sequenceId,
|
||||||
|
route.params.filename,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe(`when there's no sequenceId query parameter`, () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(bitstreamDataService, 'findByItemHandle').and.returnValue(EMPTY);
|
||||||
|
route = Object.assign({}, route, {
|
||||||
|
params: {
|
||||||
|
prefix: '123456789',
|
||||||
|
suffix: '1234',
|
||||||
|
filename: 'some-file.pdf',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it(`should call findByItemHandle with the handle, and filename from the route`, () => {
|
||||||
|
resolver(route, state, bitstreamDataService, hardRedirectService, router);
|
||||||
|
expect(bitstreamDataService.findByItemHandle).toHaveBeenCalledWith(
|
||||||
|
`${route.params.prefix}/${route.params.suffix}`,
|
||||||
|
undefined,
|
||||||
|
route.params.filename,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('should return and complete after the RemoteData has...', () => {
|
||||||
|
it('...failed', () => {
|
||||||
|
spyOn(router, 'createUrlTree').and.callThrough();
|
||||||
|
spyOn(bitstreamDataService, 'findByItemHandle').and.returnValue(cold('a-b-c', {
|
||||||
|
a: remoteDataMocks.RequestPending,
|
||||||
|
b: remoteDataMocks.ResponsePending,
|
||||||
|
c: remoteDataMocks.Error,
|
||||||
|
}));
|
||||||
|
resolver(route, state, bitstreamDataService, hardRedirectService, router).subscribe(() => {
|
||||||
|
expect(bitstreamDataService.findByItemHandle).toHaveBeenCalled();
|
||||||
|
expect(router.createUrlTree).toHaveBeenCalledWith([PAGE_NOT_FOUND_PATH]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('...succeeded without content', () => {
|
||||||
|
spyOn(router, 'createUrlTree').and.callThrough();
|
||||||
|
spyOn(bitstreamDataService, 'findByItemHandle').and.returnValue(cold('a-b-c', {
|
||||||
|
a: remoteDataMocks.RequestPending,
|
||||||
|
b: remoteDataMocks.ResponsePending,
|
||||||
|
c: remoteDataMocks.NoContent,
|
||||||
|
}));
|
||||||
|
resolver(route, state, bitstreamDataService, hardRedirectService, router).subscribe(() => {
|
||||||
|
expect(bitstreamDataService.findByItemHandle).toHaveBeenCalled();
|
||||||
|
expect(router.createUrlTree).toHaveBeenCalledWith([PAGE_NOT_FOUND_PATH]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('...succeeded', () => {
|
||||||
|
spyOn(hardRedirectService, 'redirect');
|
||||||
|
spyOn(bitstreamDataService, 'findByItemHandle').and.returnValue(cold('a-b-c', {
|
||||||
|
a: remoteDataMocks.RequestPending,
|
||||||
|
b: remoteDataMocks.ResponsePending,
|
||||||
|
c: remoteDataMocks.Success,
|
||||||
|
}));
|
||||||
|
resolver(route, state, bitstreamDataService, hardRedirectService, router).subscribe(() => {
|
||||||
|
expect(bitstreamDataService.findByItemHandle).toHaveBeenCalled();
|
||||||
|
expect(hardRedirectService.redirect).toHaveBeenCalledWith(new URL(`/bitstreams/${bitstream.uuid}/download`, window.location.origin).href, 301);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,57 @@
|
|||||||
|
import { inject } from '@angular/core';
|
||||||
|
import {
|
||||||
|
ActivatedRouteSnapshot,
|
||||||
|
CanActivateFn,
|
||||||
|
Router,
|
||||||
|
RouterStateSnapshot,
|
||||||
|
UrlTree,
|
||||||
|
} from '@angular/router';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { PAGE_NOT_FOUND_PATH } from '../app-routing-paths';
|
||||||
|
import { BitstreamDataService } from '../core/data/bitstream-data.service';
|
||||||
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
|
import { HardRedirectService } from '../core/services/hard-redirect.service';
|
||||||
|
import { Bitstream } from '../core/shared/bitstream.model';
|
||||||
|
import { getFirstCompletedRemoteData } from '../core/shared/operators';
|
||||||
|
import { hasNoValue } from '../shared/empty.util';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirects to a bitstream based on the handle of the item, and the sequence id or the filename of the
|
||||||
|
* bitstream. In production mode the status code will also be set the status code to 301 marking it as a permanent URL
|
||||||
|
* redirect for bots to the regular bitstream download Page.
|
||||||
|
*
|
||||||
|
* @returns Either a {@link UrlTree} to the 404 page when the url isn't a valid format or false in order to make the
|
||||||
|
* user wait until the {@link HardRedirectService#redirect} was performed
|
||||||
|
*/
|
||||||
|
export const legacyBitstreamURLRedirectGuard: CanActivateFn = (
|
||||||
|
route: ActivatedRouteSnapshot,
|
||||||
|
state: RouterStateSnapshot,
|
||||||
|
bitstreamDataService: BitstreamDataService = inject(BitstreamDataService),
|
||||||
|
serverHardRedirectService: HardRedirectService = inject(HardRedirectService),
|
||||||
|
router: Router = inject(Router),
|
||||||
|
): Observable<UrlTree | false> => {
|
||||||
|
const prefix = route.params.prefix;
|
||||||
|
const suffix = route.params.suffix;
|
||||||
|
const filename = route.params.filename;
|
||||||
|
let sequenceId = route.params.sequence_id;
|
||||||
|
if (hasNoValue(sequenceId)) {
|
||||||
|
sequenceId = route.queryParams.sequenceId;
|
||||||
|
}
|
||||||
|
return bitstreamDataService.findByItemHandle(
|
||||||
|
`${prefix}/${suffix}`,
|
||||||
|
sequenceId,
|
||||||
|
filename,
|
||||||
|
).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
map((rd: RemoteData<Bitstream>) => {
|
||||||
|
if (rd.hasSucceeded && !rd.hasNoContent) {
|
||||||
|
serverHardRedirectService.redirect(new URL(`/bitstreams/${rd.payload.uuid}/download`, serverHardRedirectService.getCurrentOrigin()).href, 301);
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return router.createUrlTree([PAGE_NOT_FOUND_PATH]);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
@@ -1,146 +0,0 @@
|
|||||||
import { EMPTY } from 'rxjs';
|
|
||||||
import { TestScheduler } from 'rxjs/testing';
|
|
||||||
|
|
||||||
import { BitstreamDataService } from '../core/data/bitstream-data.service';
|
|
||||||
import { RemoteData } from '../core/data/remote-data';
|
|
||||||
import { RequestEntryState } from '../core/data/request-entry-state.model';
|
|
||||||
import { legacyBitstreamUrlResolver } from './legacy-bitstream-url.resolver';
|
|
||||||
|
|
||||||
describe(`legacyBitstreamUrlResolver`, () => {
|
|
||||||
let resolver: any;
|
|
||||||
let bitstreamDataService: BitstreamDataService;
|
|
||||||
let testScheduler;
|
|
||||||
let remoteDataMocks;
|
|
||||||
let route;
|
|
||||||
let state;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
testScheduler = new TestScheduler((actual, expected) => {
|
|
||||||
expect(actual).toEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
route = {
|
|
||||||
params: {},
|
|
||||||
queryParams: {},
|
|
||||||
};
|
|
||||||
state = {};
|
|
||||||
remoteDataMocks = {
|
|
||||||
RequestPending: new RemoteData(undefined, 0, 0, RequestEntryState.RequestPending, undefined, undefined, undefined),
|
|
||||||
ResponsePending: new RemoteData(undefined, 0, 0, RequestEntryState.ResponsePending, undefined, undefined, undefined),
|
|
||||||
Success: new RemoteData(0, 0, 0, RequestEntryState.Success, undefined, {}, 200),
|
|
||||||
Error: new RemoteData(0, 0, 0, RequestEntryState.Error, 'Internal server error', undefined, 500),
|
|
||||||
};
|
|
||||||
bitstreamDataService = {
|
|
||||||
findByItemHandle: () => undefined,
|
|
||||||
} as any;
|
|
||||||
resolver = legacyBitstreamUrlResolver;
|
|
||||||
});
|
|
||||||
|
|
||||||
describe(`resolve`, () => {
|
|
||||||
describe(`For JSPUI-style URLs`, () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
spyOn(bitstreamDataService, 'findByItemHandle').and.returnValue(EMPTY);
|
|
||||||
route = Object.assign({}, route, {
|
|
||||||
params: {
|
|
||||||
prefix: '123456789',
|
|
||||||
suffix: '1234',
|
|
||||||
filename: 'some-file.pdf',
|
|
||||||
sequence_id: '5',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it(`should call findByItemHandle with the handle, sequence id, and filename from the route`, () => {
|
|
||||||
testScheduler.run(() => {
|
|
||||||
resolver(route, state, bitstreamDataService);
|
|
||||||
expect(bitstreamDataService.findByItemHandle).toHaveBeenCalledWith(
|
|
||||||
`${route.params.prefix}/${route.params.suffix}`,
|
|
||||||
route.params.sequence_id,
|
|
||||||
route.params.filename,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe(`For XMLUI-style URLs`, () => {
|
|
||||||
describe(`when there is a sequenceId query parameter`, () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
spyOn(bitstreamDataService, 'findByItemHandle').and.returnValue(EMPTY);
|
|
||||||
route = Object.assign({}, route, {
|
|
||||||
params: {
|
|
||||||
prefix: '123456789',
|
|
||||||
suffix: '1234',
|
|
||||||
filename: 'some-file.pdf',
|
|
||||||
},
|
|
||||||
queryParams: {
|
|
||||||
sequenceId: '5',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it(`should call findByItemHandle with the handle and filename from the route, and the sequence ID from the queryParams`, () => {
|
|
||||||
testScheduler.run(() => {
|
|
||||||
resolver(route, state, bitstreamDataService);
|
|
||||||
expect(bitstreamDataService.findByItemHandle).toHaveBeenCalledWith(
|
|
||||||
`${route.params.prefix}/${route.params.suffix}`,
|
|
||||||
route.queryParams.sequenceId,
|
|
||||||
route.params.filename,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe(`when there's no sequenceId query parameter`, () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
spyOn(bitstreamDataService, 'findByItemHandle').and.returnValue(EMPTY);
|
|
||||||
route = Object.assign({}, route, {
|
|
||||||
params: {
|
|
||||||
prefix: '123456789',
|
|
||||||
suffix: '1234',
|
|
||||||
filename: 'some-file.pdf',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it(`should call findByItemHandle with the handle, and filename from the route`, () => {
|
|
||||||
testScheduler.run(() => {
|
|
||||||
resolver(route, state, bitstreamDataService);
|
|
||||||
expect(bitstreamDataService.findByItemHandle).toHaveBeenCalledWith(
|
|
||||||
`${route.params.prefix}/${route.params.suffix}`,
|
|
||||||
undefined,
|
|
||||||
route.params.filename,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe(`should return and complete after the remotedata has...`, () => {
|
|
||||||
it(`...failed`, () => {
|
|
||||||
testScheduler.run(({ cold, expectObservable }) => {
|
|
||||||
spyOn(bitstreamDataService, 'findByItemHandle').and.returnValue(cold('a-b-c', {
|
|
||||||
a: remoteDataMocks.RequestPending,
|
|
||||||
b: remoteDataMocks.ResponsePending,
|
|
||||||
c: remoteDataMocks.Error,
|
|
||||||
}));
|
|
||||||
const expected = '----(c|)';
|
|
||||||
const values = {
|
|
||||||
c: remoteDataMocks.Error,
|
|
||||||
};
|
|
||||||
|
|
||||||
expectObservable(resolver(route, state, bitstreamDataService)).toBe(expected, values);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it(`...succeeded`, () => {
|
|
||||||
testScheduler.run(({ cold, expectObservable }) => {
|
|
||||||
spyOn(bitstreamDataService, 'findByItemHandle').and.returnValue(cold('a-b-c', {
|
|
||||||
a: remoteDataMocks.RequestPending,
|
|
||||||
b: remoteDataMocks.ResponsePending,
|
|
||||||
c: remoteDataMocks.Success,
|
|
||||||
}));
|
|
||||||
const expected = '----(c|)';
|
|
||||||
const values = {
|
|
||||||
c: remoteDataMocks.Success,
|
|
||||||
};
|
|
||||||
|
|
||||||
expectObservable(resolver(route, state, bitstreamDataService)).toBe(expected, values);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@@ -1,46 +0,0 @@
|
|||||||
import { inject } from '@angular/core';
|
|
||||||
import {
|
|
||||||
ActivatedRouteSnapshot,
|
|
||||||
ResolveFn,
|
|
||||||
RouterStateSnapshot,
|
|
||||||
} from '@angular/router';
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
|
|
||||||
import { BitstreamDataService } from '../core/data/bitstream-data.service';
|
|
||||||
import { RemoteData } from '../core/data/remote-data';
|
|
||||||
import { Bitstream } from '../core/shared/bitstream.model';
|
|
||||||
import { getFirstCompletedRemoteData } from '../core/shared/operators';
|
|
||||||
import { hasNoValue } from '../shared/empty.util';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve a bitstream based on the handle of the item, and the sequence id or the filename of the
|
|
||||||
* bitstream
|
|
||||||
*
|
|
||||||
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
|
|
||||||
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
|
|
||||||
* @param {BitstreamDataService} bitstreamDataService
|
|
||||||
* @returns Observable<<RemoteData<Item>> Emits the found bitstream based on the parameters in
|
|
||||||
* current route, or an error if something went wrong
|
|
||||||
*/
|
|
||||||
export const legacyBitstreamUrlResolver: ResolveFn<RemoteData<Bitstream>> = (
|
|
||||||
route: ActivatedRouteSnapshot,
|
|
||||||
state: RouterStateSnapshot,
|
|
||||||
bitstreamDataService: BitstreamDataService = inject(BitstreamDataService),
|
|
||||||
): Observable<RemoteData<Bitstream>> => {
|
|
||||||
const prefix = route.params.prefix;
|
|
||||||
const suffix = route.params.suffix;
|
|
||||||
const filename = route.params.filename;
|
|
||||||
|
|
||||||
let sequenceId = route.params.sequence_id;
|
|
||||||
if (hasNoValue(sequenceId)) {
|
|
||||||
sequenceId = route.queryParams.sequenceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
return bitstreamDataService.findByItemHandle(
|
|
||||||
`${prefix}/${suffix}`,
|
|
||||||
sequenceId,
|
|
||||||
filename,
|
|
||||||
).pipe(
|
|
||||||
getFirstCompletedRemoteData(),
|
|
||||||
);
|
|
||||||
};
|
|
@@ -7,8 +7,10 @@
|
|||||||
} }}
|
} }}
|
||||||
</h1>
|
</h1>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<ds-vocabulary-treeview [vocabularyOptions]=vocabularyOptions
|
<ds-vocabulary-treeview [description]="description"
|
||||||
|
[vocabularyOptions]=vocabularyOptions
|
||||||
[multiSelect]="true"
|
[multiSelect]="true"
|
||||||
|
[showAdd]="false"
|
||||||
(select)="onSelect($event)"
|
(select)="onSelect($event)"
|
||||||
(deselect)="onDeselect($event)">
|
(deselect)="onDeselect($event)">
|
||||||
</ds-vocabulary-treeview>
|
</ds-vocabulary-treeview>
|
||||||
|
@@ -14,7 +14,10 @@ import {
|
|||||||
Params,
|
Params,
|
||||||
RouterLink,
|
RouterLink,
|
||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import {
|
||||||
|
TranslateModule,
|
||||||
|
TranslateService,
|
||||||
|
} from '@ngx-translate/core';
|
||||||
import {
|
import {
|
||||||
BehaviorSubject,
|
BehaviorSubject,
|
||||||
Observable,
|
Observable,
|
||||||
@@ -124,6 +127,11 @@ export class BrowseByTaxonomyComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
browseDefinition$: Observable<BrowseDefinition>;
|
browseDefinition$: Observable<BrowseDefinition>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Browse description
|
||||||
|
*/
|
||||||
|
description: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscriptions to track
|
* Subscriptions to track
|
||||||
*/
|
*/
|
||||||
@@ -131,6 +139,7 @@ export class BrowseByTaxonomyComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
protected route: ActivatedRoute,
|
protected route: ActivatedRoute,
|
||||||
|
protected translate: TranslateService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,9 +150,11 @@ export class BrowseByTaxonomyComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
this.subs.push(this.browseDefinition$.subscribe((browseDefinition: HierarchicalBrowseDefinition) => {
|
this.subs.push(this.browseDefinition$.subscribe((browseDefinition: HierarchicalBrowseDefinition) => {
|
||||||
|
this.selectedItems = [];
|
||||||
this.facetType = browseDefinition.facetType;
|
this.facetType = browseDefinition.facetType;
|
||||||
this.vocabularyName = browseDefinition.vocabulary;
|
this.vocabularyName = browseDefinition.vocabulary;
|
||||||
this.vocabularyOptions = { name: this.vocabularyName, closed: true };
|
this.vocabularyOptions = { name: this.vocabularyName, closed: true };
|
||||||
|
this.description = this.translate.instant(`browse.metadata.${this.vocabularyName}.tree.descrption`);
|
||||||
}));
|
}));
|
||||||
this.subs.push(this.scope$.subscribe(() => {
|
this.subs.push(this.scope$.subscribe(() => {
|
||||||
this.updateQueryParams();
|
this.updateQueryParams();
|
||||||
|
@@ -1,43 +1,16 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { CanActivateFn } from '@angular/router';
|
||||||
import {
|
import { of as observableOf } from 'rxjs';
|
||||||
ActivatedRouteSnapshot,
|
|
||||||
ResolveFn,
|
|
||||||
Router,
|
|
||||||
RouterStateSnapshot,
|
|
||||||
} from '@angular/router';
|
|
||||||
import {
|
|
||||||
Observable,
|
|
||||||
of as observableOf,
|
|
||||||
} from 'rxjs';
|
|
||||||
|
|
||||||
import { AuthService } from '../core/auth/auth.service';
|
import { dsoPageSingleFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard';
|
||||||
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
|
||||||
import { DsoPageSingleFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard';
|
|
||||||
import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
||||||
import { RemoteData } from '../core/data/remote-data';
|
|
||||||
import { Collection } from '../core/shared/collection.model';
|
|
||||||
import { collectionPageResolver } from './collection-page.resolver';
|
import { collectionPageResolver } from './collection-page.resolver';
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
/**
|
/**
|
||||||
* Guard for preventing unauthorized access to certain {@link Collection} pages requiring administrator rights
|
* Guard for preventing unauthorized access to certain {@link Collection} pages requiring administrator rights
|
||||||
|
* Check administrator authorization rights
|
||||||
*/
|
*/
|
||||||
export class CollectionPageAdministratorGuard extends DsoPageSingleFeatureGuard<Collection> {
|
export const collectionPageAdministratorGuard: CanActivateFn =
|
||||||
|
dsoPageSingleFeatureGuard(
|
||||||
protected resolver: ResolveFn<RemoteData<Collection>> = collectionPageResolver;
|
() => collectionPageResolver,
|
||||||
|
() => observableOf(FeatureID.AdministratorOf),
|
||||||
constructor(protected authorizationService: AuthorizationDataService,
|
);
|
||||||
protected router: Router,
|
|
||||||
protected authService: AuthService) {
|
|
||||||
super(authorizationService, router, authService);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check administrator authorization rights
|
|
||||||
*/
|
|
||||||
getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
|
||||||
return observableOf(FeatureID.AdministratorOf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -1,7 +1,4 @@
|
|||||||
import {
|
import { Route } from '@angular/router';
|
||||||
mapToCanActivate,
|
|
||||||
Route,
|
|
||||||
} from '@angular/router';
|
|
||||||
|
|
||||||
import { browseByGuard } from '../browse-by/browse-by-guard';
|
import { browseByGuard } from '../browse-by/browse-by-guard';
|
||||||
import { browseByI18nBreadcrumbResolver } from '../browse-by/browse-by-i18n-breadcrumb.resolver';
|
import { browseByI18nBreadcrumbResolver } from '../browse-by/browse-by-i18n-breadcrumb.resolver';
|
||||||
@@ -15,7 +12,7 @@ import { dsoEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver';
|
|||||||
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
|
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
|
||||||
import { MenuItemType } from '../shared/menu/menu-item-type.model';
|
import { MenuItemType } from '../shared/menu/menu-item-type.model';
|
||||||
import { collectionPageResolver } from './collection-page.resolver';
|
import { collectionPageResolver } from './collection-page.resolver';
|
||||||
import { CollectionPageAdministratorGuard } from './collection-page-administrator.guard';
|
import { collectionPageAdministratorGuard } from './collection-page-administrator.guard';
|
||||||
import {
|
import {
|
||||||
COLLECTION_CREATE_PATH,
|
COLLECTION_CREATE_PATH,
|
||||||
COLLECTION_EDIT_PATH,
|
COLLECTION_EDIT_PATH,
|
||||||
@@ -65,7 +62,7 @@ export const ROUTES: Route[] = [
|
|||||||
path: COLLECTION_EDIT_PATH,
|
path: COLLECTION_EDIT_PATH,
|
||||||
loadChildren: () => import('./edit-collection-page/edit-collection-page-routes')
|
loadChildren: () => import('./edit-collection-page/edit-collection-page-routes')
|
||||||
.then((m) => m.ROUTES),
|
.then((m) => m.ROUTES),
|
||||||
canActivate: mapToCanActivate([CollectionPageAdministratorGuard]),
|
canActivate: [collectionPageAdministratorGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'delete',
|
path: 'delete',
|
||||||
|
@@ -1,10 +1,7 @@
|
|||||||
import {
|
import { Route } from '@angular/router';
|
||||||
mapToCanActivate,
|
|
||||||
Route,
|
|
||||||
} from '@angular/router';
|
|
||||||
|
|
||||||
import { i18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
|
import { i18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||||
import { CollectionAdministratorGuard } from '../../core/data/feature-authorization/feature-authorization-guard/collection-administrator.guard';
|
import { collectionAdministratorGuard } from '../../core/data/feature-authorization/feature-authorization-guard/collection-administrator.guard';
|
||||||
import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/create/resource-policy-create.component';
|
import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/create/resource-policy-create.component';
|
||||||
import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit/resource-policy-edit.component';
|
import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit/resource-policy-edit.component';
|
||||||
import { resourcePolicyResolver } from '../../shared/resource-policies/resolvers/resource-policy.resolver';
|
import { resourcePolicyResolver } from '../../shared/resource-policies/resolvers/resource-policy.resolver';
|
||||||
@@ -30,7 +27,7 @@ export const ROUTES: Route[] = [
|
|||||||
},
|
},
|
||||||
data: { breadcrumbKey: 'collection.edit' },
|
data: { breadcrumbKey: 'collection.edit' },
|
||||||
component: EditCollectionPageComponent,
|
component: EditCollectionPageComponent,
|
||||||
canActivate: mapToCanActivate([CollectionAdministratorGuard]),
|
canActivate: [collectionAdministratorGuard],
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
|
@@ -1,43 +1,16 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { CanActivateFn } from '@angular/router';
|
||||||
import {
|
import { of as observableOf } from 'rxjs';
|
||||||
ActivatedRouteSnapshot,
|
|
||||||
ResolveFn,
|
|
||||||
Router,
|
|
||||||
RouterStateSnapshot,
|
|
||||||
} from '@angular/router';
|
|
||||||
import {
|
|
||||||
Observable,
|
|
||||||
of as observableOf,
|
|
||||||
} from 'rxjs';
|
|
||||||
|
|
||||||
import { AuthService } from '../core/auth/auth.service';
|
import { dsoPageSingleFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard';
|
||||||
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
|
||||||
import { DsoPageSingleFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard';
|
|
||||||
import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
||||||
import { RemoteData } from '../core/data/remote-data';
|
|
||||||
import { Community } from '../core/shared/community.model';
|
|
||||||
import { communityPageResolver } from './community-page.resolver';
|
import { communityPageResolver } from './community-page.resolver';
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
/**
|
/**
|
||||||
* Guard for preventing unauthorized access to certain {@link Community} pages requiring administrator rights
|
* Guard for preventing unauthorized access to certain {@link Community} pages requiring administrator rights
|
||||||
|
* Check administrator authorization rights
|
||||||
*/
|
*/
|
||||||
export class CommunityPageAdministratorGuard extends DsoPageSingleFeatureGuard<Community> {
|
export const communityPageAdministratorGuard: CanActivateFn =
|
||||||
|
dsoPageSingleFeatureGuard(
|
||||||
protected resolver: ResolveFn<RemoteData<Community>> = communityPageResolver;
|
() => communityPageResolver,
|
||||||
|
() => observableOf(FeatureID.AdministratorOf),
|
||||||
constructor(protected authorizationService: AuthorizationDataService,
|
);
|
||||||
protected router: Router,
|
|
||||||
protected authService: AuthService) {
|
|
||||||
super(authorizationService, router, authService);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check administrator authorization rights
|
|
||||||
*/
|
|
||||||
getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
|
||||||
return observableOf(FeatureID.AdministratorOf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -1,7 +1,4 @@
|
|||||||
import {
|
import { Route } from '@angular/router';
|
||||||
mapToCanActivate,
|
|
||||||
Route,
|
|
||||||
} from '@angular/router';
|
|
||||||
|
|
||||||
import { browseByGuard } from '../browse-by/browse-by-guard';
|
import { browseByGuard } from '../browse-by/browse-by-guard';
|
||||||
import { browseByI18nBreadcrumbResolver } from '../browse-by/browse-by-i18n-breadcrumb.resolver';
|
import { browseByI18nBreadcrumbResolver } from '../browse-by/browse-by-i18n-breadcrumb.resolver';
|
||||||
@@ -14,7 +11,7 @@ import { dsoEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver';
|
|||||||
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
|
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
|
||||||
import { MenuItemType } from '../shared/menu/menu-item-type.model';
|
import { MenuItemType } from '../shared/menu/menu-item-type.model';
|
||||||
import { communityPageResolver } from './community-page.resolver';
|
import { communityPageResolver } from './community-page.resolver';
|
||||||
import { CommunityPageAdministratorGuard } from './community-page-administrator.guard';
|
import { communityPageAdministratorGuard } from './community-page-administrator.guard';
|
||||||
import {
|
import {
|
||||||
COMMUNITY_CREATE_PATH,
|
COMMUNITY_CREATE_PATH,
|
||||||
COMMUNITY_EDIT_PATH,
|
COMMUNITY_EDIT_PATH,
|
||||||
@@ -62,7 +59,7 @@ export const ROUTES: Route[] = [
|
|||||||
path: COMMUNITY_EDIT_PATH,
|
path: COMMUNITY_EDIT_PATH,
|
||||||
loadChildren: () => import('./edit-community-page/edit-community-page-routes')
|
loadChildren: () => import('./edit-community-page/edit-community-page-routes')
|
||||||
.then((m) => m.ROUTES),
|
.then((m) => m.ROUTES),
|
||||||
canActivate: mapToCanActivate([CommunityPageAdministratorGuard]),
|
canActivate: [communityPageAdministratorGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'delete',
|
path: 'delete',
|
||||||
|
@@ -1,10 +1,7 @@
|
|||||||
import {
|
import { Route } from '@angular/router';
|
||||||
mapToCanActivate,
|
|
||||||
Route,
|
|
||||||
} from '@angular/router';
|
|
||||||
|
|
||||||
import { i18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
|
import { i18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||||
import { CommunityAdministratorGuard } from '../../core/data/feature-authorization/feature-authorization-guard/community-administrator.guard';
|
import { communityAdministratorGuard } from '../../core/data/feature-authorization/feature-authorization-guard/community-administrator.guard';
|
||||||
import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/create/resource-policy-create.component';
|
import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/create/resource-policy-create.component';
|
||||||
import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit/resource-policy-edit.component';
|
import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit/resource-policy-edit.component';
|
||||||
import { resourcePolicyResolver } from '../../shared/resource-policies/resolvers/resource-policy.resolver';
|
import { resourcePolicyResolver } from '../../shared/resource-policies/resolvers/resource-policy.resolver';
|
||||||
@@ -28,7 +25,7 @@ export const ROUTES: Route[] = [
|
|||||||
},
|
},
|
||||||
data: { breadcrumbKey: 'community.edit' },
|
data: { breadcrumbKey: 'community.edit' },
|
||||||
component: EditCommunityPageComponent,
|
component: EditCommunityPageComponent,
|
||||||
canActivate: mapToCanActivate([CommunityAdministratorGuard]),
|
canActivate: [communityAdministratorGuard],
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
|
@@ -209,6 +209,11 @@ describe('DeleteDataImpl', () => {
|
|||||||
method: RestRequestMethod.DELETE,
|
method: RestRequestMethod.DELETE,
|
||||||
href: 'some-href?copyVirtualMetadata=a©VirtualMetadata=b©VirtualMetadata=c',
|
href: 'some-href?copyVirtualMetadata=a©VirtualMetadata=b©VirtualMetadata=c',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const callback = (rdbService.buildFromRequestUUIDAndAwait as jasmine.Spy).calls.argsFor(0)[1];
|
||||||
|
callback();
|
||||||
|
expect(service.invalidateByHref).toHaveBeenCalledWith('some-href');
|
||||||
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -75,15 +75,16 @@ export class DeleteDataImpl<T extends CacheableObject> extends IdentifiableDataS
|
|||||||
deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
|
deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
|
||||||
const requestId = this.requestService.generateRequestId();
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
|
||||||
|
let deleteHref: string = href;
|
||||||
if (copyVirtualMetadata) {
|
if (copyVirtualMetadata) {
|
||||||
copyVirtualMetadata.forEach((id) =>
|
copyVirtualMetadata.forEach((id) =>
|
||||||
href += (href.includes('?') ? '&' : '?')
|
deleteHref += (deleteHref.includes('?') ? '&' : '?')
|
||||||
+ 'copyVirtualMetadata='
|
+ 'copyVirtualMetadata='
|
||||||
+ id,
|
+ id,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const request = new DeleteRequest(requestId, href);
|
const request = new DeleteRequest(requestId, deleteHref);
|
||||||
if (hasValue(this.responseMsToLive)) {
|
if (hasValue(this.responseMsToLive)) {
|
||||||
request.responseMsToLive = this.responseMsToLive;
|
request.responseMsToLive = this.responseMsToLive;
|
||||||
}
|
}
|
||||||
|
@@ -5,10 +5,7 @@
|
|||||||
*
|
*
|
||||||
* http://www.dspace.org/license/
|
* http://www.dspace.org/license/
|
||||||
*/
|
*/
|
||||||
import {
|
import { of as observableOf } from 'rxjs';
|
||||||
Observable,
|
|
||||||
of as observableOf,
|
|
||||||
} from 'rxjs';
|
|
||||||
import { TestScheduler } from 'rxjs/testing';
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
|
|
||||||
import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock';
|
import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock';
|
||||||
@@ -18,14 +15,14 @@ import { followLink } from '../../../shared/utils/follow-link-config.model';
|
|||||||
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
||||||
import { ObjectCacheService } from '../../cache/object-cache.service';
|
import { ObjectCacheService } from '../../cache/object-cache.service';
|
||||||
import { HALEndpointService } from '../../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../../shared/hal-endpoint.service';
|
||||||
import { FindListOptions } from '../find-list-options.model';
|
|
||||||
import { RemoteData } from '../remote-data';
|
import { RemoteData } from '../remote-data';
|
||||||
import { RequestService } from '../request.service';
|
import { RequestService } from '../request.service';
|
||||||
import { RequestEntryState } from '../request-entry-state.model';
|
import { RequestEntryState } from '../request-entry-state.model';
|
||||||
import { EMBED_SEPARATOR } from './base-data.service';
|
import { EMBED_SEPARATOR } from './base-data.service';
|
||||||
import { IdentifiableDataService } from './identifiable-data.service';
|
import { IdentifiableDataService } from './identifiable-data.service';
|
||||||
|
|
||||||
const endpoint = 'https://rest.api/core';
|
const base = 'https://rest.api/core';
|
||||||
|
const endpoint = 'test';
|
||||||
|
|
||||||
class TestService extends IdentifiableDataService<any> {
|
class TestService extends IdentifiableDataService<any> {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -34,11 +31,7 @@ class TestService extends IdentifiableDataService<any> {
|
|||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
) {
|
) {
|
||||||
super(undefined, requestService, rdbService, objectCache, halService);
|
super(endpoint, requestService, rdbService, objectCache, halService);
|
||||||
}
|
|
||||||
|
|
||||||
public getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable<string> {
|
|
||||||
return observableOf(endpoint);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +48,7 @@ describe('IdentifiableDataService', () => {
|
|||||||
|
|
||||||
function initTestService(): TestService {
|
function initTestService(): TestService {
|
||||||
requestService = getMockRequestService();
|
requestService = getMockRequestService();
|
||||||
halService = new HALEndpointServiceStub('url') as any;
|
halService = new HALEndpointServiceStub(base) as any;
|
||||||
rdbService = getMockRemoteDataBuildService();
|
rdbService = getMockRemoteDataBuildService();
|
||||||
objectCache = {
|
objectCache = {
|
||||||
|
|
||||||
@@ -147,4 +140,12 @@ describe('IdentifiableDataService', () => {
|
|||||||
expect(result).toEqual(expected);
|
expect(result).toEqual(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('invalidateById', () => {
|
||||||
|
it('should invalidate the correct resource by href', () => {
|
||||||
|
spyOn(service, 'invalidateByHref').and.returnValue(observableOf(true));
|
||||||
|
service.invalidateById('123');
|
||||||
|
expect(service.invalidateByHref).toHaveBeenCalledWith(`${base}/${endpoint}/123`);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -6,7 +6,11 @@
|
|||||||
* http://www.dspace.org/license/
|
* http://www.dspace.org/license/
|
||||||
*/
|
*/
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { map } from 'rxjs/operators';
|
import {
|
||||||
|
map,
|
||||||
|
switchMap,
|
||||||
|
take,
|
||||||
|
} from 'rxjs/operators';
|
||||||
|
|
||||||
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
|
||||||
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
||||||
@@ -81,4 +85,19 @@ export class IdentifiableDataService<T extends CacheableObject> extends BaseData
|
|||||||
return this.getEndpoint().pipe(
|
return this.getEndpoint().pipe(
|
||||||
map((endpoint: string) => this.getIDHref(endpoint, resourceID, ...linksToFollow)));
|
map((endpoint: string) => this.getIDHref(endpoint, resourceID, ...linksToFollow)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate a cached resource by its identifier
|
||||||
|
* @param resourceID the identifier of the resource to invalidate
|
||||||
|
*/
|
||||||
|
invalidateById(resourceID: string): Observable<boolean> {
|
||||||
|
const ok$ = this.getIDHrefObs(resourceID).pipe(
|
||||||
|
take(1),
|
||||||
|
switchMap((href) => this.invalidateByHref(href)),
|
||||||
|
);
|
||||||
|
|
||||||
|
ok$.subscribe();
|
||||||
|
|
||||||
|
return ok$;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -72,7 +72,7 @@ describe('AuthorizationDataService', () => {
|
|||||||
const ePersonUuid = 'fake-eperson-uuid';
|
const ePersonUuid = 'fake-eperson-uuid';
|
||||||
|
|
||||||
function createExpected(providedObjectUrl: string, providedEPersonUuid?: string, providedFeatureId?: FeatureID): FindListOptions {
|
function createExpected(providedObjectUrl: string, providedEPersonUuid?: string, providedFeatureId?: FeatureID): FindListOptions {
|
||||||
const searchParams = [new RequestParam('uri', providedObjectUrl)];
|
const searchParams = [new RequestParam('uri', providedObjectUrl, false)];
|
||||||
if (hasValue(providedFeatureId)) {
|
if (hasValue(providedFeatureId)) {
|
||||||
searchParams.push(new RequestParam('feature', providedFeatureId));
|
searchParams.push(new RequestParam('feature', providedFeatureId));
|
||||||
}
|
}
|
||||||
|
@@ -147,7 +147,8 @@ export class AuthorizationDataService extends BaseDataService<Authorization> imp
|
|||||||
if (isNotEmpty(options.searchParams)) {
|
if (isNotEmpty(options.searchParams)) {
|
||||||
params = [...options.searchParams];
|
params = [...options.searchParams];
|
||||||
}
|
}
|
||||||
params.push(new RequestParam('uri', objectUrl));
|
// TODO fix encode the uri parameter in the self link in the backend and set encodeValue to true afterwards
|
||||||
|
params.push(new RequestParam('uri', objectUrl, false));
|
||||||
if (hasValue(featureId)) {
|
if (hasValue(featureId)) {
|
||||||
params.push(new RequestParam('feature', featureId));
|
params.push(new RequestParam('feature', featureId));
|
||||||
}
|
}
|
||||||
|
@@ -1,35 +1,13 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { CanActivateFn } from '@angular/router';
|
||||||
import {
|
import { of as observableOf } from 'rxjs';
|
||||||
ActivatedRouteSnapshot,
|
|
||||||
Router,
|
|
||||||
RouterStateSnapshot,
|
|
||||||
} from '@angular/router';
|
|
||||||
import {
|
|
||||||
Observable,
|
|
||||||
of as observableOf,
|
|
||||||
} from 'rxjs';
|
|
||||||
|
|
||||||
import { AuthService } from '../../../auth/auth.service';
|
|
||||||
import { AuthorizationDataService } from '../authorization-data.service';
|
|
||||||
import { FeatureID } from '../feature-id';
|
import { FeatureID } from '../feature-id';
|
||||||
import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard';
|
import { singleFeatureAuthorizationGuard } from './single-feature-authorization.guard';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prevent unauthorized activating and loading of routes when the current authenticated user
|
* Prevent unauthorized activating and loading of routes when the current authenticated user
|
||||||
* isn't a Collection administrator
|
* isn't a Collection administrator
|
||||||
|
* Check group management rights
|
||||||
*/
|
*/
|
||||||
@Injectable({
|
export const collectionAdministratorGuard: CanActivateFn =
|
||||||
providedIn: 'root',
|
singleFeatureAuthorizationGuard(() => observableOf(FeatureID.IsCollectionAdmin));
|
||||||
})
|
|
||||||
export class CollectionAdministratorGuard extends SingleFeatureAuthorizationGuard {
|
|
||||||
constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) {
|
|
||||||
super(authorizationService, router, authService);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check group management rights
|
|
||||||
*/
|
|
||||||
getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
|
||||||
return observableOf(FeatureID.IsCollectionAdmin);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -1,35 +1,13 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { CanActivateFn } from '@angular/router';
|
||||||
import {
|
import { of as observableOf } from 'rxjs';
|
||||||
ActivatedRouteSnapshot,
|
|
||||||
Router,
|
|
||||||
RouterStateSnapshot,
|
|
||||||
} from '@angular/router';
|
|
||||||
import {
|
|
||||||
Observable,
|
|
||||||
of as observableOf,
|
|
||||||
} from 'rxjs';
|
|
||||||
|
|
||||||
import { AuthService } from '../../../auth/auth.service';
|
|
||||||
import { AuthorizationDataService } from '../authorization-data.service';
|
|
||||||
import { FeatureID } from '../feature-id';
|
import { FeatureID } from '../feature-id';
|
||||||
import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard';
|
import { singleFeatureAuthorizationGuard } from './single-feature-authorization.guard';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prevent unauthorized activating and loading of routes when the current authenticated user
|
* Prevent unauthorized activating and loading of routes when the current authenticated user
|
||||||
* isn't a Community administrator
|
* isn't a Community administrator
|
||||||
|
* Check group management rights
|
||||||
*/
|
*/
|
||||||
@Injectable({
|
export const communityAdministratorGuard: CanActivateFn =
|
||||||
providedIn: 'root',
|
singleFeatureAuthorizationGuard(() => observableOf(FeatureID.IsCommunityAdmin));
|
||||||
})
|
|
||||||
export class CommunityAdministratorGuard extends SingleFeatureAuthorizationGuard {
|
|
||||||
constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) {
|
|
||||||
super(authorizationService, router, authService);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check group management rights
|
|
||||||
*/
|
|
||||||
getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
|
||||||
return observableOf(FeatureID.IsCommunityAdmin);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
import {
|
import {
|
||||||
ActivatedRouteSnapshot,
|
|
||||||
ResolveFn,
|
ResolveFn,
|
||||||
Router,
|
Router,
|
||||||
RouterStateSnapshot,
|
UrlTree,
|
||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
import {
|
import {
|
||||||
Observable,
|
Observable,
|
||||||
@@ -12,52 +12,39 @@ import {
|
|||||||
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
|
||||||
import { AuthService } from '../../../auth/auth.service';
|
import { AuthService } from '../../../auth/auth.service';
|
||||||
import { DSpaceObject } from '../../../shared/dspace-object.model';
|
import { DSpaceObject } from '../../../shared/dspace-object.model';
|
||||||
import { Item } from '../../../shared/item.model';
|
|
||||||
import { RemoteData } from '../../remote-data';
|
import { RemoteData } from '../../remote-data';
|
||||||
import { AuthorizationDataService } from '../authorization-data.service';
|
import { AuthorizationDataService } from '../authorization-data.service';
|
||||||
import { FeatureID } from '../feature-id';
|
import { FeatureID } from '../feature-id';
|
||||||
import { DsoPageSingleFeatureGuard } from './dso-page-single-feature.guard';
|
import { dsoPageSingleFeatureGuard } from './dso-page-single-feature.guard';
|
||||||
|
import {
|
||||||
|
defaultDSOGetObjectUrl,
|
||||||
|
getRouteWithDSOId,
|
||||||
|
} from './dso-page-some-feature.guard';
|
||||||
|
|
||||||
const object = {
|
|
||||||
self: 'test-selflink',
|
|
||||||
} as DSpaceObject;
|
|
||||||
|
|
||||||
const testResolver: ResolveFn<RemoteData<any>> = () => createSuccessfulRemoteDataObject$(object);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test implementation of abstract class DsoPageSingleFeatureGuard
|
|
||||||
*/
|
|
||||||
class DsoPageSingleFeatureGuardImpl extends DsoPageSingleFeatureGuard<any> {
|
|
||||||
|
|
||||||
protected resolver: ResolveFn<RemoteData<Item>> = testResolver;
|
|
||||||
|
|
||||||
constructor(protected authorizationService: AuthorizationDataService,
|
|
||||||
protected router: Router,
|
|
||||||
protected authService: AuthService,
|
|
||||||
protected featureID: FeatureID) {
|
|
||||||
super(authorizationService, router, authService);
|
|
||||||
}
|
|
||||||
|
|
||||||
getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
|
||||||
return observableOf(this.featureID);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('DsoPageSingleFeatureGuard', () => {
|
describe('DsoPageSingleFeatureGuard', () => {
|
||||||
let guard: DsoPageSingleFeatureGuard<any>;
|
|
||||||
let authorizationService: AuthorizationDataService;
|
let authorizationService: AuthorizationDataService;
|
||||||
let router: Router;
|
let router: Router;
|
||||||
let authService: AuthService;
|
let authService: AuthService;
|
||||||
|
let resolver: ResolveFn<RemoteData<any>>;
|
||||||
|
let object: DSpaceObject;
|
||||||
let route;
|
let route;
|
||||||
let parentRoute;
|
let parentRoute;
|
||||||
|
|
||||||
|
let featureId: FeatureID;
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
|
object = {
|
||||||
|
self: 'test-selflink',
|
||||||
|
} as DSpaceObject;
|
||||||
|
|
||||||
authorizationService = jasmine.createSpyObj('authorizationService', {
|
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||||
isAuthorized: observableOf(true),
|
isAuthorized: observableOf(true),
|
||||||
});
|
});
|
||||||
router = jasmine.createSpyObj('router', {
|
router = jasmine.createSpyObj('router', {
|
||||||
parseUrl: {},
|
parseUrl: {},
|
||||||
});
|
});
|
||||||
|
resolver = () => createSuccessfulRemoteDataObject$(object);
|
||||||
authService = jasmine.createSpyObj('authService', {
|
authService = jasmine.createSpyObj('authService', {
|
||||||
isAuthenticated: observableOf(true),
|
isAuthenticated: observableOf(true),
|
||||||
});
|
});
|
||||||
@@ -71,16 +58,25 @@ describe('DsoPageSingleFeatureGuard', () => {
|
|||||||
},
|
},
|
||||||
parent: parentRoute,
|
parent: parentRoute,
|
||||||
};
|
};
|
||||||
guard = new DsoPageSingleFeatureGuardImpl(authorizationService, router, authService, undefined);
|
|
||||||
|
featureId = FeatureID.LoginOnBehalfOf;
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||||
|
{ provide: Router, useValue: router },
|
||||||
|
{ provide: AuthService, useValue: authService },
|
||||||
|
],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
init();
|
init();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getObjectUrl', () => {
|
describe('defaultDSOGetObjectUrl', () => {
|
||||||
it('should return the resolved object\'s selflink', (done) => {
|
it('should return the resolved object\'s selflink', (done) => {
|
||||||
guard.getObjectUrl(route, undefined).subscribe((selflink) => {
|
defaultDSOGetObjectUrl(resolver)(route, undefined).subscribe((selflink) => {
|
||||||
expect(selflink).toEqual(object.self);
|
expect(selflink).toEqual(object.self);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
@@ -89,8 +85,23 @@ describe('DsoPageSingleFeatureGuard', () => {
|
|||||||
|
|
||||||
describe('getRouteWithDSOId', () => {
|
describe('getRouteWithDSOId', () => {
|
||||||
it('should return the route that has the UUID of the DSO', () => {
|
it('should return the route that has the UUID of the DSO', () => {
|
||||||
const foundRoute = (guard as any).getRouteWithDSOId(route);
|
const foundRoute = getRouteWithDSOId(route);
|
||||||
expect(foundRoute).toBe(parentRoute);
|
expect(foundRoute).toBe(parentRoute);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('dsoPageSingleFeatureGuard', () => {
|
||||||
|
it('should call authorizationService.isAuthenticated with the appropriate arguments', (done) => {
|
||||||
|
const result$ = TestBed.runInInjectionContext(() => {
|
||||||
|
return dsoPageSingleFeatureGuard(
|
||||||
|
() => resolver, () => observableOf(featureId),
|
||||||
|
)(route, { url: 'current-url' } as any);
|
||||||
|
}) as Observable<boolean | UrlTree>;
|
||||||
|
|
||||||
|
result$.subscribe(() => {
|
||||||
|
expect(authorizationService.isAuthorized).toHaveBeenCalledWith(featureId, object.self, undefined);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,31 +1,27 @@
|
|||||||
import {
|
import {
|
||||||
ActivatedRouteSnapshot,
|
ActivatedRouteSnapshot,
|
||||||
|
CanActivateFn,
|
||||||
|
ResolveFn,
|
||||||
RouterStateSnapshot,
|
RouterStateSnapshot,
|
||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
import { DSpaceObject } from '../../../shared/dspace-object.model';
|
import { DSpaceObject } from '../../../shared/dspace-object.model';
|
||||||
|
import { RemoteData } from '../../remote-data';
|
||||||
import { FeatureID } from '../feature-id';
|
import { FeatureID } from '../feature-id';
|
||||||
import { DsoPageSomeFeatureGuard } from './dso-page-some-feature.guard';
|
import { dsoPageSomeFeatureGuard } from './dso-page-some-feature.guard';
|
||||||
|
import { SingleFeatureGuardParamFn } from './single-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 a specific feature
|
||||||
* This guard utilizes a resolver to retrieve the relevant object to check authorizations for
|
* This guard utilizes a resolver to retrieve the relevant object to check authorizations for
|
||||||
*/
|
*/
|
||||||
export abstract class DsoPageSingleFeatureGuard<T extends DSpaceObject> extends DsoPageSomeFeatureGuard<T> {
|
export const dsoPageSingleFeatureGuard = <T extends DSpaceObject> (
|
||||||
/**
|
getResolveFn: () => ResolveFn<RemoteData<T>>,
|
||||||
* The features to check authorization for
|
getFeatureID: SingleFeatureGuardParamFn,
|
||||||
*/
|
): CanActivateFn => dsoPageSomeFeatureGuard(
|
||||||
getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID[]> {
|
getResolveFn,
|
||||||
return this.getFeatureID(route, state).pipe(
|
(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID[]> => getFeatureID(route, state).pipe(
|
||||||
map((featureID) => [featureID]),
|
map((featureID: 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,8 +1,8 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
import {
|
import {
|
||||||
ActivatedRouteSnapshot,
|
|
||||||
ResolveFn,
|
ResolveFn,
|
||||||
Router,
|
Router,
|
||||||
RouterStateSnapshot,
|
UrlTree,
|
||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
import {
|
import {
|
||||||
Observable,
|
Observable,
|
||||||
@@ -12,53 +12,39 @@ import {
|
|||||||
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
|
||||||
import { AuthService } from '../../../auth/auth.service';
|
import { AuthService } from '../../../auth/auth.service';
|
||||||
import { DSpaceObject } from '../../../shared/dspace-object.model';
|
import { DSpaceObject } from '../../../shared/dspace-object.model';
|
||||||
import { Item } from '../../../shared/item.model';
|
|
||||||
import { RemoteData } from '../../remote-data';
|
import { RemoteData } from '../../remote-data';
|
||||||
import { AuthorizationDataService } from '../authorization-data.service';
|
import { AuthorizationDataService } from '../authorization-data.service';
|
||||||
import { FeatureID } from '../feature-id';
|
import { FeatureID } from '../feature-id';
|
||||||
import { DsoPageSomeFeatureGuard } from './dso-page-some-feature.guard';
|
import {
|
||||||
|
defaultDSOGetObjectUrl,
|
||||||
|
dsoPageSomeFeatureGuard,
|
||||||
|
getRouteWithDSOId,
|
||||||
|
} from './dso-page-some-feature.guard';
|
||||||
|
|
||||||
const object = {
|
|
||||||
self: 'test-selflink',
|
|
||||||
} as DSpaceObject;
|
|
||||||
|
|
||||||
const testResolver: ResolveFn<RemoteData<any>> = () => createSuccessfulRemoteDataObject$(object);
|
describe('dsoPageSomeFeatureGuard and its functions', () => {
|
||||||
|
|
||||||
/**
|
|
||||||
* Test implementation of abstract class DsoPageSomeFeatureGuard
|
|
||||||
*/
|
|
||||||
class DsoPageSomeFeatureGuardImpl extends DsoPageSomeFeatureGuard<any> {
|
|
||||||
|
|
||||||
protected resolver: ResolveFn<RemoteData<Item>> = testResolver;
|
|
||||||
|
|
||||||
constructor(protected authorizationService: AuthorizationDataService,
|
|
||||||
protected router: Router,
|
|
||||||
protected authService: AuthService,
|
|
||||||
protected featureIDs: FeatureID[]) {
|
|
||||||
super(authorizationService, router, authService);
|
|
||||||
}
|
|
||||||
|
|
||||||
getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID[]> {
|
|
||||||
return observableOf(this.featureIDs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('DsoPageSomeFeatureGuard', () => {
|
|
||||||
let guard: DsoPageSomeFeatureGuard<any>;
|
|
||||||
let authorizationService: AuthorizationDataService;
|
let authorizationService: AuthorizationDataService;
|
||||||
let router: Router;
|
let router: Router;
|
||||||
let authService: AuthService;
|
let authService: AuthService;
|
||||||
|
let resolver: ResolveFn<RemoteData<any>>;
|
||||||
|
let object: DSpaceObject;
|
||||||
let route;
|
let route;
|
||||||
let parentRoute;
|
let parentRoute;
|
||||||
|
|
||||||
|
let featureIds: FeatureID[];
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
|
object = {
|
||||||
|
self: 'test-selflink',
|
||||||
|
} as DSpaceObject;
|
||||||
|
featureIds = [FeatureID.LoginOnBehalfOf, FeatureID.CanDelete];
|
||||||
authorizationService = jasmine.createSpyObj('authorizationService', {
|
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||||
isAuthorized: observableOf(true),
|
isAuthorized: observableOf(true),
|
||||||
});
|
});
|
||||||
router = jasmine.createSpyObj('router', {
|
router = jasmine.createSpyObj('router', {
|
||||||
parseUrl: {},
|
parseUrl: {},
|
||||||
});
|
});
|
||||||
|
resolver = () => createSuccessfulRemoteDataObject$(object);
|
||||||
authService = jasmine.createSpyObj('authService', {
|
authService = jasmine.createSpyObj('authService', {
|
||||||
isAuthenticated: observableOf(true),
|
isAuthenticated: observableOf(true),
|
||||||
});
|
});
|
||||||
@@ -72,16 +58,25 @@ describe('DsoPageSomeFeatureGuard', () => {
|
|||||||
},
|
},
|
||||||
parent: parentRoute,
|
parent: parentRoute,
|
||||||
};
|
};
|
||||||
guard = new DsoPageSomeFeatureGuardImpl(authorizationService, router, authService, []);
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||||
|
{ provide: Router, useValue: router },
|
||||||
|
{ provide: AuthService, useValue: authService },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
init();
|
init();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getObjectUrl', () => {
|
|
||||||
|
describe('defaultDSOGetObjectUrl', () => {
|
||||||
it('should return the resolved object\'s selflink', (done) => {
|
it('should return the resolved object\'s selflink', (done) => {
|
||||||
guard.getObjectUrl(route, undefined).subscribe((selflink) => {
|
defaultDSOGetObjectUrl(resolver)(route, undefined).subscribe((selflink) => {
|
||||||
expect(selflink).toEqual(object.self);
|
expect(selflink).toEqual(object.self);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
@@ -90,8 +85,26 @@ describe('DsoPageSomeFeatureGuard', () => {
|
|||||||
|
|
||||||
describe('getRouteWithDSOId', () => {
|
describe('getRouteWithDSOId', () => {
|
||||||
it('should return the route that has the UUID of the DSO', () => {
|
it('should return the route that has the UUID of the DSO', () => {
|
||||||
const foundRoute = (guard as any).getRouteWithDSOId(route);
|
const foundRoute = getRouteWithDSOId(route);
|
||||||
expect(foundRoute).toBe(parentRoute);
|
expect(foundRoute).toBe(parentRoute);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('dsoPageSomeFeatureGuard', () => {
|
||||||
|
it('should call authorizationService.isAuthenticated with the appropriate arguments', (done) => {
|
||||||
|
const result$ = TestBed.runInInjectionContext(() => {
|
||||||
|
return dsoPageSomeFeatureGuard(
|
||||||
|
() => resolver, () => observableOf(featureIds),
|
||||||
|
)(route, { url: 'current-url' } as any);
|
||||||
|
}) as Observable<boolean | UrlTree>;
|
||||||
|
|
||||||
|
result$.subscribe(() => {
|
||||||
|
featureIds.forEach((featureId: FeatureID) => {
|
||||||
|
expect(authorizationService.isAuthorized).toHaveBeenCalledWith(featureId, object.self, undefined);
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
ActivatedRouteSnapshot,
|
ActivatedRouteSnapshot,
|
||||||
|
CanActivateFn,
|
||||||
ResolveFn,
|
ResolveFn,
|
||||||
Router,
|
|
||||||
RouterStateSnapshot,
|
RouterStateSnapshot,
|
||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
@@ -11,47 +11,50 @@ import {
|
|||||||
hasNoValue,
|
hasNoValue,
|
||||||
hasValue,
|
hasValue,
|
||||||
} from '../../../../shared/empty.util';
|
} from '../../../../shared/empty.util';
|
||||||
import { AuthService } from '../../../auth/auth.service';
|
|
||||||
import { DSpaceObject } from '../../../shared/dspace-object.model';
|
import { DSpaceObject } from '../../../shared/dspace-object.model';
|
||||||
import { getAllSucceededRemoteDataPayload } from '../../../shared/operators';
|
import { getAllSucceededRemoteDataPayload } from '../../../shared/operators';
|
||||||
import { RemoteData } from '../../remote-data';
|
import { RemoteData } from '../../remote-data';
|
||||||
import { AuthorizationDataService } from '../authorization-data.service';
|
import { FeatureID } from '../feature-id';
|
||||||
import { SomeFeatureAuthorizationGuard } from './some-feature-authorization.guard';
|
import {
|
||||||
|
someFeatureAuthorizationGuard,
|
||||||
|
SomeFeatureGuardParamFn,
|
||||||
|
StringGuardParamFn,
|
||||||
|
} from './some-feature-authorization.guard';
|
||||||
|
|
||||||
|
export declare type DSOGetObjectURlFn = <T extends DSpaceObject>(resolve: ResolveFn<RemoteData<T>>) => StringGuardParamFn;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract Guard for preventing unauthorized access to {@link DSpaceObject} pages that require rights for any specific feature in a list
|
* Method to resolve resolve (parent) route that contains the UUID of the DSO
|
||||||
* This guard utilizes a resolver to retrieve the relevant object to check authorizations for
|
* @param route The current route
|
||||||
*/
|
*/
|
||||||
export abstract class DsoPageSomeFeatureGuard<T extends DSpaceObject> extends SomeFeatureAuthorizationGuard {
|
export const getRouteWithDSOId = (route: ActivatedRouteSnapshot): ActivatedRouteSnapshot => {
|
||||||
|
let routeWithDSOId = route;
|
||||||
protected abstract resolver: ResolveFn<RemoteData<DSpaceObject>>;
|
while (hasNoValue(routeWithDSOId.params.id) && hasValue(routeWithDSOId.parent)) {
|
||||||
|
routeWithDSOId = routeWithDSOId.parent;
|
||||||
constructor(protected authorizationService: AuthorizationDataService,
|
|
||||||
protected router: Router,
|
|
||||||
protected authService: AuthService) {
|
|
||||||
super(authorizationService, router, authService);
|
|
||||||
}
|
}
|
||||||
|
return routeWithDSOId;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Check authorization rights for the object resolved using the provided resolver
|
|
||||||
*/
|
export const defaultDSOGetObjectUrl: DSOGetObjectURlFn = <T extends DSpaceObject>(resolve: ResolveFn<RemoteData<T>>): StringGuardParamFn => {
|
||||||
getObjectUrl(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<string> {
|
return (route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<string> => {
|
||||||
const routeWithObjectID = this.getRouteWithDSOId(route);
|
const routeWithObjectID = getRouteWithDSOId(route);
|
||||||
return (this.resolver(routeWithObjectID, state) as Observable<RemoteData<T>>).pipe(
|
return (resolve(routeWithObjectID, state) as Observable<RemoteData<T>>).pipe(
|
||||||
getAllSucceededRemoteDataPayload(),
|
getAllSucceededRemoteDataPayload(),
|
||||||
map((dso) => dso.self),
|
map((dso) => dso.self),
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to resolve (parent) route that contains the UUID of the DSO
|
* Guard for preventing unauthorized access to {@link DSpaceObject} pages that require rights for any specific feature in a list
|
||||||
* @param route The current route
|
* This guard utilizes a resolver to retrieve the relevant object to check authorizations for
|
||||||
*/
|
*/
|
||||||
protected getRouteWithDSOId(route: ActivatedRouteSnapshot): ActivatedRouteSnapshot {
|
export const dsoPageSomeFeatureGuard = <T extends DSpaceObject>(
|
||||||
let routeWithDSOId = route;
|
getResolveFn: () => ResolveFn<RemoteData<T>>,
|
||||||
while (hasNoValue(routeWithDSOId.params.id) && hasValue(routeWithDSOId.parent)) {
|
getFeatureIDs: SomeFeatureGuardParamFn,
|
||||||
routeWithDSOId = routeWithDSOId.parent;
|
getObjectUrl: DSOGetObjectURlFn = defaultDSOGetObjectUrl,
|
||||||
}
|
getEPersonUuid?: StringGuardParamFn,
|
||||||
return routeWithDSOId;
|
): CanActivateFn => someFeatureAuthorizationGuard((route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID[]> => getFeatureIDs(route, state), getObjectUrl(getResolveFn()), getEPersonUuid);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -1,35 +1,12 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { CanActivateFn } from '@angular/router';
|
||||||
import {
|
import { of as observableOf } from 'rxjs';
|
||||||
ActivatedRouteSnapshot,
|
|
||||||
Router,
|
|
||||||
RouterStateSnapshot,
|
|
||||||
} from '@angular/router';
|
|
||||||
import {
|
|
||||||
Observable,
|
|
||||||
of as observableOf,
|
|
||||||
} from 'rxjs';
|
|
||||||
|
|
||||||
import { AuthService } from '../../../auth/auth.service';
|
|
||||||
import { AuthorizationDataService } from '../authorization-data.service';
|
|
||||||
import { FeatureID } from '../feature-id';
|
import { FeatureID } from '../feature-id';
|
||||||
import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard';
|
import { singleFeatureAuthorizationGuard } from './single-feature-authorization.guard';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prevent unauthorized activating and loading of routes when the current authenticated user doesn't have group
|
* Prevent unauthorized activating and loading of routes when the current authenticated user doesn't have group
|
||||||
* management rights
|
* management rights
|
||||||
*/
|
*/
|
||||||
@Injectable({
|
export const groupAdministratorGuard: CanActivateFn =
|
||||||
providedIn: 'root',
|
singleFeatureAuthorizationGuard(() => observableOf(FeatureID.CanManageGroups));
|
||||||
})
|
|
||||||
export class GroupAdministratorGuard extends SingleFeatureAuthorizationGuard {
|
|
||||||
constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) {
|
|
||||||
super(authorizationService, router, authService);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check group management rights
|
|
||||||
*/
|
|
||||||
getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
|
||||||
return observableOf(FeatureID.CanManageGroups);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -1,7 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
ActivatedRouteSnapshot,
|
TestBed,
|
||||||
|
waitForAsync,
|
||||||
|
} from '@angular/core/testing';
|
||||||
|
import {
|
||||||
Router,
|
Router,
|
||||||
RouterStateSnapshot,
|
UrlTree,
|
||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
import {
|
import {
|
||||||
Observable,
|
Observable,
|
||||||
@@ -11,37 +14,9 @@ import {
|
|||||||
import { AuthService } from '../../../auth/auth.service';
|
import { AuthService } from '../../../auth/auth.service';
|
||||||
import { AuthorizationDataService } from '../authorization-data.service';
|
import { AuthorizationDataService } from '../authorization-data.service';
|
||||||
import { FeatureID } from '../feature-id';
|
import { FeatureID } from '../feature-id';
|
||||||
import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard';
|
import { singleFeatureAuthorizationGuard } from './single-feature-authorization.guard';
|
||||||
|
|
||||||
/**
|
describe('singleFeatureAuthorizationGuard', () => {
|
||||||
* Test implementation of abstract class SingleFeatureAuthorizationGuard
|
|
||||||
* Provide the return values of the overwritten getters as constructor arguments
|
|
||||||
*/
|
|
||||||
class SingleFeatureAuthorizationGuardImpl extends SingleFeatureAuthorizationGuard {
|
|
||||||
constructor(protected authorizationService: AuthorizationDataService,
|
|
||||||
protected router: Router,
|
|
||||||
protected authService: AuthService,
|
|
||||||
protected featureId: FeatureID,
|
|
||||||
protected objectUrl: string,
|
|
||||||
protected ePersonUuid: string) {
|
|
||||||
super(authorizationService, router, authService);
|
|
||||||
}
|
|
||||||
|
|
||||||
getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
|
||||||
return observableOf(this.featureId);
|
|
||||||
}
|
|
||||||
|
|
||||||
getObjectUrl(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<string> {
|
|
||||||
return observableOf(this.objectUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
getEPersonUuid(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<string> {
|
|
||||||
return observableOf(this.ePersonUuid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('SingleFeatureAuthorizationGuard', () => {
|
|
||||||
let guard: SingleFeatureAuthorizationGuard;
|
|
||||||
let authorizationService: AuthorizationDataService;
|
let authorizationService: AuthorizationDataService;
|
||||||
let router: Router;
|
let router: Router;
|
||||||
let authService: AuthService;
|
let authService: AuthService;
|
||||||
@@ -64,17 +39,36 @@ describe('SingleFeatureAuthorizationGuard', () => {
|
|||||||
authService = jasmine.createSpyObj('authService', {
|
authService = jasmine.createSpyObj('authService', {
|
||||||
isAuthenticated: observableOf(true),
|
isAuthenticated: observableOf(true),
|
||||||
});
|
});
|
||||||
guard = new SingleFeatureAuthorizationGuardImpl(authorizationService, router, authService, featureId, objectUrl, ePersonUuid);
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||||
|
{ provide: Router, useValue: router },
|
||||||
|
{ provide: AuthService, useValue: authService },
|
||||||
|
],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
init();
|
init();
|
||||||
});
|
}));
|
||||||
|
|
||||||
describe('canActivate', () => {
|
describe('canActivate', () => {
|
||||||
it('should call authorizationService.isAuthenticated with the appropriate arguments', () => {
|
it('should call authorizationService.isAuthenticated with the appropriate arguments', (done: DoneFn) => {
|
||||||
guard.canActivate(undefined, { url: 'current-url' } as any).subscribe();
|
const result$ = TestBed.runInInjectionContext(() => {
|
||||||
expect(authorizationService.isAuthorized).toHaveBeenCalledWith(featureId, objectUrl, ePersonUuid);
|
return singleFeatureAuthorizationGuard(
|
||||||
|
() => observableOf(featureId),
|
||||||
|
() => observableOf(objectUrl),
|
||||||
|
() => observableOf(ePersonUuid),
|
||||||
|
)(undefined, { url: 'current-url' } as any);
|
||||||
|
}) as Observable<boolean | UrlTree>;
|
||||||
|
|
||||||
|
|
||||||
|
result$.subscribe(() => {
|
||||||
|
expect(authorizationService.isAuthorized).toHaveBeenCalledWith(featureId, objectUrl, ePersonUuid);
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,31 +1,35 @@
|
|||||||
import {
|
import {
|
||||||
ActivatedRouteSnapshot,
|
ActivatedRouteSnapshot,
|
||||||
|
CanActivateFn,
|
||||||
RouterStateSnapshot,
|
RouterStateSnapshot,
|
||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
import { FeatureID } from '../feature-id';
|
import { FeatureID } from '../feature-id';
|
||||||
import { SomeFeatureAuthorizationGuard } from './some-feature-authorization.guard';
|
import {
|
||||||
|
someFeatureAuthorizationGuard,
|
||||||
|
StringGuardParamFn,
|
||||||
|
} from './some-feature-authorization.guard';
|
||||||
|
|
||||||
|
export declare type SingleFeatureGuardParamFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => Observable<FeatureID>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract Guard for preventing unauthorized activating and loading of routes when a user
|
* Guard for preventing unauthorized activating and loading of routes when a user doesn't have
|
||||||
* doesn't have authorized rights on a specific feature and/or object.
|
* 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.
|
*
|
||||||
|
* @param getFeatureID The feature to check authorization for
|
||||||
|
* @param getObjectUrl The URL of the object to check if the user has authorized rights for,
|
||||||
|
* Optional, if not provided, the {@link Site}'s URL will be assumed
|
||||||
|
* @param getEPersonUuid The UUID of the user to check authorization rights for.
|
||||||
|
* Optional, if not provided, the authenticated user's UUID will be assumed.
|
||||||
*/
|
*/
|
||||||
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]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
export const singleFeatureAuthorizationGuard = (
|
||||||
* The type of feature to check authorization for
|
getFeatureID: SingleFeatureGuardParamFn,
|
||||||
* Override this method to define a feature
|
getObjectUrl?: StringGuardParamFn,
|
||||||
*/
|
getEPersonUuid?: StringGuardParamFn,
|
||||||
abstract getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID>;
|
): CanActivateFn => someFeatureAuthorizationGuard(
|
||||||
}
|
(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID[]> => getFeatureID(route, state).pipe(
|
||||||
|
map((featureID: FeatureID) => [featureID]),
|
||||||
|
), getObjectUrl, getEPersonUuid);
|
||||||
|
@@ -1,33 +1,12 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { CanActivateFn } from '@angular/router';
|
||||||
import {
|
import { of as observableOf } from 'rxjs';
|
||||||
ActivatedRouteSnapshot,
|
|
||||||
Router,
|
|
||||||
RouterStateSnapshot,
|
|
||||||
} from '@angular/router';
|
|
||||||
import {
|
|
||||||
Observable,
|
|
||||||
of as observableOf,
|
|
||||||
} from 'rxjs';
|
|
||||||
|
|
||||||
import { AuthService } from '../../../auth/auth.service';
|
|
||||||
import { AuthorizationDataService } from '../authorization-data.service';
|
|
||||||
import { FeatureID } from '../feature-id';
|
import { FeatureID } from '../feature-id';
|
||||||
import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard';
|
import { singleFeatureAuthorizationGuard } from './single-feature-authorization.guard';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prevent unauthorized activating and loading of routes when the current authenticated user doesn't have administrator
|
* Prevent unauthorized activating and loading of routes when the current authenticated user doesn't have administrator
|
||||||
* rights to the {@link Site}
|
* rights to the {@link Site}
|
||||||
*/
|
*/
|
||||||
@Injectable({ providedIn: 'root' })
|
export const siteAdministratorGuard: CanActivateFn =
|
||||||
export class SiteAdministratorGuard extends SingleFeatureAuthorizationGuard {
|
singleFeatureAuthorizationGuard(() => observableOf(FeatureID.AdministratorOf));
|
||||||
constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) {
|
|
||||||
super(authorizationService, router, authService);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check administrator authorization rights
|
|
||||||
*/
|
|
||||||
getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
|
||||||
return observableOf(FeatureID.AdministratorOf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -1,33 +1,12 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { CanActivateFn } from '@angular/router';
|
||||||
import {
|
import { of as observableOf } from 'rxjs';
|
||||||
ActivatedRouteSnapshot,
|
|
||||||
Router,
|
|
||||||
RouterStateSnapshot,
|
|
||||||
} from '@angular/router';
|
|
||||||
import {
|
|
||||||
Observable,
|
|
||||||
of as observableOf,
|
|
||||||
} from 'rxjs';
|
|
||||||
|
|
||||||
import { AuthService } from '../../../auth/auth.service';
|
|
||||||
import { AuthorizationDataService } from '../authorization-data.service';
|
|
||||||
import { FeatureID } from '../feature-id';
|
import { FeatureID } from '../feature-id';
|
||||||
import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard';
|
import { singleFeatureAuthorizationGuard } from './single-feature-authorization.guard';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prevent unauthorized activating and loading of routes when the current authenticated user doesn't have registration
|
* Prevent unauthorized activating and loading of routes when the current authenticated user doesn't have registration
|
||||||
* rights to the {@link Site}
|
* rights to the {@link Site}
|
||||||
*/
|
*/
|
||||||
@Injectable({ providedIn: 'root' })
|
export const siteRegisterGuard: CanActivateFn =
|
||||||
export class SiteRegisterGuard extends SingleFeatureAuthorizationGuard {
|
singleFeatureAuthorizationGuard(() => observableOf(FeatureID.EPersonRegistration));
|
||||||
constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) {
|
|
||||||
super(authorizationService, router, authService);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check registration authorization rights
|
|
||||||
*/
|
|
||||||
getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
|
||||||
return observableOf(FeatureID.EPersonRegistration);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -1,7 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
ActivatedRouteSnapshot,
|
TestBed,
|
||||||
|
waitForAsync,
|
||||||
|
} from '@angular/core/testing';
|
||||||
|
import {
|
||||||
Router,
|
Router,
|
||||||
RouterStateSnapshot,
|
UrlTree,
|
||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
import {
|
import {
|
||||||
Observable,
|
Observable,
|
||||||
@@ -11,37 +14,9 @@ import {
|
|||||||
import { AuthService } from '../../../auth/auth.service';
|
import { AuthService } from '../../../auth/auth.service';
|
||||||
import { AuthorizationDataService } from '../authorization-data.service';
|
import { AuthorizationDataService } from '../authorization-data.service';
|
||||||
import { FeatureID } from '../feature-id';
|
import { FeatureID } from '../feature-id';
|
||||||
import { SomeFeatureAuthorizationGuard } from './some-feature-authorization.guard';
|
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', () => {
|
describe('SomeFeatureAuthorizationGuard', () => {
|
||||||
let guard: SomeFeatureAuthorizationGuard;
|
|
||||||
let authorizationService: AuthorizationDataService;
|
let authorizationService: AuthorizationDataService;
|
||||||
let router: Router;
|
let router: Router;
|
||||||
let authService: AuthService;
|
let authService: AuthService;
|
||||||
@@ -62,18 +37,27 @@ describe('SomeFeatureAuthorizationGuard', () => {
|
|||||||
return observableOf(authorizedFeatureIds.indexOf(featureId) > -1);
|
return observableOf(authorizedFeatureIds.indexOf(featureId) > -1);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
router = jasmine.createSpyObj('router', {
|
router = jasmine.createSpyObj('router', {
|
||||||
parseUrl: {},
|
parseUrl: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
authService = jasmine.createSpyObj('authService', {
|
authService = jasmine.createSpyObj('authService', {
|
||||||
isAuthenticated: observableOf(true),
|
isAuthenticated: observableOf(true),
|
||||||
});
|
});
|
||||||
guard = new SomeFeatureAuthorizationGuardImpl(authorizationService, router, authService, featureIds, objectUrl, ePersonUuid);
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||||
|
{ provide: Router, useValue: router },
|
||||||
|
{ provide: AuthService, useValue: authService },
|
||||||
|
],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
init();
|
init();
|
||||||
});
|
}));
|
||||||
|
|
||||||
describe('canActivate', () => {
|
describe('canActivate', () => {
|
||||||
describe('when the user isn\'t authorized', () => {
|
describe('when the user isn\'t authorized', () => {
|
||||||
@@ -82,7 +66,16 @@ describe('SomeFeatureAuthorizationGuard', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not return true', (done) => {
|
it('should not return true', (done) => {
|
||||||
guard.canActivate(undefined, { url: 'current-url' } as any).subscribe((result) => {
|
|
||||||
|
const result$ = TestBed.runInInjectionContext(() => {
|
||||||
|
return someFeatureAuthorizationGuard(
|
||||||
|
() => observableOf(featureIds),
|
||||||
|
() => observableOf(objectUrl),
|
||||||
|
() => observableOf(ePersonUuid),
|
||||||
|
)(undefined, { url: 'current-url' } as any);
|
||||||
|
}) as Observable<boolean | UrlTree>;
|
||||||
|
|
||||||
|
result$.subscribe((result) => {
|
||||||
expect(result).not.toEqual(true);
|
expect(result).not.toEqual(true);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
@@ -95,7 +88,16 @@ describe('SomeFeatureAuthorizationGuard', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return true', (done) => {
|
it('should return true', (done) => {
|
||||||
guard.canActivate(undefined, { url: 'current-url' } as any).subscribe((result) => {
|
|
||||||
|
const result$ = TestBed.runInInjectionContext(() => {
|
||||||
|
return someFeatureAuthorizationGuard(
|
||||||
|
() => observableOf(featureIds),
|
||||||
|
() => observableOf(objectUrl),
|
||||||
|
() => observableOf(ePersonUuid),
|
||||||
|
)(undefined, { url: 'current-url' } as any);
|
||||||
|
}) as Observable<boolean | UrlTree>;
|
||||||
|
|
||||||
|
result$.subscribe((result) => {
|
||||||
expect(result).toEqual(true);
|
expect(result).toEqual(true);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
@@ -108,7 +110,16 @@ describe('SomeFeatureAuthorizationGuard', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return true', (done) => {
|
it('should return true', (done) => {
|
||||||
guard.canActivate(undefined, { url: 'current-url' } as any).subscribe((result) => {
|
|
||||||
|
const result$ = TestBed.runInInjectionContext(() => {
|
||||||
|
return someFeatureAuthorizationGuard(
|
||||||
|
() => observableOf(featureIds),
|
||||||
|
() => observableOf(objectUrl),
|
||||||
|
() => observableOf(ePersonUuid),
|
||||||
|
)(undefined, { url: 'current-url' } as any);
|
||||||
|
}) as Observable<boolean | UrlTree>;
|
||||||
|
|
||||||
|
result$.subscribe((result) => {
|
||||||
expect(result).toEqual(true);
|
expect(result).toEqual(true);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
|
import { inject } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
ActivatedRouteSnapshot,
|
ActivatedRouteSnapshot,
|
||||||
|
CanActivateFn,
|
||||||
Router,
|
Router,
|
||||||
RouterStateSnapshot,
|
RouterStateSnapshot,
|
||||||
UrlTree,
|
UrlTree,
|
||||||
@@ -16,49 +18,39 @@ import { returnForbiddenUrlTreeOrLoginOnAllFalse } from '../../../shared/authori
|
|||||||
import { AuthorizationDataService } from '../authorization-data.service';
|
import { AuthorizationDataService } from '../authorization-data.service';
|
||||||
import { FeatureID } from '../feature-id';
|
import { FeatureID } from '../feature-id';
|
||||||
|
|
||||||
|
export declare type SomeFeatureGuardParamFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => Observable<FeatureID[]>;
|
||||||
|
export declare type StringGuardParamFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => Observable<string>;
|
||||||
|
export const defaultStringGuardParamFn = () => observableOf(undefined);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract Guard for preventing unauthorized activating and loading of routes when a user
|
* Guard for preventing unauthorized activating and loading of routes when a user doesn't have
|
||||||
* doesn't have authorized rights on any of the specified features and/or object.
|
* 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.
|
|
||||||
|
* @param getFeatureIDs The features to check authorization for
|
||||||
|
* @param getObjectUrl The URL of the object to check if the user has authorized rights for,
|
||||||
|
* Optional, if not provided, the {@link Site}'s URL will be assumed
|
||||||
|
* @param getEPersonUuid The UUID of the user to check authorization rights for.
|
||||||
|
* Optional, if not provided, the authenticated user's UUID will be assumed.
|
||||||
*/
|
*/
|
||||||
export abstract class SomeFeatureAuthorizationGuard {
|
export const someFeatureAuthorizationGuard = (
|
||||||
constructor(protected authorizationService: AuthorizationDataService,
|
getFeatureIDs: SomeFeatureGuardParamFn,
|
||||||
protected router: Router,
|
getObjectUrl: StringGuardParamFn = defaultStringGuardParamFn,
|
||||||
protected authService: AuthService) {
|
getEPersonUuid: StringGuardParamFn = defaultStringGuardParamFn,
|
||||||
}
|
): CanActivateFn => {
|
||||||
|
return (route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> => {
|
||||||
/**
|
const authorizationService = inject(AuthorizationDataService);
|
||||||
* True when user has authorization rights for the feature and object provided
|
const router = inject(Router);
|
||||||
* Redirect the user to the unauthorized page when they are not authorized for the given feature
|
const authService = inject(AuthService);
|
||||||
*/
|
return observableCombineLatest([
|
||||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
|
getFeatureIDs(route, state),
|
||||||
return observableCombineLatest(this.getFeatureIDs(route, state), this.getObjectUrl(route, state), this.getEPersonUuid(route, state)).pipe(
|
getObjectUrl(route, state),
|
||||||
switchMap(([featureIDs, objectUrl, ePersonUuid]) =>
|
getEPersonUuid(route, state),
|
||||||
observableCombineLatest(...featureIDs.map((featureID) => this.authorizationService.isAuthorized(featureID, objectUrl, ePersonUuid))),
|
]).pipe(
|
||||||
|
switchMap(([featureIDs, objectUrl, ePersonUuid]: [FeatureID[], string, string]) =>
|
||||||
|
observableCombineLatest(featureIDs.map((featureID) => authorizationService.isAuthorized(featureID, objectUrl, ePersonUuid))),
|
||||||
),
|
),
|
||||||
returnForbiddenUrlTreeOrLoginOnAllFalse(this.router, this.authService, state.url),
|
returnForbiddenUrlTreeOrLoginOnAllFalse(router, authService, state.url),
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* The features to check authorization for
|
|
||||||
* Override this method to define a list of features
|
|
||||||
*/
|
|
||||||
abstract getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID[]>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The URL of the object to check if the user has authorized rights for
|
|
||||||
* Override this method to define an object URL. If not provided, the {@link Site}'s URL will be used
|
|
||||||
*/
|
|
||||||
getObjectUrl(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<string> {
|
|
||||||
return observableOf(undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The UUID of the user to check authorization rights for
|
|
||||||
* Override this method to define an {@link EPerson} UUID. If not provided, the authenticated user's UUID will be used.
|
|
||||||
*/
|
|
||||||
getEPersonUuid(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<string> {
|
|
||||||
return observableOf(undefined);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -1,35 +1,12 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { CanActivateFn } from '@angular/router';
|
||||||
import {
|
import { of as observableOf } from 'rxjs';
|
||||||
ActivatedRouteSnapshot,
|
|
||||||
Router,
|
|
||||||
RouterStateSnapshot,
|
|
||||||
} from '@angular/router';
|
|
||||||
import {
|
|
||||||
Observable,
|
|
||||||
of as observableOf,
|
|
||||||
} from 'rxjs';
|
|
||||||
|
|
||||||
import { AuthService } from '../../../auth/auth.service';
|
|
||||||
import { AuthorizationDataService } from '../authorization-data.service';
|
|
||||||
import { FeatureID } from '../feature-id';
|
import { FeatureID } from '../feature-id';
|
||||||
import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard';
|
import { singleFeatureAuthorizationGuard } from './single-feature-authorization.guard';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prevent unauthorized activating and loading of routes when the current authenticated user doesn't have group
|
* Prevent unauthorized activating and loading of routes when the current authenticated user doesn't have group
|
||||||
* management rights
|
* management rights
|
||||||
*/
|
*/
|
||||||
@Injectable({
|
export const statisticsAdministratorGuard: CanActivateFn =
|
||||||
providedIn: 'root',
|
singleFeatureAuthorizationGuard(() => observableOf(FeatureID.CanViewUsageStatistics));
|
||||||
})
|
|
||||||
export class StatisticsAdministratorGuard extends SingleFeatureAuthorizationGuard {
|
|
||||||
constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) {
|
|
||||||
super(authorizationService, router, authService);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check group management rights
|
|
||||||
*/
|
|
||||||
getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
|
||||||
return observableOf(FeatureID.CanViewUsageStatistics);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -61,6 +61,8 @@ export interface VirtualMetadataSource {
|
|||||||
|
|
||||||
export interface RelationshipIdentifiable extends Identifiable {
|
export interface RelationshipIdentifiable extends Identifiable {
|
||||||
nameVariant?: string;
|
nameVariant?: string;
|
||||||
|
originalItem: Item;
|
||||||
|
originalIsLeft: boolean
|
||||||
relatedItem: Item;
|
relatedItem: Item;
|
||||||
relationship: Relationship;
|
relationship: Relationship;
|
||||||
type: RelationshipType;
|
type: RelationshipType;
|
||||||
|
@@ -15,6 +15,7 @@ import {
|
|||||||
filter,
|
filter,
|
||||||
map,
|
map,
|
||||||
switchMap,
|
switchMap,
|
||||||
|
take,
|
||||||
} from 'rxjs/operators';
|
} from 'rxjs/operators';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -212,8 +213,14 @@ export class ObjectUpdatesService {
|
|||||||
* @param url The page's URL for which the changes are saved
|
* @param url The page's URL for which the changes are saved
|
||||||
* @param field An updated field for the page's object
|
* @param field An updated field for the page's object
|
||||||
*/
|
*/
|
||||||
saveAddFieldUpdate(url: string, field: Identifiable) {
|
saveAddFieldUpdate(url: string, field: Identifiable): Observable<boolean> {
|
||||||
|
const update$: Observable<boolean> = this.getFieldUpdatesExclusive(url, [field]).pipe(
|
||||||
|
filter((fieldUpdates: FieldUpdates) => fieldUpdates[field.uuid].changeType === FieldChangeType.ADD),
|
||||||
|
take(1),
|
||||||
|
map(() => true),
|
||||||
|
);
|
||||||
this.saveFieldUpdate(url, field, FieldChangeType.ADD);
|
this.saveFieldUpdate(url, field, FieldChangeType.ADD);
|
||||||
|
return update$;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -221,8 +228,14 @@ export class ObjectUpdatesService {
|
|||||||
* @param url The page's URL for which the changes are saved
|
* @param url The page's URL for which the changes are saved
|
||||||
* @param field An updated field for the page's object
|
* @param field An updated field for the page's object
|
||||||
*/
|
*/
|
||||||
saveRemoveFieldUpdate(url: string, field: Identifiable) {
|
saveRemoveFieldUpdate(url: string, field: Identifiable): Observable<boolean> {
|
||||||
|
const update$: Observable<boolean> = this.getFieldUpdatesExclusive(url, [field]).pipe(
|
||||||
|
filter((fieldUpdates: FieldUpdates) => fieldUpdates[field.uuid].changeType === FieldChangeType.REMOVE),
|
||||||
|
take(1),
|
||||||
|
map(() => true),
|
||||||
|
);
|
||||||
this.saveFieldUpdate(url, field, FieldChangeType.REMOVE);
|
this.saveFieldUpdate(url, field, FieldChangeType.REMOVE);
|
||||||
|
return update$;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -128,6 +128,7 @@ describe('RelationshipDataService', () => {
|
|||||||
const itemService = jasmine.createSpyObj('itemService', {
|
const itemService = jasmine.createSpyObj('itemService', {
|
||||||
findById: (uuid) => createSuccessfulRemoteDataObject(relatedItems.find((relatedItem) => relatedItem.id === uuid)),
|
findById: (uuid) => createSuccessfulRemoteDataObject(relatedItems.find((relatedItem) => relatedItem.id === uuid)),
|
||||||
findByHref: createSuccessfulRemoteDataObject$(relatedItems[0]),
|
findByHref: createSuccessfulRemoteDataObject$(relatedItems[0]),
|
||||||
|
getIDHrefObs: (uuid: string) => observableOf(`https://demo.dspace.org/server/api/core/items/${uuid}`),
|
||||||
});
|
});
|
||||||
|
|
||||||
const getRequestEntry$ = (successful: boolean) => {
|
const getRequestEntry$ = (successful: boolean) => {
|
||||||
@@ -244,6 +245,16 @@ describe('RelationshipDataService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('searchByItemsAndType', () => {
|
||||||
|
it('should call addDependency for each item to invalidate the request when one of the items is update', () => {
|
||||||
|
spyOn(service as any, 'addDependency');
|
||||||
|
|
||||||
|
service.searchByItemsAndType(relationshipType.id, item.id, relationshipType.leftwardType, ['item-id-1', 'item-id-2']);
|
||||||
|
|
||||||
|
expect((service as any).addDependency).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('resolveMetadataRepresentation', () => {
|
describe('resolveMetadataRepresentation', () => {
|
||||||
const parentItem: Item = Object.assign(new Item(), {
|
const parentItem: Item = Object.assign(new Item(), {
|
||||||
id: 'parent-item',
|
id: 'parent-item',
|
||||||
|
@@ -155,8 +155,11 @@ export class RelationshipDataService extends IdentifiableDataService<Relationshi
|
|||||||
* @param id the ID of the relationship to delete
|
* @param id the ID of the relationship to delete
|
||||||
* @param copyVirtualMetadata whether to copy this relationship's virtual metadata to the related Items
|
* @param copyVirtualMetadata whether to copy this relationship's virtual metadata to the related Items
|
||||||
* accepted values: none, all, left, right, configured
|
* accepted values: none, all, left, right, configured
|
||||||
|
* @param shouldRefresh refresh the cache for the items in the relationship after creating
|
||||||
|
* it. Disable this if you want to add relationships in bulk, and
|
||||||
|
* want to refresh the cachemanually at the end
|
||||||
*/
|
*/
|
||||||
deleteRelationship(id: string, copyVirtualMetadata: string): Observable<RemoteData<NoContent>> {
|
deleteRelationship(id: string, copyVirtualMetadata: string, shouldRefresh = true): Observable<RemoteData<NoContent>> {
|
||||||
return this.getRelationshipEndpoint(id).pipe(
|
return this.getRelationshipEndpoint(id).pipe(
|
||||||
isNotEmptyOperator(),
|
isNotEmptyOperator(),
|
||||||
take(1),
|
take(1),
|
||||||
@@ -167,7 +170,11 @@ export class RelationshipDataService extends IdentifiableDataService<Relationshi
|
|||||||
sendRequest(this.requestService),
|
sendRequest(this.requestService),
|
||||||
switchMap((restRequest: RestRequest) => this.rdbService.buildFromRequestUUID(restRequest.uuid)),
|
switchMap((restRequest: RestRequest) => this.rdbService.buildFromRequestUUID(restRequest.uuid)),
|
||||||
getFirstCompletedRemoteData(),
|
getFirstCompletedRemoteData(),
|
||||||
tap(() => this.refreshRelationshipItemsInCacheByRelationship(id)),
|
tap(() => {
|
||||||
|
if (shouldRefresh) {
|
||||||
|
this.refreshRelationshipItemsInCacheByRelationship(id);
|
||||||
|
}
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,8 +185,11 @@ export class RelationshipDataService extends IdentifiableDataService<Relationshi
|
|||||||
* @param item2 The second item of the relationship
|
* @param item2 The second item of the relationship
|
||||||
* @param leftwardValue The leftward value of the relationship
|
* @param leftwardValue The leftward value of the relationship
|
||||||
* @param rightwardValue The rightward value of the relationship
|
* @param rightwardValue The rightward value of the relationship
|
||||||
|
* @param shouldRefresh refresh the cache for the items in the relationship after creating it.
|
||||||
|
* Disable this if you want to add relationships in bulk, and want to refresh
|
||||||
|
* the cache manually at the end
|
||||||
*/
|
*/
|
||||||
addRelationship(typeId: string, item1: Item, item2: Item, leftwardValue?: string, rightwardValue?: string): Observable<RemoteData<Relationship>> {
|
addRelationship(typeId: string, item1: Item, item2: Item, leftwardValue?: string, rightwardValue?: string, shouldRefresh = true): Observable<RemoteData<Relationship>> {
|
||||||
const options: HttpOptions = Object.create({});
|
const options: HttpOptions = Object.create({});
|
||||||
let headers = new HttpHeaders();
|
let headers = new HttpHeaders();
|
||||||
headers = headers.append('Content-Type', 'text/uri-list');
|
headers = headers.append('Content-Type', 'text/uri-list');
|
||||||
@@ -194,8 +204,12 @@ export class RelationshipDataService extends IdentifiableDataService<Relationshi
|
|||||||
sendRequest(this.requestService),
|
sendRequest(this.requestService),
|
||||||
switchMap((restRequest: RestRequest) => this.rdbService.buildFromRequestUUID(restRequest.uuid)),
|
switchMap((restRequest: RestRequest) => this.rdbService.buildFromRequestUUID(restRequest.uuid)),
|
||||||
getFirstCompletedRemoteData(),
|
getFirstCompletedRemoteData(),
|
||||||
tap(() => this.refreshRelationshipItemsInCache(item1)),
|
tap(() => {
|
||||||
tap(() => this.refreshRelationshipItemsInCache(item2)),
|
if (shouldRefresh) {
|
||||||
|
this.refreshRelationshipItemsInCache(item1);
|
||||||
|
this.refreshRelationshipItemsInCache(item2);
|
||||||
|
}
|
||||||
|
}),
|
||||||
) as Observable<RemoteData<Relationship>>;
|
) as Observable<RemoteData<Relationship>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,7 +237,7 @@ export class RelationshipDataService extends IdentifiableDataService<Relationshi
|
|||||||
* Method to remove an item that's part of a relationship from the cache
|
* Method to remove an item that's part of a relationship from the cache
|
||||||
* @param item The item to remove from the cache
|
* @param item The item to remove from the cache
|
||||||
*/
|
*/
|
||||||
public refreshRelationshipItemsInCache(item) {
|
public refreshRelationshipItemsInCache(item: Item): void {
|
||||||
this.objectCache.remove(item._links.self.href);
|
this.objectCache.remove(item._links.self.href);
|
||||||
this.requestService.removeByHrefSubstring(item.uuid);
|
this.requestService.removeByHrefSubstring(item.uuid);
|
||||||
observableCombineLatest([
|
observableCombineLatest([
|
||||||
@@ -336,7 +350,19 @@ export class RelationshipDataService extends IdentifiableDataService<Relationshi
|
|||||||
} else {
|
} else {
|
||||||
findListOptions.searchParams = searchParams;
|
findListOptions.searchParams = searchParams;
|
||||||
}
|
}
|
||||||
return this.searchBy('byLabel', findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
|
||||||
|
// always set reRequestOnStale to false here, so it doesn't happen automatically in BaseDataService
|
||||||
|
const result$ = this.searchBy('byLabel', findListOptions, useCachedVersionIfAvailable, false, ...linksToFollow);
|
||||||
|
|
||||||
|
// add this result as a dependency of the item, meaning that if the item is invalided, this
|
||||||
|
// result will be as well
|
||||||
|
this.addDependency(result$, item._links.self.href);
|
||||||
|
|
||||||
|
// do the reRequestOnStale call here, to ensure any re-requests also get added as dependencies
|
||||||
|
return result$.pipe(
|
||||||
|
this.reRequestStaleRemoteData(reRequestOnStale, () =>
|
||||||
|
this.getItemRelationshipsByLabel(item, label, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -548,13 +574,18 @@ export class RelationshipDataService extends IdentifiableDataService<Relationshi
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.searchBy(
|
const searchRD$: Observable<RemoteData<PaginatedList<Relationship>>> = this.searchBy(
|
||||||
'byItemsAndType',
|
'byItemsAndType',
|
||||||
{
|
{
|
||||||
searchParams: searchParams,
|
searchParams: searchParams,
|
||||||
},
|
},
|
||||||
) as Observable<RemoteData<PaginatedList<Relationship>>>;
|
) as Observable<RemoteData<PaginatedList<Relationship>>>;
|
||||||
|
|
||||||
|
arrayOfItemIds.forEach((itemId: string) => {
|
||||||
|
this.addDependency(searchRD$, this.itemService.getIDHrefObs(encodeURIComponent(itemId)));
|
||||||
|
});
|
||||||
|
|
||||||
|
return searchRD$;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -69,6 +69,10 @@ describe('VersionHistoryDataService', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const version1WithDraft = Object.assign(new Version(), {
|
||||||
|
...version1,
|
||||||
|
versionhistory: createSuccessfulRemoteDataObject$(versionHistoryDraft),
|
||||||
|
});
|
||||||
const versions = [version1, version2];
|
const versions = [version1, version2];
|
||||||
versionHistory.versions = createSuccessfulRemoteDataObject$(createPaginatedList(versions));
|
versionHistory.versions = createSuccessfulRemoteDataObject$(createPaginatedList(versions));
|
||||||
const item1 = Object.assign(new Item(), {
|
const item1 = Object.assign(new Item(), {
|
||||||
@@ -190,21 +194,18 @@ describe('VersionHistoryDataService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('hasDraftVersion$', () => {
|
describe('hasDraftVersion$', () => {
|
||||||
beforeEach(waitForAsync(() => {
|
|
||||||
versionService.findByHref.and.returnValue(createSuccessfulRemoteDataObject$<Version>(version1));
|
|
||||||
}));
|
|
||||||
it('should return false if draftVersion is false', fakeAsync(() => {
|
it('should return false if draftVersion is false', fakeAsync(() => {
|
||||||
versionService.getHistoryFromVersion.and.returnValue(of(versionHistory));
|
versionService.findByHref.and.returnValue(createSuccessfulRemoteDataObject$<Version>(version1));
|
||||||
service.hasDraftVersion$('href').subscribe((res) => {
|
service.hasDraftVersion$('href').subscribe((res) => {
|
||||||
expect(res).toBeFalse();
|
expect(res).toBeFalse();
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should return true if draftVersion is true', fakeAsync(() => {
|
it('should return true if draftVersion is true', fakeAsync(() => {
|
||||||
versionService.getHistoryFromVersion.and.returnValue(of(versionHistoryDraft));
|
versionService.findByHref.and.returnValue(createSuccessfulRemoteDataObject$<Version>(version1WithDraft));
|
||||||
service.hasDraftVersion$('href').subscribe((res) => {
|
service.hasDraftVersion$('href').subscribe((res) => {
|
||||||
expect(res).toBeTrue();
|
expect(res).toBeTrue();
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -1,17 +1,22 @@
|
|||||||
import { HttpHeaders } from '@angular/common/http';
|
import { HttpHeaders } from '@angular/common/http';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
|
combineLatest,
|
||||||
Observable,
|
Observable,
|
||||||
of,
|
of as observableOf,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import {
|
import {
|
||||||
filter,
|
filter,
|
||||||
|
find,
|
||||||
map,
|
map,
|
||||||
switchMap,
|
switchMap,
|
||||||
take,
|
take,
|
||||||
} from 'rxjs/operators';
|
} from 'rxjs/operators';
|
||||||
|
|
||||||
import { hasValueOperator } from '../../shared/empty.util';
|
import {
|
||||||
|
hasValue,
|
||||||
|
hasValueOperator,
|
||||||
|
} from '../../shared/empty.util';
|
||||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||||
import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model';
|
import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model';
|
||||||
import {
|
import {
|
||||||
@@ -29,7 +34,6 @@ import {
|
|||||||
getFirstSucceededRemoteDataPayload,
|
getFirstSucceededRemoteDataPayload,
|
||||||
getRemoteDataPayload,
|
getRemoteDataPayload,
|
||||||
} from '../shared/operators';
|
} from '../shared/operators';
|
||||||
import { sendRequest } from '../shared/request.operators';
|
|
||||||
import { Version } from '../shared/version.model';
|
import { Version } from '../shared/version.model';
|
||||||
import { VersionHistory } from '../shared/version-history.model';
|
import { VersionHistory } from '../shared/version-history.model';
|
||||||
import { IdentifiableDataService } from './base/identifiable-data.service';
|
import { IdentifiableDataService } from './base/identifiable-data.service';
|
||||||
@@ -38,7 +42,6 @@ import { PaginatedList } from './paginated-list.model';
|
|||||||
import { RemoteData } from './remote-data';
|
import { RemoteData } from './remote-data';
|
||||||
import { PostRequest } from './request.models';
|
import { PostRequest } from './request.models';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
import { RestRequest } from './rest-request.model';
|
|
||||||
import { VersionDataService } from './version-data.service';
|
import { VersionDataService } from './version-data.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -100,19 +103,31 @@ export class VersionHistoryDataService extends IdentifiableDataService<VersionHi
|
|||||||
* @param summary the summary of the new version
|
* @param summary the summary of the new version
|
||||||
*/
|
*/
|
||||||
createVersion(itemHref: string, summary: string): Observable<RemoteData<Version>> {
|
createVersion(itemHref: string, summary: string): Observable<RemoteData<Version>> {
|
||||||
|
const requestId = this.requestService.generateRequestId();
|
||||||
const requestOptions: HttpOptions = Object.create({});
|
const requestOptions: HttpOptions = Object.create({});
|
||||||
let requestHeaders = new HttpHeaders();
|
let requestHeaders = new HttpHeaders();
|
||||||
requestHeaders = requestHeaders.append('Content-Type', 'text/uri-list');
|
requestHeaders = requestHeaders.append('Content-Type', 'text/uri-list');
|
||||||
requestOptions.headers = requestHeaders;
|
requestOptions.headers = requestHeaders;
|
||||||
|
|
||||||
return this.halService.getEndpoint(this.versionsEndpoint).pipe(
|
this.halService.getEndpoint(this.versionsEndpoint).pipe(
|
||||||
take(1),
|
take(1),
|
||||||
map((endpointUrl: string) => (summary?.length > 0) ? `${endpointUrl}?summary=${summary}` : `${endpointUrl}`),
|
map((endpointUrl: string) => (summary?.length > 0) ? `${endpointUrl}?summary=${summary}` : `${endpointUrl}`),
|
||||||
map((endpointURL: string) => new PostRequest(this.requestService.generateRequestId(), endpointURL, itemHref, requestOptions)),
|
find((href: string) => hasValue(href)),
|
||||||
sendRequest(this.requestService),
|
).subscribe((href) => {
|
||||||
switchMap((restRequest: RestRequest) => this.rdbService.buildFromRequestUUID(restRequest.uuid)),
|
const request = new PostRequest(requestId, href, itemHref, requestOptions);
|
||||||
|
if (hasValue(this.responseMsToLive)) {
|
||||||
|
request.responseMsToLive = this.responseMsToLive;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.requestService.send(request);
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.rdbService.buildFromRequestUUIDAndAwait<Version>(requestId, (versionRD) => combineLatest([
|
||||||
|
this.requestService.setStaleByHrefSubstring(versionRD.payload._links.self.href),
|
||||||
|
this.requestService.setStaleByHrefSubstring(versionRD.payload._links.versionhistory.href),
|
||||||
|
])).pipe(
|
||||||
getFirstCompletedRemoteData(),
|
getFirstCompletedRemoteData(),
|
||||||
) as Observable<RemoteData<Version>>;
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -151,7 +166,7 @@ export class VersionHistoryDataService extends IdentifiableDataService<VersionHi
|
|||||||
switchMap((res) => res.versionhistory),
|
switchMap((res) => res.versionhistory),
|
||||||
getFirstSucceededRemoteDataPayload(),
|
getFirstSucceededRemoteDataPayload(),
|
||||||
switchMap((versionHistoryRD) => this.getLatestVersionFromHistory$(versionHistoryRD)),
|
switchMap((versionHistoryRD) => this.getLatestVersionFromHistory$(versionHistoryRD)),
|
||||||
) : of(null);
|
) : observableOf(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -162,8 +177,8 @@ export class VersionHistoryDataService extends IdentifiableDataService<VersionHi
|
|||||||
isLatest$(version: Version): Observable<boolean> {
|
isLatest$(version: Version): Observable<boolean> {
|
||||||
return version ? this.getLatestVersion$(version).pipe(
|
return version ? this.getLatestVersion$(version).pipe(
|
||||||
take(1),
|
take(1),
|
||||||
switchMap((latestVersion) => of(version.version === latestVersion.version)),
|
switchMap((latestVersion) => observableOf(version.version === latestVersion.version)),
|
||||||
) : of(null);
|
) : observableOf(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -172,17 +187,22 @@ export class VersionHistoryDataService extends IdentifiableDataService<VersionHi
|
|||||||
* @returns `true` if a workspace item exists, `false` otherwise, or `null` if a version history does not exist
|
* @returns `true` if a workspace item exists, `false` otherwise, or `null` if a version history does not exist
|
||||||
*/
|
*/
|
||||||
hasDraftVersion$(versionHref: string): Observable<boolean> {
|
hasDraftVersion$(versionHref: string): Observable<boolean> {
|
||||||
return this.versionDataService.findByHref(versionHref, true, true, followLink('versionhistory')).pipe(
|
return this.versionDataService.findByHref(versionHref, false, true, followLink('versionhistory')).pipe(
|
||||||
getFirstCompletedRemoteData(),
|
getFirstCompletedRemoteData(),
|
||||||
switchMap((res) => {
|
switchMap((versionRD: RemoteData<Version>) => {
|
||||||
if (res.hasSucceeded && !res.hasNoContent) {
|
if (versionRD.hasSucceeded && !versionRD.hasNoContent) {
|
||||||
return of(res).pipe(
|
return versionRD.payload.versionhistory.pipe(
|
||||||
getFirstSucceededRemoteDataPayload(),
|
getFirstCompletedRemoteData(),
|
||||||
switchMap((version) => this.versionDataService.getHistoryFromVersion(version)),
|
map((versionHistoryRD: RemoteData<VersionHistory>) => {
|
||||||
map((versionHistory) => versionHistory ? versionHistory.draftVersion : false),
|
if (versionHistoryRD.hasSucceeded && !versionHistoryRD.hasNoContent) {
|
||||||
|
return versionHistoryRD.payload.draftVersion;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return of(false);
|
return observableOf(false);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
@@ -1,45 +0,0 @@
|
|||||||
import {
|
|
||||||
ActivatedRouteSnapshot,
|
|
||||||
Router,
|
|
||||||
RouterStateSnapshot,
|
|
||||||
UrlTree,
|
|
||||||
} from '@angular/router';
|
|
||||||
import {
|
|
||||||
Observable,
|
|
||||||
of as observableOf,
|
|
||||||
} from 'rxjs';
|
|
||||||
|
|
||||||
import { environment } from '../../../environments/environment';
|
|
||||||
import { returnEndUserAgreementUrlTreeOnFalse } from '../shared/authorized.operators';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An abstract guard for redirecting users to the user agreement page if a certain condition is met
|
|
||||||
* That condition is defined by abstract method hasAccepted
|
|
||||||
*/
|
|
||||||
export abstract class AbstractEndUserAgreementGuard {
|
|
||||||
|
|
||||||
constructor(protected router: Router) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* True when the user agreement has been accepted
|
|
||||||
* The user will be redirected to the End User Agreement page if they haven't accepted it before
|
|
||||||
* A redirect URL will be provided with the navigation so the component can redirect the user back to the blocked route
|
|
||||||
* when they're finished accepting the agreement
|
|
||||||
*/
|
|
||||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
|
|
||||||
if (!environment.info.enableEndUserAgreement) {
|
|
||||||
return observableOf(true);
|
|
||||||
}
|
|
||||||
return this.hasAccepted().pipe(
|
|
||||||
returnEndUserAgreementUrlTreeOnFalse(this.router, state.url),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This abstract method determines how the User Agreement has to be accepted before the user is allowed to visit
|
|
||||||
* the desired route
|
|
||||||
*/
|
|
||||||
abstract hasAccepted(): Observable<boolean>;
|
|
||||||
|
|
||||||
}
|
|
@@ -1,13 +1,14 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
import {
|
import {
|
||||||
Router,
|
Router,
|
||||||
UrlTree,
|
UrlTree,
|
||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
import { EndUserAgreementService } from './end-user-agreement.service';
|
import { EndUserAgreementService } from './end-user-agreement.service';
|
||||||
import { EndUserAgreementCookieGuard } from './end-user-agreement-cookie.guard';
|
import { endUserAgreementCookieGuard } from './end-user-agreement-cookie.guard';
|
||||||
|
|
||||||
describe('EndUserAgreementCookieGuard', () => {
|
describe('endUserAgreementCookieGuard', () => {
|
||||||
let guard: EndUserAgreementCookieGuard;
|
|
||||||
|
|
||||||
let endUserAgreementService: EndUserAgreementService;
|
let endUserAgreementService: EndUserAgreementService;
|
||||||
let router: Router;
|
let router: Router;
|
||||||
@@ -21,14 +22,22 @@ describe('EndUserAgreementCookieGuard', () => {
|
|||||||
parseUrl: new UrlTree(),
|
parseUrl: new UrlTree(),
|
||||||
createUrlTree: new UrlTree(),
|
createUrlTree: new UrlTree(),
|
||||||
});
|
});
|
||||||
|
TestBed.configureTestingModule({
|
||||||
guard = new EndUserAgreementCookieGuard(endUserAgreementService, router);
|
providers: [
|
||||||
|
{ provide: Router, useValue: router },
|
||||||
|
{ provide: EndUserAgreementService, useValue: endUserAgreementService },
|
||||||
|
],
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('canActivate', () => {
|
describe('canActivate', () => {
|
||||||
describe('when the cookie has been accepted', () => {
|
describe('when the cookie has been accepted', () => {
|
||||||
it('should return true', (done) => {
|
it('should return true', (done) => {
|
||||||
guard.canActivate(undefined, { url: Object.assign({ url: 'redirect' }) } as any).subscribe((result) => {
|
const result$ = TestBed.runInInjectionContext(() => {
|
||||||
|
return endUserAgreementCookieGuard(undefined, { url: Object.assign({ url: 'redirect' }) } as any);
|
||||||
|
}) as Observable<boolean | UrlTree>;
|
||||||
|
|
||||||
|
result$.subscribe((result) => {
|
||||||
expect(result).toEqual(true);
|
expect(result).toEqual(true);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
@@ -41,7 +50,11 @@ describe('EndUserAgreementCookieGuard', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return a UrlTree', (done) => {
|
it('should return a UrlTree', (done) => {
|
||||||
guard.canActivate(undefined, Object.assign({ url: 'redirect' })).subscribe((result) => {
|
const result$ = TestBed.runInInjectionContext(() => {
|
||||||
|
return endUserAgreementCookieGuard(undefined, { url: Object.assign({ url: 'redirect' }) } as any);
|
||||||
|
}) as Observable<boolean | UrlTree>;
|
||||||
|
|
||||||
|
result$.subscribe((result) => {
|
||||||
expect(result).toEqual(jasmine.any(UrlTree));
|
expect(result).toEqual(jasmine.any(UrlTree));
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
@@ -1,29 +1,19 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { inject } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { CanActivateFn } from '@angular/router';
|
||||||
import {
|
import { of as observableOf } from 'rxjs';
|
||||||
Observable,
|
|
||||||
of as observableOf,
|
|
||||||
} from 'rxjs';
|
|
||||||
|
|
||||||
import { AbstractEndUserAgreementGuard } from './abstract-end-user-agreement.guard';
|
import { endUserAgreementGuard } from './end-user-agreement.guard';
|
||||||
import { EndUserAgreementService } from './end-user-agreement.service';
|
import { EndUserAgreementService } from './end-user-agreement.service';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A guard redirecting users to the end agreement page when the user agreement cookie hasn't been accepted
|
* Guard for preventing unauthorized access to certain pages
|
||||||
|
* requiring the end user agreement to have been accepted in a cookie
|
||||||
*/
|
*/
|
||||||
@Injectable({ providedIn: 'root' })
|
export const endUserAgreementCookieGuard: CanActivateFn =
|
||||||
export class EndUserAgreementCookieGuard extends AbstractEndUserAgreementGuard {
|
endUserAgreementGuard(
|
||||||
|
() => {
|
||||||
constructor(protected endUserAgreementService: EndUserAgreementService,
|
const endUserAgreementService = inject(EndUserAgreementService);
|
||||||
protected router: Router) {
|
return observableOf(endUserAgreementService.isCookieAccepted());
|
||||||
super(router);
|
},
|
||||||
}
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* True when the user agreement cookie has been accepted
|
|
||||||
*/
|
|
||||||
hasAccepted(): Observable<boolean> {
|
|
||||||
return observableOf(this.endUserAgreementService.isCookieAccepted());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
@@ -1,16 +1,18 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
import {
|
import {
|
||||||
Router,
|
Router,
|
||||||
UrlTree,
|
UrlTree,
|
||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
import { of as observableOf } from 'rxjs';
|
import {
|
||||||
|
Observable,
|
||||||
|
of as observableOf,
|
||||||
|
} from 'rxjs';
|
||||||
|
|
||||||
import { environment } from '../../../environments/environment.test';
|
import { environment } from '../../../environments/environment.test';
|
||||||
import { EndUserAgreementService } from './end-user-agreement.service';
|
import { EndUserAgreementService } from './end-user-agreement.service';
|
||||||
import { EndUserAgreementCurrentUserGuard } from './end-user-agreement-current-user.guard';
|
import { endUserAgreementCurrentUserGuard } from './end-user-agreement-current-user.guard';
|
||||||
|
|
||||||
describe('EndUserAgreementGuard', () => {
|
|
||||||
let guard: EndUserAgreementCurrentUserGuard;
|
|
||||||
|
|
||||||
|
describe('endUserAgreementGuard', () => {
|
||||||
let endUserAgreementService: EndUserAgreementService;
|
let endUserAgreementService: EndUserAgreementService;
|
||||||
let router: Router;
|
let router: Router;
|
||||||
|
|
||||||
@@ -18,19 +20,30 @@ describe('EndUserAgreementGuard', () => {
|
|||||||
endUserAgreementService = jasmine.createSpyObj('endUserAgreementService', {
|
endUserAgreementService = jasmine.createSpyObj('endUserAgreementService', {
|
||||||
hasCurrentUserAcceptedAgreement: observableOf(true),
|
hasCurrentUserAcceptedAgreement: observableOf(true),
|
||||||
});
|
});
|
||||||
|
|
||||||
router = jasmine.createSpyObj('router', {
|
router = jasmine.createSpyObj('router', {
|
||||||
navigateByUrl: {},
|
navigateByUrl: {},
|
||||||
parseUrl: new UrlTree(),
|
parseUrl: new UrlTree(),
|
||||||
createUrlTree: new UrlTree(),
|
createUrlTree: new UrlTree(),
|
||||||
});
|
});
|
||||||
|
|
||||||
guard = new EndUserAgreementCurrentUserGuard(endUserAgreementService, router);
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
{ provide: Router, useValue: router },
|
||||||
|
{ provide: EndUserAgreementService, useValue: endUserAgreementService },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('canActivate', () => {
|
describe('canActivate', () => {
|
||||||
describe('when the user has accepted the agreement', () => {
|
describe('when the user has accepted the agreement', () => {
|
||||||
it('should return true', (done) => {
|
it('should return true', (done) => {
|
||||||
guard.canActivate(undefined, Object.assign({ url: 'redirect' })).subscribe((result) => {
|
const result$ = TestBed.runInInjectionContext(() => {
|
||||||
|
return endUserAgreementCurrentUserGuard(undefined, Object.assign({ url: 'redirect' }));
|
||||||
|
}) as Observable<boolean | UrlTree>;
|
||||||
|
|
||||||
|
result$.subscribe((result) => {
|
||||||
expect(result).toEqual(true);
|
expect(result).toEqual(true);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
@@ -43,7 +56,11 @@ describe('EndUserAgreementGuard', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return a UrlTree', (done) => {
|
it('should return a UrlTree', (done) => {
|
||||||
guard.canActivate(undefined, Object.assign({ url: 'redirect' })).subscribe((result) => {
|
const result$ = TestBed.runInInjectionContext(() => {
|
||||||
|
return endUserAgreementCurrentUserGuard(undefined, Object.assign({ url: 'redirect' }));
|
||||||
|
}) as Observable<boolean | UrlTree>;
|
||||||
|
|
||||||
|
result$.subscribe((result) => {
|
||||||
expect(result).toEqual(jasmine.any(UrlTree));
|
expect(result).toEqual(jasmine.any(UrlTree));
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
@@ -53,7 +70,12 @@ describe('EndUserAgreementGuard', () => {
|
|||||||
describe('when the end user agreement is disabled', () => {
|
describe('when the end user agreement is disabled', () => {
|
||||||
it('should return true', (done) => {
|
it('should return true', (done) => {
|
||||||
environment.info.enableEndUserAgreement = false;
|
environment.info.enableEndUserAgreement = false;
|
||||||
guard.canActivate(undefined, Object.assign({ url: 'redirect' })).subscribe((result) => {
|
|
||||||
|
const result$ = TestBed.runInInjectionContext(() => {
|
||||||
|
return endUserAgreementCurrentUserGuard(undefined, Object.assign({ url: 'redirect' }));
|
||||||
|
}) as Observable<boolean | UrlTree>;
|
||||||
|
|
||||||
|
result$.subscribe((result) => {
|
||||||
expect(result).toEqual(true);
|
expect(result).toEqual(true);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
@@ -61,7 +83,11 @@ describe('EndUserAgreementGuard', () => {
|
|||||||
|
|
||||||
it('should not resolve to the end user agreement page', (done) => {
|
it('should not resolve to the end user agreement page', (done) => {
|
||||||
environment.info.enableEndUserAgreement = false;
|
environment.info.enableEndUserAgreement = false;
|
||||||
guard.canActivate(undefined, Object.assign({ url: 'redirect' })).subscribe((result) => {
|
const result$ = TestBed.runInInjectionContext(() => {
|
||||||
|
return endUserAgreementCurrentUserGuard(undefined, Object.assign({ url: 'redirect' }));
|
||||||
|
}) as Observable<boolean | UrlTree>;
|
||||||
|
|
||||||
|
result$.subscribe((result) => {
|
||||||
expect(router.navigateByUrl).not.toHaveBeenCalled();
|
expect(router.navigateByUrl).not.toHaveBeenCalled();
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
@@ -1,34 +1,25 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { inject } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { CanActivateFn } from '@angular/router';
|
||||||
import {
|
import { of as observableOf } from 'rxjs';
|
||||||
Observable,
|
|
||||||
of as observableOf,
|
|
||||||
} from 'rxjs';
|
|
||||||
|
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
import { AbstractEndUserAgreementGuard } from './abstract-end-user-agreement.guard';
|
import { endUserAgreementGuard } from './end-user-agreement.guard';
|
||||||
import { EndUserAgreementService } from './end-user-agreement.service';
|
import { EndUserAgreementService } from './end-user-agreement.service';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A guard redirecting logged in users to the end agreement page when they haven't accepted the latest user agreement
|
* Guard for preventing unauthorized access to certain pages
|
||||||
|
* requiring the end user agreement to have been accepted by the current user
|
||||||
|
|
||||||
*/
|
*/
|
||||||
@Injectable({ providedIn: 'root' })
|
export const endUserAgreementCurrentUserGuard: CanActivateFn =
|
||||||
export class EndUserAgreementCurrentUserGuard extends AbstractEndUserAgreementGuard {
|
endUserAgreementGuard(
|
||||||
|
() => {
|
||||||
|
const endUserAgreementService = inject(EndUserAgreementService);
|
||||||
|
if (!environment.info.enableEndUserAgreement) {
|
||||||
|
return observableOf(true);
|
||||||
|
}
|
||||||
|
|
||||||
constructor(protected endUserAgreementService: EndUserAgreementService,
|
return endUserAgreementService.hasCurrentUserAcceptedAgreement(true);
|
||||||
protected router: Router) {
|
},
|
||||||
super(router);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* True when the currently logged in user has accepted the agreements or when the user is not currently authenticated
|
|
||||||
*/
|
|
||||||
hasAccepted(): Observable<boolean> {
|
|
||||||
if (!environment.info.enableEndUserAgreement) {
|
|
||||||
return observableOf(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.endUserAgreementService.hasCurrentUserAcceptedAgreement(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
34
src/app/core/end-user-agreement/end-user-agreement.guard.ts
Normal file
34
src/app/core/end-user-agreement/end-user-agreement.guard.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { inject } from '@angular/core';
|
||||||
|
import {
|
||||||
|
ActivatedRouteSnapshot,
|
||||||
|
CanActivateFn,
|
||||||
|
Router,
|
||||||
|
RouterStateSnapshot,
|
||||||
|
UrlTree,
|
||||||
|
} from '@angular/router';
|
||||||
|
import {
|
||||||
|
Observable,
|
||||||
|
of as observableOf,
|
||||||
|
} from 'rxjs';
|
||||||
|
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
import { returnEndUserAgreementUrlTreeOnFalse } from '../shared/authorized.operators';
|
||||||
|
|
||||||
|
export declare type HasAcceptedGuardParamFn = () => Observable<boolean>;
|
||||||
|
/**
|
||||||
|
* Guard for preventing activating when the user has not accepted the EndUserAgreement
|
||||||
|
* @param hasAccepted Function determining if the EndUserAgreement has been accepted
|
||||||
|
*/
|
||||||
|
export const endUserAgreementGuard = (
|
||||||
|
hasAccepted: HasAcceptedGuardParamFn,
|
||||||
|
): CanActivateFn => {
|
||||||
|
return (route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> => {
|
||||||
|
const router = inject(Router);
|
||||||
|
if (!environment.info.enableEndUserAgreement) {
|
||||||
|
return observableOf(true);
|
||||||
|
}
|
||||||
|
return hasAccepted().pipe(
|
||||||
|
returnEndUserAgreementUrlTreeOnFalse(router, state.url),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
@@ -107,13 +107,17 @@ export class EPersonDataService extends IdentifiableDataService<EPerson> impleme
|
|||||||
* @param scope Scope of the EPeople search, default byMetadata
|
* @param scope Scope of the EPeople search, default byMetadata
|
||||||
* @param query Query of search
|
* @param query Query of search
|
||||||
* @param options Options of search request
|
* @param options Options of search request
|
||||||
|
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
|
||||||
|
* no valid cached version. Defaults to true
|
||||||
|
* @param reRequestOnStale Whether or not the request should automatically be re-
|
||||||
|
* requested after the response becomes stale
|
||||||
*/
|
*/
|
||||||
public searchByScope(scope: string, query: string, options: FindListOptions = {}, useCachedVersionIfAvailable?: boolean): Observable<RemoteData<PaginatedList<EPerson>>> {
|
public searchByScope(scope: string, query: string, options: FindListOptions = {}, useCachedVersionIfAvailable?: boolean, reRequestOnStale = true): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||||
switch (scope) {
|
switch (scope) {
|
||||||
case 'metadata':
|
case 'metadata':
|
||||||
return this.getEpeopleByMetadata(query.trim(), options, useCachedVersionIfAvailable);
|
return this.getEpeopleByMetadata(query.trim(), options, useCachedVersionIfAvailable, reRequestOnStale);
|
||||||
case 'email':
|
case 'email':
|
||||||
return this.getEPersonByEmail(query.trim()).pipe(
|
return this.getEPersonByEmail(query.trim(), useCachedVersionIfAvailable, reRequestOnStale).pipe(
|
||||||
map((rd: RemoteData<EPerson | NoContent>) => {
|
map((rd: RemoteData<EPerson | NoContent>) => {
|
||||||
if (rd.hasSucceeded) {
|
if (rd.hasSucceeded) {
|
||||||
// Turn the single EPerson or NoContent in to a PaginatedList<EPerson>
|
// Turn the single EPerson or NoContent in to a PaginatedList<EPerson>
|
||||||
@@ -145,7 +149,7 @@ export class EPersonDataService extends IdentifiableDataService<EPerson> impleme
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
return this.getEpeopleByMetadata(query.trim(), options, useCachedVersionIfAvailable);
|
return this.getEpeopleByMetadata(query.trim(), options, useCachedVersionIfAvailable, reRequestOnStale);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,37 +1,11 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { CanActivateFn } from '@angular/router';
|
||||||
import {
|
import { of as observableOf } from 'rxjs';
|
||||||
ActivatedRouteSnapshot,
|
|
||||||
Router,
|
|
||||||
RouterStateSnapshot,
|
|
||||||
} from '@angular/router';
|
|
||||||
import {
|
|
||||||
Observable,
|
|
||||||
of,
|
|
||||||
} from 'rxjs';
|
|
||||||
|
|
||||||
import { AuthService } from '../auth/auth.service';
|
import { singleFeatureAuthorizationGuard } from '../data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard';
|
||||||
import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service';
|
|
||||||
import { SingleFeatureAuthorizationGuard } from '../data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard';
|
|
||||||
import { FeatureID } from '../data/feature-authorization/feature-id';
|
import { FeatureID } from '../data/feature-authorization/feature-id';
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
/**
|
/**
|
||||||
* Guard that checks if the forgot-password feature is enabled
|
* Guard that checks if the forgot-password feature is enabled
|
||||||
*/
|
*/
|
||||||
export class ForgotPasswordCheckGuard extends SingleFeatureAuthorizationGuard {
|
export const forgotPasswordCheckGuard: CanActivateFn =
|
||||||
|
singleFeatureAuthorizationGuard(() => observableOf(FeatureID.EPersonForgotPassword));
|
||||||
constructor(
|
|
||||||
protected readonly authorizationService: AuthorizationDataService,
|
|
||||||
protected readonly router: Router,
|
|
||||||
protected readonly authService: AuthService,
|
|
||||||
) {
|
|
||||||
super(authorizationService, router, authService);
|
|
||||||
}
|
|
||||||
|
|
||||||
getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
|
||||||
return of(FeatureID.EPersonForgotPassword);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
@@ -29,14 +29,12 @@ import { HALEndpointService } from '../shared/hal-endpoint.service';
|
|||||||
import { NoContent } from '../shared/NoContent.model';
|
import { NoContent } from '../shared/NoContent.model';
|
||||||
import { getFirstCompletedRemoteData } from '../shared/operators';
|
import { getFirstCompletedRemoteData } from '../shared/operators';
|
||||||
import { WorkflowItem } from './models/workflowitem.model';
|
import { WorkflowItem } from './models/workflowitem.model';
|
||||||
import { WorkspaceItem } from './models/workspaceitem.model';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A service that provides methods to make REST requests with workflow items endpoint.
|
* A service that provides methods to make REST requests with workflow items endpoint.
|
||||||
*/
|
*/
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class WorkflowItemDataService extends IdentifiableDataService<WorkflowItem> implements SearchData<WorkflowItem>, DeleteData<WorkflowItem> {
|
export class WorkflowItemDataService extends IdentifiableDataService<WorkflowItem> implements SearchData<WorkflowItem>, DeleteData<WorkflowItem> {
|
||||||
protected linkPath = 'workflowitems';
|
|
||||||
protected searchByItemLinkPath = 'item';
|
protected searchByItemLinkPath = 'item';
|
||||||
protected responseMsToLive = 10 * 1000;
|
protected responseMsToLive = 10 * 1000;
|
||||||
|
|
||||||
@@ -50,7 +48,7 @@ export class WorkflowItemDataService extends IdentifiableDataService<WorkflowIte
|
|||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
protected notificationsService: NotificationsService,
|
protected notificationsService: NotificationsService,
|
||||||
) {
|
) {
|
||||||
super('workspaceitems', requestService, rdbService, objectCache, halService);
|
super('workflowitems', requestService, rdbService, objectCache, halService);
|
||||||
|
|
||||||
this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
|
this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
|
||||||
this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive, this.constructIdEndpoint);
|
this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive, this.constructIdEndpoint);
|
||||||
@@ -113,7 +111,7 @@ export class WorkflowItemDataService extends IdentifiableDataService<WorkflowIte
|
|||||||
* @param options The {@link FindListOptions} object
|
* @param options The {@link FindListOptions} object
|
||||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||||
*/
|
*/
|
||||||
public findByItem(uuid: string, useCachedVersionIfAvailable = false, reRequestOnStale = true, options: FindListOptions = {}, ...linksToFollow: FollowLinkConfig<WorkspaceItem>[]): Observable<RemoteData<WorkspaceItem>> {
|
public findByItem(uuid: string, useCachedVersionIfAvailable = false, reRequestOnStale = true, options: FindListOptions = {}, ...linksToFollow: FollowLinkConfig<WorkflowItem>[]): Observable<RemoteData<WorkflowItem>> {
|
||||||
const findListOptions = new FindListOptions();
|
const findListOptions = new FindListOptions();
|
||||||
findListOptions.searchParams = [new RequestParam('uuid', uuid)];
|
findListOptions.searchParams = [new RequestParam('uuid', uuid)];
|
||||||
const href$ = this.searchData.getSearchByHref(this.searchByItemLinkPath, findListOptions, ...linksToFollow);
|
const href$ = this.searchData.getSearchByHref(this.searchByItemLinkPath, findListOptions, ...linksToFollow);
|
||||||
@@ -134,7 +132,7 @@ export class WorkflowItemDataService extends IdentifiableDataService<WorkflowIte
|
|||||||
* @return {Observable<RemoteData<PaginatedList<T>>}
|
* @return {Observable<RemoteData<PaginatedList<T>>}
|
||||||
* Return an observable that emits response from the server
|
* Return an observable that emits response from the server
|
||||||
*/
|
*/
|
||||||
public searchBy(searchMethod: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<WorkspaceItem>[]): Observable<RemoteData<PaginatedList<WorkspaceItem>>> {
|
public searchBy(searchMethod: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<WorkflowItem>[]): Observable<RemoteData<PaginatedList<WorkflowItem>>> {
|
||||||
return this.searchData.searchBy(searchMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
return this.searchData.searchBy(searchMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -2,13 +2,18 @@ import {
|
|||||||
HttpClient,
|
HttpClient,
|
||||||
HttpHeaders,
|
HttpHeaders,
|
||||||
} from '@angular/common/http';
|
} from '@angular/common/http';
|
||||||
|
import { waitForAsync } from '@angular/core/testing';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import {
|
import {
|
||||||
cold,
|
cold,
|
||||||
getTestScheduler,
|
getTestScheduler,
|
||||||
hot,
|
hot,
|
||||||
} from 'jasmine-marbles';
|
} from 'jasmine-marbles';
|
||||||
import { of as observableOf } from 'rxjs';
|
import {
|
||||||
|
of as observableOf,
|
||||||
|
of,
|
||||||
|
} from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
import { TestScheduler } from 'rxjs/testing';
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
|
|
||||||
import { getMockHrefOnlyDataService } from '../../shared/mocks/href-only-data.service.mock';
|
import { getMockHrefOnlyDataService } from '../../shared/mocks/href-only-data.service.mock';
|
||||||
@@ -151,12 +156,17 @@ describe('WorkspaceitemDataService test', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('findByItem', () => {
|
describe('findByItem', () => {
|
||||||
it('should proxy the call to UpdateDataServiceImpl.findByHref', () => {
|
it('should proxy the call to UpdateDataServiceImpl.findByHref', waitForAsync(() => {
|
||||||
scheduler.schedule(() => service.findByItem('1234-1234', true, true, pageInfo));
|
scheduler.schedule(() => service.findByItem('1234-1234', true, true, pageInfo));
|
||||||
scheduler.flush();
|
scheduler.flush();
|
||||||
const searchUrl = service.getIDHref('item', [new RequestParam('uuid', '1234-1234')]);
|
const searchUrl$ =
|
||||||
expect((service as any).findByHref).toHaveBeenCalledWith(searchUrl, true, true);
|
of('https://rest.api/rest/api/submission/workspaceitems/search/item')
|
||||||
});
|
.pipe(map(href => service.buildHrefFromFindOptions(href, { searchParams: [new RequestParam('uuid', '1234-1234')] }, [])));
|
||||||
|
searchUrl$.subscribe((url) => {
|
||||||
|
expect(url).toEqual('https://rest.api/rest/api/submission/workspaceitems/search/item?uuid=1234-1234');
|
||||||
|
});
|
||||||
|
expect((service as any).findByHref).toHaveBeenCalled();
|
||||||
|
}));
|
||||||
|
|
||||||
it('should return a RemoteData<WorkspaceItem> for the search', () => {
|
it('should return a RemoteData<WorkspaceItem> for the search', () => {
|
||||||
const result = service.findByItem('1234-1234', true, true, pageInfo);
|
const result = service.findByItem('1234-1234', true, true, pageInfo);
|
||||||
|
@@ -45,7 +45,7 @@ export class WorkspaceitemDataService extends IdentifiableDataService<WorkspaceI
|
|||||||
protected linkPath = 'workspaceitems';
|
protected linkPath = 'workspaceitems';
|
||||||
protected searchByItemLinkPath = 'item';
|
protected searchByItemLinkPath = 'item';
|
||||||
private deleteData: DeleteData<WorkspaceItem>;
|
private deleteData: DeleteData<WorkspaceItem>;
|
||||||
private searchData: SearchData<WorkspaceItem>;
|
private searchData: SearchDataImpl<WorkspaceItem>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected comparator: DSOChangeAnalyzer<WorkspaceItem>,
|
protected comparator: DSOChangeAnalyzer<WorkspaceItem>,
|
||||||
@@ -91,7 +91,7 @@ export class WorkspaceitemDataService extends IdentifiableDataService<WorkspaceI
|
|||||||
public findByItem(uuid: string, useCachedVersionIfAvailable = false, reRequestOnStale = true, options: FindListOptions = {}, ...linksToFollow: FollowLinkConfig<WorkspaceItem>[]): Observable<RemoteData<WorkspaceItem>> {
|
public findByItem(uuid: string, useCachedVersionIfAvailable = false, reRequestOnStale = true, options: FindListOptions = {}, ...linksToFollow: FollowLinkConfig<WorkspaceItem>[]): Observable<RemoteData<WorkspaceItem>> {
|
||||||
const findListOptions = new FindListOptions();
|
const findListOptions = new FindListOptions();
|
||||||
findListOptions.searchParams = [new RequestParam('uuid', uuid)];
|
findListOptions.searchParams = [new RequestParam('uuid', uuid)];
|
||||||
const href$ = this.getIDHref(this.searchByItemLinkPath, findListOptions, ...linksToFollow);
|
const href$ = this.searchData.getSearchByHref(this.searchByItemLinkPath, findListOptions, ...linksToFollow);
|
||||||
return this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
return this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -84,7 +84,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div *ngIf="coarLdnEnabled$ | async" class="notify-enabled text-white">
|
<div *ngIf="coarLdnEnabled$ | async" class="notify-enabled text-white">
|
||||||
<a class="coar-notify-support-route" routerLink="info/coar-notify-support">
|
<a class="coar-notify-support-route" routerLink="info/coar-notify-support">
|
||||||
<img class="n-coar" src="assets/images/n-coar.png" [attr.alt]="'menu.header.image.logo' | translate" />
|
<img class="n-coar" src="assets/images/n-coar.svg" [attr.alt]="'menu.header.image.logo' | translate" />
|
||||||
{{ 'footer.link.coar-notify-support' | translate }}
|
{{ 'footer.link.coar-notify-support' | translate }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,10 +1,7 @@
|
|||||||
import {
|
import { Route } from '@angular/router';
|
||||||
mapToCanActivate,
|
|
||||||
Route,
|
|
||||||
} from '@angular/router';
|
|
||||||
|
|
||||||
import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||||
import { SiteAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
|
import { siteAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
|
||||||
import { HealthPageComponent } from './health-page.component';
|
import { HealthPageComponent } from './health-page.component';
|
||||||
|
|
||||||
export const ROUTES: Route[] = [
|
export const ROUTES: Route[] = [
|
||||||
@@ -15,7 +12,7 @@ export const ROUTES: Route[] = [
|
|||||||
breadcrumbKey: 'health',
|
breadcrumbKey: 'health',
|
||||||
title: 'health-page.title',
|
title: 'health-page.title',
|
||||||
},
|
},
|
||||||
canActivate: mapToCanActivate([SiteAdministratorGuard]),
|
canActivate: [siteAdministratorGuard],
|
||||||
component: HealthPageComponent,
|
component: HealthPageComponent,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@@ -1,18 +1,13 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<head>
|
<h1>{{ 'coar-notify-support.title' | translate }}</h1>
|
||||||
<title>{{ 'coar-notify-support.title' | translate }}</title>
|
<p [innerHTML]="'coar-notify-support-title.content' | translate"></p>
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>{{ 'coar-notify-support.title' | translate }}</h1>
|
|
||||||
<p [innerHTML]="('coar-notify-support-title.content' | translate)"></p>
|
|
||||||
|
|
||||||
<h2>{{ 'coar-notify-support.ldn-inbox.title' | translate }}</h2>
|
<h2>{{ 'coar-notify-support.ldn-inbox.title' | translate }}</h2>
|
||||||
<p [innerHTML]="('coar-notify-support.ldn-inbox.content' | translate).replace('{ldnInboxUrl}', generateCoarRestApiLinksHTML() | async)"></p>
|
<p [innerHTML]="'coar-notify-support.ldn-inbox.content' | translate:{ ldnInboxUrl: coarRestApiUrls$ | async }"></p>
|
||||||
|
|
||||||
<h2>{{ 'coar-notify-support.message-moderation.title' | translate }}</h2>
|
<h2>{{ 'coar-notify-support.message-moderation.title' | translate }}</h2>
|
||||||
<p>
|
<p>
|
||||||
{{ 'coar-notify-support.message-moderation.content' | translate }}
|
{{ 'coar-notify-support.message-moderation.content' | translate }}
|
||||||
<a routerLink="/info/feedback" >{{ 'coar-notify-support.message-moderation.feedback-form' | translate }}</a>
|
<a routerLink="/info/feedback">{{ 'coar-notify-support.message-moderation.feedback-form' | translate }}</a>
|
||||||
</p>
|
</p>
|
||||||
</body>
|
|
||||||
</div>
|
</div>
|
||||||
|
@@ -16,7 +16,9 @@ describe('NotifyInfoComponent', () => {
|
|||||||
let notifyInfoServiceSpy: any;
|
let notifyInfoServiceSpy: any;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
notifyInfoServiceSpy = jasmine.createSpyObj('NotifyInfoService', ['getCoarLdnLocalInboxUrls']);
|
notifyInfoServiceSpy = jasmine.createSpyObj('NotifyInfoService', {
|
||||||
|
getCoarLdnLocalInboxUrls: of([]),
|
||||||
|
});
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot(), NotifyInfoComponent],
|
imports: [TranslateModule.forRoot(), NotifyInfoComponent],
|
||||||
@@ -31,8 +33,7 @@ describe('NotifyInfoComponent', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(NotifyInfoComponent);
|
fixture = TestBed.createComponent(NotifyInfoComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
component.coarRestApiUrl = of([]);
|
component.coarRestApiUrls$ = of('');
|
||||||
spyOn(component, 'generateCoarRestApiLinksHTML').and.returnValue(of(''));
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -8,7 +8,6 @@ import { TranslateModule } from '@ngx-translate/core';
|
|||||||
import {
|
import {
|
||||||
map,
|
map,
|
||||||
Observable,
|
Observable,
|
||||||
of,
|
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
|
|
||||||
import { NotifyInfoService } from '../../core/coar-notify/notify-info/notify-info.service';
|
import { NotifyInfoService } from '../../core/coar-notify/notify-info/notify-info.service';
|
||||||
@@ -31,26 +30,17 @@ export class NotifyInfoComponent implements OnInit {
|
|||||||
/**
|
/**
|
||||||
* Observable containing the COAR REST INBOX API URLs.
|
* Observable containing the COAR REST INBOX API URLs.
|
||||||
*/
|
*/
|
||||||
coarRestApiUrl: Observable<string[]> = of([]);
|
coarRestApiUrls$: Observable<string>;
|
||||||
|
|
||||||
constructor(private notifyInfoService: NotifyInfoService) {}
|
constructor(
|
||||||
|
protected notifyInfoService: NotifyInfoService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.coarRestApiUrl = this.notifyInfoService.getCoarLdnLocalInboxUrls();
|
this.coarRestApiUrls$ = this.notifyInfoService.getCoarLdnLocalInboxUrls().pipe(
|
||||||
}
|
map((urls: string[]) => urls.map((url: string) => `<a href="${url}" target="_blank">${url}</a>`).join(', ')),
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates HTML code for COAR REST API links.
|
|
||||||
* @returns An Observable that emits the generated HTML code.
|
|
||||||
*/
|
|
||||||
generateCoarRestApiLinksHTML() {
|
|
||||||
return this.coarRestApiUrl.pipe(
|
|
||||||
// transform the data into HTML
|
|
||||||
map((urls) => {
|
|
||||||
return urls.map(url => `
|
|
||||||
<code><a href="${url}" target="_blank"><span class="api-url">${url}</span></a></code>
|
|
||||||
`).join(',');
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -55,6 +55,10 @@ export class AbstractItemUpdateComponent extends AbstractTrackableComponent impl
|
|||||||
*/
|
*/
|
||||||
updates$: Observable<FieldUpdates>;
|
updates$: Observable<FieldUpdates>;
|
||||||
|
|
||||||
|
hasChanges$: Observable<boolean>;
|
||||||
|
|
||||||
|
isReinstatable$: Observable<boolean>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Route to the item's page
|
* Route to the item's page
|
||||||
*/
|
*/
|
||||||
@@ -101,10 +105,9 @@ export class AbstractItemUpdateComponent extends AbstractTrackableComponent impl
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.discardTimeOut = environment.item.edit.undoTimeout;
|
this.discardTimeOut = environment.item.edit.undoTimeout;
|
||||||
this.url = this.router.url;
|
this.url = this.router.url.split('?')[0];
|
||||||
if (this.url.indexOf('?') > 0) {
|
this.hasChanges$ = this.hasChanges();
|
||||||
this.url = this.url.substr(0, this.url.indexOf('?'));
|
this.isReinstatable$ = this.isReinstatable();
|
||||||
}
|
|
||||||
this.hasChanges().pipe(first()).subscribe((hasChanges) => {
|
this.hasChanges().pipe(first()).subscribe((hasChanges) => {
|
||||||
if (!hasChanges) {
|
if (!hasChanges) {
|
||||||
this.initializeOriginalFields();
|
this.initializeOriginalFields();
|
||||||
|
@@ -1,7 +1,4 @@
|
|||||||
import {
|
import { Route } from '@angular/router';
|
||||||
mapToCanActivate,
|
|
||||||
Route,
|
|
||||||
} from '@angular/router';
|
|
||||||
|
|
||||||
import { i18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
|
import { i18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||||
import { ThemedDsoEditMetadataComponent } from '../../dso-shared/dso-edit-metadata/themed-dso-edit-metadata.component';
|
import { ThemedDsoEditMetadataComponent } from '../../dso-shared/dso-edit-metadata/themed-dso-edit-metadata.component';
|
||||||
@@ -27,17 +24,21 @@ import { ItemCollectionMapperComponent } from './item-collection-mapper/item-col
|
|||||||
import { ItemCurateComponent } from './item-curate/item-curate.component';
|
import { ItemCurateComponent } from './item-curate/item-curate.component';
|
||||||
import { ItemDeleteComponent } from './item-delete/item-delete.component';
|
import { ItemDeleteComponent } from './item-delete/item-delete.component';
|
||||||
import { ItemMoveComponent } from './item-move/item-move.component';
|
import { ItemMoveComponent } from './item-move/item-move.component';
|
||||||
import { ItemPageAccessControlGuard } from './item-page-access-control.guard';
|
import { itemPageAccessControlGuard } from './item-page-access-control.guard';
|
||||||
import { ItemPageBitstreamsGuard } from './item-page-bitstreams.guard';
|
import { itemPageBitstreamsGuard } from './item-page-bitstreams.guard';
|
||||||
import { ItemPageCollectionMapperGuard } from './item-page-collection-mapper.guard';
|
import { itemPageCollectionMapperGuard } from './item-page-collection-mapper.guard';
|
||||||
import { ItemPageCurateGuard } from './item-page-curate.guard';
|
import { itemPageCurateGuard } from './item-page-curate.guard';
|
||||||
import { ItemPageMetadataGuard } from './item-page-metadata.guard';
|
import { itemPageDeleteGuard } from './item-page-delete.guard';
|
||||||
import { ItemPageRegisterDoiGuard } from './item-page-register-doi.guard';
|
import { itemPageEditAuthorizationsGuard } from './item-page-edit-authorizations.guard';
|
||||||
import { ItemPageReinstateGuard } from './item-page-reinstate.guard';
|
import { itemPageMetadataGuard } from './item-page-metadata.guard';
|
||||||
import { ItemPageRelationshipsGuard } from './item-page-relationships.guard';
|
import { itemPageMoveGuard } from './item-page-move.guard';
|
||||||
import { ItemPageStatusGuard } from './item-page-status.guard';
|
import { itemPagePrivateGuard } from './item-page-private.guard';
|
||||||
import { ItemPageVersionHistoryGuard } from './item-page-version-history.guard';
|
import { itemPageRegisterDoiGuard } from './item-page-register-doi.guard';
|
||||||
import { ItemPageWithdrawGuard } from './item-page-withdraw.guard';
|
import { itemPageReinstateGuard } from './item-page-reinstate.guard';
|
||||||
|
import { itemPageRelationshipsGuard } from './item-page-relationships.guard';
|
||||||
|
import { itemPageStatusGuard } from './item-page-status.guard';
|
||||||
|
import { itemPageVersionHistoryGuard } from './item-page-version-history.guard';
|
||||||
|
import { itemPageWithdrawGuard } from './item-page-withdraw.guard';
|
||||||
import { ItemPrivateComponent } from './item-private/item-private.component';
|
import { ItemPrivateComponent } from './item-private/item-private.component';
|
||||||
import { ItemPublicComponent } from './item-public/item-public.component';
|
import { ItemPublicComponent } from './item-public/item-public.component';
|
||||||
import { ItemRegisterDoiComponent } from './item-register-doi/item-register-doi.component';
|
import { ItemRegisterDoiComponent } from './item-register-doi/item-register-doi.component';
|
||||||
@@ -72,31 +73,31 @@ export const ROUTES: Route[] = [
|
|||||||
path: 'status',
|
path: 'status',
|
||||||
component: ThemedItemStatusComponent,
|
component: ThemedItemStatusComponent,
|
||||||
data: { title: 'item.edit.tabs.status.title', showBreadcrumbs: true },
|
data: { title: 'item.edit.tabs.status.title', showBreadcrumbs: true },
|
||||||
canActivate: mapToCanActivate([ItemPageStatusGuard]),
|
canActivate: [itemPageStatusGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'bitstreams',
|
path: 'bitstreams',
|
||||||
component: ItemBitstreamsComponent,
|
component: ItemBitstreamsComponent,
|
||||||
data: { title: 'item.edit.tabs.bitstreams.title', showBreadcrumbs: true },
|
data: { title: 'item.edit.tabs.bitstreams.title', showBreadcrumbs: true },
|
||||||
canActivate: mapToCanActivate([ItemPageBitstreamsGuard]),
|
canActivate: [itemPageBitstreamsGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'metadata',
|
path: 'metadata',
|
||||||
component: ThemedDsoEditMetadataComponent,
|
component: ThemedDsoEditMetadataComponent,
|
||||||
data: { title: 'item.edit.tabs.metadata.title', showBreadcrumbs: true },
|
data: { title: 'item.edit.tabs.metadata.title', showBreadcrumbs: true },
|
||||||
canActivate: mapToCanActivate([ItemPageMetadataGuard]),
|
canActivate: [itemPageMetadataGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'curate',
|
path: 'curate',
|
||||||
component: ItemCurateComponent,
|
component: ItemCurateComponent,
|
||||||
data: { title: 'item.edit.tabs.curate.title', showBreadcrumbs: true },
|
data: { title: 'item.edit.tabs.curate.title', showBreadcrumbs: true },
|
||||||
canActivate: mapToCanActivate([ItemPageCurateGuard]),
|
canActivate: [itemPageCurateGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'relationships',
|
path: 'relationships',
|
||||||
component: ItemRelationshipsComponent,
|
component: ItemRelationshipsComponent,
|
||||||
data: { title: 'item.edit.tabs.relationships.title', showBreadcrumbs: true },
|
data: { title: 'item.edit.tabs.relationships.title', showBreadcrumbs: true },
|
||||||
canActivate: mapToCanActivate([ItemPageRelationshipsGuard]),
|
canActivate: [itemPageRelationshipsGuard],
|
||||||
},
|
},
|
||||||
/* TODO - uncomment & fix when view page exists
|
/* TODO - uncomment & fix when view page exists
|
||||||
{
|
{
|
||||||
@@ -114,19 +115,19 @@ export const ROUTES: Route[] = [
|
|||||||
path: 'versionhistory',
|
path: 'versionhistory',
|
||||||
component: ItemVersionHistoryComponent,
|
component: ItemVersionHistoryComponent,
|
||||||
data: { title: 'item.edit.tabs.versionhistory.title', showBreadcrumbs: true },
|
data: { title: 'item.edit.tabs.versionhistory.title', showBreadcrumbs: true },
|
||||||
canActivate: mapToCanActivate([ItemPageVersionHistoryGuard]),
|
canActivate: [itemPageVersionHistoryGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'access-control',
|
path: 'access-control',
|
||||||
component: ItemAccessControlComponent,
|
component: ItemAccessControlComponent,
|
||||||
data: { title: 'item.edit.tabs.access-control.title', showBreadcrumbs: true },
|
data: { title: 'item.edit.tabs.access-control.title', showBreadcrumbs: true },
|
||||||
canActivate: mapToCanActivate([ItemPageAccessControlGuard]),
|
canActivate: [itemPageAccessControlGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'mapper',
|
path: 'mapper',
|
||||||
component: ItemCollectionMapperComponent,
|
component: ItemCollectionMapperComponent,
|
||||||
data: { title: 'item.edit.tabs.item-mapper.title', showBreadcrumbs: true },
|
data: { title: 'item.edit.tabs.item-mapper.title', showBreadcrumbs: true },
|
||||||
canActivate: mapToCanActivate([ItemPageCollectionMapperGuard]),
|
canActivate: [itemPageCollectionMapperGuard],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -137,16 +138,17 @@ export const ROUTES: Route[] = [
|
|||||||
{
|
{
|
||||||
path: ITEM_EDIT_WITHDRAW_PATH,
|
path: ITEM_EDIT_WITHDRAW_PATH,
|
||||||
component: ItemWithdrawComponent,
|
component: ItemWithdrawComponent,
|
||||||
canActivate: mapToCanActivate([ItemPageWithdrawGuard]),
|
canActivate: [itemPageWithdrawGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ITEM_EDIT_REINSTATE_PATH,
|
path: ITEM_EDIT_REINSTATE_PATH,
|
||||||
component: ItemReinstateComponent,
|
component: ItemReinstateComponent,
|
||||||
canActivate: mapToCanActivate([ItemPageReinstateGuard]),
|
canActivate: [itemPageReinstateGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ITEM_EDIT_PRIVATE_PATH,
|
path: ITEM_EDIT_PRIVATE_PATH,
|
||||||
component: ItemPrivateComponent,
|
component: ItemPrivateComponent,
|
||||||
|
canActivate: [itemPagePrivateGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ITEM_EDIT_PUBLIC_PATH,
|
path: ITEM_EDIT_PUBLIC_PATH,
|
||||||
@@ -155,16 +157,18 @@ export const ROUTES: Route[] = [
|
|||||||
{
|
{
|
||||||
path: ITEM_EDIT_DELETE_PATH,
|
path: ITEM_EDIT_DELETE_PATH,
|
||||||
component: ItemDeleteComponent,
|
component: ItemDeleteComponent,
|
||||||
|
canActivate: [itemPageDeleteGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ITEM_EDIT_MOVE_PATH,
|
path: ITEM_EDIT_MOVE_PATH,
|
||||||
component: ItemMoveComponent,
|
component: ItemMoveComponent,
|
||||||
data: { title: 'item.edit.move.title' },
|
data: { title: 'item.edit.move.title' },
|
||||||
|
canActivate: [itemPageMoveGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ITEM_EDIT_REGISTER_DOI_PATH,
|
path: ITEM_EDIT_REGISTER_DOI_PATH,
|
||||||
component: ItemRegisterDoiComponent,
|
component: ItemRegisterDoiComponent,
|
||||||
canActivate: mapToCanActivate([ItemPageRegisterDoiGuard]),
|
canActivate: [itemPageRegisterDoiGuard],
|
||||||
data: { title: 'item.edit.register-doi.title' },
|
data: { title: 'item.edit.register-doi.title' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -192,6 +196,7 @@ export const ROUTES: Route[] = [
|
|||||||
data: { title: 'item.edit.authorizations.title' },
|
data: { title: 'item.edit.authorizations.title' },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
canActivate: [itemPageEditAuthorizationsGuard],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@@ -6,21 +6,21 @@
|
|||||||
class="fas fa-upload"></i>
|
class="fas fa-upload"></i>
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.upload-button" | translate}}</span>
|
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.upload-button" | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-warning" *ngIf="isReinstatable() | async"
|
<button class="btn btn-warning" *ngIf="isReinstatable$ | async"
|
||||||
[attr.aria-label]="'item.edit.bitstreams.reinstate-button' | translate"
|
[attr.aria-label]="'item.edit.bitstreams.reinstate-button' | translate"
|
||||||
(click)="reinstate()"><i
|
(click)="reinstate()"><i
|
||||||
class="fas fa-undo-alt"></i>
|
class="fas fa-undo-alt"></i>
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.reinstate-button" | translate}}</span>
|
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.reinstate-button" | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-primary" [disabled]="(hasChanges() | async) !== true || submitting"
|
<button class="btn btn-primary" [disabled]="(hasChanges$ | async) !== true || submitting"
|
||||||
[attr.aria-label]="'item.edit.bitstreams.save-button' | translate"
|
[attr.aria-label]="'item.edit.bitstreams.save-button' | translate"
|
||||||
(click)="submit()"><i
|
(click)="submit()"><i
|
||||||
class="fas fa-save"></i>
|
class="fas fa-save"></i>
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.save-button" | translate}}</span>
|
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.save-button" | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-danger" *ngIf="(isReinstatable() | async) !== true"
|
<button class="btn btn-danger" *ngIf="(isReinstatable$ | async) !== true"
|
||||||
[attr.aria-label]="'item.edit.bitstreams.discard-button' | translate"
|
[attr.aria-label]="'item.edit.bitstreams.discard-button' | translate"
|
||||||
[disabled]="(hasChanges() | async) !== true || submitting"
|
[disabled]="(hasChanges$ | async) !== true || submitting"
|
||||||
(click)="discard()"><i
|
(click)="discard()"><i
|
||||||
class="fas fa-times"></i>
|
class="fas fa-times"></i>
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.discard-button" | translate}}</span>
|
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.discard-button" | translate}}</span>
|
||||||
@@ -52,21 +52,21 @@
|
|||||||
|
|
||||||
<div class="button-row bottom">
|
<div class="button-row bottom">
|
||||||
<div class="mt-4 float-right space-children-mr ml-gap">
|
<div class="mt-4 float-right space-children-mr ml-gap">
|
||||||
<button class="btn btn-warning" *ngIf="isReinstatable() | async"
|
<button class="btn btn-warning" *ngIf="isReinstatable$ | async"
|
||||||
[attr.aria-label]="'item.edit.bitstreams.reinstate-button' | translate"
|
[attr.aria-label]="'item.edit.bitstreams.reinstate-button' | translate"
|
||||||
(click)="reinstate()"><i
|
(click)="reinstate()"><i
|
||||||
class="fas fa-undo-alt"></i>
|
class="fas fa-undo-alt"></i>
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.reinstate-button" | translate}}</span>
|
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.reinstate-button" | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-primary" [disabled]="(hasChanges() | async) !== true || submitting"
|
<button class="btn btn-primary" [disabled]="(hasChanges$ | async) !== true || submitting"
|
||||||
[attr.aria-label]="'item.edit.bitstreams.save-button' | translate"
|
[attr.aria-label]="'item.edit.bitstreams.save-button' | translate"
|
||||||
(click)="submit()"><i
|
(click)="submit()"><i
|
||||||
class="fas fa-save"></i>
|
class="fas fa-save"></i>
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.save-button" | translate}}</span>
|
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.save-button" | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-danger" *ngIf="(isReinstatable() | async) !== true"
|
<button class="btn btn-danger" *ngIf="(isReinstatable$ | async) !== true"
|
||||||
[attr.aria-label]="'item.edit.bitstreams.discard-button' | translate"
|
[attr.aria-label]="'item.edit.bitstreams.discard-button' | translate"
|
||||||
[disabled]="(hasChanges() | async) !== true || submitting"
|
[disabled]="(hasChanges$ | async) !== true || submitting"
|
||||||
(click)="discard()"><i
|
(click)="discard()"><i
|
||||||
class="fas fa-times"></i>
|
class="fas fa-times"></i>
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.discard-button" | translate}}</span>
|
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.discard-button" | translate}}</span>
|
||||||
|
@@ -6,30 +6,29 @@
|
|||||||
<p>{{descriptionMessage | translate}}</p>
|
<p>{{descriptionMessage | translate}}</p>
|
||||||
<ds-modify-item-overview [item]="item"></ds-modify-item-overview>
|
<ds-modify-item-overview [item]="item"></ds-modify-item-overview>
|
||||||
|
|
||||||
<ng-container *ngVar="(types$ | async) as types">
|
<ng-container *ngVar="(typeDTOs$ | async) as types">
|
||||||
|
|
||||||
<div *ngIf="types && types.length > 0" class="mb-4">
|
<div *ngIf="types && types.length > 0" class="mb-4">
|
||||||
|
|
||||||
{{'virtual-metadata.delete-item.info' | translate}}
|
{{'virtual-metadata.delete-item.info' | translate}}
|
||||||
|
|
||||||
<div *ngFor="let type of types" class="mb-4">
|
<div *ngFor="let typeDto of types" class="mb-4">
|
||||||
|
<div *ngVar="(typeDto.isSelected$ | async) as selected"
|
||||||
<div *ngVar="(isSelected(type) | async) as selected"
|
|
||||||
class="d-flex flex-row">
|
class="d-flex flex-row">
|
||||||
|
|
||||||
<div class="m-2" (click)="setSelected(type, !selected)">
|
<div class="m-2" (click)="setSelected(typeDto.relationshipType, !selected)">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" [checked]="selected">
|
<input type="checkbox" [checked]="selected" [disabled]="isDeleting$ | async">
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-column flex-grow-1">
|
<div class="flex-column flex-grow-1">
|
||||||
<h5 (click)="setSelected(type, !selected)">
|
<h5 (click)="setSelected(typeDto.relationshipType, !selected)">
|
||||||
{{getRelationshipMessageKey(getLabel(type) | async) | translate}}
|
{{getRelationshipMessageKey(typeDto.label$ | async) | translate}}
|
||||||
</h5>
|
</h5>
|
||||||
<div *ngFor="let relationship of (getRelationships(type) | async)"
|
<div *ngFor="let relationshipDto of (typeDto.relationshipDTOs$ | async)"
|
||||||
class="d-flex flex-row">
|
class="d-flex flex-row">
|
||||||
<ng-container *ngVar="(getRelatedItem(relationship) | async) as relatedItem">
|
<ng-container *ngVar="(relationshipDto.relatedItem$ | async) as relatedItem">
|
||||||
|
|
||||||
<ds-listable-object-component-loader
|
<ds-listable-object-component-loader
|
||||||
*ngIf="relatedItem"
|
*ngIf="relatedItem"
|
||||||
@@ -46,7 +45,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-template #virtualMetadataModal>
|
<ng-template #virtualMetadataModal>
|
||||||
<div>
|
<div class="thumb-font-1">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
{{'virtual-metadata.delete-item.modal-head' | translate}}
|
{{'virtual-metadata.delete-item.modal-head' | translate}}
|
||||||
<button type="button" class="close"
|
<button type="button" class="close"
|
||||||
@@ -60,7 +59,7 @@
|
|||||||
[object]="relatedItem"
|
[object]="relatedItem"
|
||||||
[viewMode]="viewMode">
|
[viewMode]="viewMode">
|
||||||
</ds-listable-object-component-loader>
|
</ds-listable-object-component-loader>
|
||||||
<div *ngFor="let metadata of (getVirtualMetadata(relationship) | async)">
|
<div *ngFor="let metadata of (relationshipDto.virtualMetadata$ | async)">
|
||||||
<div>
|
<div>
|
||||||
<div class="font-weight-bold">
|
<div class="font-weight-bold">
|
||||||
{{metadata.metadataField}}
|
{{metadata.metadataField}}
|
||||||
@@ -87,10 +86,11 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<div class="space-children-mr">
|
<div class="space-children-mr">
|
||||||
<button (click)="performAction()"
|
<button [disabled]="isDeleting$ | async" (click)="performAction()"
|
||||||
class="btn btn-outline-secondary perform-action">{{confirmMessage | translate}}
|
class="btn btn-outline-secondary perform-action">{{confirmMessage | translate}}
|
||||||
</button>
|
</button>
|
||||||
<button [routerLink]="[itemPageRoute, 'edit']" class="btn btn-outline-secondary cancel">
|
<button [disabled]="isDeleting$ | async" [routerLink]="[itemPageRoute, 'edit']"
|
||||||
|
class="btn btn-outline-secondary cancel">
|
||||||
{{cancelMessage| translate}}
|
{{cancelMessage| translate}}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
// eslint-disable-next-line max-classes-per-file
|
||||||
import {
|
import {
|
||||||
AsyncPipe,
|
AsyncPipe,
|
||||||
NgForOf,
|
NgForOf,
|
||||||
@@ -68,6 +69,34 @@ import { ModifyItemOverviewComponent } from '../modify-item-overview/modify-item
|
|||||||
import { AbstractSimpleItemActionComponent } from '../simple-item-action/abstract-simple-item-action.component';
|
import { AbstractSimpleItemActionComponent } from '../simple-item-action/abstract-simple-item-action.component';
|
||||||
import { VirtualMetadata } from '../virtual-metadata/virtual-metadata.component';
|
import { VirtualMetadata } from '../virtual-metadata/virtual-metadata.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data Transfer Object used to prevent the HTML template to call function returning Observables
|
||||||
|
*/
|
||||||
|
class RelationshipTypeDTO {
|
||||||
|
|
||||||
|
relationshipType: RelationshipType;
|
||||||
|
|
||||||
|
isSelected$: Observable<boolean>;
|
||||||
|
|
||||||
|
label$: Observable<string>;
|
||||||
|
|
||||||
|
relationshipDTOs$: Observable<RelationshipDTO[]>;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data Transfer Object used to prevent the HTML template to call function returning Observables
|
||||||
|
*/
|
||||||
|
class RelationshipDTO {
|
||||||
|
|
||||||
|
relationship: Relationship;
|
||||||
|
|
||||||
|
relatedItem$: Observable<Item>;
|
||||||
|
|
||||||
|
virtualMetadata$: Observable<VirtualMetadata[]>;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-item-delete',
|
selector: 'ds-item-delete',
|
||||||
templateUrl: '../item-delete/item-delete.component.html',
|
templateUrl: '../item-delete/item-delete.component.html',
|
||||||
@@ -106,7 +135,7 @@ export class ItemDeleteComponent
|
|||||||
* A list of the relationship types for which this item has relations as an observable.
|
* A list of the relationship types for which this item has relations as an observable.
|
||||||
* The list doesn't contain duplicates.
|
* The list doesn't contain duplicates.
|
||||||
*/
|
*/
|
||||||
types$: BehaviorSubject<RelationshipType[]> = new BehaviorSubject([]);
|
typeDTOs$: BehaviorSubject<RelationshipTypeDTO[]> = new BehaviorSubject([]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A map which stores the relationships of this item for each type as observable lists
|
* A map which stores the relationships of this item for each type as observable lists
|
||||||
@@ -135,6 +164,8 @@ export class ItemDeleteComponent
|
|||||||
*/
|
*/
|
||||||
private subs: Subscription[] = [];
|
private subs: Subscription[] = [];
|
||||||
|
|
||||||
|
public isDeleting$: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
||||||
|
|
||||||
constructor(protected route: ActivatedRoute,
|
constructor(protected route: ActivatedRoute,
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
protected notificationsService: NotificationsService,
|
protected notificationsService: NotificationsService,
|
||||||
@@ -189,13 +220,24 @@ export class ItemDeleteComponent
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
).subscribe((types: RelationshipType[]) => this.types$.next(types)));
|
).subscribe((types: RelationshipType[]) => this.typeDTOs$.next(types.map((relationshipType: RelationshipType) => Object.assign(new RelationshipTypeDTO(), {
|
||||||
|
relationshipType: relationshipType,
|
||||||
|
isSelected$: this.isSelected(relationshipType),
|
||||||
|
label$: this.getLabel(relationshipType),
|
||||||
|
relationshipDTOs$: this.getRelationships(relationshipType).pipe(
|
||||||
|
map((relationships: Relationship[]) => relationships.map((relationship: Relationship) => Object.assign(new RelationshipDTO(), {
|
||||||
|
relationship: relationship,
|
||||||
|
relatedItem$: this.getRelatedItem(relationship),
|
||||||
|
virtualMetadata$: this.getVirtualMetadata(relationship),
|
||||||
|
} as RelationshipDTO))),
|
||||||
|
),
|
||||||
|
})))));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.subs.push(this.types$.pipe(
|
this.subs.push(this.typeDTOs$.pipe(
|
||||||
take(1),
|
take(1),
|
||||||
).subscribe((types) =>
|
).subscribe((types: RelationshipTypeDTO[]) =>
|
||||||
this.objectUpdatesService.initialize(this.url, types, this.item.lastModified),
|
this.objectUpdatesService.initialize(this.url, types.map((relationshipTypeDto: RelationshipTypeDTO) => relationshipTypeDto.relationshipType), this.item.lastModified),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,34 +410,33 @@ export class ItemDeleteComponent
|
|||||||
* @param selected whether the type should be selected
|
* @param selected whether the type should be selected
|
||||||
*/
|
*/
|
||||||
setSelected(type: RelationshipType, selected: boolean): void {
|
setSelected(type: RelationshipType, selected: boolean): void {
|
||||||
this.objectUpdatesService.setSelectedVirtualMetadata(this.url, this.item.uuid, type.uuid, selected);
|
if (this.isDeleting$.value === false) {
|
||||||
|
this.objectUpdatesService.setSelectedVirtualMetadata(this.url, this.item.uuid, type.uuid, selected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform the delete operation
|
* Perform the delete operation
|
||||||
*/
|
*/
|
||||||
performAction() {
|
performAction(): void {
|
||||||
|
this.isDeleting$.next(true);
|
||||||
this.subs.push(this.types$.pipe(
|
this.subs.push(this.typeDTOs$.pipe(
|
||||||
switchMap((types) =>
|
switchMap((types: RelationshipTypeDTO[]) =>
|
||||||
combineLatest(
|
combineLatest(
|
||||||
types.map((type) => this.isSelected(type)),
|
types.map((type: RelationshipTypeDTO) => type.isSelected$),
|
||||||
).pipe(
|
).pipe(
|
||||||
defaultIfEmpty([]),
|
defaultIfEmpty([]),
|
||||||
map((selection) => types.filter(
|
map((selection: boolean[]) => types.filter(
|
||||||
(type, index) => selection[index],
|
(type: RelationshipTypeDTO, index: number) => selection[index],
|
||||||
)),
|
)),
|
||||||
map((selectedTypes) => selectedTypes.map((type) => type.id)),
|
map((selectedDtoTypes: RelationshipTypeDTO[]) => selectedDtoTypes.map((typeDto: RelationshipTypeDTO) => typeDto.relationshipType.id)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
switchMap((types) =>
|
switchMap((types: string[]) => this.itemDataService.delete(this.item.id, types)),
|
||||||
this.itemDataService.delete(this.item.id, types).pipe(getFirstCompletedRemoteData()),
|
getFirstCompletedRemoteData(),
|
||||||
),
|
).subscribe((rd: RemoteData<NoContent>) => {
|
||||||
).subscribe(
|
this.notify(rd.hasSucceeded);
|
||||||
(rd: RemoteData<NoContent>) => {
|
}));
|
||||||
this.notify(rd.hasSucceeded);
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -405,10 +446,10 @@ export class ItemDeleteComponent
|
|||||||
notify(succeeded: boolean) {
|
notify(succeeded: boolean) {
|
||||||
if (succeeded) {
|
if (succeeded) {
|
||||||
this.notificationsService.success(this.translateService.get('item.edit.' + this.messageKey + '.success'));
|
this.notificationsService.success(this.translateService.get('item.edit.' + this.messageKey + '.success'));
|
||||||
this.router.navigate(['']);
|
void this.router.navigate(['']);
|
||||||
} else {
|
} else {
|
||||||
this.notificationsService.error(this.translateService.get('item.edit.' + this.messageKey + '.error'));
|
this.notificationsService.error(this.translateService.get('item.edit.' + this.messageKey + '.error'));
|
||||||
this.router.navigate([getItemEditRoute(this.item)]);
|
void this.router.navigate([getItemEditRoute(this.item)]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,43 +1,15 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { CanActivateFn } from '@angular/router';
|
||||||
import {
|
import { of as observableOf } from 'rxjs';
|
||||||
ActivatedRouteSnapshot,
|
|
||||||
ResolveFn,
|
|
||||||
Router,
|
|
||||||
RouterStateSnapshot,
|
|
||||||
} from '@angular/router';
|
|
||||||
import {
|
|
||||||
Observable,
|
|
||||||
of as observableOf,
|
|
||||||
} from 'rxjs';
|
|
||||||
|
|
||||||
import { AuthService } from '../../core/auth/auth.service';
|
import { dsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard';
|
||||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
|
||||||
import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard';
|
|
||||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
|
||||||
import { Item } from '../../core/shared/item.model';
|
|
||||||
import { itemPageResolver } from '../item-page.resolver';
|
import { itemPageResolver } from '../item-page.resolver';
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
/**
|
/**
|
||||||
* Guard for preventing unauthorized access to certain {@link Item} pages requiring administrator rights
|
* Guard for preventing unauthorized access to certain {@link Item} pages requiring administrator rights
|
||||||
*/
|
*/
|
||||||
export class ItemPageAccessControlGuard extends DsoPageSingleFeatureGuard<Item> {
|
export const itemPageAccessControlGuard: CanActivateFn =
|
||||||
|
dsoPageSingleFeatureGuard(
|
||||||
protected resolver: ResolveFn<RemoteData<Item>> = itemPageResolver;
|
() => itemPageResolver,
|
||||||
|
() => observableOf(FeatureID.AdministratorOf),
|
||||||
constructor(protected authorizationService: AuthorizationDataService,
|
);
|
||||||
protected router: Router,
|
|
||||||
protected authService: AuthService) {
|
|
||||||
super(authorizationService, router, authService);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check administrator authorization rights
|
|
||||||
*/
|
|
||||||
getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
|
||||||
return observableOf(FeatureID.AdministratorOf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -1,43 +1,16 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { CanActivateFn } from '@angular/router';
|
||||||
import {
|
import { of as observableOf } from 'rxjs';
|
||||||
ActivatedRouteSnapshot,
|
|
||||||
ResolveFn,
|
|
||||||
Router,
|
|
||||||
RouterStateSnapshot,
|
|
||||||
} from '@angular/router';
|
|
||||||
import {
|
|
||||||
Observable,
|
|
||||||
of as observableOf,
|
|
||||||
} from 'rxjs';
|
|
||||||
|
|
||||||
import { AuthService } from '../../core/auth/auth.service';
|
import { dsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard';
|
||||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
|
||||||
import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard';
|
|
||||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
|
||||||
import { Item } from '../../core/shared/item.model';
|
|
||||||
import { itemPageResolver } from '../item-page.resolver';
|
import { itemPageResolver } from '../item-page.resolver';
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
/**
|
/**
|
||||||
* Guard for preventing unauthorized access to certain {@link Item} pages requiring manage bitstreams rights
|
* Guard for preventing unauthorized access to certain {@link Item} pages requiring manage bitstreams rights
|
||||||
|
* Check manage bitstreams authorization rights
|
||||||
*/
|
*/
|
||||||
export class ItemPageBitstreamsGuard extends DsoPageSingleFeatureGuard<Item> {
|
export const itemPageBitstreamsGuard: CanActivateFn =
|
||||||
|
dsoPageSingleFeatureGuard(
|
||||||
protected resolver: ResolveFn<RemoteData<Item>> = itemPageResolver;
|
() => itemPageResolver,
|
||||||
|
() => observableOf(FeatureID.CanManageBitstreamBundles),
|
||||||
constructor(protected authorizationService: AuthorizationDataService,
|
);
|
||||||
protected router: Router,
|
|
||||||
protected authService: AuthService) {
|
|
||||||
super( authorizationService, router, authService);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check manage bitstreams authorization rights
|
|
||||||
*/
|
|
||||||
getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
|
||||||
return observableOf(FeatureID.CanManageBitstreamBundles);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -1,43 +1,16 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { CanActivateFn } from '@angular/router';
|
||||||
import {
|
import { of as observableOf } from 'rxjs';
|
||||||
ActivatedRouteSnapshot,
|
|
||||||
ResolveFn,
|
|
||||||
Router,
|
|
||||||
RouterStateSnapshot,
|
|
||||||
} from '@angular/router';
|
|
||||||
import {
|
|
||||||
Observable,
|
|
||||||
of as observableOf,
|
|
||||||
} from 'rxjs';
|
|
||||||
|
|
||||||
import { AuthService } from '../../core/auth/auth.service';
|
import { dsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard';
|
||||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
|
||||||
import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard';
|
|
||||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
|
||||||
import { Item } from '../../core/shared/item.model';
|
|
||||||
import { itemPageResolver } from '../item-page.resolver';
|
import { itemPageResolver } from '../item-page.resolver';
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
/**
|
/**
|
||||||
* Guard for preventing unauthorized access to certain {@link Item} pages requiring manage mappings rights
|
* Guard for preventing unauthorized access to certain {@link Item} pages requiring manage mappings rights
|
||||||
|
* Check manage mappings authorization rights
|
||||||
*/
|
*/
|
||||||
export class ItemPageCollectionMapperGuard extends DsoPageSingleFeatureGuard<Item> {
|
export const itemPageCollectionMapperGuard: CanActivateFn =
|
||||||
|
dsoPageSingleFeatureGuard(
|
||||||
protected resolver: ResolveFn<RemoteData<Item>> = itemPageResolver;
|
() => itemPageResolver,
|
||||||
|
() => observableOf(FeatureID.CanManageMappings),
|
||||||
constructor(protected authorizationService: AuthorizationDataService,
|
);
|
||||||
protected router: Router,
|
|
||||||
protected authService: AuthService) {
|
|
||||||
super(authorizationService, router, authService);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check manage mappings authorization rights
|
|
||||||
*/
|
|
||||||
getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
|
||||||
return observableOf(FeatureID.CanManageMappings);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -1,43 +1,15 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { CanActivateFn } from '@angular/router';
|
||||||
import {
|
import { of as observableOf } from 'rxjs';
|
||||||
ActivatedRouteSnapshot,
|
|
||||||
ResolveFn,
|
|
||||||
Router,
|
|
||||||
RouterStateSnapshot,
|
|
||||||
} from '@angular/router';
|
|
||||||
import {
|
|
||||||
Observable,
|
|
||||||
of as observableOf,
|
|
||||||
} from 'rxjs';
|
|
||||||
|
|
||||||
import { AuthService } from '../../core/auth/auth.service';
|
import { dsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard';
|
||||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
|
||||||
import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard';
|
|
||||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
|
||||||
import { Item } from '../../core/shared/item.model';
|
|
||||||
import { itemPageResolver } from '../item-page.resolver';
|
import { itemPageResolver } from '../item-page.resolver';
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
/**
|
/**
|
||||||
* Guard for preventing unauthorized access to certain {@link Item} pages requiring administrator rights
|
* Guard for preventing unauthorized access to certain {@link Item} pages requiring administrator rights
|
||||||
*/
|
*/
|
||||||
export class ItemPageCurateGuard extends DsoPageSingleFeatureGuard<Item> {
|
export const itemPageCurateGuard: CanActivateFn =
|
||||||
|
dsoPageSingleFeatureGuard(
|
||||||
protected resolver: ResolveFn<RemoteData<Item>> = itemPageResolver;
|
() => itemPageResolver,
|
||||||
|
() => observableOf(FeatureID.AdministratorOf),
|
||||||
constructor(protected authorizationService: AuthorizationDataService,
|
);
|
||||||
protected router: Router,
|
|
||||||
protected authService: AuthService) {
|
|
||||||
super(authorizationService, router, authService);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check administrator authorization rights
|
|
||||||
*/
|
|
||||||
getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
|
||||||
return observableOf(FeatureID.AdministratorOf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -0,0 +1,94 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import {
|
||||||
|
Router,
|
||||||
|
UrlTree,
|
||||||
|
} from '@angular/router';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import {
|
||||||
|
Observable,
|
||||||
|
of as observableOf,
|
||||||
|
} from 'rxjs';
|
||||||
|
import { AuthService } from 'src/app/core/auth/auth.service';
|
||||||
|
import { AuthorizationDataService } from 'src/app/core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { FeatureID } from 'src/app/core/data/feature-authorization/feature-id';
|
||||||
|
|
||||||
|
import { APP_DATA_SERVICES_MAP } from '../../../config/app-config.interface';
|
||||||
|
import { ItemDataService } from '../../core/data/item-data.service';
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { getMockTranslateService } from '../../shared/mocks/translate.service.mock';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
|
import { itemPageDeleteGuard } from './item-page-delete.guard';
|
||||||
|
|
||||||
|
describe('itemPageDeleteGuard', () => {
|
||||||
|
let authorizationService: AuthorizationDataService;
|
||||||
|
let authService: AuthService;
|
||||||
|
let router: Router;
|
||||||
|
let route;
|
||||||
|
let parentRoute;
|
||||||
|
let store: Store;
|
||||||
|
let itemService: ItemDataService;
|
||||||
|
let item: Item;
|
||||||
|
let uuid = '1234-abcdef-54321-fedcba';
|
||||||
|
let itemSelfLink = 'test.url/1234-abcdef-54321-fedcba';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
|
||||||
|
store = jasmine.createSpyObj('store', {
|
||||||
|
dispatch: {},
|
||||||
|
pipe: observableOf(true),
|
||||||
|
});
|
||||||
|
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||||
|
isAuthorized: observableOf(true),
|
||||||
|
});
|
||||||
|
router = jasmine.createSpyObj('router', {
|
||||||
|
parseUrl: {},
|
||||||
|
navigateByUrl: undefined,
|
||||||
|
});
|
||||||
|
authService = jasmine.createSpyObj('authService', {
|
||||||
|
isAuthenticated: observableOf(true),
|
||||||
|
});
|
||||||
|
|
||||||
|
parentRoute = {
|
||||||
|
params: {
|
||||||
|
id: '3e1a5327-dabb-41ff-af93-e6cab9d032f0',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
route = {
|
||||||
|
params: {},
|
||||||
|
parent: parentRoute,
|
||||||
|
};
|
||||||
|
item = new Item();
|
||||||
|
item.uuid = uuid;
|
||||||
|
item._links = { self: { href: itemSelfLink } } as any;
|
||||||
|
itemService = jasmine.createSpyObj('itemService', { findById: createSuccessfulRemoteDataObject$(item) });
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||||
|
{ provide: Router, useValue: router },
|
||||||
|
{ provide: AuthService, useValue: authService },
|
||||||
|
{ provide: Store, useValue: store },
|
||||||
|
{ provide: APP_DATA_SERVICES_MAP, useValue: {} },
|
||||||
|
{ provide: TranslateService, useValue: getMockTranslateService() },
|
||||||
|
{ provide: ItemDataService, useValue: itemService },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call authorizationService.isAuthorized with the appropriate arguments', (done) => {
|
||||||
|
const result$ = TestBed.runInInjectionContext(() => {
|
||||||
|
return itemPageDeleteGuard(route, { url: 'current-url' } as any);
|
||||||
|
}) as Observable<boolean | UrlTree>;
|
||||||
|
|
||||||
|
result$.subscribe((result) => {
|
||||||
|
expect(authorizationService.isAuthorized).toHaveBeenCalledWith(
|
||||||
|
FeatureID.CanDelete,
|
||||||
|
itemSelfLink, // This value is retrieved from the itemDataService.findById's return item's self link
|
||||||
|
undefined, // dsoPageSingleFeatureGuard never provides a function to retrieve a person ID
|
||||||
|
);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
16
src/app/item-page/edit-item-page/item-page-delete.guard.ts
Normal file
16
src/app/item-page/edit-item-page/item-page-delete.guard.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { CanActivateFn } from '@angular/router';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
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 { itemPageResolver } from '../item-page.resolver';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Guard for preventing unauthorized access to certain {@link Item} pages requiring specific authorizations.
|
||||||
|
* Checks authorization rights for deleting items.
|
||||||
|
*/
|
||||||
|
export const itemPageDeleteGuard: CanActivateFn =
|
||||||
|
dsoPageSingleFeatureGuard(
|
||||||
|
() => itemPageResolver,
|
||||||
|
() => observableOf(FeatureID.CanDelete),
|
||||||
|
);
|
@@ -0,0 +1,94 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import {
|
||||||
|
Router,
|
||||||
|
UrlTree,
|
||||||
|
} from '@angular/router';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import {
|
||||||
|
Observable,
|
||||||
|
of as observableOf,
|
||||||
|
} from 'rxjs';
|
||||||
|
import { AuthService } from 'src/app/core/auth/auth.service';
|
||||||
|
import { AuthorizationDataService } from 'src/app/core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { FeatureID } from 'src/app/core/data/feature-authorization/feature-id';
|
||||||
|
|
||||||
|
import { APP_DATA_SERVICES_MAP } from '../../../config/app-config.interface';
|
||||||
|
import { ItemDataService } from '../../core/data/item-data.service';
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { getMockTranslateService } from '../../shared/mocks/translate.service.mock';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
|
import { itemPageEditAuthorizationsGuard } from './item-page-edit-authorizations.guard';
|
||||||
|
|
||||||
|
describe('itemPageEditAuthorizationsGuard', () => {
|
||||||
|
let authorizationService: AuthorizationDataService;
|
||||||
|
let authService: AuthService;
|
||||||
|
let router: Router;
|
||||||
|
let route;
|
||||||
|
let parentRoute;
|
||||||
|
let store: Store;
|
||||||
|
let itemService: ItemDataService;
|
||||||
|
let item: Item;
|
||||||
|
let uuid = '1234-abcdef-54321-fedcba';
|
||||||
|
let itemSelfLink = 'test.url/1234-abcdef-54321-fedcba';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
|
||||||
|
store = jasmine.createSpyObj('store', {
|
||||||
|
dispatch: {},
|
||||||
|
pipe: observableOf(true),
|
||||||
|
});
|
||||||
|
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||||
|
isAuthorized: observableOf(true),
|
||||||
|
});
|
||||||
|
router = jasmine.createSpyObj('router', {
|
||||||
|
parseUrl: {},
|
||||||
|
navigateByUrl: undefined,
|
||||||
|
});
|
||||||
|
authService = jasmine.createSpyObj('authService', {
|
||||||
|
isAuthenticated: observableOf(true),
|
||||||
|
});
|
||||||
|
|
||||||
|
parentRoute = {
|
||||||
|
params: {
|
||||||
|
id: '3e1a5327-dabb-41ff-af93-e6cab9d032f0',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
route = {
|
||||||
|
params: {},
|
||||||
|
parent: parentRoute,
|
||||||
|
};
|
||||||
|
item = new Item();
|
||||||
|
item.uuid = uuid;
|
||||||
|
item._links = { self: { href: itemSelfLink } } as any;
|
||||||
|
itemService = jasmine.createSpyObj('itemService', { findById: createSuccessfulRemoteDataObject$(item) });
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||||
|
{ provide: Router, useValue: router },
|
||||||
|
{ provide: AuthService, useValue: authService },
|
||||||
|
{ provide: Store, useValue: store },
|
||||||
|
{ provide: APP_DATA_SERVICES_MAP, useValue: {} },
|
||||||
|
{ provide: TranslateService, useValue: getMockTranslateService() },
|
||||||
|
{ provide: ItemDataService, useValue: itemService },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call authorizationService.isAuthorized with the appropriate arguments', (done) => {
|
||||||
|
const result$ = TestBed.runInInjectionContext(() => {
|
||||||
|
return itemPageEditAuthorizationsGuard(route, { url: 'current-url' } as any);
|
||||||
|
}) as Observable<boolean | UrlTree>;
|
||||||
|
|
||||||
|
result$.subscribe((result) => {
|
||||||
|
expect(authorizationService.isAuthorized).toHaveBeenCalledWith(
|
||||||
|
FeatureID.CanManagePolicies,
|
||||||
|
itemSelfLink, // This value is retrieved from the itemDataService.findById's return item's self link
|
||||||
|
undefined, // dsoPageSingleFeatureGuard never provides a function to retrieve a person ID
|
||||||
|
);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,16 @@
|
|||||||
|
import { CanActivateFn } from '@angular/router';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
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 { itemPageResolver } from '../item-page.resolver';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Guard for preventing unauthorized access to certain {@link Item} pages requiring specific authorizations.
|
||||||
|
* Checks authorization rights for managing policies.
|
||||||
|
*/
|
||||||
|
export const itemPageEditAuthorizationsGuard: CanActivateFn =
|
||||||
|
dsoPageSingleFeatureGuard(
|
||||||
|
() => itemPageResolver,
|
||||||
|
() => observableOf(FeatureID.CanManagePolicies),
|
||||||
|
);
|
@@ -1,43 +1,16 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { CanActivateFn } from '@angular/router';
|
||||||
import {
|
import { of as observableOf } from 'rxjs';
|
||||||
ActivatedRouteSnapshot,
|
|
||||||
ResolveFn,
|
|
||||||
Router,
|
|
||||||
RouterStateSnapshot,
|
|
||||||
} from '@angular/router';
|
|
||||||
import {
|
|
||||||
Observable,
|
|
||||||
of as observableOf,
|
|
||||||
} from 'rxjs';
|
|
||||||
|
|
||||||
import { AuthService } from '../../core/auth/auth.service';
|
import { dsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard';
|
||||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
|
||||||
import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard';
|
|
||||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
|
||||||
import { Item } from '../../core/shared/item.model';
|
|
||||||
import { itemPageResolver } from '../item-page.resolver';
|
import { itemPageResolver } from '../item-page.resolver';
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
/**
|
/**
|
||||||
* Guard for preventing unauthorized access to certain {@link Item} pages requiring edit metadata rights
|
* Guard for preventing unauthorized access to certain {@link Item} pages requiring edit metadata rights
|
||||||
|
* Check edit metadata authorization rights
|
||||||
*/
|
*/
|
||||||
export class ItemPageMetadataGuard extends DsoPageSingleFeatureGuard<Item> {
|
export const itemPageMetadataGuard: CanActivateFn =
|
||||||
|
dsoPageSingleFeatureGuard(
|
||||||
protected resolver: ResolveFn<RemoteData<Item>> = itemPageResolver;
|
() => itemPageResolver,
|
||||||
|
() => observableOf(FeatureID.CanEditMetadata),
|
||||||
constructor(protected authorizationService: AuthorizationDataService,
|
);
|
||||||
protected router: Router,
|
|
||||||
protected authService: AuthService) {
|
|
||||||
super(authorizationService, router, authService);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check edit metadata authorization rights
|
|
||||||
*/
|
|
||||||
getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
|
||||||
return observableOf(FeatureID.CanEditMetadata);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -0,0 +1,94 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import {
|
||||||
|
Router,
|
||||||
|
UrlTree,
|
||||||
|
} from '@angular/router';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import {
|
||||||
|
Observable,
|
||||||
|
of as observableOf,
|
||||||
|
} from 'rxjs';
|
||||||
|
import { AuthService } from 'src/app/core/auth/auth.service';
|
||||||
|
import { AuthorizationDataService } from 'src/app/core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { FeatureID } from 'src/app/core/data/feature-authorization/feature-id';
|
||||||
|
|
||||||
|
import { APP_DATA_SERVICES_MAP } from '../../../config/app-config.interface';
|
||||||
|
import { ItemDataService } from '../../core/data/item-data.service';
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { getMockTranslateService } from '../../shared/mocks/translate.service.mock';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
|
import { itemPageMoveGuard } from './item-page-move.guard';
|
||||||
|
|
||||||
|
describe('itemPageMoveGuard', () => {
|
||||||
|
let authorizationService: AuthorizationDataService;
|
||||||
|
let authService: AuthService;
|
||||||
|
let router: Router;
|
||||||
|
let route;
|
||||||
|
let parentRoute;
|
||||||
|
let store: Store;
|
||||||
|
let itemService: ItemDataService;
|
||||||
|
let item: Item;
|
||||||
|
let uuid = '1234-abcdef-54321-fedcba';
|
||||||
|
let itemSelfLink = 'test.url/1234-abcdef-54321-fedcba';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
|
||||||
|
store = jasmine.createSpyObj('store', {
|
||||||
|
dispatch: {},
|
||||||
|
pipe: observableOf(true),
|
||||||
|
});
|
||||||
|
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||||
|
isAuthorized: observableOf(true),
|
||||||
|
});
|
||||||
|
router = jasmine.createSpyObj('router', {
|
||||||
|
parseUrl: {},
|
||||||
|
navigateByUrl: undefined,
|
||||||
|
});
|
||||||
|
authService = jasmine.createSpyObj('authService', {
|
||||||
|
isAuthenticated: observableOf(true),
|
||||||
|
});
|
||||||
|
|
||||||
|
parentRoute = {
|
||||||
|
params: {
|
||||||
|
id: '3e1a5327-dabb-41ff-af93-e6cab9d032f0',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
route = {
|
||||||
|
params: {},
|
||||||
|
parent: parentRoute,
|
||||||
|
};
|
||||||
|
item = new Item();
|
||||||
|
item.uuid = uuid;
|
||||||
|
item._links = { self: { href: itemSelfLink } } as any;
|
||||||
|
itemService = jasmine.createSpyObj('itemService', { findById: createSuccessfulRemoteDataObject$(item) });
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||||
|
{ provide: Router, useValue: router },
|
||||||
|
{ provide: AuthService, useValue: authService },
|
||||||
|
{ provide: Store, useValue: store },
|
||||||
|
{ provide: APP_DATA_SERVICES_MAP, useValue: {} },
|
||||||
|
{ provide: TranslateService, useValue: getMockTranslateService() },
|
||||||
|
{ provide: ItemDataService, useValue: itemService },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call authorizationService.isAuthorized with the appropriate arguments', (done) => {
|
||||||
|
const result$ = TestBed.runInInjectionContext(() => {
|
||||||
|
return itemPageMoveGuard(route, { url: 'current-url' } as any);
|
||||||
|
}) as Observable<boolean | UrlTree>;
|
||||||
|
|
||||||
|
result$.subscribe((result) => {
|
||||||
|
expect(authorizationService.isAuthorized).toHaveBeenCalledWith(
|
||||||
|
FeatureID.CanMove,
|
||||||
|
itemSelfLink, // This value is retrieved from the itemDataService.findById's return item's self link
|
||||||
|
undefined, // dsoPageSingleFeatureGuard never provides a function to retrieve a person ID
|
||||||
|
);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
16
src/app/item-page/edit-item-page/item-page-move.guard.ts
Normal file
16
src/app/item-page/edit-item-page/item-page-move.guard.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { CanActivateFn } from '@angular/router';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
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 { itemPageResolver } from '../item-page.resolver';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Guard for preventing unauthorized access to certain {@link Item} pages requiring specific authorizations.
|
||||||
|
* Checks authorization rights for moving items.
|
||||||
|
*/
|
||||||
|
export const itemPageMoveGuard: CanActivateFn =
|
||||||
|
dsoPageSingleFeatureGuard(
|
||||||
|
() => itemPageResolver,
|
||||||
|
() => observableOf(FeatureID.CanMove),
|
||||||
|
);
|
@@ -0,0 +1,94 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import {
|
||||||
|
Router,
|
||||||
|
UrlTree,
|
||||||
|
} from '@angular/router';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import {
|
||||||
|
Observable,
|
||||||
|
of as observableOf,
|
||||||
|
} from 'rxjs';
|
||||||
|
import { AuthService } from 'src/app/core/auth/auth.service';
|
||||||
|
import { AuthorizationDataService } from 'src/app/core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { FeatureID } from 'src/app/core/data/feature-authorization/feature-id';
|
||||||
|
|
||||||
|
import { APP_DATA_SERVICES_MAP } from '../../../config/app-config.interface';
|
||||||
|
import { ItemDataService } from '../../core/data/item-data.service';
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { getMockTranslateService } from '../../shared/mocks/translate.service.mock';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
|
import { itemPagePrivateGuard } from './item-page-private.guard';
|
||||||
|
|
||||||
|
describe('itemPagePrivateGuard', () => {
|
||||||
|
let authorizationService: AuthorizationDataService;
|
||||||
|
let authService: AuthService;
|
||||||
|
let router: Router;
|
||||||
|
let route;
|
||||||
|
let parentRoute;
|
||||||
|
let store: Store;
|
||||||
|
let itemService: ItemDataService;
|
||||||
|
let item: Item;
|
||||||
|
let uuid = '1234-abcdef-54321-fedcba';
|
||||||
|
let itemSelfLink = 'test.url/1234-abcdef-54321-fedcba';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
|
||||||
|
store = jasmine.createSpyObj('store', {
|
||||||
|
dispatch: {},
|
||||||
|
pipe: observableOf(true),
|
||||||
|
});
|
||||||
|
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||||
|
isAuthorized: observableOf(true),
|
||||||
|
});
|
||||||
|
router = jasmine.createSpyObj('router', {
|
||||||
|
parseUrl: {},
|
||||||
|
navigateByUrl: undefined,
|
||||||
|
});
|
||||||
|
authService = jasmine.createSpyObj('authService', {
|
||||||
|
isAuthenticated: observableOf(true),
|
||||||
|
});
|
||||||
|
|
||||||
|
parentRoute = {
|
||||||
|
params: {
|
||||||
|
id: '3e1a5327-dabb-41ff-af93-e6cab9d032f0',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
route = {
|
||||||
|
params: {},
|
||||||
|
parent: parentRoute,
|
||||||
|
};
|
||||||
|
item = new Item();
|
||||||
|
item.uuid = uuid;
|
||||||
|
item._links = { self: { href: itemSelfLink } } as any;
|
||||||
|
itemService = jasmine.createSpyObj('itemService', { findById: createSuccessfulRemoteDataObject$(item) });
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||||
|
{ provide: Router, useValue: router },
|
||||||
|
{ provide: AuthService, useValue: authService },
|
||||||
|
{ provide: Store, useValue: store },
|
||||||
|
{ provide: APP_DATA_SERVICES_MAP, useValue: {} },
|
||||||
|
{ provide: TranslateService, useValue: getMockTranslateService() },
|
||||||
|
{ provide: ItemDataService, useValue: itemService },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call authorizationService.isAuthorized with the appropriate arguments', (done) => {
|
||||||
|
const result$ = TestBed.runInInjectionContext(() => {
|
||||||
|
return itemPagePrivateGuard(route, { url: 'current-url' } as any);
|
||||||
|
}) as Observable<boolean | UrlTree>;
|
||||||
|
|
||||||
|
result$.subscribe((result) => {
|
||||||
|
expect(authorizationService.isAuthorized).toHaveBeenCalledWith(
|
||||||
|
FeatureID.CanMakePrivate,
|
||||||
|
itemSelfLink, // This value is retrieved from the itemDataService.findById's return item's self link
|
||||||
|
undefined, // dsoPageSingleFeatureGuard never provides a function to retrieve a person ID
|
||||||
|
);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
16
src/app/item-page/edit-item-page/item-page-private.guard.ts
Normal file
16
src/app/item-page/edit-item-page/item-page-private.guard.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { CanActivateFn } from '@angular/router';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
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 { itemPageResolver } from '../item-page.resolver';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Guard for preventing unauthorized access to certain {@link Item} pages requiring specific authorizations.
|
||||||
|
* Checks authorization rights for making items private.
|
||||||
|
*/
|
||||||
|
export const itemPagePrivateGuard: CanActivateFn =
|
||||||
|
dsoPageSingleFeatureGuard(
|
||||||
|
() => itemPageResolver,
|
||||||
|
() => observableOf(FeatureID.CanMakePrivate),
|
||||||
|
);
|
@@ -1,43 +1,16 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { CanActivateFn } from '@angular/router';
|
||||||
import {
|
import { of as observableOf } from 'rxjs';
|
||||||
ActivatedRouteSnapshot,
|
|
||||||
ResolveFn,
|
|
||||||
Router,
|
|
||||||
RouterStateSnapshot,
|
|
||||||
} from '@angular/router';
|
|
||||||
import {
|
|
||||||
Observable,
|
|
||||||
of as observableOf,
|
|
||||||
} from 'rxjs';
|
|
||||||
|
|
||||||
import { AuthService } from '../../core/auth/auth.service';
|
import { dsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard';
|
||||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
|
||||||
import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard';
|
|
||||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
|
||||||
import { Item } from '../../core/shared/item.model';
|
|
||||||
import { itemPageResolver } from '../item-page.resolver';
|
import { itemPageResolver } from '../item-page.resolver';
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
/**
|
/**
|
||||||
* Guard for preventing unauthorized access to certain {@link Item} pages requiring DOI registration rights
|
* Guard for preventing unauthorized access to certain {@link Item} pages requiring DOI registration rights
|
||||||
|
* Check DOI registration authorization rights
|
||||||
*/
|
*/
|
||||||
export class ItemPageRegisterDoiGuard extends DsoPageSingleFeatureGuard<Item> {
|
export const itemPageRegisterDoiGuard: CanActivateFn =
|
||||||
|
dsoPageSingleFeatureGuard(
|
||||||
protected resolver: ResolveFn<RemoteData<Item>> = itemPageResolver;
|
() => itemPageResolver,
|
||||||
|
() => observableOf(FeatureID.CanRegisterDOI),
|
||||||
constructor(protected authorizationService: AuthorizationDataService,
|
);
|
||||||
protected router: Router,
|
|
||||||
protected authService: AuthService) {
|
|
||||||
super(authorizationService, router, authService);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check DOI registration authorization rights
|
|
||||||
*/
|
|
||||||
getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
|
||||||
return observableOf(FeatureID.CanRegisterDOI);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -1,43 +1,16 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { CanActivateFn } from '@angular/router';
|
||||||
import {
|
import { of as observableOf } from 'rxjs';
|
||||||
ActivatedRouteSnapshot,
|
|
||||||
ResolveFn,
|
|
||||||
Router,
|
|
||||||
RouterStateSnapshot,
|
|
||||||
} from '@angular/router';
|
|
||||||
import {
|
|
||||||
Observable,
|
|
||||||
of as observableOf,
|
|
||||||
} from 'rxjs';
|
|
||||||
|
|
||||||
import { AuthService } from '../../core/auth/auth.service';
|
import { dsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard';
|
||||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
|
||||||
import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard';
|
|
||||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
|
||||||
import { Item } from '../../core/shared/item.model';
|
|
||||||
import { itemPageResolver } from '../item-page.resolver';
|
import { itemPageResolver } from '../item-page.resolver';
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
/**
|
/**
|
||||||
* Guard for preventing unauthorized access to certain {@link Item} pages requiring reinstate rights
|
* Guard for preventing unauthorized access to certain {@link Item} pages requiring reinstate rights
|
||||||
|
* Check reinstate authorization rights
|
||||||
*/
|
*/
|
||||||
export class ItemPageReinstateGuard extends DsoPageSingleFeatureGuard<Item> {
|
export const itemPageReinstateGuard: CanActivateFn =
|
||||||
|
dsoPageSingleFeatureGuard(
|
||||||
protected resolver: ResolveFn<RemoteData<Item>> = itemPageResolver;
|
() => itemPageResolver,
|
||||||
|
() => observableOf(FeatureID.ReinstateItem),
|
||||||
constructor(protected authorizationService: AuthorizationDataService,
|
);
|
||||||
protected router: Router,
|
|
||||||
protected authService: AuthService) {
|
|
||||||
super(authorizationService, router, authService);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check reinstate authorization rights
|
|
||||||
*/
|
|
||||||
getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
|
||||||
return observableOf(FeatureID.ReinstateItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -1,43 +1,16 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { CanActivateFn } from '@angular/router';
|
||||||
import {
|
import { of as observableOf } from 'rxjs';
|
||||||
ActivatedRouteSnapshot,
|
|
||||||
ResolveFn,
|
|
||||||
Router,
|
|
||||||
RouterStateSnapshot,
|
|
||||||
} from '@angular/router';
|
|
||||||
import {
|
|
||||||
Observable,
|
|
||||||
of as observableOf,
|
|
||||||
} from 'rxjs';
|
|
||||||
|
|
||||||
import { AuthService } from '../../core/auth/auth.service';
|
import { dsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard';
|
||||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
|
||||||
import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard';
|
|
||||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
|
||||||
import { Item } from '../../core/shared/item.model';
|
|
||||||
import { itemPageResolver } from '../item-page.resolver';
|
import { itemPageResolver } from '../item-page.resolver';
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
/**
|
/**
|
||||||
* Guard for preventing unauthorized access to certain {@link Item} pages requiring manage relationships rights
|
* Guard for preventing unauthorized access to certain {@link Item} pages requiring manage relationships rights
|
||||||
|
* Check manage relationships authorization rights
|
||||||
*/
|
*/
|
||||||
export class ItemPageRelationshipsGuard extends DsoPageSingleFeatureGuard<Item> {
|
export const itemPageRelationshipsGuard: CanActivateFn =
|
||||||
|
dsoPageSingleFeatureGuard(
|
||||||
protected resolver: ResolveFn<RemoteData<Item>> = itemPageResolver;
|
() => itemPageResolver,
|
||||||
|
() => observableOf(FeatureID.CanManageRelationships),
|
||||||
constructor(protected authorizationService: AuthorizationDataService,
|
);
|
||||||
protected router: Router,
|
|
||||||
protected authService: AuthService) {
|
|
||||||
super(authorizationService, router, authService);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check manage relationships authorization rights
|
|
||||||
*/
|
|
||||||
getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
|
||||||
return observableOf(FeatureID.CanManageRelationships);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -1,44 +1,17 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { CanActivateFn } from '@angular/router';
|
||||||
import {
|
import { of as observableOf } from 'rxjs';
|
||||||
ActivatedRouteSnapshot,
|
|
||||||
ResolveFn,
|
|
||||||
Router,
|
|
||||||
RouterStateSnapshot,
|
|
||||||
} from '@angular/router';
|
|
||||||
import {
|
|
||||||
Observable,
|
|
||||||
of as observableOf,
|
|
||||||
} from 'rxjs';
|
|
||||||
|
|
||||||
import { AuthService } from '../../core/auth/auth.service';
|
import { dsoPageSomeFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard';
|
||||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
|
||||||
import { DsoPageSomeFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard';
|
|
||||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
|
||||||
import { Item } from '../../core/shared/item.model';
|
|
||||||
import { itemPageResolver } from '../item-page.resolver';
|
import { itemPageResolver } from '../item-page.resolver';
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
/**
|
/**
|
||||||
* Guard for preventing unauthorized access to certain {@link Item} pages requiring any of the rights required for
|
* Guard for preventing unauthorized access to certain {@link Item} pages requiring any of the rights required for
|
||||||
* the status page
|
* the status page
|
||||||
|
* Check authorization rights
|
||||||
*/
|
*/
|
||||||
export class ItemPageStatusGuard extends DsoPageSomeFeatureGuard<Item> {
|
export const itemPageStatusGuard: CanActivateFn =
|
||||||
|
dsoPageSomeFeatureGuard(
|
||||||
protected resolver: ResolveFn<RemoteData<Item>> = itemPageResolver;
|
() => itemPageResolver,
|
||||||
|
() => observableOf([FeatureID.CanManageMappings, FeatureID.WithdrawItem, FeatureID.ReinstateItem, FeatureID.CanManagePolicies, FeatureID.CanMakePrivate, FeatureID.CanDelete, FeatureID.CanMove, FeatureID.CanRegisterDOI]),
|
||||||
constructor(protected authorizationService: AuthorizationDataService,
|
);
|
||||||
protected router: Router,
|
|
||||||
protected authService: AuthService) {
|
|
||||||
super(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, FeatureID.CanRegisterDOI]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -1,43 +1,16 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { CanActivateFn } from '@angular/router';
|
||||||
import {
|
import { of as observableOf } from 'rxjs';
|
||||||
ActivatedRouteSnapshot,
|
|
||||||
ResolveFn,
|
|
||||||
Router,
|
|
||||||
RouterStateSnapshot,
|
|
||||||
} from '@angular/router';
|
|
||||||
import {
|
|
||||||
Observable,
|
|
||||||
of as observableOf,
|
|
||||||
} from 'rxjs';
|
|
||||||
|
|
||||||
import { AuthService } from '../../core/auth/auth.service';
|
import { dsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard';
|
||||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
|
||||||
import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard';
|
|
||||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
|
||||||
import { Item } from '../../core/shared/item.model';
|
|
||||||
import { itemPageResolver } from '../item-page.resolver';
|
import { itemPageResolver } from '../item-page.resolver';
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
/**
|
/**
|
||||||
* Guard for preventing unauthorized access to certain {@link Item} pages requiring manage versions rights
|
* Guard for preventing unauthorized access to certain {@link Item} pages requiring manage versions rights
|
||||||
|
* Check manage versions authorization rights
|
||||||
*/
|
*/
|
||||||
export class ItemPageVersionHistoryGuard extends DsoPageSingleFeatureGuard<Item> {
|
export const itemPageVersionHistoryGuard: CanActivateFn =
|
||||||
|
dsoPageSingleFeatureGuard(
|
||||||
protected resolver: ResolveFn<RemoteData<Item>> = itemPageResolver;
|
() => itemPageResolver,
|
||||||
|
() => observableOf(FeatureID.CanManageVersions),
|
||||||
constructor(protected authorizationService: AuthorizationDataService,
|
);
|
||||||
protected router: Router,
|
|
||||||
protected authService: AuthService) {
|
|
||||||
super(authorizationService, router, authService);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check manage versions authorization rights
|
|
||||||
*/
|
|
||||||
getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
|
||||||
return observableOf(FeatureID.CanManageVersions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -1,43 +1,16 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { CanActivateFn } from '@angular/router';
|
||||||
import {
|
import { of as observableOf } from 'rxjs';
|
||||||
ActivatedRouteSnapshot,
|
|
||||||
ResolveFn,
|
|
||||||
Router,
|
|
||||||
RouterStateSnapshot,
|
|
||||||
} from '@angular/router';
|
|
||||||
import {
|
|
||||||
Observable,
|
|
||||||
of as observableOf,
|
|
||||||
} from 'rxjs';
|
|
||||||
|
|
||||||
import { AuthService } from '../../core/auth/auth.service';
|
import { dsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard';
|
||||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
|
||||||
import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard';
|
|
||||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
|
||||||
import { Item } from '../../core/shared/item.model';
|
|
||||||
import { itemPageResolver } from '../item-page.resolver';
|
import { itemPageResolver } from '../item-page.resolver';
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
/**
|
/**
|
||||||
* Guard for preventing unauthorized access to certain {@link Item} pages requiring withdraw rights
|
* Guard for preventing unauthorized access to certain {@link Item} pages requiring withdraw rights
|
||||||
|
* Check withdraw authorization rights
|
||||||
*/
|
*/
|
||||||
export class ItemPageWithdrawGuard extends DsoPageSingleFeatureGuard<Item> {
|
export const itemPageWithdrawGuard: CanActivateFn =
|
||||||
|
dsoPageSingleFeatureGuard(
|
||||||
protected resolver: ResolveFn<RemoteData<Item>> = itemPageResolver;
|
() => itemPageResolver,
|
||||||
|
() => observableOf(FeatureID.WithdrawItem),
|
||||||
constructor(protected authorizationService: AuthorizationDataService,
|
);
|
||||||
protected router: Router,
|
|
||||||
protected authService: AuthService) {
|
|
||||||
super(authorizationService, router, authService);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check withdraw authorization rights
|
|
||||||
*/
|
|
||||||
getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
|
||||||
return observableOf(FeatureID.WithdrawItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -0,0 +1,412 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
import { EntityTypeDataService } from '../../../core/data/entity-type-data.service';
|
||||||
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
|
import { FieldChangeType } from '../../../core/data/object-updates/field-change-type.model';
|
||||||
|
import { FieldUpdate } from '../../../core/data/object-updates/field-update.model';
|
||||||
|
import { FieldUpdates } from '../../../core/data/object-updates/field-updates.model';
|
||||||
|
import {
|
||||||
|
DeleteRelationship,
|
||||||
|
RelationshipIdentifiable,
|
||||||
|
} from '../../../core/data/object-updates/object-updates.reducer';
|
||||||
|
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
||||||
|
import { RelationshipDataService } from '../../../core/data/relationship-data.service';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import { ItemType } from '../../../core/shared/item-relationships/item-type.model';
|
||||||
|
import { Relationship } from '../../../core/shared/item-relationships/relationship.model';
|
||||||
|
import { RelationshipType } from '../../../core/shared/item-relationships/relationship-type.model';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import {
|
||||||
|
createFailedRemoteDataObject,
|
||||||
|
createSuccessfulRemoteDataObject,
|
||||||
|
createSuccessfulRemoteDataObject$,
|
||||||
|
} from '../../../shared/remote-data.utils';
|
||||||
|
import { EntityTypeDataServiceStub } from '../../../shared/testing/entity-type-data.service.stub';
|
||||||
|
import { ItemDataServiceStub } from '../../../shared/testing/item-data.service.stub';
|
||||||
|
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
||||||
|
import { ObjectUpdatesServiceStub } from '../../../shared/testing/object-updates.service.stub';
|
||||||
|
import { RelationshipDataServiceStub } from '../../../shared/testing/relationship-data.service.stub';
|
||||||
|
import { EditItemRelationshipsService } from './edit-item-relationships.service';
|
||||||
|
|
||||||
|
describe('EditItemRelationshipsService', () => {
|
||||||
|
let service: EditItemRelationshipsService;
|
||||||
|
|
||||||
|
let itemService: ItemDataServiceStub;
|
||||||
|
let objectUpdatesService: ObjectUpdatesServiceStub;
|
||||||
|
let notificationsService: NotificationsServiceStub;
|
||||||
|
let relationshipService: RelationshipDataServiceStub;
|
||||||
|
let entityTypeDataService: EntityTypeDataServiceStub;
|
||||||
|
|
||||||
|
let currentItem: Item;
|
||||||
|
|
||||||
|
let relationshipItem1: Item;
|
||||||
|
let relationshipIdentifiable1: RelationshipIdentifiable;
|
||||||
|
let relationship1: Relationship;
|
||||||
|
|
||||||
|
let relationshipItem2: Item;
|
||||||
|
let relationshipIdentifiable2: RelationshipIdentifiable;
|
||||||
|
let relationship2: Relationship;
|
||||||
|
|
||||||
|
let orgUnitType: ItemType;
|
||||||
|
let orgUnitToOrgUnitType: RelationshipType;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
itemService = new ItemDataServiceStub();
|
||||||
|
objectUpdatesService = new ObjectUpdatesServiceStub();
|
||||||
|
notificationsService = new NotificationsServiceStub();
|
||||||
|
relationshipService = new RelationshipDataServiceStub();
|
||||||
|
entityTypeDataService = new EntityTypeDataServiceStub();
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: ItemDataService, useValue: itemService },
|
||||||
|
{ provide: ObjectUpdatesService, useValue: objectUpdatesService },
|
||||||
|
{ provide: NotificationsService, useValue: notificationsService },
|
||||||
|
{ provide: RelationshipDataService, useValue: relationshipService },
|
||||||
|
{ provide: EntityTypeDataService, useValue: entityTypeDataService },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
service = TestBed.inject(EditItemRelationshipsService);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
currentItem = Object.assign(new Item(), {
|
||||||
|
uuid: uuidv4(),
|
||||||
|
metadata: {
|
||||||
|
'dspace.entity.type': 'OrgUnit',
|
||||||
|
},
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'selfLink1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
relationshipItem1 = Object.assign(new Item(), {
|
||||||
|
uuid: uuidv4(),
|
||||||
|
metadata: {
|
||||||
|
'dspace.entity.type': 'OrgUnit',
|
||||||
|
},
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'selfLink2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
relationshipIdentifiable1 = {
|
||||||
|
originalItem: currentItem,
|
||||||
|
relatedItem: relationshipItem1,
|
||||||
|
type: orgUnitToOrgUnitType,
|
||||||
|
uuid: `1-${relationshipItem1.uuid}`,
|
||||||
|
} as RelationshipIdentifiable;
|
||||||
|
relationship1 = Object.assign(new Relationship(), {
|
||||||
|
_links: {
|
||||||
|
leftItem: currentItem._links.self,
|
||||||
|
rightItem: relationshipItem1._links.self,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
relationshipItem2 = Object.assign(new Item(), {
|
||||||
|
uuid: uuidv4(),
|
||||||
|
metadata: {
|
||||||
|
'dspace.entity.type': 'OrgUnit',
|
||||||
|
},
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'selfLink3',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
relationshipIdentifiable2 = {
|
||||||
|
originalItem: currentItem,
|
||||||
|
relatedItem: relationshipItem2,
|
||||||
|
type: orgUnitToOrgUnitType,
|
||||||
|
uuid: `1-${relationshipItem2.uuid}`,
|
||||||
|
} as RelationshipIdentifiable;
|
||||||
|
relationship2 = Object.assign(new Relationship(), {
|
||||||
|
_links: {
|
||||||
|
leftItem: currentItem._links.self,
|
||||||
|
rightItem: relationshipItem2._links.self,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
orgUnitType = Object.assign(new ItemType(), {
|
||||||
|
id: '2',
|
||||||
|
label: 'OrgUnit',
|
||||||
|
});
|
||||||
|
orgUnitToOrgUnitType = Object.assign(new RelationshipType(), {
|
||||||
|
id: '1',
|
||||||
|
leftMaxCardinality: null,
|
||||||
|
leftMinCardinality: 0,
|
||||||
|
leftType: createSuccessfulRemoteDataObject$(orgUnitType),
|
||||||
|
leftwardType: 'isOrgUnitOfOrgUnit',
|
||||||
|
rightMaxCardinality: null,
|
||||||
|
rightMinCardinality: 0,
|
||||||
|
rightType: createSuccessfulRemoteDataObject$(orgUnitType),
|
||||||
|
rightwardType: 'isOrgUnitOfOrgUnit',
|
||||||
|
uuid: 'relationshiptype-1',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('submit', () => {
|
||||||
|
let fieldUpdateAddRelationship1: FieldUpdate;
|
||||||
|
let fieldUpdateRemoveRelationship2: FieldUpdate;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fieldUpdateAddRelationship1 = {
|
||||||
|
changeType: FieldChangeType.ADD,
|
||||||
|
field: relationshipIdentifiable1,
|
||||||
|
};
|
||||||
|
fieldUpdateRemoveRelationship2 = {
|
||||||
|
changeType: FieldChangeType.REMOVE,
|
||||||
|
field: relationshipIdentifiable2,
|
||||||
|
};
|
||||||
|
|
||||||
|
spyOn(service, 'addRelationship').withArgs(relationshipIdentifiable1).and.returnValue(createSuccessfulRemoteDataObject$(relationship1));
|
||||||
|
spyOn(service, 'deleteRelationship').withArgs(relationshipIdentifiable2 as DeleteRelationship).and.returnValue(createSuccessfulRemoteDataObject$({}));
|
||||||
|
spyOn(itemService, 'invalidateByHref').and.callThrough();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support performing multiple relationships manipulations in one submit() call', () => {
|
||||||
|
spyOn(objectUpdatesService, 'getFieldUpdates').and.returnValue(observableOf({
|
||||||
|
[`1-${relationshipItem1.uuid}`]: fieldUpdateAddRelationship1,
|
||||||
|
[`1-${relationshipItem2.uuid}`]: fieldUpdateRemoveRelationship2,
|
||||||
|
} as FieldUpdates));
|
||||||
|
service.submit(currentItem, `/entities/orgunit/${currentItem.uuid}/edit/relationships`);
|
||||||
|
|
||||||
|
expect(service.addRelationship).toHaveBeenCalledWith(relationshipIdentifiable1);
|
||||||
|
expect(service.deleteRelationship).toHaveBeenCalledWith(relationshipIdentifiable2 as DeleteRelationship);
|
||||||
|
|
||||||
|
expect(itemService.invalidateByHref).toHaveBeenCalledWith(currentItem.self);
|
||||||
|
expect(itemService.invalidateByHref).toHaveBeenCalledWith(relationshipItem1.self);
|
||||||
|
expect(itemService.invalidateByHref).toHaveBeenCalledWith(relationshipItem2.self);
|
||||||
|
|
||||||
|
expect(notificationsService.success).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteRelationship', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(relationshipService, 'deleteRelationship').and.callThrough();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass "all" as copyVirtualMetadata when the user want to keep the data on both sides', () => {
|
||||||
|
service.deleteRelationship({
|
||||||
|
uuid: relationshipItem1.uuid,
|
||||||
|
keepLeftVirtualMetadata: true,
|
||||||
|
keepRightVirtualMetadata: true,
|
||||||
|
} as DeleteRelationship);
|
||||||
|
|
||||||
|
expect(relationshipService.deleteRelationship).toHaveBeenCalledWith(relationshipItem1.uuid, 'all', false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass "left" as copyVirtualMetadata when the user only want to keep the data on the left side', () => {
|
||||||
|
service.deleteRelationship({
|
||||||
|
uuid: relationshipItem1.uuid,
|
||||||
|
keepLeftVirtualMetadata: true,
|
||||||
|
keepRightVirtualMetadata: false,
|
||||||
|
} as DeleteRelationship);
|
||||||
|
|
||||||
|
expect(relationshipService.deleteRelationship).toHaveBeenCalledWith(relationshipItem1.uuid, 'left', false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass "right" as copyVirtualMetadata when the user only want to keep the data on the right side', () => {
|
||||||
|
service.deleteRelationship({
|
||||||
|
uuid: relationshipItem1.uuid,
|
||||||
|
keepLeftVirtualMetadata: false,
|
||||||
|
keepRightVirtualMetadata: true,
|
||||||
|
} as DeleteRelationship);
|
||||||
|
|
||||||
|
expect(relationshipService.deleteRelationship).toHaveBeenCalledWith(relationshipItem1.uuid, 'right', false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass "none" as copyVirtualMetadata when the user doesn\'t want to keep the virtual metadata', () => {
|
||||||
|
service.deleteRelationship({
|
||||||
|
uuid: relationshipItem1.uuid,
|
||||||
|
keepLeftVirtualMetadata: false,
|
||||||
|
keepRightVirtualMetadata: false,
|
||||||
|
} as DeleteRelationship);
|
||||||
|
|
||||||
|
expect(relationshipService.deleteRelationship).toHaveBeenCalledWith(relationshipItem1.uuid, 'none', false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('addRelationship', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(relationshipService, 'addRelationship').and.callThrough();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call the addRelationship from relationshipService correctly when original item is on the right', () => {
|
||||||
|
service.addRelationship({
|
||||||
|
originalItem: currentItem,
|
||||||
|
originalIsLeft: false,
|
||||||
|
relatedItem: relationshipItem1,
|
||||||
|
type: orgUnitToOrgUnitType,
|
||||||
|
uuid: `1-${relationshipItem1.uuid}`,
|
||||||
|
} as RelationshipIdentifiable);
|
||||||
|
expect(relationshipService.addRelationship).toHaveBeenCalledWith(orgUnitToOrgUnitType.id, relationshipItem1, currentItem, undefined, null, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call the addRelationship from relationshipService correctly when original item is on the left', () => {
|
||||||
|
service.addRelationship({
|
||||||
|
originalItem: currentItem,
|
||||||
|
originalIsLeft: true,
|
||||||
|
relatedItem: relationshipItem1,
|
||||||
|
type: orgUnitToOrgUnitType,
|
||||||
|
uuid: `1-${relationshipItem1.uuid}`,
|
||||||
|
} as RelationshipIdentifiable);
|
||||||
|
|
||||||
|
expect(relationshipService.addRelationship).toHaveBeenCalledWith(orgUnitToOrgUnitType.id, currentItem, relationshipItem1, null, undefined, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isProvidedItemTypeLeftType', () => {
|
||||||
|
it('should return true if the provided item corresponds to the left type of the relationship', (done) => {
|
||||||
|
const relationshipType = Object.assign(new RelationshipType(), {
|
||||||
|
leftType: createSuccessfulRemoteDataObject$({ id: 'leftType' }),
|
||||||
|
rightType: createSuccessfulRemoteDataObject$({ id: 'rightType' }),
|
||||||
|
});
|
||||||
|
const itemType = Object.assign(new ItemType(), { id: 'leftType' } );
|
||||||
|
const item = Object.assign(new Item(), { uuid: 'item-uuid' });
|
||||||
|
|
||||||
|
const result = service.isProvidedItemTypeLeftType(relationshipType, itemType, item);
|
||||||
|
result.subscribe((resultValue) => {
|
||||||
|
expect(resultValue).toBeTrue();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false if the provided item corresponds to the right type of the relationship', (done) => {
|
||||||
|
const relationshipType = Object.assign(new RelationshipType(), {
|
||||||
|
leftType: createSuccessfulRemoteDataObject$({ id: 'leftType' }),
|
||||||
|
rightType: createSuccessfulRemoteDataObject$({ id: 'rightType' }),
|
||||||
|
});
|
||||||
|
const itemType = Object.assign(new ItemType(), { id: 'rightType' } );
|
||||||
|
const item = Object.assign(new Item(), { uuid: 'item-uuid' });
|
||||||
|
|
||||||
|
const result = service.isProvidedItemTypeLeftType(relationshipType, itemType, item);
|
||||||
|
result.subscribe((resultValue) => {
|
||||||
|
expect(resultValue).toBeFalse();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined if the provided item corresponds does not match any of the relationship types', (done) => {
|
||||||
|
const relationshipType = Object.assign(new RelationshipType(), {
|
||||||
|
leftType: createSuccessfulRemoteDataObject$({ id: 'leftType' }),
|
||||||
|
rightType: createSuccessfulRemoteDataObject$({ id: 'rightType' }),
|
||||||
|
});
|
||||||
|
const itemType = Object.assign(new ItemType(), { id: 'something-else' } );
|
||||||
|
const item = Object.assign(new Item(), { uuid: 'item-uuid' });
|
||||||
|
|
||||||
|
const result = service.isProvidedItemTypeLeftType(relationshipType, itemType, item);
|
||||||
|
result.subscribe((resultValue) => {
|
||||||
|
expect(resultValue).toBeUndefined();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('relationshipMatchesBothSameTypes', () => {
|
||||||
|
it('should return true if both left and right type of the relationship type are the same and match the provided itemtype', (done) => {
|
||||||
|
const relationshipType = Object.assign(new RelationshipType(), {
|
||||||
|
leftType: createSuccessfulRemoteDataObject$({ id: 'sameType' }),
|
||||||
|
rightType: createSuccessfulRemoteDataObject$({ id:'sameType' }),
|
||||||
|
leftwardType: 'isDepartmentOfDivision',
|
||||||
|
rightwardType: 'isDivisionOfDepartment',
|
||||||
|
});
|
||||||
|
const itemType = Object.assign(new ItemType(), { id: 'sameType' } );
|
||||||
|
|
||||||
|
const result = service.shouldDisplayBothRelationshipSides(relationshipType, itemType);
|
||||||
|
result.subscribe((resultValue) => {
|
||||||
|
expect(resultValue).toBeTrue();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should return false if both left and right type of the relationship type are the same and match the provided itemtype but the leftwardType & rightwardType is identical', (done) => {
|
||||||
|
const relationshipType = Object.assign(new RelationshipType(), {
|
||||||
|
leftType: createSuccessfulRemoteDataObject$({ id: 'sameType' }),
|
||||||
|
rightType: createSuccessfulRemoteDataObject$({ id: 'sameType' }),
|
||||||
|
leftwardType: 'isOrgUnitOfOrgUnit',
|
||||||
|
rightwardType: 'isOrgUnitOfOrgUnit',
|
||||||
|
});
|
||||||
|
const itemType = Object.assign(new ItemType(), { id: 'sameType' });
|
||||||
|
|
||||||
|
const result = service.shouldDisplayBothRelationshipSides(relationshipType, itemType);
|
||||||
|
result.subscribe((resultValue) => {
|
||||||
|
expect(resultValue).toBeFalse();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should return false if both left and right type of the relationship type are the same and do not match the provided itemtype', (done) => {
|
||||||
|
const relationshipType = Object.assign(new RelationshipType(), {
|
||||||
|
leftType: createSuccessfulRemoteDataObject$({ id: 'sameType' }),
|
||||||
|
rightType: createSuccessfulRemoteDataObject$({ id: 'sameType' }),
|
||||||
|
leftwardType: 'isDepartmentOfDivision',
|
||||||
|
rightwardType: 'isDivisionOfDepartment',
|
||||||
|
});
|
||||||
|
const itemType = Object.assign(new ItemType(), { id: 'something-else' } );
|
||||||
|
|
||||||
|
const result = service.shouldDisplayBothRelationshipSides(relationshipType, itemType);
|
||||||
|
result.subscribe((resultValue) => {
|
||||||
|
expect(resultValue).toBeFalse();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should return false if both left and right type of the relationship type are different', (done) => {
|
||||||
|
const relationshipType = Object.assign(new RelationshipType(), {
|
||||||
|
leftType: createSuccessfulRemoteDataObject$({ id: 'leftType' }),
|
||||||
|
rightType: createSuccessfulRemoteDataObject$({ id: 'rightType' }),
|
||||||
|
leftwardType: 'isAuthorOfPublication',
|
||||||
|
rightwardType: 'isPublicationOfAuthor',
|
||||||
|
});
|
||||||
|
const itemType = Object.assign(new ItemType(), { id: 'leftType' } );
|
||||||
|
|
||||||
|
const result = service.shouldDisplayBothRelationshipSides(relationshipType, itemType);
|
||||||
|
result.subscribe((resultValue) => {
|
||||||
|
expect(resultValue).toBeFalse();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('displayNotifications', () => {
|
||||||
|
it('should show one success notification when multiple requests succeeded', () => {
|
||||||
|
service.displayNotifications([
|
||||||
|
createSuccessfulRemoteDataObject({}),
|
||||||
|
createSuccessfulRemoteDataObject({}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(notificationsService.success).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show one success notification even when some requests failed', () => {
|
||||||
|
service.displayNotifications([
|
||||||
|
createSuccessfulRemoteDataObject({}),
|
||||||
|
createFailedRemoteDataObject('Request Failed'),
|
||||||
|
createSuccessfulRemoteDataObject({}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(notificationsService.success).toHaveBeenCalledTimes(1);
|
||||||
|
expect(notificationsService.error).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show a separate error notification for each failed request', () => {
|
||||||
|
service.displayNotifications([
|
||||||
|
createSuccessfulRemoteDataObject({}),
|
||||||
|
createFailedRemoteDataObject('Request Failed 1'),
|
||||||
|
createSuccessfulRemoteDataObject({}),
|
||||||
|
createFailedRemoteDataObject('Request Failed 2'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(notificationsService.success).toHaveBeenCalledTimes(1);
|
||||||
|
expect(notificationsService.error).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,267 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import {
|
||||||
|
BehaviorSubject,
|
||||||
|
combineLatest as observableCombineLatest,
|
||||||
|
EMPTY,
|
||||||
|
Observable,
|
||||||
|
Subscription,
|
||||||
|
} from 'rxjs';
|
||||||
|
import {
|
||||||
|
concatMap,
|
||||||
|
map,
|
||||||
|
switchMap,
|
||||||
|
take,
|
||||||
|
toArray,
|
||||||
|
} from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { EntityTypeDataService } from '../../../core/data/entity-type-data.service';
|
||||||
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
|
import { FieldChangeType } from '../../../core/data/object-updates/field-change-type.model';
|
||||||
|
import { FieldUpdate } from '../../../core/data/object-updates/field-update.model';
|
||||||
|
import { FieldUpdates } from '../../../core/data/object-updates/field-updates.model';
|
||||||
|
import {
|
||||||
|
DeleteRelationship,
|
||||||
|
RelationshipIdentifiable,
|
||||||
|
} from '../../../core/data/object-updates/object-updates.reducer';
|
||||||
|
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
||||||
|
import { RelationshipDataService } from '../../../core/data/relationship-data.service';
|
||||||
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import { ItemType } from '../../../core/shared/item-relationships/item-type.model';
|
||||||
|
import { Relationship } from '../../../core/shared/item-relationships/relationship.model';
|
||||||
|
import { RelationshipType } from '../../../core/shared/item-relationships/relationship-type.model';
|
||||||
|
import { NoContent } from '../../../core/shared/NoContent.model';
|
||||||
|
import {
|
||||||
|
getFirstSucceededRemoteData,
|
||||||
|
getRemoteDataPayload,
|
||||||
|
} from '../../../core/shared/operators';
|
||||||
|
import { hasValue } from '../../../shared/empty.util';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class EditItemRelationshipsService {
|
||||||
|
public notificationsPrefix = 'item.edit.relationships.notifications.';
|
||||||
|
|
||||||
|
public isSaving$: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public itemService: ItemDataService,
|
||||||
|
public objectUpdatesService: ObjectUpdatesService,
|
||||||
|
public notificationsService: NotificationsService,
|
||||||
|
protected modalService: NgbModal,
|
||||||
|
public relationshipService: RelationshipDataService,
|
||||||
|
public entityTypeService: EntityTypeDataService,
|
||||||
|
public translateService: TranslateService,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the currently selected related items back to relationships and send a delete request for each of the relationships found
|
||||||
|
* Make sure the lists are refreshed afterwards and notifications are sent for success and errors
|
||||||
|
*/
|
||||||
|
public submit(item: Item, url: string): void {
|
||||||
|
this.isSaving$.next(true);
|
||||||
|
this.objectUpdatesService.getFieldUpdates(url, [], true).pipe(
|
||||||
|
map((fieldUpdates: FieldUpdates) =>
|
||||||
|
Object.values(fieldUpdates)
|
||||||
|
.filter((fieldUpdate: FieldUpdate) => hasValue(fieldUpdate))
|
||||||
|
.filter((fieldUpdate: FieldUpdate) => fieldUpdate.changeType === FieldChangeType.ADD || fieldUpdate.changeType === FieldChangeType.REMOVE),
|
||||||
|
),
|
||||||
|
take(1),
|
||||||
|
// emit each update in the array separately
|
||||||
|
switchMap((updates) => updates),
|
||||||
|
// process each update one by one, while waiting for the previous to finish
|
||||||
|
concatMap((update: FieldUpdate) => {
|
||||||
|
if (update.changeType === FieldChangeType.REMOVE) {
|
||||||
|
return this.deleteRelationship(update.field as DeleteRelationship).pipe(
|
||||||
|
take(1),
|
||||||
|
switchMap((deleteRD: RemoteData<NoContent>) => {
|
||||||
|
if (deleteRD.hasSucceeded) {
|
||||||
|
return this.itemService.invalidateByHref((update.field as DeleteRelationship).relatedItem._links.self.href).pipe(
|
||||||
|
map(() => deleteRD),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return [deleteRD];
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else if (update.changeType === FieldChangeType.ADD) {
|
||||||
|
return this.addRelationship(update.field as RelationshipIdentifiable).pipe(
|
||||||
|
take(1),
|
||||||
|
switchMap((relationshipRD: RemoteData<Relationship>) => {
|
||||||
|
if (relationshipRD.hasSucceeded) {
|
||||||
|
// Set the newly related item to stale, so its relationships will update to include
|
||||||
|
// the new one. Only set the current item to stale at the very end so we only do it
|
||||||
|
// once
|
||||||
|
const { leftItem, rightItem } = relationshipRD.payload._links;
|
||||||
|
if (leftItem.href === item.self) {
|
||||||
|
return this.itemService.invalidateByHref(rightItem.href).pipe(
|
||||||
|
// when it's invalidated, emit the original relationshipRD for use in the pipe below
|
||||||
|
map(() => relationshipRD),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return this.itemService.invalidateByHref(leftItem.href).pipe(
|
||||||
|
// when it's invalidated, emit the original relationshipRD for use in the pipe below
|
||||||
|
map(() => relationshipRD),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return [relationshipRD];
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return EMPTY;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
toArray(),
|
||||||
|
switchMap((responses) => {
|
||||||
|
// once all relationships are made and all related items have been invalidated, invalidate
|
||||||
|
// the current item
|
||||||
|
return this.itemService.invalidateByHref(item.self).pipe(
|
||||||
|
map(() => responses),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
).subscribe((responses) => {
|
||||||
|
if (responses.length > 0) {
|
||||||
|
this.initializeOriginalFields(item, url);
|
||||||
|
this.displayNotifications(responses);
|
||||||
|
this.modalService.dismissAll();
|
||||||
|
this.isSaving$.next(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends all initial values of this item to the object updates service
|
||||||
|
*/
|
||||||
|
public initializeOriginalFields(item: Item, url: string): Subscription {
|
||||||
|
return this.relationshipService.getRelatedItems(item).pipe(
|
||||||
|
take(1),
|
||||||
|
).subscribe((items: Item[]) => {
|
||||||
|
this.objectUpdatesService.initialize(url, items, item.lastModified);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteRelationship(deleteRelationship: DeleteRelationship): Observable<RemoteData<NoContent>> {
|
||||||
|
let copyVirtualMetadata: string;
|
||||||
|
if (deleteRelationship.keepLeftVirtualMetadata && deleteRelationship.keepRightVirtualMetadata) {
|
||||||
|
copyVirtualMetadata = 'all';
|
||||||
|
} else if (deleteRelationship.keepLeftVirtualMetadata) {
|
||||||
|
copyVirtualMetadata = 'left';
|
||||||
|
} else if (deleteRelationship.keepRightVirtualMetadata) {
|
||||||
|
copyVirtualMetadata = 'right';
|
||||||
|
} else {
|
||||||
|
copyVirtualMetadata = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.relationshipService.deleteRelationship(deleteRelationship.uuid, copyVirtualMetadata, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
addRelationship(addRelationship: RelationshipIdentifiable): Observable<RemoteData<Relationship>> {
|
||||||
|
let leftItem: Item;
|
||||||
|
let rightItem: Item;
|
||||||
|
let leftwardValue: string;
|
||||||
|
let rightwardValue: string;
|
||||||
|
if (addRelationship.originalIsLeft) {
|
||||||
|
leftItem = addRelationship.originalItem;
|
||||||
|
rightItem = addRelationship.relatedItem;
|
||||||
|
leftwardValue = null;
|
||||||
|
rightwardValue = addRelationship.nameVariant;
|
||||||
|
} else {
|
||||||
|
leftItem = addRelationship.relatedItem;
|
||||||
|
rightItem = addRelationship.originalItem;
|
||||||
|
leftwardValue = addRelationship.nameVariant;
|
||||||
|
rightwardValue = null;
|
||||||
|
}
|
||||||
|
return this.relationshipService.addRelationship(addRelationship.type.id, leftItem, rightItem, leftwardValue, rightwardValue, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display notifications
|
||||||
|
* - Error notification for each failed response with their message
|
||||||
|
* - Success notification in case there's at least one successful response
|
||||||
|
* @param responses
|
||||||
|
*/
|
||||||
|
displayNotifications(responses: RemoteData<NoContent>[]): void {
|
||||||
|
const failedResponses = responses.filter((response: RemoteData<NoContent>) => response.hasFailed);
|
||||||
|
const successfulResponses = responses.filter((response: RemoteData<NoContent>) => response.hasSucceeded);
|
||||||
|
|
||||||
|
failedResponses.forEach((response: RemoteData<NoContent>) => {
|
||||||
|
this.notificationsService.error(this.getNotificationTitle('failed'), response.errorMessage);
|
||||||
|
});
|
||||||
|
if (successfulResponses.length > 0) {
|
||||||
|
this.notificationsService.success(this.getNotificationTitle('saved'), this.getNotificationContent('saved'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isProvidedItemTypeLeftType(relationshipType: RelationshipType, itemType: ItemType, item: Item): Observable<boolean> {
|
||||||
|
return this.getRelationshipLeftAndRightType(relationshipType).pipe(
|
||||||
|
map(([leftType, rightType]: [ItemType, ItemType]) => {
|
||||||
|
if (leftType.id === itemType.id) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rightType.id === itemType.id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// should never happen...
|
||||||
|
console.warn(`The item ${item.uuid} is not on the right or the left side of relationship type ${relationshipType.uuid}`);
|
||||||
|
return undefined;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether both side of the relationship need to be displayed on the edit relationship page or not.
|
||||||
|
*
|
||||||
|
* @param relationshipType The relationship type
|
||||||
|
* @param itemType The item type
|
||||||
|
*/
|
||||||
|
shouldDisplayBothRelationshipSides(relationshipType: RelationshipType, itemType: ItemType): Observable<boolean> {
|
||||||
|
return this.getRelationshipLeftAndRightType(relationshipType).pipe(
|
||||||
|
map(([leftType, rightType]: [ItemType, ItemType]) => {
|
||||||
|
return leftType.id === itemType.id && rightType.id === itemType.id && relationshipType.leftwardType !== relationshipType.rightwardType;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getRelationshipLeftAndRightType(relationshipType: RelationshipType): Observable<[ItemType, ItemType]> {
|
||||||
|
const leftType$: Observable<ItemType> = relationshipType.leftType.pipe(
|
||||||
|
getFirstSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const rightType$: Observable<ItemType> = relationshipType.rightType.pipe(
|
||||||
|
getFirstSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return observableCombineLatest([
|
||||||
|
leftType$,
|
||||||
|
rightType$,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get translated notification title
|
||||||
|
* @param key
|
||||||
|
*/
|
||||||
|
getNotificationTitle(key: string): string {
|
||||||
|
return this.translateService.instant(this.notificationsPrefix + key + '.title');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get translated notification content
|
||||||
|
* @param key
|
||||||
|
*/
|
||||||
|
getNotificationContent(key: string): string {
|
||||||
|
return this.translateService.instant(this.notificationsPrefix + key + '.content');
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,31 @@
|
|||||||
|
<ng-container *ngIf="shouldDisplayBothRelationshipSides$ | async">
|
||||||
|
<ds-edit-relationship-list
|
||||||
|
[url]="url"
|
||||||
|
[item]="item"
|
||||||
|
[itemType]="itemType"
|
||||||
|
[relationshipType]="relationshipType"
|
||||||
|
[hasChanges]="hasChanges"
|
||||||
|
[currentItemIsLeftItem$]="isLeftItem$"
|
||||||
|
class="d-block mb-4"
|
||||||
|
></ds-edit-relationship-list>
|
||||||
|
<ds-edit-relationship-list
|
||||||
|
[url]="url"
|
||||||
|
[item]="item"
|
||||||
|
[itemType]="itemType"
|
||||||
|
[relationshipType]="relationshipType"
|
||||||
|
[hasChanges]="hasChanges"
|
||||||
|
[currentItemIsLeftItem$]="isRightItem$"
|
||||||
|
></ds-edit-relationship-list>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="(shouldDisplayBothRelationshipSides$ | async) === false">
|
||||||
|
<ds-edit-relationship-list
|
||||||
|
[url]="url"
|
||||||
|
[item]="item"
|
||||||
|
[itemType]="itemType"
|
||||||
|
[relationshipType]="relationshipType"
|
||||||
|
[hasChanges]="hasChanges"
|
||||||
|
[currentItemIsLeftItem$]="currentItemIsLeftItem$"
|
||||||
|
></ds-edit-relationship-list>
|
||||||
|
</ng-container>
|
||||||
|
|
@@ -0,0 +1,122 @@
|
|||||||
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
|
import {
|
||||||
|
ComponentFixture,
|
||||||
|
TestBed,
|
||||||
|
waitForAsync,
|
||||||
|
} from '@angular/core/testing';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { cold } from 'jasmine-marbles';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { ItemType } from '../../../../core/shared/item-relationships/item-type.model';
|
||||||
|
import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
|
||||||
|
import { EditItemRelationshipsService } from '../edit-item-relationships.service';
|
||||||
|
import { EditRelationshipListComponent } from '../edit-relationship-list/edit-relationship-list.component';
|
||||||
|
import { EditRelationshipListWrapperComponent } from './edit-relationship-list-wrapper.component';
|
||||||
|
|
||||||
|
describe('EditRelationshipListWrapperComponent', () => {
|
||||||
|
let editItemRelationshipsService: EditItemRelationshipsService;
|
||||||
|
let comp: EditRelationshipListWrapperComponent;
|
||||||
|
let fixture: ComponentFixture<EditRelationshipListWrapperComponent>;
|
||||||
|
|
||||||
|
const leftType = Object.assign(new ItemType(), { id: 'leftType', label: 'leftTypeString' });
|
||||||
|
const rightType = Object.assign(new ItemType(), { id: 'rightType', label: 'rightTypeString' });
|
||||||
|
|
||||||
|
const relationshipType = Object.assign(new RelationshipType(), {
|
||||||
|
id: '1',
|
||||||
|
leftMaxCardinality: null,
|
||||||
|
leftMinCardinality: 0,
|
||||||
|
leftType: createSuccessfulRemoteDataObject$(leftType),
|
||||||
|
leftwardType: 'isOrgUnitOfOrgUnit',
|
||||||
|
rightMaxCardinality: null,
|
||||||
|
rightMinCardinality: 0,
|
||||||
|
rightType: createSuccessfulRemoteDataObject$(rightType),
|
||||||
|
rightwardType: 'isOrgUnitOfOrgUnit',
|
||||||
|
uuid: 'relationshiptype-1',
|
||||||
|
});
|
||||||
|
|
||||||
|
const item = Object.assign(new Item(), { uuid: 'item-uuid' });
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
|
||||||
|
editItemRelationshipsService = jasmine.createSpyObj('editItemRelationshipsService', {
|
||||||
|
isProvidedItemTypeLeftType: observableOf(true),
|
||||||
|
shouldDisplayBothRelationshipSides: observableOf(false),
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
EditRelationshipListWrapperComponent,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: EditItemRelationshipsService, useValue: editItemRelationshipsService },
|
||||||
|
],
|
||||||
|
schemas: [
|
||||||
|
CUSTOM_ELEMENTS_SCHEMA,
|
||||||
|
],
|
||||||
|
}).overrideComponent(EditRelationshipListWrapperComponent, {
|
||||||
|
remove: {
|
||||||
|
imports: [
|
||||||
|
EditRelationshipListComponent,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(EditRelationshipListWrapperComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
comp.relationshipType = relationshipType;
|
||||||
|
comp.itemType = leftType;
|
||||||
|
comp.item = item;
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onInit', () => {
|
||||||
|
it('should render the component', () => {
|
||||||
|
expect(comp).toBeTruthy();
|
||||||
|
});
|
||||||
|
it('should set currentItemIsLeftItem$ and bothItemsMatchType$ based on the provided relationshipType, itemType and item', () => {
|
||||||
|
expect(editItemRelationshipsService.isProvidedItemTypeLeftType).toHaveBeenCalledWith(relationshipType, leftType, item);
|
||||||
|
expect(editItemRelationshipsService.shouldDisplayBothRelationshipSides).toHaveBeenCalledWith(relationshipType, leftType);
|
||||||
|
|
||||||
|
expect(comp.currentItemIsLeftItem$.getValue()).toEqual(true);
|
||||||
|
expect(comp.shouldDisplayBothRelationshipSides$).toBeObservable(cold('(a|)', { a: false }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the current item is left', () => {
|
||||||
|
it('should render one relationship list section', () => {
|
||||||
|
const relationshipLists = fixture.debugElement.queryAll(By.css('ds-edit-relationship-list'));
|
||||||
|
expect(relationshipLists.length).toEqual(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the current item is right', () => {
|
||||||
|
it('should render one relationship list section', () => {
|
||||||
|
(editItemRelationshipsService.isProvidedItemTypeLeftType as jasmine.Spy).and.returnValue(observableOf(false));
|
||||||
|
comp.ngOnInit();
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const relationshipLists = fixture.debugElement.queryAll(By.css('ds-edit-relationship-list'));
|
||||||
|
expect(relationshipLists.length).toEqual(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the current item is both left and right', () => {
|
||||||
|
it('should render two relationship list sections', () => {
|
||||||
|
(editItemRelationshipsService.shouldDisplayBothRelationshipSides as jasmine.Spy).and.returnValue(observableOf(true));
|
||||||
|
comp.ngOnInit();
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const relationshipLists = fixture.debugElement.queryAll(By.css('ds-edit-relationship-list'));
|
||||||
|
expect(relationshipLists.length).toEqual(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -0,0 +1,111 @@
|
|||||||
|
import {
|
||||||
|
AsyncPipe,
|
||||||
|
NgIf,
|
||||||
|
} from '@angular/common';
|
||||||
|
import {
|
||||||
|
Component,
|
||||||
|
EventEmitter,
|
||||||
|
Input,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
Output,
|
||||||
|
} from '@angular/core';
|
||||||
|
import {
|
||||||
|
BehaviorSubject,
|
||||||
|
Observable,
|
||||||
|
Subscription,
|
||||||
|
} from 'rxjs';
|
||||||
|
|
||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { ItemType } from '../../../../core/shared/item-relationships/item-type.model';
|
||||||
|
import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model';
|
||||||
|
import { hasValue } from '../../../../shared/empty.util';
|
||||||
|
import { EditItemRelationshipsService } from '../edit-item-relationships.service';
|
||||||
|
import { EditRelationshipListComponent } from '../edit-relationship-list/edit-relationship-list.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-edit-relationship-list-wrapper',
|
||||||
|
styleUrls: ['./edit-relationship-list-wrapper.component.scss'],
|
||||||
|
templateUrl: './edit-relationship-list-wrapper.component.html',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
AsyncPipe,
|
||||||
|
EditRelationshipListComponent,
|
||||||
|
NgIf,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* A component creating a list of editable relationships of a certain type
|
||||||
|
* The relationships are rendered as a list of related items
|
||||||
|
*/
|
||||||
|
export class EditRelationshipListWrapperComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The item to display related items for
|
||||||
|
*/
|
||||||
|
@Input() item: Item;
|
||||||
|
|
||||||
|
@Input() itemType: ItemType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The URL to the current page
|
||||||
|
* Used to fetch updates for the current item from the store
|
||||||
|
*/
|
||||||
|
@Input() url: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The label of the relationship-type we're rendering a list for
|
||||||
|
*/
|
||||||
|
@Input() relationshipType: RelationshipType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If updated information has changed
|
||||||
|
*/
|
||||||
|
@Input() hasChanges!: Observable<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The event emmiter to submit the new information
|
||||||
|
*/
|
||||||
|
@Output() submitModal: EventEmitter<void> = new EventEmitter();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observable that emits true if {@link itemType} is on the left-hand side of {@link relationshipType},
|
||||||
|
* false if it is on the right-hand side and undefined in the rare case that it is on neither side.
|
||||||
|
*/
|
||||||
|
currentItemIsLeftItem$: BehaviorSubject<boolean> = new BehaviorSubject(undefined);
|
||||||
|
|
||||||
|
|
||||||
|
isLeftItem$ = new BehaviorSubject(true);
|
||||||
|
|
||||||
|
isRightItem$ = new BehaviorSubject(false);
|
||||||
|
|
||||||
|
shouldDisplayBothRelationshipSides$: Observable<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array to track all subscriptions and unsubscribe them onDestroy
|
||||||
|
* @type {Array}
|
||||||
|
*/
|
||||||
|
private subs: Subscription[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected editItemRelationshipsService: EditItemRelationshipsService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.subs.push(this.editItemRelationshipsService.isProvidedItemTypeLeftType(this.relationshipType, this.itemType, this.item)
|
||||||
|
.subscribe((nextValue: boolean) => {
|
||||||
|
this.currentItemIsLeftItem$.next(nextValue);
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.shouldDisplayBothRelationshipSides$ = this.editItemRelationshipsService.shouldDisplayBothRelationshipSides(this.relationshipType, this.itemType);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.subs
|
||||||
|
.filter((subscription) => hasValue(subscription))
|
||||||
|
.forEach((subscription) => subscription.unsubscribe());
|
||||||
|
}
|
||||||
|
}
|
@@ -1,5 +1,5 @@
|
|||||||
<h2 class="h4">
|
<h2 class="h4">
|
||||||
{{getRelationshipMessageKey$ | async | translate}}
|
{{relationshipMessageKey$ | async | translate}}
|
||||||
<button class="ml-2 btn btn-success" [disabled]="(hasChanges | async)" (click)="openLookup()">
|
<button class="ml-2 btn btn-success" [disabled]="(hasChanges | async)" (click)="openLookup()">
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus"></i>
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.relationships.edit.buttons.add" | translate}}</span>
|
<span class="d-none d-sm-inline"> {{"item.edit.relationships.edit.buttons.add" | translate}}</span>
|
||||||
|
@@ -10,19 +10,20 @@ import {
|
|||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import {
|
import {
|
||||||
ActivatedRoute,
|
ActivatedRoute,
|
||||||
Router,
|
RouterModule,
|
||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
import { provideMockStore } from '@ngrx/store/testing';
|
import { provideMockStore } from '@ngrx/store/testing';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { cold } from 'jasmine-marbles';
|
||||||
import { AuthRequestService } from 'src/app/core/auth/auth-request.service';
|
import {
|
||||||
import { CookieService } from 'src/app/core/services/cookie.service';
|
BehaviorSubject,
|
||||||
import { HardRedirectService } from 'src/app/core/services/hard-redirect.service';
|
of as observableOf,
|
||||||
import { ActivatedRouteStub } from 'src/app/shared/testing/active-router.stub';
|
} from 'rxjs';
|
||||||
import { AuthRequestServiceStub } from 'src/app/shared/testing/auth-request-service.stub';
|
|
||||||
|
|
||||||
import { APP_CONFIG } from '../../../../../config/app-config.interface';
|
import { APP_CONFIG } from '../../../../../config/app-config.interface';
|
||||||
|
import { environment } from '../../../../../environments/environment.test';
|
||||||
import { REQUEST } from '../../../../../express.tokens';
|
import { REQUEST } from '../../../../../express.tokens';
|
||||||
|
import { AuthRequestService } from '../../../../core/auth/auth-request.service';
|
||||||
import { LinkService } from '../../../../core/cache/builders/link.service';
|
import { LinkService } from '../../../../core/cache/builders/link.service';
|
||||||
import { ConfigurationDataService } from '../../../../core/data/configuration-data.service';
|
import { ConfigurationDataService } from '../../../../core/data/configuration-data.service';
|
||||||
import { FieldChangeType } from '../../../../core/data/object-updates/field-change-type.model';
|
import { FieldChangeType } from '../../../../core/data/object-updates/field-change-type.model';
|
||||||
@@ -31,6 +32,8 @@ import { RelationshipDataService } from '../../../../core/data/relationship-data
|
|||||||
import { RelationshipTypeDataService } from '../../../../core/data/relationship-type-data.service';
|
import { RelationshipTypeDataService } from '../../../../core/data/relationship-type-data.service';
|
||||||
import { GroupDataService } from '../../../../core/eperson/group-data.service';
|
import { GroupDataService } from '../../../../core/eperson/group-data.service';
|
||||||
import { PaginationService } from '../../../../core/pagination/pagination.service';
|
import { PaginationService } from '../../../../core/pagination/pagination.service';
|
||||||
|
import { CookieService } from '../../../../core/services/cookie.service';
|
||||||
|
import { HardRedirectService } from '../../../../core/services/hard-redirect.service';
|
||||||
import { LinkHeadService } from '../../../../core/services/link-head.service';
|
import { LinkHeadService } from '../../../../core/services/link-head.service';
|
||||||
import { ConfigurationProperty } from '../../../../core/shared/configuration-property.model';
|
import { ConfigurationProperty } from '../../../../core/shared/configuration-property.model';
|
||||||
import { Item } from '../../../../core/shared/item.model';
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
@@ -40,54 +43,60 @@ import { RelationshipType } from '../../../../core/shared/item-relationships/rel
|
|||||||
import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service';
|
import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service';
|
||||||
import { XSRFService } from '../../../../core/xsrf/xsrf.service';
|
import { XSRFService } from '../../../../core/xsrf/xsrf.service';
|
||||||
import { HostWindowService } from '../../../../shared/host-window.service';
|
import { HostWindowService } from '../../../../shared/host-window.service';
|
||||||
import { RouterMock } from '../../../../shared/mocks/router.mock';
|
|
||||||
import { SelectableListService } from '../../../../shared/object-list/selectable-list/selectable-list.service';
|
import { SelectableListService } from '../../../../shared/object-list/selectable-list/selectable-list.service';
|
||||||
import { PaginationComponent } from '../../../../shared/pagination/pagination.component';
|
import { PaginationComponent } from '../../../../shared/pagination/pagination.component';
|
||||||
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
|
||||||
|
import { ActivatedRouteStub } from '../../../../shared/testing/active-router.stub';
|
||||||
|
import { AuthRequestServiceStub } from '../../../../shared/testing/auth-request-service.stub';
|
||||||
|
import { EditItemRelationshipsServiceStub } from '../../../../shared/testing/edit-item-relationships.service.stub';
|
||||||
import { HostWindowServiceStub } from '../../../../shared/testing/host-window-service.stub';
|
import { HostWindowServiceStub } from '../../../../shared/testing/host-window-service.stub';
|
||||||
import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub';
|
import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub';
|
||||||
import { SearchConfigurationServiceStub } from '../../../../shared/testing/search-configuration-service.stub';
|
import { SearchConfigurationServiceStub } from '../../../../shared/testing/search-configuration-service.stub';
|
||||||
import { createPaginatedList } from '../../../../shared/testing/utils.test';
|
import { createPaginatedList } from '../../../../shared/testing/utils.test';
|
||||||
|
import { EditItemRelationshipsService } from '../edit-item-relationships.service';
|
||||||
import { EditRelationshipListComponent } from './edit-relationship-list.component';
|
import { EditRelationshipListComponent } from './edit-relationship-list.component';
|
||||||
|
|
||||||
let comp: EditRelationshipListComponent;
|
|
||||||
let fixture: ComponentFixture<EditRelationshipListComponent>;
|
|
||||||
let de: DebugElement;
|
|
||||||
|
|
||||||
let linkService;
|
|
||||||
let objectUpdatesService;
|
|
||||||
let relationshipService;
|
|
||||||
let selectableListService;
|
|
||||||
let paginationService;
|
|
||||||
let hostWindowService;
|
|
||||||
let hardRedirectService;
|
|
||||||
const relationshipTypeService = {};
|
|
||||||
|
|
||||||
const url = 'http://test-url.com/test-url';
|
|
||||||
|
|
||||||
let item;
|
|
||||||
let entityType;
|
|
||||||
let relatedEntityType;
|
|
||||||
let author1;
|
|
||||||
let author2;
|
|
||||||
let fieldUpdate1;
|
|
||||||
let fieldUpdate2;
|
|
||||||
let relationships;
|
|
||||||
let relationshipType;
|
|
||||||
let paginationOptions;
|
|
||||||
|
|
||||||
describe('EditRelationshipListComponent', () => {
|
describe('EditRelationshipListComponent', () => {
|
||||||
|
|
||||||
|
let comp: EditRelationshipListComponent;
|
||||||
|
let fixture: ComponentFixture<EditRelationshipListComponent>;
|
||||||
|
let de: DebugElement;
|
||||||
|
|
||||||
|
let linkService;
|
||||||
|
let objectUpdatesService;
|
||||||
|
let relationshipService;
|
||||||
|
let selectableListService;
|
||||||
|
let paginationService: PaginationServiceStub;
|
||||||
|
let hostWindowService: HostWindowServiceStub;
|
||||||
|
let hardRedirectService;
|
||||||
|
const relationshipTypeService = {};
|
||||||
|
let editItemRelationshipsService: EditItemRelationshipsServiceStub;
|
||||||
|
|
||||||
|
const url = 'http://test-url.com/test-url';
|
||||||
|
|
||||||
|
let itemLeft: Item;
|
||||||
|
let entityTypeLeft: ItemType;
|
||||||
|
let entityTypeRight: ItemType;
|
||||||
|
let itemRight1: Item;
|
||||||
|
let itemRight2: Item;
|
||||||
|
let fieldUpdate1;
|
||||||
|
let fieldUpdate2;
|
||||||
|
let relationships: Relationship[];
|
||||||
|
let relationshipType: RelationshipType;
|
||||||
|
let paginationOptions: PaginationComponentOptions;
|
||||||
|
let currentItemIsLeftItem$ = new BehaviorSubject<boolean>(true);
|
||||||
|
|
||||||
const resetComponent = () => {
|
const resetComponent = () => {
|
||||||
fixture = TestBed.createComponent(EditRelationshipListComponent);
|
fixture = TestBed.createComponent(EditRelationshipListComponent);
|
||||||
comp = fixture.componentInstance;
|
comp = fixture.componentInstance;
|
||||||
de = fixture.debugElement;
|
de = fixture.debugElement;
|
||||||
comp.item = item;
|
comp.item = itemLeft;
|
||||||
comp.itemType = entityType;
|
comp.itemType = entityTypeLeft;
|
||||||
comp.url = url;
|
comp.url = url;
|
||||||
comp.relationshipType = relationshipType;
|
comp.relationshipType = relationshipType;
|
||||||
comp.hasChanges = observableOf(false);
|
comp.hasChanges = observableOf(false);
|
||||||
|
comp.currentItemIsLeftItem$ = currentItemIsLeftItem$;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -101,29 +110,26 @@ describe('EditRelationshipListComponent', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
hardRedirectService = jasmine.createSpyObj('hardRedirectService', ['redirect']);
|
function init(leftType: string, rightType: string): void {
|
||||||
|
entityTypeLeft = Object.assign(new ItemType(), {
|
||||||
beforeEach(waitForAsync(() => {
|
id: leftType,
|
||||||
|
uuid: leftType,
|
||||||
entityType = Object.assign(new ItemType(), {
|
label: leftType,
|
||||||
id: 'Publication',
|
|
||||||
uuid: 'Publication',
|
|
||||||
label: 'Publication',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
relatedEntityType = Object.assign(new ItemType(), {
|
entityTypeRight = Object.assign(new ItemType(), {
|
||||||
id: 'Author',
|
id: rightType,
|
||||||
uuid: 'Author',
|
uuid: rightType,
|
||||||
label: 'Author',
|
label: rightType,
|
||||||
});
|
});
|
||||||
|
|
||||||
relationshipType = Object.assign(new RelationshipType(), {
|
relationshipType = Object.assign(new RelationshipType(), {
|
||||||
id: '1',
|
id: '1',
|
||||||
uuid: '1',
|
uuid: '1',
|
||||||
leftType: createSuccessfulRemoteDataObject$(entityType),
|
leftType: createSuccessfulRemoteDataObject$(entityTypeLeft),
|
||||||
rightType: createSuccessfulRemoteDataObject$(relatedEntityType),
|
rightType: createSuccessfulRemoteDataObject$(entityTypeRight),
|
||||||
leftwardType: 'isAuthorOfPublication',
|
leftwardType: `is${rightType}Of${leftType}`,
|
||||||
rightwardType: 'isPublicationOfAuthor',
|
rightwardType: `is${leftType}Of${rightType}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
paginationOptions = Object.assign(new PaginationComponentOptions(), {
|
paginationOptions = Object.assign(new PaginationComponentOptions(), {
|
||||||
@@ -132,13 +138,13 @@ describe('EditRelationshipListComponent', () => {
|
|||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
author1 = Object.assign(new Item(), {
|
itemRight1 = Object.assign(new Item(), {
|
||||||
id: 'author1',
|
id: `${rightType}-1`,
|
||||||
uuid: 'author1',
|
uuid: `${rightType}-1`,
|
||||||
});
|
});
|
||||||
author2 = Object.assign(new Item(), {
|
itemRight2 = Object.assign(new Item(), {
|
||||||
id: 'author2',
|
id: `${rightType}-2`,
|
||||||
uuid: 'author2',
|
uuid: `${rightType}-2`,
|
||||||
});
|
});
|
||||||
|
|
||||||
relationships = [
|
relationships = [
|
||||||
@@ -147,25 +153,25 @@ describe('EditRelationshipListComponent', () => {
|
|||||||
id: '2',
|
id: '2',
|
||||||
uuid: '2',
|
uuid: '2',
|
||||||
relationshipType: createSuccessfulRemoteDataObject$(relationshipType),
|
relationshipType: createSuccessfulRemoteDataObject$(relationshipType),
|
||||||
leftItem: createSuccessfulRemoteDataObject$(item),
|
leftItem: createSuccessfulRemoteDataObject$(itemLeft),
|
||||||
rightItem: createSuccessfulRemoteDataObject$(author1),
|
rightItem: createSuccessfulRemoteDataObject$(itemRight1),
|
||||||
}),
|
}),
|
||||||
Object.assign(new Relationship(), {
|
Object.assign(new Relationship(), {
|
||||||
self: url + '/3',
|
self: url + '/3',
|
||||||
id: '3',
|
id: '3',
|
||||||
uuid: '3',
|
uuid: '3',
|
||||||
relationshipType: createSuccessfulRemoteDataObject$(relationshipType),
|
relationshipType: createSuccessfulRemoteDataObject$(relationshipType),
|
||||||
leftItem: createSuccessfulRemoteDataObject$(item),
|
leftItem: createSuccessfulRemoteDataObject$(itemLeft),
|
||||||
rightItem: createSuccessfulRemoteDataObject$(author2),
|
rightItem: createSuccessfulRemoteDataObject$(itemRight2),
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
item = Object.assign(new Item(), {
|
itemLeft = Object.assign(new Item(), {
|
||||||
_links: {
|
_links: {
|
||||||
self: { href: 'fake-item-url/publication' },
|
self: { href: 'fake-item-url/publication' },
|
||||||
},
|
},
|
||||||
id: 'publication',
|
id: `1-${leftType}`,
|
||||||
uuid: 'publication',
|
uuid: `1-${leftType}`,
|
||||||
relationships: createSuccessfulRemoteDataObject$(createPaginatedList(relationships)),
|
relationships: createSuccessfulRemoteDataObject$(createPaginatedList(relationships)),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -192,12 +198,15 @@ describe('EditRelationshipListComponent', () => {
|
|||||||
[relationships[0].uuid]: fieldUpdate1,
|
[relationships[0].uuid]: fieldUpdate1,
|
||||||
[relationships[1].uuid]: fieldUpdate2,
|
[relationships[1].uuid]: fieldUpdate2,
|
||||||
}),
|
}),
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
initialize: () => {
|
||||||
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
relationshipService = jasmine.createSpyObj('relationshipService',
|
relationshipService = jasmine.createSpyObj('relationshipService',
|
||||||
{
|
{
|
||||||
getRelatedItemsByLabel: createSuccessfulRemoteDataObject$(createPaginatedList([author1, author2])),
|
getRelatedItemsByLabel: createSuccessfulRemoteDataObject$(createPaginatedList([itemRight1, itemRight2])),
|
||||||
getItemRelationshipsByLabel: createSuccessfulRemoteDataObject$(createPaginatedList(relationships)),
|
getItemRelationshipsByLabel: createSuccessfulRemoteDataObject$(createPaginatedList(relationships)),
|
||||||
isLeftItem: observableOf(true),
|
isLeftItem: observableOf(true),
|
||||||
},
|
},
|
||||||
@@ -233,14 +242,14 @@ describe('EditRelationshipListComponent', () => {
|
|||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|
||||||
const environmentUseThumbs = {
|
editItemRelationshipsService = new EditItemRelationshipsServiceStub();
|
||||||
browseBy: {
|
|
||||||
showThumbnails: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot(), EditRelationshipListComponent],
|
imports: [
|
||||||
|
EditRelationshipListComponent,
|
||||||
|
RouterModule.forRoot([]),
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
],
|
||||||
providers: [
|
providers: [
|
||||||
provideMockStore({ initialState }),
|
provideMockStore({ initialState }),
|
||||||
{ provide: ObjectUpdatesService, useValue: objectUpdatesService },
|
{ provide: ObjectUpdatesService, useValue: objectUpdatesService },
|
||||||
@@ -251,15 +260,15 @@ describe('EditRelationshipListComponent', () => {
|
|||||||
{ provide: HostWindowService, useValue: hostWindowService },
|
{ provide: HostWindowService, useValue: hostWindowService },
|
||||||
{ provide: RelationshipTypeDataService, useValue: relationshipTypeService },
|
{ provide: RelationshipTypeDataService, useValue: relationshipTypeService },
|
||||||
{ provide: GroupDataService, useValue: groupDataService },
|
{ provide: GroupDataService, useValue: groupDataService },
|
||||||
{ provide: Router, useValue: new RouterMock() },
|
|
||||||
{ provide: LinkHeadService, useValue: linkHeadService },
|
{ provide: LinkHeadService, useValue: linkHeadService },
|
||||||
{ provide: ConfigurationDataService, useValue: configurationDataService },
|
{ provide: ConfigurationDataService, useValue: configurationDataService },
|
||||||
{ provide: SearchConfigurationService, useValue: new SearchConfigurationServiceStub() },
|
{ provide: SearchConfigurationService, useValue: new SearchConfigurationServiceStub() },
|
||||||
|
{ provide: EditItemRelationshipsService, useValue: editItemRelationshipsService },
|
||||||
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
|
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
|
||||||
{ provide: AuthRequestService, useValue: new AuthRequestServiceStub() },
|
{ provide: AuthRequestService, useValue: new AuthRequestServiceStub() },
|
||||||
{ provide: HardRedirectService, useValue: hardRedirectService },
|
{ provide: HardRedirectService, useValue: hardRedirectService },
|
||||||
{ provide: XSRFService, useValue: {} },
|
{ provide: XSRFService, useValue: {} },
|
||||||
{ provide: APP_CONFIG, useValue: environmentUseThumbs },
|
{ provide: APP_CONFIG, useValue: environment },
|
||||||
{ provide: REQUEST, useValue: {} },
|
{ provide: REQUEST, useValue: {} },
|
||||||
CookieService,
|
CookieService,
|
||||||
], schemas: [
|
], schemas: [
|
||||||
@@ -268,114 +277,129 @@ describe('EditRelationshipListComponent', () => {
|
|||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|
||||||
resetComponent();
|
resetComponent();
|
||||||
}));
|
}
|
||||||
|
|
||||||
describe('changeType is REMOVE', () => {
|
describe('Publication - Author relationship', () => {
|
||||||
beforeEach(() => {
|
beforeEach(waitForAsync(() => init('Publication', 'Author')));
|
||||||
fieldUpdate1.changeType = FieldChangeType.REMOVE;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
it('the div should have class alert-danger', () => {
|
|
||||||
const element = de.queryAll(By.css('.relationship-row'))[1].nativeElement;
|
|
||||||
expect(element.classList).toContain('alert-danger');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('pagination component', () => {
|
describe('changeType is REMOVE', () => {
|
||||||
let paginationComp: PaginationComponent;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
paginationComp = de.query(By.css('ds-pagination')).componentInstance;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should receive the correct pagination config', () => {
|
|
||||||
expect(paginationComp.paginationOptions).toEqual(paginationOptions);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should receive correct collection size', () => {
|
|
||||||
expect(paginationComp.collectionSize).toEqual(relationships.length);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('relationshipService.getItemRelationshipsByLabel', () => {
|
|
||||||
it('should receive the correct pagination info', () => {
|
|
||||||
expect(relationshipService.getItemRelationshipsByLabel).toHaveBeenCalledTimes(1);
|
|
||||||
|
|
||||||
const callArgs = relationshipService.getItemRelationshipsByLabel.calls.mostRecent().args;
|
|
||||||
const findListOptions = callArgs[2];
|
|
||||||
const linksToFollow = callArgs[5];
|
|
||||||
expect(findListOptions.elementsPerPage).toEqual(paginationOptions.pageSize);
|
|
||||||
expect(findListOptions.currentPage).toEqual(paginationOptions.currentPage);
|
|
||||||
expect(linksToFollow.linksToFollow[0].name).toEqual('thumbnail');
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when the publication is on the left side of the relationship', () => {
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
relationshipType = Object.assign(new RelationshipType(), {
|
fieldUpdate1.changeType = FieldChangeType.REMOVE;
|
||||||
id: '1',
|
|
||||||
uuid: '1',
|
|
||||||
leftType: createSuccessfulRemoteDataObject$(entityType), // publication
|
|
||||||
rightType: createSuccessfulRemoteDataObject$(relatedEntityType), // author
|
|
||||||
leftwardType: 'isAuthorOfPublication',
|
|
||||||
rightwardType: 'isPublicationOfAuthor',
|
|
||||||
});
|
|
||||||
relationshipService.getItemRelationshipsByLabel.calls.reset();
|
|
||||||
resetComponent();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should fetch isAuthorOfPublication', () => {
|
|
||||||
expect(relationshipService.getItemRelationshipsByLabel).toHaveBeenCalledTimes(1);
|
|
||||||
|
|
||||||
const callArgs = relationshipService.getItemRelationshipsByLabel.calls.mostRecent().args;
|
|
||||||
const label = callArgs[1];
|
|
||||||
|
|
||||||
expect(label).toEqual('isAuthorOfPublication');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when the publication is on the right side of the relationship', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
relationshipType = Object.assign(new RelationshipType(), {
|
|
||||||
id: '1',
|
|
||||||
uuid: '1',
|
|
||||||
leftType: createSuccessfulRemoteDataObject$(relatedEntityType), // author
|
|
||||||
rightType: createSuccessfulRemoteDataObject$(entityType), // publication
|
|
||||||
leftwardType: 'isPublicationOfAuthor',
|
|
||||||
rightwardType: 'isAuthorOfPublication',
|
|
||||||
});
|
|
||||||
relationshipService.getItemRelationshipsByLabel.calls.reset();
|
|
||||||
resetComponent();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should fetch isAuthorOfPublication', () => {
|
|
||||||
expect(relationshipService.getItemRelationshipsByLabel).toHaveBeenCalledTimes(1);
|
|
||||||
|
|
||||||
const callArgs = relationshipService.getItemRelationshipsByLabel.calls.mostRecent().args;
|
|
||||||
const label = callArgs[1];
|
|
||||||
|
|
||||||
expect(label).toEqual('isAuthorOfPublication');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
describe('changes managment for add buttons', () => {
|
|
||||||
|
|
||||||
it('should show enabled add buttons', () => {
|
|
||||||
const element = de.query(By.css('.btn-success'));
|
|
||||||
expect(element.nativeElement?.disabled).toBeFalse();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('after hash changes changed', () => {
|
|
||||||
comp.hasChanges = observableOf(true);
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
const element = de.query(By.css('.btn-success'));
|
});
|
||||||
expect(element.nativeElement?.disabled).toBeTrue();
|
it('the div should have class alert-danger', () => {
|
||||||
|
const element = de.queryAll(By.css('.relationship-row'))[1].nativeElement;
|
||||||
|
expect(element.classList).toContain('alert-danger');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('pagination component', () => {
|
||||||
|
let paginationComp: PaginationComponent;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
paginationComp = de.query(By.css('ds-pagination')).componentInstance;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should receive the correct pagination config', () => {
|
||||||
|
expect(paginationComp.paginationOptions).toEqual(paginationOptions);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should receive correct collection size', () => {
|
||||||
|
expect(paginationComp.collectionSize).toEqual(relationships.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('relationshipService.getItemRelationshipsByLabel', () => {
|
||||||
|
it('should receive the correct pagination info', () => {
|
||||||
|
expect(relationshipService.getItemRelationshipsByLabel).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
const callArgs = relationshipService.getItemRelationshipsByLabel.calls.mostRecent().args;
|
||||||
|
const findListOptions = callArgs[2];
|
||||||
|
const linksToFollow = callArgs[5];
|
||||||
|
expect(findListOptions.elementsPerPage).toEqual(paginationOptions.pageSize);
|
||||||
|
expect(findListOptions.currentPage).toEqual(paginationOptions.currentPage);
|
||||||
|
expect(linksToFollow.linksToFollow[0].name).toEqual('thumbnail');
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the publication is on the left side of the relationship', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
relationshipType = Object.assign(new RelationshipType(), {
|
||||||
|
id: '1',
|
||||||
|
uuid: '1',
|
||||||
|
leftType: createSuccessfulRemoteDataObject$(entityTypeLeft), // publication
|
||||||
|
rightType: createSuccessfulRemoteDataObject$(entityTypeRight), // author
|
||||||
|
leftwardType: 'isAuthorOfPublication',
|
||||||
|
rightwardType: 'isPublicationOfAuthor',
|
||||||
|
});
|
||||||
|
currentItemIsLeftItem$ = new BehaviorSubject<boolean>(true);
|
||||||
|
relationshipService.getItemRelationshipsByLabel.calls.reset();
|
||||||
|
resetComponent();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fetch isAuthorOfPublication', () => {
|
||||||
|
expect(relationshipService.getItemRelationshipsByLabel).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
const callArgs = relationshipService.getItemRelationshipsByLabel.calls.mostRecent().args;
|
||||||
|
const label = callArgs[1];
|
||||||
|
|
||||||
|
expect(label).toEqual('isAuthorOfPublication');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the publication is on the right side of the relationship', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
relationshipType = Object.assign(new RelationshipType(), {
|
||||||
|
id: '1',
|
||||||
|
uuid: '1',
|
||||||
|
leftType: createSuccessfulRemoteDataObject$(entityTypeRight), // author
|
||||||
|
rightType: createSuccessfulRemoteDataObject$(entityTypeLeft), // publication
|
||||||
|
leftwardType: 'isPublicationOfAuthor',
|
||||||
|
rightwardType: 'isAuthorOfPublication',
|
||||||
|
});
|
||||||
|
currentItemIsLeftItem$ = new BehaviorSubject<boolean>(false);
|
||||||
|
relationshipService.getItemRelationshipsByLabel.calls.reset();
|
||||||
|
resetComponent();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fetch isAuthorOfPublication', () => {
|
||||||
|
expect(relationshipService.getItemRelationshipsByLabel).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
const callArgs = relationshipService.getItemRelationshipsByLabel.calls.mostRecent().args;
|
||||||
|
const label = callArgs[1];
|
||||||
|
|
||||||
|
expect(label).toEqual('isAuthorOfPublication');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
describe('changes managment for add buttons', () => {
|
||||||
|
|
||||||
|
it('should show enabled add buttons', () => {
|
||||||
|
const element = de.query(By.css('.btn-success'));
|
||||||
|
expect(element.nativeElement?.disabled).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('after hash changes changed', () => {
|
||||||
|
comp.hasChanges = observableOf(true);
|
||||||
|
fixture.detectChanges();
|
||||||
|
const element = de.query(By.css('.btn-success'));
|
||||||
|
expect(element.nativeElement?.disabled).toBeTrue();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('OrgUnit - OrgUnit relationship', () => {
|
||||||
|
beforeEach(waitForAsync(() => init('OrgUnit', 'OrgUnit')));
|
||||||
|
|
||||||
|
it('should emit the relatedEntityType$ even for same entity relationships', () => {
|
||||||
|
expect(comp.relatedEntityType$).toBeObservable(cold('(a|)', {
|
||||||
|
a: entityTypeRight,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user