mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-14 21:43:04 +00:00
Merge remote-tracking branch 'origin/main' into CST-4633-search-refactoring
This commit is contained in:
@@ -359,7 +359,7 @@ dspace-angular
|
|||||||
│ ├── plugins * Folder for Cypress plugins (if any)
|
│ ├── plugins * Folder for Cypress plugins (if any)
|
||||||
│ ├── support * Folder for global e2e test actions/commands (run for all tests)
|
│ ├── support * Folder for global e2e test actions/commands (run for all tests)
|
||||||
│ └── tsconfig.json * TypeScript configuration file for e2e tests
|
│ └── tsconfig.json * TypeScript configuration file for e2e tests
|
||||||
├── docker *
|
├── docker * See docker/README.md for details
|
||||||
│ ├── cli.assetstore.yml *
|
│ ├── cli.assetstore.yml *
|
||||||
│ ├── cli.ingest.yml *
|
│ ├── cli.ingest.yml *
|
||||||
│ ├── cli.yml *
|
│ ├── cli.yml *
|
||||||
@@ -367,8 +367,6 @@ dspace-angular
|
|||||||
│ ├── docker-compose-ci.yml *
|
│ ├── docker-compose-ci.yml *
|
||||||
│ ├── docker-compose-rest.yml *
|
│ ├── docker-compose-rest.yml *
|
||||||
│ ├── docker-compose.yml *
|
│ ├── docker-compose.yml *
|
||||||
│ ├── environment.dev.ts *
|
|
||||||
│ ├── local.cfg *
|
|
||||||
│ └── README.md *
|
│ └── README.md *
|
||||||
├── docs * Folder for documentation
|
├── docs * Folder for documentation
|
||||||
│ └── Configuration.md * Configuration documentation
|
│ └── Configuration.md * Configuration documentation
|
||||||
|
@@ -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.
|
- Docker compose file that provides a DSpace CLI container to work with a running DSpace REST container.
|
||||||
- cli.assetstore.yml
|
- 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.
|
- 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
|
## To refresh / pull DSpace images from Dockerhub
|
||||||
|
@@ -18,10 +18,19 @@ services:
|
|||||||
dspace-cli:
|
dspace-cli:
|
||||||
image: "${DOCKER_OWNER:-dspace}/dspace-cli:${DSPACE_VER:-dspace-7_x}"
|
image: "${DOCKER_OWNER:-dspace}/dspace-cli:${DSPACE_VER:-dspace-7_x}"
|
||||||
container_name: dspace-cli
|
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:
|
volumes:
|
||||||
- "assetstore:/dspace/assetstore"
|
- "assetstore:/dspace/assetstore"
|
||||||
- "./local.cfg:/dspace/config/local.cfg"
|
|
||||||
entrypoint: /dspace/bin/dspace
|
entrypoint: /dspace/bin/dspace
|
||||||
command: help
|
command: help
|
||||||
networks:
|
networks:
|
||||||
|
@@ -17,6 +17,19 @@ services:
|
|||||||
# DSpace (backend) webapp container
|
# DSpace (backend) webapp container
|
||||||
dspace:
|
dspace:
|
||||||
container_name: 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:
|
depends_on:
|
||||||
- dspacedb
|
- dspacedb
|
||||||
image: dspace/dspace:dspace-7_x-test
|
image: dspace/dspace:dspace-7_x-test
|
||||||
@@ -29,7 +42,6 @@ services:
|
|||||||
tty: true
|
tty: true
|
||||||
volumes:
|
volumes:
|
||||||
- assetstore:/dspace/assetstore
|
- 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)
|
# Mount DSpace's solr configs to a volume, so that we can share to 'dspacesolr' container (see below)
|
||||||
- solr_configs:/dspace/solr
|
- solr_configs:/dspace/solr
|
||||||
# Ensure that the database is ready BEFORE starting tomcat
|
# Ensure that the database is ready BEFORE starting tomcat
|
||||||
|
@@ -13,10 +13,32 @@
|
|||||||
version: '3.7'
|
version: '3.7'
|
||||||
networks:
|
networks:
|
||||||
dspacenet:
|
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:
|
services:
|
||||||
# DSpace (backend) webapp container
|
# DSpace (backend) webapp container
|
||||||
dspace:
|
dspace:
|
||||||
container_name: 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
|
image: dspace/dspace:dspace-7_x-test
|
||||||
depends_on:
|
depends_on:
|
||||||
- dspacedb
|
- dspacedb
|
||||||
@@ -29,7 +51,6 @@ services:
|
|||||||
tty: true
|
tty: true
|
||||||
volumes:
|
volumes:
|
||||||
- assetstore:/dspace/assetstore
|
- 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)
|
# Mount DSpace's solr configs to a volume, so that we can share to 'dspacesolr' container (see below)
|
||||||
- solr_configs:/dspace/solr
|
- solr_configs:/dspace/solr
|
||||||
# Ensure that the database is ready BEFORE starting tomcat
|
# Ensure that the database is ready BEFORE starting tomcat
|
||||||
|
@@ -16,10 +16,14 @@ services:
|
|||||||
dspace-angular:
|
dspace-angular:
|
||||||
container_name: dspace-angular
|
container_name: dspace-angular
|
||||||
environment:
|
environment:
|
||||||
DSPACE_HOST: dspace-angular
|
DSPACE_UI_SSL: false
|
||||||
DSPACE_NAMESPACE: /
|
DSPACE_UI_HOST: dspace-angular
|
||||||
DSPACE_PORT: '4000'
|
DSPACE_UI_PORT: '4000'
|
||||||
DSPACE_SSL: "false"
|
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
|
image: dspace/dspace-angular:dspace-7_x
|
||||||
build:
|
build:
|
||||||
context: ..
|
context: ..
|
||||||
@@ -33,5 +37,3 @@ services:
|
|||||||
target: 9876
|
target: 9876
|
||||||
stdin_open: true
|
stdin_open: true
|
||||||
tty: true
|
tty: true
|
||||||
volumes:
|
|
||||||
- ./environment.dev.ts:/app/src/environments/environment.dev.ts
|
|
||||||
|
@@ -1,18 +0,0 @@
|
|||||||
/**
|
|
||||||
* The contents of this file are subject to the license and copyright
|
|
||||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
|
||||||
* tree and available online at
|
|
||||||
*
|
|
||||||
* http://www.dspace.org/license/
|
|
||||||
*/
|
|
||||||
// This file is based on environment.template.ts provided by Angular UI
|
|
||||||
export const environment = {
|
|
||||||
// Default to using the local REST API (running in Docker)
|
|
||||||
rest: {
|
|
||||||
ssl: false,
|
|
||||||
host: 'localhost',
|
|
||||||
port: 8080,
|
|
||||||
// NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
|
|
||||||
nameSpace: '/server'
|
|
||||||
}
|
|
||||||
};
|
|
@@ -1,6 +0,0 @@
|
|||||||
dspace.dir=/dspace
|
|
||||||
db.url=jdbc:postgresql://dspacedb:5432/dspace
|
|
||||||
dspace.server.url=http://localhost:8080/server
|
|
||||||
dspace.ui.url=http://localhost:4000
|
|
||||||
dspace.name=DSpace Started with Docker Compose
|
|
||||||
solr.server=http://dspacesolr:8983/solr
|
|
@@ -19,7 +19,7 @@
|
|||||||
class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}</button>
|
class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}</button>
|
||||||
</div>
|
</div>
|
||||||
<div between class="btn-group">
|
<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}}
|
<i class="fa fa-key"></i> {{'admin.access-control.epeople.actions.reset' | translate}}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -36,9 +36,13 @@
|
|||||||
</button>
|
</button>
|
||||||
</ds-form>
|
</ds-form>
|
||||||
|
|
||||||
|
<ds-loading [showMessage]="false" *ngIf="!formGroup"></ds-loading>
|
||||||
|
|
||||||
<div *ngIf="epersonService.getActiveEPerson() | async">
|
<div *ngIf="epersonService.getActiveEPerson() | async">
|
||||||
<h5>{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}</h5>
|
<h5>{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}</h5>
|
||||||
|
|
||||||
|
<ds-loading [showMessage]="false" *ngIf="!(groups | async)"></ds-loading>
|
||||||
|
|
||||||
<ds-pagination
|
<ds-pagination
|
||||||
*ngIf="(groups | async)?.payload?.totalElements > 0"
|
*ngIf="(groups | async)?.payload?.totalElements > 0"
|
||||||
[paginationOptions]="config"
|
[paginationOptions]="config"
|
||||||
|
@@ -2,7 +2,7 @@ import { Observable, of as observableOf } from 'rxjs';
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
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 { BrowserModule, By } from '@angular/platform-browser';
|
||||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
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 { PageInfo } from '../../../core/shared/page-info.model';
|
||||||
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
|
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import { EPeopleRegistryComponent } from '../epeople-registry.component';
|
||||||
import { EPersonFormComponent } from './eperson-form.component';
|
import { EPersonFormComponent } from './eperson-form.component';
|
||||||
import { EPersonMock, EPersonMock2 } from '../../../shared/testing/eperson.mock';
|
import { EPersonMock, EPersonMock2 } from '../../../shared/testing/eperson.mock';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
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 { RequestService } from '../../../core/data/request.service';
|
||||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||||
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
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 { ValidateEmailNotTaken } from './validators/email-taken.validator';
|
||||||
|
import { EpersonRegistrationService } from '../../../core/data/eperson-registration.service';
|
||||||
|
|
||||||
describe('EPersonFormComponent', () => {
|
describe('EPersonFormComponent', () => {
|
||||||
let component: EPersonFormComponent;
|
let component: EPersonFormComponent;
|
||||||
@@ -42,6 +42,7 @@ describe('EPersonFormComponent', () => {
|
|||||||
let authService: AuthServiceStub;
|
let authService: AuthServiceStub;
|
||||||
let authorizationService: AuthorizationDataService;
|
let authorizationService: AuthorizationDataService;
|
||||||
let groupsDataService: GroupDataService;
|
let groupsDataService: GroupDataService;
|
||||||
|
let epersonRegistrationService: EpersonRegistrationService;
|
||||||
|
|
||||||
let paginationService;
|
let paginationService;
|
||||||
|
|
||||||
@@ -199,12 +200,18 @@ describe('EPersonFormComponent', () => {
|
|||||||
{ provide: AuthService, useValue: authService },
|
{ provide: AuthService, useValue: authService },
|
||||||
{ provide: AuthorizationDataService, useValue: authorizationService },
|
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||||
{ provide: PaginationService, useValue: paginationService },
|
{ 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]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
epersonRegistrationService = jasmine.createSpyObj('epersonRegistrationService', {
|
||||||
|
registerEmail: createSuccessfulRemoteDataObject$(null)
|
||||||
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(EPersonFormComponent);
|
fixture = TestBed.createComponent(EPersonFormComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
@@ -514,4 +521,23 @@ describe('EPersonFormComponent', () => {
|
|||||||
expect(component.epersonService.deleteEPerson).toHaveBeenCalledWith(eperson);
|
expect(component.epersonService.deleteEPerson).toHaveBeenCalledWith(eperson);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Reset Password', () => {
|
||||||
|
let ePersonId;
|
||||||
|
let ePersonEmail;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
ePersonId = 'testEPersonId';
|
||||||
|
ePersonEmail = 'person.email@4science.it';
|
||||||
|
component.epersonInitial = Object.assign(new EPerson(), {
|
||||||
|
id: ePersonId,
|
||||||
|
email: ePersonEmail
|
||||||
|
});
|
||||||
|
component.resetPassword();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call epersonRegistrationService.registerEmail', () => {
|
||||||
|
expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith(ePersonEmail);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -34,6 +34,8 @@ import { NoContent } from '../../../core/shared/NoContent.model';
|
|||||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||||
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||||
import { ValidateEmailNotTaken } from './validators/email-taken.validator';
|
import { ValidateEmailNotTaken } from './validators/email-taken.validator';
|
||||||
|
import { Registration } from '../../../core/shared/registration.model';
|
||||||
|
import { EpersonRegistrationService } from '../../../core/data/eperson-registration.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-eperson-form',
|
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
|
* 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)
|
* 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
|
* Observable whether or not the admin is allowed to delete the EPerson
|
||||||
@@ -167,17 +169,20 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
emailValueChangeSubscribe: Subscription;
|
emailValueChangeSubscribe: Subscription;
|
||||||
|
|
||||||
constructor(protected changeDetectorRef: ChangeDetectorRef,
|
constructor(
|
||||||
public epersonService: EPersonDataService,
|
protected changeDetectorRef: ChangeDetectorRef,
|
||||||
public groupsDataService: GroupDataService,
|
public epersonService: EPersonDataService,
|
||||||
private formBuilderService: FormBuilderService,
|
public groupsDataService: GroupDataService,
|
||||||
private translateService: TranslateService,
|
private formBuilderService: FormBuilderService,
|
||||||
private notificationsService: NotificationsService,
|
private translateService: TranslateService,
|
||||||
private authService: AuthService,
|
private notificationsService: NotificationsService,
|
||||||
private authorizationService: AuthorizationDataService,
|
private authService: AuthService,
|
||||||
private modalService: NgbModal,
|
private authorizationService: AuthorizationDataService,
|
||||||
private paginationService: PaginationService,
|
private modalService: NgbModal,
|
||||||
public requestService: RequestService) {
|
private paginationService: PaginationService,
|
||||||
|
public requestService: RequestService,
|
||||||
|
private epersonRegistrationService: EpersonRegistrationService,
|
||||||
|
) {
|
||||||
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
||||||
this.epersonInitial = eperson;
|
this.epersonInitial = eperson;
|
||||||
if (hasValue(eperson)) {
|
if (hasValue(eperson)) {
|
||||||
@@ -310,6 +315,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
this.canDelete$ = activeEPerson$.pipe(
|
this.canDelete$ = activeEPerson$.pipe(
|
||||||
switchMap((eperson) => this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined))
|
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;
|
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
|
* Cancel the current edit when component is destroyed & unsub all subscriptions
|
||||||
*/
|
*/
|
||||||
|
@@ -13,6 +13,7 @@ export enum FeatureID {
|
|||||||
CanManageGroup = 'canManageGroup',
|
CanManageGroup = 'canManageGroup',
|
||||||
IsCollectionAdmin = 'isCollectionAdmin',
|
IsCollectionAdmin = 'isCollectionAdmin',
|
||||||
IsCommunityAdmin = 'isCommunityAdmin',
|
IsCommunityAdmin = 'isCommunityAdmin',
|
||||||
|
CanChangePassword = 'canChangePassword',
|
||||||
CanDownload = 'canDownload',
|
CanDownload = 'canDownload',
|
||||||
CanRequestACopy = 'canRequestACopy',
|
CanRequestACopy = 'canRequestACopy',
|
||||||
CanManageVersions = 'canManageVersions',
|
CanManageVersions = 'canManageVersions',
|
||||||
|
@@ -47,6 +47,12 @@ export class Version extends DSpaceObject {
|
|||||||
@autoserialize
|
@autoserialize
|
||||||
summary: string;
|
summary: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the submitter of this version
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
submitterName: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Date this version was created
|
* The Date this version was created
|
||||||
*/
|
*/
|
||||||
|
@@ -7,7 +7,7 @@
|
|||||||
<ds-profile-page-metadata-form [user]="user"></ds-profile-page-metadata-form>
|
<ds-profile-page-metadata-form [user]="user"></ds-profile-page-metadata-form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card mb-4">
|
<div *ngIf="canChangePassword$ | async" class="card mb-4 security-section">
|
||||||
<div class="card-header">{{'profile.card.security' | translate}}</div>
|
<div class="card-header">{{'profile.card.security' | translate}}</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<ds-profile-page-security-form
|
<ds-profile-page-security-form
|
||||||
|
@@ -13,10 +13,13 @@ import { NotificationsService } from '../shared/notifications/notifications.serv
|
|||||||
import { authReducer } from '../core/auth/auth.reducer';
|
import { authReducer } from '../core/auth/auth.reducer';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
|
||||||
import { createPaginatedList } from '../shared/testing/utils.test';
|
import { createPaginatedList } from '../shared/testing/utils.test';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { BehaviorSubject, of as observableOf } from 'rxjs';
|
||||||
import { AuthService } from '../core/auth/auth.service';
|
import { AuthService } from '../core/auth/auth.service';
|
||||||
import { RestResponse } from '../core/cache/response.models';
|
import { RestResponse } from '../core/cache/response.models';
|
||||||
import { provideMockStore } from '@ngrx/store/testing';
|
import { provideMockStore } from '@ngrx/store/testing';
|
||||||
|
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { getTestScheduler } from 'jasmine-marbles';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
|
||||||
describe('ProfilePageComponent', () => {
|
describe('ProfilePageComponent', () => {
|
||||||
let component: ProfilePageComponent;
|
let component: ProfilePageComponent;
|
||||||
@@ -28,10 +31,13 @@ describe('ProfilePageComponent', () => {
|
|||||||
let epersonService;
|
let epersonService;
|
||||||
let notificationsService;
|
let notificationsService;
|
||||||
|
|
||||||
|
const canChangePassword = new BehaviorSubject(true);
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
user = Object.assign(new EPerson(), {
|
user = Object.assign(new EPerson(), {
|
||||||
id: 'userId',
|
id: 'userId',
|
||||||
groups: createSuccessfulRemoteDataObject$(createPaginatedList([]))
|
groups: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||||
|
_links: {self: {href: 'test.com/uuid/1234567654321'}}
|
||||||
});
|
});
|
||||||
initialState = {
|
initialState = {
|
||||||
core: {
|
core: {
|
||||||
@@ -74,6 +80,7 @@ describe('ProfilePageComponent', () => {
|
|||||||
{ provide: EPersonDataService, useValue: epersonService },
|
{ provide: EPersonDataService, useValue: epersonService },
|
||||||
{ provide: NotificationsService, useValue: notificationsService },
|
{ provide: NotificationsService, useValue: notificationsService },
|
||||||
{ provide: AuthService, useValue: authService },
|
{ provide: AuthService, useValue: authService },
|
||||||
|
{ provide: AuthorizationDataService, useValue: jasmine.createSpyObj('authorizationService', { isAuthorized: canChangePassword }) },
|
||||||
provideMockStore({ initialState }),
|
provideMockStore({ initialState }),
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
@@ -183,7 +190,7 @@ describe('ProfilePageComponent', () => {
|
|||||||
component.setPasswordValue('testest');
|
component.setPasswordValue('testest');
|
||||||
component.setInvalid(false);
|
component.setInvalid(false);
|
||||||
|
|
||||||
operations = [{op: 'add', path: '/password', value: 'testest'}];
|
operations = [{ op: 'add', path: '/password', value: 'testest' }];
|
||||||
result = component.updateSecurity();
|
result = component.updateSecurity();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -196,4 +203,36 @@ describe('ProfilePageComponent', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('canChangePassword$', () => {
|
||||||
|
describe('when the user is allowed to change their password', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
canChangePassword.next(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should contain true', () => {
|
||||||
|
getTestScheduler().expectObservable(component.canChangePassword$).toBe('(a)', { a: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show the security section on the page', () => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(fixture.debugElement.query(By.css('.security-section'))).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the user is not allowed to change their password', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
canChangePassword.next(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should contain false', () => {
|
||||||
|
getTestScheduler().expectObservable(component.canChangePassword$).toBe('(a)', { a: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show the security section on the page', () => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(fixture.debugElement.query(By.css('.security-section'))).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -18,6 +18,8 @@ import { hasValue, isNotEmpty } from '../shared/empty.util';
|
|||||||
import { followLink } from '../shared/utils/follow-link-config.model';
|
import { followLink } from '../shared/utils/follow-link-config.model';
|
||||||
import { AuthService } from '../core/auth/auth.service';
|
import { AuthService } from '../core/auth/auth.service';
|
||||||
import { Operation } from 'fast-json-patch';
|
import { Operation } from 'fast-json-patch';
|
||||||
|
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-profile-page',
|
selector: 'ds-profile-page',
|
||||||
@@ -67,11 +69,13 @@ export class ProfilePageComponent implements OnInit {
|
|||||||
* The authenticated user
|
* The authenticated user
|
||||||
*/
|
*/
|
||||||
private currentUser: EPerson;
|
private currentUser: EPerson;
|
||||||
|
canChangePassword$: Observable<boolean>;
|
||||||
|
|
||||||
constructor(private authService: AuthService,
|
constructor(private authService: AuthService,
|
||||||
private notificationsService: NotificationsService,
|
private notificationsService: NotificationsService,
|
||||||
private translate: TranslateService,
|
private translate: TranslateService,
|
||||||
private epersonService: EPersonDataService) {
|
private epersonService: EPersonDataService,
|
||||||
|
private authorizationService: AuthorizationDataService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@@ -83,6 +87,7 @@ export class ProfilePageComponent implements OnInit {
|
|||||||
tap((user: EPerson) => this.currentUser = user)
|
tap((user: EPerson) => this.currentUser = user)
|
||||||
);
|
);
|
||||||
this.groupsRD$ = this.user$.pipe(switchMap((user: EPerson) => user.groups));
|
this.groupsRD$ = this.user$.pipe(switchMap((user: EPerson) => user.groups));
|
||||||
|
this.canChangePassword$ = this.user$.pipe(switchMap((user: EPerson) => this.authorizationService.isAuthorized(FeatureID.CanChangePassword, user._links.self.href)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -17,7 +17,7 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">{{"item.version.history.table.version" | translate}}</th>
|
<th scope="col">{{"item.version.history.table.version" | translate}}</th>
|
||||||
<th scope="col" *ngIf="(hasEpersons$ | async)">{{"item.version.history.table.editor" | translate}}</th>
|
<th scope="col" *ngIf="(showSubmitter() | async)">{{"item.version.history.table.editor" | translate}}</th>
|
||||||
<th scope="col">{{"item.version.history.table.date" | translate}}</th>
|
<th scope="col">{{"item.version.history.table.date" | translate}}</th>
|
||||||
<th scope="col">{{"item.version.history.table.summary" | translate}}</th>
|
<th scope="col">{{"item.version.history.table.summary" | translate}}</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -87,10 +87,8 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</td>
|
</td>
|
||||||
<td *ngIf="(hasEpersons$ | async)" class="version-row-element-editor">
|
<td class="version-row-element-editor" *ngIf="(showSubmitter() | async)">
|
||||||
<span *ngVar="(version?.eperson | async)?.payload as eperson">
|
{{version?.submitterName}}
|
||||||
<a *ngIf="eperson" [href]="'mailto:' + eperson?.email">{{eperson?.name}}</a>
|
|
||||||
</span>
|
|
||||||
</td>
|
</td>
|
||||||
<td class="version-row-element-date">
|
<td class="version-row-element-date">
|
||||||
{{version?.created | date : 'yyyy-MM-dd HH:mm:ss'}}
|
{{version?.created | date : 'yyyy-MM-dd HH:mm:ss'}}
|
||||||
|
@@ -24,6 +24,7 @@ import { AuthorizationDataService } from '../../../core/data/feature-authorizati
|
|||||||
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||||
import { WorkspaceitemDataService } from '../../../core/submission/workspaceitem-data.service';
|
import { WorkspaceitemDataService } from '../../../core/submission/workspaceitem-data.service';
|
||||||
import { WorkflowItemDataService } from '../../../core/submission/workflowitem-data.service';
|
import { WorkflowItemDataService } from '../../../core/submission/workflowitem-data.service';
|
||||||
|
import { ConfigurationDataService } from '../../../core/data/configuration-data.service';
|
||||||
|
|
||||||
describe('ItemVersionsComponent', () => {
|
describe('ItemVersionsComponent', () => {
|
||||||
let component: ItemVersionsComponent;
|
let component: ItemVersionsComponent;
|
||||||
@@ -34,6 +35,7 @@ describe('ItemVersionsComponent', () => {
|
|||||||
let workspaceItemDataService: WorkspaceitemDataService;
|
let workspaceItemDataService: WorkspaceitemDataService;
|
||||||
let workflowItemDataService: WorkflowItemDataService;
|
let workflowItemDataService: WorkflowItemDataService;
|
||||||
let versionService: VersionDataService;
|
let versionService: VersionDataService;
|
||||||
|
let configurationService: ConfigurationDataService;
|
||||||
|
|
||||||
const versionHistory = Object.assign(new VersionHistory(), {
|
const versionHistory = Object.assign(new VersionHistory(), {
|
||||||
id: '1',
|
id: '1',
|
||||||
@@ -109,6 +111,10 @@ describe('ItemVersionsComponent', () => {
|
|||||||
findById: EMPTY,
|
findById: EMPTY,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const configurationServiceSpy = jasmine.createSpyObj('configurationService', {
|
||||||
|
findByPropertyName: of(true),
|
||||||
|
});
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@@ -125,6 +131,7 @@ describe('ItemVersionsComponent', () => {
|
|||||||
{provide: VersionDataService, useValue: versionServiceSpy},
|
{provide: VersionDataService, useValue: versionServiceSpy},
|
||||||
{provide: WorkspaceitemDataService, useValue: workspaceItemDataServiceSpy},
|
{provide: WorkspaceitemDataService, useValue: workspaceItemDataServiceSpy},
|
||||||
{provide: WorkflowItemDataService, useValue: workflowItemDataServiceSpy},
|
{provide: WorkflowItemDataService, useValue: workflowItemDataServiceSpy},
|
||||||
|
{provide: ConfigurationDataService, useValue: configurationServiceSpy},
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
@@ -135,6 +142,7 @@ describe('ItemVersionsComponent', () => {
|
|||||||
workspaceItemDataService = TestBed.inject(WorkspaceitemDataService);
|
workspaceItemDataService = TestBed.inject(WorkspaceitemDataService);
|
||||||
workflowItemDataService = TestBed.inject(WorkflowItemDataService);
|
workflowItemDataService = TestBed.inject(WorkflowItemDataService);
|
||||||
versionService = TestBed.inject(VersionDataService);
|
versionService = TestBed.inject(VersionDataService);
|
||||||
|
configurationService = TestBed.inject(ConfigurationDataService);
|
||||||
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@@ -5,7 +5,6 @@ import { RemoteData } from '../../../core/data/remote-data';
|
|||||||
import {
|
import {
|
||||||
BehaviorSubject,
|
BehaviorSubject,
|
||||||
combineLatest,
|
combineLatest,
|
||||||
combineLatest as observableCombineLatest,
|
|
||||||
Observable,
|
Observable,
|
||||||
of,
|
of,
|
||||||
Subscription,
|
Subscription,
|
||||||
@@ -48,6 +47,7 @@ import { ItemVersionsSharedService } from './item-versions-shared.service';
|
|||||||
import { WorkspaceItem } from '../../../core/submission/models/workspaceitem.model';
|
import { WorkspaceItem } from '../../../core/submission/models/workspaceitem.model';
|
||||||
import { WorkspaceitemDataService } from '../../../core/submission/workspaceitem-data.service';
|
import { WorkspaceitemDataService } from '../../../core/submission/workspaceitem-data.service';
|
||||||
import { WorkflowItemDataService } from '../../../core/submission/workflowitem-data.service';
|
import { WorkflowItemDataService } from '../../../core/submission/workflowitem-data.service';
|
||||||
|
import { ConfigurationDataService } from '../../../core/data/configuration-data.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-item-versions',
|
selector: 'ds-item-versions',
|
||||||
@@ -180,6 +180,7 @@ export class ItemVersionsComponent implements OnInit {
|
|||||||
private authorizationService: AuthorizationDataService,
|
private authorizationService: AuthorizationDataService,
|
||||||
private workspaceItemDataService: WorkspaceitemDataService,
|
private workspaceItemDataService: WorkspaceitemDataService,
|
||||||
private workflowItemDataService: WorkflowItemDataService,
|
private workflowItemDataService: WorkflowItemDataService,
|
||||||
|
private configurationService: ConfigurationDataService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -375,6 +376,36 @@ export class ItemVersionsComponent implements OnInit {
|
|||||||
return this.authorizationService.isAuthorized(FeatureID.CanEditVersion, version.self);
|
return this.authorizationService.isAuthorized(FeatureID.CanEditVersion, version.self);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show submitter in version history table
|
||||||
|
*/
|
||||||
|
showSubmitter() {
|
||||||
|
|
||||||
|
const includeSubmitter$ = this.configurationService.findByPropertyName('versioning.item.history.include.submitter').pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
map((configurationProperty) => configurationProperty.values[0]),
|
||||||
|
startWith(false),
|
||||||
|
);
|
||||||
|
|
||||||
|
const isAdmin$ = combineLatest([
|
||||||
|
this.authorizationService.isAuthorized(FeatureID.IsCollectionAdmin),
|
||||||
|
this.authorizationService.isAuthorized(FeatureID.IsCommunityAdmin),
|
||||||
|
this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
|
||||||
|
]).pipe(
|
||||||
|
map(([isCollectionAdmin, isCommunityAdmin, isSiteAdmin]) => {
|
||||||
|
return isCollectionAdmin || isCommunityAdmin || isSiteAdmin;
|
||||||
|
}),
|
||||||
|
take(1),
|
||||||
|
);
|
||||||
|
|
||||||
|
return combineLatest([includeSubmitter$, isAdmin$]).pipe(
|
||||||
|
map(([includeSubmitter, isAdmin]) => {
|
||||||
|
return includeSubmitter && isAdmin;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the current user can delete the version
|
* Check if the current user can delete the version
|
||||||
* @param version
|
* @param version
|
||||||
@@ -389,7 +420,7 @@ export class ItemVersionsComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
getAllVersions(versionHistory$: Observable<VersionHistory>): void {
|
getAllVersions(versionHistory$: Observable<VersionHistory>): void {
|
||||||
const currentPagination = this.paginationService.getCurrentPagination(this.options.id, this.options);
|
const currentPagination = this.paginationService.getCurrentPagination(this.options.id, this.options);
|
||||||
observableCombineLatest([versionHistory$, currentPagination]).pipe(
|
combineLatest([versionHistory$, currentPagination]).pipe(
|
||||||
switchMap(([versionHistory, options]: [VersionHistory, PaginationComponentOptions]) => {
|
switchMap(([versionHistory, options]: [VersionHistory, PaginationComponentOptions]) => {
|
||||||
return this.versionHistoryService.getVersions(versionHistory.id,
|
return this.versionHistoryService.getVersions(versionHistory.id,
|
||||||
new PaginatedSearchOptions({pagination: Object.assign({}, options, {currentPage: options.currentPage})}),
|
new PaginatedSearchOptions({pagination: Object.assign({}, options, {currentPage: options.currentPage})}),
|
||||||
@@ -486,7 +517,7 @@ export class ItemVersionsComponent implements OnInit {
|
|||||||
);
|
);
|
||||||
this.itemPageRoutes$ = this.versionsRD$.pipe(
|
this.itemPageRoutes$ = this.versionsRD$.pipe(
|
||||||
getAllSucceededRemoteDataPayload(),
|
getAllSucceededRemoteDataPayload(),
|
||||||
switchMap((versions) => observableCombineLatest(...versions.page.map((version) => version.item.pipe(getAllSucceededRemoteDataPayload())))),
|
switchMap((versions) => combineLatest(versions.page.map((version) => version.item.pipe(getAllSucceededRemoteDataPayload())))),
|
||||||
map((versions) => {
|
map((versions) => {
|
||||||
const itemPageRoutes = {};
|
const itemPageRoutes = {};
|
||||||
versions.forEach((item) => itemPageRoutes[item.uuid] = getItemPageRoute(item));
|
versions.forEach((item) => itemPageRoutes[item.uuid] = getItemPageRoute(item));
|
||||||
|
Reference in New Issue
Block a user