- {{ePerson.eperson.id}}
- {{ePerson.eperson.id}}
+ {{ePerson.eperson.name}}
-
+
+ {{messagePrefix + '.table.email' | translate}}: {{ ePerson.eperson.email ? ePerson.eperson.email : '-' }}
+ {{messagePrefix + '.table.netid' | translate}}: {{ ePerson.eperson.netid ? ePerson.eperson.netid : '-' }}
+
+
- {{messagePrefix + '.table.id' | translate}}
- {{messagePrefix + '.table.name' | translate}}
- {{messagePrefix + '.table.edit' | translate}}
+ {{messagePrefix + '.table.id' | translate}}
+ {{messagePrefix + '.table.name' | translate}}
+ {{messagePrefix + '.table.collectionOrCommunity' | translate}}
+ {{messagePrefix + '.table.edit' | translate}}
- {{group.id}}
- {{group.id}}
+ {{group.name}}
-
+ {{(group.object | async)?.payload?.name}}
+
- {{messagePrefix + '.table.id' | translate}}
- {{messagePrefix + '.table.name' | translate}}
+ {{messagePrefix + '.table.id' | translate}}
+ {{messagePrefix + '.table.name' | translate}}
+ {{messagePrefix + '.table.collectionOrCommunity' | translate}}
{{messagePrefix + '.table.edit' | translate}}
- {{group.id}}
- {{group.id}}
+ {{group.name}}
-
+ {{(group.object | async)?.payload?.name}}
+
this.groupDataService.findAllByHref(this.groupBeingEdited._links.subgroups.href, {
currentPage: config.currentPage,
elementsPerPage: config.pageSize
- }
+ },
+ true,
+ true,
+ followLink('object')
))
).subscribe((rd: RemoteData>) => {
this.subGroups$.next(rd);
@@ -217,7 +221,8 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
switchMap((config) => this.groupDataService.searchGroups(this.currentSearchQuery, {
currentPage: config.currentPage,
elementsPerPage: config.pageSize
- }))
+ }, true, true, followLink('object')
+ ))
).subscribe((rd: RemoteData>) => {
this.searchResults$.next(rd);
}));
diff --git a/src/app/access-control/group-registry/group-page.guard.spec.ts b/src/app/access-control/group-registry/group-page.guard.spec.ts
new file mode 100644
index 0000000000..48fa124c07
--- /dev/null
+++ b/src/app/access-control/group-registry/group-page.guard.spec.ts
@@ -0,0 +1,83 @@
+import { GroupPageGuard } from './group-page.guard';
+import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
+import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
+import { ActivatedRouteSnapshot, Router } from '@angular/router';
+import { of as observableOf } from 'rxjs';
+import { AuthService } from '../../core/auth/auth.service';
+import { FeatureID } from '../../core/data/feature-authorization/feature-id';
+
+describe('GroupPageGuard', () => {
+ const groupsEndpointUrl = 'https://test.org/api/eperson/groups';
+ const groupUuid = '0d6f89df-f95a-4829-943c-f21f434fb892';
+ const groupEndpointUrl = `${groupsEndpointUrl}/${groupUuid}`;
+ const routeSnapshotWithGroupId = {
+ params: {
+ groupId: groupUuid,
+ }
+ } as unknown as ActivatedRouteSnapshot;
+
+ let guard: GroupPageGuard;
+ let halEndpointService: HALEndpointService;
+ let authorizationService: AuthorizationDataService;
+ let router: Router;
+ let authService: AuthService;
+
+ beforeEach(() => {
+ halEndpointService = jasmine.createSpyObj(['getEndpoint']);
+ (halEndpointService as any).getEndpoint.and.returnValue(observableOf(groupsEndpointUrl));
+
+ authorizationService = jasmine.createSpyObj(['isAuthorized']);
+ // NOTE: value is set in beforeEach
+
+ router = jasmine.createSpyObj(['parseUrl']);
+ (router as any).parseUrl.and.returnValue = {};
+
+ authService = jasmine.createSpyObj(['isAuthenticated']);
+ (authService as any).isAuthenticated.and.returnValue(observableOf(true));
+
+ guard = new GroupPageGuard(halEndpointService, authorizationService, router, authService);
+ });
+
+ it('should be created', () => {
+ expect(guard).toBeTruthy();
+ });
+
+ describe('canActivate', () => {
+ describe('when the current user can manage the group', () => {
+ beforeEach(() => {
+ (authorizationService as any).isAuthorized.and.returnValue(observableOf(true));
+ });
+
+ it('should return true', (done) => {
+ guard.canActivate(
+ routeSnapshotWithGroupId, { url: 'current-url'} as any
+ ).subscribe((result) => {
+ expect(authorizationService.isAuthorized).toHaveBeenCalledWith(
+ FeatureID.CanManageGroup, groupEndpointUrl, undefined
+ );
+ expect(result).toBeTrue();
+ done();
+ });
+ });
+ });
+
+ describe('when the current user can not manage the group', () => {
+ beforeEach(() => {
+ (authorizationService as any).isAuthorized.and.returnValue(observableOf(false));
+ });
+
+ it('should not return true', (done) => {
+ guard.canActivate(
+ routeSnapshotWithGroupId, { url: 'current-url'} as any
+ ).subscribe((result) => {
+ expect(authorizationService.isAuthorized).toHaveBeenCalledWith(
+ FeatureID.CanManageGroup, groupEndpointUrl, undefined
+ );
+ expect(result).not.toBeTrue();
+ done();
+ });
+ });
+ });
+ });
+
+});
diff --git a/src/app/access-control/group-registry/group-page.guard.ts b/src/app/access-control/group-registry/group-page.guard.ts
new file mode 100644
index 0000000000..057f67ddeb
--- /dev/null
+++ b/src/app/access-control/group-registry/group-page.guard.ts
@@ -0,0 +1,35 @@
+import { Injectable } from '@angular/core';
+import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
+import { Observable, of as observableOf } from 'rxjs';
+import { FeatureID } from '../../core/data/feature-authorization/feature-id';
+import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
+import { AuthService } from '../../core/auth/auth.service';
+import { SomeFeatureAuthorizationGuard } from '../../core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard';
+import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
+import { map } from 'rxjs/operators';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class GroupPageGuard extends SomeFeatureAuthorizationGuard {
+
+ protected groupsEndpoint = 'groups';
+
+ constructor(protected halEndpointService: HALEndpointService,
+ protected authorizationService: AuthorizationDataService,
+ protected router: Router,
+ protected authService: AuthService) {
+ super(authorizationService, router, authService);
+ }
+
+ getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable {
+ return observableOf([FeatureID.CanManageGroup]);
+ }
+
+ getObjectUrl(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable {
+ return this.halEndpointService.getEndpoint(this.groupsEndpoint).pipe(
+ map(groupsUrl => `${groupsUrl}/${route?.params?.groupId}`)
+ );
+ }
+
+}
diff --git a/src/app/access-control/group-registry/groups-registry.component.html b/src/app/access-control/group-registry/groups-registry.component.html
index e5e25ae944..e791b7f2a0 100644
--- a/src/app/access-control/group-registry/groups-registry.component.html
+++ b/src/app/access-control/group-registry/groups-registry.component.html
@@ -33,9 +33,9 @@
-
+
0 && !(searching$ | async)"
+ *ngIf="(pageInfoState$ | async)?.totalElements > 0 && !(loading$ | async)"
[paginationOptions]="config"
[pageInfoState]="pageInfoState$"
[collectionSize]="(pageInfoState$ | async)?.totalElements"
@@ -48,6 +48,7 @@
{{messagePrefix + 'table.id' | translate}}
{{messagePrefix + 'table.name' | translate}}
+ {{messagePrefix + 'table.collectionOrCommunity' | translate}}
{{messagePrefix + 'table.members' | translate}}
{{messagePrefix + 'table.edit' | translate}}
@@ -56,14 +57,27 @@
{{groupDto.group.id}}
{{groupDto.group.name}}
+ {{(groupDto.group.object | async)?.payload?.name}}
{{groupDto.epersons?.totalElements + groupDto.subgroups?.totalElements}}
-
-
-
+
+
+
+
+
+
+
+
diff --git a/src/app/access-control/group-registry/groups-registry.component.spec.ts b/src/app/access-control/group-registry/groups-registry.component.spec.ts
index 10064800e1..0b30a551fd 100644
--- a/src/app/access-control/group-registry/groups-registry.component.spec.ts
+++ b/src/app/access-control/group-registry/groups-registry.component.spec.ts
@@ -30,6 +30,7 @@ import { routeServiceStub } from '../../shared/testing/route-service.stub';
import { RouterMock } from '../../shared/mocks/router.mock';
import { PaginationService } from '../../core/pagination/pagination.service';
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
+import { FeatureID } from '../../core/data/feature-authorization/feature-id';
describe('GroupRegistryComponent', () => {
let component: GroupsRegistryComponent;
@@ -43,6 +44,26 @@ describe('GroupRegistryComponent', () => {
let mockEPeople;
let paginationService;
+ /**
+ * Set authorizationService.isAuthorized to return the following values.
+ * @param isAdmin whether or not the current user is an admin.
+ * @param canManageGroup whether or not the current user can manage all groups.
+ */
+ const setIsAuthorized = (isAdmin: boolean, canManageGroup: boolean) => {
+ (authorizationService as any).isAuthorized.and.callFake((featureId?: FeatureID) => {
+ switch (featureId) {
+ case FeatureID.AdministratorOf:
+ return observableOf(isAdmin);
+ case FeatureID.CanManageGroup:
+ return observableOf(canManageGroup);
+ case FeatureID.CanDelete:
+ return observableOf(true);
+ default:
+ throw new Error(`setIsAuthorized: this fake implementation does not support ${featureId}.`);
+ }
+ });
+ };
+
beforeEach(waitForAsync(() => {
mockGroups = [GroupMock, GroupMock2];
mockEPeople = [EPersonMock, EPersonMock2];
@@ -131,9 +152,9 @@ describe('GroupRegistryComponent', () => {
return createSuccessfulRemoteDataObject$(undefined);
}
};
- authorizationService = jasmine.createSpyObj('authorizationService', {
- isAuthorized: observableOf(true)
- });
+
+ authorizationService = jasmine.createSpyObj('authorizationService', ['isAuthorized']);
+ setIsAuthorized(true, true);
paginationService = new PaginationServiceStub();
TestBed.configureTestingModule({
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
@@ -180,6 +201,88 @@ describe('GroupRegistryComponent', () => {
});
});
+ it('should display community/collection name if present', () => {
+ const collectionNamesFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(3)'));
+ expect(collectionNamesFound.length).toEqual(2);
+ expect(collectionNamesFound[0].nativeElement.textContent).toEqual('');
+ expect(collectionNamesFound[1].nativeElement.textContent).toEqual('testgroupid2objectName');
+ });
+
+ describe('edit buttons', () => {
+ describe('when the user is a general admin', () => {
+ beforeEach(fakeAsync(() => {
+ // NOTE: setting canManageGroup to false should not matter, since isAdmin takes priority
+ setIsAuthorized(true, false);
+
+ // force rerender after setup changes
+ component.search({ query: '' });
+ tick();
+ fixture.detectChanges();
+ }));
+
+ it('should be active', () => {
+ const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(5) button.btn-edit'));
+ expect(editButtonsFound.length).toEqual(2);
+ editButtonsFound.forEach((editButtonFound) => {
+ expect(editButtonFound.nativeElement.disabled).toBeFalse();
+ });
+ });
+
+ it('should not check the canManageGroup permissions', () => {
+ expect(authorizationService.isAuthorized).not.toHaveBeenCalledWith(
+ FeatureID.CanManageGroup, mockGroups[0].self
+ );
+ expect(authorizationService.isAuthorized).not.toHaveBeenCalledWith(
+ FeatureID.CanManageGroup, mockGroups[0].self, undefined // treated differently
+ );
+ expect(authorizationService.isAuthorized).not.toHaveBeenCalledWith(
+ FeatureID.CanManageGroup, mockGroups[1].self
+ );
+ expect(authorizationService.isAuthorized).not.toHaveBeenCalledWith(
+ FeatureID.CanManageGroup, mockGroups[1].self, undefined // treated differently
+ );
+ });
+ });
+
+ describe('when the user can edit the groups', () => {
+ beforeEach(fakeAsync(() => {
+ setIsAuthorized(false, true);
+
+ // force rerender after setup changes
+ component.search({ query: '' });
+ tick();
+ fixture.detectChanges();
+ }));
+
+ it('should be active', () => {
+ const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(5) button.btn-edit'));
+ expect(editButtonsFound.length).toEqual(2);
+ editButtonsFound.forEach((editButtonFound) => {
+ expect(editButtonFound.nativeElement.disabled).toBeFalse();
+ });
+ });
+ });
+
+ describe('when the user can not edit the groups', () => {
+ beforeEach(fakeAsync(() => {
+ setIsAuthorized(false, false);
+
+ // force rerender after setup changes
+ component.search({ query: '' });
+ tick();
+ fixture.detectChanges();
+ }));
+
+ it('should not be active', () => {
+ const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(5) button.btn-edit'));
+ expect(editButtonsFound.length).toEqual(2);
+ editButtonsFound.forEach((editButtonFound) => {
+ expect(editButtonFound.nativeElement.disabled).toBeTrue();
+ });
+ });
+ });
+ });
+
describe('search', () => {
describe('when searching with query', () => {
let groupIdsFound;
diff --git a/src/app/access-control/group-registry/groups-registry.component.ts b/src/app/access-control/group-registry/groups-registry.component.ts
index b28ac043d9..da861518da 100644
--- a/src/app/access-control/group-registry/groups-registry.component.ts
+++ b/src/app/access-control/group-registry/groups-registry.component.ts
@@ -9,7 +9,7 @@ import {
of as observableOf,
Subscription
} from 'rxjs';
-import { catchError, map, switchMap, take } from 'rxjs/operators';
+import { catchError, map, switchMap, take, tap } from 'rxjs/operators';
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
@@ -35,6 +35,7 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { NoContent } from '../../core/shared/NoContent.model';
import { PaginationService } from '../../core/pagination/pagination.service';
+import { followLink } from '../../shared/utils/follow-link-config.model';
@Component({
selector: 'ds-groups-registry',
@@ -75,7 +76,7 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
/**
* A boolean representing if a search is pending
*/
- searching$: BehaviorSubject = new BehaviorSubject(false);
+ loading$: BehaviorSubject = new BehaviorSubject(false);
// Current search in groups registry
currentSearchQuery: string;
@@ -118,12 +119,12 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
* @param data Contains query param
*/
search(data: any) {
- this.searching$.next(true);
if (hasValue(this.searchSub)) {
this.searchSub.unsubscribe();
this.subs = this.subs.filter((sub: Subscription) => sub !== this.searchSub);
}
this.searchSub = this.paginationService.getCurrentPagination(this.config.id, this.config).pipe(
+ tap(() => this.loading$.next(true)),
switchMap((paginationOptions) => {
const query: string = data.query;
if (query != null && this.currentSearchQuery !== query) {
@@ -132,8 +133,8 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
}
return this.groupService.searchGroups(this.currentSearchQuery.trim(), {
currentPage: paginationOptions.currentPage,
- elementsPerPage: paginationOptions.pageSize
- });
+ elementsPerPage: paginationOptions.pageSize,
+ }, true, true, followLink('object'));
}),
getAllSucceededRemoteData(),
getRemoteDataPayload(),
@@ -141,39 +142,53 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
if (groups.page.length === 0) {
return observableOf(buildPaginatedList(groups.pageInfo, []));
}
- return observableCombineLatest(groups.page.map((group: Group) => {
- if (!this.deletedGroupsIds.includes(group.id)) {
- return observableCombineLatest([
- this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(group) ? group.self : undefined),
- this.hasLinkedDSO(group),
- this.getSubgroups(group),
- this.getMembers(group)
- ]).pipe(
- map(([isAuthorized, hasLinkedDSO, subgroups, members]:
- [boolean, boolean, RemoteData>, RemoteData>]) => {
- const groupDtoModel: GroupDtoModel = new GroupDtoModel();
- groupDtoModel.ableToDelete = isAuthorized && !hasLinkedDSO;
- groupDtoModel.group = group;
- groupDtoModel.subgroups = subgroups.payload;
- groupDtoModel.epersons = members.payload;
- return groupDtoModel;
- }
- )
- );
- }
- })).pipe(map((dtos: GroupDtoModel[]) => {
- return buildPaginatedList(groups.pageInfo, dtos);
- }));
+ return this.authorizationService.isAuthorized(FeatureID.AdministratorOf).pipe(
+ switchMap((isSiteAdmin: boolean) => {
+ return observableCombineLatest(groups.page.map((group: Group) => {
+ if (hasValue(group) && !this.deletedGroupsIds.includes(group.id)) {
+ return observableCombineLatest([
+ this.authorizationService.isAuthorized(FeatureID.CanDelete, group.self),
+ this.canManageGroup$(isSiteAdmin, group),
+ this.hasLinkedDSO(group),
+ this.getSubgroups(group),
+ this.getMembers(group)
+ ]).pipe(
+ map(([canDelete, canManageGroup, hasLinkedDSO, subgroups, members]:
+ [boolean, boolean, boolean, RemoteData>, RemoteData>]) => {
+ const groupDtoModel: GroupDtoModel = new GroupDtoModel();
+ groupDtoModel.ableToDelete = canDelete && !hasLinkedDSO;
+ groupDtoModel.ableToEdit = canManageGroup;
+ groupDtoModel.group = group;
+ groupDtoModel.subgroups = subgroups.payload;
+ groupDtoModel.epersons = members.payload;
+ return groupDtoModel;
+ }
+ )
+ );
+ }
+ })).pipe(map((dtos: GroupDtoModel[]) => {
+ return buildPaginatedList(groups.pageInfo, dtos);
+ }));
+ })
+ );
})
).subscribe((value: PaginatedList) => {
this.groupsDto$.next(value);
this.pageInfoState$.next(value.pageInfo);
- this.searching$.next(false);
+ this.loading$.next(false);
});
this.subs.push(this.searchSub);
}
+ canManageGroup$(isSiteAdmin: boolean, group: Group): Observable {
+ if (isSiteAdmin) {
+ return observableOf(true);
+ } else {
+ return this.authorizationService.isAuthorized(FeatureID.CanManageGroup, group.self);
+ }
+ }
+
/**
* Delete Group
*/
diff --git a/src/app/+admin/admin-curation-tasks/admin-curation-tasks.component.html b/src/app/admin/admin-curation-tasks/admin-curation-tasks.component.html
similarity index 100%
rename from src/app/+admin/admin-curation-tasks/admin-curation-tasks.component.html
rename to src/app/admin/admin-curation-tasks/admin-curation-tasks.component.html
diff --git a/src/app/+admin/admin-curation-tasks/admin-curation-tasks.component.spec.ts b/src/app/admin/admin-curation-tasks/admin-curation-tasks.component.spec.ts
similarity index 100%
rename from src/app/+admin/admin-curation-tasks/admin-curation-tasks.component.spec.ts
rename to src/app/admin/admin-curation-tasks/admin-curation-tasks.component.spec.ts
diff --git a/src/app/+admin/admin-curation-tasks/admin-curation-tasks.component.ts b/src/app/admin/admin-curation-tasks/admin-curation-tasks.component.ts
similarity index 100%
rename from src/app/+admin/admin-curation-tasks/admin-curation-tasks.component.ts
rename to src/app/admin/admin-curation-tasks/admin-curation-tasks.component.ts
diff --git a/src/app/+admin/admin-import-metadata-page/metadata-import-page.component.html b/src/app/admin/admin-import-metadata-page/metadata-import-page.component.html
similarity index 100%
rename from src/app/+admin/admin-import-metadata-page/metadata-import-page.component.html
rename to src/app/admin/admin-import-metadata-page/metadata-import-page.component.html
diff --git a/src/app/+admin/admin-import-metadata-page/metadata-import-page.component.spec.ts b/src/app/admin/admin-import-metadata-page/metadata-import-page.component.spec.ts
similarity index 87%
rename from src/app/+admin/admin-import-metadata-page/metadata-import-page.component.spec.ts
rename to src/app/admin/admin-import-metadata-page/metadata-import-page.component.spec.ts
index db6bb7db84..d663481b8c 100644
--- a/src/app/+admin/admin-import-metadata-page/metadata-import-page.component.spec.ts
+++ b/src/app/admin/admin-import-metadata-page/metadata-import-page.component.spec.ts
@@ -6,10 +6,7 @@ import { By } from '@angular/platform-browser';
import { Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { TranslateModule } from '@ngx-translate/core';
-import { of as observableOf } from 'rxjs';
-import { AuthService } from '../../core/auth/auth.service';
import { METADATA_IMPORT_SCRIPT_NAME, ScriptDataService } from '../../core/data/processes/script-data.service';
-import { EPerson } from '../../core/eperson/models/eperson.model';
import { ProcessParameter } from '../../process-page/processes/process-parameter.model';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
@@ -22,12 +19,9 @@ describe('MetadataImportPageComponent', () => {
let comp: MetadataImportPageComponent;
let fixture: ComponentFixture;
- let user;
-
let notificationService: NotificationsServiceStub;
let scriptService: any;
let router;
- let authService;
let locationStub;
function init() {
@@ -37,13 +31,6 @@ describe('MetadataImportPageComponent', () => {
invoke: createSuccessfulRemoteDataObject$({ processId: '45' })
}
);
- user = Object.assign(new EPerson(), {
- id: 'userId',
- email: 'user@test.com'
- });
- authService = jasmine.createSpyObj('authService', {
- getAuthenticatedUserFromStore: observableOf(user)
- });
router = jasmine.createSpyObj('router', {
navigateByUrl: jasmine.createSpy('navigateByUrl')
});
@@ -65,7 +52,6 @@ describe('MetadataImportPageComponent', () => {
{ provide: NotificationsService, useValue: notificationService },
{ provide: ScriptDataService, useValue: scriptService },
{ provide: Router, useValue: router },
- { provide: AuthService, useValue: authService },
{ provide: Location, useValue: locationStub },
],
schemas: [NO_ERRORS_SCHEMA]
@@ -107,9 +93,8 @@ describe('MetadataImportPageComponent', () => {
proceed.click();
fixture.detectChanges();
}));
- it('metadata-import script is invoked with its -e currentUserEmail, -f fileName and the mockFile', () => {
+ it('metadata-import script is invoked with -f fileName and the mockFile', () => {
const parameterValues: ProcessParameter[] = [
- Object.assign(new ProcessParameter(), { name: '-e', value: user.email }),
Object.assign(new ProcessParameter(), { name: '-f', value: 'filename.txt' }),
];
expect(scriptService.invoke).toHaveBeenCalledWith(METADATA_IMPORT_SCRIPT_NAME, parameterValues, [fileMock]);
diff --git a/src/app/+admin/admin-import-metadata-page/metadata-import-page.component.ts b/src/app/admin/admin-import-metadata-page/metadata-import-page.component.ts
similarity index 71%
rename from src/app/+admin/admin-import-metadata-page/metadata-import-page.component.ts
rename to src/app/admin/admin-import-metadata-page/metadata-import-page.component.ts
index bcef54377b..3bdcca3084 100644
--- a/src/app/+admin/admin-import-metadata-page/metadata-import-page.component.ts
+++ b/src/app/admin/admin-import-metadata-page/metadata-import-page.component.ts
@@ -23,20 +23,14 @@ import { getProcessDetailRoute } from '../../process-page/process-page-routing.p
/**
* Component that represents a metadata import page for administrators
*/
-export class MetadataImportPageComponent implements OnInit {
+export class MetadataImportPageComponent {
/**
* The current value of the file
*/
fileObject: File;
- /**
- * The authenticated user's email
- */
- private currentUserEmail$: Observable;
-
- public constructor(protected authService: AuthService,
- private location: Location,
+ public constructor(private location: Location,
protected translate: TranslateService,
protected notificationsService: NotificationsService,
private scriptDataService: ScriptDataService,
@@ -51,15 +45,6 @@ export class MetadataImportPageComponent implements OnInit {
this.fileObject = file;
}
- /**
- * Method provided by Angular. Invoked after the constructor.
- */
- ngOnInit() {
- this.currentUserEmail$ = this.authService.getAuthenticatedUserFromStore().pipe(
- map((user: EPerson) => user.email)
- );
- }
-
/**
* When return button is pressed go to previous location
*/
@@ -68,22 +53,17 @@ export class MetadataImportPageComponent implements OnInit {
}
/**
- * Starts import-metadata script with -e currentUserEmail -f fileName (and the selected file)
+ * Starts import-metadata script with -f fileName (and the selected file)
*/
public importMetadata() {
if (this.fileObject == null) {
this.notificationsService.error(this.translate.get('admin.metadata-import.page.error.addFile'));
} else {
- this.currentUserEmail$.pipe(
- switchMap((email: string) => {
- if (isNotEmpty(email)) {
- const parameterValues: ProcessParameter[] = [
- Object.assign(new ProcessParameter(), { name: '-e', value: email }),
- Object.assign(new ProcessParameter(), { name: '-f', value: this.fileObject.name }),
- ];
- return this.scriptDataService.invoke(METADATA_IMPORT_SCRIPT_NAME, parameterValues, [this.fileObject]);
- }
- }),
+ const parameterValues: ProcessParameter[] = [
+ Object.assign(new ProcessParameter(), { name: '-f', value: this.fileObject.name }),
+ ];
+
+ this.scriptDataService.invoke(METADATA_IMPORT_SCRIPT_NAME, parameterValues, [this.fileObject]).pipe(
getFirstCompletedRemoteData(),
).subscribe((rd: RemoteData) => {
if (rd.hasSucceeded) {
diff --git a/src/app/+admin/admin-registries/admin-registries-routing-paths.ts b/src/app/admin/admin-registries/admin-registries-routing-paths.ts
similarity index 100%
rename from src/app/+admin/admin-registries/admin-registries-routing-paths.ts
rename to src/app/admin/admin-registries/admin-registries-routing-paths.ts
diff --git a/src/app/+admin/admin-registries/admin-registries-routing.module.ts b/src/app/admin/admin-registries/admin-registries-routing.module.ts
similarity index 100%
rename from src/app/+admin/admin-registries/admin-registries-routing.module.ts
rename to src/app/admin/admin-registries/admin-registries-routing.module.ts
diff --git a/src/app/+admin/admin-registries/admin-registries.module.ts b/src/app/admin/admin-registries/admin-registries.module.ts
similarity index 100%
rename from src/app/+admin/admin-registries/admin-registries.module.ts
rename to src/app/admin/admin-registries/admin-registries.module.ts
diff --git a/src/app/+admin/admin-registries/bitstream-formats/add-bitstream-format/add-bitstream-format.component.html b/src/app/admin/admin-registries/bitstream-formats/add-bitstream-format/add-bitstream-format.component.html
similarity index 100%
rename from src/app/+admin/admin-registries/bitstream-formats/add-bitstream-format/add-bitstream-format.component.html
rename to src/app/admin/admin-registries/bitstream-formats/add-bitstream-format/add-bitstream-format.component.html
diff --git a/src/app/+admin/admin-registries/bitstream-formats/add-bitstream-format/add-bitstream-format.component.spec.ts b/src/app/admin/admin-registries/bitstream-formats/add-bitstream-format/add-bitstream-format.component.spec.ts
similarity index 100%
rename from src/app/+admin/admin-registries/bitstream-formats/add-bitstream-format/add-bitstream-format.component.spec.ts
rename to src/app/admin/admin-registries/bitstream-formats/add-bitstream-format/add-bitstream-format.component.spec.ts
diff --git a/src/app/+admin/admin-registries/bitstream-formats/add-bitstream-format/add-bitstream-format.component.ts b/src/app/admin/admin-registries/bitstream-formats/add-bitstream-format/add-bitstream-format.component.ts
similarity index 100%
rename from src/app/+admin/admin-registries/bitstream-formats/add-bitstream-format/add-bitstream-format.component.ts
rename to src/app/admin/admin-registries/bitstream-formats/add-bitstream-format/add-bitstream-format.component.ts
diff --git a/src/app/+admin/admin-registries/bitstream-formats/bitstream-format.actions.ts b/src/app/admin/admin-registries/bitstream-formats/bitstream-format.actions.ts
similarity index 100%
rename from src/app/+admin/admin-registries/bitstream-formats/bitstream-format.actions.ts
rename to src/app/admin/admin-registries/bitstream-formats/bitstream-format.actions.ts
diff --git a/src/app/+admin/admin-registries/bitstream-formats/bitstream-format.reducers.spec.ts b/src/app/admin/admin-registries/bitstream-formats/bitstream-format.reducers.spec.ts
similarity index 100%
rename from src/app/+admin/admin-registries/bitstream-formats/bitstream-format.reducers.spec.ts
rename to src/app/admin/admin-registries/bitstream-formats/bitstream-format.reducers.spec.ts
diff --git a/src/app/+admin/admin-registries/bitstream-formats/bitstream-format.reducers.ts b/src/app/admin/admin-registries/bitstream-formats/bitstream-format.reducers.ts
similarity index 100%
rename from src/app/+admin/admin-registries/bitstream-formats/bitstream-format.reducers.ts
rename to src/app/admin/admin-registries/bitstream-formats/bitstream-format.reducers.ts
diff --git a/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats-routing.module.ts b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats-routing.module.ts
similarity index 100%
rename from src/app/+admin/admin-registries/bitstream-formats/bitstream-formats-routing.module.ts
rename to src/app/admin/admin-registries/bitstream-formats/bitstream-formats-routing.module.ts
diff --git a/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.html b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.html
similarity index 100%
rename from src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.html
rename to src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.html
diff --git a/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts
similarity index 100%
rename from src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts
rename to src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts
diff --git a/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.ts b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.ts
similarity index 100%
rename from src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.ts
rename to src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.ts
diff --git a/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.module.ts b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.module.ts
similarity index 100%
rename from src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.module.ts
rename to src/app/admin/admin-registries/bitstream-formats/bitstream-formats.module.ts
diff --git a/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.resolver.ts b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.resolver.ts
similarity index 100%
rename from src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.resolver.ts
rename to src/app/admin/admin-registries/bitstream-formats/bitstream-formats.resolver.ts
diff --git a/src/app/+admin/admin-registries/bitstream-formats/edit-bitstream-format/edit-bitstream-format.component.html b/src/app/admin/admin-registries/bitstream-formats/edit-bitstream-format/edit-bitstream-format.component.html
similarity index 100%
rename from src/app/+admin/admin-registries/bitstream-formats/edit-bitstream-format/edit-bitstream-format.component.html
rename to src/app/admin/admin-registries/bitstream-formats/edit-bitstream-format/edit-bitstream-format.component.html
diff --git a/src/app/+admin/admin-registries/bitstream-formats/edit-bitstream-format/edit-bitstream-format.component.spec.ts b/src/app/admin/admin-registries/bitstream-formats/edit-bitstream-format/edit-bitstream-format.component.spec.ts
similarity index 100%
rename from src/app/+admin/admin-registries/bitstream-formats/edit-bitstream-format/edit-bitstream-format.component.spec.ts
rename to src/app/admin/admin-registries/bitstream-formats/edit-bitstream-format/edit-bitstream-format.component.spec.ts
diff --git a/src/app/+admin/admin-registries/bitstream-formats/edit-bitstream-format/edit-bitstream-format.component.ts b/src/app/admin/admin-registries/bitstream-formats/edit-bitstream-format/edit-bitstream-format.component.ts
similarity index 100%
rename from src/app/+admin/admin-registries/bitstream-formats/edit-bitstream-format/edit-bitstream-format.component.ts
rename to src/app/admin/admin-registries/bitstream-formats/edit-bitstream-format/edit-bitstream-format.component.ts
diff --git a/src/app/+admin/admin-registries/bitstream-formats/format-form/format-form.component.html b/src/app/admin/admin-registries/bitstream-formats/format-form/format-form.component.html
similarity index 100%
rename from src/app/+admin/admin-registries/bitstream-formats/format-form/format-form.component.html
rename to src/app/admin/admin-registries/bitstream-formats/format-form/format-form.component.html
diff --git a/src/app/+admin/admin-registries/bitstream-formats/format-form/format-form.component.spec.ts b/src/app/admin/admin-registries/bitstream-formats/format-form/format-form.component.spec.ts
similarity index 100%
rename from src/app/+admin/admin-registries/bitstream-formats/format-form/format-form.component.spec.ts
rename to src/app/admin/admin-registries/bitstream-formats/format-form/format-form.component.spec.ts
diff --git a/src/app/+admin/admin-registries/bitstream-formats/format-form/format-form.component.ts b/src/app/admin/admin-registries/bitstream-formats/format-form/format-form.component.ts
similarity index 100%
rename from src/app/+admin/admin-registries/bitstream-formats/format-form/format-form.component.ts
rename to src/app/admin/admin-registries/bitstream-formats/format-form/format-form.component.ts
diff --git a/src/app/+admin/admin-registries/metadata-registry/metadata-registry.actions.ts b/src/app/admin/admin-registries/metadata-registry/metadata-registry.actions.ts
similarity index 100%
rename from src/app/+admin/admin-registries/metadata-registry/metadata-registry.actions.ts
rename to src/app/admin/admin-registries/metadata-registry/metadata-registry.actions.ts
diff --git a/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.html b/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.html
similarity index 100%
rename from src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.html
rename to src/app/admin/admin-registries/metadata-registry/metadata-registry.component.html
diff --git a/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.scss b/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.scss
similarity index 100%
rename from src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.scss
rename to src/app/admin/admin-registries/metadata-registry/metadata-registry.component.scss
diff --git a/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.spec.ts b/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.spec.ts
similarity index 100%
rename from src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.spec.ts
rename to src/app/admin/admin-registries/metadata-registry/metadata-registry.component.spec.ts
diff --git a/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.ts b/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.ts
similarity index 100%
rename from src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.ts
rename to src/app/admin/admin-registries/metadata-registry/metadata-registry.component.ts
diff --git a/src/app/+admin/admin-registries/metadata-registry/metadata-registry.reducers.spec.ts b/src/app/admin/admin-registries/metadata-registry/metadata-registry.reducers.spec.ts
similarity index 100%
rename from src/app/+admin/admin-registries/metadata-registry/metadata-registry.reducers.spec.ts
rename to src/app/admin/admin-registries/metadata-registry/metadata-registry.reducers.spec.ts
diff --git a/src/app/+admin/admin-registries/metadata-registry/metadata-registry.reducers.ts b/src/app/admin/admin-registries/metadata-registry/metadata-registry.reducers.ts
similarity index 100%
rename from src/app/+admin/admin-registries/metadata-registry/metadata-registry.reducers.ts
rename to src/app/admin/admin-registries/metadata-registry/metadata-registry.reducers.ts
diff --git a/src/app/+admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.html b/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.html
similarity index 100%
rename from src/app/+admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.html
rename to src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.html
diff --git a/src/app/+admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.spec.ts b/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.spec.ts
similarity index 100%
rename from src/app/+admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.spec.ts
rename to src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.spec.ts
diff --git a/src/app/+admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.ts b/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.ts
similarity index 100%
rename from src/app/+admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.ts
rename to src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.ts
diff --git a/src/app/+admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.html b/src/app/admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.html
similarity index 100%
rename from src/app/+admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.html
rename to src/app/admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.html
diff --git a/src/app/+admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.spec.ts b/src/app/admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.spec.ts
similarity index 100%
rename from src/app/+admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.spec.ts
rename to src/app/admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.spec.ts
diff --git a/src/app/+admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.ts b/src/app/admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.ts
similarity index 100%
rename from src/app/+admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.ts
rename to src/app/admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.ts
diff --git a/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.html b/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.html
similarity index 100%
rename from src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.html
rename to src/app/admin/admin-registries/metadata-schema/metadata-schema.component.html
diff --git a/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.scss b/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.scss
similarity index 100%
rename from src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.scss
rename to src/app/admin/admin-registries/metadata-schema/metadata-schema.component.scss
diff --git a/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.spec.ts b/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.spec.ts
similarity index 100%
rename from src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.spec.ts
rename to src/app/admin/admin-registries/metadata-schema/metadata-schema.component.spec.ts
diff --git a/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.ts b/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.ts
similarity index 100%
rename from src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.ts
rename to src/app/admin/admin-registries/metadata-schema/metadata-schema.component.ts
diff --git a/src/app/+admin/admin-routing-paths.ts b/src/app/admin/admin-routing-paths.ts
similarity index 100%
rename from src/app/+admin/admin-routing-paths.ts
rename to src/app/admin/admin-routing-paths.ts
diff --git a/src/app/+admin/admin-routing.module.ts b/src/app/admin/admin-routing.module.ts
similarity index 100%
rename from src/app/+admin/admin-routing.module.ts
rename to src/app/admin/admin-routing.module.ts
diff --git a/src/app/+admin/admin-search-page/admin-search-page.component.html b/src/app/admin/admin-search-page/admin-search-page.component.html
similarity index 100%
rename from src/app/+admin/admin-search-page/admin-search-page.component.html
rename to src/app/admin/admin-search-page/admin-search-page.component.html
diff --git a/src/app/+admin/admin-search-page/admin-search-page.component.scss b/src/app/admin/admin-search-page/admin-search-page.component.scss
similarity index 100%
rename from src/app/+admin/admin-search-page/admin-search-page.component.scss
rename to src/app/admin/admin-search-page/admin-search-page.component.scss
diff --git a/src/app/+admin/admin-search-page/admin-search-page.component.spec.ts b/src/app/admin/admin-search-page/admin-search-page.component.spec.ts
similarity index 100%
rename from src/app/+admin/admin-search-page/admin-search-page.component.spec.ts
rename to src/app/admin/admin-search-page/admin-search-page.component.spec.ts
diff --git a/src/app/+admin/admin-search-page/admin-search-page.component.ts b/src/app/admin/admin-search-page/admin-search-page.component.ts
similarity index 100%
rename from src/app/+admin/admin-search-page/admin-search-page.component.ts
rename to src/app/admin/admin-search-page/admin-search-page.component.ts
diff --git a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component.html b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component.html
similarity index 100%
rename from src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component.html
rename to src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component.html
diff --git a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component.scss b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component.scss
similarity index 100%
rename from src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component.scss
rename to src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component.scss
diff --git a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component.spec.ts b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component.spec.ts
similarity index 96%
rename from src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component.spec.ts
rename to src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component.spec.ts
index 38762057ad..2cb0413bbc 100644
--- a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component.spec.ts
+++ b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component.spec.ts
@@ -12,7 +12,7 @@ import { CollectionSearchResult } from '../../../../../shared/object-collection/
import { Collection } from '../../../../../core/shared/collection.model';
import { By } from '@angular/platform-browser';
import { RouterTestingModule } from '@angular/router/testing';
-import { getCollectionEditRoute } from '../../../../../+collection-page/collection-page-routing-paths';
+import { getCollectionEditRoute } from '../../../../../collection-page/collection-page-routing-paths';
import { LinkService } from '../../../../../core/cache/builders/link.service';
describe('CollectionAdminSearchResultGridElementComponent', () => {
diff --git a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component.ts b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component.ts
similarity index 92%
rename from src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component.ts
rename to src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component.ts
index 9477544f60..1412090e0f 100644
--- a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component.ts
+++ b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component.ts
@@ -5,7 +5,7 @@ import { Context } from '../../../../../core/shared/context.model';
import { CollectionSearchResult } from '../../../../../shared/object-collection/shared/collection-search-result.model';
import { Collection } from '../../../../../core/shared/collection.model';
import { SearchResultGridElementComponent } from '../../../../../shared/object-grid/search-result-grid-element/search-result-grid-element.component';
-import { getCollectionEditRoute } from '../../../../../+collection-page/collection-page-routing-paths';
+import { getCollectionEditRoute } from '../../../../../collection-page/collection-page-routing-paths';
@listableObjectComponent(CollectionSearchResult, ViewMode.GridElement, Context.AdminSearch)
@Component({
diff --git a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component.html b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component.html
similarity index 100%
rename from src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component.html
rename to src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component.html
diff --git a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component.scss b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component.scss
similarity index 100%
rename from src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component.scss
rename to src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component.scss
diff --git a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component.spec.ts b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component.spec.ts
similarity index 96%
rename from src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component.spec.ts
rename to src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component.spec.ts
index 66df215f87..17ce2cd7a1 100644
--- a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component.spec.ts
+++ b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component.spec.ts
@@ -14,7 +14,7 @@ import { RouterTestingModule } from '@angular/router/testing';
import { CommunityAdminSearchResultGridElementComponent } from './community-admin-search-result-grid-element.component';
import { CommunitySearchResult } from '../../../../../shared/object-collection/shared/community-search-result.model';
import { Community } from '../../../../../core/shared/community.model';
-import { getCommunityEditRoute } from '../../../../../+community-page/community-page-routing-paths';
+import { getCommunityEditRoute } from '../../../../../community-page/community-page-routing-paths';
import { LinkService } from '../../../../../core/cache/builders/link.service';
describe('CommunityAdminSearchResultGridElementComponent', () => {
diff --git a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component.ts b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component.ts
similarity index 93%
rename from src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component.ts
rename to src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component.ts
index 59117b1f65..b0d603338b 100644
--- a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component.ts
+++ b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component.ts
@@ -5,7 +5,7 @@ import { Context } from '../../../../../core/shared/context.model';
import { CommunitySearchResult } from '../../../../../shared/object-collection/shared/community-search-result.model';
import { Community } from '../../../../../core/shared/community.model';
import { SearchResultGridElementComponent } from '../../../../../shared/object-grid/search-result-grid-element/search-result-grid-element.component';
-import { getCommunityEditRoute } from '../../../../../+community-page/community-page-routing-paths';
+import { getCommunityEditRoute } from '../../../../../community-page/community-page-routing-paths';
@listableObjectComponent(CommunitySearchResult, ViewMode.GridElement, Context.AdminSearch)
@Component({
diff --git a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.html b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.html
similarity index 100%
rename from src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.html
rename to src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.html
diff --git a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.scss b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.scss
similarity index 100%
rename from src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.scss
rename to src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.scss
diff --git a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.spec.ts b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.spec.ts
similarity index 100%
rename from src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.spec.ts
rename to src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.spec.ts
diff --git a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.ts b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.ts
similarity index 100%
rename from src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.ts
rename to src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.ts
diff --git a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-list-element/collection-search-result/collection-admin-search-result-list-element.component.html b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/collection-search-result/collection-admin-search-result-list-element.component.html
similarity index 100%
rename from src/app/+admin/admin-search-page/admin-search-results/admin-search-result-list-element/collection-search-result/collection-admin-search-result-list-element.component.html
rename to src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/collection-search-result/collection-admin-search-result-list-element.component.html
diff --git a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-list-element/collection-search-result/collection-admin-search-result-list-element.component.scss b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/collection-search-result/collection-admin-search-result-list-element.component.scss
similarity index 100%
rename from src/app/+admin/admin-search-page/admin-search-results/admin-search-result-list-element/collection-search-result/collection-admin-search-result-list-element.component.scss
rename to src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/collection-search-result/collection-admin-search-result-list-element.component.scss
diff --git a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-list-element/collection-search-result/collection-admin-search-result-list-element.component.spec.ts b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/collection-search-result/collection-admin-search-result-list-element.component.spec.ts
similarity index 96%
rename from src/app/+admin/admin-search-page/admin-search-results/admin-search-result-list-element/collection-search-result/collection-admin-search-result-list-element.component.spec.ts
rename to src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/collection-search-result/collection-admin-search-result-list-element.component.spec.ts
index 56e25264cf..b394caef56 100644
--- a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-list-element/collection-search-result/collection-admin-search-result-list-element.component.spec.ts
+++ b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/collection-search-result/collection-admin-search-result-list-element.component.spec.ts
@@ -10,7 +10,7 @@ import { CollectionSearchResult } from '../../../../../shared/object-collection/
import { Collection } from '../../../../../core/shared/collection.model';
import { By } from '@angular/platform-browser';
import { RouterTestingModule } from '@angular/router/testing';
-import { getCollectionEditRoute } from '../../../../../+collection-page/collection-page-routing-paths';
+import { getCollectionEditRoute } from '../../../../../collection-page/collection-page-routing-paths';
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
diff --git a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-list-element/collection-search-result/collection-admin-search-result-list-element.component.ts b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/collection-search-result/collection-admin-search-result-list-element.component.ts
similarity index 92%
rename from src/app/+admin/admin-search-page/admin-search-results/admin-search-result-list-element/collection-search-result/collection-admin-search-result-list-element.component.ts
rename to src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/collection-search-result/collection-admin-search-result-list-element.component.ts
index 9622888660..8bcf20b230 100644
--- a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-list-element/collection-search-result/collection-admin-search-result-list-element.component.ts
+++ b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/collection-search-result/collection-admin-search-result-list-element.component.ts
@@ -5,7 +5,7 @@ import { Context } from '../../../../../core/shared/context.model';
import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component';
import { CollectionSearchResult } from '../../../../../shared/object-collection/shared/collection-search-result.model';
import { Collection } from '../../../../../core/shared/collection.model';
-import { getCollectionEditRoute } from '../../../../../+collection-page/collection-page-routing-paths';
+import { getCollectionEditRoute } from '../../../../../collection-page/collection-page-routing-paths';
@listableObjectComponent(CollectionSearchResult, ViewMode.ListElement, Context.AdminSearch)
@Component({
diff --git a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component.html b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component.html
similarity index 100%
rename from src/app/+admin/admin-search-page/admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component.html
rename to src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component.html
diff --git a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component.scss b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component.scss
similarity index 100%
rename from src/app/+admin/admin-search-page/admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component.scss
rename to src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component.scss
diff --git a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component.spec.ts b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component.spec.ts
similarity index 96%
rename from src/app/+admin/admin-search-page/admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component.spec.ts
rename to src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component.spec.ts
index 29d9925326..155d7f7509 100644
--- a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component.spec.ts
+++ b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component.spec.ts
@@ -10,7 +10,7 @@ import { RouterTestingModule } from '@angular/router/testing';
import { CommunityAdminSearchResultListElementComponent } from './community-admin-search-result-list-element.component';
import { CommunitySearchResult } from '../../../../../shared/object-collection/shared/community-search-result.model';
import { Community } from '../../../../../core/shared/community.model';
-import { getCommunityEditRoute } from '../../../../../+community-page/community-page-routing-paths';
+import { getCommunityEditRoute } from '../../../../../community-page/community-page-routing-paths';
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
diff --git a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component.ts b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component.ts
similarity index 93%
rename from src/app/+admin/admin-search-page/admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component.ts
rename to src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component.ts
index 2e82e7f3b8..9419ae3f3f 100644
--- a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component.ts
+++ b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component.ts
@@ -5,7 +5,7 @@ import { Context } from '../../../../../core/shared/context.model';
import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component';
import { CommunitySearchResult } from '../../../../../shared/object-collection/shared/community-search-result.model';
import { Community } from '../../../../../core/shared/community.model';
-import { getCommunityEditRoute } from '../../../../../+community-page/community-page-routing-paths';
+import { getCommunityEditRoute } from '../../../../../community-page/community-page-routing-paths';
@listableObjectComponent(CommunitySearchResult, ViewMode.ListElement, Context.AdminSearch)
@Component({
diff --git a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-list-element/item-search-result/item-admin-search-result-list-element.component.html b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/item-search-result/item-admin-search-result-list-element.component.html
similarity index 100%
rename from src/app/+admin/admin-search-page/admin-search-results/admin-search-result-list-element/item-search-result/item-admin-search-result-list-element.component.html
rename to src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/item-search-result/item-admin-search-result-list-element.component.html
diff --git a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-list-element/item-search-result/item-admin-search-result-list-element.component.scss b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/item-search-result/item-admin-search-result-list-element.component.scss
similarity index 100%
rename from src/app/+admin/admin-search-page/admin-search-results/admin-search-result-list-element/item-search-result/item-admin-search-result-list-element.component.scss
rename to src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/item-search-result/item-admin-search-result-list-element.component.scss
diff --git a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-list-element/item-search-result/item-admin-search-result-list-element.component.spec.ts b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/item-search-result/item-admin-search-result-list-element.component.spec.ts
similarity index 100%
rename from src/app/+admin/admin-search-page/admin-search-results/admin-search-result-list-element/item-search-result/item-admin-search-result-list-element.component.spec.ts
rename to src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/item-search-result/item-admin-search-result-list-element.component.spec.ts
diff --git a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-list-element/item-search-result/item-admin-search-result-list-element.component.ts b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/item-search-result/item-admin-search-result-list-element.component.ts
similarity index 100%
rename from src/app/+admin/admin-search-page/admin-search-results/admin-search-result-list-element/item-search-result/item-admin-search-result-list-element.component.ts
rename to src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/item-search-result/item-admin-search-result-list-element.component.ts
diff --git a/src/app/+admin/admin-search-page/admin-search-results/item-admin-search-result-actions.component.html b/src/app/admin/admin-search-page/admin-search-results/item-admin-search-result-actions.component.html
similarity index 100%
rename from src/app/+admin/admin-search-page/admin-search-results/item-admin-search-result-actions.component.html
rename to src/app/admin/admin-search-page/admin-search-results/item-admin-search-result-actions.component.html
diff --git a/src/app/+admin/admin-search-page/admin-search-results/item-admin-search-result-actions.component.scss b/src/app/admin/admin-search-page/admin-search-results/item-admin-search-result-actions.component.scss
similarity index 100%
rename from src/app/+admin/admin-search-page/admin-search-results/item-admin-search-result-actions.component.scss
rename to src/app/admin/admin-search-page/admin-search-results/item-admin-search-result-actions.component.scss
diff --git a/src/app/+admin/admin-search-page/admin-search-results/item-admin-search-result-actions.component.spec.ts b/src/app/admin/admin-search-page/admin-search-results/item-admin-search-result-actions.component.spec.ts
similarity index 97%
rename from src/app/+admin/admin-search-page/admin-search-results/item-admin-search-result-actions.component.spec.ts
rename to src/app/admin/admin-search-page/admin-search-results/item-admin-search-result-actions.component.spec.ts
index 2696e621de..f354ac5f89 100644
--- a/src/app/+admin/admin-search-page/admin-search-results/item-admin-search-result-actions.component.spec.ts
+++ b/src/app/admin/admin-search-page/admin-search-results/item-admin-search-result-actions.component.spec.ts
@@ -7,7 +7,7 @@ import { RouterTestingModule } from '@angular/router/testing';
import { ItemAdminSearchResultActionsComponent } from './item-admin-search-result-actions.component';
import { Item } from '../../../core/shared/item.model';
import { URLCombiner } from '../../../core/url-combiner/url-combiner';
-import { getItemEditRoute } from '../../../+item-page/item-page-routing-paths';
+import { getItemEditRoute } from '../../../item-page/item-page-routing-paths';
import {
ITEM_EDIT_DELETE_PATH,
ITEM_EDIT_MOVE_PATH,
@@ -15,7 +15,7 @@ import {
ITEM_EDIT_PUBLIC_PATH,
ITEM_EDIT_REINSTATE_PATH,
ITEM_EDIT_WITHDRAW_PATH
-} from '../../../+item-page/edit-item-page/edit-item-page.routing-paths';
+} from '../../../item-page/edit-item-page/edit-item-page.routing-paths';
describe('ItemAdminSearchResultActionsComponent', () => {
let component: ItemAdminSearchResultActionsComponent;
diff --git a/src/app/+admin/admin-search-page/admin-search-results/item-admin-search-result-actions.component.ts b/src/app/admin/admin-search-page/admin-search-results/item-admin-search-result-actions.component.ts
similarity index 93%
rename from src/app/+admin/admin-search-page/admin-search-results/item-admin-search-result-actions.component.ts
rename to src/app/admin/admin-search-page/admin-search-results/item-admin-search-result-actions.component.ts
index cded519796..fcc3cf0f17 100644
--- a/src/app/+admin/admin-search-page/admin-search-results/item-admin-search-result-actions.component.ts
+++ b/src/app/admin/admin-search-page/admin-search-results/item-admin-search-result-actions.component.ts
@@ -1,7 +1,7 @@
import { Component, Input } from '@angular/core';
import { Item } from '../../../core/shared/item.model';
import { URLCombiner } from '../../../core/url-combiner/url-combiner';
-import { getItemEditRoute } from '../../../+item-page/item-page-routing-paths';
+import { getItemEditRoute } from '../../../item-page/item-page-routing-paths';
import {
ITEM_EDIT_MOVE_PATH,
ITEM_EDIT_DELETE_PATH,
@@ -9,7 +9,7 @@ import {
ITEM_EDIT_PRIVATE_PATH,
ITEM_EDIT_REINSTATE_PATH,
ITEM_EDIT_WITHDRAW_PATH
-} from '../../../+item-page/edit-item-page/edit-item-page.routing-paths';
+} from '../../../item-page/edit-item-page/edit-item-page.routing-paths';
@Component({
selector: 'ds-item-admin-search-result-actions-element',
diff --git a/src/app/+admin/admin-search-page/admin-search.module.ts b/src/app/admin/admin-search-page/admin-search.module.ts
similarity index 100%
rename from src/app/+admin/admin-search-page/admin-search.module.ts
rename to src/app/admin/admin-search-page/admin-search.module.ts
diff --git a/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.html b/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.html
new file mode 100644
index 0000000000..8706b40ee0
--- /dev/null
+++ b/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.html
@@ -0,0 +1,23 @@
+
diff --git a/src/app/+admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.scss b/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.scss
similarity index 100%
rename from src/app/+admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.scss
rename to src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.scss
diff --git a/src/app/+admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.spec.ts b/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.spec.ts
similarity index 100%
rename from src/app/+admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.spec.ts
rename to src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.spec.ts
diff --git a/src/app/+admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.ts b/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.ts
similarity index 65%
rename from src/app/+admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.ts
rename to src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.ts
index a19a1f95e4..50f9f8a79e 100644
--- a/src/app/+admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.ts
+++ b/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.ts
@@ -5,12 +5,15 @@ import { MenuService } from '../../../shared/menu/menu.service';
import { rendersSectionForMenu } from '../../../shared/menu/menu-section.decorator';
import { LinkMenuItemModel } from '../../../shared/menu/menu-item/models/link.model';
import { MenuSection } from '../../../shared/menu/menu.reducer';
+import { isNotEmpty } from '../../../shared/empty.util';
+import { Router } from '@angular/router';
/**
* Represents a non-expandable section in the admin sidebar
*/
@Component({
- selector: 'ds-admin-sidebar-section',
+ /* tslint:disable:component-selector */
+ selector: 'li[ds-admin-sidebar-section]',
templateUrl: './admin-sidebar-section.component.html',
styleUrls: ['./admin-sidebar-section.component.scss'],
@@ -23,12 +26,26 @@ export class AdminSidebarSectionComponent extends MenuSectionComponent implement
*/
menuID: MenuID = MenuID.ADMIN;
itemModel;
- constructor(@Inject('sectionDataProvider') menuSection: MenuSection, protected menuService: MenuService, protected injector: Injector,) {
+ hasLink: boolean;
+ constructor(
+ @Inject('sectionDataProvider') menuSection: MenuSection,
+ protected menuService: MenuService,
+ protected injector: Injector,
+ protected router: Router,
+ ) {
super(menuSection, menuService, injector);
this.itemModel = menuSection.model as LinkMenuItemModel;
}
ngOnInit(): void {
+ this.hasLink = isNotEmpty(this.itemModel?.link);
super.ngOnInit();
}
+
+ navigate(event: any): void {
+ event.preventDefault();
+ if (this.hasLink) {
+ this.router.navigate(this.itemModel.link);
+ }
+ }
}
diff --git a/src/app/+admin/admin-sidebar/admin-sidebar.component.html b/src/app/admin/admin-sidebar/admin-sidebar.component.html
similarity index 53%
rename from src/app/+admin/admin-sidebar/admin-sidebar.component.html
rename to src/app/admin/admin-sidebar/admin-sidebar.component.html
index 4dddc89920..84402c64e9 100644
--- a/src/app/+admin/admin-sidebar/admin-sidebar.component.html
+++ b/src/app/admin/admin-sidebar/admin-sidebar.component.html
@@ -4,24 +4,26 @@
value: (!(sidebarExpanded | async) ? 'collapsed' : 'expanded'),
params: {sidebarWidth: (sidebarWidth | async)}
}" (@slideSidebar.done)="finishSlide($event)" (@slideSidebar.start)="startSlide($event)"
- *ngIf="menuVisible | async" (mouseenter)="expandPreview($event)"
- (mouseleave)="collapsePreview($event)"
+ *ngIf="menuVisible | async"
+ (mouseenter)="handleMouseEnter($event)"
+ (mouseleave)="handleMouseLeave($event)"
role="navigation" [attr.aria-label]="'menu.header.admin.description' |translate">
diff --git a/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.scss b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.scss
similarity index 100%
rename from src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.scss
rename to src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.scss
diff --git a/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts
similarity index 92%
rename from src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts
rename to src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts
index 2e7eb4e1d1..67f8866e6d 100644
--- a/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts
+++ b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts
@@ -18,12 +18,8 @@ import { hasValue } from '../../shared/empty.util';
import { FormControl, FormGroup } from '@angular/forms';
import { FileSizePipe } from '../../shared/utils/file-size-pipe';
import { VarDirective } from '../../shared/utils/var.directive';
-import {
- createSuccessfulRemoteDataObject,
- createSuccessfulRemoteDataObject$
-} from '../../shared/remote-data.utils';
-import { RouterStub } from '../../shared/testing/router.stub';
-import { getEntityEditRoute, getItemEditRoute } from '../../+item-page/item-page-routing-paths';
+import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
+import { getEntityEditRoute } from '../../item-page/item-page-routing-paths';
import { createPaginatedList } from '../../shared/testing/utils.test';
import { Item } from '../../core/shared/item.model';
@@ -39,7 +35,6 @@ let bitstream: Bitstream;
let selectedFormat: BitstreamFormat;
let allFormats: BitstreamFormat[];
let router: Router;
-let routerStub;
describe('EditBitstreamPageComponent', () => {
let comp: EditBitstreamPageComponent;
@@ -129,10 +124,6 @@ describe('EditBitstreamPageComponent', () => {
findAll: createSuccessfulRemoteDataObject$(createPaginatedList(allFormats))
});
- const itemPageUrl = `fake-url/some-uuid`;
- routerStub = Object.assign(new RouterStub(), {
- url: `${itemPageUrl}`
- });
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), RouterTestingModule],
declarations: [EditBitstreamPageComponent, FileSizePipe, VarDirective],
@@ -142,7 +133,6 @@ describe('EditBitstreamPageComponent', () => {
{ provide: ActivatedRoute, useValue: { data: observableOf({ bitstream: createSuccessfulRemoteDataObject(bitstream) }), snapshot: { queryParams: {} } } },
{ provide: BitstreamDataService, useValue: bitstreamService },
{ provide: BitstreamFormatDataService, useValue: bitstreamFormatService },
- { provide: Router, useValue: routerStub },
ChangeDetectorRef
],
schemas: [NO_ERRORS_SCHEMA]
@@ -154,7 +144,8 @@ describe('EditBitstreamPageComponent', () => {
fixture = TestBed.createComponent(EditBitstreamPageComponent);
comp = fixture.componentInstance;
fixture.detectChanges();
- router = (comp as any).router;
+ router = TestBed.inject(Router);
+ spyOn(router, 'navigate');
});
describe('on startup', () => {
@@ -241,14 +232,14 @@ describe('EditBitstreamPageComponent', () => {
it('should redirect to the item edit page on the bitstreams tab with the itemId from the component', () => {
comp.itemId = 'some-uuid1';
comp.navigateToItemEditBitstreams();
- expect(routerStub.navigate).toHaveBeenCalledWith([getEntityEditRoute(null, 'some-uuid1'), 'bitstreams']);
+ expect(router.navigate).toHaveBeenCalledWith([getEntityEditRoute(null, 'some-uuid1'), 'bitstreams']);
});
});
describe('when navigateToItemEditBitstreams is called, and the component does not have an itemId', () => {
it('should redirect to the item edit page on the bitstreams tab with the itemId from the bundle links ', () => {
comp.itemId = undefined;
comp.navigateToItemEditBitstreams();
- expect(routerStub.navigate).toHaveBeenCalledWith([getEntityEditRoute(null, 'some-uuid'), 'bitstreams']);
+ expect(router.navigate).toHaveBeenCalledWith([getEntityEditRoute(null, 'some-uuid'), 'bitstreams']);
});
});
});
diff --git a/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts
similarity index 95%
rename from src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts
rename to src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts
index 8a4d584647..f6ece7f4fa 100644
--- a/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts
+++ b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts
@@ -19,10 +19,10 @@ import { cloneDeep } from 'lodash';
import { BitstreamDataService } from '../../core/data/bitstream-data.service';
import {
getAllSucceededRemoteDataPayload,
- getFirstSucceededRemoteDataPayload,
- getRemoteDataPayload,
+ getFirstCompletedRemoteData,
getFirstSucceededRemoteData,
- getFirstCompletedRemoteData
+ getFirstSucceededRemoteDataPayload,
+ getRemoteDataPayload
} from '../../core/shared/operators';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { BitstreamFormatDataService } from '../../core/data/bitstream-format-data.service';
@@ -33,7 +33,7 @@ import { Metadata } from '../../core/shared/metadata.utils';
import { Location } from '@angular/common';
import { RemoteData } from '../../core/data/remote-data';
import { PaginatedList } from '../../core/data/paginated-list.model';
-import { getEntityEditRoute, getItemEditRoute } from '../../+item-page/item-page-routing-paths';
+import { getEntityEditRoute, getItemEditRoute } from '../../item-page/item-page-routing-paths';
import { Bundle } from '../../core/shared/bundle.model';
@Component({
@@ -131,15 +131,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
rows: 10
});
- /**
- * The Dynamic Input Model for the file's embargo (disabled on this page)
- */
- embargoModel = new DynamicInputModel({
- id: 'embargo',
- name: 'embargo',
- disabled: true
- });
-
/**
* The Dynamic Input Model for the selected format
*/
@@ -159,7 +150,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
/**
* All input models in a simple array for easier iterations
*/
- inputModels = [this.fileNameModel, this.primaryBitstreamModel, this.descriptionModel, this.embargoModel, this.selectedFormatModel, this.newFormatModel];
+ inputModels = [this.fileNameModel, this.primaryBitstreamModel, this.descriptionModel, this.selectedFormatModel, this.newFormatModel];
/**
* The dynamic form fields used for editing the information of a bitstream
@@ -179,12 +170,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
this.descriptionModel
]
}),
- new DynamicFormGroupModel({
- id: 'embargoContainer',
- group: [
- this.embargoModel
- ]
- }),
new DynamicFormGroupModel({
id: 'formatContainer',
group: [
@@ -243,11 +228,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
host: 'row'
}
},
- embargoContainer: {
- grid: {
- host: 'row'
- }
- },
formatContainer: {
grid: {
host: 'row'
diff --git a/src/app/bitstream-page/legacy-bitstream-url.resolver.spec.ts b/src/app/bitstream-page/legacy-bitstream-url.resolver.spec.ts
new file mode 100644
index 0000000000..25e245c5b7
--- /dev/null
+++ b/src/app/bitstream-page/legacy-bitstream-url.resolver.spec.ts
@@ -0,0 +1,145 @@
+import { LegacyBitstreamUrlResolver } from './legacy-bitstream-url.resolver';
+import { of as observableOf, EMPTY } from 'rxjs';
+import { BitstreamDataService } from '../core/data/bitstream-data.service';
+import { RemoteData } from '../core/data/remote-data';
+import { RequestEntryState } from '../core/data/request.reducer';
+import { TestScheduler } from 'rxjs/testing';
+
+describe(`LegacyBitstreamUrlResolver`, () => {
+ let resolver: LegacyBitstreamUrlResolver;
+ 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 = new LegacyBitstreamUrlResolver(bitstreamDataService);
+ });
+
+ 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.resolve(route, state);
+ 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.resolve(route, state);
+ 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.resolve(route, state);
+ 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.resolve(route, state)).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.resolve(route, state)).toBe(expected, values);
+ });
+ });
+ });
+ });
+});
diff --git a/src/app/bitstream-page/legacy-bitstream-url.resolver.ts b/src/app/bitstream-page/legacy-bitstream-url.resolver.ts
new file mode 100644
index 0000000000..948bec2473
--- /dev/null
+++ b/src/app/bitstream-page/legacy-bitstream-url.resolver.ts
@@ -0,0 +1,48 @@
+import { Injectable } from '@angular/core';
+import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
+import { Observable } from 'rxjs';
+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';
+import { BitstreamDataService } from '../core/data/bitstream-data.service';
+
+/**
+ * This class resolves a bitstream based on the DSpace 6 XMLUI or JSPUI bitstream download URLs
+ */
+@Injectable({
+ providedIn: 'root'
+})
+export class LegacyBitstreamUrlResolver implements Resolve> {
+ constructor(protected bitstreamDataService: BitstreamDataService) {
+ }
+
+ /**
+ * 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
+ * @returns Observable<> Emits the found bitstream based on the parameters in
+ * current route, or an error if something went wrong
+ */
+ resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot):
+ Observable> {
+ 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 this.bitstreamDataService.findByItemHandle(
+ `${prefix}/${suffix}`,
+ sequenceId,
+ filename,
+ ).pipe(
+ getFirstCompletedRemoteData()
+ );
+ }
+}
diff --git a/src/app/+browse-by/+browse-by-date-page/browse-by-date-page.component.spec.ts b/src/app/browse-by/browse-by-date-page/browse-by-date-page.component.spec.ts
similarity index 96%
rename from src/app/+browse-by/+browse-by-date-page/browse-by-date-page.component.spec.ts
rename to src/app/browse-by/browse-by-date-page/browse-by-date-page.component.spec.ts
index a5cc69e430..7b0ddcb18e 100644
--- a/src/app/+browse-by/+browse-by-date-page/browse-by-date-page.component.spec.ts
+++ b/src/app/browse-by/browse-by-date-page/browse-by-date-page.component.spec.ts
@@ -15,7 +15,7 @@ import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
import { Community } from '../../core/shared/community.model';
import { Item } from '../../core/shared/item.model';
import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model';
-import { toRemoteData } from '../+browse-by-metadata-page/browse-by-metadata-page.component.spec';
+import { toRemoteData } from '../browse-by-metadata-page/browse-by-metadata-page.component.spec';
import { VarDirective } from '../../shared/utils/var.directive';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
@@ -107,6 +107,6 @@ describe('BrowseByDatePageComponent', () => {
});
it('should create a list of startsWith options with the current year first', () => {
- expect(comp.startsWithOptions[0]).toEqual(new Date().getFullYear());
+ expect(comp.startsWithOptions[0]).toEqual(new Date().getUTCFullYear());
});
});
diff --git a/src/app/+browse-by/+browse-by-date-page/browse-by-date-page.component.ts b/src/app/browse-by/browse-by-date-page/browse-by-date-page.component.ts
similarity index 93%
rename from src/app/+browse-by/+browse-by-date-page/browse-by-date-page.component.ts
rename to src/app/browse-by/browse-by-date-page/browse-by-date-page.component.ts
index a9eaa09e2f..3158c3d7cc 100644
--- a/src/app/+browse-by/+browse-by-date-page/browse-by-date-page.component.ts
+++ b/src/app/browse-by/browse-by-date-page/browse-by-date-page.component.ts
@@ -2,7 +2,7 @@ import { ChangeDetectorRef, Component } from '@angular/core';
import {
BrowseByMetadataPageComponent,
browseParamsToOptions
-} from '../+browse-by-metadata-page/browse-by-metadata-page.component';
+} from '../browse-by-metadata-page/browse-by-metadata-page.component';
import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model';
import { combineLatest as observableCombineLatest } from 'rxjs';
import { RemoteData } from '../../core/data/remote-data';
@@ -12,7 +12,7 @@ import { ActivatedRoute, Params, Router } from '@angular/router';
import { BrowseService } from '../../core/browse/browse.service';
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
import { StartsWithType } from '../../shared/starts-with/starts-with-decorator';
-import { BrowseByType, rendersBrowseBy } from '../+browse-by-switcher/browse-by-decorator';
+import { BrowseByType, rendersBrowseBy } from '../browse-by-switcher/browse-by-decorator';
import { environment } from '../../../environments/environment';
import { PaginationService } from '../../core/pagination/pagination.service';
import { map } from 'rxjs/operators';
@@ -21,8 +21,8 @@ import { SortDirection, SortOptions } from '../../core/cache/models/sort-options
@Component({
selector: 'ds-browse-by-date-page',
- styleUrls: ['../+browse-by-metadata-page/browse-by-metadata-page.component.scss'],
- templateUrl: '../+browse-by-metadata-page/browse-by-metadata-page.component.html'
+ styleUrls: ['../browse-by-metadata-page/browse-by-metadata-page.component.scss'],
+ templateUrl: '../browse-by-metadata-page/browse-by-metadata-page.component.html'
})
/**
* Component for browsing items by metadata definition of type 'date'
@@ -92,7 +92,7 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
}
}
const options = [];
- const currentYear = new Date().getFullYear();
+ const currentYear = new Date().getUTCFullYear();
const oneYearBreak = Math.floor((currentYear - environment.browseBy.oneYearLimit) / 5) * 5;
const fiveYearBreak = Math.floor((currentYear - environment.browseBy.fiveYearLimit) / 10) * 10;
if (lowerLimit <= fiveYearBreak) {
diff --git a/src/app/+browse-by/browse-by-dso-breadcrumb.resolver.ts b/src/app/browse-by/browse-by-dso-breadcrumb.resolver.ts
similarity index 100%
rename from src/app/+browse-by/browse-by-dso-breadcrumb.resolver.ts
rename to src/app/browse-by/browse-by-dso-breadcrumb.resolver.ts
diff --git a/src/app/+browse-by/browse-by-guard.spec.ts b/src/app/browse-by/browse-by-guard.spec.ts
similarity index 100%
rename from src/app/+browse-by/browse-by-guard.spec.ts
rename to src/app/browse-by/browse-by-guard.spec.ts
diff --git a/src/app/+browse-by/browse-by-guard.ts b/src/app/browse-by/browse-by-guard.ts
similarity index 100%
rename from src/app/+browse-by/browse-by-guard.ts
rename to src/app/browse-by/browse-by-guard.ts
diff --git a/src/app/+browse-by/browse-by-i18n-breadcrumb.resolver.ts b/src/app/browse-by/browse-by-i18n-breadcrumb.resolver.ts
similarity index 100%
rename from src/app/+browse-by/browse-by-i18n-breadcrumb.resolver.ts
rename to src/app/browse-by/browse-by-i18n-breadcrumb.resolver.ts
diff --git a/src/app/+browse-by/+browse-by-metadata-page/browse-by-metadata-page.component.html b/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.html
similarity index 100%
rename from src/app/+browse-by/+browse-by-metadata-page/browse-by-metadata-page.component.html
rename to src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.html
diff --git a/src/app/+browse-by/+browse-by-metadata-page/browse-by-metadata-page.component.scss b/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.scss
similarity index 100%
rename from src/app/+browse-by/+browse-by-metadata-page/browse-by-metadata-page.component.scss
rename to src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.scss
diff --git a/src/app/+browse-by/+browse-by-metadata-page/browse-by-metadata-page.component.spec.ts b/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.spec.ts
similarity index 100%
rename from src/app/+browse-by/+browse-by-metadata-page/browse-by-metadata-page.component.spec.ts
rename to src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.spec.ts
diff --git a/src/app/+browse-by/+browse-by-metadata-page/browse-by-metadata-page.component.ts b/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.ts
similarity index 98%
rename from src/app/+browse-by/+browse-by-metadata-page/browse-by-metadata-page.component.ts
rename to src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.ts
index f5adefc779..3573ffb264 100644
--- a/src/app/+browse-by/+browse-by-metadata-page/browse-by-metadata-page.component.ts
+++ b/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.ts
@@ -14,7 +14,7 @@ import { getFirstSucceededRemoteData } from '../../core/shared/operators';
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { StartsWithType } from '../../shared/starts-with/starts-with-decorator';
-import { BrowseByType, rendersBrowseBy } from '../+browse-by-switcher/browse-by-decorator';
+import { BrowseByType, rendersBrowseBy } from '../browse-by-switcher/browse-by-decorator';
import { PaginationService } from '../../core/pagination/pagination.service';
import { map } from 'rxjs/operators';
@@ -167,7 +167,6 @@ export class BrowseByMetadataPageComponent implements OnInit {
* @param value The value of the browse-entry to display items for
*/
updatePageWithItems(searchOptions: BrowseEntrySearchOptions, value: string) {
- console.log('updatePAge', searchOptions);
this.items$ = this.browseService.getBrowseItemsFor(value, searchOptions);
}
diff --git a/src/app/+browse-by/browse-by-page.module.ts b/src/app/browse-by/browse-by-page.module.ts
similarity index 100%
rename from src/app/+browse-by/browse-by-page.module.ts
rename to src/app/browse-by/browse-by-page.module.ts
diff --git a/src/app/+browse-by/browse-by-routing.module.ts b/src/app/browse-by/browse-by-routing.module.ts
similarity index 89%
rename from src/app/+browse-by/browse-by-routing.module.ts
rename to src/app/browse-by/browse-by-routing.module.ts
index 8cf989695a..72d78f13fd 100644
--- a/src/app/+browse-by/browse-by-routing.module.ts
+++ b/src/app/browse-by/browse-by-routing.module.ts
@@ -3,7 +3,7 @@ import { NgModule } from '@angular/core';
import { BrowseByGuard } from './browse-by-guard';
import { BrowseByDSOBreadcrumbResolver } from './browse-by-dso-breadcrumb.resolver';
import { BrowseByI18nBreadcrumbResolver } from './browse-by-i18n-breadcrumb.resolver';
-import { ThemedBrowseBySwitcherComponent } from './+browse-by-switcher/themed-browse-by-switcher.component';
+import { ThemedBrowseBySwitcherComponent } from './browse-by-switcher/themed-browse-by-switcher.component';
@NgModule({
imports: [
diff --git a/src/app/+browse-by/+browse-by-switcher/browse-by-decorator.spec.ts b/src/app/browse-by/browse-by-switcher/browse-by-decorator.spec.ts
similarity index 100%
rename from src/app/+browse-by/+browse-by-switcher/browse-by-decorator.spec.ts
rename to src/app/browse-by/browse-by-switcher/browse-by-decorator.spec.ts
diff --git a/src/app/+browse-by/+browse-by-switcher/browse-by-decorator.ts b/src/app/browse-by/browse-by-switcher/browse-by-decorator.ts
similarity index 74%
rename from src/app/+browse-by/+browse-by-switcher/browse-by-decorator.ts
rename to src/app/browse-by/browse-by-switcher/browse-by-decorator.ts
index 0143377922..efb4a4a9f4 100644
--- a/src/app/+browse-by/+browse-by-switcher/browse-by-decorator.ts
+++ b/src/app/browse-by/browse-by-switcher/browse-by-decorator.ts
@@ -1,4 +1,6 @@
import { hasNoValue } from '../../shared/empty.util';
+import { InjectionToken } from '@angular/core';
+import { GenericConstructor } from '../../core/shared/generic-constructor';
export enum BrowseByType {
Title = 'title',
@@ -8,6 +10,11 @@ export enum BrowseByType {
export const DEFAULT_BROWSE_BY_TYPE = BrowseByType.Metadata;
+export const BROWSE_BY_COMPONENT_FACTORY = new InjectionToken<(browseByType) => GenericConstructor>('getComponentByBrowseByType', {
+ providedIn: 'root',
+ factory: () => getComponentByBrowseByType
+});
+
const map = new Map();
/**
diff --git a/src/app/+browse-by/+browse-by-switcher/browse-by-switcher.component.html b/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.html
similarity index 100%
rename from src/app/+browse-by/+browse-by-switcher/browse-by-switcher.component.html
rename to src/app/browse-by/browse-by-switcher/browse-by-switcher.component.html
diff --git a/src/app/+browse-by/+browse-by-switcher/browse-by-switcher.component.spec.ts b/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.spec.ts
similarity index 81%
rename from src/app/+browse-by/+browse-by-switcher/browse-by-switcher.component.spec.ts
rename to src/app/browse-by/browse-by-switcher/browse-by-switcher.component.spec.ts
index 545b766c88..f340237e26 100644
--- a/src/app/+browse-by/+browse-by-switcher/browse-by-switcher.component.spec.ts
+++ b/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.spec.ts
@@ -2,12 +2,11 @@ import { BrowseBySwitcherComponent } from './browse-by-switcher.component';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
-import * as decorator from './browse-by-decorator';
import { BehaviorSubject } from 'rxjs';
import { environment } from '../../../environments/environment';
-import createSpy = jasmine.createSpy;
+import { BROWSE_BY_COMPONENT_FACTORY } from './browse-by-decorator';
-xdescribe('BrowseBySwitcherComponent', () => {
+describe('BrowseBySwitcherComponent', () => {
let comp: BrowseBySwitcherComponent;
let fixture: ComponentFixture;
@@ -23,7 +22,8 @@ xdescribe('BrowseBySwitcherComponent', () => {
TestBed.configureTestingModule({
declarations: [BrowseBySwitcherComponent],
providers: [
- { provide: ActivatedRoute, useValue: activatedRouteStub }
+ { provide: ActivatedRoute, useValue: activatedRouteStub },
+ { provide: BROWSE_BY_COMPONENT_FACTORY, useValue: jasmine.createSpy('getComponentByBrowseByType').and.returnValue(null) }
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
@@ -32,7 +32,6 @@ xdescribe('BrowseBySwitcherComponent', () => {
beforeEach(waitForAsync(() => {
fixture = TestBed.createComponent(BrowseBySwitcherComponent);
comp = fixture.componentInstance;
- spyOnProperty(decorator, 'getComponentByBrowseByType').and.returnValue(createSpy('getComponentByItemType'));
}));
types.forEach((type) => {
@@ -43,7 +42,7 @@ xdescribe('BrowseBySwitcherComponent', () => {
});
it(`should call getComponentByBrowseByType with type "${type.type}"`, () => {
- expect(decorator.getComponentByBrowseByType).toHaveBeenCalledWith(type.type);
+ expect((comp as any).getComponentByBrowseByType).toHaveBeenCalledWith(type.type);
});
});
});
diff --git a/src/app/+browse-by/+browse-by-switcher/browse-by-switcher.component.ts b/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.ts
similarity index 67%
rename from src/app/+browse-by/+browse-by-switcher/browse-by-switcher.component.ts
rename to src/app/browse-by/browse-by-switcher/browse-by-switcher.component.ts
index 6cfeebf796..043a4ce90a 100644
--- a/src/app/+browse-by/+browse-by-switcher/browse-by-switcher.component.ts
+++ b/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.ts
@@ -1,10 +1,11 @@
-import { Component, OnInit } from '@angular/core';
+import { Component, Inject, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs';
import { BrowseByTypeConfig } from '../../../config/browse-by-type-config.interface';
import { map } from 'rxjs/operators';
-import { getComponentByBrowseByType } from './browse-by-decorator';
+import { BROWSE_BY_COMPONENT_FACTORY } from './browse-by-decorator';
import { environment } from '../../../environments/environment';
+import { GenericConstructor } from '../../core/shared/generic-constructor';
@Component({
selector: 'ds-browse-by-switcher',
@@ -20,7 +21,8 @@ export class BrowseBySwitcherComponent implements OnInit {
*/
browseByComponent: Observable;
- public constructor(protected route: ActivatedRoute) {
+ public constructor(protected route: ActivatedRoute,
+ @Inject(BROWSE_BY_COMPONENT_FACTORY) private getComponentByBrowseByType: (browseByType) => GenericConstructor) {
}
/**
@@ -32,7 +34,7 @@ export class BrowseBySwitcherComponent implements OnInit {
const id = params.id;
return environment.browseBy.types.find((config: BrowseByTypeConfig) => config.id === id);
}),
- map((config: BrowseByTypeConfig) => getComponentByBrowseByType(config.type))
+ map((config: BrowseByTypeConfig) => this.getComponentByBrowseByType(config.type))
);
}
diff --git a/src/app/+browse-by/+browse-by-switcher/themed-browse-by-switcher.component.ts b/src/app/browse-by/browse-by-switcher/themed-browse-by-switcher.component.ts
similarity index 87%
rename from src/app/+browse-by/+browse-by-switcher/themed-browse-by-switcher.component.ts
rename to src/app/browse-by/browse-by-switcher/themed-browse-by-switcher.component.ts
index e92fe30ba9..0187d4e3c5 100644
--- a/src/app/+browse-by/+browse-by-switcher/themed-browse-by-switcher.component.ts
+++ b/src/app/browse-by/browse-by-switcher/themed-browse-by-switcher.component.ts
@@ -17,7 +17,7 @@ export class ThemedBrowseBySwitcherComponent extends ThemedComponent {
- return import(`../../../themes/${themeName}/app/+browse-by/+browse-by-switcher/browse-by-switcher.component`);
+ return import(`../../../themes/${themeName}/app/browse-by/browse-by-switcher/browse-by-switcher.component`);
}
protected importUnthemedComponent(): Promise {
diff --git a/src/app/+browse-by/+browse-by-title-page/browse-by-title-page.component.spec.ts b/src/app/browse-by/browse-by-title-page/browse-by-title-page.component.spec.ts
similarity index 97%
rename from src/app/+browse-by/+browse-by-title-page/browse-by-title-page.component.spec.ts
rename to src/app/browse-by/browse-by-title-page/browse-by-title-page.component.spec.ts
index d44c667044..584da1c45a 100644
--- a/src/app/+browse-by/+browse-by-title-page/browse-by-title-page.component.spec.ts
+++ b/src/app/browse-by/browse-by-title-page/browse-by-title-page.component.spec.ts
@@ -9,7 +9,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { EnumKeysPipe } from '../../shared/utils/enum-keys-pipe';
import { NO_ERRORS_SCHEMA } from '@angular/core';
-import { toRemoteData } from '../+browse-by-metadata-page/browse-by-metadata-page.component.spec';
+import { toRemoteData } from '../browse-by-metadata-page/browse-by-metadata-page.component.spec';
import { BrowseByTitlePageComponent } from './browse-by-title-page.component';
import { ItemDataService } from '../../core/data/item-data.service';
import { Community } from '../../core/shared/community.model';
diff --git a/src/app/+browse-by/+browse-by-title-page/browse-by-title-page.component.ts b/src/app/browse-by/browse-by-title-page/browse-by-title-page.component.ts
similarity index 89%
rename from src/app/+browse-by/+browse-by-title-page/browse-by-title-page.component.ts
rename to src/app/browse-by/browse-by-title-page/browse-by-title-page.component.ts
index 381684f9f0..b3a2ceed00 100644
--- a/src/app/+browse-by/+browse-by-title-page/browse-by-title-page.component.ts
+++ b/src/app/browse-by/browse-by-title-page/browse-by-title-page.component.ts
@@ -5,20 +5,20 @@ import { hasValue } from '../../shared/empty.util';
import {
BrowseByMetadataPageComponent,
browseParamsToOptions
-} from '../+browse-by-metadata-page/browse-by-metadata-page.component';
+} from '../browse-by-metadata-page/browse-by-metadata-page.component';
import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model';
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
import { BrowseService } from '../../core/browse/browse.service';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
-import { BrowseByType, rendersBrowseBy } from '../+browse-by-switcher/browse-by-decorator';
+import { BrowseByType, rendersBrowseBy } from '../browse-by-switcher/browse-by-decorator';
import { PaginationService } from '../../core/pagination/pagination.service';
import { map } from 'rxjs/operators';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
@Component({
selector: 'ds-browse-by-title-page',
- styleUrls: ['../+browse-by-metadata-page/browse-by-metadata-page.component.scss'],
- templateUrl: '../+browse-by-metadata-page/browse-by-metadata-page.component.html'
+ styleUrls: ['../browse-by-metadata-page/browse-by-metadata-page.component.scss'],
+ templateUrl: '../browse-by-metadata-page/browse-by-metadata-page.component.html'
})
/**
* Component for browsing items by title (dc.title)
diff --git a/src/app/+browse-by/browse-by.module.ts b/src/app/browse-by/browse-by.module.ts
similarity index 63%
rename from src/app/+browse-by/browse-by.module.ts
rename to src/app/browse-by/browse-by.module.ts
index 08b6c5739b..2d3618aae6 100644
--- a/src/app/+browse-by/browse-by.module.ts
+++ b/src/app/browse-by/browse-by.module.ts
@@ -1,11 +1,11 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
-import { BrowseByTitlePageComponent } from './+browse-by-title-page/browse-by-title-page.component';
+import { BrowseByTitlePageComponent } from './browse-by-title-page/browse-by-title-page.component';
import { SharedModule } from '../shared/shared.module';
-import { BrowseByMetadataPageComponent } from './+browse-by-metadata-page/browse-by-metadata-page.component';
-import { BrowseByDatePageComponent } from './+browse-by-date-page/browse-by-date-page.component';
-import { BrowseBySwitcherComponent } from './+browse-by-switcher/browse-by-switcher.component';
-import { ThemedBrowseBySwitcherComponent } from './+browse-by-switcher/themed-browse-by-switcher.component';
+import { BrowseByMetadataPageComponent } from './browse-by-metadata-page/browse-by-metadata-page.component';
+import { BrowseByDatePageComponent } from './browse-by-date-page/browse-by-date-page.component';
+import { BrowseBySwitcherComponent } from './browse-by-switcher/browse-by-switcher.component';
+import { ThemedBrowseBySwitcherComponent } from './browse-by-switcher/themed-browse-by-switcher.component';
const ENTRY_COMPONENTS = [
// put only entry components that use custom decorator
diff --git a/src/app/+collection-page/collection-form/collection-form.component.ts b/src/app/collection-page/collection-form/collection-form.component.ts
similarity index 100%
rename from src/app/+collection-page/collection-form/collection-form.component.ts
rename to src/app/collection-page/collection-form/collection-form.component.ts
diff --git a/src/app/+collection-page/collection-form/collection-form.module.ts b/src/app/collection-page/collection-form/collection-form.module.ts
similarity index 100%
rename from src/app/+collection-page/collection-form/collection-form.module.ts
rename to src/app/collection-page/collection-form/collection-form.module.ts
diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html b/src/app/collection-page/collection-item-mapper/collection-item-mapper.component.html
similarity index 97%
rename from src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html
rename to src/app/collection-page/collection-item-mapper/collection-item-mapper.component.html
index cc4b0c22a1..e10b9da247 100644
--- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html
+++ b/src/app/collection-page/collection-item-mapper/collection-item-mapper.component.html
@@ -42,6 +42,7 @@
[key]="'map'"
[dsoRD$]="mappedItemsRD$"
[paginationOptions]="(searchOptions$ | async)?.pagination"
+ [featureId]="FeatureIds.CanManageMappings"
[confirmButton]="'collection.edit.item-mapper.confirm'"
[cancelButton]="'collection.edit.item-mapper.cancel'"
(confirm)="mapItems($event)"
diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.scss b/src/app/collection-page/collection-item-mapper/collection-item-mapper.component.scss
similarity index 100%
rename from src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.scss
rename to src/app/collection-page/collection-item-mapper/collection-item-mapper.component.scss
diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts b/src/app/collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts
similarity index 96%
rename from src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts
rename to src/app/collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts
index 49b6a0d63c..5ae1445cef 100644
--- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts
+++ b/src/app/collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts
@@ -40,6 +40,7 @@ import {
createSuccessfulRemoteDataObject$
} from '../../shared/remote-data.utils';
import { createPaginatedList } from '../../shared/testing/utils.test';
+import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
describe('CollectionItemMapperComponent', () => {
let comp: CollectionItemMapperComponent;
@@ -136,6 +137,10 @@ describe('CollectionItemMapperComponent', () => {
}
};
+ const authorizationDataService = jasmine.createSpyObj('authorizationDataService', {
+ isAuthorized: observableOf(true)
+ });
+
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
@@ -152,6 +157,7 @@ describe('CollectionItemMapperComponent', () => {
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
{ provide: ObjectSelectService, useValue: new ObjectSelectServiceStub() },
{ provide: RouteService, useValue: routeServiceStub },
+ { provide: AuthorizationDataService, useValue: authorizationDataService }
]
}).compileComponents();
}));
diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts b/src/app/collection-page/collection-item-mapper/collection-item-mapper.component.ts
similarity index 98%
rename from src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts
rename to src/app/collection-page/collection-item-mapper/collection-item-mapper.component.ts
index 571b755897..9a93457436 100644
--- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts
+++ b/src/app/collection-page/collection-item-mapper/collection-item-mapper.component.ts
@@ -22,12 +22,13 @@ import { ItemDataService } from '../../core/data/item-data.service';
import { TranslateService } from '@ngx-translate/core';
import { CollectionDataService } from '../../core/data/collection-data.service';
import { isNotEmpty } from '../../shared/empty.util';
-import { SEARCH_CONFIG_SERVICE } from '../../+my-dspace-page/my-dspace-page.component';
+import { SEARCH_CONFIG_SERVICE } from '../../my-dspace-page/my-dspace-page.component';
import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service';
import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model';
import { SearchService } from '../../core/shared/search/search.service';
import { followLink } from '../../shared/utils/follow-link-config.model';
import { NoContent } from '../../core/shared/NoContent.model';
+import { FeatureID } from '../../core/data/feature-authorization/feature-id';
@Component({
selector: 'ds-collection-item-mapper',
@@ -50,6 +51,8 @@ import { NoContent } from '../../core/shared/NoContent.model';
*/
export class CollectionItemMapperComponent implements OnInit {
+ FeatureIds = FeatureID;
+
/**
* A view on the tabset element
* Used to switch tabs programmatically
diff --git a/src/app/+collection-page/collection-page-administrator.guard.ts b/src/app/collection-page/collection-page-administrator.guard.ts
similarity index 84%
rename from src/app/+collection-page/collection-page-administrator.guard.ts
rename to src/app/collection-page/collection-page-administrator.guard.ts
index 748cca81cb..c7866515b2 100644
--- a/src/app/+collection-page/collection-page-administrator.guard.ts
+++ b/src/app/collection-page/collection-page-administrator.guard.ts
@@ -4,7 +4,7 @@ import { Collection } from '../core/shared/collection.model';
import { CollectionPageResolver } from './collection-page.resolver';
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
import { Observable, of as observableOf } from 'rxjs';
-import { DsoPageFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard';
+import { DsoPageSingleFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard';
import { FeatureID } from '../core/data/feature-authorization/feature-id';
import { AuthService } from '../core/auth/auth.service';
@@ -14,7 +14,7 @@ import { AuthService } from '../core/auth/auth.service';
/**
* Guard for preventing unauthorized access to certain {@link Collection} pages requiring administrator rights
*/
-export class CollectionPageAdministratorGuard extends DsoPageFeatureGuard {
+export class CollectionPageAdministratorGuard extends DsoPageSingleFeatureGuard {
constructor(protected resolver: CollectionPageResolver,
protected authorizationService: AuthorizationDataService,
protected router: Router,
diff --git a/src/app/+collection-page/collection-page-routing-paths.ts b/src/app/collection-page/collection-page-routing-paths.ts
similarity index 100%
rename from src/app/+collection-page/collection-page-routing-paths.ts
rename to src/app/collection-page/collection-page-routing-paths.ts
diff --git a/src/app/+collection-page/collection-page-routing.module.ts b/src/app/collection-page/collection-page-routing.module.ts
similarity index 100%
rename from src/app/+collection-page/collection-page-routing.module.ts
rename to src/app/collection-page/collection-page-routing.module.ts
diff --git a/src/app/+collection-page/collection-page.component.html b/src/app/collection-page/collection-page.component.html
similarity index 100%
rename from src/app/+collection-page/collection-page.component.html
rename to src/app/collection-page/collection-page.component.html
diff --git a/src/app/+collection-page/collection-page.component.scss b/src/app/collection-page/collection-page.component.scss
similarity index 100%
rename from src/app/+collection-page/collection-page.component.scss
rename to src/app/collection-page/collection-page.component.scss
diff --git a/src/app/+collection-page/collection-page.component.ts b/src/app/collection-page/collection-page.component.ts
similarity index 93%
rename from src/app/+collection-page/collection-page.component.ts
rename to src/app/collection-page/collection-page.component.ts
index 9eba2e4ab2..366e1da7b1 100644
--- a/src/app/+collection-page/collection-page.component.ts
+++ b/src/app/collection-page/collection-page.component.ts
@@ -1,6 +1,11 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
-import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, Subject } from 'rxjs';
+import {
+ BehaviorSubject,
+ combineLatest as observableCombineLatest,
+ Observable,
+ Subject
+} from 'rxjs';
import { filter, map, mergeMap, startWith, switchMap, take } from 'rxjs/operators';
import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model';
import { SearchService } from '../core/shared/search/search.service';
@@ -8,8 +13,6 @@ import { SortDirection, SortOptions } from '../core/cache/models/sort-options.mo
import { CollectionDataService } from '../core/data/collection-data.service';
import { PaginatedList } from '../core/data/paginated-list.model';
import { RemoteData } from '../core/data/remote-data';
-
-import { MetadataService } from '../core/metadata/metadata.service';
import { Bitstream } from '../core/shared/bitstream.model';
import { Collection } from '../core/shared/collection.model';
@@ -65,7 +68,6 @@ export class CollectionPageComponent implements OnInit {
constructor(
private collectionDataService: CollectionDataService,
private searchService: SearchService,
- private metadata: MetadataService,
private route: ActivatedRoute,
private router: Router,
private authService: AuthService,
@@ -122,10 +124,6 @@ export class CollectionPageComponent implements OnInit {
getAllSucceededRemoteDataPayload(),
map((collection) => getCollectionPageRoute(collection.id))
);
-
- this.route.queryParams.pipe(take(1)).subscribe((params) => {
- this.metadata.processRemoteData(this.collectionRD$);
- });
}
isNotEmpty(object: any) {
diff --git a/src/app/+collection-page/collection-page.module.ts b/src/app/collection-page/collection-page.module.ts
similarity index 94%
rename from src/app/+collection-page/collection-page.module.ts
rename to src/app/collection-page/collection-page.module.ts
index 7946e7a1a4..a13731ed23 100644
--- a/src/app/+collection-page/collection-page.module.ts
+++ b/src/app/collection-page/collection-page.module.ts
@@ -8,7 +8,7 @@ import { CollectionPageRoutingModule } from './collection-page-routing.module';
import { CreateCollectionPageComponent } from './create-collection-page/create-collection-page.component';
import { DeleteCollectionPageComponent } from './delete-collection-page/delete-collection-page.component';
import { EditItemTemplatePageComponent } from './edit-item-template-page/edit-item-template-page.component';
-import { EditItemPageModule } from '../+item-page/edit-item-page/edit-item-page.module';
+import { EditItemPageModule } from '../item-page/edit-item-page/edit-item-page.module';
import { CollectionItemMapperComponent } from './collection-item-mapper/collection-item-mapper.component';
import { SearchService } from '../core/shared/search/search.service';
import { StatisticsModule } from '../statistics/statistics.module';
diff --git a/src/app/+collection-page/collection-page.resolver.spec.ts b/src/app/collection-page/collection-page.resolver.spec.ts
similarity index 100%
rename from src/app/+collection-page/collection-page.resolver.spec.ts
rename to src/app/collection-page/collection-page.resolver.spec.ts
diff --git a/src/app/+collection-page/collection-page.resolver.ts b/src/app/collection-page/collection-page.resolver.ts
similarity index 97%
rename from src/app/+collection-page/collection-page.resolver.ts
rename to src/app/collection-page/collection-page.resolver.ts
index f6f87f117c..d476a180d3 100644
--- a/src/app/+collection-page/collection-page.resolver.ts
+++ b/src/app/collection-page/collection-page.resolver.ts
@@ -14,7 +14,7 @@ import { ResolvedAction } from '../core/resolving/resolver.actions';
* Requesting them as embeds will limit the number of requests
*/
export const COLLECTION_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig[] = [
- followLink('parentCommunity', undefined, true, true, true,
+ followLink('parentCommunity', {},
followLink('parentCommunity')
),
followLink('logo')
diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.html b/src/app/collection-page/create-collection-page/create-collection-page.component.html
similarity index 100%
rename from src/app/+collection-page/create-collection-page/create-collection-page.component.html
rename to src/app/collection-page/create-collection-page/create-collection-page.component.html
diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.scss b/src/app/collection-page/create-collection-page/create-collection-page.component.scss
similarity index 100%
rename from src/app/+collection-page/create-collection-page/create-collection-page.component.scss
rename to src/app/collection-page/create-collection-page/create-collection-page.component.scss
diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts b/src/app/collection-page/create-collection-page/create-collection-page.component.spec.ts
similarity index 100%
rename from src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts
rename to src/app/collection-page/create-collection-page/create-collection-page.component.spec.ts
diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts b/src/app/collection-page/create-collection-page/create-collection-page.component.ts
similarity index 100%
rename from src/app/+collection-page/create-collection-page/create-collection-page.component.ts
rename to src/app/collection-page/create-collection-page/create-collection-page.component.ts
diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.guard.spec.ts b/src/app/collection-page/create-collection-page/create-collection-page.guard.spec.ts
similarity index 100%
rename from src/app/+collection-page/create-collection-page/create-collection-page.guard.spec.ts
rename to src/app/collection-page/create-collection-page/create-collection-page.guard.spec.ts
diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.guard.ts b/src/app/collection-page/create-collection-page/create-collection-page.guard.ts
similarity index 100%
rename from src/app/+collection-page/create-collection-page/create-collection-page.guard.ts
rename to src/app/collection-page/create-collection-page/create-collection-page.guard.ts
diff --git a/src/app/collection-page/delete-collection-page/delete-collection-page.component.html b/src/app/collection-page/delete-collection-page/delete-collection-page.component.html
new file mode 100644
index 0000000000..4abb149498
--- /dev/null
+++ b/src/app/collection-page/delete-collection-page/delete-collection-page.component.html
@@ -0,0 +1,23 @@
+
+
+
+
+
+
{{ 'collection.delete.text' | translate:{ dso: dso.name } }}
+
+
+
+
+
+
+
diff --git a/src/app/+collection-page/delete-collection-page/delete-collection-page.component.scss b/src/app/collection-page/delete-collection-page/delete-collection-page.component.scss
similarity index 100%
rename from src/app/+collection-page/delete-collection-page/delete-collection-page.component.scss
rename to src/app/collection-page/delete-collection-page/delete-collection-page.component.scss
diff --git a/src/app/+collection-page/delete-collection-page/delete-collection-page.component.spec.ts b/src/app/collection-page/delete-collection-page/delete-collection-page.component.spec.ts
similarity index 100%
rename from src/app/+collection-page/delete-collection-page/delete-collection-page.component.spec.ts
rename to src/app/collection-page/delete-collection-page/delete-collection-page.component.spec.ts
diff --git a/src/app/+collection-page/delete-collection-page/delete-collection-page.component.ts b/src/app/collection-page/delete-collection-page/delete-collection-page.component.ts
similarity index 100%
rename from src/app/+collection-page/delete-collection-page/delete-collection-page.component.ts
rename to src/app/collection-page/delete-collection-page/delete-collection-page.component.ts
diff --git a/src/app/+collection-page/edit-collection-page/collection-authorizations/collection-authorizations.component.html b/src/app/collection-page/edit-collection-page/collection-authorizations/collection-authorizations.component.html
similarity index 100%
rename from src/app/+collection-page/edit-collection-page/collection-authorizations/collection-authorizations.component.html
rename to src/app/collection-page/edit-collection-page/collection-authorizations/collection-authorizations.component.html
diff --git a/src/app/+collection-page/edit-collection-page/collection-authorizations/collection-authorizations.component.spec.ts b/src/app/collection-page/edit-collection-page/collection-authorizations/collection-authorizations.component.spec.ts
similarity index 100%
rename from src/app/+collection-page/edit-collection-page/collection-authorizations/collection-authorizations.component.spec.ts
rename to src/app/collection-page/edit-collection-page/collection-authorizations/collection-authorizations.component.spec.ts
diff --git a/src/app/+collection-page/edit-collection-page/collection-authorizations/collection-authorizations.component.ts b/src/app/collection-page/edit-collection-page/collection-authorizations/collection-authorizations.component.ts
similarity index 100%
rename from src/app/+collection-page/edit-collection-page/collection-authorizations/collection-authorizations.component.ts
rename to src/app/collection-page/edit-collection-page/collection-authorizations/collection-authorizations.component.ts
diff --git a/src/app/+collection-page/edit-collection-page/collection-curate/collection-curate.component.html b/src/app/collection-page/edit-collection-page/collection-curate/collection-curate.component.html
similarity index 100%
rename from src/app/+collection-page/edit-collection-page/collection-curate/collection-curate.component.html
rename to src/app/collection-page/edit-collection-page/collection-curate/collection-curate.component.html
diff --git a/src/app/+collection-page/edit-collection-page/collection-curate/collection-curate.component.spec.ts b/src/app/collection-page/edit-collection-page/collection-curate/collection-curate.component.spec.ts
similarity index 100%
rename from src/app/+collection-page/edit-collection-page/collection-curate/collection-curate.component.spec.ts
rename to src/app/collection-page/edit-collection-page/collection-curate/collection-curate.component.spec.ts
diff --git a/src/app/+collection-page/edit-collection-page/collection-curate/collection-curate.component.ts b/src/app/collection-page/edit-collection-page/collection-curate/collection-curate.component.ts
similarity index 100%
rename from src/app/+collection-page/edit-collection-page/collection-curate/collection-curate.component.ts
rename to src/app/collection-page/edit-collection-page/collection-curate/collection-curate.component.ts
diff --git a/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.html b/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.html
similarity index 100%
rename from src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.html
rename to src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.html
diff --git a/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.spec.ts b/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.spec.ts
similarity index 100%
rename from src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.spec.ts
rename to src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.spec.ts
diff --git a/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts b/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts
similarity index 100%
rename from src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts
rename to src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts
diff --git a/src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.html b/src/app/collection-page/edit-collection-page/collection-roles/collection-roles.component.html
similarity index 100%
rename from src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.html
rename to src/app/collection-page/edit-collection-page/collection-roles/collection-roles.component.html
diff --git a/src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.spec.ts b/src/app/collection-page/edit-collection-page/collection-roles/collection-roles.component.spec.ts
similarity index 100%
rename from src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.spec.ts
rename to src/app/collection-page/edit-collection-page/collection-roles/collection-roles.component.spec.ts
diff --git a/src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.ts b/src/app/collection-page/edit-collection-page/collection-roles/collection-roles.component.ts
similarity index 100%
rename from src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.ts
rename to src/app/collection-page/edit-collection-page/collection-roles/collection-roles.component.ts
diff --git a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.html b/src/app/collection-page/edit-collection-page/collection-source/collection-source.component.html
similarity index 100%
rename from src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.html
rename to src/app/collection-page/edit-collection-page/collection-source/collection-source.component.html
diff --git a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.spec.ts b/src/app/collection-page/edit-collection-page/collection-source/collection-source.component.spec.ts
similarity index 100%
rename from src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.spec.ts
rename to src/app/collection-page/edit-collection-page/collection-source/collection-source.component.spec.ts
diff --git a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts b/src/app/collection-page/edit-collection-page/collection-source/collection-source.component.ts
similarity index 100%
rename from src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts
rename to src/app/collection-page/edit-collection-page/collection-source/collection-source.component.ts
diff --git a/src/app/+collection-page/edit-collection-page/edit-collection-page.component.spec.ts b/src/app/collection-page/edit-collection-page/edit-collection-page.component.spec.ts
similarity index 100%
rename from src/app/+collection-page/edit-collection-page/edit-collection-page.component.spec.ts
rename to src/app/collection-page/edit-collection-page/edit-collection-page.component.spec.ts
diff --git a/src/app/+collection-page/edit-collection-page/edit-collection-page.component.ts b/src/app/collection-page/edit-collection-page/edit-collection-page.component.ts
similarity index 100%
rename from src/app/+collection-page/edit-collection-page/edit-collection-page.component.ts
rename to src/app/collection-page/edit-collection-page/edit-collection-page.component.ts
diff --git a/src/app/+collection-page/edit-collection-page/edit-collection-page.module.ts b/src/app/collection-page/edit-collection-page/edit-collection-page.module.ts
similarity index 100%
rename from src/app/+collection-page/edit-collection-page/edit-collection-page.module.ts
rename to src/app/collection-page/edit-collection-page/edit-collection-page.module.ts
diff --git a/src/app/+collection-page/edit-collection-page/edit-collection-page.routing.module.ts b/src/app/collection-page/edit-collection-page/edit-collection-page.routing.module.ts
similarity index 100%
rename from src/app/+collection-page/edit-collection-page/edit-collection-page.routing.module.ts
rename to src/app/collection-page/edit-collection-page/edit-collection-page.routing.module.ts
diff --git a/src/app/+collection-page/edit-item-template-page/edit-item-template-page.component.html b/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.html
similarity index 100%
rename from src/app/+collection-page/edit-item-template-page/edit-item-template-page.component.html
rename to src/app/collection-page/edit-item-template-page/edit-item-template-page.component.html
diff --git a/src/app/+collection-page/edit-item-template-page/edit-item-template-page.component.spec.ts b/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.spec.ts
similarity index 100%
rename from src/app/+collection-page/edit-item-template-page/edit-item-template-page.component.spec.ts
rename to src/app/collection-page/edit-item-template-page/edit-item-template-page.component.spec.ts
diff --git a/src/app/+collection-page/edit-item-template-page/edit-item-template-page.component.ts b/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.ts
similarity index 100%
rename from src/app/+collection-page/edit-item-template-page/edit-item-template-page.component.ts
rename to src/app/collection-page/edit-item-template-page/edit-item-template-page.component.ts
diff --git a/src/app/+collection-page/edit-item-template-page/item-template-page.resolver.spec.ts b/src/app/collection-page/edit-item-template-page/item-template-page.resolver.spec.ts
similarity index 100%
rename from src/app/+collection-page/edit-item-template-page/item-template-page.resolver.spec.ts
rename to src/app/collection-page/edit-item-template-page/item-template-page.resolver.spec.ts
diff --git a/src/app/+collection-page/edit-item-template-page/item-template-page.resolver.ts b/src/app/collection-page/edit-item-template-page/item-template-page.resolver.ts
similarity index 100%
rename from src/app/+collection-page/edit-item-template-page/item-template-page.resolver.ts
rename to src/app/collection-page/edit-item-template-page/item-template-page.resolver.ts
diff --git a/src/app/+collection-page/themed-collection-page.component.ts b/src/app/collection-page/themed-collection-page.component.ts
similarity index 88%
rename from src/app/+collection-page/themed-collection-page.component.ts
rename to src/app/collection-page/themed-collection-page.component.ts
index 4ad9ed87e3..82074e43e6 100644
--- a/src/app/+collection-page/themed-collection-page.component.ts
+++ b/src/app/collection-page/themed-collection-page.component.ts
@@ -16,7 +16,7 @@ export class ThemedCollectionPageComponent extends ThemedComponent {
- return import(`../../themes/${themeName}/app/+collection-page/collection-page.component`);
+ return import(`../../themes/${themeName}/app/collection-page/collection-page.component`);
}
protected importUnthemedComponent(): Promise {
diff --git a/src/app/community-list-page/community-list-service.ts b/src/app/community-list-page/community-list-service.ts
index cbf70ca39a..76d33585da 100644
--- a/src/app/community-list-page/community-list-service.ts
+++ b/src/app/community-list-page/community-list-service.ts
@@ -16,8 +16,8 @@ import { PaginatedList, buildPaginatedList } from '../core/data/paginated-list.m
import { CollectionDataService } from '../core/data/collection-data.service';
import { CommunityListSaveAction } from './community-list.actions';
import { CommunityListState } from './community-list.reducer';
-import { getCommunityPageRoute } from '../+community-page/community-page-routing-paths';
-import { getCollectionPageRoute } from '../+collection-page/collection-page-routing-paths';
+import { getCommunityPageRoute } from '../community-page/community-page-routing-paths';
+import { getCollectionPageRoute } from '../collection-page/collection-page-routing-paths';
import { getFirstSucceededRemoteData, getFirstCompletedRemoteData } from '../core/shared/operators';
import { followLink } from '../shared/utils/follow-link-config.model';
@@ -174,8 +174,8 @@ export class CommunityListService {
direction: options.sort.direction
}
},
- followLink('subcommunities', this.configOnePage, true, true),
- followLink('collections', this.configOnePage, true, true))
+ followLink('subcommunities', { findListOptions: this.configOnePage }),
+ followLink('collections', { findListOptions: this.configOnePage }))
.pipe(
getFirstSucceededRemoteData(),
map((results) => results.payload),
@@ -242,8 +242,8 @@ export class CommunityListService {
elementsPerPage: MAX_COMCOLS_PER_PAGE,
currentPage: i
},
- followLink('subcommunities', this.configOnePage, true, true),
- followLink('collections', this.configOnePage, true, true))
+ followLink('subcommunities', { findListOptions: this.configOnePage }),
+ followLink('collections', { findListOptions: this.configOnePage }))
.pipe(
getFirstCompletedRemoteData(),
switchMap((rd: RemoteData>) => {
diff --git a/src/app/+community-page/community-form/community-form.component.ts b/src/app/community-page/community-form/community-form.component.ts
similarity index 100%
rename from src/app/+community-page/community-form/community-form.component.ts
rename to src/app/community-page/community-form/community-form.component.ts
diff --git a/src/app/+community-page/community-form/community-form.module.ts b/src/app/community-page/community-form/community-form.module.ts
similarity index 100%
rename from src/app/+community-page/community-form/community-form.module.ts
rename to src/app/community-page/community-form/community-form.module.ts
diff --git a/src/app/+community-page/community-page-administrator.guard.ts b/src/app/community-page/community-page-administrator.guard.ts
similarity index 84%
rename from src/app/+community-page/community-page-administrator.guard.ts
rename to src/app/community-page/community-page-administrator.guard.ts
index fad4a78f07..fd7ce5f7bf 100644
--- a/src/app/+community-page/community-page-administrator.guard.ts
+++ b/src/app/community-page/community-page-administrator.guard.ts
@@ -4,7 +4,7 @@ import { Community } from '../core/shared/community.model';
import { CommunityPageResolver } from './community-page.resolver';
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
import { Observable, of as observableOf } from 'rxjs';
-import { DsoPageFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard';
+import { DsoPageSingleFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard';
import { FeatureID } from '../core/data/feature-authorization/feature-id';
import { AuthService } from '../core/auth/auth.service';
@@ -14,7 +14,7 @@ import { AuthService } from '../core/auth/auth.service';
/**
* Guard for preventing unauthorized access to certain {@link Community} pages requiring administrator rights
*/
-export class CommunityPageAdministratorGuard extends DsoPageFeatureGuard {
+export class CommunityPageAdministratorGuard extends DsoPageSingleFeatureGuard {
constructor(protected resolver: CommunityPageResolver,
protected authorizationService: AuthorizationDataService,
protected router: Router,
diff --git a/src/app/+community-page/community-page-routing-paths.ts b/src/app/community-page/community-page-routing-paths.ts
similarity index 91%
rename from src/app/+community-page/community-page-routing-paths.ts
rename to src/app/community-page/community-page-routing-paths.ts
index 02adcea718..759d72cf3e 100644
--- a/src/app/+community-page/community-page-routing-paths.ts
+++ b/src/app/community-page/community-page-routing-paths.ts
@@ -1,4 +1,4 @@
-import { getCollectionPageRoute } from '../+collection-page/collection-page-routing-paths';
+import { getCollectionPageRoute } from '../collection-page/collection-page-routing-paths';
import { URLCombiner } from '../core/url-combiner/url-combiner';
export const COMMUNITY_PARENT_PARAMETER = 'parent';
diff --git a/src/app/+community-page/community-page-routing.module.ts b/src/app/community-page/community-page-routing.module.ts
similarity index 100%
rename from src/app/+community-page/community-page-routing.module.ts
rename to src/app/community-page/community-page-routing.module.ts
diff --git a/src/app/+community-page/community-page.component.html b/src/app/community-page/community-page.component.html
similarity index 100%
rename from src/app/+community-page/community-page.component.html
rename to src/app/community-page/community-page.component.html
diff --git a/src/app/+community-page/community-page.component.scss b/src/app/community-page/community-page.component.scss
similarity index 100%
rename from src/app/+community-page/community-page.component.scss
rename to src/app/community-page/community-page.component.scss
diff --git a/src/app/+community-page/community-page.component.ts b/src/app/community-page/community-page.component.ts
similarity index 100%
rename from src/app/+community-page/community-page.component.ts
rename to src/app/community-page/community-page.component.ts
diff --git a/src/app/+community-page/community-page.module.ts b/src/app/community-page/community-page.module.ts
similarity index 100%
rename from src/app/+community-page/community-page.module.ts
rename to src/app/community-page/community-page.module.ts
diff --git a/src/app/+community-page/community-page.resolver.spec.ts b/src/app/community-page/community-page.resolver.spec.ts
similarity index 100%
rename from src/app/+community-page/community-page.resolver.spec.ts
rename to src/app/community-page/community-page.resolver.spec.ts
diff --git a/src/app/+community-page/community-page.resolver.ts b/src/app/community-page/community-page.resolver.ts
similarity index 100%
rename from src/app/+community-page/community-page.resolver.ts
rename to src/app/community-page/community-page.resolver.ts
diff --git a/src/app/+community-page/create-community-page/create-community-page.component.html b/src/app/community-page/create-community-page/create-community-page.component.html
similarity index 100%
rename from src/app/+community-page/create-community-page/create-community-page.component.html
rename to src/app/community-page/create-community-page/create-community-page.component.html
diff --git a/src/app/+community-page/create-community-page/create-community-page.component.scss b/src/app/community-page/create-community-page/create-community-page.component.scss
similarity index 100%
rename from src/app/+community-page/create-community-page/create-community-page.component.scss
rename to src/app/community-page/create-community-page/create-community-page.component.scss
diff --git a/src/app/+community-page/create-community-page/create-community-page.component.spec.ts b/src/app/community-page/create-community-page/create-community-page.component.spec.ts
similarity index 100%
rename from src/app/+community-page/create-community-page/create-community-page.component.spec.ts
rename to src/app/community-page/create-community-page/create-community-page.component.spec.ts
diff --git a/src/app/+community-page/create-community-page/create-community-page.component.ts b/src/app/community-page/create-community-page/create-community-page.component.ts
similarity index 100%
rename from src/app/+community-page/create-community-page/create-community-page.component.ts
rename to src/app/community-page/create-community-page/create-community-page.component.ts
diff --git a/src/app/+community-page/create-community-page/create-community-page.guard.spec.ts b/src/app/community-page/create-community-page/create-community-page.guard.spec.ts
similarity index 100%
rename from src/app/+community-page/create-community-page/create-community-page.guard.spec.ts
rename to src/app/community-page/create-community-page/create-community-page.guard.spec.ts
diff --git a/src/app/+community-page/create-community-page/create-community-page.guard.ts b/src/app/community-page/create-community-page/create-community-page.guard.ts
similarity index 100%
rename from src/app/+community-page/create-community-page/create-community-page.guard.ts
rename to src/app/community-page/create-community-page/create-community-page.guard.ts
diff --git a/src/app/+community-page/delete-community-page/delete-community-page.component.html b/src/app/community-page/delete-community-page/delete-community-page.component.html
similarity index 66%
rename from src/app/+community-page/delete-community-page/delete-community-page.component.html
rename to src/app/community-page/delete-community-page/delete-community-page.component.html
index 85aa8b1bce..658f3da436 100644
--- a/src/app/+community-page/delete-community-page/delete-community-page.component.html
+++ b/src/app/community-page/delete-community-page/delete-community-page.component.html
@@ -6,11 +6,12 @@
{{ 'community.delete.text' | translate:{ dso: dso.name } }}
diff --git a/src/app/+community-page/delete-community-page/delete-community-page.component.scss b/src/app/community-page/delete-community-page/delete-community-page.component.scss
similarity index 100%
rename from src/app/+community-page/delete-community-page/delete-community-page.component.scss
rename to src/app/community-page/delete-community-page/delete-community-page.component.scss
diff --git a/src/app/+community-page/delete-community-page/delete-community-page.component.spec.ts b/src/app/community-page/delete-community-page/delete-community-page.component.spec.ts
similarity index 100%
rename from src/app/+community-page/delete-community-page/delete-community-page.component.spec.ts
rename to src/app/community-page/delete-community-page/delete-community-page.component.spec.ts
diff --git a/src/app/+community-page/delete-community-page/delete-community-page.component.ts b/src/app/community-page/delete-community-page/delete-community-page.component.ts
similarity index 100%
rename from src/app/+community-page/delete-community-page/delete-community-page.component.ts
rename to src/app/community-page/delete-community-page/delete-community-page.component.ts
diff --git a/src/app/+community-page/edit-community-page/community-authorizations/community-authorizations.component.html b/src/app/community-page/edit-community-page/community-authorizations/community-authorizations.component.html
similarity index 100%
rename from src/app/+community-page/edit-community-page/community-authorizations/community-authorizations.component.html
rename to src/app/community-page/edit-community-page/community-authorizations/community-authorizations.component.html
diff --git a/src/app/+community-page/edit-community-page/community-authorizations/community-authorizations.component.spec.ts b/src/app/community-page/edit-community-page/community-authorizations/community-authorizations.component.spec.ts
similarity index 100%
rename from src/app/+community-page/edit-community-page/community-authorizations/community-authorizations.component.spec.ts
rename to src/app/community-page/edit-community-page/community-authorizations/community-authorizations.component.spec.ts
diff --git a/src/app/+community-page/edit-community-page/community-authorizations/community-authorizations.component.ts b/src/app/community-page/edit-community-page/community-authorizations/community-authorizations.component.ts
similarity index 100%
rename from src/app/+community-page/edit-community-page/community-authorizations/community-authorizations.component.ts
rename to src/app/community-page/edit-community-page/community-authorizations/community-authorizations.component.ts
diff --git a/src/app/+community-page/edit-community-page/community-curate/community-curate.component.html b/src/app/community-page/edit-community-page/community-curate/community-curate.component.html
similarity index 100%
rename from src/app/+community-page/edit-community-page/community-curate/community-curate.component.html
rename to src/app/community-page/edit-community-page/community-curate/community-curate.component.html
diff --git a/src/app/+community-page/edit-community-page/community-curate/community-curate.component.spec.ts b/src/app/community-page/edit-community-page/community-curate/community-curate.component.spec.ts
similarity index 100%
rename from src/app/+community-page/edit-community-page/community-curate/community-curate.component.spec.ts
rename to src/app/community-page/edit-community-page/community-curate/community-curate.component.spec.ts
diff --git a/src/app/+community-page/edit-community-page/community-curate/community-curate.component.ts b/src/app/community-page/edit-community-page/community-curate/community-curate.component.ts
similarity index 100%
rename from src/app/+community-page/edit-community-page/community-curate/community-curate.component.ts
rename to src/app/community-page/edit-community-page/community-curate/community-curate.component.ts
diff --git a/src/app/+community-page/edit-community-page/community-metadata/community-metadata.component.html b/src/app/community-page/edit-community-page/community-metadata/community-metadata.component.html
similarity index 100%
rename from src/app/+community-page/edit-community-page/community-metadata/community-metadata.component.html
rename to src/app/community-page/edit-community-page/community-metadata/community-metadata.component.html
diff --git a/src/app/+community-page/edit-community-page/community-metadata/community-metadata.component.spec.ts b/src/app/community-page/edit-community-page/community-metadata/community-metadata.component.spec.ts
similarity index 100%
rename from src/app/+community-page/edit-community-page/community-metadata/community-metadata.component.spec.ts
rename to src/app/community-page/edit-community-page/community-metadata/community-metadata.component.spec.ts
diff --git a/src/app/+community-page/edit-community-page/community-metadata/community-metadata.component.ts b/src/app/community-page/edit-community-page/community-metadata/community-metadata.component.ts
similarity index 100%
rename from src/app/+community-page/edit-community-page/community-metadata/community-metadata.component.ts
rename to src/app/community-page/edit-community-page/community-metadata/community-metadata.component.ts
diff --git a/src/app/+community-page/edit-community-page/community-roles/community-roles.component.html b/src/app/community-page/edit-community-page/community-roles/community-roles.component.html
similarity index 100%
rename from src/app/+community-page/edit-community-page/community-roles/community-roles.component.html
rename to src/app/community-page/edit-community-page/community-roles/community-roles.component.html
diff --git a/src/app/+community-page/edit-community-page/community-roles/community-roles.component.spec.ts b/src/app/community-page/edit-community-page/community-roles/community-roles.component.spec.ts
similarity index 100%
rename from src/app/+community-page/edit-community-page/community-roles/community-roles.component.spec.ts
rename to src/app/community-page/edit-community-page/community-roles/community-roles.component.spec.ts
diff --git a/src/app/+community-page/edit-community-page/community-roles/community-roles.component.ts b/src/app/community-page/edit-community-page/community-roles/community-roles.component.ts
similarity index 100%
rename from src/app/+community-page/edit-community-page/community-roles/community-roles.component.ts
rename to src/app/community-page/edit-community-page/community-roles/community-roles.component.ts
diff --git a/src/app/+community-page/edit-community-page/edit-community-page.component.spec.ts b/src/app/community-page/edit-community-page/edit-community-page.component.spec.ts
similarity index 100%
rename from src/app/+community-page/edit-community-page/edit-community-page.component.spec.ts
rename to src/app/community-page/edit-community-page/edit-community-page.component.spec.ts
diff --git a/src/app/+community-page/edit-community-page/edit-community-page.component.ts b/src/app/community-page/edit-community-page/edit-community-page.component.ts
similarity index 100%
rename from src/app/+community-page/edit-community-page/edit-community-page.component.ts
rename to src/app/community-page/edit-community-page/edit-community-page.component.ts
diff --git a/src/app/+community-page/edit-community-page/edit-community-page.module.ts b/src/app/community-page/edit-community-page/edit-community-page.module.ts
similarity index 100%
rename from src/app/+community-page/edit-community-page/edit-community-page.module.ts
rename to src/app/community-page/edit-community-page/edit-community-page.module.ts
diff --git a/src/app/+community-page/edit-community-page/edit-community-page.routing.module.ts b/src/app/community-page/edit-community-page/edit-community-page.routing.module.ts
similarity index 100%
rename from src/app/+community-page/edit-community-page/edit-community-page.routing.module.ts
rename to src/app/community-page/edit-community-page/edit-community-page.routing.module.ts
diff --git a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.html b/src/app/community-page/sub-collection-list/community-page-sub-collection-list.component.html
similarity index 100%
rename from src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.html
rename to src/app/community-page/sub-collection-list/community-page-sub-collection-list.component.html
diff --git a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.scss b/src/app/community-page/sub-collection-list/community-page-sub-collection-list.component.scss
similarity index 100%
rename from src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.scss
rename to src/app/community-page/sub-collection-list/community-page-sub-collection-list.component.scss
diff --git a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts b/src/app/community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts
similarity index 100%
rename from src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts
rename to src/app/community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts
diff --git a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.ts b/src/app/community-page/sub-collection-list/community-page-sub-collection-list.component.ts
similarity index 100%
rename from src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.ts
rename to src/app/community-page/sub-collection-list/community-page-sub-collection-list.component.ts
diff --git a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.html b/src/app/community-page/sub-community-list/community-page-sub-community-list.component.html
similarity index 100%
rename from src/app/+community-page/sub-community-list/community-page-sub-community-list.component.html
rename to src/app/community-page/sub-community-list/community-page-sub-community-list.component.html
diff --git a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.scss b/src/app/community-page/sub-community-list/community-page-sub-community-list.component.scss
similarity index 100%
rename from src/app/+community-page/sub-community-list/community-page-sub-community-list.component.scss
rename to src/app/community-page/sub-community-list/community-page-sub-community-list.component.scss
diff --git a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts b/src/app/community-page/sub-community-list/community-page-sub-community-list.component.spec.ts
similarity index 100%
rename from src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts
rename to src/app/community-page/sub-community-list/community-page-sub-community-list.component.spec.ts
diff --git a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.ts b/src/app/community-page/sub-community-list/community-page-sub-community-list.component.ts
similarity index 100%
rename from src/app/+community-page/sub-community-list/community-page-sub-community-list.component.ts
rename to src/app/community-page/sub-community-list/community-page-sub-community-list.component.ts
diff --git a/src/app/+community-page/themed-community-page.component.ts b/src/app/community-page/themed-community-page.component.ts
similarity index 88%
rename from src/app/+community-page/themed-community-page.component.ts
rename to src/app/community-page/themed-community-page.component.ts
index 97dd59821c..eeb058fb04 100644
--- a/src/app/+community-page/themed-community-page.component.ts
+++ b/src/app/community-page/themed-community-page.component.ts
@@ -16,7 +16,7 @@ export class ThemedCommunityPageComponent extends ThemedComponent {
- return import(`../../themes/${themeName}/app/+community-page/community-page.component`);
+ return import(`../../themes/${themeName}/app/community-page/community-page.component`);
}
protected importUnthemedComponent(): Promise {
diff --git a/src/app/core/auth/auth.actions.ts b/src/app/core/auth/auth.actions.ts
index f80be89034..15e42c8576 100644
--- a/src/app/core/auth/auth.actions.ts
+++ b/src/app/core/auth/auth.actions.ts
@@ -34,7 +34,9 @@ export const AuthActionTypes = {
RETRIEVE_AUTHENTICATED_EPERSON: type('dspace/auth/RETRIEVE_AUTHENTICATED_EPERSON'),
RETRIEVE_AUTHENTICATED_EPERSON_SUCCESS: type('dspace/auth/RETRIEVE_AUTHENTICATED_EPERSON_SUCCESS'),
RETRIEVE_AUTHENTICATED_EPERSON_ERROR: type('dspace/auth/RETRIEVE_AUTHENTICATED_EPERSON_ERROR'),
- REDIRECT_AFTER_LOGIN_SUCCESS: type('dspace/auth/REDIRECT_AFTER_LOGIN_SUCCESS')
+ REDIRECT_AFTER_LOGIN_SUCCESS: type('dspace/auth/REDIRECT_AFTER_LOGIN_SUCCESS'),
+ SET_USER_AS_IDLE: type('dspace/auth/SET_USER_AS_IDLE'),
+ UNSET_USER_AS_IDLE: type('dspace/auth/UNSET_USER_AS_IDLE')
};
/* tslint:disable:max-classes-per-file */
@@ -391,6 +393,24 @@ export class RetrieveAuthenticatedEpersonErrorAction implements Action {
this.payload = payload ;
}
}
+
+/**
+ * Set the current user as being idle.
+ * @class SetUserAsIdleAction
+ * @implements {Action}
+ */
+export class SetUserAsIdleAction implements Action {
+ public type: string = AuthActionTypes.SET_USER_AS_IDLE;
+}
+
+/**
+ * Unset the current user as being idle.
+ * @class UnsetUserAsIdleAction
+ * @implements {Action}
+ */
+export class UnsetUserAsIdleAction implements Action {
+ public type: string = AuthActionTypes.UNSET_USER_AS_IDLE;
+}
/* tslint:enable:max-classes-per-file */
/**
@@ -421,4 +441,7 @@ export type AuthActions
| RetrieveAuthenticatedEpersonErrorAction
| RetrieveAuthenticatedEpersonSuccessAction
| SetRedirectUrlAction
- | RedirectAfterLoginSuccessAction;
+ | RedirectAfterLoginSuccessAction
+ | SetUserAsIdleAction
+ | UnsetUserAsIdleAction;
+
diff --git a/src/app/core/auth/auth.effects.spec.ts b/src/app/core/auth/auth.effects.spec.ts
index 5d530f39a9..ed91eb3eea 100644
--- a/src/app/core/auth/auth.effects.spec.ts
+++ b/src/app/core/auth/auth.effects.spec.ts
@@ -35,6 +35,7 @@ import { EPersonMock } from '../../shared/testing/eperson.mock';
import { AppState, storeModuleConfig } from '../../app.reducer';
import { StoreActionTypes } from '../../store.actions';
import { isAuthenticated, isAuthenticatedLoaded } from './selectors';
+import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service';
describe('AuthEffects', () => {
let authEffects: AuthEffects;
@@ -44,6 +45,8 @@ describe('AuthEffects', () => {
let token;
let store: MockStore;
+ const authorizationService = jasmine.createSpyObj(['invalidateAuthorizationsRequestCache']);
+
function init() {
authServiceStub = new AuthServiceStub();
token = authServiceStub.getToken();
@@ -68,6 +71,7 @@ describe('AuthEffects', () => {
providers: [
AuthEffects,
provideMockStore({ initialState }),
+ { provide: AuthorizationDataService, useValue: authorizationService },
{ provide: AuthService, useValue: authServiceStub },
provideMockActions(() => actions),
// other providers
@@ -417,4 +421,16 @@ describe('AuthEffects', () => {
}));
});
});
+
+ describe('invalidateAuthorizationsRequestCache$', () => {
+ it('should call invalidateAuthorizationsRequestCache method in response to a REHYDRATE action', (done) => {
+ actions = hot('--a-|', { a: { type: StoreActionTypes.REHYDRATE } });
+
+ authEffects.invalidateAuthorizationsRequestCache$.subscribe(() => {
+ expect((authEffects as any).authorizationsService.invalidateAuthorizationsRequestCache).toHaveBeenCalled();
+ });
+
+ done();
+ });
+ });
});
diff --git a/src/app/core/auth/auth.effects.ts b/src/app/core/auth/auth.effects.ts
index 9452af1fb8..1477a1832e 100644
--- a/src/app/core/auth/auth.effects.ts
+++ b/src/app/core/auth/auth.effects.ts
@@ -1,7 +1,14 @@
-import { Injectable } from '@angular/core';
+import { Injectable, NgZone } from '@angular/core';
-import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
-import { catchError, filter, map, switchMap, take, tap } from 'rxjs/operators';
+import {
+ asyncScheduler,
+ combineLatest as observableCombineLatest,
+ Observable,
+ of as observableOf,
+ queueScheduler,
+ timer
+} from 'rxjs';
+import { catchError, filter, map, observeOn, switchMap, take, tap } from 'rxjs/operators';
// import @ngrx
import { Actions, Effect, ofType } from '@ngrx/effects';
import { Action, select, Store } from '@ngrx/store';
@@ -37,9 +44,21 @@ import {
RetrieveAuthMethodsAction,
RetrieveAuthMethodsErrorAction,
RetrieveAuthMethodsSuccessAction,
- RetrieveTokenAction
+ RetrieveTokenAction,
+ SetUserAsIdleAction
} from './auth.actions';
import { hasValue } from '../../shared/empty.util';
+import { environment } from '../../../environments/environment';
+import { RequestActionTypes } from '../data/request.actions';
+import { NotificationsActionTypes } from '../../shared/notifications/notifications.actions';
+import { LeaveZoneScheduler } from '../utilities/leave-zone.scheduler';
+import { EnterZoneScheduler } from '../utilities/enter-zone.scheduler';
+import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service';
+
+// Action Types that do not break/prevent the user from an idle state
+const IDLE_TIMER_IGNORE_TYPES: string[]
+ = [...Object.values(AuthActionTypes).filter((t: string) => t !== AuthActionTypes.UNSET_USER_AS_IDLE),
+ ...Object.values(RequestActionTypes), ...Object.values(NotificationsActionTypes)];
@Injectable()
export class AuthEffects {
@@ -200,6 +219,16 @@ export class AuthEffects {
);
}));
+ /**
+ * When the store is rehydrated in the browser, invalidate all cache hits regarding the
+ * authorizations endpoint, to be sure to have consistent responses after a login with external idp
+ *
+ */
+ @Effect({ dispatch: false }) invalidateAuthorizationsRequestCache$ = this.actions$
+ .pipe(ofType(StoreActionTypes.REHYDRATE),
+ tap(() => this.authorizationsService.invalidateAuthorizationsRequestCache())
+ );
+
@Effect()
public logOut$: Observable = this.actions$
.pipe(
@@ -242,13 +271,37 @@ export class AuthEffects {
})
);
+ /**
+ * For any action that is not in {@link IDLE_TIMER_IGNORE_TYPES} that comes in => Start the idleness timer
+ * If the idleness timer runs out (so no un-ignored action come through for that amount of time)
+ * => Return the action to set the user as idle ({@link SetUserAsIdleAction})
+ * @method trackIdleness
+ */
+ @Effect()
+ public trackIdleness$: Observable = this.actions$.pipe(
+ filter((action: Action) => !IDLE_TIMER_IGNORE_TYPES.includes(action.type)),
+ // Using switchMap the effect will stop subscribing to the previous timer if a new action comes
+ // in, and start a new timer
+ switchMap(() =>
+ // Start a timer outside of Angular's zone
+ timer(environment.auth.ui.timeUntilIdle, new LeaveZoneScheduler(this.zone, asyncScheduler))
+ ),
+ // Re-enter the zone to dispatch the action
+ observeOn(new EnterZoneScheduler(this.zone, queueScheduler)),
+ map(() => new SetUserAsIdleAction()),
+ );
+
/**
* @constructor
* @param {Actions} actions$
+ * @param {NgZone} zone
+ * @param {AuthorizationDataService} authorizationsService
* @param {AuthService} authService
* @param {Store} store
*/
constructor(private actions$: Actions,
+ private zone: NgZone,
+ private authorizationsService: AuthorizationDataService,
private authService: AuthService,
private store: Store) {
}
diff --git a/src/app/core/auth/auth.interceptor.ts b/src/app/core/auth/auth.interceptor.ts
index 7b9a08de92..a49030110b 100644
--- a/src/app/core/auth/auth.interceptor.ts
+++ b/src/app/core/auth/auth.interceptor.ts
@@ -1,6 +1,6 @@
import { Observable, of as observableOf, throwError as observableThrowError } from 'rxjs';
-import { catchError, filter, map } from 'rxjs/operators';
+import { catchError, map } from 'rxjs/operators';
import { Injectable, Injector } from '@angular/core';
import {
HttpErrorResponse,
@@ -12,14 +12,13 @@ import {
HttpResponse,
HttpResponseBase
} from '@angular/common/http';
-import { find } from 'lodash';
import { AppState } from '../../app.reducer';
import { AuthService } from './auth.service';
import { AuthStatus } from './models/auth-status.model';
import { AuthTokenInfo } from './models/auth-token-info.model';
-import { hasValue, isNotEmpty, isNotNull, isUndefined } from '../../shared/empty.util';
-import { RedirectWhenTokenExpiredAction, RefreshTokenAction } from './auth.actions';
+import { hasValue, isNotEmpty, isNotNull } from '../../shared/empty.util';
+import { RedirectWhenTokenExpiredAction } from './auth.actions';
import { Store } from '@ngrx/store';
import { Router } from '@angular/router';
import { AuthMethod } from './models/auth.method';
@@ -28,7 +27,7 @@ import { AuthMethodType } from './models/auth.method-type';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
- // Intercetor is called twice per request,
+ // Interceptor is called twice per request,
// so to prevent RefreshTokenAction is dispatched twice
// we're creating a refresh token request list
protected refreshTokenRequestUrls = [];
@@ -216,23 +215,8 @@ export class AuthInterceptor implements HttpInterceptor {
let authorization: string;
if (authService.isTokenExpired()) {
- authService.setRedirectUrl(this.router.url);
- // The access token is expired
- // Redirect to the login route
- this.store.dispatch(new RedirectWhenTokenExpiredAction('auth.messages.expired'));
return observableOf(null);
} else if ((!this.isAuthRequest(req) || this.isLogoutResponse(req)) && isNotEmpty(token)) {
- // Intercept a request that is not to the authentication endpoint
- authService.isTokenExpiring().pipe(
- filter((isExpiring) => isExpiring))
- .subscribe(() => {
- // If the current request url is already in the refresh token request list, skip it
- if (isUndefined(find(this.refreshTokenRequestUrls, req.url))) {
- // When a token is about to expire, refresh it
- this.store.dispatch(new RefreshTokenAction(token));
- this.refreshTokenRequestUrls.push(req.url);
- }
- });
// Get the auth header from the service.
authorization = authService.buildAuthHeader(token);
let newHeaders = req.headers.set('authorization', authorization);
diff --git a/src/app/core/auth/auth.reducer.spec.ts b/src/app/core/auth/auth.reducer.spec.ts
index 4c6f1e2a25..8cd587b61a 100644
--- a/src/app/core/auth/auth.reducer.spec.ts
+++ b/src/app/core/auth/auth.reducer.spec.ts
@@ -23,7 +23,9 @@ import {
RetrieveAuthMethodsAction,
RetrieveAuthMethodsErrorAction,
RetrieveAuthMethodsSuccessAction,
- SetRedirectUrlAction
+ SetRedirectUrlAction,
+ SetUserAsIdleAction,
+ UnsetUserAsIdleAction
} from './auth.actions';
import { AuthTokenInfo } from './models/auth-token-info.model';
import { EPersonMock } from '../../shared/testing/eperson.mock';
@@ -44,6 +46,7 @@ describe('authReducer', () => {
loaded: false,
blocking: true,
loading: false,
+ idle: false
};
const action = new AuthenticateAction('user', 'password');
const newState = authReducer(initialState, action);
@@ -53,7 +56,8 @@ describe('authReducer', () => {
blocking: true,
error: undefined,
loading: true,
- info: undefined
+ info: undefined,
+ idle: false
};
expect(newState).toEqual(state);
@@ -66,7 +70,8 @@ describe('authReducer', () => {
error: undefined,
blocking: true,
loading: true,
- info: undefined
+ info: undefined,
+ idle: false
};
const action = new AuthenticationSuccessAction(mockTokenInfo);
const newState = authReducer(initialState, action);
@@ -81,7 +86,8 @@ describe('authReducer', () => {
error: undefined,
blocking: true,
loading: true,
- info: undefined
+ info: undefined,
+ idle: false
};
const action = new AuthenticationErrorAction(mockError);
const newState = authReducer(initialState, action);
@@ -92,7 +98,8 @@ describe('authReducer', () => {
loading: false,
info: undefined,
authToken: undefined,
- error: 'Test error message'
+ error: 'Test error message',
+ idle: false
};
expect(newState).toEqual(state);
@@ -105,7 +112,8 @@ describe('authReducer', () => {
loaded: false,
error: undefined,
loading: true,
- info: undefined
+ info: undefined,
+ idle: false
};
const action = new AuthenticatedAction(mockTokenInfo);
const newState = authReducer(initialState, action);
@@ -115,7 +123,8 @@ describe('authReducer', () => {
loaded: false,
error: undefined,
loading: true,
- info: undefined
+ info: undefined,
+ idle: false
};
expect(newState).toEqual(state);
});
@@ -127,7 +136,8 @@ describe('authReducer', () => {
error: undefined,
blocking: true,
loading: true,
- info: undefined
+ info: undefined,
+ idle: false
};
const action = new AuthenticatedSuccessAction(true, mockTokenInfo, EPersonMock._links.self.href);
const newState = authReducer(initialState, action);
@@ -138,7 +148,8 @@ describe('authReducer', () => {
error: undefined,
blocking: true,
loading: true,
- info: undefined
+ info: undefined,
+ idle: false
};
expect(newState).toEqual(state);
});
@@ -150,7 +161,8 @@ describe('authReducer', () => {
error: undefined,
blocking: true,
loading: true,
- info: undefined
+ info: undefined,
+ idle: false
};
const action = new AuthenticatedErrorAction(mockError);
const newState = authReducer(initialState, action);
@@ -161,7 +173,8 @@ describe('authReducer', () => {
loaded: true,
blocking: false,
loading: false,
- info: undefined
+ info: undefined,
+ idle: false
};
expect(newState).toEqual(state);
});
@@ -172,6 +185,7 @@ describe('authReducer', () => {
loaded: false,
blocking: false,
loading: false,
+ idle: false
};
const action = new CheckAuthenticationTokenAction();
const newState = authReducer(initialState, action);
@@ -180,6 +194,7 @@ describe('authReducer', () => {
loaded: false,
blocking: true,
loading: true,
+ idle: false
};
expect(newState).toEqual(state);
});
@@ -190,6 +205,7 @@ describe('authReducer', () => {
loaded: false,
blocking: false,
loading: true,
+ idle: false
};
const action = new CheckAuthenticationTokenCookieAction();
const newState = authReducer(initialState, action);
@@ -198,6 +214,7 @@ describe('authReducer', () => {
loaded: false,
blocking: true,
loading: true,
+ idle: false
};
expect(newState).toEqual(state);
});
@@ -211,7 +228,8 @@ describe('authReducer', () => {
blocking: false,
loading: false,
info: undefined,
- userId: EPersonMock.id
+ userId: EPersonMock.id,
+ idle: false
};
const action = new LogOutAction();
@@ -229,7 +247,8 @@ describe('authReducer', () => {
blocking: false,
loading: false,
info: undefined,
- userId: EPersonMock.id
+ userId: EPersonMock.id,
+ idle: false
};
const action = new LogOutSuccessAction();
@@ -243,7 +262,8 @@ describe('authReducer', () => {
loading: true,
info: undefined,
refreshing: false,
- userId: undefined
+ userId: undefined,
+ idle: false
};
expect(newState).toEqual(state);
});
@@ -257,7 +277,8 @@ describe('authReducer', () => {
blocking: false,
loading: false,
info: undefined,
- userId: EPersonMock.id
+ userId: EPersonMock.id,
+ idle: false
};
const action = new LogOutErrorAction(mockError);
@@ -270,7 +291,8 @@ describe('authReducer', () => {
blocking: false,
loading: false,
info: undefined,
- userId: EPersonMock.id
+ userId: EPersonMock.id,
+ idle: false
};
expect(newState).toEqual(state);
});
@@ -283,7 +305,8 @@ describe('authReducer', () => {
error: undefined,
blocking: true,
loading: true,
- info: undefined
+ info: undefined,
+ idle: false
};
const action = new RetrieveAuthenticatedEpersonSuccessAction(EPersonMock.id);
const newState = authReducer(initialState, action);
@@ -295,7 +318,8 @@ describe('authReducer', () => {
blocking: false,
loading: false,
info: undefined,
- userId: EPersonMock.id
+ userId: EPersonMock.id,
+ idle: false
};
expect(newState).toEqual(state);
});
@@ -307,7 +331,8 @@ describe('authReducer', () => {
error: undefined,
blocking: true,
loading: true,
- info: undefined
+ info: undefined,
+ idle: false
};
const action = new RetrieveAuthenticatedEpersonErrorAction(mockError);
const newState = authReducer(initialState, action);
@@ -318,7 +343,8 @@ describe('authReducer', () => {
loaded: true,
blocking: false,
loading: false,
- info: undefined
+ info: undefined,
+ idle: false
};
expect(newState).toEqual(state);
});
@@ -332,7 +358,8 @@ describe('authReducer', () => {
blocking: false,
loading: false,
info: undefined,
- userId: EPersonMock.id
+ userId: EPersonMock.id,
+ idle: false
};
const newTokenInfo = new AuthTokenInfo('Refreshed token');
const action = new RefreshTokenAction(newTokenInfo);
@@ -346,7 +373,8 @@ describe('authReducer', () => {
loading: false,
info: undefined,
userId: EPersonMock.id,
- refreshing: true
+ refreshing: true,
+ idle: false
};
expect(newState).toEqual(state);
});
@@ -361,7 +389,8 @@ describe('authReducer', () => {
loading: false,
info: undefined,
userId: EPersonMock.id,
- refreshing: true
+ refreshing: true,
+ idle: false
};
const newTokenInfo = new AuthTokenInfo('Refreshed token');
const action = new RefreshTokenSuccessAction(newTokenInfo);
@@ -375,7 +404,8 @@ describe('authReducer', () => {
loading: false,
info: undefined,
userId: EPersonMock.id,
- refreshing: false
+ refreshing: false,
+ idle: false
};
expect(newState).toEqual(state);
});
@@ -390,7 +420,8 @@ describe('authReducer', () => {
loading: false,
info: undefined,
userId: EPersonMock.id,
- refreshing: true
+ refreshing: true,
+ idle: false
};
const action = new RefreshTokenErrorAction();
const newState = authReducer(initialState, action);
@@ -403,7 +434,8 @@ describe('authReducer', () => {
loading: false,
info: undefined,
refreshing: false,
- userId: undefined
+ userId: undefined,
+ idle: false
};
expect(newState).toEqual(state);
});
@@ -417,7 +449,8 @@ describe('authReducer', () => {
blocking: false,
loading: false,
info: undefined,
- userId: EPersonMock.id
+ userId: EPersonMock.id,
+ idle: false
};
state = {
@@ -428,7 +461,8 @@ describe('authReducer', () => {
loading: false,
error: undefined,
info: 'Message',
- userId: undefined
+ userId: undefined,
+ idle: false
};
});
@@ -450,6 +484,7 @@ describe('authReducer', () => {
loaded: false,
blocking: false,
loading: false,
+ idle: false
};
const action = new AddAuthenticationMessageAction('Message');
const newState = authReducer(initialState, action);
@@ -458,7 +493,8 @@ describe('authReducer', () => {
loaded: false,
blocking: false,
loading: false,
- info: 'Message'
+ info: 'Message',
+ idle: false
};
expect(newState).toEqual(state);
});
@@ -470,7 +506,8 @@ describe('authReducer', () => {
blocking: false,
loading: false,
error: 'Error',
- info: 'Message'
+ info: 'Message',
+ idle: false
};
const action = new ResetAuthenticationMessagesAction();
const newState = authReducer(initialState, action);
@@ -480,7 +517,8 @@ describe('authReducer', () => {
blocking: false,
loading: false,
error: undefined,
- info: undefined
+ info: undefined,
+ idle: false
};
expect(newState).toEqual(state);
});
@@ -490,7 +528,8 @@ describe('authReducer', () => {
authenticated: false,
loaded: false,
blocking: false,
- loading: false
+ loading: false,
+ idle: false
};
const action = new SetRedirectUrlAction('redirect.url');
const newState = authReducer(initialState, action);
@@ -499,7 +538,8 @@ describe('authReducer', () => {
loaded: false,
blocking: false,
loading: false,
- redirectUrl: 'redirect.url'
+ redirectUrl: 'redirect.url',
+ idle: false
};
expect(newState).toEqual(state);
});
@@ -510,7 +550,8 @@ describe('authReducer', () => {
loaded: false,
blocking: false,
loading: false,
- authMethods: []
+ authMethods: [],
+ idle: false
};
const action = new RetrieveAuthMethodsAction(new AuthStatus());
const newState = authReducer(initialState, action);
@@ -519,7 +560,8 @@ describe('authReducer', () => {
loaded: false,
blocking: true,
loading: true,
- authMethods: []
+ authMethods: [],
+ idle: false
};
expect(newState).toEqual(state);
});
@@ -530,7 +572,8 @@ describe('authReducer', () => {
loaded: false,
blocking: true,
loading: true,
- authMethods: []
+ authMethods: [],
+ idle: false
};
const authMethods = [
new AuthMethod(AuthMethodType.Password),
@@ -543,7 +586,8 @@ describe('authReducer', () => {
loaded: false,
blocking: false,
loading: false,
- authMethods: authMethods
+ authMethods: authMethods,
+ idle: false
};
expect(newState).toEqual(state);
});
@@ -554,7 +598,8 @@ describe('authReducer', () => {
loaded: false,
blocking: true,
loading: true,
- authMethods: []
+ authMethods: [],
+ idle: false
};
const action = new RetrieveAuthMethodsErrorAction();
@@ -564,7 +609,50 @@ describe('authReducer', () => {
loaded: false,
blocking: false,
loading: false,
- authMethods: [new AuthMethod(AuthMethodType.Password)]
+ authMethods: [new AuthMethod(AuthMethodType.Password)],
+ idle: false
+ };
+ expect(newState).toEqual(state);
+ });
+
+ it('should properly set the state, in response to a SET_USER_AS_IDLE action', () => {
+ initialState = {
+ authenticated: true,
+ loaded: true,
+ blocking: false,
+ loading: false,
+ idle: false
+ };
+
+ const action = new SetUserAsIdleAction();
+ const newState = authReducer(initialState, action);
+ state = {
+ authenticated: true,
+ loaded: true,
+ blocking: false,
+ loading: false,
+ idle: true
+ };
+ expect(newState).toEqual(state);
+ });
+
+ it('should properly set the state, in response to a UNSET_USER_AS_IDLE action', () => {
+ initialState = {
+ authenticated: true,
+ loaded: true,
+ blocking: false,
+ loading: false,
+ idle: true
+ };
+
+ const action = new UnsetUserAsIdleAction();
+ const newState = authReducer(initialState, action);
+ state = {
+ authenticated: true,
+ loaded: true,
+ blocking: false,
+ loading: false,
+ idle: false
};
expect(newState).toEqual(state);
});
diff --git a/src/app/core/auth/auth.reducer.ts b/src/app/core/auth/auth.reducer.ts
index 6d5635f263..2fc79a8861 100644
--- a/src/app/core/auth/auth.reducer.ts
+++ b/src/app/core/auth/auth.reducer.ts
@@ -58,6 +58,9 @@ export interface AuthState {
// all authentication Methods enabled at the backend
authMethods?: AuthMethod[];
+ // true when the current user is idle
+ idle: boolean;
+
}
/**
@@ -68,7 +71,8 @@ const initialState: AuthState = {
loaded: false,
blocking: true,
loading: false,
- authMethods: []
+ authMethods: [],
+ idle: false
};
/**
@@ -188,6 +192,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
return Object.assign({}, state, {
authToken: (action as RefreshTokenSuccessAction).payload,
refreshing: false,
+ blocking: false
});
case AuthActionTypes.ADD_MESSAGE:
@@ -233,6 +238,16 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
blocking: true,
});
+ case AuthActionTypes.SET_USER_AS_IDLE:
+ return Object.assign({}, state, {
+ idle: true,
+ });
+
+ case AuthActionTypes.UNSET_USER_AS_IDLE:
+ return Object.assign({}, state, {
+ idle: false,
+ });
+
default:
return state;
}
diff --git a/src/app/core/auth/auth.service.spec.ts b/src/app/core/auth/auth.service.spec.ts
index 505f925e8e..ced8bb94c8 100644
--- a/src/app/core/auth/auth.service.spec.ts
+++ b/src/app/core/auth/auth.service.spec.ts
@@ -27,6 +27,11 @@ import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.util
import { authMethodsMock } from '../../shared/testing/auth-service.stub';
import { AuthMethod } from './models/auth.method';
import { HardRedirectService } from '../services/hard-redirect.service';
+import { NotificationsService } from '../../shared/notifications/notifications.service';
+import { TranslateService } from '@ngx-translate/core';
+import { getMockTranslateService } from '../../shared/mocks/translate.service.mock';
+import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
+import { SetUserAsIdleAction, UnsetUserAsIdleAction } from './auth.actions';
describe('AuthService test', () => {
@@ -47,6 +52,7 @@ describe('AuthService test', () => {
let token: AuthTokenInfo;
let authenticatedState;
let unAuthenticatedState;
+ let idleState;
let linkService;
let hardRedirectService;
@@ -64,14 +70,24 @@ describe('AuthService test', () => {
loaded: true,
loading: false,
authToken: token,
- user: EPersonMock
+ user: EPersonMock,
+ idle: false
};
unAuthenticatedState = {
authenticated: false,
loaded: true,
loading: false,
authToken: undefined,
- user: undefined
+ user: undefined,
+ idle: false
+ };
+ idleState = {
+ authenticated: true,
+ loaded: true,
+ loading: false,
+ authToken: token,
+ user: EPersonMock,
+ idle: true
};
authRequest = new AuthRequestServiceStub();
routeStub = new ActivatedRouteStub();
@@ -107,6 +123,8 @@ describe('AuthService test', () => {
{ provide: Store, useValue: mockStore },
{ provide: EPersonDataService, useValue: mockEpersonDataService },
{ provide: HardRedirectService, useValue: hardRedirectService },
+ { provide: NotificationsService, useValue: NotificationsServiceStub },
+ { provide: TranslateService, useValue: getMockTranslateService() },
CookieService,
AuthService
],
@@ -180,6 +198,26 @@ describe('AuthService test', () => {
expect(authMethods.length).toBe(2);
});
});
+
+ describe('setIdle true', () => {
+ beforeEach(() => {
+ authService.setIdle(true);
+ });
+
+ it('store should dispatch SetUserAsIdleAction', () => {
+ expect(mockStore.dispatch).toHaveBeenCalledWith(new SetUserAsIdleAction());
+ });
+ });
+
+ describe('setIdle false', () => {
+ beforeEach(() => {
+ authService.setIdle(false);
+ });
+
+ it('store should dispatch UnsetUserAsIdleAction', () => {
+ expect(mockStore.dispatch).toHaveBeenCalledWith(new UnsetUserAsIdleAction());
+ });
+ });
});
describe('', () => {
@@ -207,13 +245,13 @@ describe('AuthService test', () => {
}).compileComponents();
}));
- beforeEach(inject([CookieService, AuthRequestService, Store, Router, RouteService], (cookieService: CookieService, authReqService: AuthRequestService, store: Store, router: Router, routeService: RouteService) => {
+ beforeEach(inject([CookieService, AuthRequestService, Store, Router, RouteService], (cookieService: CookieService, authReqService: AuthRequestService, store: Store, router: Router, routeService: RouteService, notificationsService: NotificationsService, translateService: TranslateService) => {
store
.subscribe((state) => {
(state as any).core = Object.create({});
(state as any).core.auth = authenticatedState;
});
- authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService);
+ authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService, notificationsService, translateService);
}));
it('should return true when user is logged in', () => {
@@ -250,6 +288,12 @@ describe('AuthService test', () => {
});
});
+ it('isUserIdle should return false when user is not yet idle', () => {
+ authService.isUserIdle().subscribe((status: boolean) => {
+ expect(status).toBe(false);
+ });
+ });
+
});
describe('', () => {
@@ -277,7 +321,7 @@ describe('AuthService test', () => {
}).compileComponents();
}));
- beforeEach(inject([ClientCookieService, AuthRequestService, Store, Router, RouteService], (cookieService: ClientCookieService, authReqService: AuthRequestService, store: Store, router: Router, routeService: RouteService) => {
+ beforeEach(inject([ClientCookieService, AuthRequestService, Store, Router, RouteService], (cookieService: ClientCookieService, authReqService: AuthRequestService, store: Store, router: Router, routeService: RouteService, notificationsService: NotificationsService, translateService: TranslateService) => {
const expiredToken: AuthTokenInfo = new AuthTokenInfo('test_token');
expiredToken.expires = Date.now() - (1000 * 60 * 60);
authenticatedState = {
@@ -292,7 +336,7 @@ describe('AuthService test', () => {
(state as any).core = Object.create({});
(state as any).core.auth = authenticatedState;
});
- authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService);
+ authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService, notificationsService, translateService);
storage = (authService as any).storage;
routeServiceMock = TestBed.inject(RouteService);
routerStub = TestBed.inject(Router);
@@ -493,13 +537,13 @@ describe('AuthService test', () => {
}).compileComponents();
}));
- beforeEach(inject([CookieService, AuthRequestService, Store, Router, RouteService], (cookieService: CookieService, authReqService: AuthRequestService, store: Store, router: Router, routeService: RouteService) => {
+ beforeEach(inject([CookieService, AuthRequestService, Store, Router, RouteService], (cookieService: CookieService, authReqService: AuthRequestService, store: Store, router: Router, routeService: RouteService, notificationsService: NotificationsService, translateService: TranslateService) => {
store
.subscribe((state) => {
(state as any).core = Object.create({});
(state as any).core.auth = unAuthenticatedState;
});
- authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService);
+ authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService, notificationsService, translateService);
}));
it('should return null for the shortlived token', () => {
@@ -508,4 +552,44 @@ describe('AuthService test', () => {
});
});
});
+
+ describe('when user is idle', () => {
+ beforeEach(waitForAsync(() => {
+ init();
+ TestBed.configureTestingModule({
+ imports: [
+ StoreModule.forRoot({ authReducer }, {
+ runtimeChecks: {
+ strictStateImmutability: false,
+ strictActionImmutability: false
+ }
+ })
+ ],
+ providers: [
+ { provide: AuthRequestService, useValue: authRequest },
+ { provide: REQUEST, useValue: {} },
+ { provide: Router, useValue: routerStub },
+ { provide: RouteService, useValue: routeServiceStub },
+ { provide: RemoteDataBuildService, useValue: linkService },
+ CookieService,
+ AuthService
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(inject([CookieService, AuthRequestService, Store, Router, RouteService], (cookieService: CookieService, authReqService: AuthRequestService, store: Store, router: Router, routeService: RouteService, notificationsService: NotificationsService, translateService: TranslateService) => {
+ store
+ .subscribe((state) => {
+ (state as any).core = Object.create({});
+ (state as any).core.auth = idleState;
+ });
+ authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService, notificationsService, translateService);
+ }));
+
+ it('isUserIdle should return true when user is not idle', () => {
+ authService.isUserIdle().subscribe((status: boolean) => {
+ expect(status).toBe(true);
+ });
+ });
+ });
});
diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts
index fa29f1bc36..09848d9044 100644
--- a/src/app/core/auth/auth.service.ts
+++ b/src/app/core/auth/auth.service.ts
@@ -29,13 +29,17 @@ import {
getRedirectUrl,
isAuthenticated,
isAuthenticatedLoaded,
+ isIdle,
isTokenRefreshing
} from './selectors';
import { AppState } from '../../app.reducer';
import {
CheckAuthenticationTokenAction,
+ RefreshTokenAction,
ResetAuthenticationMessagesAction,
- SetRedirectUrlAction
+ SetRedirectUrlAction,
+ SetUserAsIdleAction,
+ UnsetUserAsIdleAction
} from './auth.actions';
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
import { Base64EncodeUrl } from '../../shared/utils/encode-decode.util';
@@ -45,6 +49,9 @@ import { getAllSucceededRemoteDataPayload } from '../shared/operators';
import { AuthMethod } from './models/auth.method';
import { HardRedirectService } from '../services/hard-redirect.service';
import { RemoteData } from '../data/remote-data';
+import { environment } from '../../../environments/environment';
+import { NotificationsService } from '../../shared/notifications/notifications.service';
+import { TranslateService } from '@ngx-translate/core';
export const LOGIN_ROUTE = '/login';
export const LOGOUT_ROUTE = '/logout';
@@ -63,6 +70,11 @@ export class AuthService {
*/
protected _authenticated: boolean;
+ /**
+ * Timer to track time until token refresh
+ */
+ private tokenRefreshTimer;
+
constructor(@Inject(REQUEST) protected req: any,
@Inject(NativeWindowService) protected _window: NativeWindowRef,
@Optional() @Inject(RESPONSE) private response: any,
@@ -72,7 +84,9 @@ export class AuthService {
protected routeService: RouteService,
protected storage: CookieService,
protected store: Store,
- protected hardRedirectService: HardRedirectService
+ protected hardRedirectService: HardRedirectService,
+ private notificationService: NotificationsService,
+ private translateService: TranslateService
) {
this.store.pipe(
select(isAuthenticated),
@@ -186,7 +200,7 @@ export class AuthService {
return this.store.pipe(
select(getAuthenticatedUserId),
hasValueOperator(),
- switchMap((id: string) => this.epersonService.findById(id) ),
+ switchMap((id: string) => this.epersonService.findById(id)),
getAllSucceededRemoteDataPayload()
);
}
@@ -297,7 +311,7 @@ export class AuthService {
*/
public getToken(): AuthTokenInfo {
let token: AuthTokenInfo;
- this.store.pipe(select(getAuthenticationToken))
+ this.store.pipe(take(1), select(getAuthenticationToken))
.subscribe((authTokenInfo: AuthTokenInfo) => {
// Retrieve authentication token info and check if is valid
token = authTokenInfo || null;
@@ -305,6 +319,44 @@ export class AuthService {
return token;
}
+ /**
+ * Method that checks when the session token from store expires and refreshes it when needed
+ */
+ public trackTokenExpiration(): void {
+ let token: AuthTokenInfo;
+ let currentlyRefreshingToken = false;
+ this.store.pipe(select(getAuthenticationToken)).subscribe((authTokenInfo: AuthTokenInfo) => {
+ // If new token is undefined an it wasn't previously => Refresh failed
+ if (currentlyRefreshingToken && token !== undefined && authTokenInfo === undefined) {
+ // Token refresh failed => Error notification => 10 second wait => Page reloads & user logged out
+ this.notificationService.error(this.translateService.get('auth.messages.token-refresh-failed'));
+ setTimeout(() => this.navigateToRedirectUrl(this.hardRedirectService.getCurrentRoute()), 10000);
+ currentlyRefreshingToken = false;
+ }
+ // If new token.expires is different => Refresh succeeded
+ if (currentlyRefreshingToken && authTokenInfo !== undefined && token.expires !== authTokenInfo.expires) {
+ currentlyRefreshingToken = false;
+ }
+ // Check if/when token needs to be refreshed
+ if (!currentlyRefreshingToken) {
+ token = authTokenInfo || null;
+ if (token !== undefined && token !== null) {
+ let timeLeftBeforeRefresh = token.expires - new Date().getTime() - environment.auth.rest.timeLeftBeforeTokenRefresh;
+ if (timeLeftBeforeRefresh < 0) {
+ timeLeftBeforeRefresh = 0;
+ }
+ if (hasValue(this.tokenRefreshTimer)) {
+ clearTimeout(this.tokenRefreshTimer);
+ }
+ this.tokenRefreshTimer = setTimeout(() => {
+ this.store.dispatch(new RefreshTokenAction(token));
+ currentlyRefreshingToken = true;
+ }, timeLeftBeforeRefresh);
+ }
+ }
+ });
+ }
+
/**
* Check if a token is next to be expired
* @returns {boolean}
@@ -345,7 +397,7 @@ export class AuthService {
// Set the cookie expire date
const expires = new Date(expireDate);
- const options: CookieAttributes = { expires: expires };
+ const options: CookieAttributes = {expires: expires};
// Save cookie with the token
return this.storage.set(TOKENITEM, token, options);
@@ -395,11 +447,14 @@ export class AuthService {
* @param redirectUrl
*/
public navigateToRedirectUrl(redirectUrl: string) {
- let url = `/reload/${new Date().getTime()}`;
- if (isNotEmpty(redirectUrl) && !redirectUrl.startsWith(LOGIN_ROUTE)) {
- url += `?redirect=${encodeURIComponent(redirectUrl)}`;
+ // Don't do redirect if already on reload url
+ if (!hasValue(redirectUrl) || !redirectUrl.includes('/reload/')) {
+ let url = `/reload/${new Date().getTime()}`;
+ if (isNotEmpty(redirectUrl) && !redirectUrl.startsWith(LOGIN_ROUTE)) {
+ url += `?redirect=${encodeURIComponent(redirectUrl)}`;
+ }
+ this.hardRedirectService.redirect(url);
}
- this.hardRedirectService.redirect(url);
}
/**
@@ -434,7 +489,7 @@ export class AuthService {
// Set the cookie expire date
const expires = new Date(expireDate);
- const options: CookieAttributes = { expires: expires };
+ const options: CookieAttributes = {expires: expires};
this.storage.set(REDIRECT_COOKIE, url, options);
this.store.dispatch(new SetRedirectUrlAction(isNotUndefined(url) ? url : ''));
}
@@ -518,4 +573,24 @@ export class AuthService {
);
}
+ /**
+ * Determines if current user is idle
+ * @returns {Observable}
+ */
+ public isUserIdle(): Observable {
+ return this.store.pipe(select(isIdle));
+ }
+
+ /**
+ * Set idle of auth state
+ * @returns {Observable}
+ */
+ public setIdle(idle: boolean): void {
+ if (idle) {
+ this.store.dispatch(new SetUserAsIdleAction());
+ } else {
+ this.store.dispatch(new UnsetUserAsIdleAction());
+ }
+ }
+
}
diff --git a/src/app/core/auth/selectors.ts b/src/app/core/auth/selectors.ts
index c4e95a0fb3..9ee9f7eb2e 100644
--- a/src/app/core/auth/selectors.ts
+++ b/src/app/core/auth/selectors.ts
@@ -115,6 +115,14 @@ const _getRedirectUrl = (state: AuthState) => state.redirectUrl;
const _getAuthenticationMethods = (state: AuthState) => state.authMethods;
+/**
+ * Returns true if the user is idle.
+ * @function _isIdle
+ * @param {State} state
+ * @returns {boolean}
+ */
+const _isIdle = (state: AuthState) => state.idle;
+
/**
* Returns the authentication methods enabled at the backend
* @function getAuthenticationMethods
@@ -231,3 +239,12 @@ export const getRegistrationError = createSelector(getAuthState, _getRegistratio
* @return {string}
*/
export const getRedirectUrl = createSelector(getAuthState, _getRedirectUrl);
+
+/**
+ * Returns true if the user is idle
+ * @function isIdle
+ * @param {AuthState} state
+ * @param {any} props
+ * @return {boolean}
+ */
+export const isIdle = createSelector(getAuthState, _isIdle);
diff --git a/src/app/core/auth/server-auth.service.ts b/src/app/core/auth/server-auth.service.ts
index 9840b22267..ea5a3b41f2 100644
--- a/src/app/core/auth/server-auth.service.ts
+++ b/src/app/core/auth/server-auth.service.ts
@@ -4,7 +4,7 @@ import { HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
-import { isNotEmpty, hasValue } from '../../shared/empty.util';
+import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
import { AuthService } from './auth.service';
import { AuthStatus } from './models/auth-status.model';
diff --git a/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts
index d41446c185..46c49add06 100644
--- a/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts
+++ b/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts
@@ -4,7 +4,7 @@ import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver';
import { Collection } from '../shared/collection.model';
import { CollectionDataService } from '../data/collection-data.service';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
-import { COLLECTION_PAGE_LINKS_TO_FOLLOW } from '../../+collection-page/collection-page.resolver';
+import { COLLECTION_PAGE_LINKS_TO_FOLLOW } from '../../collection-page/collection-page.resolver';
/**
* The class that resolves the BreadcrumbConfig object for a Collection
diff --git a/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts
index 27cc207c58..309927771d 100644
--- a/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts
+++ b/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts
@@ -4,7 +4,7 @@ import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver';
import { CommunityDataService } from '../data/community-data.service';
import { Community } from '../shared/community.model';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
-import { COMMUNITY_PAGE_LINKS_TO_FOLLOW } from '../../+community-page/community-page.resolver';
+import { COMMUNITY_PAGE_LINKS_TO_FOLLOW } from '../../community-page/community-page.resolver';
/**
* The class that resolves the BreadcrumbConfig object for a Community
diff --git a/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts
index 2b9bbd6b3d..3005b6f09a 100644
--- a/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts
+++ b/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts
@@ -4,7 +4,7 @@ import { ItemDataService } from '../data/item-data.service';
import { Item } from '../shared/item.model';
import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
-import { ITEM_PAGE_LINKS_TO_FOLLOW } from '../../+item-page/item-page.resolver';
+import { ITEM_PAGE_LINKS_TO_FOLLOW } from '../../item-page/item.resolver';
/**
* The class that resolves the BreadcrumbConfig object for an Item
diff --git a/src/app/core/browse/browse.service.spec.ts b/src/app/core/browse/browse.service.spec.ts
index 89875b3069..a28add2e30 100644
--- a/src/app/core/browse/browse.service.spec.ts
+++ b/src/app/core/browse/browse.service.spec.ts
@@ -127,7 +127,8 @@ describe('BrowseService', () => {
});
describe('getBrowseEntriesFor and findList', () => {
- const mockAuthorName = 'Donald Smith';
+ // should contain special characters such that url encoding can be tested as well
+ const mockAuthorName = 'Donald Smith & Sons';
beforeEach(() => {
requestService = getMockRequestService(getRequestEntry$(true));
@@ -152,7 +153,7 @@ describe('BrowseService', () => {
describe('when findList is called with a valid browse definition id', () => {
it('should call hrefOnlyDataService.findAllByHref with the expected href', () => {
- const expected = browseDefinitions[1]._links.items.href + '?filterValue=' + mockAuthorName;
+ const expected = browseDefinitions[1]._links.items.href + '?filterValue=' + encodeURIComponent(mockAuthorName);
scheduler.schedule(() => service.getBrowseItemsFor(mockAuthorName, new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe());
scheduler.flush();
diff --git a/src/app/core/browse/browse.service.ts b/src/app/core/browse/browse.service.ts
index 7e55d381a6..ffc6f313b9 100644
--- a/src/app/core/browse/browse.service.ts
+++ b/src/app/core/browse/browse.service.ts
@@ -130,7 +130,7 @@ export class BrowseService {
args.push(`startsWith=${options.startsWith}`);
}
if (isNotEmpty(filterValue)) {
- args.push(`filterValue=${filterValue}`);
+ args.push(`filterValue=${encodeURIComponent(filterValue)}`);
}
if (isNotEmpty(args)) {
href = new URLCombiner(href, `?${args.join('&')}`).toString();
diff --git a/src/app/core/cache/builders/build-decorators.ts b/src/app/core/cache/builders/build-decorators.ts
index 1ea09b6684..b561ababde 100644
--- a/src/app/core/cache/builders/build-decorators.ts
+++ b/src/app/core/cache/builders/build-decorators.ts
@@ -8,6 +8,20 @@ import {
TypedObject,
getResourceTypeValueFor
} from '../object-cache.reducer';
+import { InjectionToken } from '@angular/core';
+
+export const DATA_SERVICE_FACTORY = new InjectionToken<(resourceType: ResourceType) => GenericConstructor>('getDataServiceFor', {
+ providedIn: 'root',
+ factory: () => getDataServiceFor
+});
+export const LINK_DEFINITION_FACTORY = new InjectionToken<(source: GenericConstructor, linkName: keyof T['_links']) => LinkDefinition>('getLinkDefinition', {
+ providedIn: 'root',
+ factory: () => getLinkDefinition
+});
+export const LINK_DEFINITION_MAP_FACTORY = new InjectionToken<(source: GenericConstructor) => Map>>('getLinkDefinitions', {
+ providedIn: 'root',
+ factory: () => getLinkDefinitions
+});
const resolvedLinkKey = Symbol('resolvedLink');
diff --git a/src/app/core/cache/builders/link.service.spec.ts b/src/app/core/cache/builders/link.service.spec.ts
index 1c41cfee86..f567c39314 100644
--- a/src/app/core/cache/builders/link.service.spec.ts
+++ b/src/app/core/cache/builders/link.service.spec.ts
@@ -5,15 +5,9 @@ import { FindListOptions } from '../../data/request.models';
import { HALLink } from '../../shared/hal-link.model';
import { HALResource } from '../../shared/hal-resource.model';
import { ResourceType } from '../../shared/resource-type';
-import * as decorators from './build-decorators';
import { LinkService } from './link.service';
-
-const spyOnFunction = (obj: T, func: keyof T) => {
- const spy = jasmine.createSpy(func as string);
- spyOnProperty(obj, func, 'get').and.returnValue(spy);
-
- return spy;
-};
+import { DATA_SERVICE_FACTORY, LINK_DEFINITION_FACTORY, LINK_DEFINITION_MAP_FACTORY } from './build-decorators';
+import { isEmpty } from 'rxjs/operators';
const TEST_MODEL = new ResourceType('testmodel');
let result: any;
@@ -51,7 +45,7 @@ let testDataService: TestDataService;
let testModel: TestModel;
-xdescribe('LinkService', () => {
+describe('LinkService', () => {
let service: LinkService;
beforeEach(() => {
@@ -76,6 +70,30 @@ xdescribe('LinkService', () => {
providers: [LinkService, {
provide: TestDataService,
useValue: testDataService
+ }, {
+ provide: DATA_SERVICE_FACTORY,
+ useValue: jasmine.createSpy('getDataServiceFor').and.returnValue(TestDataService),
+ }, {
+ provide: LINK_DEFINITION_FACTORY,
+ useValue: jasmine.createSpy('getLinkDefinition').and.returnValue({
+ resourceType: TEST_MODEL,
+ linkName: 'predecessor',
+ propertyName: 'predecessor'
+ }),
+ }, {
+ provide: LINK_DEFINITION_MAP_FACTORY,
+ useValue: jasmine.createSpy('getLinkDefinitions').and.returnValue([
+ {
+ resourceType: TEST_MODEL,
+ linkName: 'predecessor',
+ propertyName: 'predecessor',
+ },
+ {
+ resourceType: TEST_MODEL,
+ linkName: 'successor',
+ propertyName: 'successor',
+ }
+ ]),
}]
});
service = TestBed.inject(LinkService);
@@ -84,13 +102,7 @@ xdescribe('LinkService', () => {
describe('resolveLink', () => {
describe(`when the linkdefinition concerns a single object`, () => {
beforeEach(() => {
- spyOnFunction(decorators, 'getLinkDefinition').and.returnValue({
- resourceType: TEST_MODEL,
- linkName: 'predecessor',
- propertyName: 'predecessor'
- });
- spyOnFunction(decorators, 'getDataServiceFor').and.returnValue(TestDataService);
- service.resolveLink(testModel, followLink('predecessor', {}, true, true, true, followLink('successor')));
+ service.resolveLink(testModel, followLink('predecessor', {}, followLink('successor')));
});
it('should call dataservice.findByHref with the correct href and nested links', () => {
expect(testDataService.findByHref).toHaveBeenCalledWith(testModel._links.predecessor.href, true, true, followLink('successor'));
@@ -98,14 +110,13 @@ xdescribe('LinkService', () => {
});
describe(`when the linkdefinition concerns a list`, () => {
beforeEach(() => {
- spyOnFunction(decorators, 'getLinkDefinition').and.returnValue({
+ ((service as any).getLinkDefinition as jasmine.Spy).and.returnValue({
resourceType: TEST_MODEL,
linkName: 'predecessor',
propertyName: 'predecessor',
isList: true
});
- spyOnFunction(decorators, 'getDataServiceFor').and.returnValue(TestDataService);
- service.resolveLink(testModel, followLink('predecessor', { some: 'options ' } as any, true, true, true, followLink('successor')));
+ service.resolveLink(testModel, followLink('predecessor', { findListOptions: { some: 'options ' } as any }, followLink('successor')));
});
it('should call dataservice.findAllByHref with the correct href, findListOptions, and nested links', () => {
expect(testDataService.findAllByHref).toHaveBeenCalledWith(testModel._links.predecessor.href, { some: 'options ' } as any, true, true, followLink('successor'));
@@ -113,21 +124,15 @@ xdescribe('LinkService', () => {
});
describe('either way', () => {
beforeEach(() => {
- spyOnFunction(decorators, 'getLinkDefinition').and.returnValue({
- resourceType: TEST_MODEL,
- linkName: 'predecessor',
- propertyName: 'predecessor'
- });
- spyOnFunction(decorators, 'getDataServiceFor').and.returnValue(TestDataService);
- result = service.resolveLink(testModel, followLink('predecessor', {}, true, true, true, followLink('successor')));
+ result = service.resolveLink(testModel, followLink('predecessor', {}, followLink('successor')));
});
it('should call getLinkDefinition with the correct model and link', () => {
- expect(decorators.getLinkDefinition).toHaveBeenCalledWith(testModel.constructor as any, 'predecessor');
+ expect((service as any).getLinkDefinition).toHaveBeenCalledWith(testModel.constructor as any, 'predecessor');
});
it('should call getDataServiceFor with the correct resource type', () => {
- expect(decorators.getDataServiceFor).toHaveBeenCalledWith(TEST_MODEL);
+ expect((service as any).getDataServiceFor).toHaveBeenCalledWith(TEST_MODEL);
});
it('should return the model with the resolved link', () => {
@@ -140,27 +145,22 @@ xdescribe('LinkService', () => {
describe(`when the specified link doesn't exist on the model's class`, () => {
beforeEach(() => {
- spyOnFunction(decorators, 'getLinkDefinition').and.returnValue(undefined);
+ ((service as any).getLinkDefinition as jasmine.Spy).and.returnValue(undefined);
});
it('should throw an error', () => {
expect(() => {
- service.resolveLink(testModel, followLink('predecessor', {}, true, true, true, followLink('successor')));
+ service.resolveLink(testModel, followLink('predecessor', {}, followLink('successor')));
}).toThrow();
});
});
describe(`when there is no dataservice for the resourcetype in the link`, () => {
beforeEach(() => {
- spyOnFunction(decorators, 'getLinkDefinition').and.returnValue({
- resourceType: TEST_MODEL,
- linkName: 'predecessor',
- propertyName: 'predecessor'
- });
- spyOnFunction(decorators, 'getDataServiceFor').and.returnValue(undefined);
+ ((service as any).getDataServiceFor as jasmine.Spy).and.returnValue(undefined);
});
it('should throw an error', () => {
expect(() => {
- service.resolveLink(testModel, followLink('predecessor', {}, true, true, true, followLink('successor')));
+ service.resolveLink(testModel, followLink('predecessor', {}, followLink('successor')));
}).toThrow();
});
});
@@ -188,18 +188,6 @@ xdescribe('LinkService', () => {
beforeEach(() => {
testModel.predecessor = 'predecessor value' as any;
testModel.successor = 'successor value' as any;
- spyOnFunction(decorators, 'getLinkDefinitions').and.returnValue([
- {
- resourceType: TEST_MODEL,
- linkName: 'predecessor',
- propertyName: 'predecessor',
- },
- {
- resourceType: TEST_MODEL,
- linkName: 'successor',
- propertyName: 'successor',
- }
- ]);
});
it('should return a new version of the object without any resolved links', () => {
@@ -231,16 +219,10 @@ xdescribe('LinkService', () => {
}
}
});
- spyOnFunction(decorators, 'getDataServiceFor').and.returnValue(TestDataService);
});
describe('resolving the available link', () => {
beforeEach(() => {
- spyOnFunction(decorators, 'getLinkDefinition').and.returnValue({
- resourceType: TEST_MODEL,
- linkName: 'predecessor',
- propertyName: 'predecessor'
- });
result = service.resolveLinks(testModel, followLink('predecessor'));
});
@@ -251,7 +233,7 @@ xdescribe('LinkService', () => {
describe('resolving the missing link', () => {
beforeEach(() => {
- spyOnFunction(decorators, 'getLinkDefinition').and.returnValue({
+ ((service as any).getLinkDefinition as jasmine.Spy).and.returnValue({
resourceType: TEST_MODEL,
linkName: 'successor',
propertyName: 'successor'
@@ -259,8 +241,11 @@ xdescribe('LinkService', () => {
result = service.resolveLinks(testModel, followLink('successor'));
});
- it('should return the model with no resolved link', () => {
- expect(result.successor).toBeUndefined();
+ it('should resolve to an empty observable', (done) => {
+ result.successor.pipe(isEmpty()).subscribe((v) => {
+ expect(v).toEqual(true);
+ done();
+ });
});
});
});
diff --git a/src/app/core/cache/builders/link.service.ts b/src/app/core/cache/builders/link.service.ts
index 56a1154b77..66f91dbbd6 100644
--- a/src/app/core/cache/builders/link.service.ts
+++ b/src/app/core/cache/builders/link.service.ts
@@ -1,17 +1,18 @@
-import { Injectable, Injector } from '@angular/core';
+import { Inject, Injectable, Injector } from '@angular/core';
import { hasNoValue, hasValue, isNotEmpty } from '../../../shared/empty.util';
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
import { GenericConstructor } from '../../shared/generic-constructor';
import { HALResource } from '../../shared/hal-resource.model';
import {
- getDataServiceFor,
- getLinkDefinition,
- getLinkDefinitions,
+ DATA_SERVICE_FACTORY,
+ LINK_DEFINITION_FACTORY,
+ LINK_DEFINITION_MAP_FACTORY,
LinkDefinition
} from './build-decorators';
import { RemoteData } from '../../data/remote-data';
import { Observable } from 'rxjs/internal/Observable';
import { EMPTY } from 'rxjs';
+import { ResourceType } from '../../shared/resource-type';
/**
* A Service to handle the resolving and removing
@@ -24,6 +25,9 @@ export class LinkService {
constructor(
protected parentInjector: Injector,
+ @Inject(DATA_SERVICE_FACTORY) private getDataServiceFor: (resourceType: ResourceType) => GenericConstructor,
+ @Inject(LINK_DEFINITION_FACTORY) private getLinkDefinition: (source: GenericConstructor, linkName: keyof T['_links']) => LinkDefinition,
+ @Inject(LINK_DEFINITION_MAP_FACTORY) private getLinkDefinitions: (source: GenericConstructor) => Map>,
) {
}
@@ -35,7 +39,7 @@ export class LinkService {
*/
public resolveLinks(model: T, ...linksToFollow: FollowLinkConfig[]): T {
linksToFollow.forEach((linkToFollow: FollowLinkConfig) => {
- this.resolveLink(model, linkToFollow);
+ this.resolveLink(model, linkToFollow);
});
return model;
}
@@ -49,12 +53,10 @@ export class LinkService {
* @param linkToFollow the {@link FollowLinkConfig} to resolve
*/
public resolveLinkWithoutAttaching(model, linkToFollow: FollowLinkConfig): Observable> {
- const matchingLinkDef = getLinkDefinition(model.constructor, linkToFollow.name);
+ const matchingLinkDef = this.getLinkDefinition(model.constructor, linkToFollow.name);
- if (hasNoValue(matchingLinkDef)) {
- throw new Error(`followLink('${linkToFollow.name}') was used for a ${model.constructor.name}, but there is no property on ${model.constructor.name} models with an @link() for ${linkToFollow.name}`);
- } else {
- const provider = getDataServiceFor(matchingLinkDef.resourceType);
+ if (hasValue(matchingLinkDef)) {
+ const provider = this.getDataServiceFor(matchingLinkDef.resourceType);
if (hasNoValue(provider)) {
throw new Error(`The @link() for ${linkToFollow.name} on ${model.constructor.name} models uses the resource type ${matchingLinkDef.resourceType.value.toUpperCase()}, but there is no service with an @dataService(${matchingLinkDef.resourceType.value.toUpperCase()}) annotation in order to retrieve it`);
@@ -80,7 +82,10 @@ export class LinkService {
throw e;
}
}
+ } else if (!linkToFollow.isOptional) {
+ throw new Error(`followLink('${linkToFollow.name}') was used as a required link for a ${model.constructor.name}, but there is no property on ${model.constructor.name} models with an @link() for ${linkToFollow.name}`);
}
+
return EMPTY;
}
@@ -104,7 +109,7 @@ export class LinkService {
*/
public removeResolvedLinks(model: T): T {
const result = Object.assign(new (model.constructor as GenericConstructor)(), model);
- const linkDefs = getLinkDefinitions(model.constructor as GenericConstructor);
+ const linkDefs = this.getLinkDefinitions(model.constructor as GenericConstructor);
if (isNotEmpty(linkDefs)) {
linkDefs.forEach((linkDef: LinkDefinition) => {
result[linkDef.propertyName] = undefined;
diff --git a/src/app/core/cache/builders/remote-data-build.service.spec.ts b/src/app/core/cache/builders/remote-data-build.service.spec.ts
index cb193724a7..0cb45733a6 100644
--- a/src/app/core/cache/builders/remote-data-build.service.spec.ts
+++ b/src/app/core/cache/builders/remote-data-build.service.spec.ts
@@ -523,7 +523,7 @@ describe('RemoteDataBuildService', () => {
let paginatedLinksToFollow;
beforeEach(() => {
paginatedLinksToFollow = [
- followLink('page', undefined, true, true, true, ...linksToFollow),
+ followLink('page', {}, ...linksToFollow),
...linksToFollow
];
});
diff --git a/src/app/core/cache/builders/remote-data-build.service.ts b/src/app/core/cache/builders/remote-data-build.service.ts
index 11815c133b..6b67549f2d 100644
--- a/src/app/core/cache/builders/remote-data-build.service.ts
+++ b/src/app/core/cache/builders/remote-data-build.service.ts
@@ -271,7 +271,7 @@ export class RemoteDataBuildService {
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
*/
buildList(href$: string | Observable, ...linksToFollow: FollowLinkConfig[]): Observable>> {
- return this.buildFromHref>(href$, followLink('page', undefined, false, true, true, ...linksToFollow));
+ return this.buildFromHref>(href$, followLink('page', { shouldEmbed: false }, ...linksToFollow));
}
/**
diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts
index 619a7dbadc..026c87be9d 100644
--- a/src/app/core/core.module.ts
+++ b/src/app/core/core.module.ts
@@ -10,7 +10,7 @@ import {
import { EffectsModule } from '@ngrx/effects';
import { Action, StoreConfig, StoreModule } from '@ngrx/store';
-import { MyDSpaceGuard } from '../+my-dspace-page/my-dspace.guard';
+import { MyDSpaceGuard } from '../my-dspace-page/my-dspace.guard';
import { isNotEmpty } from '../shared/empty.util';
import { FormBuilderService } from '../shared/form/builder/form-builder.service';
@@ -161,6 +161,8 @@ import { ShortLivedToken } from './auth/models/short-lived-token.model';
import { UsageReport } from './statistics/models/usage-report.model';
import { RootDataService } from './data/root-data.service';
import { Root } from './data/root.model';
+import { SearchConfig } from './shared/search/search-filters/search-config.model';
+import { SequenceService } from './shared/sequence.service';
/**
* When not in production, endpoint responses can be mocked for testing purposes
@@ -281,7 +283,8 @@ const PROVIDERS = [
FilteredDiscoveryPageResponseParsingService,
{ provide: NativeWindowService, useFactory: NativeWindowFactory },
VocabularyService,
- VocabularyTreeviewService
+ VocabularyTreeviewService,
+ SequenceService,
];
/**
@@ -340,6 +343,7 @@ export const models =
Registration,
UsageReport,
Root,
+ SearchConfig
];
@NgModule({
diff --git a/src/app/core/core.reducers.ts b/src/app/core/core.reducers.ts
index 077aa3dc95..8b3ec32b46 100644
--- a/src/app/core/core.reducers.ts
+++ b/src/app/core/core.reducers.ts
@@ -11,8 +11,9 @@ import { routeReducer, RouteState } from './services/route.reducer';
import {
bitstreamFormatReducer,
BitstreamFormatRegistryState
-} from '../+admin/admin-registries/bitstream-formats/bitstream-format.reducers';
+} from '../admin/admin-registries/bitstream-formats/bitstream-format.reducers';
import { historyReducer, HistoryState } from './history/history.reducer';
+import { metaTagReducer, MetaTagState } from './metadata/meta-tag.reducer';
export interface CoreState {
'bitstreamFormats': BitstreamFormatRegistryState;
@@ -24,6 +25,7 @@ export interface CoreState {
'index': MetaIndexState;
'auth': AuthState;
'json/patch': JsonPatchOperationsState;
+ 'metaTag': MetaTagState;
'route': RouteState;
}
@@ -37,5 +39,6 @@ export const coreReducers: ActionReducerMap = {
'index': indexReducer,
'auth': authReducer,
'json/patch': jsonPatchOperationsReducer,
+ 'metaTag': metaTagReducer,
'route': routeReducer
};
diff --git a/src/app/core/data/bitstream-data.service.ts b/src/app/core/data/bitstream-data.service.ts
index 1a16abc47f..23aec80ff2 100644
--- a/src/app/core/data/bitstream-data.service.ts
+++ b/src/app/core/data/bitstream-data.service.ts
@@ -3,7 +3,7 @@ import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
import { map, switchMap, take } from 'rxjs/operators';
-import { hasValue, isNotEmpty } from '../../shared/empty.util';
+import { hasValue } from '../../shared/empty.util';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { dataService } from '../cache/builders/build-decorators';
@@ -18,7 +18,7 @@ import { Item } from '../shared/item.model';
import { BundleDataService } from './bundle-data.service';
import { DataService } from './data.service';
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
-import { PaginatedList, buildPaginatedList } from './paginated-list.model';
+import { buildPaginatedList, PaginatedList } from './paginated-list.model';
import { RemoteData } from './remote-data';
import { FindListOptions, PutRequest } from './request.models';
import { RequestService } from './request.service';
@@ -28,7 +28,7 @@ import { HttpOptions } from '../dspace-rest/dspace-rest.service';
import { sendRequest } from '../shared/operators';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { PageInfo } from '../shared/page-info.model';
-import { RequestEntryState } from './request.reducer';
+import { RequestParam } from '../cache/models/request-param.model';
/**
* A service to retrieve {@link Bitstream}s from the REST API
@@ -75,92 +75,6 @@ export class BitstreamDataService extends DataService {
return this.findAllByHref(bundle._links.bitstreams.href, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}
- /**
- * Retrieves the thumbnail for the given item
- * @returns {Observable>} the first bitstream in the THUMBNAIL bundle
- */
- // TODO should be implemented rest side. {@link Item} should get a thumbnail link
- public getThumbnailFor(item: Item): Observable> {
- return this.bundleService.findByItemAndName(item, 'THUMBNAIL').pipe(
- switchMap((bundleRD: RemoteData) => {
- if (isNotEmpty(bundleRD.payload)) {
- return this.findAllByBundle(bundleRD.payload, { elementsPerPage: 1 }).pipe(
- map((bitstreamRD: RemoteData>) => {
- if (hasValue(bitstreamRD.payload) && hasValue(bitstreamRD.payload.page)) {
- return new RemoteData(
- bitstreamRD.timeCompleted,
- bitstreamRD.msToLive,
- bitstreamRD.lastUpdated,
- bitstreamRD.state,
- bitstreamRD.errorMessage,
- bitstreamRD.payload.page[0],
- bitstreamRD.statusCode
- );
- } else {
- return bitstreamRD as any;
- }
- })
- );
- } else {
- return [bundleRD as any];
- }
- })
- );
- }
-
- /**
- * Retrieve the matching thumbnail for a {@link Bitstream}.
- *
- * The {@link Item} is technically redundant, but is available
- * in all current use cases, and having it simplifies this method
- *
- * @param item The {@link Item} the {@link Bitstream} and its thumbnail are a part of
- * @param bitstreamInOriginal The original {@link Bitstream} to find the thumbnail for
- */
- // TODO should be implemented rest side
- public getMatchingThumbnail(item: Item, bitstreamInOriginal: Bitstream): Observable> {
- return this.bundleService.findByItemAndName(item, 'THUMBNAIL').pipe(
- switchMap((bundleRD: RemoteData) => {
- if (isNotEmpty(bundleRD.payload)) {
- return this.findAllByBundle(bundleRD.payload, { elementsPerPage: 9999 }).pipe(
- map((bitstreamRD: RemoteData>) => {
- if (hasValue(bitstreamRD.payload) && hasValue(bitstreamRD.payload.page)) {
- const matchingThumbnail = bitstreamRD.payload.page.find((thumbnail: Bitstream) =>
- thumbnail.name.startsWith(bitstreamInOriginal.name)
- );
- if (hasValue(matchingThumbnail)) {
- return new RemoteData(
- bitstreamRD.timeCompleted,
- bitstreamRD.msToLive,
- bitstreamRD.lastUpdated,
- bitstreamRD.state,
- bitstreamRD.errorMessage,
- matchingThumbnail,
- bitstreamRD.statusCode
- );
- } else {
- return new RemoteData(
- bitstreamRD.timeCompleted,
- bitstreamRD.msToLive,
- bitstreamRD.lastUpdated,
- RequestEntryState.Error,
- 'No matching thumbnail found',
- undefined,
- 404
- );
- }
- } else {
- return bitstreamRD as any;
- }
- })
- );
- } else {
- return [bundleRD as any];
- }
- })
- );
- }
-
/**
* Retrieve all {@link Bitstream}s in a certain {@link Bundle}.
*
@@ -223,4 +137,50 @@ export class BitstreamDataService extends DataService {
return this.rdbService.buildFromRequestUUID(requestId);
}
+ /**
+ * Returns an observable of {@link RemoteData} of a {@link Bitstream}, based on a handle and an
+ * optional sequenceId or filename, with a list of {@link FollowLinkConfig}, to automatically
+ * resolve {@link HALLink}s of the object
+ *
+ * @param handle The handle of the bitstream we want to retrieve
+ * @param sequenceId The sequence id of the bitstream we want to retrieve
+ * @param filename The filename of the bitstream we want to retrieve
+ * @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
+ * @param linksToFollow List of {@link FollowLinkConfig} that indicate which
+ * {@link HALLink}s should be automatically resolved
+ */
+ findByItemHandle(
+ handle: string,
+ sequenceId?: string,
+ filename?: string,
+ useCachedVersionIfAvailable = true,
+ reRequestOnStale = true,
+ ...linksToFollow: FollowLinkConfig[]
+ ): Observable> {
+ const searchParams = [];
+ searchParams.push(new RequestParam('handle', handle));
+ if (hasValue(sequenceId)) {
+ searchParams.push(new RequestParam('sequenceId', sequenceId));
+ }
+ if (hasValue(filename)) {
+ searchParams.push(new RequestParam('filename', filename));
+ }
+
+ const hrefObs = this.getSearchByHref(
+ 'byItemHandle',
+ { searchParams },
+ ...linksToFollow
+ );
+
+ return this.findByHref(
+ hrefObs,
+ useCachedVersionIfAvailable,
+ reRequestOnStale,
+ ...linksToFollow
+ );
+ }
+
}
diff --git a/src/app/core/data/bitstream-format-data.service.spec.ts b/src/app/core/data/bitstream-format-data.service.spec.ts
index bb3fe2f064..c072803c83 100644
--- a/src/app/core/data/bitstream-format-data.service.spec.ts
+++ b/src/app/core/data/bitstream-format-data.service.spec.ts
@@ -15,7 +15,7 @@ import {
BitstreamFormatsRegistryDeselectAction,
BitstreamFormatsRegistryDeselectAllAction,
BitstreamFormatsRegistrySelectAction
-} from '../../+admin/admin-registries/bitstream-formats/bitstream-format.actions';
+} from '../../admin/admin-registries/bitstream-formats/bitstream-format.actions';
import { TestScheduler } from 'rxjs/testing';
import { CoreState } from '../core.reducers';
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
diff --git a/src/app/core/data/bitstream-format-data.service.ts b/src/app/core/data/bitstream-format-data.service.ts
index 424c9fbd99..0d0dc5eb63 100644
--- a/src/app/core/data/bitstream-format-data.service.ts
+++ b/src/app/core/data/bitstream-format-data.service.ts
@@ -7,8 +7,8 @@ import {
BitstreamFormatsRegistryDeselectAction,
BitstreamFormatsRegistryDeselectAllAction,
BitstreamFormatsRegistrySelectAction
-} from '../../+admin/admin-registries/bitstream-formats/bitstream-format.actions';
-import { BitstreamFormatRegistryState } from '../../+admin/admin-registries/bitstream-formats/bitstream-format.reducers';
+} from '../../admin/admin-registries/bitstream-formats/bitstream-format.actions';
+import { BitstreamFormatRegistryState } from '../../admin/admin-registries/bitstream-formats/bitstream-format.reducers';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { dataService } from '../cache/builders/build-decorators';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
diff --git a/src/app/core/data/data.service.spec.ts b/src/app/core/data/data.service.spec.ts
index 88b15754af..5bc7423824 100644
--- a/src/app/core/data/data.service.spec.ts
+++ b/src/app/core/data/data.service.spec.ts
@@ -233,7 +233,7 @@ describe('DataService', () => {
const config: FindListOptions = Object.assign(new FindListOptions(), {
elementsPerPage: 5
});
- (service as any).getFindAllHref({}, null, followLink('bundles', config, true, true, true)).subscribe((value) => {
+ (service as any).getFindAllHref({}, null, followLink('bundles', { findListOptions: config })).subscribe((value) => {
expect(value).toBe(expected);
});
});
@@ -253,7 +253,7 @@ describe('DataService', () => {
elementsPerPage: 2
});
- (service as any).getFindAllHref({}, null, followLink('bundles'), followLink('owningCollection', config, true, true, true), followLink('templateItemOf')).subscribe((value) => {
+ (service as any).getFindAllHref({}, null, followLink('bundles'), followLink('owningCollection', { findListOptions: config }), followLink('templateItemOf')).subscribe((value) => {
expect(value).toBe(expected);
});
});
@@ -261,7 +261,13 @@ describe('DataService', () => {
it('should not include linksToFollow with shouldEmbed = false', () => {
const expected = `${endpoint}?embed=templateItemOf`;
- (service as any).getFindAllHref({}, null, followLink('bundles', undefined, false), followLink('owningCollection', undefined, false), followLink('templateItemOf')).subscribe((value) => {
+ (service as any).getFindAllHref(
+ {},
+ null,
+ followLink('bundles', { shouldEmbed: false }),
+ followLink('owningCollection', { shouldEmbed: false }),
+ followLink('templateItemOf')
+ ).subscribe((value) => {
expect(value).toBe(expected);
});
});
@@ -269,7 +275,7 @@ describe('DataService', () => {
it('should include nested linksToFollow 3lvl', () => {
const expected = `${endpoint}?embed=owningCollection/itemtemplate/relationships`;
- (service as any).getFindAllHref({}, null, followLink('owningCollection', undefined, true, true, true, followLink('itemtemplate', undefined, true, true, true, followLink('relationships')))).subscribe((value) => {
+ (service as any).getFindAllHref({}, null, followLink('owningCollection', {}, followLink('itemtemplate', {}, followLink('relationships')))).subscribe((value) => {
expect(value).toBe(expected);
});
});
@@ -279,7 +285,7 @@ describe('DataService', () => {
const config: FindListOptions = Object.assign(new FindListOptions(), {
elementsPerPage: 4
});
- (service as any).getFindAllHref({}, null, followLink('owningCollection', undefined, true, true, true, followLink('itemtemplate', config, true, true, true))).subscribe((value) => {
+ (service as any).getFindAllHref({}, null, followLink('owningCollection', {}, followLink('itemtemplate', { findListOptions: config }))).subscribe((value) => {
expect(value).toBe(expected);
});
});
@@ -308,13 +314,19 @@ describe('DataService', () => {
it('should not include linksToFollow with shouldEmbed = false', () => {
const expected = `${endpointMock}/${resourceIdMock}?embed=templateItemOf`;
- const result = (service as any).getIDHref(endpointMock, resourceIdMock, followLink('bundles', undefined, false), followLink('owningCollection', undefined, false), followLink('templateItemOf'));
+ const result = (service as any).getIDHref(
+ endpointMock,
+ resourceIdMock,
+ followLink('bundles', { shouldEmbed: false }),
+ followLink('owningCollection', { shouldEmbed: false }),
+ followLink('templateItemOf')
+ );
expect(result).toEqual(expected);
});
it('should include nested linksToFollow 3lvl', () => {
const expected = `${endpointMock}/${resourceIdMock}?embed=owningCollection/itemtemplate/relationships`;
- const result = (service as any).getIDHref(endpointMock, resourceIdMock, followLink('owningCollection', undefined, true, true, true,followLink('itemtemplate', undefined, true, true, true, followLink('relationships'))));
+ const result = (service as any).getIDHref(endpointMock, resourceIdMock, followLink('owningCollection', {}, followLink('itemtemplate', {}, followLink('relationships'))));
expect(result).toEqual(expected);
});
});
diff --git a/src/app/core/data/dso-redirect-data.service.spec.ts b/src/app/core/data/dso-redirect-data.service.spec.ts
index d64f37ad78..bcd25487c2 100644
--- a/src/app/core/data/dso-redirect-data.service.spec.ts
+++ b/src/app/core/data/dso-redirect-data.service.spec.ts
@@ -119,7 +119,7 @@ describe('DsoRedirectDataService', () => {
});
it('should navigate to entities route with the corresponding entity type', () => {
remoteData.payload.type = 'item';
- remoteData.payload.metadata = {
+ remoteData.payload.metadata = {
'dspace.entity.type': [
{
language: 'en_US',
@@ -174,13 +174,29 @@ describe('DsoRedirectDataService', () => {
it('should not include linksToFollow with shouldEmbed = false', () => {
const expected = `${requestUUIDURL}&embed=templateItemOf`;
- const result = (service as any).getIDHref(pidLink, dsoUUID, followLink('bundles', undefined, false), followLink('owningCollection', undefined, false), followLink('templateItemOf'));
+ const result = (service as any).getIDHref(
+ pidLink,
+ dsoUUID,
+ followLink('bundles', { shouldEmbed: false }),
+ followLink('owningCollection', { shouldEmbed: false }),
+ followLink('templateItemOf')
+ );
expect(result).toEqual(expected);
});
it('should include nested linksToFollow 3lvl', () => {
const expected = `${requestUUIDURL}&embed=owningCollection/itemtemplate/relationships`;
- const result = (service as any).getIDHref(pidLink, dsoUUID, followLink('owningCollection', undefined, true, true, true, followLink('itemtemplate', undefined, true, true, true, followLink('relationships'))));
+ const result = (service as any).getIDHref(
+ pidLink,
+ dsoUUID,
+ followLink('owningCollection',
+ {},
+ followLink('itemtemplate',
+ {},
+ followLink('relationships')
+ )
+ )
+ );
expect(result).toEqual(expected);
});
});
diff --git a/src/app/core/data/dso-redirect-data.service.ts b/src/app/core/data/dso-redirect-data.service.ts
index 693ae0b41a..83395d4719 100644
--- a/src/app/core/data/dso-redirect-data.service.ts
+++ b/src/app/core/data/dso-redirect-data.service.ts
@@ -19,7 +19,7 @@ import { RequestService } from './request.service';
import { getFirstCompletedRemoteData } from '../shared/operators';
import { DSpaceObject } from '../shared/dspace-object.model';
import { Item } from '../shared/item.model';
-import { getItemPageRoute } from '../../+item-page/item-page-routing-paths';
+import { getItemPageRoute } from '../../item-page/item-page-routing-paths';
@Injectable()
export class DsoRedirectDataService extends DataService {
diff --git a/src/app/core/data/eperson-registration.service.spec.ts b/src/app/core/data/eperson-registration.service.spec.ts
index fe3a1958eb..768d83c024 100644
--- a/src/app/core/data/eperson-registration.service.spec.ts
+++ b/src/app/core/data/eperson-registration.service.spec.ts
@@ -8,8 +8,11 @@ import { Registration } from '../shared/registration.model';
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
import { of as observableOf } from 'rxjs/internal/observable/of';
+import { TestScheduler } from 'rxjs/testing';
describe('EpersonRegistrationService', () => {
+ let testScheduler;
+
let service: EpersonRegistrationService;
let requestService: RequestService;
@@ -29,6 +32,12 @@ describe('EpersonRegistrationService', () => {
rd = createSuccessfulRemoteDataObject(registrationWithUser);
halService = new HALEndpointServiceStub('rest-url');
+ testScheduler = new TestScheduler((actual, expected) => {
+ // asserting the two objects are equal
+ // e.g. using chai.
+ expect(actual).toEqual(expected);
+ });
+
requestService = jasmine.createSpyObj('requestService', {
generateRequestId: 'request-id',
send: {},
@@ -36,7 +45,8 @@ describe('EpersonRegistrationService', () => {
{ a: Object.assign(new RequestEntry(), { response: new RestResponse(true, 200, 'Success') }) })
});
rdbService = jasmine.createSpyObj('rdbService', {
- buildFromRequestUUID: observableOf(rd)
+ buildSingle: observableOf(rd),
+ buildFromRequestUUID: observableOf(rd),
});
service = new EpersonRegistrationService(
requestService,
@@ -86,8 +96,28 @@ describe('EpersonRegistrationService', () => {
user: registrationWithUser.user
})
}));
-
});
+
+ // tslint:disable:no-shadowed-variable
+ it('should use cached responses and /registrations/search/findByToken?', () => {
+ testScheduler.run(({ cold, expectObservable }) => {
+ rdbService.buildSingle.and.returnValue(cold('a', { a: rd }));
+
+ service.searchByToken('test-token');
+
+ expect(requestService.send).toHaveBeenCalledWith(
+ jasmine.objectContaining({
+ uuid: 'request-id', method: 'GET',
+ href: 'rest-url/registrations/search/findByToken?token=test-token',
+ }), true
+ );
+ expectObservable(rdbService.buildSingle.calls.argsFor(0)[0]).toBe('(a|)', {
+ a: 'rest-url/registrations/search/findByToken?token=test-token'
+ });
+ });
+ });
+
});
});
+/**/
diff --git a/src/app/core/data/eperson-registration.service.ts b/src/app/core/data/eperson-registration.service.ts
index fd55c031d8..adf01b0ce9 100644
--- a/src/app/core/data/eperson-registration.service.ts
+++ b/src/app/core/data/eperson-registration.service.ts
@@ -3,7 +3,7 @@ import { RequestService } from './request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { GetRequest, PostRequest } from './request.models';
import { Observable } from 'rxjs';
-import { filter, find, map, take } from 'rxjs/operators';
+import { filter, find, map, skipWhile } from 'rxjs/operators';
import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { Registration } from '../shared/registration.model';
import { getFirstCompletedRemoteData, getFirstSucceededRemoteData } from '../shared/operators';
@@ -60,9 +60,9 @@ export class EpersonRegistrationService {
const requestId = this.requestService.generateRequestId();
- const hrefObs = this.getRegistrationEndpoint();
+ const href$ = this.getRegistrationEndpoint();
- hrefObs.pipe(
+ href$.pipe(
find((href: string) => hasValue(href)),
map((href: string) => {
const request = new PostRequest(requestId, href, registration);
@@ -82,27 +82,28 @@ export class EpersonRegistrationService {
searchByToken(token: string): Observable {
const requestId = this.requestService.generateRequestId();
- const hrefObs = this.getTokenSearchEndpoint(token);
-
- hrefObs.pipe(
+ const href$ = this.getTokenSearchEndpoint(token).pipe(
find((href: string) => hasValue(href)),
- map((href: string) => {
- const request = new GetRequest(requestId, href);
- Object.assign(request, {
- getResponseParser(): GenericConstructor {
- return RegistrationResponseParsingService;
- }
- });
- this.requestService.send(request, true);
- })
- ).subscribe();
+ );
- return this.rdbService.buildFromRequestUUID(requestId).pipe(
+ href$.subscribe((href: string) => {
+ const request = new GetRequest(requestId, href);
+ Object.assign(request, {
+ getResponseParser(): GenericConstructor {
+ return RegistrationResponseParsingService;
+ }
+ });
+ this.requestService.send(request, true);
+ });
+
+ return this.rdbService.buildSingle(href$).pipe(
+ skipWhile((rd: RemoteData) => rd.isStale),
getFirstSucceededRemoteData(),
map((restResponse: RemoteData) => {
- return Object.assign(new Registration(), {email: restResponse.payload.email, token: token, user: restResponse.payload.user});
+ return Object.assign(new Registration(), {
+ email: restResponse.payload.email, token: token, user: restResponse.payload.user
+ });
}),
- take(1),
);
}
diff --git a/src/app/core/data/feature-authorization/authorization-data.service.spec.ts b/src/app/core/data/feature-authorization/authorization-data.service.spec.ts
index 23457b8409..01bd23d7c7 100644
--- a/src/app/core/data/feature-authorization/authorization-data.service.spec.ts
+++ b/src/app/core/data/feature-authorization/authorization-data.service.spec.ts
@@ -21,6 +21,10 @@ describe('AuthorizationDataService', () => {
let site: Site;
let ePerson: EPerson;
+ const requestService = jasmine.createSpyObj('requestService', {
+ setStaleByHrefSubstring: jasmine.createSpy('setStaleByHrefSubstring')
+ });
+
function init() {
site = Object.assign(new Site(), {
id: 'test-site',
@@ -39,7 +43,7 @@ describe('AuthorizationDataService', () => {
isAuthenticated: () => observableOf(true),
getAuthenticatedUserFromStore: () => observableOf(ePerson)
} as AuthService;
- service = new AuthorizationDataService(undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, authService, siteService);
+ service = new AuthorizationDataService(requestService, undefined, undefined, undefined, undefined, undefined, undefined, undefined, authService, siteService);
}
beforeEach(() => {
@@ -47,6 +51,11 @@ describe('AuthorizationDataService', () => {
spyOn(service, 'searchBy').and.returnValue(observableOf(undefined));
});
+ it('should call setStaleByHrefSubstring method', () => {
+ service.invalidateAuthorizationsRequestCache();
+ expect((service as any).requestService.setStaleByHrefSubstring).toHaveBeenCalledWith((service as any).linkPath);
+ });
+
describe('searchByObject', () => {
const objectUrl = 'fake-object-url';
const ePersonUuid = 'fake-eperson-uuid';
diff --git a/src/app/core/data/feature-authorization/authorization-data.service.ts b/src/app/core/data/feature-authorization/authorization-data.service.ts
index 170e82f5f8..b9812cdbb3 100644
--- a/src/app/core/data/feature-authorization/authorization-data.service.ts
+++ b/src/app/core/data/feature-authorization/authorization-data.service.ts
@@ -51,6 +51,13 @@ export class AuthorizationDataService extends DataService {
super();
}
+ /**
+ * Set all authorization requests to stale
+ */
+ invalidateAuthorizationsRequestCache() {
+ this.requestService.setStaleByHrefSubstring(this.linkPath);
+ }
+
/**
* Checks if an {@link EPerson} (or anonymous) has access to a specific object within a {@link Feature}
* @param objectUrl URL to the object to search {@link Authorization}s for.
diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/collection-administrator.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/collection-administrator.guard.ts
index bc39397ed9..b41a322cb6 100644
--- a/src/app/core/data/feature-authorization/feature-authorization-guard/collection-administrator.guard.ts
+++ b/src/app/core/data/feature-authorization/feature-authorization-guard/collection-administrator.guard.ts
@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
-import { FeatureAuthorizationGuard } from './feature-authorization.guard';
+import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard';
import { AuthorizationDataService } from '../authorization-data.service';
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
import { AuthService } from '../../../auth/auth.service';
@@ -13,7 +13,7 @@ import { FeatureID } from '../feature-id';
@Injectable({
providedIn: 'root'
})
-export class CollectionAdministratorGuard extends FeatureAuthorizationGuard {
+export class CollectionAdministratorGuard extends SingleFeatureAuthorizationGuard {
constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) {
super(authorizationService, router, authService);
}
diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/community-administrator.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/community-administrator.guard.ts
index afb1fea63d..2ab77a00cc 100644
--- a/src/app/core/data/feature-authorization/feature-authorization-guard/community-administrator.guard.ts
+++ b/src/app/core/data/feature-authorization/feature-authorization-guard/community-administrator.guard.ts
@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
-import { FeatureAuthorizationGuard } from './feature-authorization.guard';
+import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard';
import { AuthorizationDataService } from '../authorization-data.service';
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
import { AuthService } from '../../../auth/auth.service';
@@ -13,7 +13,7 @@ import { FeatureID } from '../feature-id';
@Injectable({
providedIn: 'root'
})
-export class CommunityAdministratorGuard extends FeatureAuthorizationGuard {
+export class CommunityAdministratorGuard extends SingleFeatureAuthorizationGuard {
constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) {
super(authorizationService, router, authService);
}
diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard.spec.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.spec.ts
similarity index 85%
rename from src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard.spec.ts
rename to src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.spec.ts
index f98e3f1837..6c1f330c69 100644
--- a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard.spec.ts
+++ b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.spec.ts
@@ -4,14 +4,14 @@ import { RemoteData } from '../../remote-data';
import { Observable, of as observableOf } from 'rxjs';
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
import { DSpaceObject } from '../../../shared/dspace-object.model';
-import { DsoPageFeatureGuard } from './dso-page-feature.guard';
+import { DsoPageSingleFeatureGuard } from './dso-page-single-feature.guard';
import { FeatureID } from '../feature-id';
import { AuthService } from '../../../auth/auth.service';
/**
- * Test implementation of abstract class DsoPageAdministratorGuard
+ * Test implementation of abstract class DsoPageSingleFeatureGuard
*/
-class DsoPageFeatureGuardImpl extends DsoPageFeatureGuard {
+class DsoPageSingleFeatureGuardImpl extends DsoPageSingleFeatureGuard {
constructor(protected resolver: Resolve>,
protected authorizationService: AuthorizationDataService,
protected router: Router,
@@ -25,8 +25,8 @@ class DsoPageFeatureGuardImpl extends DsoPageFeatureGuard {
}
}
-describe('DsoPageAdministratorGuard', () => {
- let guard: DsoPageFeatureGuard;
+describe('DsoPageSingleFeatureGuard', () => {
+ let guard: DsoPageSingleFeatureGuard;
let authorizationService: AuthorizationDataService;
let router: Router;
let authService: AuthService;
@@ -62,7 +62,7 @@ describe('DsoPageAdministratorGuard', () => {
},
parent: parentRoute
};
- guard = new DsoPageFeatureGuardImpl(resolver, authorizationService, router, authService, undefined);
+ guard = new DsoPageSingleFeatureGuardImpl(resolver, authorizationService, router, authService, undefined);
}
beforeEach(() => {
diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.ts
new file mode 100644
index 0000000000..3fc90f9069
--- /dev/null
+++ b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.ts
@@ -0,0 +1,27 @@
+import { DSpaceObject } from '../../../shared/dspace-object.model';
+import { DsoPageSomeFeatureGuard } from './dso-page-some-feature.guard';
+import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
+import { FeatureID } from '../feature-id';
+import { map } from 'rxjs/operators';
+import { Observable } from 'rxjs';
+
+/**
+ * Abstract Guard for preventing unauthorized access to {@link DSpaceObject} pages that require rights for a specific feature
+ * This guard utilizes a resolver to retrieve the relevant object to check authorizations for
+ */
+export abstract class DsoPageSingleFeatureGuard extends DsoPageSomeFeatureGuard {
+ /**
+ * The features to check authorization for
+ */
+ getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable {
+ return this.getFeatureID(route, state).pipe(
+ map((featureID) => [featureID]),
+ );
+ }
+
+ /**
+ * The type of feature to check authorization for
+ * Override this method to define a feature
+ */
+ abstract getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable;
+}
diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.spec.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.spec.ts
new file mode 100644
index 0000000000..071b1b0731
--- /dev/null
+++ b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.spec.ts
@@ -0,0 +1,87 @@
+import { AuthorizationDataService } from '../authorization-data.service';
+import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router';
+import { RemoteData } from '../../remote-data';
+import { Observable, of as observableOf } from 'rxjs';
+import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
+import { DSpaceObject } from '../../../shared/dspace-object.model';
+import { FeatureID } from '../feature-id';
+import { AuthService } from '../../../auth/auth.service';
+import { DsoPageSomeFeatureGuard } from './dso-page-some-feature.guard';
+
+/**
+ * Test implementation of abstract class DsoPageSomeFeatureGuard
+ */
+class DsoPageSomeFeatureGuardImpl extends DsoPageSomeFeatureGuard {
+ constructor(protected resolver: Resolve>,
+ protected authorizationService: AuthorizationDataService,
+ protected router: Router,
+ protected authService: AuthService,
+ protected featureIDs: FeatureID[]) {
+ super(resolver, authorizationService, router, authService);
+ }
+
+ getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable {
+ return observableOf(this.featureIDs);
+ }
+}
+
+describe('DsoPageSomeFeatureGuard', () => {
+ let guard: DsoPageSomeFeatureGuard;
+ let authorizationService: AuthorizationDataService;
+ let router: Router;
+ let authService: AuthService;
+ let resolver: Resolve>;
+ let object: DSpaceObject;
+ let route;
+ let parentRoute;
+
+ function init() {
+ object = {
+ self: 'test-selflink'
+ } as DSpaceObject;
+
+ authorizationService = jasmine.createSpyObj('authorizationService', {
+ isAuthorized: observableOf(true)
+ });
+ router = jasmine.createSpyObj('router', {
+ parseUrl: {}
+ });
+ resolver = jasmine.createSpyObj('resolver', {
+ resolve: createSuccessfulRemoteDataObject$(object)
+ });
+ authService = jasmine.createSpyObj('authService', {
+ isAuthenticated: observableOf(true)
+ });
+ parentRoute = {
+ params: {
+ id: '3e1a5327-dabb-41ff-af93-e6cab9d032f0'
+ }
+ };
+ route = {
+ params: {
+ },
+ parent: parentRoute
+ };
+ guard = new DsoPageSomeFeatureGuardImpl(resolver, authorizationService, router, authService, []);
+ }
+
+ beforeEach(() => {
+ init();
+ });
+
+ describe('getObjectUrl', () => {
+ it('should return the resolved object\'s selflink', (done) => {
+ guard.getObjectUrl(route, undefined).subscribe((selflink) => {
+ expect(selflink).toEqual(object.self);
+ done();
+ });
+ });
+ });
+
+ describe('getRouteWithDSOId', () => {
+ it('should return the route that has the UUID of the DSO', () => {
+ const foundRoute = (guard as any).getRouteWithDSOId(route);
+ expect(foundRoute).toBe(parentRoute);
+ });
+ });
+});
diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.ts
similarity index 87%
rename from src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard.ts
rename to src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.ts
index c50dd7f95d..8683709345 100644
--- a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard.ts
+++ b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.ts
@@ -5,15 +5,15 @@ import { Observable } from 'rxjs';
import { getAllSucceededRemoteDataPayload } from '../../../shared/operators';
import { map } from 'rxjs/operators';
import { DSpaceObject } from '../../../shared/dspace-object.model';
-import { FeatureAuthorizationGuard } from './feature-authorization.guard';
import { AuthService } from '../../../auth/auth.service';
import { hasNoValue, hasValue } from '../../../../shared/empty.util';
+import { SomeFeatureAuthorizationGuard } from './some-feature-authorization.guard';
/**
- * Abstract Guard for preventing unauthorized access to {@link DSpaceObject} pages that require rights for a specific feature
+ * Abstract Guard for preventing unauthorized access to {@link DSpaceObject} pages that require rights for any specific feature in a list
* This guard utilizes a resolver to retrieve the relevant object to check authorizations for
*/
-export abstract class DsoPageFeatureGuard extends FeatureAuthorizationGuard {
+export abstract class DsoPageSomeFeatureGuard extends SomeFeatureAuthorizationGuard {
constructor(protected resolver: Resolve>,
protected authorizationService: AuthorizationDataService,
protected router: Router,
diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/group-administrator.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/group-administrator.guard.ts
index 3fee767fdc..5afd572326 100644
--- a/src/app/core/data/feature-authorization/feature-authorization-guard/group-administrator.guard.ts
+++ b/src/app/core/data/feature-authorization/feature-authorization-guard/group-administrator.guard.ts
@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
-import { FeatureAuthorizationGuard } from './feature-authorization.guard';
+import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard';
import { AuthorizationDataService } from '../authorization-data.service';
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
import { AuthService } from '../../../auth/auth.service';
@@ -13,7 +13,7 @@ import { FeatureID } from '../feature-id';
@Injectable({
providedIn: 'root'
})
-export class GroupAdministratorGuard extends FeatureAuthorizationGuard {
+export class GroupAdministratorGuard extends SingleFeatureAuthorizationGuard {
constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) {
super(authorizationService, router, authService);
}
diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/feature-authorization.guard.spec.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.spec.ts
similarity index 82%
rename from src/app/core/data/feature-authorization/feature-authorization-guard/feature-authorization.guard.spec.ts
rename to src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.spec.ts
index 2c6f4b0717..635aa3530b 100644
--- a/src/app/core/data/feature-authorization/feature-authorization-guard/feature-authorization.guard.spec.ts
+++ b/src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.spec.ts
@@ -1,4 +1,4 @@
-import { FeatureAuthorizationGuard } from './feature-authorization.guard';
+import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard';
import { AuthorizationDataService } from '../authorization-data.service';
import { FeatureID } from '../feature-id';
import { Observable, of as observableOf } from 'rxjs';
@@ -6,10 +6,10 @@ import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/ro
import { AuthService } from '../../../auth/auth.service';
/**
- * Test implementation of abstract class FeatureAuthorizationGuard
+ * Test implementation of abstract class SingleFeatureAuthorizationGuard
* Provide the return values of the overwritten getters as constructor arguments
*/
-class FeatureAuthorizationGuardImpl extends FeatureAuthorizationGuard {
+class SingleFeatureAuthorizationGuardImpl extends SingleFeatureAuthorizationGuard {
constructor(protected authorizationService: AuthorizationDataService,
protected router: Router,
protected authService: AuthService,
@@ -32,8 +32,8 @@ class FeatureAuthorizationGuardImpl extends FeatureAuthorizationGuard {
}
}
-describe('FeatureAuthorizationGuard', () => {
- let guard: FeatureAuthorizationGuard;
+describe('SingleFeatureAuthorizationGuard', () => {
+ let guard: SingleFeatureAuthorizationGuard;
let authorizationService: AuthorizationDataService;
let router: Router;
let authService: AuthService;
@@ -56,7 +56,7 @@ describe('FeatureAuthorizationGuard', () => {
authService = jasmine.createSpyObj('authService', {
isAuthenticated: observableOf(true)
});
- guard = new FeatureAuthorizationGuardImpl(authorizationService, router, authService, featureId, objectUrl, ePersonUuid);
+ guard = new SingleFeatureAuthorizationGuardImpl(authorizationService, router, authService, featureId, objectUrl, ePersonUuid);
}
beforeEach(() => {
diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.ts
new file mode 100644
index 0000000000..cb71d2f418
--- /dev/null
+++ b/src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.ts
@@ -0,0 +1,27 @@
+import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
+import { FeatureID } from '../feature-id';
+import { Observable } from 'rxjs';
+import { map} from 'rxjs/operators';
+import { SomeFeatureAuthorizationGuard } from './some-feature-authorization.guard';
+
+/**
+ * Abstract Guard for preventing unauthorized activating and loading of routes when a user
+ * doesn't have authorized rights on a specific feature and/or object.
+ * Override the desired getters in the parent class for checking specific authorization on a feature and/or object.
+ */
+export abstract class SingleFeatureAuthorizationGuard extends SomeFeatureAuthorizationGuard {
+ /**
+ * The features to check authorization for
+ */
+ getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable {
+ return this.getFeatureID(route, state).pipe(
+ map((featureID) => [featureID]),
+ );
+ }
+
+ /**
+ * The type of feature to check authorization for
+ * Override this method to define a feature
+ */
+ abstract getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable;
+}
diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/site-administrator.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/site-administrator.guard.ts
index bb678ebf33..cc6f50c161 100644
--- a/src/app/core/data/feature-authorization/feature-authorization-guard/site-administrator.guard.ts
+++ b/src/app/core/data/feature-authorization/feature-authorization-guard/site-administrator.guard.ts
@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
-import { FeatureAuthorizationGuard } from './feature-authorization.guard';
+import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard';
import { FeatureID } from '../feature-id';
import { AuthorizationDataService } from '../authorization-data.service';
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
@@ -13,7 +13,7 @@ import { AuthService } from '../../../auth/auth.service';
@Injectable({
providedIn: 'root'
})
-export class SiteAdministratorGuard extends FeatureAuthorizationGuard {
+export class SiteAdministratorGuard extends SingleFeatureAuthorizationGuard {
constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) {
super(authorizationService, router, authService);
}
diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/site-register.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/site-register.guard.ts
index 709d9ff266..bdbb8250e2 100644
--- a/src/app/core/data/feature-authorization/feature-authorization-guard/site-register.guard.ts
+++ b/src/app/core/data/feature-authorization/feature-authorization-guard/site-register.guard.ts
@@ -1,4 +1,4 @@
-import { FeatureAuthorizationGuard } from './feature-authorization.guard';
+import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard';
import { Injectable } from '@angular/core';
import { AuthorizationDataService } from '../authorization-data.service';
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
@@ -13,7 +13,7 @@ import { AuthService } from '../../../auth/auth.service';
@Injectable({
providedIn: 'root'
})
-export class SiteRegisterGuard extends FeatureAuthorizationGuard {
+export class SiteRegisterGuard extends SingleFeatureAuthorizationGuard {
constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) {
super(authorizationService, router, authService);
}
diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.spec.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.spec.ts
new file mode 100644
index 0000000000..90153d2d14
--- /dev/null
+++ b/src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.spec.ts
@@ -0,0 +1,110 @@
+import { AuthorizationDataService } from '../authorization-data.service';
+import { FeatureID } from '../feature-id';
+import { Observable, of as observableOf } from 'rxjs';
+import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
+import { AuthService } from '../../../auth/auth.service';
+import { SomeFeatureAuthorizationGuard } from './some-feature-authorization.guard';
+
+/**
+ * Test implementation of abstract class SomeFeatureAuthorizationGuard
+ * Provide the return values of the overwritten getters as constructor arguments
+ */
+class SomeFeatureAuthorizationGuardImpl extends SomeFeatureAuthorizationGuard {
+ constructor(protected authorizationService: AuthorizationDataService,
+ protected router: Router,
+ protected authService: AuthService,
+ protected featureIds: FeatureID[],
+ protected objectUrl: string,
+ protected ePersonUuid: string) {
+ super(authorizationService, router, authService);
+ }
+
+ getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable {
+ return observableOf(this.featureIds);
+ }
+
+ getObjectUrl(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable {
+ return observableOf(this.objectUrl);
+ }
+
+ getEPersonUuid(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable {
+ return observableOf(this.ePersonUuid);
+ }
+}
+
+describe('SomeFeatureAuthorizationGuard', () => {
+ let guard: SomeFeatureAuthorizationGuard;
+ let authorizationService: AuthorizationDataService;
+ let router: Router;
+ let authService: AuthService;
+
+ let featureIds: FeatureID[];
+ let authorizedFeatureIds: FeatureID[];
+ let objectUrl: string;
+ let ePersonUuid: string;
+
+ function init() {
+ featureIds = [FeatureID.LoginOnBehalfOf, FeatureID.CanDelete];
+ authorizedFeatureIds = [];
+ objectUrl = 'fake-object-url';
+ ePersonUuid = 'fake-eperson-uuid';
+
+ authorizationService = Object.assign({
+ isAuthorized(featureId?: FeatureID): Observable {
+ return observableOf(authorizedFeatureIds.indexOf(featureId) > -1);
+ }
+ });
+ router = jasmine.createSpyObj('router', {
+ parseUrl: {}
+ });
+ authService = jasmine.createSpyObj('authService', {
+ isAuthenticated: observableOf(true)
+ });
+ guard = new SomeFeatureAuthorizationGuardImpl(authorizationService, router, authService, featureIds, objectUrl, ePersonUuid);
+ }
+
+ beforeEach(() => {
+ init();
+ });
+
+ describe('canActivate', () => {
+ describe('when the user isn\'t authorized', () => {
+ beforeEach(() => {
+ authorizedFeatureIds = [];
+ });
+
+ it('should not return true', (done) => {
+ guard.canActivate(undefined, { url: 'current-url' } as any).subscribe((result) => {
+ expect(result).not.toEqual(true);
+ done();
+ });
+ });
+ });
+
+ describe('when the user is authorized for at least one of the guard\'s features', () => {
+ beforeEach(() => {
+ authorizedFeatureIds = [featureIds[0]];
+ });
+
+ it('should return true', (done) => {
+ guard.canActivate(undefined, { url: 'current-url' } as any).subscribe((result) => {
+ expect(result).toEqual(true);
+ done();
+ });
+ });
+ });
+
+ describe('when the user is authorized for all of the guard\'s features', () => {
+ beforeEach(() => {
+ authorizedFeatureIds = featureIds;
+ });
+
+ it('should return true', (done) => {
+ guard.canActivate(undefined, { url: 'current-url' } as any).subscribe((result) => {
+ expect(result).toEqual(true);
+ done();
+ });
+ });
+ });
+ });
+});
diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/feature-authorization.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.ts
similarity index 64%
rename from src/app/core/data/feature-authorization/feature-authorization-guard/feature-authorization.guard.ts
rename to src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.ts
index 86b75b637e..3a6cf745c9 100644
--- a/src/app/core/data/feature-authorization/feature-authorization-guard/feature-authorization.guard.ts
+++ b/src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.ts
@@ -2,16 +2,16 @@ import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTr
import { AuthorizationDataService } from '../authorization-data.service';
import { FeatureID } from '../feature-id';
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
-import { returnForbiddenUrlTreeOrLoginOnFalse } from '../../../shared/operators';
+import { returnForbiddenUrlTreeOrLoginOnAllFalse } from '../../../shared/operators';
import { switchMap } from 'rxjs/operators';
import { AuthService } from '../../../auth/auth.service';
/**
* Abstract Guard for preventing unauthorized activating and loading of routes when a user
- * doesn't have authorized rights on a specific feature and/or object.
- * Override the desired getters in the parent class for checking specific authorization on a feature and/or object.
+ * doesn't have authorized rights on any of the specified features and/or object.
+ * Override the desired getters in the parent class for checking specific authorization on a list of features and/or object.
*/
-export abstract class FeatureAuthorizationGuard implements CanActivate {
+export abstract class SomeFeatureAuthorizationGuard implements CanActivate {
constructor(protected authorizationService: AuthorizationDataService,
protected router: Router,
protected authService: AuthService) {
@@ -22,17 +22,19 @@ export abstract class FeatureAuthorizationGuard implements CanActivate {
* Redirect the user to the unauthorized page when he/she's not authorized for the given feature
*/
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable {
- return observableCombineLatest(this.getFeatureID(route, state), this.getObjectUrl(route, state), this.getEPersonUuid(route, state)).pipe(
- switchMap(([featureID, objectUrl, ePersonUuid]) => this.authorizationService.isAuthorized(featureID, objectUrl, ePersonUuid)),
- returnForbiddenUrlTreeOrLoginOnFalse(this.router, this.authService, state.url)
+ return observableCombineLatest(this.getFeatureIDs(route, state), this.getObjectUrl(route, state), this.getEPersonUuid(route, state)).pipe(
+ switchMap(([featureIDs, objectUrl, ePersonUuid]) =>
+ observableCombineLatest(...featureIDs.map((featureID) => this.authorizationService.isAuthorized(featureID, objectUrl, ePersonUuid)))
+ ),
+ returnForbiddenUrlTreeOrLoginOnAllFalse(this.router, this.authService, state.url)
);
}
/**
- * The type of feature to check authorization for
- * Override this method to define a feature
+ * The features to check authorization for
+ * Override this method to define a list of features
*/
- abstract getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable;
+ abstract getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable;
/**
* The URL of the object to check if the user has authorized rights for
diff --git a/src/app/core/data/feature-authorization/feature-id.ts b/src/app/core/data/feature-authorization/feature-id.ts
index dbdd794665..ac045b93b0 100644
--- a/src/app/core/data/feature-authorization/feature-id.ts
+++ b/src/app/core/data/feature-authorization/feature-id.ts
@@ -10,7 +10,15 @@ export enum FeatureID {
ReinstateItem = 'reinstateItem',
EPersonRegistration = 'epersonRegistration',
CanManageGroups = 'canManageGroups',
+ CanManageGroup = 'canManageGroup',
IsCollectionAdmin = 'isCollectionAdmin',
IsCommunityAdmin = 'isCommunityAdmin',
CanDownload = 'canDownload',
+ CanManageVersions = 'canManageVersions',
+ CanManageBitstreamBundles = 'canManageBitstreamBundles',
+ CanManageRelationships = 'canManageRelationships',
+ CanManageMappings = 'canManageMappings',
+ CanManagePolicies = 'canManagePolicies',
+ CanMakePrivate = 'canMakePrivate',
+ CanMove = 'canMove',
}
diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts
index e56f9f2b0c..7a0116fe86 100644
--- a/src/app/core/data/item-data.service.ts
+++ b/src/app/core/data/item-data.service.ts
@@ -23,14 +23,7 @@ import { DataService } from './data.service';
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
import { PaginatedList } from './paginated-list.model';
import { RemoteData } from './remote-data';
-import {
- DeleteRequest,
- FindListOptions,
- GetRequest,
- PostRequest,
- PutRequest,
- RestRequest
-} from './request.models';
+import { DeleteRequest, FindListOptions, GetRequest, PostRequest, PutRequest, RestRequest } from './request.models';
import { RequestService } from './request.service';
import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model';
import { Bundle } from '../shared/bundle.model';
@@ -38,6 +31,9 @@ import { MetadataMap } from '../shared/metadata.models';
import { BundleDataService } from './bundle-data.service';
import { Operation } from 'fast-json-patch';
import { NoContent } from '../shared/NoContent.model';
+import { GenericConstructor } from '../shared/generic-constructor';
+import { ResponseParsingService } from './parsing.service';
+import { StatusCodeOnlyResponseParsingService } from './status-code-only-response-parsing.service';
@Injectable()
@dataService(ITEM)
@@ -229,7 +225,7 @@ export class ItemDataService extends DataService- {
* @param itemId
* @param collection
*/
- public moveToCollection(itemId: string, collection: Collection): Observable
> {
+ public moveToCollection(itemId: string, collection: Collection): Observable> {
const options: HttpOptions = Object.create({});
let headers = new HttpHeaders();
headers = headers.append('Content-Type', 'text/uri-list');
@@ -242,9 +238,17 @@ export class ItemDataService extends DataService- {
find((href: string) => hasValue(href)),
map((href: string) => {
const request = new PutRequest(requestId, href, collection._links.self.href, options);
- this.requestService.send(request);
+ Object.assign(request, {
+ // TODO: for now, the move Item endpoint returns a malformed collection -- only look at the status code
+ getResponseParser(): GenericConstructor
{
+ return StatusCodeOnlyResponseParsingService;
+ }
+ });
+ return request;
})
- ).subscribe();
+ ).subscribe((request) => {
+ this.requestService.send(request);
+ });
return this.rdbService.buildFromRequestUUID(requestId);
}
diff --git a/src/app/core/data/relationship.service.spec.ts b/src/app/core/data/relationship.service.spec.ts
index 0a8ec48464..0f7dd319c3 100644
--- a/src/app/core/data/relationship.service.spec.ts
+++ b/src/app/core/data/relationship.service.spec.ts
@@ -1,5 +1,4 @@
import { of as observableOf } from 'rxjs';
-import * as ItemRelationshipsUtils from '../../+item-page/simple/item-types/shared/item-relationships-utils';
import { followLink } from '../../shared/utils/follow-link-config.model';
import { ObjectCacheService } from '../cache/object-cache.service';
import { RelationshipType } from '../shared/item-relationships/relationship-type.model';
@@ -15,9 +14,9 @@ import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-servic
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { getMockRemoteDataBuildServiceHrefMap } from '../../shared/mocks/remote-data-build.service.mock';
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
-import { createPaginatedList, spyOnOperator } from '../../shared/testing/utils.test';
+import { createPaginatedList } from '../../shared/testing/utils.test';
-xdescribe('RelationshipService', () => {
+describe('RelationshipService', () => {
let service: RelationshipService;
let requestService: RequestService;
@@ -132,7 +131,8 @@ xdescribe('RelationshipService', () => {
null,
null,
null,
- null
+ null,
+ jasmine.createSpy('paginatedRelationsToItems').and.returnValue((v) => v),
);
}
@@ -195,8 +195,6 @@ xdescribe('RelationshipService', () => {
const rd$ = createSuccessfulRemoteDataObject$(relationsList);
spyOn(service, 'getItemRelationshipsByLabel').and.returnValue(rd$);
-
- spyOnOperator(ItemRelationshipsUtils, 'paginatedRelationsToItems').and.returnValue((v) => v);
});
it('should call getItemRelationshipsByLabel with the correct params', (done) => {
@@ -225,7 +223,7 @@ xdescribe('RelationshipService', () => {
mockLabel,
mockOptions
).subscribe((result) => {
- expect(ItemRelationshipsUtils.paginatedRelationsToItems).toHaveBeenCalledWith(mockItem.uuid);
+ expect((service as any).paginatedRelationsToItems).toHaveBeenCalledWith(mockItem.uuid);
done();
});
});
diff --git a/src/app/core/data/relationship.service.ts b/src/app/core/data/relationship.service.ts
index 95512be678..727cd105e6 100644
--- a/src/app/core/data/relationship.service.ts
+++ b/src/app/core/data/relationship.service.ts
@@ -1,13 +1,12 @@
import { HttpClient, HttpHeaders } from '@angular/common/http';
-import { Injectable } from '@angular/core';
+import { Inject, Injectable } from '@angular/core';
import { MemoizedSelector, select, Store } from '@ngrx/store';
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
import { distinctUntilChanged, filter, map, mergeMap, startWith, switchMap, take, tap } from 'rxjs/operators';
import {
- compareArraysUsingIds,
- paginatedRelationsToItems,
+ compareArraysUsingIds, PAGINATED_RELATIONS_TO_ITEMS_OPERATOR,
relationsToItems
-} from '../../+item-page/simple/item-types/shared/item-relationships-utils';
+} from '../../item-page/simple/item-types/shared/item-relationships-utils';
import { AppState, keySelector } from '../../app.reducer';
import { hasValue, hasValueOperator, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
import { ReorderableRelationship } from '../../shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component';
@@ -87,7 +86,8 @@ export class RelationshipService extends DataService {
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected comparator: DefaultChangeAnalyzer,
- protected appStore: Store) {
+ protected appStore: Store,
+ @Inject(PAGINATED_RELATIONS_TO_ITEMS_OPERATOR) private paginatedRelationsToItems: (thisId: string) => (source: Observable>>) => Observable>>) {
super();
}
@@ -254,7 +254,7 @@ export class RelationshipService extends DataService {
* @param options
*/
getRelatedItemsByLabel(item: Item, label: string, options?: FindListOptions): Observable>> {
- return this.getItemRelationshipsByLabel(item, label, options, true, true, followLink('leftItem'), followLink('rightItem'), followLink('relationshipType')).pipe(paginatedRelationsToItems(item.uuid));
+ return this.getItemRelationshipsByLabel(item, label, options, true, true, followLink('leftItem'), followLink('rightItem'), followLink('relationshipType')).pipe(this.paginatedRelationsToItems(item.uuid));
}
/**
diff --git a/src/app/core/data/request.service.spec.ts b/src/app/core/data/request.service.spec.ts
index e6dde7a032..7a07f6fe10 100644
--- a/src/app/core/data/request.service.spec.ts
+++ b/src/app/core/data/request.service.spec.ts
@@ -579,4 +579,19 @@ describe('RequestService', () => {
});
});
});
+
+ describe('uriEncodeBody', () => {
+ it('should properly encode the body', () => {
+ const body = {
+ 'property1': 'multiple\nlines\nto\nsend',
+ 'property2': 'sp&ci@l characters',
+ 'sp&ci@l-chars in prop': 'test123',
+ };
+ const queryParams = service.uriEncodeBody(body);
+ expect(queryParams).toEqual(
+ 'property1=multiple%0Alines%0Ato%0Asend&property2=sp%26ci%40l%20characters&sp%26ci%40l-chars%20in%20prop=test123'
+ );
+ });
+ });
+
});
diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts
index 4a85df0d34..14499b8214 100644
--- a/src/app/core/data/request.service.ts
+++ b/src/app/core/data/request.service.ts
@@ -265,11 +265,13 @@ export class RequestService {
if (isNotEmpty(body) && typeof body === 'object') {
Object.keys(body)
.forEach((param) => {
- const paramValue = `${param}=${body[param]}`;
+ const encodedParam = encodeURIComponent(param);
+ const encodedBody = encodeURIComponent(body[param]);
+ const paramValue = `${encodedParam}=${encodedBody}`;
queryParams = isEmpty(queryParams) ? queryParams.concat(paramValue) : queryParams.concat('&', paramValue);
});
}
- return encodeURI(queryParams);
+ return queryParams;
}
/**
diff --git a/src/app/core/dspace-rest/dspace-rest.service.ts b/src/app/core/dspace-rest/dspace-rest.service.ts
index d949fff140..ea4e8c2831 100644
--- a/src/app/core/dspace-rest/dspace-rest.service.ts
+++ b/src/app/core/dspace-rest/dspace-rest.service.ts
@@ -112,11 +112,15 @@ export class DspaceRestService {
statusText: res.statusText
})),
catchError((err) => {
- return observableThrowError({
- statusCode: err.status,
- statusText: err.statusText,
- message: (hasValue(err.error) && isNotEmpty(err.error.message)) ? err.error.message : err.message
- });
+ if (hasValue(err.status)) {
+ return observableThrowError({
+ statusCode: err.status,
+ statusText: err.statusText,
+ message: (hasValue(err.error) && isNotEmpty(err.error.message)) ? err.error.message : err.message
+ });
+ } else {
+ return observableThrowError(err);
+ }
}));
}
diff --git a/src/app/core/eperson/eperson-data.service.spec.ts b/src/app/core/eperson/eperson-data.service.spec.ts
index cd7b664379..ac08bda589 100644
--- a/src/app/core/eperson/eperson-data.service.spec.ts
+++ b/src/app/core/eperson/eperson-data.service.spec.ts
@@ -56,11 +56,11 @@ describe('EPersonDataService', () => {
}
function init() {
- restEndpointURL = 'https://dspace.4science.it/dspace-spring-rest/api/eperson';
+ restEndpointURL = 'https://rest.api/dspace-spring-rest/api/eperson';
epersonsEndpoint = `${restEndpointURL}/epersons`;
epeople = [EPersonMock, EPersonMock2];
epeople$ = createSuccessfulRemoteDataObject$(createPaginatedList([epeople]));
- rdbService = getMockRemoteDataBuildServiceHrefMap(undefined, { 'https://dspace.4science.it/dspace-spring-rest/api/eperson/epersons': epeople$ });
+ rdbService = getMockRemoteDataBuildServiceHrefMap(undefined, { 'https://rest.api/dspace-spring-rest/api/eperson/epersons': epeople$ });
halService = new HALEndpointServiceStub(restEndpointURL);
TestBed.configureTestingModule({
diff --git a/src/app/core/eperson/models/group-dto.model.ts b/src/app/core/eperson/models/group-dto.model.ts
index 47a70cf326..ef65c498fd 100644
--- a/src/app/core/eperson/models/group-dto.model.ts
+++ b/src/app/core/eperson/models/group-dto.model.ts
@@ -17,6 +17,11 @@ export class GroupDtoModel {
*/
public ableToDelete: boolean;
+ /**
+ * Whether or not the current user is able to edit the linked group
+ */
+ public ableToEdit: boolean;
+
/**
* List of subgroups of this group
*/
diff --git a/src/app/core/json-patch/builder/json-patch-operations-builder.ts b/src/app/core/json-patch/builder/json-patch-operations-builder.ts
index ced3750834..d3896c4a6c 100644
--- a/src/app/core/json-patch/builder/json-patch-operations-builder.ts
+++ b/src/app/core/json-patch/builder/json-patch-operations-builder.ts
@@ -9,7 +9,7 @@ import {
import { JsonPatchOperationPathObject } from './json-patch-operation-path-combiner';
import { Injectable } from '@angular/core';
import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util';
-import { dateToISOFormat } from '../../../shared/date.util';
+import { dateToISOFormat, dateToString, isNgbDateStruct } from '../../../shared/date.util';
import { VocabularyEntry } from '../../submission/vocabularies/models/vocabulary-entry.model';
import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model';
import { FormFieldLanguageValueObject } from '../../../shared/form/builder/models/form-field-language-value.model';
@@ -136,6 +136,8 @@ export class JsonPatchOperationsBuilder {
operationValue = new FormFieldMetadataValueObject(value.value, value.language);
} else if (value.hasOwnProperty('authority')) {
operationValue = new FormFieldMetadataValueObject(value.value, value.language, value.authority);
+ } else if (isNgbDateStruct(value)) {
+ operationValue = new FormFieldMetadataValueObject(dateToString(value));
} else if (value.hasOwnProperty('value')) {
operationValue = new FormFieldMetadataValueObject(value.value);
} else {
diff --git a/src/app/core/json-patch/json-patch-operations.actions.ts b/src/app/core/json-patch/json-patch-operations.actions.ts
index 4c26703472..91fc6cb0da 100644
--- a/src/app/core/json-patch/json-patch-operations.actions.ts
+++ b/src/app/core/json-patch/json-patch-operations.actions.ts
@@ -20,6 +20,7 @@ export const JsonPatchOperationsActionTypes = {
ROLLBACK_JSON_PATCH_OPERATIONS: type('dspace/core/patch/ROLLBACK_JSON_PATCH_OPERATIONS'),
FLUSH_JSON_PATCH_OPERATIONS: type('dspace/core/patch/FLUSH_JSON_PATCH_OPERATIONS'),
START_TRANSACTION_JSON_PATCH_OPERATIONS: type('dspace/core/patch/START_TRANSACTION_JSON_PATCH_OPERATIONS'),
+ DELETE_PENDING_JSON_PATCH_OPERATIONS: type('dspace/core/patch/DELETE_PENDING_JSON_PATCH_OPERATIONS'),
};
/* tslint:disable:max-classes-per-file */
@@ -261,6 +262,13 @@ export class NewPatchReplaceOperationAction implements Action {
}
}
+/**
+ * An ngrx action to delete all pending JSON Patch Operations.
+ */
+export class DeletePendingJsonPatchOperationsAction implements Action {
+ type = JsonPatchOperationsActionTypes.DELETE_PENDING_JSON_PATCH_OPERATIONS;
+}
+
/* tslint:enable:max-classes-per-file */
/**
@@ -276,4 +284,5 @@ export type PatchOperationsActions
| NewPatchRemoveOperationAction
| NewPatchReplaceOperationAction
| RollbacktPatchOperationsAction
- | StartTransactionPatchOperationsAction;
+ | StartTransactionPatchOperationsAction
+ | DeletePendingJsonPatchOperationsAction;
diff --git a/src/app/core/json-patch/json-patch-operations.reducer.spec.ts b/src/app/core/json-patch/json-patch-operations.reducer.spec.ts
index 34378b819b..1f98cf0920 100644
--- a/src/app/core/json-patch/json-patch-operations.reducer.spec.ts
+++ b/src/app/core/json-patch/json-patch-operations.reducer.spec.ts
@@ -1,7 +1,7 @@
import * as deepFreeze from 'deep-freeze';
import {
- CommitPatchOperationsAction,
+ CommitPatchOperationsAction, DeletePendingJsonPatchOperationsAction,
FlushPatchOperationsAction,
NewPatchAddOperationAction,
NewPatchRemoveOperationAction,
@@ -323,4 +323,19 @@ describe('jsonPatchOperationsReducer test suite', () => {
});
+ describe('When DeletePendingJsonPatchOperationsAction has been dispatched', () => {
+ it('should set set the JsonPatchOperationsState to null ', () => {
+ const action = new DeletePendingJsonPatchOperationsAction();
+ initState = Object.assign({}, testState, {
+ [testJsonPatchResourceType]: Object.assign({}, testState[testJsonPatchResourceType], {
+ transactionStartTime: startTimestamp,
+ commitPending: true
+ })
+ });
+ const newState = jsonPatchOperationsReducer(initState, action);
+
+ expect(newState).toBeNull();
+ });
+ });
+
});
diff --git a/src/app/core/json-patch/json-patch-operations.reducer.ts b/src/app/core/json-patch/json-patch-operations.reducer.ts
index 8d630dbfa1..5e00027edb 100644
--- a/src/app/core/json-patch/json-patch-operations.reducer.ts
+++ b/src/app/core/json-patch/json-patch-operations.reducer.ts
@@ -11,7 +11,8 @@ import {
NewPatchReplaceOperationAction,
CommitPatchOperationsAction,
StartTransactionPatchOperationsAction,
- RollbacktPatchOperationsAction
+ RollbacktPatchOperationsAction,
+ DeletePendingJsonPatchOperationsAction
} from './json-patch-operations.actions';
import { JsonPatchOperationModel, JsonPatchOperationType } from './json-patch.model';
@@ -101,6 +102,10 @@ export function jsonPatchOperationsReducer(state = initialState, action: PatchOp
return startTransactionPatchOperations(state, action as StartTransactionPatchOperationsAction);
}
+ case JsonPatchOperationsActionTypes.DELETE_PENDING_JSON_PATCH_OPERATIONS: {
+ return deletePendingOperations(state, action as DeletePendingJsonPatchOperationsAction);
+ }
+
default: {
return state;
}
@@ -178,6 +183,20 @@ function rollbackOperations(state: JsonPatchOperationsState, action: RollbacktPa
}
}
+/**
+ * Set the JsonPatchOperationsState to its initial value.
+ *
+ * @param state
+ * the current state
+ * @param action
+ * an DeletePendingJsonPatchOperationsAction
+ * @return JsonPatchOperationsState
+ * the new state.
+ */
+function deletePendingOperations(state: JsonPatchOperationsState, action: DeletePendingJsonPatchOperationsAction): JsonPatchOperationsState {
+ return null;
+}
+
/**
* Add new JSON patch operation list.
*
diff --git a/src/app/core/json-patch/json-patch-operations.service.spec.ts b/src/app/core/json-patch/json-patch-operations.service.spec.ts
index 97aba52e56..d1b2948777 100644
--- a/src/app/core/json-patch/json-patch-operations.service.spec.ts
+++ b/src/app/core/json-patch/json-patch-operations.service.spec.ts
@@ -17,6 +17,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service';
import { JsonPatchOperationsEntry, JsonPatchOperationsResourceEntry } from './json-patch-operations.reducer';
import {
CommitPatchOperationsAction,
+ DeletePendingJsonPatchOperationsAction,
RollbacktPatchOperationsAction,
StartTransactionPatchOperationsAction
} from './json-patch-operations.actions';
@@ -288,4 +289,19 @@ describe('JsonPatchOperationsService test suite', () => {
});
});
+ describe('deletePendingJsonPatchOperations', () => {
+ beforeEach(() => {
+ store.dispatch.and.callFake(() => { /* */ });
+ });
+
+ it('should dispatch a new DeletePendingJsonPatchOperationsAction', () => {
+
+ const expectedAction = new DeletePendingJsonPatchOperationsAction();
+ scheduler.schedule(() => service.deletePendingJsonPatchOperations());
+ scheduler.flush();
+
+ expect(store.dispatch).toHaveBeenCalledWith(expectedAction);
+ });
+ });
+
});
diff --git a/src/app/core/json-patch/json-patch-operations.service.ts b/src/app/core/json-patch/json-patch-operations.service.ts
index 84d946daba..c3363f4db4 100644
--- a/src/app/core/json-patch/json-patch-operations.service.ts
+++ b/src/app/core/json-patch/json-patch-operations.service.ts
@@ -10,7 +10,7 @@ import { CoreState } from '../core.reducers';
import { jsonPatchOperationsByResourceType } from './selectors';
import { JsonPatchOperationsResourceEntry } from './json-patch-operations.reducer';
import {
- CommitPatchOperationsAction,
+ CommitPatchOperationsAction, DeletePendingJsonPatchOperationsAction,
RollbacktPatchOperationsAction,
StartTransactionPatchOperationsAction
} from './json-patch-operations.actions';
@@ -105,6 +105,13 @@ export abstract class JsonPatchOperationsService {
let spyOnSet;
let authService;
let routeService;
+ let document;
authService = jasmine.createSpyObj('AuthService', {
isAuthenticated: jasmine.createSpy('isAuthenticated'),
@@ -43,6 +44,7 @@ describe('LocaleService test suite', () => {
{ provide: CookieService, useValue: new CookieServiceMock() },
{ provide: AuthService, userValue: authService },
{ provide: RouteService, useValue: routeServiceStub },
+ { provide: Document, useValue: document },
]
});
}));
@@ -52,7 +54,8 @@ describe('LocaleService test suite', () => {
translateService = TestBed.inject(TranslateService);
routeService = TestBed.inject(RouteService);
window = new NativeWindowRef();
- service = new LocaleService(window, cookieService, translateService, authService, routeService);
+ document = { documentElement: { lang: 'en' } };
+ service = new LocaleService(window, cookieService, translateService, authService, routeService, document);
serviceAsAny = service;
spyOnGet = spyOn(cookieService, 'get');
spyOnSet = spyOn(cookieService, 'set');
@@ -114,6 +117,12 @@ describe('LocaleService test suite', () => {
expect(translateService.use).toHaveBeenCalledWith('es');
expect(service.saveLanguageCodeToCookie).toHaveBeenCalledWith('es');
});
+
+ it('should set the current language on the html tag', () => {
+ spyOn(service, 'getCurrentLanguageCode').and.returnValue('es');
+ service.setCurrentLanguageCode();
+ expect((service as any).document.documentElement.lang).toEqual('es');
+ });
});
describe('', () => {
diff --git a/src/app/core/locale/locale.service.ts b/src/app/core/locale/locale.service.ts
index c73b297e40..1052021479 100644
--- a/src/app/core/locale/locale.service.ts
+++ b/src/app/core/locale/locale.service.ts
@@ -10,6 +10,7 @@ import { combineLatest, Observable, of as observableOf } from 'rxjs';
import { map, mergeMap, take } from 'rxjs/operators';
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
import { RouteService } from '../services/route.service';
+import { DOCUMENT } from '@angular/common';
export const LANG_COOKIE = 'dsLanguage';
@@ -38,7 +39,9 @@ export class LocaleService {
protected cookie: CookieService,
protected translate: TranslateService,
protected authService: AuthService,
- protected routeService: RouteService) {
+ protected routeService: RouteService,
+ @Inject(DOCUMENT) private document: any
+ ) {
}
/**
@@ -148,6 +151,7 @@ export class LocaleService {
}
this.translate.use(lang);
this.saveLanguageCodeToCookie(lang);
+ this.document.documentElement.lang = lang;
}
/**
diff --git a/src/app/core/log/log.interceptor.spec.ts b/src/app/core/log/log.interceptor.spec.ts
new file mode 100644
index 0000000000..9bda4b7934
--- /dev/null
+++ b/src/app/core/log/log.interceptor.spec.ts
@@ -0,0 +1,76 @@
+import { TestBed } from '@angular/core/testing';
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { HTTP_INTERCEPTORS } from '@angular/common/http';
+import { Router } from '@angular/router';
+
+import { LogInterceptor } from './log.interceptor';
+import { DspaceRestService } from '../dspace-rest/dspace-rest.service';
+import { RestRequestMethod } from '../data/rest-request-method';
+import { CookieService } from '../services/cookie.service';
+import { CookieServiceMock } from '../../shared/mocks/cookie.service.mock';
+import { RouterStub } from '../../shared/testing/router.stub';
+
+
+describe('LogInterceptor', () => {
+ let service: DspaceRestService;
+ let httpMock: HttpTestingController;
+ let cookieService: CookieService;
+ const router = Object.assign(new RouterStub(),{url : '/statistics'});
+
+ // Mock payload/statuses are dummy content as we are not testing the results
+ // of any below requests. We are only testing for X-XSRF-TOKEN header.
+ const mockPayload = {
+ id: 1
+ };
+ const mockStatusCode = 200;
+ const mockStatusText = 'SUCCESS';
+
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [HttpClientTestingModule],
+ providers: [
+ DspaceRestService,
+ // LogInterceptor,
+ {
+ provide: HTTP_INTERCEPTORS,
+ useClass: LogInterceptor,
+ multi: true,
+ },
+ { provide: CookieService, useValue: new CookieServiceMock() },
+ { provide: Router, useValue: router },
+ ],
+ });
+
+ service = TestBed.get(DspaceRestService);
+ httpMock = TestBed.get(HttpTestingController);
+ cookieService = TestBed.get(CookieService);
+
+ cookieService.set('CORRELATION-ID','123455');
+ });
+
+
+ it('headers should be set', (done) => {
+ service.request(RestRequestMethod.POST, 'server/api/core/items', 'test', { withCredentials: false }).subscribe((response) => {
+ expect(response).toBeTruthy();
+ done();
+ });
+
+ const httpRequest = httpMock.expectOne('server/api/core/items');
+ httpRequest.flush(mockPayload, { status: mockStatusCode, statusText: mockStatusText });
+ expect(httpRequest.request.headers.has('X-CORRELATION-ID')).toBeTrue();
+ expect(httpRequest.request.headers.has('X-REFERRER')).toBeTrue();
+ });
+
+ it('headers should have the right values', (done) => {
+ service.request(RestRequestMethod.POST, 'server/api/core/items', 'test', { withCredentials: false }).subscribe((response) => {
+ expect(response).toBeTruthy();
+ done();
+ });
+
+ const httpRequest = httpMock.expectOne('server/api/core/items');
+ httpRequest.flush(mockPayload, { status: mockStatusCode, statusText: mockStatusText });
+ expect(httpRequest.request.headers.get('X-CORRELATION-ID')).toEqual('123455');
+ expect(httpRequest.request.headers.get('X-REFERRER')).toEqual('/statistics');
+ });
+});
diff --git a/src/app/core/log/log.interceptor.ts b/src/app/core/log/log.interceptor.ts
new file mode 100644
index 0000000000..bf843f1da8
--- /dev/null
+++ b/src/app/core/log/log.interceptor.ts
@@ -0,0 +1,36 @@
+import { Injectable } from '@angular/core';
+import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
+import { Router } from '@angular/router';
+
+import { Observable } from 'rxjs';
+
+import { CookieService } from '../services/cookie.service';
+import { hasValue } from '../../shared/empty.util';
+
+/**
+ * Log Interceptor intercepting Http Requests & Responses to
+ * exchange add headers of the user using the application utilizing unique id in cookies.
+ * Add header for users current page path.
+ */
+@Injectable()
+export class LogInterceptor implements HttpInterceptor {
+
+ constructor(private cookieService: CookieService, private router: Router) {}
+
+ intercept(request: HttpRequest, next: HttpHandler): Observable> {
+
+ // Get Unique id of the user from the cookies
+ const correlationId = this.cookieService.get('CORRELATION-ID');
+
+ // Add headers from the intercepted request
+ let headers = request.headers;
+ if (hasValue(correlationId)) {
+ headers = headers.append('X-CORRELATION-ID', correlationId);
+ }
+ headers = headers.append('X-REFERRER', this.router.url);
+
+ // Add new headers to the intercepted request
+ request = request.clone({ withCredentials: true, headers: headers });
+ return next.handle(request);
+ }
+}
diff --git a/src/app/core/metadata/meta-tag.actions.ts b/src/app/core/metadata/meta-tag.actions.ts
new file mode 100644
index 0000000000..cd048d3be2
--- /dev/null
+++ b/src/app/core/metadata/meta-tag.actions.ts
@@ -0,0 +1,23 @@
+import { type } from '../../shared/ngrx/type';
+import { Action } from '@ngrx/store';
+
+// tslint:disable:max-classes-per-file
+export const MetaTagTypes = {
+ ADD: type('dspace/meta-tag/ADD'),
+ CLEAR: type('dspace/meta-tag/CLEAR')
+};
+
+export class AddMetaTagAction implements Action {
+ type = MetaTagTypes.ADD;
+ payload: string;
+
+ constructor(property: string) {
+ this.payload = property;
+ }
+}
+
+export class ClearMetaTagAction implements Action {
+ type = MetaTagTypes.CLEAR;
+}
+
+export type MetaTagAction = AddMetaTagAction | ClearMetaTagAction;
diff --git a/src/app/core/metadata/meta-tag.reducer.spec.ts b/src/app/core/metadata/meta-tag.reducer.spec.ts
new file mode 100644
index 0000000000..1fcd7d83e3
--- /dev/null
+++ b/src/app/core/metadata/meta-tag.reducer.spec.ts
@@ -0,0 +1,50 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+
+import { metaTagReducer } from './meta-tag.reducer';
+import { AddMetaTagAction, ClearMetaTagAction } from './meta-tag.actions';
+
+const nullAction = { type: null };
+
+describe('metaTagReducer', () => {
+ it('should start with an empty array', () => {
+ const state0 = metaTagReducer(undefined, nullAction);
+ expect(state0.tagsInUse).toEqual([]);
+ });
+
+ it('should return the current state on invalid action', () => {
+ const state0 = {
+ tagsInUse: ['foo', 'bar'],
+ };
+
+ const state1 = metaTagReducer(state0, nullAction);
+ expect(state1).toEqual(state0);
+ });
+
+ it('should add tags on AddMetaTagAction', () => {
+ const state0 = {
+ tagsInUse: ['foo'],
+ };
+
+ const state1 = metaTagReducer(state0, new AddMetaTagAction('bar'));
+ const state2 = metaTagReducer(state1, new AddMetaTagAction('baz'));
+
+ expect(state1.tagsInUse).toEqual(['foo', 'bar']);
+ expect(state2.tagsInUse).toEqual(['foo', 'bar', 'baz']);
+ });
+
+ it('should clear tags on ClearMetaTagAction', () => {
+ const state0 = {
+ tagsInUse: ['foo', 'bar'],
+ };
+
+ const state1 = metaTagReducer(state0, new ClearMetaTagAction());
+
+ expect(state1.tagsInUse).toEqual([]);
+ });
+});
diff --git a/src/app/core/metadata/meta-tag.reducer.ts b/src/app/core/metadata/meta-tag.reducer.ts
new file mode 100644
index 0000000000..0af6fb0aab
--- /dev/null
+++ b/src/app/core/metadata/meta-tag.reducer.ts
@@ -0,0 +1,38 @@
+import {
+ MetaTagAction,
+ MetaTagTypes,
+ AddMetaTagAction,
+ ClearMetaTagAction,
+} from './meta-tag.actions';
+
+export interface MetaTagState {
+ tagsInUse: string[];
+}
+
+const initialstate: MetaTagState = {
+ tagsInUse: []
+};
+
+export const metaTagReducer = (state: MetaTagState = initialstate, action: MetaTagAction): MetaTagState => {
+ switch (action.type) {
+ case MetaTagTypes.ADD: {
+ return addMetaTag(state, action as AddMetaTagAction);
+ }
+ case MetaTagTypes.CLEAR: {
+ return clearMetaTags(state, action as ClearMetaTagAction);
+ }
+ default: {
+ return state;
+ }
+ }
+};
+
+const addMetaTag = (state: MetaTagState, action: AddMetaTagAction): MetaTagState => {
+ return {
+ tagsInUse: [...state.tagsInUse, action.payload]
+ };
+};
+
+const clearMetaTags = (state: MetaTagState, action: ClearMetaTagAction): MetaTagState => {
+ return Object.assign({}, initialstate);
+};
diff --git a/src/app/core/metadata/metadata.service.spec.ts b/src/app/core/metadata/metadata.service.spec.ts
index 18421dd489..985851c321 100644
--- a/src/app/core/metadata/metadata.service.spec.ts
+++ b/src/app/core/metadata/metadata.service.spec.ts
@@ -1,82 +1,28 @@
-import { CommonModule, Location } from '@angular/common';
-import { HttpClient } from '@angular/common/http';
-import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
-import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
-import { Meta, MetaDefinition, Title } from '@angular/platform-browser';
-import { ActivatedRoute, Router } from '@angular/router';
-import { RouterTestingModule } from '@angular/router/testing';
+import { fakeAsync, tick } from '@angular/core/testing';
+import { Meta, Title } from '@angular/platform-browser';
+import { NavigationEnd, Router } from '@angular/router';
-import { Store, StoreModule } from '@ngrx/store';
-
-import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
-import { EmptyError, Observable, of } from 'rxjs';
+import { TranslateService } from '@ngx-translate/core';
+import { Observable, of } from 'rxjs';
import { RemoteData } from '../data/remote-data';
import { Item } from '../shared/item.model';
-import {
- ItemMock,
- MockBitstream1,
- MockBitstream2,
- MockBitstreamFormat1,
- MockBitstreamFormat2
-} from '../../shared/mocks/item.mock';
-import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
-import { NotificationsService } from '../../shared/notifications/notifications.service';
-import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
-import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
-import { AuthService } from '../auth/auth.service';
-import { BrowseService } from '../browse/browse.service';
-import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
-import { ObjectCacheService } from '../cache/object-cache.service';
-
-import { CoreState } from '../core.reducers';
-import { BitstreamDataService } from '../data/bitstream-data.service';
-import { BitstreamFormatDataService } from '../data/bitstream-format-data.service';
-import { CommunityDataService } from '../data/community-data.service';
-import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service';
-import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
-
-import { ItemDataService } from '../data/item-data.service';
-import { buildPaginatedList, PaginatedList } from '../data/paginated-list.model';
-import { FindListOptions } from '../data/request.models';
-import { RequestService } from '../data/request.service';
-import { BitstreamFormat } from '../shared/bitstream-format.model';
+import { ItemMock, MockBitstream1, MockBitstream3 } from '../../shared/mocks/item.mock';
+import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
+import { PaginatedList } from '../data/paginated-list.model';
import { Bitstream } from '../shared/bitstream.model';
-import { HALEndpointService } from '../shared/hal-endpoint.service';
import { MetadataValue } from '../shared/metadata.models';
-import { PageInfo } from '../shared/page-info.model';
-import { UUIDService } from '../shared/uuid.service';
import { MetadataService } from './metadata.service';
-import { environment } from '../../../environments/environment';
-import { storeModuleConfig } from '../../app.reducer';
-import { HardRedirectService } from '../services/hard-redirect.service';
-import { URLCombiner } from '../url-combiner/url-combiner';
import { RootDataService } from '../data/root-data.service';
-import { Root } from '../data/root.model';
-
-/* tslint:disable:max-classes-per-file */
-@Component({
- template: `
- `
-})
-class TestComponent {
- constructor(private metadata: MetadataService) {
- metadata.listenForRouteChange();
- }
-}
-
-@Component({ template: '' })
-class DummyItemComponent {
- constructor(private route: ActivatedRoute, private items: ItemDataService, private metadata: MetadataService) {
- this.route.params.subscribe((params) => {
- this.metadata.processRemoteData(this.items.findById(params.id));
- });
- }
-}
-
-/* tslint:enable:max-classes-per-file */
+import { Bundle } from '../shared/bundle.model';
+import { createPaginatedList } from '../../shared/testing/utils.test';
+import { getMockTranslateService } from '../../shared/mocks/translate.service.mock';
+import { DSONameService } from '../breadcrumbs/dso-name.service';
+import { HardRedirectService } from '../services/hard-redirect.service';
+import { getMockStore } from '@ngrx/store/testing';
+import { AddMetaTagAction, ClearMetaTagAction } from './meta-tag.actions';
describe('MetadataService', () => {
let metadataService: MetadataService;
@@ -85,188 +31,339 @@ describe('MetadataService', () => {
let title: Title;
- let store: Store;
+ let dsoNameService: DSONameService;
- let objectCacheService: ObjectCacheService;
- let requestService: RequestService;
- let uuidService: UUIDService;
- let remoteDataBuildService: RemoteDataBuildService;
- let itemDataService: ItemDataService;
- let authService: AuthService;
+ let bundleDataService;
+ let bitstreamDataService;
let rootService: RootDataService;
let translateService: TranslateService;
+ let hardRedirectService: HardRedirectService;
- let location: Location;
let router: Router;
- let fixture: ComponentFixture;
+ let store;
+
+ const initialState = { 'core': { metaTag: { tagsInUse: ['title', 'description'] }}};
- let tagStore: Map;
beforeEach(() => {
+ rootService = jasmine.createSpyObj({
+ findRoot: createSuccessfulRemoteDataObject$({ dspaceVersion: 'mock-dspace-version' })
+ });
+ bitstreamDataService = jasmine.createSpyObj({
+ findAllByHref: createSuccessfulRemoteDataObject$(createPaginatedList([MockBitstream3]))
+ });
+ bundleDataService = jasmine.createSpyObj({
+ findByItemAndName: mockBundleRD$([MockBitstream3])
+ });
+ translateService = getMockTranslateService();
+ meta = jasmine.createSpyObj('meta', {
+ addTag: {},
+ removeTag: {}
+ });
+ title = jasmine.createSpyObj({
+ setTitle: {}
+ });
+ dsoNameService = jasmine.createSpyObj({
+ getName: ItemMock.firstMetadataValue('dc.title')
+ });
+ router = {
+ url: '/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357',
+ events: of(new NavigationEnd(1, '', '')),
+ routerState: {
+ root: {}
+ }
+ } as any as Router;
+ hardRedirectService = jasmine.createSpyObj( {
+ getCurrentOrigin: 'https://request.org',
+ });
- store = new Store(undefined, undefined, undefined);
+ // @ts-ignore
+ store = getMockStore({ initialState });
spyOn(store, 'dispatch');
- objectCacheService = new ObjectCacheService(store, undefined);
- uuidService = new UUIDService();
- requestService = new RequestService(objectCacheService, uuidService, store, undefined);
- remoteDataBuildService = new RemoteDataBuildService(objectCacheService, undefined, requestService);
- const mockBitstreamDataService = {
- findAllByItemAndBundleName(item: Item, bundleName: string, options?: FindListOptions, ...linksToFollow: FollowLinkConfig[]): Observable>> {
- if (item.equals(ItemMock)) {
- return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [MockBitstream1, MockBitstream2]));
- } else {
- return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), []));
- }
- },
- };
- const mockBitstreamFormatDataService = {
- findByBitstream(bitstream: Bitstream): Observable> {
- switch (bitstream) {
- case MockBitstream1:
- return createSuccessfulRemoteDataObject$(MockBitstreamFormat1);
- break;
- case MockBitstream2:
- return createSuccessfulRemoteDataObject$(MockBitstreamFormat2);
- break;
- default:
- return createSuccessfulRemoteDataObject$(new BitstreamFormat());
- }
- }
- };
- rootService = jasmine.createSpyObj('rootService', {
- findRoot: createSuccessfulRemoteDataObject$(Object.assign(new Root(), {
- dspaceVersion: 'mock-dspace-version'
- }))
- });
-
- TestBed.configureTestingModule({
- imports: [
- CommonModule,
- StoreModule.forRoot({}, storeModuleConfig),
- TranslateModule.forRoot({
- loader: {
- provide: TranslateLoader,
- useClass: TranslateLoaderMock
- }
- }),
- RouterTestingModule.withRoutes([
- { path: 'items/:id', component: DummyItemComponent, pathMatch: 'full' },
- {
- path: 'other',
- component: DummyItemComponent,
- pathMatch: 'full',
- data: { title: 'Dummy Title', description: 'This is a dummy item component for testing!' }
- }
- ])
- ],
- declarations: [
- TestComponent,
- DummyItemComponent
- ],
- providers: [
- { provide: ObjectCacheService, useValue: objectCacheService },
- { provide: RequestService, useValue: requestService },
- { provide: RemoteDataBuildService, useValue: remoteDataBuildService },
- { provide: HALEndpointService, useValue: {} },
- { provide: AuthService, useValue: {} },
- { provide: NotificationsService, useValue: {} },
- { provide: HttpClient, useValue: {} },
- { provide: DSOChangeAnalyzer, useValue: {} },
- { provide: CommunityDataService, useValue: {} },
- { provide: DefaultChangeAnalyzer, useValue: {} },
- { provide: BitstreamFormatDataService, useValue: mockBitstreamFormatDataService },
- { provide: BitstreamDataService, useValue: mockBitstreamDataService },
- { provide: RootDataService, useValue: rootService },
- Meta,
- Title,
- // tslint:disable-next-line:no-empty
- { provide: ItemDataService, useValue: { findById: () => {} } },
- BrowseService,
- MetadataService
- ],
- schemas: [CUSTOM_ELEMENTS_SCHEMA]
- });
- meta = TestBed.inject(Meta);
- title = TestBed.inject(Title);
- itemDataService = TestBed.inject(ItemDataService);
- metadataService = TestBed.inject(MetadataService);
- authService = TestBed.inject(AuthService);
- translateService = TestBed.inject(TranslateService);
-
- router = TestBed.inject(Router);
- location = TestBed.inject(Location);
-
- fixture = TestBed.createComponent(TestComponent);
-
- tagStore = metadataService.getTagStore();
+ metadataService = new MetadataService(
+ router,
+ translateService,
+ meta,
+ title,
+ dsoNameService,
+ bundleDataService,
+ bitstreamDataService,
+ undefined,
+ rootService,
+ store,
+ hardRedirectService
+ );
});
it('items page should set meta tags', fakeAsync(() => {
- spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(ItemMock));
- router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']);
+ (metadataService as any).processRouteChange({
+ data: {
+ value: {
+ dso: createSuccessfulRemoteDataObject(ItemMock),
+ }
+ }
+ });
tick();
- expect(title.getTitle()).toEqual('Test PowerPoint Document');
- expect(tagStore.get('citation_title')[0].content).toEqual('Test PowerPoint Document');
- expect(tagStore.get('citation_author')[0].content).toEqual('Doe, Jane');
- expect(tagStore.get('citation_date')[0].content).toEqual('1650-06-26');
- expect(tagStore.get('citation_issn')[0].content).toEqual('123456789');
- expect(tagStore.get('citation_language')[0].content).toEqual('en');
- expect(tagStore.get('citation_keywords')[0].content).toEqual('keyword1; keyword2; keyword3');
+ expect(title.setTitle).toHaveBeenCalledWith('Test PowerPoint Document');
+ expect(meta.addTag).toHaveBeenCalledWith({
+ name: 'citation_title',
+ content: 'Test PowerPoint Document'
+ });
+ expect(meta.addTag).toHaveBeenCalledWith({ name: 'citation_author', content: 'Doe, Jane' });
+ expect(meta.addTag).toHaveBeenCalledWith({
+ name: 'citation_publication_date',
+ content: '1650-06-26'
+ });
+ expect(meta.addTag).toHaveBeenCalledWith({ name: 'citation_issn', content: '123456789' });
+ expect(meta.addTag).toHaveBeenCalledWith({ name: 'citation_language', content: 'en' });
+ expect(meta.addTag).toHaveBeenCalledWith({
+ name: 'citation_keywords',
+ content: 'keyword1; keyword2; keyword3'
+ });
}));
it('items page should set meta tags as published Thesis', fakeAsync(() => {
- spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(mockPublisher(mockType(ItemMock, 'Thesis'))));
- router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']);
+ (metadataService as any).processRouteChange({
+ data: {
+ value: {
+ dso: createSuccessfulRemoteDataObject(mockPublisher(mockType(ItemMock, 'Thesis'))),
+ }
+ }
+ });
tick();
- expect(tagStore.get('citation_dissertation_name')[0].content).toEqual('Test PowerPoint Document');
- expect(tagStore.get('citation_dissertation_institution')[0].content).toEqual('Mock Publisher');
- expect(tagStore.get('citation_abstract_html_url')[0].content).toEqual([environment.ui.baseUrl, router.url].join(''));
- expect(tagStore.get('citation_pdf_url')[0].content).toEqual('/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28/download');
+ expect(meta.addTag).toHaveBeenCalledWith({
+ name: 'citation_dissertation_name',
+ content: 'Test PowerPoint Document'
+ });
+ expect(meta.addTag).toHaveBeenCalledWith({
+ name: 'citation_pdf_url',
+ content: 'https://request.org/bitstreams/4db100c1-e1f5-4055-9404-9bc3e2d15f29/download'
+ });
}));
it('items page should set meta tags as published Technical Report', fakeAsync(() => {
- spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(mockPublisher(mockType(ItemMock, 'Technical Report'))));
- router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']);
- tick();
- expect(tagStore.get('citation_technical_report_institution')[0].content).toEqual('Mock Publisher');
- }));
-
- it('other navigation should add title, description and Generator', fakeAsync(() => {
- spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(ItemMock));
- spyOn(translateService, 'get').and.returnValues(of('DSpace :: '), of('Dummy Title'), of('This is a dummy item component for testing!'));
- router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']);
- tick();
- expect(tagStore.size).toBeGreaterThan(0);
- router.navigate(['/other']);
- tick();
- expect(tagStore.size).toEqual(3);
- expect(title.getTitle()).toEqual('DSpace :: Dummy Title');
- expect(tagStore.get('title')[0].content).toEqual('DSpace :: Dummy Title');
- expect(tagStore.get('description')[0].content).toEqual('This is a dummy item component for testing!');
- expect(tagStore.get('Generator')[0].content).toEqual('mock-dspace-version');
- }));
-
- describe('when the item has no bitstreams', () => {
-
- beforeEach(() => {
- // this.bitstreamDataService.findAllByItemAndBundleName(this.item, 'ORIGINAL')
- // spyOn(MockItem, 'getFiles').and.returnValue(observableOf([]));
+ (metadataService as any).processRouteChange({
+ data: {
+ value: {
+ dso: createSuccessfulRemoteDataObject(mockPublisher(mockType(ItemMock, 'Technical Report'))),
+ }
+ }
});
+ tick();
+ expect(meta.addTag).toHaveBeenCalledWith({
+ name: 'citation_technical_report_institution',
+ content: 'Mock Publisher'
+ });
+ }));
- it('processRemoteData should not produce an EmptyError', fakeAsync(() => {
- spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(ItemMock));
- spyOn(metadataService, 'processRemoteData').and.callThrough();
- router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']);
+ it('other navigation should add title and description', fakeAsync(() => {
+ (translateService.get as jasmine.Spy).and.returnValues(of('DSpace :: '), of('Dummy Title'), of('This is a dummy item component for testing!'));
+ (metadataService as any).processRouteChange({
+ data: {
+ value: {
+ title: 'Dummy Title',
+ description: 'This is a dummy item component for testing!'
+ }
+ }
+ });
+ tick();
+ expect(title.setTitle).toHaveBeenCalledWith('DSpace :: Dummy Title');
+ expect(meta.addTag).toHaveBeenCalledWith({
+ name: 'title',
+ content: 'DSpace :: Dummy Title'
+ });
+ expect(meta.addTag).toHaveBeenCalledWith({
+ name: 'description',
+ content: 'This is a dummy item component for testing!'
+ });
+ }));
+
+ describe(`listenForRouteChange`, () => {
+ it(`should call processRouteChange`, fakeAsync(() => {
+ spyOn(metadataService as any, 'processRouteChange').and.callFake(() => undefined);
+ metadataService.listenForRouteChange();
tick();
- expect(metadataService.processRemoteData).not.toThrow(new EmptyError());
+ expect((metadataService as any).processRouteChange).toHaveBeenCalled();
+ }));
+ it(`should add Generator`, fakeAsync(() => {
+ spyOn(metadataService as any, 'processRouteChange').and.callFake(() => undefined);
+ metadataService.listenForRouteChange();
+ tick();
+ expect(meta.addTag).toHaveBeenCalledWith({
+ name: 'Generator',
+ content: 'mock-dspace-version'
+ });
}));
-
});
- const mockRemoteData = (mockItem: Item): Observable> => {
- return createSuccessfulRemoteDataObject$(ItemMock);
- };
+ describe('citation_abstract_html_url', () => {
+ it('should use dc.identifier.uri if available', fakeAsync(() => {
+ (metadataService as any).processRouteChange({
+ data: {
+ value: {
+ dso: createSuccessfulRemoteDataObject(mockUri(ItemMock, 'https://ddg.gg')),
+ }
+ }
+ });
+ tick();
+ expect(meta.addTag).toHaveBeenCalledWith({
+ name: 'citation_abstract_html_url',
+ content: 'https://ddg.gg'
+ });
+ }));
+
+ it('should use current route as fallback', fakeAsync(() => {
+ (metadataService as any).processRouteChange({
+ data: {
+ value: {
+ dso: createSuccessfulRemoteDataObject(mockUri(ItemMock)),
+ }
+ }
+ });
+ tick();
+ expect(meta.addTag).toHaveBeenCalledWith({
+ name: 'citation_abstract_html_url',
+ content: 'https://request.org/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357'
+ });
+ }));
+ });
+
+ describe('citation_*_institution / citation_publisher', () => {
+ it('should use citation_dissertation_institution tag for dissertations', fakeAsync(() => {
+ (metadataService as any).processRouteChange({
+ data: {
+ value: {
+ dso: createSuccessfulRemoteDataObject(mockPublisher(mockType(ItemMock, 'Thesis'))),
+ }
+ }
+ });
+ tick();
+ expect(meta.addTag).toHaveBeenCalledWith({
+ name: 'citation_dissertation_institution',
+ content: 'Mock Publisher'
+ });
+ expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ name: 'citation_technical_report_institution' }));
+ expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ name: 'citation_publisher' }));
+ }));
+
+ it('should use citation_tech_report_institution tag for tech reports', fakeAsync(() => {
+ (metadataService as any).processRouteChange({
+ data: {
+ value: {
+ dso: createSuccessfulRemoteDataObject(mockPublisher(mockType(ItemMock, 'Technical Report'))),
+ }
+ }
+ });
+ tick();
+ expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ name: 'citation_dissertation_institution' }));
+ expect(meta.addTag).toHaveBeenCalledWith({
+ name: 'citation_technical_report_institution',
+ content: 'Mock Publisher'
+ });
+ expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ name: 'citation_publisher' }));
+ }));
+
+ it('should use citation_publisher for other item types', fakeAsync(() => {
+ (metadataService as any).processRouteChange({
+ data: {
+ value: {
+ dso: createSuccessfulRemoteDataObject(mockPublisher(mockType(ItemMock, 'Some Other Type'))),
+ }
+ }
+ });
+ tick();
+ expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ name: 'citation_dissertation_institution' }));
+ expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ name: 'citation_technical_report_institution' }));
+ expect(meta.addTag).toHaveBeenCalledWith({
+ name: 'citation_publisher',
+ content: 'Mock Publisher'
+ });
+ }));
+ });
+
+ describe('citation_pdf_url', () => {
+ it('should link to primary Bitstream URL regardless of format', fakeAsync(() => {
+ (bundleDataService.findByItemAndName as jasmine.Spy).and.returnValue(mockBundleRD$([], MockBitstream3));
+
+ (metadataService as any).processRouteChange({
+ data: {
+ value: {
+ dso: createSuccessfulRemoteDataObject(ItemMock),
+ }
+ }
+ });
+ tick();
+ expect(meta.addTag).toHaveBeenCalledWith({
+ name: 'citation_pdf_url',
+ content: 'https://request.org/bitstreams/4db100c1-e1f5-4055-9404-9bc3e2d15f29/download'
+ });
+ }));
+
+ describe('no primary Bitstream', () => {
+ it('should link to first and only Bitstream regardless of format', fakeAsync(() => {
+ (bundleDataService.findByItemAndName as jasmine.Spy).and.returnValue(mockBundleRD$([MockBitstream3]));
+
+ (metadataService as any).processRouteChange({
+ data: {
+ value: {
+ dso: createSuccessfulRemoteDataObject(ItemMock),
+ }
+ }
+ });
+ tick();
+ expect(meta.addTag).toHaveBeenCalledWith({
+ name: 'citation_pdf_url',
+ content: 'https://request.org/bitstreams/4db100c1-e1f5-4055-9404-9bc3e2d15f29/download'
+ });
+ }));
+
+ it('should link to first Bitstream with allowed format', fakeAsync(() => {
+ const bitstreams = [MockBitstream3, MockBitstream3, MockBitstream1];
+ (bundleDataService.findByItemAndName as jasmine.Spy).and.returnValue(mockBundleRD$(bitstreams));
+ (bitstreamDataService.findAllByHref as jasmine.Spy).and.returnValues(
+ ...mockBitstreamPages$(bitstreams).map(bp => createSuccessfulRemoteDataObject$(bp)),
+ );
+
+ (metadataService as any).processRouteChange({
+ data: {
+ value: {
+ dso: createSuccessfulRemoteDataObject(ItemMock),
+ }
+ }
+ });
+ tick();
+ expect(meta.addTag).toHaveBeenCalledWith({
+ name: 'citation_pdf_url',
+ content: 'https://request.org/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/download'
+ });
+ }));
+ });
+ });
+
+ describe('tagstore', () => {
+ beforeEach(fakeAsync(() => {
+ (metadataService as any).processRouteChange({
+ data: {
+ value: {
+ dso: createSuccessfulRemoteDataObject(ItemMock),
+ }
+ }
+ });
+ tick();
+ }));
+
+ it('should remove previous tags on route change', fakeAsync(() => {
+ expect(meta.removeTag).toHaveBeenCalledWith('name=\'title\'');
+ expect(meta.removeTag).toHaveBeenCalledWith('name=\'description\'');
+ }));
+
+ it('should clear all tags and add new ones on route change', () => {
+ expect(store.dispatch.calls.argsFor(0)).toEqual([new ClearMetaTagAction()]);
+ expect(store.dispatch.calls.argsFor(1)).toEqual([new AddMetaTagAction('title')]);
+ expect(store.dispatch.calls.argsFor(2)).toEqual([new AddMetaTagAction('description')]);
+ });
+ });
const mockType = (mockItem: Item, type: string): Item => {
const typedMockItem = Object.assign(new Item(), mockItem) as Item;
@@ -285,4 +382,30 @@ describe('MetadataService', () => {
return publishedMockItem;
};
+ const mockUri = (mockItem: Item, uri?: string): Item => {
+ const publishedMockItem = Object.assign(new Item(), mockItem) as Item;
+ publishedMockItem.metadata['dc.identifier.uri'] = [{ value: uri }] as MetadataValue[];
+ return publishedMockItem;
+ };
+
+ const mockBundleRD$ = (bitstreams: Bitstream[], primary?: Bitstream): Observable> => {
+ return createSuccessfulRemoteDataObject$(
+ Object.assign(new Bundle(), {
+ name: 'ORIGINAL',
+ bitstreams: createSuccessfulRemoteDataObject$(mockBitstreamPages$(bitstreams)[0]),
+ primaryBitstream: createSuccessfulRemoteDataObject$(primary),
+ })
+ );
+ };
+
+ const mockBitstreamPages$ = (bitstreams: Bitstream[]): PaginatedList[] => {
+ return bitstreams.map((bitstream, index) => Object.assign(createPaginatedList([bitstream]), {
+ pageInfo: {
+ totalElements: bitstreams.length, // announce multiple elements/pages
+ },
+ _links: index < bitstreams.length - 1
+ ? { next: { href: 'not empty' }} // fake link to the next bitstream page
+ : { next: { href: undefined }}, // last page has no link
+ }));
+ };
});
diff --git a/src/app/core/metadata/metadata.service.ts b/src/app/core/metadata/metadata.service.ts
index 807f7a42ab..1c6946b0d3 100644
--- a/src/app/core/metadata/metadata.service.ts
+++ b/src/app/core/metadata/metadata.service.ts
@@ -5,12 +5,11 @@ import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
-import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
-import { catchError, distinctUntilKeyChanged, filter, first, map, take } from 'rxjs/operators';
+import { BehaviorSubject, combineLatest, EMPTY, Observable, of as observableOf } from 'rxjs';
+import { expand, filter, map, switchMap, take } from 'rxjs/operators';
-import { hasValue, isNotEmpty } from '../../shared/empty.util';
+import { hasNoValue, hasValue } from '../../shared/empty.util';
import { DSONameService } from '../breadcrumbs/dso-name.service';
-import { CacheableObject } from '../cache/object-cache.reducer';
import { BitstreamDataService } from '../data/bitstream-data.service';
import { BitstreamFormatDataService } from '../data/bitstream-format-data.service';
@@ -19,22 +18,57 @@ import { BitstreamFormat } from '../shared/bitstream-format.model';
import { Bitstream } from '../shared/bitstream.model';
import { DSpaceObject } from '../shared/dspace-object.model';
import { Item } from '../shared/item.model';
-import {
- getFirstSucceededRemoteDataPayload,
- getFirstSucceededRemoteListPayload
-} from '../shared/operators';
-import { environment } from '../../../environments/environment';
+import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../shared/operators';
import { RootDataService } from '../data/root-data.service';
import { getBitstreamDownloadRoute } from '../../app-routing-paths';
+import { BundleDataService } from '../data/bundle-data.service';
+import { followLink } from '../../shared/utils/follow-link-config.model';
+import { Bundle } from '../shared/bundle.model';
+import { PaginatedList } from '../data/paginated-list.model';
+import { URLCombiner } from '../url-combiner/url-combiner';
+import { HardRedirectService } from '../services/hard-redirect.service';
+import { MetaTagState } from './meta-tag.reducer';
+import { createSelector, select, Store } from '@ngrx/store';
+import { AddMetaTagAction, ClearMetaTagAction } from './meta-tag.actions';
+import { coreSelector } from '../core.selectors';
+import { CoreState } from '../core.reducers';
+
+/**
+ * The base selector function to select the metaTag section in the store
+ */
+const metaTagSelector = createSelector(
+ coreSelector,
+ (state: CoreState) => state.metaTag
+);
+
+/**
+ * Selector function to select the tags in use from the MetaTagState
+ */
+const tagsInUseSelector =
+ createSelector(
+ metaTagSelector,
+ (state: MetaTagState) => state.tagsInUse,
+ );
@Injectable()
export class MetadataService {
- private initialized: boolean;
+ private currentObject: BehaviorSubject = new BehaviorSubject(undefined);
- private tagStore: Map;
-
- private currentObject: BehaviorSubject;
+ /**
+ * When generating the citation_pdf_url meta tag for Items with more than one Bitstream (and no primary Bitstream),
+ * the first Bitstream to match one of the following MIME types is selected.
+ * See {@linkcode getFirstAllowedFormatBitstreamLink}
+ * @private
+ */
+ private readonly CITATION_PDF_URL_MIMETYPES = [
+ 'application/pdf', // .pdf
+ 'application/postscript', // .ps
+ 'application/msword', // .doc
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // .docx
+ 'application/rtf', // .rtf
+ 'application/epub+zip', // .epub
+ ];
constructor(
private router: Router,
@@ -42,21 +76,19 @@ export class MetadataService {
private meta: Meta,
private title: Title,
private dsoNameService: DSONameService,
+ private bundleDataService: BundleDataService,
private bitstreamDataService: BitstreamDataService,
private bitstreamFormatDataService: BitstreamFormatDataService,
- private rootService: RootDataService
+ private rootService: RootDataService,
+ private store: Store,
+ private hardRedirectService: HardRedirectService,
) {
- // TODO: determine what open graph meta tags are needed and whether
- // the differ per route. potentially add image based on DSpaceObject
- this.meta.addTags([
- { property: 'og:title', content: 'DSpace Angular Universal' },
- { property: 'og:description', content: 'The modern front-end for DSpace 7.' }
- ]);
- this.initialized = false;
- this.tagStore = new Map();
}
public listenForRouteChange(): void {
+ // This never changes, set it only once
+ this.setGenerator();
+
this.router.events.pipe(
filter((event) => event instanceof NavigationEnd),
map(() => this.router.routerState.root),
@@ -68,22 +100,9 @@ export class MetadataService {
});
}
- public processRemoteData(remoteData: Observable>): void {
- remoteData.pipe(map((rd: RemoteData) => rd.payload),
- filter((co: CacheableObject) => hasValue(co)),
- take(1))
- .subscribe((dspaceObject: DSpaceObject) => {
- if (!this.initialized) {
- this.initialize(dspaceObject);
- }
- this.currentObject.next(dspaceObject);
- });
- }
-
private processRouteChange(routeInfo: any): void {
- if (routeInfo.params.value.id === undefined) {
- this.clearMetaTags();
- }
+ this.clearMetaTags();
+
if (routeInfo.data.value.title) {
const titlePrefix = this.translate.get('repository.title.prefix');
const title = this.translate.get(routeInfo.data.value.title, routeInfo.data.value);
@@ -98,15 +117,10 @@ export class MetadataService {
});
}
- this.setGenerator();
- }
-
- private initialize(dspaceObject: DSpaceObject): void {
- this.currentObject = new BehaviorSubject(dspaceObject);
- this.currentObject.asObservable().pipe(distinctUntilKeyChanged('uuid')).subscribe(() => {
- this.setMetaTags();
- });
- this.initialized = true;
+ if (hasValue(routeInfo.data.value.dso) && hasValue(routeInfo.data.value.dso.payload)) {
+ this.currentObject.next(routeInfo.data.value.dso.payload);
+ this.setDSOMetaTags();
+ }
}
private getCurrentRoute(route: ActivatedRoute): ActivatedRoute {
@@ -116,16 +130,14 @@ export class MetadataService {
return route;
}
- private setMetaTags(): void {
-
- this.clearMetaTags();
+ private setDSOMetaTags(): void {
this.setTitleTag();
this.setDescriptionTag();
this.setCitationTitleTag();
this.setCitationAuthorTags();
- this.setCitationDateTag();
+ this.setCitationPublicationDateTag();
this.setCitationISSNTag();
this.setCitationISBNTag();
@@ -134,14 +146,10 @@ export class MetadataService {
this.setCitationAbstractUrlTag();
this.setCitationPdfUrlTag();
+ this.setCitationPublisherTag();
if (this.isDissertation()) {
this.setCitationDissertationNameTag();
- this.setCitationDissertationInstitutionTag();
- }
-
- if (this.isTechReport()) {
- this.setCitationTechReportInstitutionTag();
}
// this.setCitationJournalTitleTag();
@@ -176,7 +184,7 @@ export class MetadataService {
private setDescriptionTag(): void {
// TODO: truncate abstract
const value = this.getMetaTagValue('dc.description.abstract');
- this.addMetaTag('desciption', value);
+ this.addMetaTag('description', value);
}
/**
@@ -196,11 +204,11 @@ export class MetadataService {
}
/**
- * Add to the
+ * Add to the
*/
- private setCitationDateTag(): void {
+ private setCitationPublicationDateTag(): void {
const value = this.getFirstMetaTagValue(['dc.date.copyright', 'dc.date.issued', 'dc.date.available', 'dc.date.accessioned']);
- this.addMetaTag('citation_date', value);
+ this.addMetaTag('citation_publication_date', value);
}
/**
@@ -236,19 +244,17 @@ export class MetadataService {
}
/**
- * Add to the
+ * Add dc.publisher to the . The tag name depends on the item type.
*/
- private setCitationDissertationInstitutionTag(): void {
+ private setCitationPublisherTag(): void {
const value = this.getMetaTagValue('dc.publisher');
- this.addMetaTag('citation_dissertation_institution', value);
- }
-
- /**
- * Add to the
- */
- private setCitationTechReportInstitutionTag(): void {
- const value = this.getMetaTagValue('dc.publisher');
- this.addMetaTag('citation_technical_report_institution', value);
+ if (this.isDissertation()) {
+ this.addMetaTag('citation_dissertation_institution', value);
+ } else if (this.isTechReport()) {
+ this.addMetaTag('citation_technical_report_institution', value);
+ } else {
+ this.addMetaTag('citation_publisher', value);
+ }
}
/**
@@ -264,8 +270,11 @@ export class MetadataService {
*/
private setCitationAbstractUrlTag(): void {
if (this.currentObject.value instanceof Item) {
- const value = [environment.ui.baseUrl, this.router.url].join('');
- this.addMetaTag('citation_abstract_html_url', value);
+ let url = this.getMetaTagValue('dc.identifier.uri');
+ if (hasNoValue(url)) {
+ url = new URLCombiner(this.hardRedirectService.getCurrentOrigin(), this.router.url).toString();
+ }
+ this.addMetaTag('citation_abstract_html_url', url);
}
}
@@ -275,35 +284,126 @@ export class MetadataService {
private setCitationPdfUrlTag(): void {
if (this.currentObject.value instanceof Item) {
const item = this.currentObject.value as Item;
- this.bitstreamDataService.findAllByItemAndBundleName(item, 'ORIGINAL')
- .pipe(
- getFirstSucceededRemoteListPayload(),
- first((files) => isNotEmpty(files)),
- catchError((error) => {
- console.debug(error.message);
- return [];
- }))
- .subscribe((bitstreams: Bitstream[]) => {
- for (const bitstream of bitstreams) {
- this.bitstreamFormatDataService.findByBitstream(bitstream).pipe(
- getFirstSucceededRemoteDataPayload()
- ).subscribe((format: BitstreamFormat) => {
- if (format.mimetype === 'application/pdf') {
- const bitstreamLink = getBitstreamDownloadRoute(bitstream);
- this.addMetaTag('citation_pdf_url', bitstreamLink);
+
+ // Retrieve the ORIGINAL bundle for the item
+ this.bundleDataService.findByItemAndName(
+ item,
+ 'ORIGINAL',
+ true,
+ true,
+ followLink('primaryBitstream'),
+ followLink('bitstreams', {}, followLink('format')),
+ ).pipe(
+ getFirstSucceededRemoteDataPayload(),
+ switchMap((bundle: Bundle) =>
+
+ // First try the primary bitstream
+ bundle.primaryBitstream.pipe(
+ getFirstCompletedRemoteData(),
+ map((rd: RemoteData) => {
+ if (hasValue(rd.payload)) {
+ return rd.payload;
+ } else {
+ return null;
}
- });
+ }),
+ // return the bundle as well so we can use it again if there's no primary bitstream
+ map((bitstream: Bitstream) => [bundle, bitstream])
+ )
+ ),
+ switchMap(([bundle, primaryBitstream]: [Bundle, Bitstream]) => {
+ if (hasValue(primaryBitstream)) {
+ // If there was a primary bitstream, emit its link
+ return [getBitstreamDownloadRoute(primaryBitstream)];
+ } else {
+ // Otherwise consider the regular bitstreams in the bundle
+ return bundle.bitstreams.pipe(
+ getFirstCompletedRemoteData(),
+ switchMap((bitstreamRd: RemoteData>) => {
+ if (hasValue(bitstreamRd.payload) && bitstreamRd.payload.totalElements === 1) {
+ // If there's only one bitstream in the bundle, emit its link
+ return [getBitstreamDownloadRoute(bitstreamRd.payload.page[0])];
+ } else {
+ // Otherwise check all bitstreams to see if one matches the format whitelist
+ return this.getFirstAllowedFormatBitstreamLink(bitstreamRd);
+ }
+ })
+ );
}
- });
+ }),
+ take(1)
+ ).subscribe((link: string) => {
+ // Use the found link to set the tag
+ this.addMetaTag(
+ 'citation_pdf_url',
+ new URLCombiner(this.hardRedirectService.getCurrentOrigin(), link).toString()
+ );
+ });
}
}
+ /**
+ * For Items with more than one Bitstream (and no primary Bitstream), link to the first Bitstream with a MIME type
+ * included in {@linkcode CITATION_PDF_URL_MIMETYPES}
+ * @param bitstreamRd
+ * @private
+ */
+ private getFirstAllowedFormatBitstreamLink(bitstreamRd: RemoteData>): Observable {
+ return observableOf(bitstreamRd.payload).pipe(
+ // Because there can be more than one page of bitstreams, this expand operator
+ // will retrieve them in turn. Due to the take(1) at the bottom, it will only
+ // retrieve pages until a match is found
+ expand((paginatedList: PaginatedList) => {
+ if (hasNoValue(paginatedList.next)) {
+ // If there's no next page, stop.
+ return EMPTY;
+ } else {
+ // Otherwise retrieve the next page
+ return this.bitstreamDataService.findAllByHref(
+ paginatedList.next,
+ undefined,
+ true,
+ true,
+ followLink('format')
+ ).pipe(
+ getFirstCompletedRemoteData(),
+ map((next: RemoteData>) => {
+ if (hasValue(next.payload)) {
+ return next.payload;
+ } else {
+ return EMPTY;
+ }
+ })
+ );
+ }
+ }),
+ // Return the array of bitstreams inside each paginated list
+ map((paginatedList: PaginatedList) => paginatedList.page),
+ // Emit the bitstreams in the list one at a time
+ switchMap((bitstreams: Bitstream[]) => bitstreams),
+ // Retrieve the format for each bitstream
+ switchMap((bitstream: Bitstream) => bitstream.format.pipe(
+ getFirstSucceededRemoteDataPayload(),
+ // Keep the original bitstream, because it, not the format, is what we'll need
+ // for the link at the end
+ map((format: BitstreamFormat) => [bitstream, format])
+ )),
+ // Filter out only pairs with whitelisted formats
+ filter(([, format]: [Bitstream, BitstreamFormat]) =>
+ hasValue(format) && this.CITATION_PDF_URL_MIMETYPES.includes(format.mimetype)),
+ // We only need 1
+ take(1),
+ // Emit the link of the match
+ map(([bitstream, ]: [Bitstream, BitstreamFormat]) => getBitstreamDownloadRoute(bitstream))
+ );
+ }
+
/**
* Add to the containing the current DSpace version
*/
private setGenerator(): void {
this.rootService.findRoot().pipe(getFirstSucceededRemoteDataPayload()).subscribe((root) => {
- this.addMetaTag('Generator', root.dspaceVersion);
+ this.meta.addTag({ name: 'Generator', content: root.dspaceVersion });
});
}
@@ -347,47 +447,35 @@ export class MetadataService {
return this.currentObject.value.allMetadataValues(keys);
}
- private addMetaTag(property: string, content: string): void {
+ private addMetaTag(name: string, content: string): void {
if (content) {
- const tag = { property, content } as MetaDefinition;
+ const tag = { name, content } as MetaDefinition;
this.meta.addTag(tag);
- this.storeTag(property, tag);
+ this.storeTag(name);
}
}
- private addMetaTags(property: string, content: string[]): void {
+ private addMetaTags(name: string, content: string[]): void {
for (const value of content) {
- this.addMetaTag(property, value);
+ this.addMetaTag(name, value);
}
}
- private storeTag(key: string, tag: MetaDefinition): void {
- const tags: MetaDefinition[] = this.getTags(key);
- tags.push(tag);
- this.setTags(key, tags);
- }
-
- private getTags(key: string): MetaDefinition[] {
- let tags: MetaDefinition[] = this.tagStore.get(key);
- if (tags === undefined) {
- tags = [];
- }
- return tags;
- }
-
- private setTags(key: string, tags: MetaDefinition[]): void {
- this.tagStore.set(key, tags);
+ private storeTag(key: string): void {
+ this.store.dispatch(new AddMetaTagAction(key));
}
public clearMetaTags() {
- this.tagStore.forEach((tags: MetaDefinition[], property: string) => {
- this.meta.removeTag('property=\'' + property + '\'');
+ this.store.pipe(
+ select(tagsInUseSelector),
+ take(1)
+ ).subscribe((tagsInUse: string[]) => {
+ for (const name of tagsInUse) {
+ this.meta.removeTag('name=\'' + name + '\'');
+ }
+ this.store.dispatch(new ClearMetaTagAction());
});
- this.tagStore.clear();
}
- public getTagStore(): Map {
- return this.tagStore;
- }
}
diff --git a/src/app/core/registry/registry.service.spec.ts b/src/app/core/registry/registry.service.spec.ts
index 5f2f123f01..199f43e98e 100644
--- a/src/app/core/registry/registry.service.spec.ts
+++ b/src/app/core/registry/registry.service.spec.ts
@@ -15,7 +15,7 @@ import {
MetadataRegistryEditSchemaAction,
MetadataRegistrySelectFieldAction,
MetadataRegistrySelectSchemaAction
-} from '../../+admin/admin-registries/metadata-registry/metadata-registry.actions';
+} from '../../admin/admin-registries/metadata-registry/metadata-registry.actions';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { StoreMock } from '../../shared/testing/store.mock';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
diff --git a/src/app/core/registry/registry.service.ts b/src/app/core/registry/registry.service.ts
index b7b35c6a5a..0046dbdb19 100644
--- a/src/app/core/registry/registry.service.ts
+++ b/src/app/core/registry/registry.service.ts
@@ -7,7 +7,7 @@ import { hasValue, hasValueOperator, isNotEmptyOperator } from '../../shared/emp
import { getFirstSucceededRemoteDataPayload } from '../shared/operators';
import { createSelector, select, Store } from '@ngrx/store';
import { AppState } from '../../app.reducer';
-import { MetadataRegistryState } from '../../+admin/admin-registries/metadata-registry/metadata-registry.reducers';
+import { MetadataRegistryState } from '../../admin/admin-registries/metadata-registry/metadata-registry.reducers';
import {
MetadataRegistryCancelFieldAction,
MetadataRegistryCancelSchemaAction,
@@ -19,7 +19,7 @@ import {
MetadataRegistryEditSchemaAction,
MetadataRegistrySelectFieldAction,
MetadataRegistrySelectSchemaAction
-} from '../../+admin/admin-registries/metadata-registry/metadata-registry.actions';
+} from '../../admin/admin-registries/metadata-registry/metadata-registry.actions';
import { map, mergeMap, tap } from 'rxjs/operators';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
diff --git a/src/app/core/services/browser-hard-redirect.service.spec.ts b/src/app/core/services/browser-hard-redirect.service.spec.ts
index d0fecd9be5..b9745906c3 100644
--- a/src/app/core/services/browser-hard-redirect.service.spec.ts
+++ b/src/app/core/services/browser-hard-redirect.service.spec.ts
@@ -2,7 +2,7 @@ import { TestBed } from '@angular/core/testing';
import { BrowserHardRedirectService } from './browser-hard-redirect.service';
describe('BrowserHardRedirectService', () => {
- const origin = 'test origin';
+ const origin = 'https://test-host.com:4000';
const mockLocation = {
href: undefined,
pathname: '/pathname',
@@ -43,7 +43,7 @@ describe('BrowserHardRedirectService', () => {
describe('when requesting the origin', () => {
it('should return the location origin', () => {
- expect(service.getRequestOrigin()).toEqual(origin);
+ expect(service.getCurrentOrigin()).toEqual(origin);
});
});
diff --git a/src/app/core/services/browser-hard-redirect.service.ts b/src/app/core/services/browser-hard-redirect.service.ts
index 5f90fb8dbd..eeb9006039 100644
--- a/src/app/core/services/browser-hard-redirect.service.ts
+++ b/src/app/core/services/browser-hard-redirect.service.ts
@@ -28,16 +28,20 @@ export class BrowserHardRedirectService extends HardRedirectService {
}
/**
- * Get the origin of a request
+ * Get the current route, with query params included
+ * e.g. /search?page=1&query=open%20access&f.dateIssued.min=1980&f.dateIssued.max=2020
*/
- getCurrentRoute() {
+ getCurrentRoute(): string {
return this.location.pathname + this.location.search;
}
/**
- * Get the hostname of the request
+ * Get the origin of the current URL
+ * i.e. "://" [ ":" ]
+ * e.g. if the URL is https://demo7.dspace.org/search?query=test,
+ * the origin would be https://demo7.dspace.org
*/
- getRequestOrigin() {
+ getCurrentOrigin(): string {
return this.location.origin;
}
}
diff --git a/src/app/core/services/hard-redirect.service.spec.ts b/src/app/core/services/hard-redirect.service.spec.ts
deleted file mode 100644
index f060143e65..0000000000
--- a/src/app/core/services/hard-redirect.service.spec.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-import { HardRedirectService } from './hard-redirect.service';
-import { environment } from '../../../environments/environment';
-import { TestBed } from '@angular/core/testing';
-import { Injectable } from '@angular/core';
-
-const requestOrigin = 'http://dspace-angular-ui.dspace.com';
-
-describe('HardRedirectService', () => {
- let service: TestHardRedirectService;
-
- beforeEach(() => {
- TestBed.configureTestingModule({ providers: [TestHardRedirectService] });
- service = TestBed.inject(TestHardRedirectService);
- });
-
- describe('when calling rewriteDownloadURL', () => {
- let originalValue;
- const relativePath = '/test/url/path';
- const testURL = environment.rest.baseUrl + relativePath;
- beforeEach(() => {
- originalValue = environment.rewriteDownloadUrls;
- });
-
- it('it should return the same url when rewriteDownloadURL is false', () => {
- environment.rewriteDownloadUrls = false;
- expect(service.rewriteDownloadURL(testURL)).toEqual(testURL);
- });
-
- it('it should replace part of the url when rewriteDownloadURL is true', () => {
- environment.rewriteDownloadUrls = true;
- expect(service.rewriteDownloadURL(testURL)).toEqual(requestOrigin + environment.rest.nameSpace + relativePath);
- });
-
- afterEach(() => {
- environment.rewriteDownloadUrls = originalValue;
- });
- });
-});
-
-@Injectable()
-class TestHardRedirectService extends HardRedirectService {
- constructor() {
- super();
- }
-
- redirect(url: string) {
- return undefined;
- }
-
- getCurrentRoute() {
- return undefined;
- }
-
- getRequestOrigin() {
- return requestOrigin;
- }
-}
diff --git a/src/app/core/services/hard-redirect.service.ts b/src/app/core/services/hard-redirect.service.ts
index a09521dae5..3733059283 100644
--- a/src/app/core/services/hard-redirect.service.ts
+++ b/src/app/core/services/hard-redirect.service.ts
@@ -1,6 +1,4 @@
import { Injectable } from '@angular/core';
-import { environment } from '../../../environments/environment';
-import { URLCombiner } from '../url-combiner/url-combiner';
/**
* Service to take care of hard redirects
@@ -20,21 +18,13 @@ export abstract class HardRedirectService {
* Get the current route, with query params included
* e.g. /search?page=1&query=open%20access&f.dateIssued.min=1980&f.dateIssued.max=2020
*/
- abstract getCurrentRoute();
+ abstract getCurrentRoute(): string;
/**
- * Get the hostname of the request
+ * Get the origin of the current URL
+ * i.e. "://" [ ":" ]
+ * e.g. if the URL is https://demo7.dspace.org/search?query=test,
+ * the origin would be https://demo7.dspace.org
*/
- abstract getRequestOrigin();
-
- public rewriteDownloadURL(originalUrl: string): string {
- if (environment.rewriteDownloadUrls) {
- const hostName = this.getRequestOrigin();
- const namespace = environment.rest.nameSpace;
- const rewrittenUrl = new URLCombiner(hostName, namespace).toString();
- return originalUrl.replace(environment.rest.baseUrl, rewrittenUrl);
- } else {
- return originalUrl;
- }
- }
+ abstract getCurrentOrigin(): string;
}
diff --git a/src/app/core/services/server-hard-redirect.service.spec.ts b/src/app/core/services/server-hard-redirect.service.spec.ts
index 892f4f4087..4501547b92 100644
--- a/src/app/core/services/server-hard-redirect.service.spec.ts
+++ b/src/app/core/services/server-hard-redirect.service.spec.ts
@@ -7,11 +7,12 @@ describe('ServerHardRedirectService', () => {
const mockResponse = jasmine.createSpyObj(['redirect', 'end']);
const service: ServerHardRedirectService = new ServerHardRedirectService(mockRequest, mockResponse);
- const origin = 'test-host';
+ const origin = 'https://test-host.com:4000';
beforeEach(() => {
+ mockRequest.protocol = 'https';
mockRequest.headers = {
- host: 'test-host',
+ host: 'test-host.com:4000',
};
TestBed.configureTestingModule({});
@@ -49,7 +50,7 @@ describe('ServerHardRedirectService', () => {
describe('when requesting the origin', () => {
it('should return the location origin', () => {
- expect(service.getRequestOrigin()).toEqual(origin);
+ expect(service.getCurrentOrigin()).toEqual(origin);
});
});
diff --git a/src/app/core/services/server-hard-redirect.service.ts b/src/app/core/services/server-hard-redirect.service.ts
index 65b404ca6c..94b9ed6198 100644
--- a/src/app/core/services/server-hard-redirect.service.ts
+++ b/src/app/core/services/server-hard-redirect.service.ts
@@ -55,16 +55,19 @@ export class ServerHardRedirectService extends HardRedirectService {
}
/**
- * Get the origin of a request
+ * Get the URL of the current route
*/
- getCurrentRoute() {
+ getCurrentRoute(): string {
return this.req.originalUrl;
}
/**
- * Get the hostname of the request
+ * Get the origin of the current URL
+ * i.e. "://" [ ":" ]
+ * e.g. if the URL is https://demo7.dspace.org/search?query=test,
+ * the origin would be https://demo7.dspace.org
*/
- getRequestOrigin() {
- return this.req.headers.host;
+ getCurrentOrigin(): string {
+ return this.req.protocol + '://' + this.req.headers.host;
}
}
diff --git a/src/app/core/shared/bitstream.model.ts b/src/app/core/shared/bitstream.model.ts
index 314818b482..baf2f82635 100644
--- a/src/app/core/shared/bitstream.model.ts
+++ b/src/app/core/shared/bitstream.model.ts
@@ -43,13 +43,14 @@ export class Bitstream extends DSpaceObject implements HALResource {
bundle: HALLink;
format: HALLink;
content: HALLink;
+ thumbnail: HALLink;
};
/**
* The thumbnail for this Bitstream
- * Needs to be resolved first, but isn't available as a {@link HALLink} yet
- * Use BitstreamDataService.getThumbnailFor(…) for now.
+ * Will be undefined unless the thumbnail {@link HALLink} has been resolved.
*/
+ @link(BITSTREAM, false, 'thumbnail')
thumbnail?: Observable>;
/**
diff --git a/src/app/core/shared/dspace-object.model.ts b/src/app/core/shared/dspace-object.model.ts
index 9d1fba4f86..5ea2bced3d 100644
--- a/src/app/core/shared/dspace-object.model.ts
+++ b/src/app/core/shared/dspace-object.model.ts
@@ -163,8 +163,8 @@ export class DSpaceObject extends ListableObject implements CacheableObject {
* Find metadata on a specific field and order all of them using their "place" property.
* @param key
*/
- findMetadataSortedByPlace(key: string): MetadataValue[] {
- return this.allMetadata([key]).sort((a: MetadataValue, b: MetadataValue) => {
+ findMetadataSortedByPlace(keyOrKeys: string | string[]): MetadataValue[] {
+ return this.allMetadata(keyOrKeys).sort((a: MetadataValue, b: MetadataValue) => {
if (hasNoValue(a.place) && hasNoValue(b.place)) {
return 0;
}
diff --git a/src/app/core/shared/item.model.ts b/src/app/core/shared/item.model.ts
index 53eb5e3ce2..d98c22225e 100644
--- a/src/app/core/shared/item.model.ts
+++ b/src/app/core/shared/item.model.ts
@@ -1,4 +1,4 @@
-import { autoserialize, autoserializeAs, deserialize, inheritSerialization } from 'cerialize';
+import { autoserialize, autoserializeAs, deserialize, deserializeAs, inheritSerialization } from 'cerialize';
import { Observable } from 'rxjs';
import { isEmpty } from '../../shared/empty.util';
import { ListableObject } from '../../shared/object-collection/shared/listable-object.model';
@@ -19,6 +19,8 @@ import { ITEM } from './item.resource-type';
import { ChildHALResource } from './child-hal-resource.model';
import { Version } from './version.model';
import { VERSION } from './version.resource-type';
+import { BITSTREAM } from './bitstream.resource-type';
+import { Bitstream } from './bitstream.model';
/**
* Class representing a DSpace Item
@@ -37,7 +39,7 @@ export class Item extends DSpaceObject implements ChildHALResource {
/**
* The Date of the last modification of this Item
*/
- @deserialize
+ @deserializeAs(Date)
lastModified: Date;
/**
@@ -69,6 +71,7 @@ export class Item extends DSpaceObject implements ChildHALResource {
owningCollection: HALLink;
templateItemOf: HALLink;
version: HALLink;
+ thumbnail: HALLink;
self: HALLink;
};
@@ -100,6 +103,13 @@ export class Item extends DSpaceObject implements ChildHALResource {
@link(RELATIONSHIP, true)
relationships?: Observable>>;
+ /**
+ * The thumbnail for this Item
+ * Will be undefined unless the thumbnail {@link HALLink} has been resolved.
+ */
+ @link(BITSTREAM, false, 'thumbnail')
+ thumbnail?: Observable>;
+
/**
* Method that returns as which type of object this object should be rendered
*/
diff --git a/src/app/core/shared/operators.spec.ts b/src/app/core/shared/operators.spec.ts
index 780487eade..6fd15ceacc 100644
--- a/src/app/core/shared/operators.spec.ts
+++ b/src/app/core/shared/operators.spec.ts
@@ -15,7 +15,12 @@ import {
redirectOn4xx
} from './operators';
import { of as observableOf } from 'rxjs';
-import { createFailedRemoteDataObject, createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
+import {
+ createFailedRemoteDataObject,
+ createSuccessfulRemoteDataObject
+} from '../../shared/remote-data.utils';
+
+// tslint:disable:no-shadowed-variable
describe('Core Module - RxJS Operators', () => {
let scheduler: TestScheduler;
@@ -172,8 +177,12 @@ describe('Core Module - RxJS Operators', () => {
describe('redirectOn4xx', () => {
let router;
let authService;
+ let testScheduler;
beforeEach(() => {
+ testScheduler = new TestScheduler((actual, expected) => {
+ expect(actual).toEqual(expected);
+ });
router = jasmine.createSpyObj('router', ['navigateByUrl']);
authService = jasmine.createSpyObj('authService', {
isAuthenticated: observableOf(true),
@@ -181,32 +190,69 @@ describe('Core Module - RxJS Operators', () => {
});
});
- it('should call navigateByUrl to a 404 page, when the remote data contains a 404 error', () => {
- const testRD = createFailedRemoteDataObject('Object was not found', 404);
+ it('should call navigateByUrl to a 404 page, when the remote data contains a 404 error, and not emit anything', () => {
+ testScheduler.run(({ cold, expectObservable, flush }) => {
+ const testRD = createFailedRemoteDataObject('Object was not found', 404);
+ const source = cold('a', { a: testRD });
+ const expected = '-';
+ const values = {};
- observableOf(testRD).pipe(redirectOn4xx(router, authService)).subscribe();
- expect(router.navigateByUrl).toHaveBeenCalledWith('/404', { skipLocationChange: true });
+ expectObservable(source.pipe(redirectOn4xx(router, authService))).toBe(expected, values);
+ flush();
+ expect(router.navigateByUrl).toHaveBeenCalledWith('/404', { skipLocationChange: true });
+ });
});
- it('should call navigateByUrl to a 401 page, when the remote data contains a 403 error', () => {
- const testRD = createFailedRemoteDataObject('Forbidden', 403);
+ it('should call navigateByUrl to a 404 page, when the remote data contains a 422 error, and not emit anything', () => {
+ testScheduler.run(({ cold, expectObservable, flush }) => {
+ const testRD = createFailedRemoteDataObject('Unprocessable Entity', 422);
+ const source = cold('a', { a: testRD });
+ const expected = '-';
+ const values = {};
- observableOf(testRD).pipe(redirectOn4xx(router, authService)).subscribe();
- expect(router.navigateByUrl).toHaveBeenCalledWith('/403', { skipLocationChange: true });
+ expectObservable(source.pipe(redirectOn4xx(router, authService))).toBe(expected, values);
+ flush();
+ expect(router.navigateByUrl).toHaveBeenCalledWith('/404', { skipLocationChange: true });
+ });
});
- it('should not call navigateByUrl to a 404, 403 or 401 page, when the remote data contains another error than a 404, 403 or 401', () => {
- const testRD = createFailedRemoteDataObject('Something went wrong', 500);
+ it('should call navigateByUrl to a 401 page, when the remote data contains a 403 error, and not emit anything', () => {
+ testScheduler.run(({ cold, expectObservable, flush }) => {
+ const testRD = createFailedRemoteDataObject('Forbidden', 403);
+ const source = cold('a', { a: testRD });
+ const expected = '-';
+ const values = {};
- observableOf(testRD).pipe(redirectOn4xx(router, authService)).subscribe();
- expect(router.navigateByUrl).not.toHaveBeenCalled();
+ expectObservable(source.pipe(redirectOn4xx(router, authService))).toBe(expected, values);
+ flush();
+ expect(router.navigateByUrl).toHaveBeenCalledWith('/403', { skipLocationChange: true });
+ });
});
- it('should not call navigateByUrl to a 404, 403 or 401 page, when the remote data contains no error', () => {
- const testRD = createSuccessfulRemoteDataObject(undefined);
+ it('should not call navigateByUrl to a 404, 403 or 401 page, when the remote data contains another error than a 404, 422, 403 or 401, and emit the source rd', () => {
+ testScheduler.run(({ cold, expectObservable, flush }) => {
+ const testRD = createFailedRemoteDataObject('Something went wrong', 500);
+ const source = cold('a', { a: testRD });
+ const expected = 'a';
+ const values = { a: testRD };
- observableOf(testRD).pipe(redirectOn4xx(router, authService)).subscribe();
- expect(router.navigateByUrl).not.toHaveBeenCalled();
+ expectObservable(source.pipe(redirectOn4xx(router, authService))).toBe(expected, values);
+ flush();
+ expect(router.navigateByUrl).not.toHaveBeenCalled();
+ });
+ });
+
+ it('should not call navigateByUrl to a 404, 403 or 401 page, when the remote data contains no error, and emit the source rd', () => {
+ testScheduler.run(({ cold, expectObservable, flush }) => {
+ const testRD = createSuccessfulRemoteDataObject(undefined);
+ const source = cold('a', { a: testRD });
+ const expected = 'a';
+ const values = { a: testRD };
+
+ expectObservable(source.pipe(redirectOn4xx(router, authService))).toBe(expected, values);
+ flush();
+ expect(router.navigateByUrl).not.toHaveBeenCalled();
+ });
});
describe('when the user is not authenticated', () => {
@@ -214,20 +260,32 @@ describe('Core Module - RxJS Operators', () => {
(authService.isAuthenticated as jasmine.Spy).and.returnValue(observableOf(false));
});
- it('should set the redirect url and navigate to login when the remote data contains a 401 error', () => {
- const testRD = createFailedRemoteDataObject('The current user is unauthorized', 401);
+ it('should set the redirect url and navigate to login when the remote data contains a 401 error, and not emit anything', () => {
+ testScheduler.run(({ cold, expectObservable, flush }) => {
+ const testRD = createFailedRemoteDataObject('The current user is unauthorized', 401);
+ const source = cold('a', { a: testRD });
+ const expected = '-';
+ const values = {};
- observableOf(testRD).pipe(redirectOn4xx(router, authService)).subscribe();
- expect(authService.setRedirectUrl).toHaveBeenCalled();
- expect(router.navigateByUrl).toHaveBeenCalledWith('login');
+ expectObservable(source.pipe(redirectOn4xx(router, authService))).toBe(expected, values);
+ flush();
+ expect(authService.setRedirectUrl).toHaveBeenCalled();
+ expect(router.navigateByUrl).toHaveBeenCalledWith('login');
+ });
});
- it('should set the redirect url and navigate to login when the remote data contains a 403 error', () => {
- const testRD = createFailedRemoteDataObject('Forbidden', 403);
+ it('should set the redirect url and navigate to login when the remote data contains a 403 error, and not emit anything', () => {
+ testScheduler.run(({ cold, expectObservable, flush }) => {
+ const testRD = createFailedRemoteDataObject('Forbidden', 403);
+ const source = cold('a', { a: testRD });
+ const expected = '-';
+ const values = {};
- observableOf(testRD).pipe(redirectOn4xx(router, authService)).subscribe();
- expect(authService.setRedirectUrl).toHaveBeenCalled();
- expect(router.navigateByUrl).toHaveBeenCalledWith('login');
+ expectObservable(source.pipe(redirectOn4xx(router, authService))).toBe(expected, values);
+ flush();
+ expect(authService.setRedirectUrl).toHaveBeenCalled();
+ expect(router.navigateByUrl).toHaveBeenCalledWith('login');
+ });
});
});
});
diff --git a/src/app/core/shared/operators.ts b/src/app/core/shared/operators.ts
index 3128538ea9..3be04447ab 100644
--- a/src/app/core/shared/operators.ts
+++ b/src/app/core/shared/operators.ts
@@ -1,6 +1,17 @@
import { Router, UrlTree } from '@angular/router';
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
-import { filter, find, map, mergeMap, switchMap, take, takeWhile, tap } from 'rxjs/operators';
+import {
+ debounceTime,
+ filter,
+ find,
+ map,
+ mergeMap,
+ switchMap,
+ take,
+ takeWhile,
+ tap,
+ withLatestFrom
+} from 'rxjs/operators';
import { hasNoValue, hasValue, hasValueOperator, isNotEmpty } from '../../shared/empty.util';
import { SearchResult } from '../../shared/search/search-result.model';
import { PaginatedList } from '../data/paginated-list.model';
@@ -15,6 +26,17 @@ import { DSpaceObject } from './dspace-object.model';
import { getForbiddenRoute, getPageNotFoundRoute } from '../../app-routing-paths';
import { getEndUserAgreementPath } from '../../info/info-routing-paths';
import { AuthService } from '../auth/auth.service';
+import { InjectionToken } from '@angular/core';
+
+export const DEBOUNCE_TIME_OPERATOR = new InjectionToken<(dueTime: number) => (source: Observable) => Observable>('debounceTime', {
+ providedIn: 'root',
+ factory: () => debounceTime
+});
+
+export const REDIRECT_ON_4XX = new InjectionToken<(router: Router, authService: AuthService) => (source: Observable>) => Observable>>('redirectOn4xx', {
+ providedIn: 'root',
+ factory: () => redirectOn4xx
+});
/**
* This file contains custom RxJS operators that can be used in multiple places
@@ -169,29 +191,37 @@ export const getAllSucceededRemoteListPayload = () =>
);
/**
- * Operator that checks if a remote data object returned a 401 or 404 error
- * When it does contain such an error, it will redirect the user to the related error page, without altering the current URL
+ * Operator that checks if a remote data object returned a 4xx error
+ * When it does contain such an error, it will redirect the user to the related error page, without
+ * altering the current URL
+ *
* @param router The router used to navigate to a new page
* @param authService Service to check if the user is authenticated
*/
export const redirectOn4xx = (router: Router, authService: AuthService) =>
(source: Observable>): Observable> =>
- observableCombineLatest(source, authService.isAuthenticated()).pipe(
- map(([rd, isAuthenticated]: [RemoteData, boolean]) => {
+ source.pipe(
+ withLatestFrom(authService.isAuthenticated()),
+ filter(([rd, isAuthenticated]: [RemoteData, boolean]) => {
if (rd.hasFailed) {
- if (rd.statusCode === 404) {
- router.navigateByUrl(getPageNotFoundRoute(), {skipLocationChange: true});
+ if (rd.statusCode === 404 || rd.statusCode === 422) {
+ router.navigateByUrl(getPageNotFoundRoute(), { skipLocationChange: true });
+ return false;
} else if (rd.statusCode === 403 || rd.statusCode === 401) {
if (isAuthenticated) {
- router.navigateByUrl(getForbiddenRoute(), {skipLocationChange: true});
+ router.navigateByUrl(getForbiddenRoute(), { skipLocationChange: true });
+ return false;
} else {
authService.setRedirectUrl(router.url);
router.navigateByUrl('login');
+ return false;
}
}
}
- return rd;
- }));
+ return true;
+ }),
+ map(([rd,]: [RemoteData, boolean]) => rd)
+ );
/**
* Operator that returns a UrlTree to a forbidden page or the login page when the boolean received is false
@@ -201,10 +231,23 @@ export const redirectOn4xx = (router: Router, authService: AuthService) =>
*/
export const returnForbiddenUrlTreeOrLoginOnFalse = (router: Router, authService: AuthService, redirectUrl: string) =>
(source: Observable): Observable =>
+ source.pipe(
+ map((authorized) => [authorized]),
+ returnForbiddenUrlTreeOrLoginOnAllFalse(router, authService, redirectUrl),
+ );
+
+/**
+ * Operator that returns a UrlTree to a forbidden page or the login page when the booleans received are all false
+ * @param router The router used to navigate to a forbidden page
+ * @param authService The AuthService used to determine whether or not the user is logged in
+ * @param redirectUrl The URL to redirect back to after logging in
+ */
+export const returnForbiddenUrlTreeOrLoginOnAllFalse = (router: Router, authService: AuthService, redirectUrl: string) =>
+ (source: Observable): Observable =>
observableCombineLatest(source, authService.isAuthenticated()).pipe(
- map(([authorized, authenticated]: [boolean, boolean]) => {
- if (authorized) {
- return authorized;
+ map(([authorizedList, authenticated]: [boolean[], boolean]) => {
+ if (authorizedList.some((b: boolean) => b === true)) {
+ return true;
} else {
if (authenticated) {
return router.parseUrl(getForbiddenRoute());
diff --git a/src/app/core/shared/search/search-configuration.service.spec.ts b/src/app/core/shared/search/search-configuration.service.spec.ts
index 061182c2fc..805ecd0486 100644
--- a/src/app/core/shared/search/search-configuration.service.spec.ts
+++ b/src/app/core/shared/search/search-configuration.service.spec.ts
@@ -23,7 +23,10 @@ describe('SearchConfigurationService', () => {
scope: ''
});
- const backendFilters = [new SearchFilter('f.author', ['another value']), new SearchFilter('f.date', ['[2013 TO 2018]'])];
+ const backendFilters = [
+ new SearchFilter('f.author', ['another value']),
+ new SearchFilter('f.date', ['[2013 TO 2018]'], 'equals')
+ ];
const routeService = jasmine.createSpyObj('RouteService', {
getQueryParameterValue: observableOf(value1),
diff --git a/src/app/core/shared/search/search-configuration.service.ts b/src/app/core/shared/search/search-configuration.service.ts
index 798a0de287..74af230810 100644
--- a/src/app/core/shared/search/search-configuration.service.ts
+++ b/src/app/core/shared/search/search-configuration.service.ts
@@ -1,8 +1,15 @@
import { Injectable, OnDestroy } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
-import { BehaviorSubject, combineLatest as observableCombineLatest, merge as observableMerge, Observable, Subscription } from 'rxjs';
-import { filter, map, startWith } from 'rxjs/operators';
+import {
+ BehaviorSubject,
+ combineLatest,
+ combineLatest as observableCombineLatest,
+ merge as observableMerge,
+ Observable,
+ Subscription
+} from 'rxjs';
+import { distinctUntilChanged, filter, map, startWith, switchMap, take } from 'rxjs/operators';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
import { SearchOptions } from '../../../shared/search/search-options.model';
import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model';
@@ -11,9 +18,15 @@ import { RemoteData } from '../../data/remote-data';
import { DSpaceObjectType } from '../dspace-object-type.model';
import { SortDirection, SortOptions } from '../../cache/models/sort-options.model';
import { RouteService } from '../../services/route.service';
-import { getFirstSucceededRemoteData } from '../operators';
+import {
+ getAllSucceededRemoteDataPayload,
+ getFirstSucceededRemoteData
+} from '../operators';
import { hasNoValue, hasValue, isNotEmpty, isNotEmptyOperator } from '../../../shared/empty.util';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
+import { SearchConfig } from './search-filters/search-config.model';
+import { SearchService } from './search.service';
+import { of } from 'rxjs/internal/observable/of';
import { PaginationService } from '../../pagination/pagination.service';
/**
@@ -168,7 +181,7 @@ export class SearchConfigurationService implements OnDestroy {
if (hasNoValue(filters.find((f) => f.key === realKey))) {
const min = filterParams[realKey + '.min'] ? filterParams[realKey + '.min'][0] : '*';
const max = filterParams[realKey + '.max'] ? filterParams[realKey + '.max'][0] : '*';
- filters.push(new SearchFilter(realKey, ['[' + min + ' TO ' + max + ']']));
+ filters.push(new SearchFilter(realKey, ['[' + min + ' TO ' + max + ']'], 'equals'));
}
} else {
filters.push(new SearchFilter(key, filterParams[key]));
@@ -194,6 +207,60 @@ export class SearchConfigurationService implements OnDestroy {
return this.routeService.getQueryParamsWithPrefix('f.');
}
+ /**
+ * Creates an observable of SearchConfig every time the configuration$ stream emits.
+ * @param configuration$
+ * @param service
+ */
+ getConfigurationSearchConfigObservable(configuration$: Observable, service: SearchService): Observable {
+ return configuration$.pipe(
+ distinctUntilChanged(),
+ switchMap((configuration) => service.getSearchConfigurationFor(null, configuration)),
+ getAllSucceededRemoteDataPayload());
+ }
+
+ /**
+ * Every time searchConfig change (after a configuration change) it update the navigation with the default sort option
+ * and emit the new paginateSearchOptions value.
+ * @param configuration$
+ * @param service
+ */
+ initializeSortOptionsFromConfiguration(searchConfig$: Observable) {
+ const subscription = searchConfig$.pipe(switchMap((searchConfig) => combineLatest([
+ of(searchConfig),
+ this.paginatedSearchOptions.pipe(take(1))
+ ]))).subscribe(([searchConfig, searchOptions]) => {
+ const field = searchConfig.sortOptions[0].name;
+ const direction = searchConfig.sortOptions[0].sortOrder.toLowerCase() === SortDirection.ASC.toLowerCase() ? SortDirection.ASC : SortDirection.DESC;
+ const updateValue = Object.assign(new PaginatedSearchOptions({}), searchOptions, {
+ sort: new SortOptions(field, direction)
+ });
+ this.paginationService.updateRoute(this.paginationID,
+ {
+ sortDirection: updateValue.sort.direction,
+ sortField: updateValue.sort.field,
+ });
+ this.paginatedSearchOptions.next(updateValue);
+ });
+ this.subs.push(subscription);
+ }
+
+ /**
+ * Creates an observable of available SortOptions[] every time the searchConfig$ stream emits.
+ * @param searchConfig$
+ * @param service
+ */
+ getConfigurationSortOptionsObservable(searchConfig$: Observable): Observable {
+ return searchConfig$.pipe(map((searchConfig) => {
+ const sortOptions = [];
+ searchConfig.sortOptions.forEach(sortOption => {
+ sortOptions.push(new SortOptions(sortOption.name, SortDirection.ASC));
+ sortOptions.push(new SortOptions(sortOption.name, SortDirection.DESC));
+ });
+ return sortOptions;
+ }));
+ }
+
/**
* Sets up a subscription to all necessary parameters to make sure the searchOptions emits a new value every time they update
* @param {SearchOptions} defaults Default values for when no parameters are available
diff --git a/src/app/core/shared/search/search-filters/search-config.model.ts b/src/app/core/shared/search/search-filters/search-config.model.ts
new file mode 100644
index 0000000000..725761fe7b
--- /dev/null
+++ b/src/app/core/shared/search/search-filters/search-config.model.ts
@@ -0,0 +1,76 @@
+import { autoserialize, deserialize } from 'cerialize';
+
+import { SEARCH_CONFIG } from './search-config.resource-type';
+import { typedObject } from '../../../cache/builders/build-decorators';
+import { CacheableObject } from '../../../cache/object-cache.reducer';
+import { HALLink } from '../../hal-link.model';
+import { ResourceType } from '../../resource-type';
+
+/**
+ * The configuration for a search
+ */
+@typedObject
+export class SearchConfig implements CacheableObject {
+ static type = SEARCH_CONFIG;
+
+ /**
+ * The id of this search configuration.
+ */
+ @autoserialize
+ id: string;
+
+ /**
+ * The configured filters.
+ */
+ @autoserialize
+ filters: FilterConfig[];
+
+ /**
+ * The configured sort options.
+ */
+ @autoserialize
+ sortOptions: SortOption[];
+
+ /**
+ * The object type.
+ */
+ @autoserialize
+ type: ResourceType;
+
+ /**
+ * The {@link HALLink}s for this Item
+ */
+ @deserialize
+ _links: {
+ facets: HALLink;
+ objects: HALLink;
+ self: HALLink;
+ };
+}
+
+/**
+ * Interface to model filter's configuration.
+ */
+export interface FilterConfig {
+ filter: string;
+ hasFacets: boolean;
+ operators: OperatorConfig[];
+ openByDefault: boolean;
+ pageSize: number;
+ type: string;
+}
+
+/**
+ * Interface to model sort option's configuration.
+ */
+export interface SortOption {
+ name: string;
+ sortOrder: string;
+}
+
+/**
+ * Interface to model operator's configuration.
+ */
+export interface OperatorConfig {
+ operator: string;
+}
diff --git a/src/app/core/shared/search/search-filters/search-config.resource-type.ts b/src/app/core/shared/search/search-filters/search-config.resource-type.ts
new file mode 100644
index 0000000000..967a654006
--- /dev/null
+++ b/src/app/core/shared/search/search-filters/search-config.resource-type.ts
@@ -0,0 +1,9 @@
+import {ResourceType} from '../../resource-type';
+
+/**
+ * The resource type for SearchConfig
+ *
+ * Needs to be in a separate file to prevent circular
+ * dependencies in webpack.
+ */
+export const SEARCH_CONFIG = new ResourceType('discover');
diff --git a/src/app/core/shared/search/search.service.spec.ts b/src/app/core/shared/search/search.service.spec.ts
index 60cb0a87b9..00f10230c3 100644
--- a/src/app/core/shared/search/search.service.spec.ts
+++ b/src/app/core/shared/search/search.service.spec.ts
@@ -240,5 +240,55 @@ describe('SearchService', () => {
expect((searchService as any).requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({ href: requestUrl }), true);
});
});
+
+ describe('when getSearchConfigurationFor is called without a scope', () => {
+ const endPoint = 'http://endpoint.com/test/config';
+ beforeEach(() => {
+ spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(observableOf(endPoint));
+ spyOn((searchService as any).rdb, 'buildFromHref').and.callThrough();
+ /* tslint:disable:no-empty */
+ searchService.getSearchConfigurationFor(null).subscribe((t) => {
+ }); // subscribe to make sure all methods are called
+ /* tslint:enable:no-empty */
+ });
+
+ it('should call getEndpoint on the halService', () => {
+ expect((searchService as any).halService.getEndpoint).toHaveBeenCalled();
+ });
+
+ it('should send out the request on the request service', () => {
+ expect((searchService as any).requestService.send).toHaveBeenCalled();
+ });
+
+ it('should call send containing a request with the correct request url', () => {
+ expect((searchService as any).requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({ href: endPoint }), true);
+ });
+ });
+
+ describe('when getSearchConfigurationFor is called with a scope', () => {
+ const endPoint = 'http://endpoint.com/test/config';
+ const scope = 'test';
+ const requestUrl = endPoint + '?scope=' + scope;
+ beforeEach(() => {
+ spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(observableOf(endPoint));
+ /* tslint:disable:no-empty */
+ searchService.getSearchConfigurationFor(scope).subscribe((t) => {
+ }); // subscribe to make sure all methods are called
+ /* tslint:enable:no-empty */
+ });
+
+ it('should call getEndpoint on the halService', () => {
+ expect((searchService as any).halService.getEndpoint).toHaveBeenCalled();
+ });
+
+ it('should send out the request on the request service', () => {
+ expect((searchService as any).requestService.send).toHaveBeenCalled();
+ });
+
+ it('should call send containing a request with the correct request url', () => {
+ expect((searchService as any).requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({ href: requestUrl }), true);
+ });
+ });
+
});
});
diff --git a/src/app/core/shared/search/search.service.ts b/src/app/core/shared/search/search.service.ts
index 054bde4c08..75723366bc 100644
--- a/src/app/core/shared/search/search.service.ts
+++ b/src/app/core/shared/search/search.service.ts
@@ -37,15 +37,59 @@ import { ListableObject } from '../../../shared/object-collection/shared/listabl
import { getSearchResultFor } from '../../../shared/search/search-result-element-decorator';
import { FacetConfigResponse } from '../../../shared/search/facet-config-response.model';
import { FacetValues } from '../../../shared/search/facet-values.model';
+import { SearchConfig } from './search-filters/search-config.model';
import { PaginationService } from '../../pagination/pagination.service';
import { SearchConfigurationService } from './search-configuration.service';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
+import { DataService } from '../../data/data.service';
+import { Store } from '@ngrx/store';
+import { CoreState } from '../../core.reducers';
+import { ObjectCacheService } from '../../cache/object-cache.service';
+import { NotificationsService } from '../../../shared/notifications/notifications.service';
+import { HttpClient } from '@angular/common/http';
+import { DSOChangeAnalyzer } from '../../data/dso-change-analyzer.service';
+
+/* tslint:disable:max-classes-per-file */
+/**
+ * A class that lets us delegate some methods to DataService
+ */
+class DataServiceImpl extends DataService {
+ protected linkPath = 'discover';
+
+ constructor(
+ protected requestService: RequestService,
+ protected rdbService: RemoteDataBuildService,
+ protected store: Store,
+ protected objectCache: ObjectCacheService,
+ protected halService: HALEndpointService,
+ protected notificationsService: NotificationsService,
+ protected http: HttpClient,
+ protected comparator: DSOChangeAnalyzer) {
+ super();
+ }
+
+ /**
+ * Adds the embed options to the link for the request
+ * @param href The href the params are to be added to
+ * @param args params for the query string
+ * @param linksToFollow links we want to embed in query string if shouldEmbed is true
+ */
+ public addEmbedParams(href: string, args: string[], ...linksToFollow: FollowLinkConfig[]) {
+ return super.addEmbedParams(href, args, ...linksToFollow);
+ }
+}
/**
* Service that performs all general actions that have to do with the search page
*/
@Injectable()
export class SearchService implements OnDestroy {
+
+ /**
+ * Endpoint link path for retrieving search configurations
+ */
+ private configurationLinkPath = 'discover/search';
+
/**
* Endpoint link path for retrieving general search results
*/
@@ -71,6 +115,11 @@ export class SearchService implements OnDestroy {
*/
private sub;
+ /**
+ * Instance of DataServiceImpl that lets us delegate some methods to DataService
+ */
+ private searchDataService: DataServiceImpl;
+
constructor(private router: Router,
private routeService: RouteService,
protected requestService: RequestService,
@@ -82,6 +131,16 @@ export class SearchService implements OnDestroy {
private paginationService: PaginationService,
private searchConfigurationService: SearchConfigurationService
) {
+ this.searchDataService = new DataServiceImpl(
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined
+ );
}
/**
@@ -124,7 +183,17 @@ export class SearchService implements OnDestroy {
search(searchOptions?: PaginatedSearchOptions, responseMsToLive?: number, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> {
const href$ = this.getEndpoint(searchOptions);
- href$.pipe(take(1)).subscribe((url: string) => {
+ href$.pipe(
+ take(1),
+ map((href: string) => {
+ const args = this.searchDataService.addEmbedParams(href, [], ...linksToFollow);
+ if (isNotEmpty(args)) {
+ return new URLCombiner(href, `?${args.join('&')}`).toString();
+ } else {
+ return href;
+ }
+ })
+ ).subscribe((url: string) => {
const request = new this.request(this.requestService.generateRequestId(), url);
const getResponseParserFn: () => GenericConstructor = () => {
@@ -145,7 +214,7 @@ export class SearchService implements OnDestroy {
);
return this.directlyAttachIndexableObjects(sqr$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
- }
+ }
/**
* Method to retrieve request entries for search results from the server
@@ -229,6 +298,24 @@ export class SearchService implements OnDestroy {
);
}
+ private getConfigUrl(url: string, scope?: string, configurationName?: string) {
+ const args: string[] = [];
+
+ if (isNotEmpty(scope)) {
+ args.push(`scope=${scope}`);
+ }
+
+ if (isNotEmpty(configurationName)) {
+ args.push(`configuration=${configurationName}`);
+ }
+
+ if (isNotEmpty(args)) {
+ url = new URLCombiner(url, `?${args.join('&')}`).toString();
+ }
+
+ return url;
+ }
+
/**
* Request the filter configuration for a given scope or the whole repository
* @param {string} scope UUID of the object for which config the filter config is requested, when no scope is provided the configuration for the whole repository is loaded
@@ -237,33 +324,17 @@ export class SearchService implements OnDestroy {
*/
getConfig(scope?: string, configurationName?: string): Observable> {
const href$ = this.halService.getEndpoint(this.facetLinkPathPrefix).pipe(
- map((url: string) => {
- const args: string[] = [];
-
- if (isNotEmpty(scope)) {
- args.push(`scope=${scope}`);
- }
-
- if (isNotEmpty(configurationName)) {
- args.push(`configuration=${configurationName}`);
- }
-
- if (isNotEmpty(args)) {
- url = new URLCombiner(url, `?${args.join('&')}`).toString();
- }
-
- return url;
- }),
+ map((url: string) => this.getConfigUrl(url, scope, configurationName)),
);
href$.pipe(take(1)).subscribe((url: string) => {
- let request = new this.request(this.requestService.generateRequestId(), url);
- request = Object.assign(request, {
- getResponseParser(): GenericConstructor {
- return FacetConfigResponseParsingService;
- }
- });
- this.requestService.send(request, true);
+ let request = new this.request(this.requestService.generateRequestId(), url);
+ request = Object.assign(request, {
+ getResponseParser(): GenericConstructor {
+ return FacetConfigResponseParsingService;
+ }
+ });
+ this.requestService.send(request, true);
});
return this.rdb.buildFromHref(href$).pipe(
@@ -390,14 +461,33 @@ export class SearchService implements OnDestroy {
let pageParams = { page: 1 };
const queryParams = { view: viewMode };
if (viewMode === ViewMode.DetailedListElement) {
- pageParams = Object.assign(pageParams, {pageSize: 1});
+ pageParams = Object.assign(pageParams, { pageSize: 1 });
} else if (config.pageSize === 1) {
- pageParams = Object.assign(pageParams, {pageSize: 10});
+ pageParams = Object.assign(pageParams, { pageSize: 10 });
}
this.paginationService.updateRouteWithUrl(this.searchConfigurationService.paginationID, hasValue(searchLinkParts) ? searchLinkParts : [this.getSearchLink()], pageParams, queryParams);
});
}
+ /**
+ * Request the search configuration for a given scope or the whole repository
+ * @param {string} scope UUID of the object for which config the filter config is requested, when no scope is provided the configuration for the whole repository is loaded
+ * @param {string} configurationName the name of the configuration
+ * @returns {Observable>} The found configuration
+ */
+ getSearchConfigurationFor(scope?: string, configurationName?: string): Observable> {
+ const href$ = this.halService.getEndpoint(this.configurationLinkPath).pipe(
+ map((url: string) => this.getConfigUrl(url, scope, configurationName)),
+ );
+
+ href$.pipe(take(1)).subscribe((url: string) => {
+ const request = new this.request(this.requestService.generateRequestId(), url);
+ this.requestService.send(request, true);
+ });
+
+ return this.rdb.buildFromHref(href$);
+ }
+
/**
* @returns {string} The base path to the search page
*/
diff --git a/src/app/core/shared/sequence.service.spec.ts b/src/app/core/shared/sequence.service.spec.ts
new file mode 100644
index 0000000000..e48ad3efcc
--- /dev/null
+++ b/src/app/core/shared/sequence.service.spec.ts
@@ -0,0 +1,22 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+import { SequenceService } from './sequence.service';
+
+let service: SequenceService;
+
+describe('SequenceService', () => {
+ beforeEach(() => {
+ service = new SequenceService();
+ });
+
+ it('should return sequential numbers on next(), starting with 1', () => {
+ const NUMBERS = [1,2,3,4,5];
+ const sequence = NUMBERS.map(() => service.next());
+ expect(sequence).toEqual(NUMBERS);
+ });
+});
diff --git a/src/app/core/shared/sequence.service.ts b/src/app/core/shared/sequence.service.ts
new file mode 100644
index 0000000000..2340ffb259
--- /dev/null
+++ b/src/app/core/shared/sequence.service.ts
@@ -0,0 +1,24 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+import { Injectable } from '@angular/core';
+
+@Injectable()
+/**
+ * Provides unique sequential numbers
+ */
+export class SequenceService {
+ private value: number;
+
+ constructor() {
+ this.value = 0;
+ }
+
+ public next(): number {
+ return ++this.value;
+ }
+}
diff --git a/src/app/core/submission/submission-cc-license-url-data.service.ts b/src/app/core/submission/submission-cc-license-url-data.service.ts
index ba0baca2eb..0ca3853d0e 100644
--- a/src/app/core/submission/submission-cc-license-url-data.service.ts
+++ b/src/app/core/submission/submission-cc-license-url-data.service.ts
@@ -22,7 +22,7 @@ import { isNotEmpty } from '../../shared/empty.util';
@dataService(SUBMISSION_CC_LICENSE_URL)
export class SubmissionCcLicenseUrlDataService extends DataService {
- protected linkPath = 'submissioncclicenseUrl-search';
+ protected linkPath = 'submissioncclicenseUrls-search';
constructor(
protected comparator: DefaultChangeAnalyzer,
diff --git a/src/app/core/submission/vocabularies/vocabulary.service.ts b/src/app/core/submission/vocabularies/vocabulary.service.ts
index d82ef01087..da58512441 100644
--- a/src/app/core/submission/vocabularies/vocabulary.service.ts
+++ b/src/app/core/submission/vocabularies/vocabulary.service.ts
@@ -173,7 +173,7 @@ export class VocabularyService {
);
// TODO remove false for the entries embed when https://github.com/DSpace/DSpace/issues/3096 is solved
- return this.findVocabularyById(vocabularyOptions.name, true, true, followLink('entries', options, false)).pipe(
+ return this.findVocabularyById(vocabularyOptions.name, true, true, followLink('entries', { findListOptions: options, shouldEmbed: false })).pipe(
getFirstSucceededRemoteDataPayload(),
switchMap((vocabulary: Vocabulary) => vocabulary.entries),
);
@@ -200,7 +200,7 @@ export class VocabularyService {
);
// TODO remove false for the entries embed when https://github.com/DSpace/DSpace/issues/3096 is solved
- return this.findVocabularyById(vocabularyOptions.name, true, true, followLink('entries', options, false)).pipe(
+ return this.findVocabularyById(vocabularyOptions.name, true, true, followLink('entries', { findListOptions: options, shouldEmbed: false })).pipe(
getFirstSucceededRemoteDataPayload(),
switchMap((vocabulary: Vocabulary) => vocabulary.entries),
);
@@ -249,7 +249,7 @@ export class VocabularyService {
);
// TODO remove false for the entries embed when https://github.com/DSpace/DSpace/issues/3096 is solved
- return this.findVocabularyById(vocabularyOptions.name, true, true, followLink('entries', options, false)).pipe(
+ return this.findVocabularyById(vocabularyOptions.name, true, true, followLink('entries', { findListOptions: options, shouldEmbed: false })).pipe(
getFirstSucceededRemoteDataPayload(),
switchMap((vocabulary: Vocabulary) => vocabulary.entries),
getFirstSucceededRemoteListPayload(),
diff --git a/src/app/core/utilities/enter-zone.scheduler.ts b/src/app/core/utilities/enter-zone.scheduler.ts
new file mode 100644
index 0000000000..96aee7d9a5
--- /dev/null
+++ b/src/app/core/utilities/enter-zone.scheduler.ts
@@ -0,0 +1,19 @@
+import { SchedulerLike, Subscription } from 'rxjs';
+import { NgZone } from '@angular/core';
+
+/**
+ * An RXJS scheduler that will re-enter the Angular zone to run what's scheduled
+ */
+export class EnterZoneScheduler implements SchedulerLike {
+ constructor(private zone: NgZone, private scheduler: SchedulerLike) { }
+
+ schedule(...args: any[]): Subscription {
+ return this.zone.run(() =>
+ this.scheduler.schedule.apply(this.scheduler, args)
+ );
+ }
+
+ now (): number {
+ return this.scheduler.now();
+ }
+}
diff --git a/src/app/core/utilities/leave-zone.scheduler.ts b/src/app/core/utilities/leave-zone.scheduler.ts
new file mode 100644
index 0000000000..2587563661
--- /dev/null
+++ b/src/app/core/utilities/leave-zone.scheduler.ts
@@ -0,0 +1,19 @@
+import { SchedulerLike, Subscription } from 'rxjs';
+import { NgZone } from '@angular/core';
+
+/**
+ * An RXJS scheduler that will run what's scheduled outside of the Angular zone
+ */
+export class LeaveZoneScheduler implements SchedulerLike {
+ constructor(private zone: NgZone, private scheduler: SchedulerLike) { }
+
+ schedule(...args: any[]): Subscription {
+ return this.zone.runOutsideAngular(() =>
+ this.scheduler.schedule.apply(this.scheduler, args)
+ );
+ }
+
+ now (): number {
+ return this.scheduler.now();
+ }
+}
diff --git a/src/app/entity-groups/iiif-entities/iiif-entities.module.ts b/src/app/entity-groups/iiif-entities/iiif-entities.module.ts
deleted file mode 100644
index eacbd1d340..0000000000
--- a/src/app/entity-groups/iiif-entities/iiif-entities.module.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import {IIIFSearchableComponent} from './item-pages/iiif-searchable/iiif-searchable.component';
-import {CUSTOM_ELEMENTS_SCHEMA, NgModule} from '@angular/core';
-import {CommonModule} from '@angular/common';
-import {SharedModule} from '../../shared/shared.module';
-import {IIIFSearchableGridElementComponent} from './item-grid-elements/iiif-searchable/iiif-searchable-grid-element.component';
-import {IIIFSearchableSearchResultListElementComponent} from './item-list-elements/search-result-list-elements/iiif-searchable/iiif-searchable-search-result-list-element.component';
-import {IIIFSearchableSearchResultGridElementComponent} from './item-grid-elements/search-result-grid-elements/iiif-searchable/iiif-searchable-search-result-grid-element.component';
-import {MiradorViewerComponent} from './mirador-viewer/mirador-viewer.component';
-import {IIIFSearchableListElementComponent} from './item-list-elements/iiif-searchable/iiif-searchable-list-element.component';
-import {IIIFComponent} from './item-pages/iiif/iiif.component';
-import {IIIFListElementComponent} from './item-list-elements/iiif/iiif-list-element.component';
-import {IIIFGridElementComponent} from './item-grid-elements/iiif/iiif-grid-element.component';
-import {IIIFSearchResultListElementComponent} from './item-list-elements/search-result-list-elements/iiif/iiif-search-result-list-element.component';
-import {IIIFSearchResultGridElementComponent} from './item-grid-elements/search-result-grid-elements/iiif/iiif-search-result-grid-element.component';
-
-const ENTRY_COMPONENTS = [
- IIIFComponent,
- IIIFSearchableComponent,
- IIIFListElementComponent,
- IIIFSearchableListElementComponent,
- IIIFGridElementComponent,
- IIIFSearchableGridElementComponent,
- IIIFSearchResultListElementComponent,
- IIIFSearchableSearchResultListElementComponent,
- IIIFSearchResultGridElementComponent,
- IIIFSearchableSearchResultGridElementComponent,
- MiradorViewerComponent
-];
-@NgModule({
- imports: [
- CommonModule,
- SharedModule,
- ],
- declarations: [
- ...ENTRY_COMPONENTS
- ],
- entryComponents: [
- ...ENTRY_COMPONENTS
- ],
- schemas: [ CUSTOM_ELEMENTS_SCHEMA ]
-})
-export class IIIFEntitiesModule {
-
- static withEntryComponents() {
- return {
- ngModule: IIIFEntitiesModule,
- providers: ENTRY_COMPONENTS.map((component) => ({provide: component}))
- };
- }
-}
diff --git a/src/app/entity-groups/iiif-entities/item-grid-elements/iiif-searchable/iiif-searchable-grid-element.component.html b/src/app/entity-groups/iiif-entities/item-grid-elements/iiif-searchable/iiif-searchable-grid-element.component.html
deleted file mode 100644
index ac575227ed..0000000000
--- a/src/app/entity-groups/iiif-entities/item-grid-elements/iiif-searchable/iiif-searchable-grid-element.component.html
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/src/app/entity-groups/iiif-entities/item-grid-elements/iiif-searchable/iiif-searchable-grid-element.component.ts b/src/app/entity-groups/iiif-entities/item-grid-elements/iiif-searchable/iiif-searchable-grid-element.component.ts
deleted file mode 100644
index d1794b2055..0000000000
--- a/src/app/entity-groups/iiif-entities/item-grid-elements/iiif-searchable/iiif-searchable-grid-element.component.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { Component } from '@angular/core';
-import { ViewMode } from '../../../../core/shared/view-mode.model';
-import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
-import { AbstractListableElementComponent } from '../../../../shared/object-collection/shared/object-collection-element/abstract-listable-element.component';
-import { Item } from '../../../../core/shared/item.model';
-
-@listableObjectComponent('IIIFSearchable', ViewMode.GridElement)
-@Component({
- selector: 'ds-iiif-searchable-grid-element',
- styleUrls: ['./iiif-searchable-grid-element.component.scss'],
- templateUrl: './iiif-searchable-grid-element.component.html'
-})
-/**
- * The component for displaying a list element for an item of the type IIIFSearchable.
- */
-export class IIIFSearchableGridElementComponent extends AbstractListableElementComponent- {
-}
diff --git a/src/app/entity-groups/iiif-entities/item-grid-elements/iiif/iiif-grid-element.component.html b/src/app/entity-groups/iiif-entities/item-grid-elements/iiif/iiif-grid-element.component.html
deleted file mode 100644
index cb602f0175..0000000000
--- a/src/app/entity-groups/iiif-entities/item-grid-elements/iiif/iiif-grid-element.component.html
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/src/app/entity-groups/iiif-entities/item-grid-elements/iiif/iiif-grid-element.component.spec.ts b/src/app/entity-groups/iiif-entities/item-grid-elements/iiif/iiif-grid-element.component.spec.ts
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/src/app/entity-groups/iiif-entities/item-grid-elements/iiif/iiif-grid-element.component.ts b/src/app/entity-groups/iiif-entities/item-grid-elements/iiif/iiif-grid-element.component.ts
deleted file mode 100644
index d21694c1bf..0000000000
--- a/src/app/entity-groups/iiif-entities/item-grid-elements/iiif/iiif-grid-element.component.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { Component } from '@angular/core';
-import { ViewMode } from '../../../../core/shared/view-mode.model';
-import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
-import { AbstractListableElementComponent } from '../../../../shared/object-collection/shared/object-collection-element/abstract-listable-element.component';
-import { Item } from '../../../../core/shared/item.model';
-
-@listableObjectComponent('IIIF', ViewMode.GridElement)
-@Component({
- selector: 'ds-iiif-grid-element',
- styleUrls: ['./iiif-grid-element.component.scss'],
- templateUrl: './iiif-grid-element.component.html'
-})
-/**
- * The component for displaying a list element for an item of the type IIIF.
- */
-export class IIIFGridElementComponent extends AbstractListableElementComponent- {
-}
diff --git a/src/app/entity-groups/iiif-entities/item-grid-elements/search-result-grid-elements/iiif-searchable/iiif-searchable-search-result-grid-element.component.html b/src/app/entity-groups/iiif-entities/item-grid-elements/search-result-grid-elements/iiif-searchable/iiif-searchable-search-result-grid-element.component.html
deleted file mode 100644
index 621e3450fc..0000000000
--- a/src/app/entity-groups/iiif-entities/item-grid-elements/search-result-grid-elements/iiif-searchable/iiif-searchable-search-result-grid-element.component.html
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-