mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge branch 'main' into DSC-389
This commit is contained in:
@@ -359,7 +359,7 @@ dspace-angular
|
||||
│ ├── plugins * Folder for Cypress plugins (if any)
|
||||
│ ├── support * Folder for global e2e test actions/commands (run for all tests)
|
||||
│ └── tsconfig.json * TypeScript configuration file for e2e tests
|
||||
├── docker *
|
||||
├── docker * See docker/README.md for details
|
||||
│ ├── cli.assetstore.yml *
|
||||
│ ├── cli.ingest.yml *
|
||||
│ ├── cli.yml *
|
||||
@@ -367,8 +367,6 @@ dspace-angular
|
||||
│ ├── docker-compose-ci.yml *
|
||||
│ ├── docker-compose-rest.yml *
|
||||
│ ├── docker-compose.yml *
|
||||
│ ├── environment.dev.ts *
|
||||
│ ├── local.cfg *
|
||||
│ └── README.md *
|
||||
├── docs * Folder for documentation
|
||||
│ └── Configuration.md * Configuration documentation
|
||||
|
@@ -53,7 +53,7 @@ describe('Search Page', () => {
|
||||
|
||||
// Click to display grid view
|
||||
// TODO: These buttons should likely have an easier way to uniquely select
|
||||
cy.get('#search-sidebar-content > ds-view-mode-switch > .btn-group > [href="/search?spc.sf=score&spc.sd=DESC&view=grid"] > .fas').click();
|
||||
cy.get('#search-sidebar-content > ds-view-mode-switch > .btn-group > [href="/search?view=grid"] > .fas').click();
|
||||
|
||||
// <ds-search-page> tag must be loaded
|
||||
cy.get('ds-search-page').should('exist');
|
||||
|
@@ -29,10 +29,6 @@ docker push dspace/dspace-angular:dspace-7_x
|
||||
- Docker compose file that provides a DSpace CLI container to work with a running DSpace REST container.
|
||||
- cli.assetstore.yml
|
||||
- Docker compose file that will download and install data into a DSpace REST assetstore. This script points to a default dataset that will be utilized for CI testing.
|
||||
- environment.dev.ts
|
||||
- Environment file for running DSpace Angular in Docker
|
||||
- local.cfg
|
||||
- Environment file for running the DSpace 7 REST API in Docker.
|
||||
|
||||
|
||||
## To refresh / pull DSpace images from Dockerhub
|
||||
|
@@ -18,10 +18,19 @@ services:
|
||||
dspace-cli:
|
||||
image: "${DOCKER_OWNER:-dspace}/dspace-cli:${DSPACE_VER:-dspace-7_x}"
|
||||
container_name: dspace-cli
|
||||
#environment:
|
||||
environment:
|
||||
# Below syntax may look odd, but it is how to override dspace.cfg settings via env variables.
|
||||
# See https://github.com/DSpace/DSpace/blob/main/dspace/config/config-definition.xml
|
||||
# __P__ => "." (e.g. dspace__P__dir => dspace.dir)
|
||||
# __D__ => "-" (e.g. google__D__metadata => google-metadata)
|
||||
# dspace.dir
|
||||
dspace__P__dir: /dspace
|
||||
# db.url: Ensure we are using the 'dspacedb' image for our database
|
||||
db__P__url: 'jdbc:postgresql://dspacedb:5432/dspace'
|
||||
# solr.server: Ensure we are using the 'dspacesolr' image for Solr
|
||||
solr__P__server: http://dspacesolr:8983/solr
|
||||
volumes:
|
||||
- "assetstore:/dspace/assetstore"
|
||||
- "./local.cfg:/dspace/config/local.cfg"
|
||||
entrypoint: /dspace/bin/dspace
|
||||
command: help
|
||||
networks:
|
||||
|
@@ -17,6 +17,19 @@ services:
|
||||
# DSpace (backend) webapp container
|
||||
dspace:
|
||||
container_name: dspace
|
||||
environment:
|
||||
# Below syntax may look odd, but it is how to override dspace.cfg settings via env variables.
|
||||
# See https://github.com/DSpace/DSpace/blob/main/dspace/config/config-definition.xml
|
||||
# __P__ => "." (e.g. dspace__P__dir => dspace.dir)
|
||||
# __D__ => "-" (e.g. google__D__metadata => google-metadata)
|
||||
# dspace.dir, dspace.server.url and dspace.ui.url
|
||||
dspace__P__dir: /dspace
|
||||
dspace__P__server__P__url: http://localhost:8080/server
|
||||
dspace__P__ui__P__url: http://localhost:4000
|
||||
# db.url: Ensure we are using the 'dspacedb' image for our database
|
||||
db__P__url: 'jdbc:postgresql://dspacedb:5432/dspace'
|
||||
# solr.server: Ensure we are using the 'dspacesolr' image for Solr
|
||||
solr__P__server: http://dspacesolr:8983/solr
|
||||
depends_on:
|
||||
- dspacedb
|
||||
image: dspace/dspace:dspace-7_x-test
|
||||
@@ -29,7 +42,6 @@ services:
|
||||
tty: true
|
||||
volumes:
|
||||
- assetstore:/dspace/assetstore
|
||||
- "./local.cfg:/dspace/config/local.cfg"
|
||||
# Mount DSpace's solr configs to a volume, so that we can share to 'dspacesolr' container (see below)
|
||||
- solr_configs:/dspace/solr
|
||||
# Ensure that the database is ready BEFORE starting tomcat
|
||||
|
@@ -13,10 +13,32 @@
|
||||
version: '3.7'
|
||||
networks:
|
||||
dspacenet:
|
||||
ipam:
|
||||
config:
|
||||
# Define a custom subnet for our DSpace network, so that we can easily trust requests from host to container.
|
||||
# If you customize this value, be sure to customize the 'proxies.trusted.ipranges' env variable below.
|
||||
- subnet: 172.23.0.0/16
|
||||
services:
|
||||
# DSpace (backend) webapp container
|
||||
dspace:
|
||||
container_name: dspace
|
||||
environment:
|
||||
# Below syntax may look odd, but it is how to override dspace.cfg settings via env variables.
|
||||
# See https://github.com/DSpace/DSpace/blob/main/dspace/config/config-definition.xml
|
||||
# __P__ => "." (e.g. dspace__P__dir => dspace.dir)
|
||||
# __D__ => "-" (e.g. google__D__metadata => google-metadata)
|
||||
# dspace.dir, dspace.server.url, dspace.ui.url and dspace.name
|
||||
dspace__P__dir: /dspace
|
||||
dspace__P__server__P__url: http://localhost:8080/server
|
||||
dspace__P__ui__P__url: http://localhost:4000
|
||||
dspace__P__name: 'DSpace Started with Docker Compose'
|
||||
# db.url: Ensure we are using the 'dspacedb' image for our database
|
||||
db__P__url: 'jdbc:postgresql://dspacedb:5432/dspace'
|
||||
# solr.server: Ensure we are using the 'dspacesolr' image for Solr
|
||||
solr__P__server: http://dspacesolr:8983/solr
|
||||
# proxies.trusted.ipranges: This setting is required for a REST API running in Docker to trust requests
|
||||
# from the host machine. This IP range MUST correspond to the 'dspacenet' subnet defined above.
|
||||
proxies__P__trusted__P__ipranges: '172.23.0'
|
||||
image: dspace/dspace:dspace-7_x-test
|
||||
depends_on:
|
||||
- dspacedb
|
||||
@@ -29,7 +51,6 @@ services:
|
||||
tty: true
|
||||
volumes:
|
||||
- assetstore:/dspace/assetstore
|
||||
- "./local.cfg:/dspace/config/local.cfg"
|
||||
# Mount DSpace's solr configs to a volume, so that we can share to 'dspacesolr' container (see below)
|
||||
- solr_configs:/dspace/solr
|
||||
# Ensure that the database is ready BEFORE starting tomcat
|
||||
|
@@ -16,10 +16,14 @@ services:
|
||||
dspace-angular:
|
||||
container_name: dspace-angular
|
||||
environment:
|
||||
DSPACE_HOST: dspace-angular
|
||||
DSPACE_NAMESPACE: /
|
||||
DSPACE_PORT: '4000'
|
||||
DSPACE_SSL: "false"
|
||||
DSPACE_UI_SSL: 'false'
|
||||
DSPACE_UI_HOST: dspace-angular
|
||||
DSPACE_UI_PORT: '4000'
|
||||
DSPACE_UI_NAMESPACE: /
|
||||
DSPACE_REST_SSL: 'false'
|
||||
DSPACE_REST_HOST: localhost
|
||||
DSPACE_REST_PORT: 8080
|
||||
DSPACE_REST_NAMESPACE: /server
|
||||
image: dspace/dspace-angular:dspace-7_x
|
||||
build:
|
||||
context: ..
|
||||
@@ -33,5 +37,3 @@ services:
|
||||
target: 9876
|
||||
stdin_open: true
|
||||
tty: true
|
||||
volumes:
|
||||
- ./environment.dev.ts:/app/src/environments/environment.dev.ts
|
||||
|
@@ -1,18 +0,0 @@
|
||||
/**
|
||||
* 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/
|
||||
*/
|
||||
// This file is based on environment.template.ts provided by Angular UI
|
||||
export const environment = {
|
||||
// Default to using the local REST API (running in Docker)
|
||||
rest: {
|
||||
ssl: false,
|
||||
host: 'localhost',
|
||||
port: 8080,
|
||||
// NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
|
||||
nameSpace: '/server'
|
||||
}
|
||||
};
|
@@ -1,6 +0,0 @@
|
||||
dspace.dir=/dspace
|
||||
db.url=jdbc:postgresql://dspacedb:5432/dspace
|
||||
dspace.server.url=http://localhost:8080/server
|
||||
dspace.ui.url=http://localhost:4000
|
||||
dspace.name=DSpace Started with Docker Compose
|
||||
solr.server=http://dspacesolr:8983/solr
|
@@ -30,7 +30,7 @@
|
||||
"clean:json": "rimraf *.records.json",
|
||||
"clean:node": "rimraf node_modules",
|
||||
"clean:prod": "yarn run clean:dist && yarn run clean:log && yarn run clean:doc && yarn run clean:coverage && yarn run clean:json",
|
||||
"clean": "yarn run clean:prod && yarn run clean:node && yarn run clean:dev:config",
|
||||
"clean": "yarn run clean:prod && yarn run clean:dev:config && yarn run clean:node",
|
||||
"sync-i18n": "ts-node --project ./tsconfig.ts-node.json scripts/sync-i18n-files.ts",
|
||||
"build:mirador": "webpack --config webpack/webpack.mirador.config.ts",
|
||||
"merge-i18n": "ts-node --project ./tsconfig.ts-node.json scripts/merge-i18n-files.ts",
|
||||
|
@@ -9,13 +9,15 @@ import { GroupFormComponent } from './group-registry/group-form/group-form.compo
|
||||
import { MembersListComponent } from './group-registry/group-form/members-list/members-list.component';
|
||||
import { SubgroupsListComponent } from './group-registry/group-form/subgroup-list/subgroups-list.component';
|
||||
import { GroupsRegistryComponent } from './group-registry/groups-registry.component';
|
||||
import { FormModule } from '../shared/form/form.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
RouterModule,
|
||||
AccessControlRoutingModule
|
||||
AccessControlRoutingModule,
|
||||
FormModule
|
||||
],
|
||||
declarations: [
|
||||
EPeopleRegistryComponent,
|
||||
|
@@ -19,7 +19,7 @@
|
||||
class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}</button>
|
||||
</div>
|
||||
<div between class="btn-group">
|
||||
<button class="btn btn-primary" [disabled]="!(canReset$ | async)">
|
||||
<button class="btn btn-primary" [disabled]="!(canReset$ | async)" (click)="resetPassword()">
|
||||
<i class="fa fa-key"></i> {{'admin.access-control.epeople.actions.reset' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
@@ -36,9 +36,13 @@
|
||||
</button>
|
||||
</ds-form>
|
||||
|
||||
<ds-loading [showMessage]="false" *ngIf="!formGroup"></ds-loading>
|
||||
|
||||
<div *ngIf="epersonService.getActiveEPerson() | async">
|
||||
<h5>{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}</h5>
|
||||
|
||||
<ds-loading [showMessage]="false" *ngIf="!(groups | async)"></ds-loading>
|
||||
|
||||
<ds-pagination
|
||||
*ngIf="(groups | async)?.payload?.totalElements > 0"
|
||||
[paginationOptions]="config"
|
||||
|
@@ -2,7 +2,7 @@ import { Observable, of as observableOf } from 'rxjs';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { BrowserModule, By } from '@angular/platform-browser';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
@@ -14,6 +14,7 @@ import { EPerson } from '../../../core/eperson/models/eperson.model';
|
||||
import { PageInfo } from '../../../core/shared/page-info.model';
|
||||
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { EPeopleRegistryComponent } from '../epeople-registry.component';
|
||||
import { EPersonFormComponent } from './eperson-form.component';
|
||||
import { EPersonMock, EPersonMock2 } from '../../../shared/testing/eperson.mock';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||
@@ -28,9 +29,8 @@ import { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||
import { RequestService } from '../../../core/data/request.service';
|
||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
||||
import { FormArray, FormControl, FormGroup,Validators, NG_VALIDATORS, NG_ASYNC_VALIDATORS } from '@angular/forms';
|
||||
import { ValidateEmailNotTaken } from './validators/email-taken.validator';
|
||||
|
||||
import { EpersonRegistrationService } from '../../../core/data/eperson-registration.service';
|
||||
|
||||
describe('EPersonFormComponent', () => {
|
||||
let component: EPersonFormComponent;
|
||||
@@ -42,6 +42,7 @@ describe('EPersonFormComponent', () => {
|
||||
let authService: AuthServiceStub;
|
||||
let authorizationService: AuthorizationDataService;
|
||||
let groupsDataService: GroupDataService;
|
||||
let epersonRegistrationService: EpersonRegistrationService;
|
||||
|
||||
let paginationService;
|
||||
|
||||
@@ -199,12 +200,18 @@ describe('EPersonFormComponent', () => {
|
||||
{ provide: AuthService, useValue: authService },
|
||||
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||
{ provide: PaginationService, useValue: paginationService },
|
||||
{ provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeByHrefSubstring']) }
|
||||
{ provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeByHrefSubstring'])},
|
||||
{ provide: EpersonRegistrationService, useValue: epersonRegistrationService },
|
||||
EPeopleRegistryComponent
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
epersonRegistrationService = jasmine.createSpyObj('epersonRegistrationService', {
|
||||
registerEmail: createSuccessfulRemoteDataObject$(null)
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(EPersonFormComponent);
|
||||
component = fixture.componentInstance;
|
||||
@@ -514,4 +521,23 @@ describe('EPersonFormComponent', () => {
|
||||
expect(component.epersonService.deleteEPerson).toHaveBeenCalledWith(eperson);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Reset Password', () => {
|
||||
let ePersonId;
|
||||
let ePersonEmail;
|
||||
|
||||
beforeEach(() => {
|
||||
ePersonId = 'testEPersonId';
|
||||
ePersonEmail = 'person.email@4science.it';
|
||||
component.epersonInitial = Object.assign(new EPerson(), {
|
||||
id: ePersonId,
|
||||
email: ePersonEmail
|
||||
});
|
||||
component.resetPassword();
|
||||
});
|
||||
|
||||
it('should call epersonRegistrationService.registerEmail', () => {
|
||||
expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith(ePersonEmail);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -34,6 +34,8 @@ import { NoContent } from '../../../core/shared/NoContent.model';
|
||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||
import { ValidateEmailNotTaken } from './validators/email-taken.validator';
|
||||
import { Registration } from '../../../core/shared/registration.model';
|
||||
import { EpersonRegistrationService } from '../../../core/data/eperson-registration.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-eperson-form',
|
||||
@@ -121,7 +123,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
* Observable whether or not the admin is allowed to reset the EPerson's password
|
||||
* TODO: Initialize the observable once the REST API supports this (currently hardcoded to return false)
|
||||
*/
|
||||
canReset$: Observable<boolean> = observableOf(false);
|
||||
canReset$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Observable whether or not the admin is allowed to delete the EPerson
|
||||
@@ -167,7 +169,8 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
emailValueChangeSubscribe: Subscription;
|
||||
|
||||
constructor(protected changeDetectorRef: ChangeDetectorRef,
|
||||
constructor(
|
||||
protected changeDetectorRef: ChangeDetectorRef,
|
||||
public epersonService: EPersonDataService,
|
||||
public groupsDataService: GroupDataService,
|
||||
private formBuilderService: FormBuilderService,
|
||||
@@ -177,7 +180,9 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
private authorizationService: AuthorizationDataService,
|
||||
private modalService: NgbModal,
|
||||
private paginationService: PaginationService,
|
||||
public requestService: RequestService) {
|
||||
public requestService: RequestService,
|
||||
private epersonRegistrationService: EpersonRegistrationService,
|
||||
) {
|
||||
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
||||
this.epersonInitial = eperson;
|
||||
if (hasValue(eperson)) {
|
||||
@@ -310,6 +315,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
this.canDelete$ = activeEPerson$.pipe(
|
||||
switchMap((eperson) => this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined))
|
||||
);
|
||||
this.canReset$ = observableOf(true);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -479,6 +485,26 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
this.isImpersonated = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an email to current eperson address with the information
|
||||
* to reset password
|
||||
*/
|
||||
resetPassword() {
|
||||
if (hasValue(this.epersonInitial.email)) {
|
||||
this.epersonRegistrationService.registerEmail(this.epersonInitial.email).pipe(getFirstCompletedRemoteData())
|
||||
.subscribe((response: RemoteData<Registration>) => {
|
||||
if (response.hasSucceeded) {
|
||||
this.notificationsService.success(this.translateService.get('admin.access-control.epeople.actions.reset'),
|
||||
this.translateService.get('forgot-email.form.success.content', {email: this.epersonInitial.email}));
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get('forgot-email.form.error.head'),
|
||||
this.translateService.get('forgot-email.form.error.content', {email: this.epersonInitial.email}));
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the current edit when component is destroyed & unsub all subscriptions
|
||||
*/
|
||||
|
@@ -8,6 +8,7 @@ import { SharedModule } from '../../shared/shared.module';
|
||||
import { MetadataSchemaFormComponent } from './metadata-registry/metadata-schema-form/metadata-schema-form.component';
|
||||
import { MetadataFieldFormComponent } from './metadata-schema/metadata-field-form/metadata-field-form.component';
|
||||
import { BitstreamFormatsModule } from './bitstream-formats/bitstream-formats.module';
|
||||
import { FormModule } from '../../shared/form/form.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -15,7 +16,8 @@ import { BitstreamFormatsModule } from './bitstream-formats/bitstream-formats.mo
|
||||
SharedModule,
|
||||
RouterModule,
|
||||
BitstreamFormatsModule,
|
||||
AdminRegistriesRoutingModule
|
||||
AdminRegistriesRoutingModule,
|
||||
FormModule
|
||||
],
|
||||
declarations: [
|
||||
MetadataRegistryComponent,
|
||||
|
@@ -7,13 +7,15 @@ import { FormatFormComponent } from './format-form/format-form.component';
|
||||
import { EditBitstreamFormatComponent } from './edit-bitstream-format/edit-bitstream-format.component';
|
||||
import { BitstreamFormatsRoutingModule } from './bitstream-formats-routing.module';
|
||||
import { AddBitstreamFormatComponent } from './add-bitstream-format/add-bitstream-format.component';
|
||||
import { FormModule } from '../../../shared/form/form.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
RouterModule,
|
||||
BitstreamFormatsRoutingModule
|
||||
BitstreamFormatsRoutingModule,
|
||||
FormModule
|
||||
],
|
||||
declarations: [
|
||||
BitstreamFormatsComponent,
|
||||
|
@@ -10,6 +10,7 @@ import { CollectionAdminSearchResultGridElementComponent } from './admin-search-
|
||||
import { ItemAdminSearchResultActionsComponent } from './admin-search-results/item-admin-search-result-actions.component';
|
||||
import { JournalEntitiesModule } from '../../entity-groups/journal-entities/journal-entities.module';
|
||||
import { ResearchEntitiesModule } from '../../entity-groups/research-entities/research-entities.module';
|
||||
import { SearchModule } from '../../shared/search/search.module';
|
||||
|
||||
const ENTRY_COMPONENTS = [
|
||||
// put only entry components that use custom decorator
|
||||
@@ -24,6 +25,7 @@ const ENTRY_COMPONENTS = [
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
SearchModule,
|
||||
SharedModule.withEntryComponents(),
|
||||
JournalEntitiesModule.withEntryComponents(),
|
||||
ResearchEntitiesModule.withEntryComponents()
|
||||
|
@@ -18,6 +18,8 @@ import { ActivatedRoute } from '@angular/router';
|
||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||
import createSpy = jasmine.createSpy;
|
||||
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
|
||||
describe('AdminSidebarComponent', () => {
|
||||
let comp: AdminSidebarComponent;
|
||||
@@ -26,6 +28,28 @@ describe('AdminSidebarComponent', () => {
|
||||
let authorizationService: AuthorizationDataService;
|
||||
let scriptService;
|
||||
|
||||
|
||||
const mockItem = Object.assign(new Item(), {
|
||||
id: 'fake-id',
|
||||
uuid: 'fake-id',
|
||||
handle: 'fake/handle',
|
||||
lastModified: '2018',
|
||||
_links: {
|
||||
self: {
|
||||
href: 'https://localhost:8000/items/fake-id'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const routeStub = {
|
||||
data: observableOf({
|
||||
dso: createSuccessfulRemoteDataObject(mockItem)
|
||||
}),
|
||||
children: []
|
||||
};
|
||||
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||
isAuthorized: observableOf(true)
|
||||
@@ -42,6 +66,7 @@ describe('AdminSidebarComponent', () => {
|
||||
{ provide: ActivatedRoute, useValue: {} },
|
||||
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||
{ provide: ScriptDataService, useValue: scriptService },
|
||||
{ provide: ActivatedRoute, useValue: routeStub },
|
||||
{
|
||||
provide: NgbModal, useValue: {
|
||||
open: () => {/*comment*/
|
||||
|
@@ -21,6 +21,7 @@ import { MenuService } from '../../shared/menu/menu.service';
|
||||
import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service';
|
||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
|
||||
/**
|
||||
* Component representing the admin sidebar
|
||||
@@ -67,10 +68,11 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
private variableService: CSSVariableService,
|
||||
private authService: AuthService,
|
||||
private modalService: NgbModal,
|
||||
private authorizationService: AuthorizationDataService,
|
||||
public authorizationService: AuthorizationDataService,
|
||||
private scriptDataService: ScriptDataService,
|
||||
public route: ActivatedRoute
|
||||
) {
|
||||
super(menuService, injector);
|
||||
super(menuService, injector, authorizationService, route);
|
||||
this.inFocus$ = new BehaviorSubject(false);
|
||||
}
|
||||
|
||||
|
@@ -5,6 +5,7 @@ import { WorkflowItemSearchResultAdminWorkflowGridElementComponent } from './adm
|
||||
import { WorkflowItemAdminWorkflowActionsComponent } from './admin-workflow-search-results/workflow-item-admin-workflow-actions.component';
|
||||
import { WorkflowItemSearchResultAdminWorkflowListElementComponent } from './admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component';
|
||||
import { AdminWorkflowPageComponent } from './admin-workflow-page.component';
|
||||
import { SearchModule } from '../../shared/search/search.module';
|
||||
|
||||
const ENTRY_COMPONENTS = [
|
||||
// put only entry components that use custom decorator
|
||||
@@ -14,6 +15,7 @@ const ENTRY_COMPONENTS = [
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
SearchModule,
|
||||
SharedModule.withEntryComponents()
|
||||
],
|
||||
declarations: [
|
||||
|
@@ -24,7 +24,7 @@ const ENTRY_COMPONENTS = [
|
||||
AccessControlModule,
|
||||
AdminSearchModule.withEntryComponents(),
|
||||
AdminWorkflowModuleModule.withEntryComponents(),
|
||||
SharedModule,
|
||||
SharedModule
|
||||
],
|
||||
declarations: [
|
||||
AdminCurationTasksComponent,
|
||||
|
@@ -89,6 +89,12 @@ export function getPageNotFoundRoute() {
|
||||
return `/${PAGE_NOT_FOUND_PATH}`;
|
||||
}
|
||||
|
||||
export const INTERNAL_SERVER_ERROR = '500';
|
||||
|
||||
export function getPageInternalServerErrorRoute() {
|
||||
return `/${INTERNAL_SERVER_ERROR}`;
|
||||
}
|
||||
|
||||
export const INFO_MODULE_PATH = 'info';
|
||||
export function getInfoModulePath() {
|
||||
return `/${INFO_MODULE_PATH}`;
|
||||
|
@@ -11,10 +11,12 @@ import {
|
||||
FORBIDDEN_PATH,
|
||||
FORGOT_PASSWORD_PATH,
|
||||
INFO_MODULE_PATH,
|
||||
INTERNAL_SERVER_ERROR,
|
||||
LEGACY_BITSTREAM_MODULE_PATH,
|
||||
PROFILE_MODULE_PATH,
|
||||
REGISTER_PATH,
|
||||
REQUEST_COPY_MODULE_PATH,
|
||||
WORKFLOW_ITEM_MODULE_PATH,
|
||||
LEGACY_BITSTREAM_MODULE_PATH, REQUEST_COPY_MODULE_PATH,
|
||||
} from './app-routing-paths';
|
||||
import { COLLECTION_MODULE_PATH } from './collection-page/collection-page-routing-paths';
|
||||
import { COMMUNITY_MODULE_PATH } from './community-page/community-page-routing-paths';
|
||||
@@ -26,14 +28,25 @@ import { SiteRegisterGuard } from './core/data/feature-authorization/feature-aut
|
||||
import { ThemedPageNotFoundComponent } from './pagenotfound/themed-pagenotfound.component';
|
||||
import { ThemedForbiddenComponent } from './forbidden/themed-forbidden.component';
|
||||
import { GroupAdministratorGuard } from './core/data/feature-authorization/feature-authorization-guard/group-administrator.guard';
|
||||
import { ThemedPageInternalServerErrorComponent } from './page-internal-server-error/themed-page-internal-server-error.component';
|
||||
import { ServerCheckGuard } from './core/server-check/server-check.guard';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forRoot([{
|
||||
path: '', canActivate: [AuthBlockingGuard],
|
||||
RouterModule.forRoot([
|
||||
{ path: INTERNAL_SERVER_ERROR, component: ThemedPageInternalServerErrorComponent },
|
||||
{
|
||||
path: '',
|
||||
canActivate: [AuthBlockingGuard],
|
||||
canActivateChild: [ServerCheckGuard],
|
||||
children: [
|
||||
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
||||
{ path: 'reload/:rnd', component: ThemedPageNotFoundComponent, pathMatch: 'full', canActivate: [ReloadGuard] },
|
||||
{
|
||||
path: 'reload/:rnd',
|
||||
component: ThemedPageNotFoundComponent,
|
||||
pathMatch: 'full',
|
||||
canActivate: [ReloadGuard]
|
||||
},
|
||||
{
|
||||
path: 'home',
|
||||
loadChildren: () => import('./home-page/home-page.module')
|
||||
@@ -89,7 +102,8 @@ import { GroupAdministratorGuard } from './core/data/feature-authorization/featu
|
||||
.then((m) => m.ItemPageModule),
|
||||
canActivate: [EndUserAgreementCurrentUserGuard]
|
||||
},
|
||||
{ path: 'entities/:entity-type',
|
||||
{
|
||||
path: 'entities/:entity-type',
|
||||
loadChildren: () => import('./item-page/item-page.module')
|
||||
.then((m) => m.ItemPageModule),
|
||||
canActivate: [EndUserAgreementCurrentUserGuard]
|
||||
@@ -133,12 +147,12 @@ import { GroupAdministratorGuard } from './core/data/feature-authorization/featu
|
||||
{
|
||||
path: 'login',
|
||||
loadChildren: () => import('./login-page/login-page.module')
|
||||
.then((m) => m.LoginPageModule),
|
||||
.then((m) => m.LoginPageModule)
|
||||
},
|
||||
{
|
||||
path: 'logout',
|
||||
loadChildren: () => import('./logout-page/logout-page.module')
|
||||
.then((m) => m.LogoutPageModule),
|
||||
.then((m) => m.LogoutPageModule)
|
||||
},
|
||||
{
|
||||
path: 'submit',
|
||||
@@ -178,7 +192,7 @@ import { GroupAdministratorGuard } from './core/data/feature-authorization/featu
|
||||
},
|
||||
{
|
||||
path: INFO_MODULE_PATH,
|
||||
loadChildren: () => import('./info/info.module').then((m) => m.InfoModule),
|
||||
loadChildren: () => import('./info/info.module').then((m) => m.InfoModule)
|
||||
},
|
||||
{
|
||||
path: REQUEST_COPY_MODULE_PATH,
|
||||
@@ -192,7 +206,7 @@ import { GroupAdministratorGuard } from './core/data/feature-authorization/featu
|
||||
{
|
||||
path: 'statistics',
|
||||
loadChildren: () => import('./statistics-page/statistics-page-routing.module')
|
||||
.then((m) => m.StatisticsPageRoutingModule),
|
||||
.then((m) => m.StatisticsPageRoutingModule)
|
||||
},
|
||||
{
|
||||
path: ACCESS_CONTROL_MODULE_PATH,
|
||||
@@ -200,8 +214,9 @@ import { GroupAdministratorGuard } from './core/data/feature-authorization/featu
|
||||
canActivate: [GroupAdministratorGuard],
|
||||
},
|
||||
{ path: '**', pathMatch: 'full', component: ThemedPageNotFoundComponent },
|
||||
]}
|
||||
],{
|
||||
]
|
||||
}
|
||||
], {
|
||||
onSameUrlNavigation: 'reload',
|
||||
})
|
||||
],
|
||||
|
@@ -54,11 +54,10 @@ import { ThemedFooterComponent } from './footer/themed-footer.component';
|
||||
import { ThemedBreadcrumbsComponent } from './breadcrumbs/themed-breadcrumbs.component';
|
||||
import { ThemedHeaderNavbarWrapperComponent } from './header-nav-wrapper/themed-header-navbar-wrapper.component';
|
||||
import { IdleModalComponent } from './shared/idle-modal/idle-modal.component';
|
||||
import { ThemedPageInternalServerErrorComponent } from './page-internal-server-error/themed-page-internal-server-error.component';
|
||||
import { PageInternalServerErrorComponent } from './page-internal-server-error/page-internal-server-error.component';
|
||||
|
||||
import { UUIDService } from './core/shared/uuid.service';
|
||||
import { CookieService } from './core/services/cookie.service';
|
||||
|
||||
import { AppConfig, APP_CONFIG } from '../config/app-config.interface';
|
||||
import { APP_CONFIG, AppConfig } from '../config/app-config.interface';
|
||||
|
||||
export function getConfig() {
|
||||
return environment;
|
||||
@@ -156,21 +155,6 @@ const PROVIDERS = [
|
||||
useClass: LogInterceptor,
|
||||
multi: true
|
||||
},
|
||||
// insert the unique id of the user that is using the application utilizing cookies
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
useFactory: (cookieService: CookieService, uuidService: UUIDService) => {
|
||||
const correlationId = cookieService.get('CORRELATION-ID');
|
||||
|
||||
// Check if cookie exists, if don't, set it with unique id
|
||||
if (!correlationId) {
|
||||
cookieService.set('CORRELATION-ID', uuidService.generate());
|
||||
}
|
||||
return () => true;
|
||||
},
|
||||
multi: true,
|
||||
deps: [CookieService, UUIDService]
|
||||
},
|
||||
{
|
||||
provide: DYNAMIC_ERROR_MESSAGES_MATCHER,
|
||||
useValue: ValidateEmailErrorStateMatcher
|
||||
@@ -199,7 +183,9 @@ const DECLARATIONS = [
|
||||
ThemedBreadcrumbsComponent,
|
||||
ForbiddenComponent,
|
||||
ThemedForbiddenComponent,
|
||||
IdleModalComponent
|
||||
IdleModalComponent,
|
||||
ThemedPageInternalServerErrorComponent,
|
||||
PageInternalServerErrorComponent
|
||||
];
|
||||
|
||||
const EXPORTS = [
|
||||
|
@@ -49,6 +49,7 @@ import {
|
||||
import { sidebarReducer, SidebarState } from './shared/sidebar/sidebar.reducer';
|
||||
import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer';
|
||||
import { ThemeState, themeReducer } from './shared/theme-support/theme.reducer';
|
||||
import { correlationIdReducer } from './correlation-id/correlation-id.reducer';
|
||||
|
||||
export interface AppState {
|
||||
router: fromRouter.RouterReducerState;
|
||||
@@ -69,6 +70,7 @@ export interface AppState {
|
||||
communityList: CommunityListState;
|
||||
epeopleRegistry: EPeopleRegistryState;
|
||||
groupRegistry: GroupRegistryState;
|
||||
correlationId: string;
|
||||
}
|
||||
|
||||
export const appReducers: ActionReducerMap<AppState> = {
|
||||
@@ -90,6 +92,7 @@ export const appReducers: ActionReducerMap<AppState> = {
|
||||
communityList: CommunityListReducer,
|
||||
epeopleRegistry: ePeopleRegistryReducer,
|
||||
groupRegistry: groupRegistryReducer,
|
||||
correlationId: correlationIdReducer
|
||||
};
|
||||
|
||||
export const routerStateSelector = (state: AppState) => state.router;
|
||||
|
@@ -4,6 +4,8 @@ import { SharedModule } from '../shared/shared.module';
|
||||
import { EditBitstreamPageComponent } from './edit-bitstream-page/edit-bitstream-page.component';
|
||||
import { BitstreamPageRoutingModule } from './bitstream-page-routing.module';
|
||||
import { BitstreamAuthorizationsComponent } from './bitstream-authorizations/bitstream-authorizations.component';
|
||||
import { FormModule } from '../shared/form/form.module';
|
||||
import { ResourcePoliciesModule } from '../shared/resource-policies/resource-policies.module';
|
||||
|
||||
/**
|
||||
* This module handles all components that are necessary for Bitstream related pages
|
||||
@@ -12,7 +14,9 @@ import { BitstreamAuthorizationsComponent } from './bitstream-authorizations/bit
|
||||
imports: [
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
BitstreamPageRoutingModule
|
||||
BitstreamPageRoutingModule,
|
||||
FormModule,
|
||||
ResourcePoliciesModule
|
||||
],
|
||||
declarations: [
|
||||
BitstreamAuthorizationsComponent,
|
||||
|
@@ -63,7 +63,7 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
|
||||
this.browseId = params.id || this.defaultBrowseId;
|
||||
this.startsWith = +params.startsWith || params.startsWith;
|
||||
const searchOptions = browseParamsToOptions(params, currentPage, currentSort, this.browseId);
|
||||
this.updatePageWithItems(searchOptions, this.value);
|
||||
this.updatePageWithItems(searchOptions, this.value, undefined);
|
||||
this.updateParent(params.scope);
|
||||
this.updateStartsWithOptions(this.browseId, metadataKeys, params.scope);
|
||||
}));
|
||||
|
@@ -99,6 +99,11 @@ export class BrowseByMetadataPageComponent implements OnInit {
|
||||
*/
|
||||
value = '';
|
||||
|
||||
/**
|
||||
* The authority key (may be undefined) associated with {@link #value}.
|
||||
*/
|
||||
authority: string;
|
||||
|
||||
/**
|
||||
* The current startsWith option (fetched and updated from query-params)
|
||||
*/
|
||||
@@ -123,11 +128,12 @@ export class BrowseByMetadataPageComponent implements OnInit {
|
||||
})
|
||||
).subscribe(([params, currentPage, currentSort]: [Params, PaginationComponentOptions, SortOptions]) => {
|
||||
this.browseId = params.id || this.defaultBrowseId;
|
||||
this.authority = params.authority;
|
||||
this.value = +params.value || params.value || '';
|
||||
this.startsWith = +params.startsWith || params.startsWith;
|
||||
const searchOptions = browseParamsToOptions(params, currentPage, currentSort, this.browseId);
|
||||
if (isNotEmpty(this.value)) {
|
||||
this.updatePageWithItems(searchOptions, this.value);
|
||||
this.updatePageWithItems(searchOptions, this.value, this.authority);
|
||||
} else {
|
||||
this.updatePage(searchOptions);
|
||||
}
|
||||
@@ -166,8 +172,8 @@ export class BrowseByMetadataPageComponent implements OnInit {
|
||||
* scope: string }
|
||||
* @param value The value of the browse-entry to display items for
|
||||
*/
|
||||
updatePageWithItems(searchOptions: BrowseEntrySearchOptions, value: string) {
|
||||
this.items$ = this.browseService.getBrowseItemsFor(value, searchOptions);
|
||||
updatePageWithItems(searchOptions: BrowseEntrySearchOptions, value: string, authority: string) {
|
||||
this.items$ = this.browseService.getBrowseItemsFor(value, authority, searchOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -46,7 +46,7 @@ export class BrowseByTitlePageComponent extends BrowseByMetadataPageComponent {
|
||||
})
|
||||
).subscribe(([params, currentPage, currentSort]: [Params, PaginationComponentOptions, SortOptions]) => {
|
||||
this.browseId = params.id || this.defaultBrowseId;
|
||||
this.updatePageWithItems(browseParamsToOptions(params, currentPage, currentSort, this.browseId), undefined);
|
||||
this.updatePageWithItems(browseParamsToOptions(params, currentPage, currentSort, this.browseId), undefined, undefined);
|
||||
this.updateParent(params.scope);
|
||||
}));
|
||||
this.updateStartsWithTextOptions();
|
||||
|
@@ -6,6 +6,7 @@ import { BrowseByMetadataPageComponent } from './browse-by-metadata-page/browse-
|
||||
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 { ComcolModule } from '../shared/comcol/comcol.module';
|
||||
|
||||
const ENTRY_COMPONENTS = [
|
||||
// put only entry components that use custom decorator
|
||||
@@ -17,6 +18,7 @@ const ENTRY_COMPONENTS = [
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
ComcolModule,
|
||||
SharedModule
|
||||
],
|
||||
declarations: [
|
||||
|
@@ -10,7 +10,7 @@ import {
|
||||
} from '@ng-dynamic-forms/core';
|
||||
|
||||
import { Collection } from '../../core/shared/collection.model';
|
||||
import { ComColFormComponent } from '../../shared/comcol-forms/comcol-form/comcol-form.component';
|
||||
import { ComColFormComponent } from '../../shared/comcol/comcol-forms/comcol-form/comcol-form.component';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
@@ -28,8 +28,8 @@ import { NONE_ENTITY_TYPE } from '../../core/shared/item-relationships/item-type
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-collection-form',
|
||||
styleUrls: ['../../shared/comcol-forms/comcol-form/comcol-form.component.scss'],
|
||||
templateUrl: '../../shared/comcol-forms/comcol-form/comcol-form.component.html'
|
||||
styleUrls: ['../../shared/comcol/comcol-forms/comcol-form/comcol-form.component.scss'],
|
||||
templateUrl: '../../shared/comcol/comcol-forms/comcol-form/comcol-form.component.html'
|
||||
})
|
||||
export class CollectionFormComponent extends ComColFormComponent<Collection> implements OnInit {
|
||||
/**
|
||||
|
@@ -2,9 +2,13 @@ import { NgModule } from '@angular/core';
|
||||
|
||||
import { CollectionFormComponent } from './collection-form.component';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { ComcolModule } from '../../shared/comcol/comcol.module';
|
||||
import { FormModule } from '../../shared/form/form.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
ComcolModule,
|
||||
FormModule,
|
||||
SharedModule
|
||||
],
|
||||
declarations: [
|
||||
|
@@ -33,7 +33,7 @@ import { ErrorComponent } from '../../shared/error/error.component';
|
||||
import { LoadingComponent } from '../../shared/loading/loading.component';
|
||||
import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service';
|
||||
import { SearchService } from '../../core/shared/search/search.service';
|
||||
import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model';
|
||||
import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model';
|
||||
import {
|
||||
createFailedRemoteDataObject$,
|
||||
createSuccessfulRemoteDataObject,
|
||||
|
@@ -9,10 +9,11 @@ import { Collection } from '../../core/shared/collection.model';
|
||||
import { PaginatedList } from '../../core/data/paginated-list.model';
|
||||
import { map, startWith, switchMap, take } from 'rxjs/operators';
|
||||
import {
|
||||
getRemoteDataPayload,
|
||||
getAllSucceededRemoteData,
|
||||
getFirstCompletedRemoteData,
|
||||
getFirstSucceededRemoteData,
|
||||
toDSpaceObjectListRD,
|
||||
getFirstCompletedRemoteData, getAllSucceededRemoteData
|
||||
getRemoteDataPayload,
|
||||
toDSpaceObjectListRD
|
||||
} from '../../core/shared/operators';
|
||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||
import { DSpaceObjectType } from '../../core/shared/dspace-object-type.model';
|
||||
@@ -24,7 +25,7 @@ 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 { SearchConfigurationService } from '../../core/shared/search/search-configuration.service';
|
||||
import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model';
|
||||
import { PaginatedSearchOptions } from '../../shared/search/models/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';
|
||||
|
@@ -1,13 +1,8 @@
|
||||
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 { PaginatedSearchOptions } from '../shared/search/models/paginated-search-options.model';
|
||||
import { SearchService } from '../core/shared/search/search.service';
|
||||
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
|
||||
import { CollectionDataService } from '../core/data/collection-data.service';
|
||||
@@ -103,7 +98,7 @@ export class CollectionPageComponent implements OnInit {
|
||||
const currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, this.sortConfig);
|
||||
|
||||
this.itemRD$ = observableCombineLatest([currentPagination$, currentSort$]).pipe(
|
||||
switchMap(([currentPagination, currentSort ]) => this.collectionRD$.pipe(
|
||||
switchMap(([currentPagination, currentSort]) => this.collectionRD$.pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
map((rd) => rd.payload.id),
|
||||
switchMap((id: string) => {
|
||||
|
@@ -14,6 +14,7 @@ import { SearchService } from '../core/shared/search/search.service';
|
||||
import { StatisticsModule } from '../statistics/statistics.module';
|
||||
import { CollectionFormModule } from './collection-form/collection-form.module';
|
||||
import { ThemedCollectionPageComponent } from './themed-collection-page.component';
|
||||
import { ComcolModule } from '../shared/comcol/comcol.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -22,7 +23,8 @@ import { ThemedCollectionPageComponent } from './themed-collection-page.componen
|
||||
CollectionPageRoutingModule,
|
||||
StatisticsModule.forRoot(),
|
||||
EditItemPageModule,
|
||||
CollectionFormModule
|
||||
CollectionFormModule,
|
||||
ComcolModule
|
||||
],
|
||||
declarations: [
|
||||
CollectionPageComponent,
|
||||
|
@@ -2,12 +2,12 @@ import { Component } from '@angular/core';
|
||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
import { RouteService } from '../../core/services/route.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { CreateComColPageComponent } from '../../shared/comcol-forms/create-comcol-page/create-comcol-page.component';
|
||||
import { CreateComColPageComponent } from '../../shared/comcol/comcol-forms/create-comcol-page/create-comcol-page.component';
|
||||
import { Collection } from '../../core/shared/collection.model';
|
||||
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import {RequestService} from '../../core/data/request.service';
|
||||
import { RequestService } from '../../core/data/request.service';
|
||||
|
||||
/**
|
||||
* Component that represents the page where a user can create a new Collection
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { DeleteComColPageComponent } from '../../shared/comcol-forms/delete-comcol-page/delete-comcol-page.component';
|
||||
import { DeleteComColPageComponent } from '../../shared/comcol/comcol-forms/delete-comcol-page/delete-comcol-page.component';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||
import { Collection } from '../../core/shared/collection.model';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import {RequestService} from '../../core/data/request.service';
|
||||
import { RequestService } from '../../core/data/request.service';
|
||||
|
||||
/**
|
||||
* Component that represents the page where a user can delete an existing Collection
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ComcolMetadataComponent } from '../../../shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component';
|
||||
import { ComcolMetadataComponent } from '../../../shared/comcol/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component';
|
||||
import { Collection } from '../../../core/shared/collection.model';
|
||||
import { CollectionDataService } from '../../../core/data/collection-data.service';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
|
@@ -12,6 +12,7 @@ import { RequestService } from '../../../core/data/request.service';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { ComcolModule } from '../../../shared/comcol/comcol.module';
|
||||
|
||||
describe('CollectionRolesComponent', () => {
|
||||
|
||||
@@ -65,6 +66,7 @@ describe('CollectionRolesComponent', () => {
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
ComcolModule,
|
||||
SharedModule,
|
||||
RouterTestingModule.withRoutes([]),
|
||||
TranslateModule.forRoot(),
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { EditComColPageComponent } from '../../shared/comcol-forms/edit-comcol-page/edit-comcol-page.component';
|
||||
import { EditComColPageComponent } from '../../shared/comcol/comcol-forms/edit-comcol-page/edit-comcol-page.component';
|
||||
import { Collection } from '../../core/shared/collection.model';
|
||||
import { getCollectionPageRoute } from '../collection-page-routing-paths';
|
||||
|
||||
@@ -9,7 +9,7 @@ import { getCollectionPageRoute } from '../collection-page-routing-paths';
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-edit-collection',
|
||||
templateUrl: '../../shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.html'
|
||||
templateUrl: '../../shared/comcol/comcol-forms/edit-comcol-page/edit-comcol-page.component.html'
|
||||
})
|
||||
export class EditCollectionPageComponent extends EditComColPageComponent<Collection> {
|
||||
type = 'collection';
|
||||
|
@@ -10,6 +10,9 @@ import { CollectionSourceComponent } from './collection-source/collection-source
|
||||
import { CollectionAuthorizationsComponent } from './collection-authorizations/collection-authorizations.component';
|
||||
import { CollectionFormModule } from '../collection-form/collection-form.module';
|
||||
import { CollectionSourceControlsComponent } from './collection-source/collection-source-controls/collection-source-controls.component';
|
||||
import { ResourcePoliciesModule } from '../../shared/resource-policies/resource-policies.module';
|
||||
import { FormModule } from '../../shared/form/form.module';
|
||||
import { ComcolModule } from '../../shared/comcol/comcol.module';
|
||||
|
||||
/**
|
||||
* Module that contains all components related to the Edit Collection page administrator functionality
|
||||
@@ -19,7 +22,10 @@ import { CollectionSourceControlsComponent } from './collection-source/collectio
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
EditCollectionPageRoutingModule,
|
||||
CollectionFormModule
|
||||
CollectionFormModule,
|
||||
ResourcePoliciesModule,
|
||||
FormModule,
|
||||
ComcolModule
|
||||
],
|
||||
declarations: [
|
||||
EditCollectionPageComponent,
|
||||
|
@@ -6,7 +6,7 @@ import {
|
||||
DynamicTextAreaModel
|
||||
} from '@ng-dynamic-forms/core';
|
||||
import { Community } from '../../core/shared/community.model';
|
||||
import { ComColFormComponent } from '../../shared/comcol-forms/comcol-form/comcol-form.component';
|
||||
import { ComColFormComponent } from '../../shared/comcol/comcol-forms/comcol-form/comcol-form.component';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
@@ -19,8 +19,8 @@ import { ObjectCacheService } from '../../core/cache/object-cache.service';
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-community-form',
|
||||
styleUrls: ['../../shared/comcol-forms/comcol-form/comcol-form.component.scss'],
|
||||
templateUrl: '../../shared/comcol-forms/comcol-form/comcol-form.component.html'
|
||||
styleUrls: ['../../shared/comcol/comcol-forms/comcol-form/comcol-form.component.scss'],
|
||||
templateUrl: '../../shared/comcol/comcol-forms/comcol-form/comcol-form.component.html'
|
||||
})
|
||||
export class CommunityFormComponent extends ComColFormComponent<Community> {
|
||||
/**
|
||||
|
@@ -2,9 +2,13 @@ import { NgModule } from '@angular/core';
|
||||
|
||||
import { CommunityFormComponent } from './community-form.component';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { ComcolModule } from '../../shared/comcol/comcol.module';
|
||||
import { FormModule } from '../../shared/form/form.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
ComcolModule,
|
||||
FormModule,
|
||||
SharedModule
|
||||
],
|
||||
declarations: [
|
||||
|
@@ -12,6 +12,7 @@ import { DeleteCommunityPageComponent } from './delete-community-page/delete-com
|
||||
import { StatisticsModule } from '../statistics/statistics.module';
|
||||
import { CommunityFormModule } from './community-form/community-form.module';
|
||||
import { ThemedCommunityPageComponent } from './themed-community-page.component';
|
||||
import { ComcolModule } from '../shared/comcol/comcol.module';
|
||||
|
||||
const DECLARATIONS = [CommunityPageComponent,
|
||||
ThemedCommunityPageComponent,
|
||||
@@ -26,7 +27,8 @@ const DECLARATIONS = [CommunityPageComponent,
|
||||
SharedModule,
|
||||
CommunityPageRoutingModule,
|
||||
StatisticsModule.forRoot(),
|
||||
CommunityFormModule
|
||||
CommunityFormModule,
|
||||
ComcolModule
|
||||
],
|
||||
declarations: [
|
||||
...DECLARATIONS
|
||||
|
@@ -3,7 +3,7 @@ import { Community } from '../../core/shared/community.model';
|
||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
import { RouteService } from '../../core/services/route.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { CreateComColPageComponent } from '../../shared/comcol-forms/create-comcol-page/create-comcol-page.component';
|
||||
import { CreateComColPageComponent } from '../../shared/comcol/comcol-forms/create-comcol-page/create-comcol-page.component';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { RequestService } from '../../core/data/request.service';
|
||||
|
@@ -2,10 +2,10 @@ import { Component } from '@angular/core';
|
||||
import { Community } from '../../core/shared/community.model';
|
||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { DeleteComColPageComponent } from '../../shared/comcol-forms/delete-comcol-page/delete-comcol-page.component';
|
||||
import { DeleteComColPageComponent } from '../../shared/comcol/comcol-forms/delete-comcol-page/delete-comcol-page.component';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import {RequestService} from '../../core/data/request.service';
|
||||
import { RequestService } from '../../core/data/request.service';
|
||||
|
||||
/**
|
||||
* Component that represents the page where a user can delete an existing Community
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ComcolMetadataComponent } from '../../../shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component';
|
||||
import { ComcolMetadataComponent } from '../../../shared/comcol/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Community } from '../../../core/shared/community.model';
|
||||
import { CommunityDataService } from '../../../core/data/community-data.service';
|
||||
|
@@ -12,6 +12,7 @@ import { SharedModule } from '../../../shared/shared.module';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { ComcolModule } from '../../../shared/comcol/comcol.module';
|
||||
|
||||
describe('CommunityRolesComponent', () => {
|
||||
|
||||
@@ -50,6 +51,7 @@ describe('CommunityRolesComponent', () => {
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
ComcolModule,
|
||||
SharedModule,
|
||||
RouterTestingModule.withRoutes([]),
|
||||
TranslateModule.forRoot(),
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Community } from '../../core/shared/community.model';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { EditComColPageComponent } from '../../shared/comcol-forms/edit-comcol-page/edit-comcol-page.component';
|
||||
import { EditComColPageComponent } from '../../shared/comcol/comcol-forms/edit-comcol-page/edit-comcol-page.component';
|
||||
import { getCommunityPageRoute } from '../community-page-routing-paths';
|
||||
|
||||
/**
|
||||
@@ -9,7 +9,7 @@ import { getCommunityPageRoute } from '../community-page-routing-paths';
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-edit-community',
|
||||
templateUrl: '../../shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.html'
|
||||
templateUrl: '../../shared/comcol/comcol-forms/edit-comcol-page/edit-comcol-page.component.html'
|
||||
})
|
||||
export class EditCommunityPageComponent extends EditComColPageComponent<Community> {
|
||||
type = 'community';
|
||||
|
@@ -8,6 +8,8 @@ import { CommunityMetadataComponent } from './community-metadata/community-metad
|
||||
import { CommunityRolesComponent } from './community-roles/community-roles.component';
|
||||
import { CommunityAuthorizationsComponent } from './community-authorizations/community-authorizations.component';
|
||||
import { CommunityFormModule } from '../community-form/community-form.module';
|
||||
import { ResourcePoliciesModule } from '../../shared/resource-policies/resource-policies.module';
|
||||
import { ComcolModule } from '../../shared/comcol/comcol.module';
|
||||
|
||||
/**
|
||||
* Module that contains all components related to the Edit Community page administrator functionality
|
||||
@@ -17,7 +19,9 @@ import { CommunityFormModule } from '../community-form/community-form.module';
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
EditCommunityPageRoutingModule,
|
||||
CommunityFormModule
|
||||
CommunityFormModule,
|
||||
ComcolModule,
|
||||
ResourcePoliciesModule
|
||||
],
|
||||
declarations: [
|
||||
EditCommunityPageComponent,
|
||||
|
@@ -129,6 +129,7 @@ describe('BrowseService', () => {
|
||||
describe('getBrowseEntriesFor and findList', () => {
|
||||
// should contain special characters such that url encoding can be tested as well
|
||||
const mockAuthorName = 'Donald Smith & Sons';
|
||||
const mockAuthorityKey = 'some authority key ?=;';
|
||||
|
||||
beforeEach(() => {
|
||||
requestService = getMockRequestService(getRequestEntry$(true));
|
||||
@@ -155,7 +156,7 @@ describe('BrowseService', () => {
|
||||
it('should call hrefOnlyDataService.findAllByHref with the expected href', () => {
|
||||
const expected = browseDefinitions[1]._links.items.href + '?filterValue=' + encodeURIComponent(mockAuthorName);
|
||||
|
||||
scheduler.schedule(() => service.getBrowseItemsFor(mockAuthorName, new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe());
|
||||
scheduler.schedule(() => service.getBrowseItemsFor(mockAuthorName, undefined, new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe());
|
||||
scheduler.flush();
|
||||
|
||||
expect(getFirstUsedArgumentOfSpyMethod(hrefOnlyDataService.findAllByHref)).toBeObservable(cold('(a|)', {
|
||||
@@ -164,6 +165,20 @@ describe('BrowseService', () => {
|
||||
});
|
||||
|
||||
});
|
||||
describe('when getBrowseItemsFor is called with a valid filter value and authority key', () => {
|
||||
it('should call hrefOnlyDataService.findAllByHref with the expected href', () => {
|
||||
const expected = browseDefinitions[1]._links.items.href +
|
||||
'?filterValue=' + encodeURIComponent(mockAuthorName) +
|
||||
'&filterAuthority=' + encodeURIComponent(mockAuthorityKey);
|
||||
|
||||
scheduler.schedule(() => service.getBrowseItemsFor(mockAuthorName, mockAuthorityKey, new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe());
|
||||
scheduler.flush();
|
||||
|
||||
expect(getFirstUsedArgumentOfSpyMethod(hrefOnlyDataService.findAllByHref)).toBeObservable(cold('(a|)', {
|
||||
a: expected
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBrowseURLFor', () => {
|
||||
|
@@ -105,7 +105,7 @@ export class BrowseService {
|
||||
* @param options Options to narrow down your search
|
||||
* @returns {Observable<RemoteData<PaginatedList<Item>>>}
|
||||
*/
|
||||
getBrowseItemsFor(filterValue: string, options: BrowseEntrySearchOptions): Observable<RemoteData<PaginatedList<Item>>> {
|
||||
getBrowseItemsFor(filterValue: string, filterAuthority: string, options: BrowseEntrySearchOptions): Observable<RemoteData<PaginatedList<Item>>> {
|
||||
const href$ = this.getBrowseDefinitions().pipe(
|
||||
getBrowseDefinitionLinks(options.metadataDefinition),
|
||||
hasValueOperator(),
|
||||
@@ -132,6 +132,9 @@ export class BrowseService {
|
||||
if (isNotEmpty(filterValue)) {
|
||||
args.push(`filterValue=${encodeURIComponent(filterValue)}`);
|
||||
}
|
||||
if (isNotEmpty(filterAuthority)) {
|
||||
args.push(`filterAuthority=${encodeURIComponent(filterAuthority)}`);
|
||||
}
|
||||
if (isNotEmpty(args)) {
|
||||
href = new URLCombiner(href, `?${args.join('&')}`).toString();
|
||||
}
|
||||
|
@@ -163,6 +163,7 @@ 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';
|
||||
import { GroupDataService } from './eperson/group-data.service';
|
||||
|
||||
/**
|
||||
* When not in production, endpoint responses can be mocked for testing purposes
|
||||
@@ -285,6 +286,7 @@ const PROVIDERS = [
|
||||
VocabularyService,
|
||||
VocabularyTreeviewService,
|
||||
SequenceService,
|
||||
GroupDataService
|
||||
];
|
||||
|
||||
/**
|
||||
|
@@ -20,7 +20,7 @@ import { PaginatedList } from './paginated-list.model';
|
||||
import { RemoteData } from './remote-data';
|
||||
import { FindListOptions, GetRequest } from './request.models';
|
||||
import { RequestService } from './request.service';
|
||||
import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model';
|
||||
import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model';
|
||||
import { Bitstream } from '../shared/bitstream.model';
|
||||
import { RequestEntryState } from './request.reducer';
|
||||
|
||||
|
@@ -12,7 +12,7 @@ import { HttpClient } from '@angular/common/http';
|
||||
import { FindListOptions } from './request.models';
|
||||
import { Observable } from 'rxjs';
|
||||
import { distinctUntilChanged, map, switchMap, take } from 'rxjs/operators';
|
||||
import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model';
|
||||
import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model';
|
||||
import { hasValue, isNotEmptyOperator } from '../../shared/empty.util';
|
||||
import { RemoteData } from './remote-data';
|
||||
import { PaginatedList } from './paginated-list.model';
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { SearchFilterConfig } from '../../shared/search/search-filter-config.model';
|
||||
import { SearchFilterConfig } from '../../shared/search/models/search-filter-config.model';
|
||||
import { ParsedResponse } from '../cache/response.models';
|
||||
import { RawRestResponse } from '../dspace-rest/raw-rest-response.model';
|
||||
import { DSpaceSerializer } from '../dspace-rest/dspace.serializer';
|
||||
import { RestRequest } from './request.models';
|
||||
import { DspaceRestResponseParsingService } from './dspace-rest-response-parsing.service';
|
||||
import { FacetConfigResponse } from '../../shared/search/facet-config-response.model';
|
||||
import { FacetConfigResponse } from '../../shared/search/models/facet-config-response.model';
|
||||
|
||||
@Injectable()
|
||||
export class FacetConfigResponseParsingService extends DspaceRestResponseParsingService {
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { FacetValue } from '../../shared/search/facet-value.model';
|
||||
import { FacetValue } from '../../shared/search/models/facet-value.model';
|
||||
import { ParsedResponse } from '../cache/response.models';
|
||||
import { RawRestResponse } from '../dspace-rest/raw-rest-response.model';
|
||||
import { DSpaceSerializer } from '../dspace-rest/dspace.serializer';
|
||||
import { RestRequest } from './request.models';
|
||||
import { FacetValues } from '../../shared/search/facet-values.model';
|
||||
import { FacetValues } from '../../shared/search/models/facet-values.model';
|
||||
import { DspaceRestResponseParsingService } from './dspace-rest-response-parsing.service';
|
||||
|
||||
@Injectable()
|
||||
|
@@ -0,0 +1,27 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
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';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import { FeatureID } from '../feature-id';
|
||||
|
||||
/**
|
||||
* Prevent unauthorized activating and loading of routes when the current authenticated user doesn't have group
|
||||
* management rights
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class StatisticsAdministratorGuard extends SingleFeatureAuthorizationGuard {
|
||||
constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) {
|
||||
super(authorizationService, router, authService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check group management rights
|
||||
*/
|
||||
getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
||||
return observableOf(FeatureID.CanViewUsageStatistics);
|
||||
}
|
||||
}
|
@@ -13,6 +13,7 @@ export enum FeatureID {
|
||||
CanManageGroup = 'canManageGroup',
|
||||
IsCollectionAdmin = 'isCollectionAdmin',
|
||||
IsCommunityAdmin = 'isCommunityAdmin',
|
||||
CanChangePassword = 'canChangePassword',
|
||||
CanDownload = 'canDownload',
|
||||
CanRequestACopy = 'canRequestACopy',
|
||||
CanManageVersions = 'canManageVersions',
|
||||
@@ -25,4 +26,5 @@ export enum FeatureID {
|
||||
CanEditVersion = 'canEditVersion',
|
||||
CanDeleteVersion = 'canDeleteVersion',
|
||||
CanCreateVersion = 'canCreateVersion',
|
||||
CanViewUsageStatistics = 'canViewUsageStatistics',
|
||||
}
|
||||
|
@@ -25,7 +25,7 @@ import { PaginatedList } from './paginated-list.model';
|
||||
import { RemoteData } from './remote-data';
|
||||
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 { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model';
|
||||
import { Bundle } from '../shared/bundle.model';
|
||||
import { MetadataMap } from '../shared/metadata.models';
|
||||
import { BundleDataService } from './bundle-data.service';
|
||||
|
@@ -5,9 +5,9 @@ import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.util
|
||||
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||
import { buildPaginatedList } from './paginated-list.model';
|
||||
import { PageInfo } from '../shared/page-info.model';
|
||||
import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model';
|
||||
import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model';
|
||||
import { RelationshipOptions } from '../../shared/form/builder/models/relationship-options.model';
|
||||
import { SearchResult } from '../../shared/search/search-result.model';
|
||||
import { SearchResult } from '../../shared/search/models/search-result.model';
|
||||
import { Item } from '../shared/item.model';
|
||||
import { skip, take } from 'rxjs/operators';
|
||||
import { ExternalSource } from '../shared/external-source.model';
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import { ExternalSourceService } from './external-source.service';
|
||||
import { SearchService } from '../shared/search/search.service';
|
||||
import { concat, distinctUntilChanged, map, multicast, startWith, take, takeWhile } from 'rxjs/operators';
|
||||
import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model';
|
||||
import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model';
|
||||
import { Observable, ReplaySubject } from 'rxjs';
|
||||
import { RemoteData } from './remote-data';
|
||||
import { PaginatedList } from './paginated-list.model';
|
||||
import { SearchResult } from '../../shared/search/search-result.model';
|
||||
import { SearchResult } from '../../shared/search/models/search-result.model';
|
||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||
import { RelationshipOptions } from '../../shared/form/builder/models/relationship-options.model';
|
||||
import { Item } from '../shared/item.model';
|
||||
|
@@ -4,7 +4,7 @@ import { DSpaceSerializer } from '../dspace-rest/dspace.serializer';
|
||||
import { RestRequest } from './request.models';
|
||||
import { RawRestResponse } from '../dspace-rest/raw-rest-response.model';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import { SearchObjects } from '../../shared/search/search-objects.model';
|
||||
import { SearchObjects } from '../../shared/search/models/search-objects.model';
|
||||
import { MetadataMap, MetadataValue } from '../shared/metadata.models';
|
||||
import { DspaceRestResponseParsingService } from './dspace-rest-response-parsing.service';
|
||||
|
||||
|
@@ -1,13 +1,16 @@
|
||||
import { RootDataService } from './root-data.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { RemoteData } from './remote-data';
|
||||
import { Root } from './root.model';
|
||||
import { RawRestResponse } from '../dspace-rest/raw-rest-response.model';
|
||||
import { cold } from 'jasmine-marbles';
|
||||
|
||||
describe('RootDataService', () => {
|
||||
let service: RootDataService;
|
||||
let halService: HALEndpointService;
|
||||
let restService;
|
||||
let rootEndpoint;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -15,7 +18,10 @@ describe('RootDataService', () => {
|
||||
halService = jasmine.createSpyObj('halService', {
|
||||
getRootHref: rootEndpoint
|
||||
});
|
||||
service = new RootDataService(null, null, null, null, halService, null, null, null);
|
||||
restService = jasmine.createSpyObj('halService', {
|
||||
get: jasmine.createSpy('get')
|
||||
});
|
||||
service = new RootDataService(null, null, null, null, halService, null, null, null, restService);
|
||||
(service as any).dataService = jasmine.createSpyObj('dataService', {
|
||||
findByHref: createSuccessfulRemoteDataObject$({})
|
||||
});
|
||||
@@ -35,4 +41,37 @@ describe('RootDataService', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkServerAvailability', () => {
|
||||
let result$: Observable<boolean>;
|
||||
|
||||
it('should return observable of true when root endpoint is available', () => {
|
||||
const mockResponse = {
|
||||
statusCode: 200,
|
||||
statusText: 'OK'
|
||||
} as RawRestResponse;
|
||||
|
||||
restService.get.and.returnValue(of(mockResponse));
|
||||
result$ = service.checkServerAvailability();
|
||||
|
||||
expect(result$).toBeObservable(cold('(a|)', {
|
||||
a: true
|
||||
}));
|
||||
});
|
||||
|
||||
it('should return observable of false when root endpoint is not available', () => {
|
||||
const mockResponse = {
|
||||
statusCode: 500,
|
||||
statusText: 'Internal Server Error'
|
||||
} as RawRestResponse;
|
||||
|
||||
restService.get.and.returnValue(of(mockResponse));
|
||||
result$ = service.checkServerAvailability();
|
||||
|
||||
expect(result$).toBeObservable(cold('(a|)', {
|
||||
a: false
|
||||
}));
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
@@ -17,6 +17,10 @@ import { RemoteData } from './remote-data';
|
||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
import { FindListOptions } from './request.models';
|
||||
import { PaginatedList } from './paginated-list.model';
|
||||
import { DspaceRestService } from '../dspace-rest/dspace-rest.service';
|
||||
import { RawRestResponse } from '../dspace-rest/raw-rest-response.model';
|
||||
import { catchError, map } from 'rxjs/operators';
|
||||
import { of } from 'rxjs/internal/observable/of';
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
|
||||
@@ -59,10 +63,24 @@ export class RootDataService {
|
||||
protected halService: HALEndpointService,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected http: HttpClient,
|
||||
protected comparator: DefaultChangeAnalyzer<Root>) {
|
||||
protected comparator: DefaultChangeAnalyzer<Root>,
|
||||
protected restService: DspaceRestService) {
|
||||
this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if root endpoint is available
|
||||
*/
|
||||
checkServerAvailability(): Observable<boolean> {
|
||||
return this.restService.get(this.halService.getRootHref()).pipe(
|
||||
catchError((err ) => {
|
||||
console.error(err);
|
||||
return of(false);
|
||||
}),
|
||||
map((res: RawRestResponse) => res.statusCode === 200)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the {@link Root} object of the REST API
|
||||
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
|
||||
@@ -106,5 +124,12 @@ export class RootDataService {
|
||||
findAllByHref(href: string | Observable<string>, findListOptions: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Root>[]): Observable<RemoteData<PaginatedList<Root>>> {
|
||||
return this.dataService.findAllByHref(href, findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to sale the root endpoint cache hit
|
||||
*/
|
||||
invalidateRootCache() {
|
||||
this.requestService.setStaleByHrefSubstring(this.halService.getRootHref());
|
||||
}
|
||||
}
|
||||
/* tslint:enable:max-classes-per-file */
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import { SearchObjects } from '../../shared/search/search-objects.model';
|
||||
import { SearchObjects } from '../../shared/search/models/search-objects.model';
|
||||
import { ParsedResponse } from '../cache/response.models';
|
||||
import { RawRestResponse } from '../dspace-rest/raw-rest-response.model';
|
||||
import { DSpaceSerializer } from '../dspace-rest/dspace.serializer';
|
||||
|
@@ -12,7 +12,7 @@ import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
||||
import { FindListOptions, PostRequest, RestRequest } from './request.models';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model';
|
||||
import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model';
|
||||
import { RemoteData } from './remote-data';
|
||||
import { PaginatedList } from './paginated-list.model';
|
||||
import { Version } from '../shared/version.model';
|
||||
|
@@ -40,9 +40,7 @@ const editGroupSelector = createSelector(groupRegistryStateSelector, (groupRegis
|
||||
/**
|
||||
* Provides methods to retrieve eperson group resources from the REST API & Group related CRUD actions.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
@Injectable()
|
||||
@dataService(GROUP)
|
||||
export class GroupDataService extends DataService<Group> {
|
||||
protected linkPath = 'groups';
|
||||
|
@@ -9,12 +9,17 @@ 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';
|
||||
import { CorrelationIdService } from '../../correlation-id/correlation-id.service';
|
||||
import { UUIDService } from '../shared/uuid.service';
|
||||
import { StoreModule } from '@ngrx/store';
|
||||
import { appReducers, storeModuleConfig } from '../../app.reducer';
|
||||
|
||||
|
||||
describe('LogInterceptor', () => {
|
||||
let service: DspaceRestService;
|
||||
let httpMock: HttpTestingController;
|
||||
let cookieService: CookieService;
|
||||
let correlationIdService: CorrelationIdService;
|
||||
const router = Object.assign(new RouterStub(),{url : '/statistics'});
|
||||
|
||||
// Mock payload/statuses are dummy content as we are not testing the results
|
||||
@@ -28,7 +33,10 @@ describe('LogInterceptor', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [HttpClientTestingModule],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
StoreModule.forRoot(appReducers, storeModuleConfig),
|
||||
],
|
||||
providers: [
|
||||
DspaceRestService,
|
||||
// LogInterceptor,
|
||||
@@ -39,14 +47,18 @@ describe('LogInterceptor', () => {
|
||||
},
|
||||
{ provide: CookieService, useValue: new CookieServiceMock() },
|
||||
{ provide: Router, useValue: router },
|
||||
{ provide: CorrelationIdService, useClass: CorrelationIdService },
|
||||
{ provide: UUIDService, useClass: UUIDService },
|
||||
],
|
||||
});
|
||||
|
||||
service = TestBed.get(DspaceRestService);
|
||||
httpMock = TestBed.get(HttpTestingController);
|
||||
cookieService = TestBed.get(CookieService);
|
||||
service = TestBed.inject(DspaceRestService);
|
||||
httpMock = TestBed.inject(HttpTestingController);
|
||||
cookieService = TestBed.inject(CookieService);
|
||||
correlationIdService = TestBed.inject(CorrelationIdService);
|
||||
|
||||
cookieService.set('CORRELATION-ID','123455');
|
||||
correlationIdService.initCorrelationId();
|
||||
});
|
||||
|
||||
|
||||
|
@@ -3,9 +3,8 @@ import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/c
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { CookieService } from '../services/cookie.service';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import { CorrelationIdService } from '../../correlation-id/correlation-id.service';
|
||||
|
||||
/**
|
||||
* Log Interceptor intercepting Http Requests & Responses to
|
||||
@@ -15,12 +14,12 @@ import { hasValue } from '../../shared/empty.util';
|
||||
@Injectable()
|
||||
export class LogInterceptor implements HttpInterceptor {
|
||||
|
||||
constructor(private cookieService: CookieService, private router: Router) {}
|
||||
constructor(private cidService: CorrelationIdService, private router: Router) {}
|
||||
|
||||
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||
|
||||
// Get Unique id of the user from the cookies
|
||||
const correlationId = this.cookieService.get('CORRELATION-ID');
|
||||
// Get the correlation id for the user from the store
|
||||
const correlationId = this.cidService.getCorrelationId();
|
||||
|
||||
// Add headers from the intercepted request
|
||||
let headers = request.headers;
|
||||
|
68
src/app/core/server-check/server-check.guard.spec.ts
Normal file
68
src/app/core/server-check/server-check.guard.spec.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { ServerCheckGuard } from './server-check.guard';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { of } from 'rxjs';
|
||||
import { take } from 'rxjs/operators';
|
||||
|
||||
import { getPageInternalServerErrorRoute } from '../../app-routing-paths';
|
||||
import { RootDataService } from '../data/root-data.service';
|
||||
import SpyObj = jasmine.SpyObj;
|
||||
|
||||
describe('ServerCheckGuard', () => {
|
||||
let guard: ServerCheckGuard;
|
||||
let router: SpyObj<Router>;
|
||||
let rootDataServiceStub: SpyObj<RootDataService>;
|
||||
|
||||
rootDataServiceStub = jasmine.createSpyObj('RootDataService', {
|
||||
checkServerAvailability: jasmine.createSpy('checkServerAvailability'),
|
||||
invalidateRootCache: jasmine.createSpy('invalidateRootCache')
|
||||
});
|
||||
router = jasmine.createSpyObj('Router', {
|
||||
navigateByUrl: jasmine.createSpy('navigateByUrl')
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
guard = new ServerCheckGuard(router, rootDataServiceStub);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
router.navigateByUrl.calls.reset();
|
||||
rootDataServiceStub.invalidateRootCache.calls.reset();
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(guard).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('when root endpoint has succeeded', () => {
|
||||
beforeEach(() => {
|
||||
rootDataServiceStub.checkServerAvailability.and.returnValue(of(true));
|
||||
});
|
||||
|
||||
it('should not redirect to error page', () => {
|
||||
guard.canActivateChild({} as any, {} as any).pipe(
|
||||
take(1)
|
||||
).subscribe((canActivate: boolean) => {
|
||||
expect(canActivate).toEqual(true);
|
||||
expect(rootDataServiceStub.invalidateRootCache).not.toHaveBeenCalled();
|
||||
expect(router.navigateByUrl).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when root endpoint has not succeeded', () => {
|
||||
beforeEach(() => {
|
||||
rootDataServiceStub.checkServerAvailability.and.returnValue(of(false));
|
||||
});
|
||||
|
||||
it('should redirect to error page', () => {
|
||||
guard.canActivateChild({} as any, {} as any).pipe(
|
||||
take(1)
|
||||
).subscribe((canActivate: boolean) => {
|
||||
expect(canActivate).toEqual(false);
|
||||
expect(rootDataServiceStub.invalidateRootCache).toHaveBeenCalled();
|
||||
expect(router.navigateByUrl).toHaveBeenCalledWith(getPageInternalServerErrorRoute());
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
39
src/app/core/server-check/server-check.guard.ts
Normal file
39
src/app/core/server-check/server-check.guard.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, CanActivateChild, Router, RouterStateSnapshot } from '@angular/router';
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import { take, tap } from 'rxjs/operators';
|
||||
|
||||
import { RootDataService } from '../data/root-data.service';
|
||||
import { getPageInternalServerErrorRoute } from '../../app-routing-paths';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
/**
|
||||
* A guard that checks if root api endpoint is reachable.
|
||||
* If not redirect to 500 error page
|
||||
*/
|
||||
export class ServerCheckGuard implements CanActivateChild {
|
||||
constructor(private router: Router, private rootDataService: RootDataService) {
|
||||
}
|
||||
|
||||
/**
|
||||
* True when root api endpoint is reachable.
|
||||
*/
|
||||
canActivateChild(
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot): Observable<boolean> {
|
||||
|
||||
return this.rootDataService.checkServerAvailability().pipe(
|
||||
take(1),
|
||||
tap((isAvailable: boolean) => {
|
||||
if (!isAvailable) {
|
||||
this.rootDataService.invalidateRootCache();
|
||||
this.router.navigateByUrl(getPageInternalServerErrorRoute());
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
}
|
||||
}
|
@@ -31,4 +31,8 @@ export class ServerResponseService {
|
||||
setNotFound(message = 'Not found'): this {
|
||||
return this.setStatus(404, message);
|
||||
}
|
||||
|
||||
setInternalServerError(message = 'Internal Server Error'): this {
|
||||
return this.setStatus(500, message);
|
||||
}
|
||||
}
|
||||
|
@@ -13,7 +13,7 @@ import {
|
||||
withLatestFrom
|
||||
} from 'rxjs/operators';
|
||||
import { hasNoValue, hasValue, hasValueOperator, isNotEmpty } from '../../shared/empty.util';
|
||||
import { SearchResult } from '../../shared/search/search-result.model';
|
||||
import { SearchResult } from '../../shared/search/models/search-result.model';
|
||||
import { PaginatedList } from '../data/paginated-list.model';
|
||||
import { RemoteData } from '../data/remote-data';
|
||||
import { RestRequest } from '../data/request.models';
|
||||
|
@@ -2,8 +2,8 @@ import { SearchConfigurationService } from './search-configuration.service';
|
||||
import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub';
|
||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||
import { SortDirection, SortOptions } from '../../cache/models/sort-options.model';
|
||||
import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model';
|
||||
import { SearchFilter } from '../../../shared/search/search-filter.model';
|
||||
import { PaginatedSearchOptions } from '../../../shared/search/models/paginated-search-options.model';
|
||||
import { SearchFilter } from '../../../shared/search/models/search-filter.model';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
||||
|
||||
|
@@ -3,31 +3,27 @@ import { ActivatedRoute, Params } from '@angular/router';
|
||||
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatest,
|
||||
combineLatest as observableCombineLatest,
|
||||
merge as observableMerge,
|
||||
Observable,
|
||||
Subscription
|
||||
} from 'rxjs';
|
||||
import { distinctUntilChanged, filter, map, startWith, switchMap, take } from 'rxjs/operators';
|
||||
import { filter, map, startWith } 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';
|
||||
import { SearchFilter } from '../../../shared/search/search-filter.model';
|
||||
import { SearchOptions } from '../../../shared/search/models/search-options.model';
|
||||
import { PaginatedSearchOptions } from '../../../shared/search/models/paginated-search-options.model';
|
||||
import { SearchFilter } from '../../../shared/search/models/search-filter.model';
|
||||
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 {
|
||||
getAllSucceededRemoteDataPayload,
|
||||
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 { SearchConfig, SortConfig } from './search-filters/search-config.model';
|
||||
import { SearchService } from './search.service';
|
||||
import { of } from 'rxjs';
|
||||
import { PaginationService } from '../../pagination/pagination.service';
|
||||
import { ViewMode } from '../view-mode.model';
|
||||
|
||||
/**
|
||||
* Service that performs all actions that have to do with the current search configuration
|
||||
@@ -35,7 +31,21 @@ import { PaginationService } from '../../pagination/pagination.service';
|
||||
@Injectable()
|
||||
export class SearchConfigurationService implements OnDestroy {
|
||||
|
||||
/**
|
||||
* Default pagination id
|
||||
*/
|
||||
public paginationID = 'spc';
|
||||
|
||||
/**
|
||||
* Emits the current search options
|
||||
*/
|
||||
public searchOptions: BehaviorSubject<SearchOptions>;
|
||||
|
||||
/**
|
||||
* Emits the current search options including pagination and sort
|
||||
*/
|
||||
public paginatedSearchOptions: BehaviorSubject<PaginatedSearchOptions>;
|
||||
|
||||
/**
|
||||
* Default pagination settings
|
||||
*/
|
||||
@@ -45,16 +55,6 @@ export class SearchConfigurationService implements OnDestroy {
|
||||
currentPage: 1
|
||||
});
|
||||
|
||||
/**
|
||||
* Default sort settings
|
||||
*/
|
||||
protected defaultSort = new SortOptions('score', SortDirection.DESC);
|
||||
|
||||
/**
|
||||
* Default configuration parameter setting
|
||||
*/
|
||||
protected defaultConfiguration;
|
||||
|
||||
/**
|
||||
* Default scope setting
|
||||
*/
|
||||
@@ -71,23 +71,14 @@ export class SearchConfigurationService implements OnDestroy {
|
||||
protected _defaults: Observable<RemoteData<PaginatedSearchOptions>>;
|
||||
|
||||
/**
|
||||
* Emits the current search options
|
||||
* A map of subscriptions to unsubscribe from on destroy
|
||||
*/
|
||||
public searchOptions: BehaviorSubject<SearchOptions>;
|
||||
|
||||
/**
|
||||
* Emits the current search options including pagination and sort
|
||||
*/
|
||||
public paginatedSearchOptions: BehaviorSubject<PaginatedSearchOptions>;
|
||||
|
||||
/**
|
||||
* List of subscriptions to unsubscribe from on destroy
|
||||
*/
|
||||
protected subs: Subscription[] = [];
|
||||
protected subs: Map<string, Subscription[]> = new Map<string, Subscription[]>(null);
|
||||
|
||||
/**
|
||||
* Initialize the search options
|
||||
* @param {RouteService} routeService
|
||||
* @param {PaginationService} paginationService
|
||||
* @param {ActivatedRoute} route
|
||||
*/
|
||||
constructor(protected routeService: RouteService,
|
||||
@@ -98,29 +89,28 @@ export class SearchConfigurationService implements OnDestroy {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the search options
|
||||
* Default values for the Search Options
|
||||
*/
|
||||
protected initDefaults() {
|
||||
this.defaults
|
||||
.pipe(getFirstSucceededRemoteData())
|
||||
.subscribe((defRD: RemoteData<PaginatedSearchOptions>) => {
|
||||
const defs = defRD.payload;
|
||||
this.paginatedSearchOptions = new BehaviorSubject<PaginatedSearchOptions>(defs);
|
||||
this.searchOptions = new BehaviorSubject<SearchOptions>(defs);
|
||||
this.subs.push(this.subscribeToSearchOptions(defs));
|
||||
this.subs.push(this.subscribeToPaginatedSearchOptions(defs.pagination.id, defs));
|
||||
get defaults(): Observable<RemoteData<PaginatedSearchOptions>> {
|
||||
if (hasNoValue(this._defaults)) {
|
||||
const options = new PaginatedSearchOptions({
|
||||
pagination: this.defaultPagination,
|
||||
scope: this.defaultScope,
|
||||
query: this.defaultQuery
|
||||
});
|
||||
this._defaults = createSuccessfulRemoteDataObject$(options, new Date().getTime());
|
||||
}
|
||||
);
|
||||
return this._defaults;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Observable<string>} Emits the current configuration string
|
||||
*/
|
||||
getCurrentConfiguration(defaultConfiguration: string) {
|
||||
return observableCombineLatest(
|
||||
return observableCombineLatest([
|
||||
this.routeService.getQueryParameterValue('configuration').pipe(startWith(undefined)),
|
||||
this.routeService.getRouteParameterValue('configuration').pipe(startWith(undefined))
|
||||
).pipe(
|
||||
]).pipe(
|
||||
map(([queryConfig, routeConfig]) => {
|
||||
return queryConfig || routeConfig || defaultConfiguration;
|
||||
})
|
||||
@@ -208,59 +198,91 @@ export class SearchConfigurationService implements OnDestroy {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an observable of SearchConfig every time the configuration$ stream emits.
|
||||
* @param configuration$
|
||||
* @param service
|
||||
* @returns {Observable<string>} Emits the current view mode
|
||||
*/
|
||||
getConfigurationSearchConfigObservable(configuration$: Observable<string>, service: SearchService): Observable<SearchConfig> {
|
||||
return configuration$.pipe(
|
||||
distinctUntilChanged(),
|
||||
switchMap((configuration) => service.getSearchConfigurationFor(null, configuration)),
|
||||
getAllSucceededRemoteDataPayload());
|
||||
}
|
||||
|
||||
/**
|
||||
* Every time searchConfig change (after a configuration change) it update the navigation with the default sort option
|
||||
* and emit the new paginateSearchOptions value.
|
||||
* @param configuration$
|
||||
* @param service
|
||||
*/
|
||||
initializeSortOptionsFromConfiguration(searchConfig$: Observable<SearchConfig>) {
|
||||
const subscription = searchConfig$.pipe(switchMap((searchConfig) => combineLatest([
|
||||
of(searchConfig),
|
||||
this.paginatedSearchOptions.pipe(take(1))
|
||||
]))).subscribe(([searchConfig, searchOptions]) => {
|
||||
const field = searchConfig.sortOptions[0].name;
|
||||
const direction = searchConfig.sortOptions[0].sortOrder.toLowerCase() === SortDirection.ASC.toLowerCase() ? SortDirection.ASC : SortDirection.DESC;
|
||||
const updateValue = Object.assign(new PaginatedSearchOptions({}), searchOptions, {
|
||||
sort: new SortOptions(field, direction)
|
||||
});
|
||||
this.paginationService.updateRoute(this.paginationID,
|
||||
{
|
||||
sortDirection: updateValue.sort.direction,
|
||||
sortField: updateValue.sort.field,
|
||||
});
|
||||
this.paginatedSearchOptions.next(updateValue);
|
||||
});
|
||||
this.subs.push(subscription);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an observable of available SortOptions[] every time the searchConfig$ stream emits.
|
||||
* @param searchConfig$
|
||||
* @param service
|
||||
*/
|
||||
getConfigurationSortOptionsObservable(searchConfig$: Observable<SearchConfig>): Observable<SortOptions[]> {
|
||||
return searchConfig$.pipe(map((searchConfig) => {
|
||||
const sortOptions = [];
|
||||
searchConfig.sortOptions.forEach(sortOption => {
|
||||
sortOptions.push(new SortOptions(sortOption.name, SortDirection.ASC));
|
||||
sortOptions.push(new SortOptions(sortOption.name, SortDirection.DESC));
|
||||
});
|
||||
return sortOptions;
|
||||
getCurrentViewMode(defaultViewMode: ViewMode) {
|
||||
return this.routeService.getQueryParameterValue('view').pipe(map((viewMode) => {
|
||||
return viewMode || defaultViewMode;
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an observable of SearchConfig every time the configuration stream emits.
|
||||
* @param configuration The search configuration
|
||||
* @param service The search service to use
|
||||
* @param scope The search scope if exists
|
||||
*/
|
||||
getConfigurationSearchConfig(configuration: string, service: SearchService, scope?: string): Observable<SearchConfig> {
|
||||
return service.getSearchConfigurationFor(scope, configuration).pipe(
|
||||
getAllSucceededRemoteDataPayload()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the SortOptions list available for the given SearchConfig
|
||||
* @param searchConfig The SearchConfig object
|
||||
*/
|
||||
getConfigurationSortOptions(searchConfig: SearchConfig): SortOptions[] {
|
||||
return searchConfig.sortOptions.map((entry: SortConfig) => ({
|
||||
field: entry.name,
|
||||
direction: entry.sortOrder.toLowerCase() === SortDirection.ASC.toLowerCase() ? SortDirection.ASC : SortDirection.DESC
|
||||
}));
|
||||
}
|
||||
|
||||
setPaginationId(paginationId): void {
|
||||
if (isNotEmpty(paginationId)) {
|
||||
const currentValue: PaginatedSearchOptions = this.paginatedSearchOptions.getValue();
|
||||
const updatedValue: PaginatedSearchOptions = Object.assign(new PaginatedSearchOptions({}), currentValue, {
|
||||
pagination: Object.assign({}, currentValue.pagination, {
|
||||
id: paginationId
|
||||
})
|
||||
});
|
||||
// unsubscribe from subscription related to old pagination id
|
||||
this.unsubscribeFromSearchOptions(this.paginationID);
|
||||
|
||||
// change to the new pagination id
|
||||
this.paginationID = paginationId;
|
||||
this.paginatedSearchOptions.next(updatedValue);
|
||||
this.setSearchSubscription(this.paginationID, this.paginatedSearchOptions.value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure to unsubscribe from all existing subscription to prevent memory leaks
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.subs
|
||||
.forEach((subs: Subscription[]) => subs
|
||||
.filter((sub) => hasValue(sub))
|
||||
.forEach((sub) => sub.unsubscribe())
|
||||
);
|
||||
|
||||
this.subs = new Map<string, Subscription[]>(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the search options
|
||||
*/
|
||||
protected initDefaults() {
|
||||
this.defaults
|
||||
.pipe(getFirstSucceededRemoteData())
|
||||
.subscribe((defRD: RemoteData<PaginatedSearchOptions>) => {
|
||||
const defs = defRD.payload;
|
||||
this.paginatedSearchOptions = new BehaviorSubject<PaginatedSearchOptions>(defs);
|
||||
this.searchOptions = new BehaviorSubject<SearchOptions>(defs);
|
||||
this.setSearchSubscription(this.paginationID, defs);
|
||||
});
|
||||
}
|
||||
|
||||
private setSearchSubscription(paginationID: string, defaults: PaginatedSearchOptions) {
|
||||
this.unsubscribeFromSearchOptions(paginationID);
|
||||
const subs = [
|
||||
this.subscribeToSearchOptions(defaults),
|
||||
this.subscribeToPaginatedSearchOptions(paginationID || defaults.pagination.id, defaults)
|
||||
];
|
||||
this.subs.set(this.paginationID, subs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -273,7 +295,8 @@ export class SearchConfigurationService implements OnDestroy {
|
||||
this.getQueryPart(defaults.query),
|
||||
this.getDSOTypePart(),
|
||||
this.getFiltersPart(),
|
||||
this.getFixedFilterPart()
|
||||
this.getFixedFilterPart(),
|
||||
this.getViewModePart(defaults.view)
|
||||
).subscribe((update) => {
|
||||
const currentValue: SearchOptions = this.searchOptions.getValue();
|
||||
const updatedValue: SearchOptions = Object.assign(new PaginatedSearchOptions({}), currentValue, update);
|
||||
@@ -283,19 +306,21 @@ export class SearchConfigurationService implements OnDestroy {
|
||||
|
||||
/**
|
||||
* Sets up a subscription to all necessary parameters to make sure the paginatedSearchOptions emits a new value every time they update
|
||||
* @param {string} paginationId The pagination ID
|
||||
* @param {PaginatedSearchOptions} defaults Default values for when no parameters are available
|
||||
* @returns {Subscription} The subscription to unsubscribe from
|
||||
*/
|
||||
private subscribeToPaginatedSearchOptions(paginationId: string, defaults: PaginatedSearchOptions): Subscription {
|
||||
return observableMerge(
|
||||
this.getConfigurationPart(defaults.configuration),
|
||||
this.getPaginationPart(paginationId, defaults.pagination),
|
||||
this.getSortPart(paginationId, defaults.sort),
|
||||
this.getConfigurationPart(defaults.configuration),
|
||||
this.getScopePart(defaults.scope),
|
||||
this.getQueryPart(defaults.query),
|
||||
this.getDSOTypePart(),
|
||||
this.getFiltersPart(),
|
||||
this.getFixedFilterPart()
|
||||
this.getFixedFilterPart(),
|
||||
this.getViewModePart(defaults.view)
|
||||
).subscribe((update) => {
|
||||
const currentValue: PaginatedSearchOptions = this.paginatedSearchOptions.getValue();
|
||||
const updatedValue: PaginatedSearchOptions = Object.assign(new PaginatedSearchOptions({}), currentValue, update);
|
||||
@@ -304,30 +329,16 @@ export class SearchConfigurationService implements OnDestroy {
|
||||
}
|
||||
|
||||
/**
|
||||
* Default values for the Search Options
|
||||
* Unsubscribe from all subscriptions related to the given paginationID
|
||||
* @param paginationId The pagination id
|
||||
*/
|
||||
get defaults(): Observable<RemoteData<PaginatedSearchOptions>> {
|
||||
if (hasNoValue(this._defaults)) {
|
||||
const options = new PaginatedSearchOptions({
|
||||
pagination: this.defaultPagination,
|
||||
configuration: this.defaultConfiguration,
|
||||
sort: this.defaultSort,
|
||||
scope: this.defaultScope,
|
||||
query: this.defaultQuery
|
||||
});
|
||||
this._defaults = createSuccessfulRemoteDataObject$(options, new Date().getTime());
|
||||
private unsubscribeFromSearchOptions(paginationId: string): void {
|
||||
if (this.subs.has(this.paginationID)) {
|
||||
this.subs.get(this.paginationID)
|
||||
.filter((sub) => hasValue(sub))
|
||||
.forEach((sub) => sub.unsubscribe());
|
||||
this.subs.delete(paginationId);
|
||||
}
|
||||
return this._defaults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure to unsubscribe from all existing subscription to prevent memory leaks
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.subs.forEach((sub) => {
|
||||
sub.unsubscribe();
|
||||
});
|
||||
this.subs = [];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -404,4 +415,13 @@ export class SearchConfigurationService implements OnDestroy {
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Observable<Params>} Emits the current view mode as a partial SearchOptions object
|
||||
*/
|
||||
private getViewModePart(defaultViewMode: ViewMode): Observable<any> {
|
||||
return this.getCurrentViewMode(defaultViewMode).pipe(map((view) => {
|
||||
return { view };
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
@@ -10,8 +10,8 @@ import {
|
||||
SearchFilterToggleAction
|
||||
} from '../../../shared/search/search-filters/search-filter/search-filter.actions';
|
||||
import { SearchFiltersState } from '../../../shared/search/search-filters/search-filter/search-filter.reducer';
|
||||
import { SearchFilterConfig } from '../../../shared/search/search-filter-config.model';
|
||||
import { FilterType } from '../../../shared/search/filter-type.model';
|
||||
import { SearchFilterConfig } from '../../../shared/search/models/search-filter-config.model';
|
||||
import { FilterType } from '../../../shared/search/models/filter-type.model';
|
||||
import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { SortDirection, SortOptions } from '../../cache/models/sort-options.model';
|
||||
|
@@ -16,7 +16,7 @@ import {
|
||||
SearchFilterToggleAction
|
||||
} from '../../../shared/search/search-filters/search-filter/search-filter.actions';
|
||||
import { hasValue, isNotEmpty, } from '../../../shared/empty.util';
|
||||
import { SearchFilterConfig } from '../../../shared/search/search-filter-config.model';
|
||||
import { SearchFilterConfig } from '../../../shared/search/models/search-filter-config.model';
|
||||
import { SortDirection, SortOptions } from '../../cache/models/sort-options.model';
|
||||
import { RouteService } from '../../services/route.service';
|
||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||
|
@@ -29,7 +29,7 @@ export class SearchConfig implements CacheableObject {
|
||||
* The configured sort options.
|
||||
*/
|
||||
@autoserialize
|
||||
sortOptions: SortOption[];
|
||||
sortOptions: SortConfig[];
|
||||
|
||||
/**
|
||||
* The object type.
|
||||
@@ -63,7 +63,7 @@ export interface FilterConfig {
|
||||
/**
|
||||
* Interface to model sort option's configuration.
|
||||
*/
|
||||
export interface SortOption {
|
||||
export interface SortConfig {
|
||||
name: string;
|
||||
sortOrder: string;
|
||||
}
|
||||
|
@@ -9,7 +9,7 @@ import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub';
|
||||
import { RouterStub } from '../../../shared/testing/router.stub';
|
||||
import { HALEndpointService } from '../hal-endpoint.service';
|
||||
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
|
||||
import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model';
|
||||
import { PaginatedSearchOptions } from '../../../shared/search/models/paginated-search-options.model';
|
||||
import { RemoteData } from '../../data/remote-data';
|
||||
import { RequestEntry } from '../../data/request.reducer';
|
||||
import { getMockRequestService } from '../../../shared/mocks/request.service.mock';
|
||||
@@ -21,11 +21,8 @@ import { RouteService } from '../../services/route.service';
|
||||
import { routeServiceStub } from '../../../shared/testing/route-service.stub';
|
||||
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||
import { SearchObjects } from '../../../shared/search/search-objects.model';
|
||||
import { SearchObjects } from '../../../shared/search/models/search-objects.model';
|
||||
import { PaginationService } from '../../pagination/pagination.service';
|
||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||
import { SortDirection, SortOptions } from '../../cache/models/sort-options.model';
|
||||
import { FindListOptions } from '../../data/request.models';
|
||||
import { SearchConfigurationService } from './search-configuration.service';
|
||||
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
||||
|
||||
|
@@ -14,24 +14,24 @@ import { GenericConstructor } from '../generic-constructor';
|
||||
import { HALEndpointService } from '../hal-endpoint.service';
|
||||
import { URLCombiner } from '../../url-combiner/url-combiner';
|
||||
import { hasValue, hasValueOperator, isNotEmpty } from '../../../shared/empty.util';
|
||||
import { SearchOptions } from '../../../shared/search/search-options.model';
|
||||
import { SearchFilterConfig } from '../../../shared/search/search-filter-config.model';
|
||||
import { SearchOptions } from '../../../shared/search/models/search-options.model';
|
||||
import { SearchFilterConfig } from '../../../shared/search/models/search-filter-config.model';
|
||||
import { SearchResponseParsingService } from '../../data/search-response-parsing.service';
|
||||
import { SearchObjects } from '../../../shared/search/search-objects.model';
|
||||
import { SearchObjects } from '../../../shared/search/models/search-objects.model';
|
||||
import { FacetValueResponseParsingService } from '../../data/facet-value-response-parsing.service';
|
||||
import { FacetConfigResponseParsingService } from '../../data/facet-config-response-parsing.service';
|
||||
import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model';
|
||||
import { PaginatedSearchOptions } from '../../../shared/search/models/paginated-search-options.model';
|
||||
import { CommunityDataService } from '../../data/community-data.service';
|
||||
import { ViewMode } from '../view-mode.model';
|
||||
import { DSpaceObjectDataService } from '../../data/dspace-object-data.service';
|
||||
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
||||
import { getFirstCompletedRemoteData, getRemoteDataPayload } from '../operators';
|
||||
import { RouteService } from '../../services/route.service';
|
||||
import { SearchResult } from '../../../shared/search/search-result.model';
|
||||
import { SearchResult } from '../../../shared/search/models/search-result.model';
|
||||
import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model';
|
||||
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 { FacetConfigResponse } from '../../../shared/search/models/facet-config-response.model';
|
||||
import { FacetValues } from '../../../shared/search/models/facet-values.model';
|
||||
import { SearchConfig } from './search-filters/search-config.model';
|
||||
import { PaginationService } from '../../pagination/pagination.service';
|
||||
import { SearchConfigurationService } from './search-configuration.service';
|
||||
@@ -407,6 +407,7 @@ export class SearchService implements OnDestroy {
|
||||
/**
|
||||
* Changes the current view mode in the current URL
|
||||
* @param {ViewMode} viewMode Mode to switch to
|
||||
* @param {string[]} searchLinkParts
|
||||
*/
|
||||
setViewMode(viewMode: ViewMode, searchLinkParts?: string[]) {
|
||||
this.paginationService.getCurrentPagination(this.searchConfigurationService.paginationID, new PaginationComponentOptions()).pipe(take(1))
|
||||
|
@@ -47,6 +47,12 @@ export class Version extends DSpaceObject {
|
||||
@autoserialize
|
||||
summary: string;
|
||||
|
||||
/**
|
||||
* The name of the submitter of this version
|
||||
*/
|
||||
@autoserialize
|
||||
submitterName: string;
|
||||
|
||||
/**
|
||||
* The Date this version was created
|
||||
*/
|
||||
|
@@ -3,8 +3,8 @@
|
||||
*/
|
||||
|
||||
export enum ViewMode {
|
||||
ListElement = 'listElement',
|
||||
GridElement = 'gridElement',
|
||||
DetailedListElement = 'detailedListElement',
|
||||
StandalonePage = 'standalonePage',
|
||||
ListElement = 'list',
|
||||
GridElement = 'grid',
|
||||
DetailedListElement = 'detailed',
|
||||
StandalonePage = 'standalone',
|
||||
}
|
||||
|
21
src/app/correlation-id/correlation-id.actions.ts
Normal file
21
src/app/correlation-id/correlation-id.actions.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { type } from '../shared/ngrx/type';
|
||||
import { Action } from '@ngrx/store';
|
||||
|
||||
export const CorrelationIDActionTypes = {
|
||||
SET: type('dspace/core/correlationId/SET')
|
||||
};
|
||||
|
||||
/**
|
||||
* Action for setting a new correlation ID
|
||||
*/
|
||||
export class SetCorrelationIdAction implements Action {
|
||||
type = CorrelationIDActionTypes.SET;
|
||||
|
||||
constructor(public payload: string) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Type alias for all correlation ID actions
|
||||
*/
|
||||
export type CorrelationIdAction = SetCorrelationIdAction;
|
23
src/app/correlation-id/correlation-id.reducer.spec.ts
Normal file
23
src/app/correlation-id/correlation-id.reducer.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { correlationIdReducer } from './correlation-id.reducer';
|
||||
import { SetCorrelationIdAction } from './correlation-id.actions';
|
||||
|
||||
describe('correlationIdReducer', () => {
|
||||
it('should set the correlatinId with SET action', () => {
|
||||
const initialState = null;
|
||||
const currentState = correlationIdReducer(initialState, new SetCorrelationIdAction('new ID'));
|
||||
|
||||
expect(currentState).toBe('new ID');
|
||||
});
|
||||
|
||||
it('should leave correlatinId unchanged otherwise', () => {
|
||||
const initialState = null;
|
||||
|
||||
let currentState = correlationIdReducer(initialState, { type: 'unknown' } as any);
|
||||
expect(currentState).toBe(null);
|
||||
|
||||
currentState = correlationIdReducer(currentState, new SetCorrelationIdAction('new ID'));
|
||||
currentState = correlationIdReducer(currentState, { type: 'unknown' } as any);
|
||||
|
||||
expect(currentState).toBe('new ID');
|
||||
});
|
||||
});
|
27
src/app/correlation-id/correlation-id.reducer.ts
Normal file
27
src/app/correlation-id/correlation-id.reducer.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import {
|
||||
CorrelationIdAction,
|
||||
CorrelationIDActionTypes,
|
||||
SetCorrelationIdAction
|
||||
} from './correlation-id.actions';
|
||||
import { AppState } from '../app.reducer';
|
||||
|
||||
const initialState = null;
|
||||
|
||||
export const correlationIdSelector = (state: AppState) => state.correlationId;
|
||||
|
||||
/**
|
||||
* Reducer that handles actions to update the correlation ID
|
||||
* @param {string} state the previous correlation ID (null if unset)
|
||||
* @param {CorrelationIdAction} action the action to perform
|
||||
* @return {string} the new correlation ID
|
||||
*/
|
||||
export const correlationIdReducer = (state = initialState, action: CorrelationIdAction): string => {
|
||||
switch (action.type) {
|
||||
case CorrelationIDActionTypes.SET: {
|
||||
return (action as SetCorrelationIdAction).payload;
|
||||
}
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
};
|
83
src/app/correlation-id/correlation-id.service.spec.ts
Normal file
83
src/app/correlation-id/correlation-id.service.spec.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { CorrelationIdService } from './correlation-id.service';
|
||||
import { CookieServiceMock } from '../shared/mocks/cookie.service.mock';
|
||||
import { UUIDService } from '../core/shared/uuid.service';
|
||||
import { MockStore, provideMockStore } from '@ngrx/store/testing';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { Store, StoreModule } from '@ngrx/store';
|
||||
import { appReducers, AppState, storeModuleConfig } from '../app.reducer';
|
||||
import { SetCorrelationIdAction } from './correlation-id.actions';
|
||||
|
||||
describe('CorrelationIdService', () => {
|
||||
let service: CorrelationIdService;
|
||||
|
||||
let cookieService;
|
||||
let uuidService;
|
||||
let store;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
StoreModule.forRoot(appReducers, storeModuleConfig),
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cookieService = new CookieServiceMock();
|
||||
uuidService = new UUIDService();
|
||||
store = TestBed.inject(Store) as MockStore<AppState>;
|
||||
service = new CorrelationIdService(cookieService, uuidService, store);
|
||||
});
|
||||
|
||||
describe('getCorrelationId', () => {
|
||||
it('should get from from store', () => {
|
||||
expect(service.getCorrelationId()).toBe(null);
|
||||
store.dispatch(new SetCorrelationIdAction('some value'));
|
||||
expect(service.getCorrelationId()).toBe('some value');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('initCorrelationId', () => {
|
||||
const cookieCID = 'cookie CID';
|
||||
const storeCID = 'store CID';
|
||||
|
||||
it('should set cookie and store values to a newly generated value if neither ex', () => {
|
||||
service.initCorrelationId();
|
||||
|
||||
expect(cookieService.get('CORRELATION-ID')).toBeTruthy();
|
||||
expect(service.getCorrelationId()).toBeTruthy();
|
||||
expect(cookieService.get('CORRELATION-ID')).toEqual(service.getCorrelationId());
|
||||
});
|
||||
|
||||
it('should set store value to cookie value if present', () => {
|
||||
expect(service.getCorrelationId()).toBe(null);
|
||||
|
||||
cookieService.set('CORRELATION-ID', cookieCID);
|
||||
|
||||
service.initCorrelationId();
|
||||
|
||||
expect(cookieService.get('CORRELATION-ID')).toBe(cookieCID);
|
||||
expect(service.getCorrelationId()).toBe(cookieCID);
|
||||
});
|
||||
|
||||
it('should set cookie value to store value if present', () => {
|
||||
store.dispatch(new SetCorrelationIdAction(storeCID));
|
||||
|
||||
service.initCorrelationId();
|
||||
|
||||
expect(cookieService.get('CORRELATION-ID')).toBe(storeCID);
|
||||
expect(service.getCorrelationId()).toBe(storeCID);
|
||||
});
|
||||
|
||||
it('should set store value to cookie value if both are present', () => {
|
||||
cookieService.set('CORRELATION-ID', cookieCID);
|
||||
store.dispatch(new SetCorrelationIdAction(storeCID));
|
||||
|
||||
service.initCorrelationId();
|
||||
|
||||
expect(cookieService.get('CORRELATION-ID')).toBe(cookieCID);
|
||||
expect(service.getCorrelationId()).toBe(cookieCID);
|
||||
});
|
||||
});
|
||||
});
|
64
src/app/correlation-id/correlation-id.service.ts
Normal file
64
src/app/correlation-id/correlation-id.service.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { CookieService } from '../core/services/cookie.service';
|
||||
import { UUIDService } from '../core/shared/uuid.service';
|
||||
import { Store, select } from '@ngrx/store';
|
||||
import { AppState } from '../app.reducer';
|
||||
import { isEmpty } from '../shared/empty.util';
|
||||
import { correlationIdSelector } from './correlation-id.reducer';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { SetCorrelationIdAction } from './correlation-id.actions';
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
/**
|
||||
* Service to manage the correlation id, an id used to give context to server side logs
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class CorrelationIdService {
|
||||
|
||||
constructor(
|
||||
protected cookieService: CookieService,
|
||||
protected uuidService: UUIDService,
|
||||
protected store: Store<AppState>,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the correlation id based on the cookie or the ngrx store
|
||||
*/
|
||||
initCorrelationId(): void {
|
||||
// first see of there's a cookie with a correlation-id
|
||||
let correlationId = this.cookieService.get('CORRELATION-ID');
|
||||
|
||||
// if there isn't see if there's an ID in the store
|
||||
if (isEmpty(correlationId)) {
|
||||
correlationId = this.getCorrelationId();
|
||||
}
|
||||
|
||||
// if no id was found, create a new id
|
||||
if (isEmpty(correlationId)) {
|
||||
correlationId = this.uuidService.generate();
|
||||
}
|
||||
|
||||
// Store the correct id both in the store and as a cookie to ensure they're in sync
|
||||
this.store.dispatch(new SetCorrelationIdAction(correlationId));
|
||||
this.cookieService.set('CORRELATION-ID', correlationId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the correlation id from the store
|
||||
*/
|
||||
getCorrelationId(): string {
|
||||
let correlationId;
|
||||
|
||||
this.store.pipe(
|
||||
select(correlationIdSelector),
|
||||
take(1)
|
||||
).subscribe((storeId: string) => {
|
||||
// we can do this because ngrx selects are synchronous
|
||||
correlationId = storeId;
|
||||
});
|
||||
|
||||
return correlationId;
|
||||
}
|
||||
}
|
@@ -19,6 +19,7 @@ import { JournalVolumeSearchResultGridElementComponent } from './item-grid-eleme
|
||||
import { JournalVolumeSidebarSearchListElementComponent } from './item-list-elements/sidebar-search-list-elements/journal-volume/journal-volume-sidebar-search-list-element.component';
|
||||
import { JournalIssueSidebarSearchListElementComponent } from './item-list-elements/sidebar-search-list-elements/journal-issue/journal-issue-sidebar-search-list-element.component';
|
||||
import { JournalSidebarSearchListElementComponent } from './item-list-elements/sidebar-search-list-elements/journal/journal-sidebar-search-list-element.component';
|
||||
import { ItemSharedModule } from '../../item-page/item-shared.module';
|
||||
|
||||
const ENTRY_COMPONENTS = [
|
||||
// put only entry components that use custom decorator
|
||||
@@ -45,6 +46,7 @@ const ENTRY_COMPONENTS = [
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
ItemSharedModule,
|
||||
SharedModule
|
||||
],
|
||||
declarations: [
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { OrgUnitComponent } from './item-pages/org-unit/org-unit.component';
|
||||
import { PersonComponent } from './item-pages/person/person.component';
|
||||
@@ -27,6 +28,7 @@ import { ExternalSourceEntryListSubmissionElementComponent } from './submission/
|
||||
import { OrgUnitSidebarSearchListElementComponent } from './item-list-elements/sidebar-search-list-elements/org-unit/org-unit-sidebar-search-list-element.component';
|
||||
import { PersonSidebarSearchListElementComponent } from './item-list-elements/sidebar-search-list-elements/person/person-sidebar-search-list-element.component';
|
||||
import { ProjectSidebarSearchListElementComponent } from './item-list-elements/sidebar-search-list-elements/project/project-sidebar-search-list-element.component';
|
||||
import { ItemSharedModule } from '../../item-page/item-shared.module';
|
||||
|
||||
const ENTRY_COMPONENTS = [
|
||||
// put only entry components that use custom decorator
|
||||
@@ -65,7 +67,9 @@ const COMPONENTS = [
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
SharedModule
|
||||
ItemSharedModule,
|
||||
SharedModule,
|
||||
NgbTooltipModule
|
||||
],
|
||||
declarations: [
|
||||
...COMPONENTS,
|
||||
@@ -79,7 +83,7 @@ export class ResearchEntitiesModule {
|
||||
static withEntryComponents() {
|
||||
return {
|
||||
ngModule: ResearchEntitiesModule,
|
||||
providers: ENTRY_COMPONENTS.map((component) => ({provide: component}))
|
||||
providers: ENTRY_COMPONENTS.map((component) => ({ provide: component }))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -15,7 +15,7 @@
|
||||
<div class="autocomplete dropdown-menu" [ngClass]="{'show': (show | async) && isNotEmpty(suggestions)}">
|
||||
<div class="dropdown-list">
|
||||
<div *ngFor="let suggestionOption of suggestions">
|
||||
<a href="#" class="d-block dropdown-item" (click)="onClickSuggestion(suggestionOption)" #suggestion>
|
||||
<a href="javascript:void(0);" class="d-block dropdown-item" (click)="onClickSuggestion(suggestionOption)" #suggestion>
|
||||
<span [innerHTML]="suggestionOption"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
@@ -16,7 +16,7 @@
|
||||
<div class="autocomplete dropdown-menu" [ngClass]="{'show': (show | async) && isNotEmpty(suggestions)}">
|
||||
<div class="dropdown-list">
|
||||
<div *ngFor="let suggestionOption of suggestions">
|
||||
<a href="#" class="d-block dropdown-item" (click)="onClickSuggestion(suggestionOption)" #suggestion>
|
||||
<a href="javascript:void(0);" class="d-block dropdown-item" (click)="onClickSuggestion(suggestionOption)" #suggestion>
|
||||
<span [innerHTML]="suggestionOption"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
@@ -64,7 +64,7 @@
|
||||
</p>
|
||||
<ul class="footer-info list-unstyled small d-flex justify-content-center mb-0">
|
||||
<li>
|
||||
<a class="text-white" href="#"
|
||||
<a class="text-white" href="javascript:void(0);"
|
||||
(click)="showCookieSettings()">{{ 'footer.link.cookies' | translate}}</a>
|
||||
</li>
|
||||
<li>
|
||||
|
@@ -1,5 +1,8 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { EditItemPageRoutingModule } from './edit-item-page.routing.module';
|
||||
import { EditItemPageComponent } from './edit-item-page.component';
|
||||
@@ -31,6 +34,8 @@ import { VirtualMetadataComponent } from './virtual-metadata/virtual-metadata.co
|
||||
import { ItemVersionHistoryComponent } from './item-version-history/item-version-history.component';
|
||||
import { ItemAuthorizationsComponent } from './item-authorizations/item-authorizations.component';
|
||||
import { ObjectValuesPipe } from '../../shared/utils/object-values-pipe';
|
||||
import { ResourcePoliciesModule } from '../../shared/resource-policies/resource-policies.module';
|
||||
|
||||
|
||||
/**
|
||||
* Module that contains all components related to the Edit Item page administrator functionality
|
||||
@@ -39,9 +44,11 @@ import { ObjectValuesPipe } from '../../shared/utils/object-values-pipe';
|
||||
imports: [
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
NgbTooltipModule,
|
||||
EditItemPageRoutingModule,
|
||||
SearchPageModule,
|
||||
DragDropModule
|
||||
DragDropModule,
|
||||
ResourcePoliciesModule
|
||||
],
|
||||
declarations: [
|
||||
EditItemPageComponent,
|
||||
|
@@ -15,14 +15,11 @@ import { getFirstSucceededRemoteData, getRemoteDataPayload } from '../../../core
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
||||
import { Bundle } from '../../../core/shared/bundle.model';
|
||||
import {
|
||||
FieldUpdate,
|
||||
FieldUpdates
|
||||
} from '../../../core/data/object-updates/object-updates.reducer';
|
||||
import { FieldUpdate, FieldUpdates } from '../../../core/data/object-updates/object-updates.reducer';
|
||||
import { Bitstream } from '../../../core/shared/bitstream.model';
|
||||
import { FieldChangeType } from '../../../core/data/object-updates/object-updates.actions';
|
||||
import { BundleDataService } from '../../../core/data/bundle-data.service';
|
||||
import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model';
|
||||
import { PaginatedSearchOptions } from '../../../shared/search/models/paginated-search-options.model';
|
||||
import { ResponsiveColumnSizes } from '../../../shared/responsive-table-sizes/responsive-column-sizes';
|
||||
import { ResponsiveTableSizes } from '../../../shared/responsive-table-sizes/responsive-table-sizes';
|
||||
import { NoContent } from '../../../core/shared/NoContent.model';
|
||||
|
@@ -5,7 +5,7 @@ import { Bitstream } from '../../../../../core/shared/bitstream.model';
|
||||
import { ObjectUpdatesService } from '../../../../../core/data/object-updates/object-updates.service';
|
||||
import { BundleDataService } from '../../../../../core/data/bundle-data.service';
|
||||
import { switchMap } from 'rxjs/operators';
|
||||
import { PaginatedSearchOptions } from '../../../../../shared/search/paginated-search-options.model';
|
||||
import { PaginatedSearchOptions } from '../../../../../shared/search/models/paginated-search-options.model';
|
||||
import { ResponsiveTableSizes } from '../../../../../shared/responsive-table-sizes/responsive-table-sizes';
|
||||
import { followLink } from '../../../../../shared/utils/follow-link-config.model';
|
||||
import { ObjectValuesPipe } from '../../../../../shared/utils/object-values-pipe';
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { EventEmitter } from '@angular/core';
|
||||
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
@@ -25,7 +25,7 @@ import { ObjectSelectService } from '../../../shared/object-select/object-select
|
||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
|
||||
import { SearchFormComponent } from '../../../shared/search-form/search-form.component';
|
||||
import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model';
|
||||
import { PaginatedSearchOptions } from '../../../shared/search/models/paginated-search-options.model';
|
||||
import { HostWindowServiceStub } from '../../../shared/testing/host-window-service.stub';
|
||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
||||
import { ObjectSelectServiceStub } from '../../../shared/testing/object-select-service.stub';
|
||||
|
@@ -9,11 +9,12 @@ import { PaginatedList } from '../../../core/data/paginated-list.model';
|
||||
import { Collection } from '../../../core/shared/collection.model';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import {
|
||||
getAllSucceededRemoteData,
|
||||
getFirstCompletedRemoteData,
|
||||
getFirstSucceededRemoteData,
|
||||
getFirstSucceededRemoteDataPayload,
|
||||
getRemoteDataPayload,
|
||||
getFirstSucceededRemoteData,
|
||||
toDSpaceObjectListRD,
|
||||
getAllSucceededRemoteData, getFirstCompletedRemoteData
|
||||
toDSpaceObjectListRD
|
||||
} from '../../../core/shared/operators';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { filter, map, startWith, switchMap, take } from 'rxjs/operators';
|
||||
@@ -22,7 +23,7 @@ import { TranslateService } from '@ngx-translate/core';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model';
|
||||
import { hasValue, isNotEmpty } from '../../../shared/empty.util';
|
||||
import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model';
|
||||
import { PaginatedSearchOptions } from '../../../shared/search/models/paginated-search-options.model';
|
||||
import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service';
|
||||
import { SearchService } from '../../../core/shared/search/search.service';
|
||||
import { NoContent } from '../../../core/shared/NoContent.model';
|
||||
|
@@ -3,7 +3,13 @@ import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { LinkService } from '../../../../core/cache/builders/link.service';
|
||||
import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions';
|
||||
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
|
||||
import { combineLatest as observableCombineLatest, from as observableFrom, BehaviorSubject, Observable, Subscription } from 'rxjs';
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatest as observableCombineLatest,
|
||||
from as observableFrom,
|
||||
Observable,
|
||||
Subscription
|
||||
} from 'rxjs';
|
||||
import {
|
||||
FieldUpdate,
|
||||
FieldUpdates,
|
||||
@@ -25,7 +31,7 @@ import { ItemType } from '../../../../core/shared/item-relationships/item-type.m
|
||||
import { DsDynamicLookupRelationModalComponent } from '../../../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component';
|
||||
import { RelationshipOptions } from '../../../../shared/form/builder/models/relationship-options.model';
|
||||
import { SelectableListService } from '../../../../shared/object-list/selectable-list/selectable-list.service';
|
||||
import { SearchResult } from '../../../../shared/search/search-result.model';
|
||||
import { SearchResult } from '../../../../shared/search/models/search-result.model';
|
||||
import { followLink } from '../../../../shared/utils/follow-link-config.model';
|
||||
import { PaginatedList } from '../../../../core/data/paginated-list.model';
|
||||
import { RemoteData } from '../../../../core/data/remote-data';
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user