Merge branch 'main' into DSC-389

This commit is contained in:
Corrado Lombardi
2022-01-26 12:27:50 +01:00
305 changed files with 3387 additions and 2270 deletions

View File

@@ -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

View File

@@ -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');

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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'
}
};

View File

@@ -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

View File

@@ -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",

View File

@@ -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,

View File

@@ -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"

View File

@@ -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);
});
});
});

View File

@@ -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,17 +169,20 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
*/
emailValueChangeSubscribe: Subscription;
constructor(protected changeDetectorRef: ChangeDetectorRef,
public epersonService: EPersonDataService,
public groupsDataService: GroupDataService,
private formBuilderService: FormBuilderService,
private translateService: TranslateService,
private notificationsService: NotificationsService,
private authService: AuthService,
private authorizationService: AuthorizationDataService,
private modalService: NgbModal,
private paginationService: PaginationService,
public requestService: RequestService) {
constructor(
protected changeDetectorRef: ChangeDetectorRef,
public epersonService: EPersonDataService,
public groupsDataService: GroupDataService,
private formBuilderService: FormBuilderService,
private translateService: TranslateService,
private notificationsService: NotificationsService,
private authService: AuthService,
private authorizationService: AuthorizationDataService,
private modalService: NgbModal,
private paginationService: PaginationService,
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
*/

View File

@@ -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,

View File

@@ -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,

View File

@@ -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()

View File

@@ -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*/
@@ -229,19 +254,19 @@ describe('AdminSidebarComponent', () => {
it('should contain site admin section', () => {
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
id: 'admin_search', visible: true,
id: 'admin_search', visible: true,
}));
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
id: 'registries', visible: true,
id: 'registries', visible: true,
}));
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
parentID: 'registries', visible: true,
parentID: 'registries', visible: true,
}));
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
id: 'curation_tasks', visible: true,
id: 'curation_tasks', visible: true,
}));
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
id: 'workflow', visible: true,
id: 'workflow', visible: true,
}));
});
});
@@ -259,7 +284,7 @@ describe('AdminSidebarComponent', () => {
it('should show edit_community', () => {
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
id: 'edit_community', visible: true,
id: 'edit_community', visible: true,
}));
});
});
@@ -277,7 +302,7 @@ describe('AdminSidebarComponent', () => {
it('should show edit_collection', () => {
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
id: 'edit_collection', visible: true,
id: 'edit_collection', visible: true,
}));
});
});
@@ -295,10 +320,10 @@ describe('AdminSidebarComponent', () => {
it('should show access control section', () => {
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
id: 'access_control', visible: true,
id: 'access_control', visible: true,
}));
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
parentID: 'access_control', visible: true,
parentID: 'access_control', visible: true,
}));
});
});

View File

@@ -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
@@ -63,14 +64,15 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
inFocus$: BehaviorSubject<boolean>;
constructor(protected menuService: MenuService,
protected injector: Injector,
private variableService: CSSVariableService,
private authService: AuthService,
private modalService: NgbModal,
private authorizationService: AuthorizationDataService,
private scriptDataService: ScriptDataService,
protected injector: Injector,
private variableService: CSSVariableService,
private authService: AuthService,
private modalService: NgbModal,
public authorizationService: AuthorizationDataService,
private scriptDataService: ScriptDataService,
public route: ActivatedRoute
) {
super(menuService, injector);
super(menuService, injector, authorizationService, route);
this.inFocus$ = new BehaviorSubject(false);
}
@@ -144,7 +146,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
type: MenuItemType.TEXT,
text: 'menu.section.new'
} as TextMenuItemModel,
icon: 'plus',
icon: 'plus',
index: 0
},
{

View File

@@ -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: [

View File

@@ -24,7 +24,7 @@ const ENTRY_COMPONENTS = [
AccessControlModule,
AdminSearchModule.withEntryComponents(),
AdminWorkflowModuleModule.withEntryComponents(),
SharedModule,
SharedModule
],
declarations: [
AdminCurationTasksComponent,

View File

@@ -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}`;

View File

@@ -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,9 +214,10 @@ import { GroupAdministratorGuard } from './core/data/feature-authorization/featu
canActivate: [GroupAdministratorGuard],
},
{ path: '**', pathMatch: 'full', component: ThemedPageNotFoundComponent },
]}
],{
onSameUrlNavigation: 'reload',
]
}
], {
onSameUrlNavigation: 'reload',
})
],
exports: [RouterModule],

View File

@@ -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 = [

View File

@@ -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;

View File

@@ -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,

View File

@@ -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);
}));

View File

@@ -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);
}
/**

View File

@@ -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();

View File

@@ -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: [

View File

@@ -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 {
/**

View File

@@ -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: [

View File

@@ -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,

View File

@@ -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,
getFirstSucceededRemoteData,
toDSpaceObjectListRD,
getFirstCompletedRemoteData, getAllSucceededRemoteData
getAllSucceededRemoteData,
getFirstCompletedRemoteData,
getFirstSucceededRemoteData,
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';

View File

@@ -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,20 +98,20 @@ 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) => {
return this.searchService.search(
new PaginatedSearchOptions({
scope: id,
pagination: currentPagination,
sort: currentSort,
dsoTypes: [DSpaceObjectType.ITEM]
})).pipe(toDSpaceObjectListRD()) as Observable<RemoteData<PaginatedList<Item>>>;
new PaginatedSearchOptions({
scope: id,
pagination: currentPagination,
sort: currentSort,
dsoTypes: [DSpaceObjectType.ITEM]
})).pipe(toDSpaceObjectListRD()) as Observable<RemoteData<PaginatedList<Item>>>;
}),
startWith(undefined) // Make sure switching pages shows loading component
)
)
)
);

View File

@@ -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,

View File

@@ -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

View File

@@ -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

View File

@@ -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';

View File

@@ -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(),

View File

@@ -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';

View File

@@ -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,

View File

@@ -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> {
/**

View File

@@ -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: [

View File

@@ -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

View File

@@ -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';

View File

@@ -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

View File

@@ -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';

View File

@@ -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(),

View File

@@ -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';

View File

@@ -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,

View File

@@ -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', () => {

View File

@@ -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();
}

View File

@@ -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
];
/**

View File

@@ -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';

View File

@@ -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';

View File

@@ -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 {

View File

@@ -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()

View File

@@ -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);
}
}

View File

@@ -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',
}

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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
}));
});
});
});

View File

@@ -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 */

View 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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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();
});

View File

@@ -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;

View 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());
});
});
});
});

View 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());
}
})
);
}
}

View File

@@ -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);
}
}

View File

@@ -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';

View File

@@ -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';

View File

@@ -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 };
}));
}
}

View File

@@ -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';

View File

@@ -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';

View File

@@ -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;
}

View File

@@ -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';

View File

@@ -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))

View File

@@ -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
*/

View File

@@ -3,8 +3,8 @@
*/
export enum ViewMode {
ListElement = 'listElement',
GridElement = 'gridElement',
DetailedListElement = 'detailedListElement',
StandalonePage = 'standalonePage',
ListElement = 'list',
GridElement = 'grid',
DetailedListElement = 'detailed',
StandalonePage = 'standalone',
}

View 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;

View 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');
});
});

View 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;
}
}
};

View 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);
});
});
});

View 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;
}
}

View File

@@ -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: [

View File

@@ -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 }))
};
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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,

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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