mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-12 20:43:08 +00:00
Merge tag 'dspace-7.4' into w2p-94390_replace-dso-page-edit-buttons-with-a-menu
This commit is contained in:
@@ -14,6 +14,8 @@ ui:
|
||||
rateLimiter:
|
||||
windowMs: 60000 # 1 minute
|
||||
max: 500 # limit each IP to 500 requests per windowMs
|
||||
# Trust X-FORWARDED-* headers from proxies (default = true)
|
||||
useProxies: true
|
||||
|
||||
# The REST API server settings
|
||||
# NOTE: these settings define which (publicly available) REST API to use. They are usually
|
||||
@@ -162,6 +164,9 @@ languages:
|
||||
- code: bn
|
||||
label: বাংলা
|
||||
active: true
|
||||
- code: hi
|
||||
label: हिंदी
|
||||
active: true
|
||||
- code: el
|
||||
label: Ελληνικά
|
||||
active: true
|
||||
@@ -285,3 +290,9 @@ mediaViewer:
|
||||
info:
|
||||
enableEndUserAgreement: true
|
||||
enablePrivacyStatement: true
|
||||
|
||||
# Whether to enable Markdown (https://commonmark.org/) and MathJax (https://www.mathjax.org/)
|
||||
# display in supported metadata fields. By default, only dc.description.abstract is supported.
|
||||
markdown:
|
||||
enabled: false
|
||||
mathjax: false
|
@@ -24,7 +24,7 @@ import 'cypress-axe';
|
||||
beforeEach(() => {
|
||||
// Pre-agree to all Klaro cookies by setting the klaro-anonymous cookie
|
||||
// This just ensures it doesn't get in the way of matching other objects in the page.
|
||||
cy.setCookie('klaro-anonymous', '{%22authentication%22:true%2C%22preferences%22:true%2C%22acknowledgement%22:true%2C%22google-analytics%22:true}');
|
||||
cy.setCookie('klaro-anonymous', '{%22authentication%22:true%2C%22preferences%22:true%2C%22acknowledgement%22:true%2C%22google-analytics%22:true%2C%22google-recaptcha%22:true}');
|
||||
});
|
||||
|
||||
// For better stability between tests, we visit "about:blank" (i.e. blank page) after each test.
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dspace-angular",
|
||||
"version": "7.4.0-next",
|
||||
"version": "7.4.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"config:watch": "nodemon",
|
||||
@@ -78,6 +78,7 @@
|
||||
"@nguniversal/express-engine": "^13.0.2",
|
||||
"@ngx-translate/core": "^13.0.0",
|
||||
"@nicky-lenaers/ngx-scroll-to": "^9.0.0",
|
||||
"@types/grecaptcha": "^3.0.4",
|
||||
"angular-idle-preload": "3.0.0",
|
||||
"angulartics2": "^12.0.0",
|
||||
"axios": "^0.27.2",
|
||||
|
@@ -238,7 +238,6 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
||||
this.epersonService.deleteEPerson(ePerson).pipe(getFirstCompletedRemoteData()).subscribe((restResponse: RemoteData<NoContent>) => {
|
||||
if (restResponse.hasSucceeded) {
|
||||
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', {name: ePerson.name}));
|
||||
this.reset();
|
||||
} else {
|
||||
this.notificationsService.error('Error occured when trying to delete EPerson with id: ' + ePerson.id + ' with code: ' + restResponse.statusCode + ' and message: ' + restResponse.errorMessage);
|
||||
}
|
||||
|
@@ -209,7 +209,6 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
||||
if (activeGroup != null) {
|
||||
const response = this.groupDataService.deleteMemberFromGroup(activeGroup, ePerson.eperson);
|
||||
this.showNotifications('deleteMember', response, ePerson.eperson.name, activeGroup);
|
||||
this.search({ scope: this.currentSearchScope, query: this.currentSearchQuery });
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup'));
|
||||
}
|
||||
@@ -315,7 +314,6 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
||||
response.pipe(getFirstCompletedRemoteData()).subscribe((rd: RemoteData<any>) => {
|
||||
if (rd.hasSucceeded) {
|
||||
this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.success.' + messageSuffix, { name: nameObject }));
|
||||
this.ePersonDataService.clearLinkRequests(activeGroup._links.epersons.href);
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.' + messageSuffix, { name: nameObject }));
|
||||
}
|
||||
|
@@ -5,11 +5,12 @@ import { TranslateService } from '@ngx-translate/core';
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatest as observableCombineLatest,
|
||||
EMPTY,
|
||||
Observable,
|
||||
of as observableOf,
|
||||
Subscription
|
||||
} from 'rxjs';
|
||||
import { catchError, map, switchMap, tap } from 'rxjs/operators';
|
||||
import { catchError, defaultIfEmpty, map, switchMap, tap } from 'rxjs/operators';
|
||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||
@@ -144,7 +145,7 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
return this.authorizationService.isAuthorized(FeatureID.AdministratorOf).pipe(
|
||||
switchMap((isSiteAdmin: boolean) => {
|
||||
return observableCombineLatest(groups.page.map((group: Group) => {
|
||||
return observableCombineLatest([...groups.page.map((group: Group) => {
|
||||
if (hasValue(group) && !this.deletedGroupsIds.includes(group.id)) {
|
||||
return observableCombineLatest([
|
||||
this.authorizationService.isAuthorized(FeatureID.CanDelete, group.self),
|
||||
@@ -165,8 +166,10 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
)
|
||||
);
|
||||
} else {
|
||||
return EMPTY;
|
||||
}
|
||||
})).pipe(map((dtos: GroupDtoModel[]) => {
|
||||
})]).pipe(defaultIfEmpty([]), map((dtos: GroupDtoModel[]) => {
|
||||
return buildPaginatedList(groups.pageInfo, dtos);
|
||||
}));
|
||||
})
|
||||
|
@@ -0,0 +1,35 @@
|
||||
<div class="container">
|
||||
<h2 id="header">{{'admin.batch-import.page.header' | translate}}</h2>
|
||||
<p>{{'admin.batch-import.page.help' | translate}}</p>
|
||||
<p *ngIf="dso">
|
||||
selected collection: <b>{{getDspaceObjectName()}}</b>
|
||||
<a href="javascript:void(0)" (click)="removeDspaceObject()">{{'admin.batch-import.page.remove' | translate}}</a>
|
||||
</p>
|
||||
<p>
|
||||
<button class="btn btn-primary" (click)="this.selectCollection();">{{'admin.metadata-import.page.button.select-collection' | translate}}</button>
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="validateOnly" [(ngModel)]="validateOnly">
|
||||
<label class="form-check-label" for="validateOnly">
|
||||
{{'admin.metadata-import.page.validateOnly' | translate}}
|
||||
</label>
|
||||
</div>
|
||||
<small id="validateOnlyHelpBlock" class="form-text text-muted">
|
||||
{{'admin.batch-import.page.validateOnly.hint' | translate}}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<ds-file-dropzone-no-uploader
|
||||
(onFileAdded)="setFile($event)"
|
||||
[dropMessageLabel]="'admin.batch-import.page.dropMsg'"
|
||||
[dropMessageLabelReplacement]="'admin.batch-import.page.dropMsgReplace'">
|
||||
</ds-file-dropzone-no-uploader>
|
||||
|
||||
<div class="space-children-mr">
|
||||
<button class="btn btn-secondary" id="backButton"
|
||||
(click)="this.onReturn();">{{'admin.metadata-import.page.button.return' | translate}}</button>
|
||||
<button class="btn btn-primary" id="proceedButton"
|
||||
(click)="this.importMetadata();">{{'admin.metadata-import.page.button.proceed' | translate}}</button>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,151 @@
|
||||
import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { BatchImportPageComponent } from './batch-import-page.component';
|
||||
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
|
||||
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { FileValueAccessorDirective } from '../../shared/utils/file-value-accessor.directive';
|
||||
import { FileValidator } from '../../shared/utils/require-file.validator';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import {
|
||||
BATCH_IMPORT_SCRIPT_NAME,
|
||||
ScriptDataService
|
||||
} from '../../core/data/processes/script-data.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { Location } from '@angular/common';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { ProcessParameter } from '../../process-page/processes/process-parameter.model';
|
||||
|
||||
describe('BatchImportPageComponent', () => {
|
||||
let component: BatchImportPageComponent;
|
||||
let fixture: ComponentFixture<BatchImportPageComponent>;
|
||||
|
||||
let notificationService: NotificationsServiceStub;
|
||||
let scriptService: any;
|
||||
let router;
|
||||
let locationStub;
|
||||
|
||||
function init() {
|
||||
notificationService = new NotificationsServiceStub();
|
||||
scriptService = jasmine.createSpyObj('scriptService',
|
||||
{
|
||||
invoke: createSuccessfulRemoteDataObject$({ processId: '46' })
|
||||
}
|
||||
);
|
||||
router = jasmine.createSpyObj('router', {
|
||||
navigateByUrl: jasmine.createSpy('navigateByUrl')
|
||||
});
|
||||
locationStub = jasmine.createSpyObj('location', {
|
||||
back: jasmine.createSpy('back')
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
init();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
FormsModule,
|
||||
TranslateModule.forRoot(),
|
||||
RouterTestingModule.withRoutes([])
|
||||
],
|
||||
declarations: [BatchImportPageComponent, FileValueAccessorDirective, FileValidator],
|
||||
providers: [
|
||||
{ provide: NotificationsService, useValue: notificationService },
|
||||
{ provide: ScriptDataService, useValue: scriptService },
|
||||
{ provide: Router, useValue: router },
|
||||
{ provide: Location, useValue: locationStub },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(BatchImportPageComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('if back button is pressed', () => {
|
||||
beforeEach(fakeAsync(() => {
|
||||
const proceed = fixture.debugElement.query(By.css('#backButton')).nativeElement;
|
||||
proceed.click();
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
it('should do location.back', () => {
|
||||
expect(locationStub.back).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('if file is set', () => {
|
||||
let fileMock: File;
|
||||
|
||||
beforeEach(() => {
|
||||
fileMock = new File([''], 'filename.zip', { type: 'application/zip' });
|
||||
component.setFile(fileMock);
|
||||
});
|
||||
|
||||
describe('if proceed button is pressed without validate only', () => {
|
||||
beforeEach(fakeAsync(() => {
|
||||
component.validateOnly = false;
|
||||
const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement;
|
||||
proceed.click();
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
it('metadata-import script is invoked with --zip fileName and the mockFile', () => {
|
||||
const parameterValues: ProcessParameter[] = [
|
||||
Object.assign(new ProcessParameter(), { name: '--zip', value: 'filename.zip' }),
|
||||
];
|
||||
parameterValues.push(Object.assign(new ProcessParameter(), { name: '--add' }));
|
||||
expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [fileMock]);
|
||||
});
|
||||
it('success notification is shown', () => {
|
||||
expect(notificationService.success).toHaveBeenCalled();
|
||||
});
|
||||
it('redirected to process page', () => {
|
||||
expect(router.navigateByUrl).toHaveBeenCalledWith('/processes/46');
|
||||
});
|
||||
});
|
||||
|
||||
describe('if proceed button is pressed with validate only', () => {
|
||||
beforeEach(fakeAsync(() => {
|
||||
component.validateOnly = true;
|
||||
const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement;
|
||||
proceed.click();
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
it('metadata-import script is invoked with --zip fileName and the mockFile and -v validate-only', () => {
|
||||
const parameterValues: ProcessParameter[] = [
|
||||
Object.assign(new ProcessParameter(), { name: '--zip', value: 'filename.zip' }),
|
||||
Object.assign(new ProcessParameter(), { name: '--add' }),
|
||||
Object.assign(new ProcessParameter(), { name: '-v', value: true }),
|
||||
];
|
||||
expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [fileMock]);
|
||||
});
|
||||
it('success notification is shown', () => {
|
||||
expect(notificationService.success).toHaveBeenCalled();
|
||||
});
|
||||
it('redirected to process page', () => {
|
||||
expect(router.navigateByUrl).toHaveBeenCalledWith('/processes/46');
|
||||
});
|
||||
});
|
||||
|
||||
describe('if proceed is pressed; but script invoke fails', () => {
|
||||
beforeEach(fakeAsync(() => {
|
||||
jasmine.getEnv().allowRespy(true);
|
||||
spyOn(scriptService, 'invoke').and.returnValue(createFailedRemoteDataObject$('Error', 500));
|
||||
const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement;
|
||||
proceed.click();
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
it('error notification is shown', () => {
|
||||
expect(notificationService.error).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,124 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Location } from '@angular/common';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { BATCH_IMPORT_SCRIPT_NAME, ScriptDataService } from '../../core/data/processes/script-data.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { ProcessParameter } from '../../process-page/processes/process-parameter.model';
|
||||
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { Process } from '../../process-page/processes/process.model';
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
import { getProcessDetailRoute } from '../../process-page/process-page-routing.paths';
|
||||
import {
|
||||
ImportBatchSelectorComponent
|
||||
} from '../../shared/dso-selector/modal-wrappers/import-batch-selector/import-batch-selector.component';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-batch-import-page',
|
||||
templateUrl: './batch-import-page.component.html'
|
||||
})
|
||||
export class BatchImportPageComponent {
|
||||
/**
|
||||
* The current value of the file
|
||||
*/
|
||||
fileObject: File;
|
||||
|
||||
/**
|
||||
* The validate only flag
|
||||
*/
|
||||
validateOnly = true;
|
||||
/**
|
||||
* dso object for community or collection
|
||||
*/
|
||||
dso: DSpaceObject = null;
|
||||
|
||||
public constructor(private location: Location,
|
||||
protected translate: TranslateService,
|
||||
protected notificationsService: NotificationsService,
|
||||
private scriptDataService: ScriptDataService,
|
||||
private router: Router,
|
||||
private modalService: NgbModal,
|
||||
private dsoNameService: DSONameService) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set file
|
||||
* @param file
|
||||
*/
|
||||
setFile(file) {
|
||||
this.fileObject = file;
|
||||
}
|
||||
|
||||
/**
|
||||
* When return button is pressed go to previous location
|
||||
*/
|
||||
public onReturn() {
|
||||
this.location.back();
|
||||
}
|
||||
|
||||
public selectCollection() {
|
||||
const modalRef = this.modalService.open(ImportBatchSelectorComponent);
|
||||
modalRef.componentInstance.response.pipe(take(1)).subscribe((dso) => {
|
||||
this.dso = dso || null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts import-metadata script with --zip fileName (and the selected file)
|
||||
*/
|
||||
public importMetadata() {
|
||||
if (this.fileObject == null) {
|
||||
this.notificationsService.error(this.translate.get('admin.metadata-import.page.error.addFile'));
|
||||
} else {
|
||||
const parameterValues: ProcessParameter[] = [
|
||||
Object.assign(new ProcessParameter(), { name: '--zip', value: this.fileObject.name }),
|
||||
Object.assign(new ProcessParameter(), { name: '--add' })
|
||||
];
|
||||
if (this.dso) {
|
||||
parameterValues.push(Object.assign(new ProcessParameter(), { name: '--collection', value: this.dso.uuid }));
|
||||
}
|
||||
if (this.validateOnly) {
|
||||
parameterValues.push(Object.assign(new ProcessParameter(), { name: '-v', value: true }));
|
||||
}
|
||||
|
||||
this.scriptDataService.invoke(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [this.fileObject]).pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
).subscribe((rd: RemoteData<Process>) => {
|
||||
if (rd.hasSucceeded) {
|
||||
const title = this.translate.get('process.new.notification.success.title');
|
||||
const content = this.translate.get('process.new.notification.success.content');
|
||||
this.notificationsService.success(title, content);
|
||||
if (isNotEmpty(rd.payload)) {
|
||||
this.router.navigateByUrl(getProcessDetailRoute(rd.payload.processId));
|
||||
}
|
||||
} else {
|
||||
const title = this.translate.get('process.new.notification.error.title');
|
||||
const content = this.translate.get('process.new.notification.error.content');
|
||||
this.notificationsService.error(title, content);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* return selected dspace object name
|
||||
*/
|
||||
getDspaceObjectName(): string {
|
||||
if (this.dso) {
|
||||
return this.dsoNameService.getName(this.dso);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* remove selected dso object
|
||||
*/
|
||||
removeDspaceObject(): void {
|
||||
this.dso = null;
|
||||
}
|
||||
}
|
@@ -20,14 +20,12 @@ import { TestScheduler } from 'rxjs/testing';
|
||||
import {
|
||||
createNoContentRemoteDataObject$,
|
||||
createSuccessfulRemoteDataObject,
|
||||
createSuccessfulRemoteDataObject$
|
||||
createSuccessfulRemoteDataObject$,
|
||||
createFailedRemoteDataObject$
|
||||
} from '../../../shared/remote-data.utils';
|
||||
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
|
||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
||||
import { FindListOptions } from '../../../core/data/find-list-options.model';
|
||||
|
||||
describe('BitstreamFormatsComponent', () => {
|
||||
let comp: BitstreamFormatsComponent;
|
||||
@@ -85,10 +83,6 @@ describe('BitstreamFormatsComponent', () => {
|
||||
];
|
||||
const mockFormatsRD = createSuccessfulRemoteDataObject(createPaginatedList(mockFormatsList));
|
||||
|
||||
const pagination = Object.assign(new PaginationComponentOptions(), { currentPage: 1, pageSize: 20 });
|
||||
const sort = new SortOptions('score', SortDirection.DESC);
|
||||
const findlistOptions = Object.assign(new FindListOptions(), { currentPage: 1, elementsPerPage: 20 });
|
||||
|
||||
const initAsync = () => {
|
||||
notificationsServiceStub = new NotificationsServiceStub();
|
||||
|
||||
@@ -246,7 +240,7 @@ describe('BitstreamFormatsComponent', () => {
|
||||
));
|
||||
|
||||
beforeEach(initBeforeEach);
|
||||
it('should clear bitstream formats ', () => {
|
||||
it('should clear bitstream formats and show a success notification', () => {
|
||||
comp.deleteFormats();
|
||||
|
||||
expect(bitstreamFormatService.clearBitStreamFormatRequests).toHaveBeenCalled();
|
||||
@@ -275,7 +269,7 @@ describe('BitstreamFormatsComponent', () => {
|
||||
selectBitstreamFormat: {},
|
||||
deselectBitstreamFormat: {},
|
||||
deselectAllBitstreamFormats: {},
|
||||
delete: observableOf(false),
|
||||
delete: createFailedRemoteDataObject$(),
|
||||
clearBitStreamFormatRequests: observableOf('cleared')
|
||||
});
|
||||
|
||||
@@ -295,7 +289,7 @@ describe('BitstreamFormatsComponent', () => {
|
||||
));
|
||||
|
||||
beforeEach(initBeforeEach);
|
||||
it('should clear bitstream formats ', () => {
|
||||
it('should clear bitstream formats and show an error notification', () => {
|
||||
comp.deleteFormats();
|
||||
|
||||
expect(bitstreamFormatService.clearBitStreamFormatRequests).toHaveBeenCalled();
|
||||
|
@@ -5,7 +5,7 @@ import { PaginatedList } from '../../../core/data/paginated-list.model';
|
||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||
import { BitstreamFormat } from '../../../core/shared/bitstream-format.model';
|
||||
import { BitstreamFormatDataService } from '../../../core/data/bitstream-format-data.service';
|
||||
import { map, switchMap, take } from 'rxjs/operators';
|
||||
import { map, mergeMap, switchMap, take, toArray } from 'rxjs/operators';
|
||||
import { hasValue } from '../../../shared/empty.util';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { Router } from '@angular/router';
|
||||
@@ -13,6 +13,7 @@ import { TranslateService } from '@ngx-translate/core';
|
||||
import { NoContent } from '../../../core/shared/NoContent.model';
|
||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||
import { FindListOptions } from '../../../core/data/find-list-options.model';
|
||||
import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
|
||||
|
||||
/**
|
||||
* This component renders a list of bitstream formats
|
||||
@@ -58,18 +59,26 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy {
|
||||
* Deletes the currently selected formats from the registry and updates the presented list
|
||||
*/
|
||||
deleteFormats() {
|
||||
this.bitstreamFormatService.clearBitStreamFormatRequests().subscribe();
|
||||
this.bitstreamFormatService.getSelectedBitstreamFormats().pipe(take(1)).subscribe(
|
||||
(formats) => {
|
||||
const tasks$ = [];
|
||||
for (const format of formats) {
|
||||
if (hasValue(format.id)) {
|
||||
tasks$.push(this.bitstreamFormatService.delete(format.id).pipe(map((response: RemoteData<NoContent>) => response.hasSucceeded)));
|
||||
}
|
||||
}
|
||||
zip(...tasks$).subscribe((results: boolean[]) => {
|
||||
this.bitstreamFormatService.clearBitStreamFormatRequests();
|
||||
this.bitstreamFormatService.getSelectedBitstreamFormats().pipe(
|
||||
take(1),
|
||||
// emit all formats in the array one at a time
|
||||
mergeMap((formats: BitstreamFormat[]) => formats),
|
||||
// delete each format
|
||||
mergeMap((format: BitstreamFormat) => this.bitstreamFormatService.delete(format.id).pipe(
|
||||
// wait for each response to come back
|
||||
getFirstCompletedRemoteData(),
|
||||
// return a boolean to indicate whether a response succeeded
|
||||
map((response: RemoteData<NoContent>) => response.hasSucceeded),
|
||||
)),
|
||||
// wait for all responses to come in and return them as a single array
|
||||
toArray()
|
||||
).subscribe((results: boolean[]) => {
|
||||
// Count the number of succeeded and failed deletions
|
||||
const successResponses = results.filter((result: boolean) => result);
|
||||
const failedResponses = results.filter((result: boolean) => !result);
|
||||
|
||||
// Show a notification indicating the number of succeeded and failed deletions
|
||||
if (successResponses.length > 0) {
|
||||
this.showNotification(true, successResponses.length);
|
||||
}
|
||||
@@ -77,13 +86,13 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy {
|
||||
this.showNotification(false, failedResponses.length);
|
||||
}
|
||||
|
||||
// reset the selection
|
||||
this.deselectAll();
|
||||
|
||||
// reload the page
|
||||
this.paginationService.resetPage(this.pageConfig.id);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deselects all selecetd bitstream formats
|
||||
|
@@ -7,6 +7,7 @@ import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow
|
||||
import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service';
|
||||
import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curation-tasks.component';
|
||||
import { REGISTRIES_MODULE_PATH } from './admin-routing-paths';
|
||||
import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -40,6 +41,12 @@ import { REGISTRIES_MODULE_PATH } from './admin-routing-paths';
|
||||
component: MetadataImportPageComponent,
|
||||
data: { title: 'admin.metadata-import.title', breadcrumbKey: 'admin.metadata-import' }
|
||||
},
|
||||
{
|
||||
path: 'batch-import',
|
||||
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||
component: BatchImportPageComponent,
|
||||
data: { title: 'admin.batch-import.title', breadcrumbKey: 'admin.batch-import' }
|
||||
},
|
||||
])
|
||||
],
|
||||
providers: [
|
||||
|
@@ -9,6 +9,7 @@ import { AdminWorkflowModuleModule } from './admin-workflow-page/admin-workflow.
|
||||
import { AdminSearchModule } from './admin-search-page/admin-search.module';
|
||||
import { AdminSidebarSectionComponent } from './admin-sidebar/admin-sidebar-section/admin-sidebar-section.component';
|
||||
import { ExpandableAdminSidebarSectionComponent } from './admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component';
|
||||
import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component';
|
||||
|
||||
const ENTRY_COMPONENTS = [
|
||||
// put only entry components that use custom decorator
|
||||
@@ -28,7 +29,8 @@ const ENTRY_COMPONENTS = [
|
||||
],
|
||||
declarations: [
|
||||
AdminCurationTasksComponent,
|
||||
MetadataImportPageComponent
|
||||
MetadataImportPageComponent,
|
||||
BatchImportPageComponent
|
||||
]
|
||||
})
|
||||
export class AdminModule {
|
||||
|
@@ -25,6 +25,7 @@ import { ObjectCacheService } from '../../cache/object-cache.service';
|
||||
import { HALDataService } from './hal-data-service.interface';
|
||||
import { getFirstCompletedRemoteData } from '../../shared/operators';
|
||||
|
||||
export const EMBED_SEPARATOR = '%2F';
|
||||
/**
|
||||
* Common functionality for data services.
|
||||
* Specific functionality that not all services would need
|
||||
@@ -202,7 +203,7 @@ export class BaseDataService<T extends CacheableObject> implements HALDataServic
|
||||
let nestEmbed = embedString;
|
||||
linksToFollow.forEach((linkToFollow: FollowLinkConfig<T>) => {
|
||||
if (hasValue(linkToFollow) && linkToFollow.shouldEmbed) {
|
||||
nestEmbed = nestEmbed + '/' + String(linkToFollow.name);
|
||||
nestEmbed = nestEmbed + EMBED_SEPARATOR + String(linkToFollow.name);
|
||||
// Add the nested embeds size if given in the FollowLinkConfig.FindListOptions
|
||||
if (hasValue(linkToFollow.findListOptions) && hasValue(linkToFollow.findListOptions.elementsPerPage)) {
|
||||
const nestedEmbedSize = 'embed.size=' + nestEmbed.split('=')[1] + '=' + linkToFollow.findListOptions.elementsPerPage;
|
||||
|
@@ -22,6 +22,7 @@ import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.s
|
||||
import { HALEndpointService } from '../../shared/hal-endpoint.service';
|
||||
import { ObjectCacheService } from '../../cache/object-cache.service';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import { EMBED_SEPARATOR } from './base-data.service';
|
||||
|
||||
/**
|
||||
* Tests whether calls to `FindAllData` methods are correctly patched through in a concrete data service that implements it
|
||||
@@ -276,7 +277,7 @@ describe('FindAllDataImpl', () => {
|
||||
});
|
||||
|
||||
it('should include nested linksToFollow 3lvl', () => {
|
||||
const expected = `${endpoint}?embed=owningCollection/itemtemplate/relationships`;
|
||||
const expected = `${endpoint}?embed=owningCollection${EMBED_SEPARATOR}itemtemplate${EMBED_SEPARATOR}relationships`;
|
||||
|
||||
(service as any).getFindAllHref({}, null, followLink('owningCollection', {}, followLink('itemtemplate', {}, followLink('relationships')))).subscribe((value) => {
|
||||
expect(value).toBe(expected);
|
||||
@@ -284,7 +285,7 @@ describe('FindAllDataImpl', () => {
|
||||
});
|
||||
|
||||
it('should include nested linksToFollow 2lvl and nested embed\'s size', () => {
|
||||
const expected = `${endpoint}?embed.size=owningCollection/itemtemplate=4&embed=owningCollection/itemtemplate`;
|
||||
const expected = `${endpoint}?embed.size=owningCollection${EMBED_SEPARATOR}itemtemplate=4&embed=owningCollection${EMBED_SEPARATOR}itemtemplate`;
|
||||
const config: FindListOptions = Object.assign(new FindListOptions(), {
|
||||
elementsPerPage: 4,
|
||||
});
|
||||
|
@@ -19,6 +19,7 @@ import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.s
|
||||
import { HALEndpointService } from '../../shared/hal-endpoint.service';
|
||||
import { ObjectCacheService } from '../../cache/object-cache.service';
|
||||
import { IdentifiableDataService } from './identifiable-data.service';
|
||||
import { EMBED_SEPARATOR } from './base-data.service';
|
||||
|
||||
const endpoint = 'https://rest.api/core';
|
||||
|
||||
@@ -137,7 +138,7 @@ describe('IdentifiableDataService', () => {
|
||||
});
|
||||
|
||||
it('should include nested linksToFollow 3lvl', () => {
|
||||
const expected = `${endpointMock}/${resourceIdMock}?embed=owningCollection/itemtemplate/relationships`;
|
||||
const expected = `${endpointMock}/${resourceIdMock}?embed=owningCollection${EMBED_SEPARATOR}itemtemplate${EMBED_SEPARATOR}relationships`;
|
||||
const result = (service as any).getIDHref(endpointMock, resourceIdMock, followLink('owningCollection', {}, followLink('itemtemplate', {}, followLink('relationships'))));
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
@@ -9,6 +9,7 @@ import { GetRequest, IdentifierType } from './request.models';
|
||||
import { RequestService } from './request.service';
|
||||
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
|
||||
import { Item } from '../shared/item.model';
|
||||
import { EMBED_SEPARATOR } from './base/base-data.service';
|
||||
|
||||
describe('DsoRedirectService', () => {
|
||||
let scheduler: TestScheduler;
|
||||
@@ -174,7 +175,7 @@ describe('DsoRedirectService', () => {
|
||||
});
|
||||
|
||||
it('should include nested linksToFollow 3lvl', () => {
|
||||
const expected = `${requestUUIDURL}&embed=owningCollection/itemtemplate/relationships`;
|
||||
const expected = `${requestUUIDURL}&embed=owningCollection${EMBED_SEPARATOR}itemtemplate${EMBED_SEPARATOR}relationships`;
|
||||
const result = (service as any).dataService.getIDHref(
|
||||
pidLink,
|
||||
dsoUUID,
|
||||
|
@@ -9,6 +9,8 @@ import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { RequestEntry } from './request-entry.model';
|
||||
import { HttpHeaders } from '@angular/common/http';
|
||||
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||
|
||||
describe('EpersonRegistrationService', () => {
|
||||
let testScheduler;
|
||||
@@ -79,8 +81,23 @@ describe('EpersonRegistrationService', () => {
|
||||
it('should send an email registration', () => {
|
||||
|
||||
const expected = service.registerEmail('test@mail.org');
|
||||
let headers = new HttpHeaders();
|
||||
const options: HttpOptions = Object.create({});
|
||||
options.headers = headers;
|
||||
|
||||
expect(requestService.send).toHaveBeenCalledWith(new PostRequest('request-id', 'rest-url/registrations', registration));
|
||||
expect(requestService.send).toHaveBeenCalledWith(new PostRequest('request-id', 'rest-url/registrations', registration, options));
|
||||
expect(expected).toBeObservable(cold('(a|)', { a: rd }));
|
||||
});
|
||||
|
||||
it('should send an email registration with captcha', () => {
|
||||
|
||||
const expected = service.registerEmail('test@mail.org', 'afreshcaptchatoken');
|
||||
let headers = new HttpHeaders();
|
||||
const options: HttpOptions = Object.create({});
|
||||
headers = headers.append('x-recaptcha-token', 'afreshcaptchatoken');
|
||||
options.headers = headers;
|
||||
|
||||
expect(requestService.send).toHaveBeenCalledWith(new PostRequest('request-id', 'rest-url/registrations', registration, options));
|
||||
expect(expected).toBeObservable(cold('(a|)', { a: rd }));
|
||||
});
|
||||
});
|
||||
|
@@ -3,15 +3,17 @@ import { RequestService } from './request.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { GetRequest, PostRequest } from './request.models';
|
||||
import { Observable } from 'rxjs';
|
||||
import { filter, find, map, skipWhile } from 'rxjs/operators';
|
||||
import { filter, find, map } from 'rxjs/operators';
|
||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||
import { Registration } from '../shared/registration.model';
|
||||
import { getFirstCompletedRemoteData, getFirstSucceededRemoteData } from '../shared/operators';
|
||||
import { getFirstCompletedRemoteData } from '../shared/operators';
|
||||
import { ResponseParsingService } from './parsing.service';
|
||||
import { GenericConstructor } from '../shared/generic-constructor';
|
||||
import { RegistrationResponseParsingService } from './registration-response-parsing.service';
|
||||
import { RemoteData } from './remote-data';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||
import { HttpHeaders } from '@angular/common/http';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -51,8 +53,9 @@ export class EpersonRegistrationService {
|
||||
/**
|
||||
* Register a new email address
|
||||
* @param email
|
||||
* @param captchaToken the value of x-recaptcha-token header
|
||||
*/
|
||||
registerEmail(email: string): Observable<RemoteData<Registration>> {
|
||||
registerEmail(email: string, captchaToken: string = null): Observable<RemoteData<Registration>> {
|
||||
const registration = new Registration();
|
||||
registration.email = email;
|
||||
|
||||
@@ -60,10 +63,17 @@ export class EpersonRegistrationService {
|
||||
|
||||
const href$ = this.getRegistrationEndpoint();
|
||||
|
||||
const options: HttpOptions = Object.create({});
|
||||
let headers = new HttpHeaders();
|
||||
if (captchaToken) {
|
||||
headers = headers.append('x-recaptcha-token', captchaToken);
|
||||
}
|
||||
options.headers = headers;
|
||||
|
||||
href$.pipe(
|
||||
find((href: string) => hasValue(href)),
|
||||
map((href: string) => {
|
||||
const request = new PostRequest(requestId, href, registration);
|
||||
const request = new PostRequest(requestId, href, registration, options);
|
||||
this.requestService.send(request);
|
||||
})
|
||||
).subscribe();
|
||||
|
@@ -24,6 +24,8 @@ import { dataService } from '../base/data-service.decorator';
|
||||
|
||||
export const METADATA_IMPORT_SCRIPT_NAME = 'metadata-import';
|
||||
export const METADATA_EXPORT_SCRIPT_NAME = 'metadata-export';
|
||||
export const BATCH_IMPORT_SCRIPT_NAME = 'import';
|
||||
export const BATCH_EXPORT_SCRIPT_NAME = 'export';
|
||||
|
||||
@Injectable()
|
||||
@dataService(SCRIPT)
|
||||
|
@@ -50,7 +50,6 @@ describe('EndUserAgreementGuard', () => {
|
||||
it('should return true', (done) => {
|
||||
environment.info.enableEndUserAgreement = false;
|
||||
guard.canActivate(undefined, Object.assign({ url: 'redirect' })).subscribe((result) => {
|
||||
console.log(result);
|
||||
expect(result).toEqual(true);
|
||||
done();
|
||||
});
|
||||
|
@@ -191,9 +191,7 @@ describe('GroupDataService', () => {
|
||||
callback();
|
||||
|
||||
expect(objectCache.getByHref).toHaveBeenCalledWith(EPersonMock2._links.self.href);
|
||||
expect(objectCache.getByHref).toHaveBeenCalledWith(GroupMock._links.self.href);
|
||||
expect(requestService.setStaleByUUID).toHaveBeenCalledTimes(4);
|
||||
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request1');
|
||||
expect(requestService.setStaleByUUID).toHaveBeenCalledTimes(2);
|
||||
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request2');
|
||||
});
|
||||
});
|
||||
@@ -218,9 +216,7 @@ describe('GroupDataService', () => {
|
||||
callback();
|
||||
|
||||
expect(objectCache.getByHref).toHaveBeenCalledWith(EPersonMock._links.self.href);
|
||||
expect(objectCache.getByHref).toHaveBeenCalledWith(GroupMock._links.self.href);
|
||||
expect(requestService.setStaleByUUID).toHaveBeenCalledTimes(4);
|
||||
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request1');
|
||||
expect(requestService.setStaleByUUID).toHaveBeenCalledTimes(2);
|
||||
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request2');
|
||||
});
|
||||
});
|
||||
|
@@ -179,7 +179,6 @@ export class GroupDataService extends IdentifiableDataService<Group> implements
|
||||
|
||||
return this.rdbService.buildFromRequestUUIDAndAwait(requestId, () => observableZip(
|
||||
this.invalidateByHref(ePerson._links.self.href),
|
||||
this.invalidateByHref(activeGroup._links.self.href),
|
||||
this.requestService.setStaleByHrefSubstring(ePerson._links.groups.href).pipe(take(1)),
|
||||
this.requestService.setStaleByHrefSubstring(activeGroup._links.epersons.href).pipe(take(1)),
|
||||
));
|
||||
@@ -198,7 +197,6 @@ export class GroupDataService extends IdentifiableDataService<Group> implements
|
||||
|
||||
return this.rdbService.buildFromRequestUUIDAndAwait(requestId, () => observableZip(
|
||||
this.invalidateByHref(ePerson._links.self.href),
|
||||
this.invalidateByHref(activeGroup._links.self.href),
|
||||
this.requestService.setStaleByHrefSubstring(ePerson._links.groups.href).pipe(take(1)),
|
||||
this.requestService.setStaleByHrefSubstring(activeGroup._links.epersons.href).pipe(take(1)),
|
||||
));
|
||||
|
@@ -0,0 +1,3 @@
|
||||
import { ResourceType } from '../../shared/resource-type';
|
||||
|
||||
export const WORKSPACEITEM = new ResourceType('workspaceitem');
|
25
src/app/core/google-recaptcha/google-recaptcha.module.ts
Normal file
25
src/app/core/google-recaptcha/google-recaptcha.module.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { GoogleRecaptchaComponent } from '../../shared/google-recaptcha/google-recaptcha.component';
|
||||
|
||||
import { GoogleRecaptchaService } from './google-recaptcha.service';
|
||||
|
||||
const PROVIDERS = [
|
||||
GoogleRecaptchaService
|
||||
];
|
||||
|
||||
const COMPONENTS = [
|
||||
GoogleRecaptchaComponent
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [ CommonModule ],
|
||||
providers: [...PROVIDERS],
|
||||
declarations: [...COMPONENTS],
|
||||
exports: [...COMPONENTS]
|
||||
})
|
||||
|
||||
/**
|
||||
* This module handles google recaptcha functionalities
|
||||
*/
|
||||
export class GoogleRecaptchaModule {}
|
@@ -0,0 +1,57 @@
|
||||
import { GoogleRecaptchaService } from './google-recaptcha.service';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import { NativeWindowRef } from '../services/window.service';
|
||||
|
||||
describe('GoogleRecaptchaService', () => {
|
||||
let service: GoogleRecaptchaService;
|
||||
|
||||
let rendererFactory2;
|
||||
let configurationDataService;
|
||||
let spy: jasmine.Spy;
|
||||
let scriptElementMock: any;
|
||||
let cookieService;
|
||||
let window;
|
||||
const innerHTMLTestValue = 'mock-script-inner-html';
|
||||
const document = { documentElement: { lang: 'en' } } as Document;
|
||||
scriptElementMock = {
|
||||
set innerHTML(newVal) { /* noop */ },
|
||||
get innerHTML() { return innerHTMLTestValue; }
|
||||
};
|
||||
|
||||
function init() {
|
||||
window = new NativeWindowRef();
|
||||
rendererFactory2 = jasmine.createSpyObj('rendererFactory2', {
|
||||
createRenderer: observableOf('googleRecaptchaToken'),
|
||||
createElement: scriptElementMock
|
||||
});
|
||||
configurationDataService = jasmine.createSpyObj('configurationDataService', {
|
||||
findByPropertyName: createSuccessfulRemoteDataObject$({ values: ['googleRecaptchaToken'] })
|
||||
});
|
||||
cookieService = jasmine.createSpyObj('cookieService', {
|
||||
get: '{%22token_item%22:true%2C%22impersonation%22:true%2C%22redirect%22:true%2C%22language%22:true%2C%22klaro%22:true%2C%22has_agreed_end_user%22:true%2C%22google-analytics%22:true}',
|
||||
set: () => {
|
||||
/* empty */
|
||||
}
|
||||
});
|
||||
service = new GoogleRecaptchaService(cookieService, document, window, rendererFactory2, configurationDataService);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
init();
|
||||
});
|
||||
|
||||
describe('getRecaptchaToken', () => {
|
||||
let result;
|
||||
|
||||
beforeEach(() => {
|
||||
spy = spyOn(service, 'getRecaptchaToken').and.stub();
|
||||
});
|
||||
|
||||
it('should send a Request with action', () => {
|
||||
service.getRecaptchaToken('test');
|
||||
expect(spy).toHaveBeenCalledWith('test');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
176
src/app/core/google-recaptcha/google-recaptcha.service.ts
Normal file
176
src/app/core/google-recaptcha/google-recaptcha.service.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import { Inject, Injectable, Renderer2, RendererFactory2 } from '@angular/core';
|
||||
import { getFirstCompletedRemoteData } from '../shared/operators';
|
||||
import { ConfigurationProperty } from '../shared/configuration-property.model';
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { ConfigurationDataService } from '../data/configuration-data.service';
|
||||
import { RemoteData } from '../data/remote-data';
|
||||
import { map, switchMap, take } from 'rxjs/operators';
|
||||
import { BehaviorSubject, combineLatest, Observable, of } from 'rxjs';
|
||||
import { CookieService } from '../services/cookie.service';
|
||||
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
|
||||
|
||||
export const CAPTCHA_COOKIE = '_GRECAPTCHA';
|
||||
export const CAPTCHA_NAME = 'google-recaptcha';
|
||||
|
||||
/**
|
||||
* A GoogleRecaptchaService used to send action and get a token from REST
|
||||
*/
|
||||
@Injectable()
|
||||
export class GoogleRecaptchaService {
|
||||
|
||||
private renderer: Renderer2;
|
||||
|
||||
/**
|
||||
* A Google Recaptcha version
|
||||
*/
|
||||
private captchaVersionSubject$ = new BehaviorSubject<string>(null);
|
||||
|
||||
/**
|
||||
* The Google Recaptcha Key
|
||||
*/
|
||||
private captchaKeySubject$ = new BehaviorSubject<string>(null);
|
||||
|
||||
/**
|
||||
* The Google Recaptcha mode
|
||||
*/
|
||||
private captchaModeSubject$ = new BehaviorSubject<string>(null);
|
||||
|
||||
captchaKey(): Observable<string> {
|
||||
return this.captchaKeySubject$.asObservable();
|
||||
}
|
||||
|
||||
captchaMode(): Observable<string> {
|
||||
return this.captchaModeSubject$.asObservable();
|
||||
}
|
||||
|
||||
captchaVersion(): Observable<string> {
|
||||
return this.captchaVersionSubject$.asObservable();
|
||||
}
|
||||
|
||||
constructor(
|
||||
private cookieService: CookieService,
|
||||
@Inject(DOCUMENT) private _document: Document,
|
||||
@Inject(NativeWindowService) private _window: NativeWindowRef,
|
||||
rendererFactory: RendererFactory2,
|
||||
private configService: ConfigurationDataService,
|
||||
) {
|
||||
if (this._window.nativeWindow) {
|
||||
this._window.nativeWindow.refreshCaptchaScript = this.refreshCaptchaScript;
|
||||
}
|
||||
this.renderer = rendererFactory.createRenderer(null, null);
|
||||
const registrationVerification$ = this.configService.findByPropertyName('registration.verification.enabled').pipe(
|
||||
take(1),
|
||||
getFirstCompletedRemoteData(),
|
||||
map((res: RemoteData<ConfigurationProperty>) => {
|
||||
return res.hasSucceeded && res.payload && isNotEmpty(res.payload.values) && res.payload.values[0].toLowerCase() === 'true';
|
||||
})
|
||||
);
|
||||
registrationVerification$.subscribe(registrationVerification => {
|
||||
if (registrationVerification) {
|
||||
this.loadRecaptchaProperties();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadRecaptchaProperties() {
|
||||
const recaptchaKeyRD$ = this.configService.findByPropertyName('google.recaptcha.key.site').pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
);
|
||||
const recaptchaVersionRD$ = this.configService.findByPropertyName('google.recaptcha.version').pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
);
|
||||
const recaptchaModeRD$ = this.configService.findByPropertyName('google.recaptcha.mode').pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
);
|
||||
combineLatest([recaptchaVersionRD$, recaptchaModeRD$, recaptchaKeyRD$]).subscribe(([recaptchaVersionRD, recaptchaModeRD, recaptchaKeyRD]) => {
|
||||
|
||||
if (
|
||||
this.cookieService.get('klaro-anonymous') && this.cookieService.get('klaro-anonymous')[CAPTCHA_NAME] &&
|
||||
recaptchaKeyRD.hasSucceeded && recaptchaVersionRD.hasSucceeded &&
|
||||
isNotEmpty(recaptchaVersionRD.payload?.values) && isNotEmpty(recaptchaKeyRD.payload?.values)
|
||||
) {
|
||||
const key = recaptchaKeyRD.payload?.values[0];
|
||||
const version = recaptchaVersionRD.payload?.values[0];
|
||||
this.captchaKeySubject$.next(key);
|
||||
this.captchaVersionSubject$.next(version);
|
||||
|
||||
let captchaUrl;
|
||||
switch (version) {
|
||||
case 'v3':
|
||||
if (recaptchaKeyRD.hasSucceeded && isNotEmpty(recaptchaKeyRD.payload?.values)) {
|
||||
captchaUrl = this.buildCaptchaUrl(key);
|
||||
this.captchaModeSubject$.next('invisible');
|
||||
}
|
||||
break;
|
||||
case 'v2':
|
||||
if (recaptchaModeRD.hasSucceeded && isNotEmpty(recaptchaModeRD.payload?.values)) {
|
||||
captchaUrl = this.buildCaptchaUrl();
|
||||
this.captchaModeSubject$.next(recaptchaModeRD.payload?.values[0]);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// TODO handle error
|
||||
}
|
||||
if (captchaUrl) {
|
||||
this.loadScript(captchaUrl);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable of string
|
||||
* @param action action is the process type in which used to protect multiple spam REST calls
|
||||
*/
|
||||
public getRecaptchaToken(action) {
|
||||
return this.captchaKey().pipe(
|
||||
switchMap((key) => grecaptcha.execute(key, {action: action}))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable of string
|
||||
*/
|
||||
public executeRecaptcha() {
|
||||
return of(grecaptcha.execute());
|
||||
}
|
||||
|
||||
public getRecaptchaTokenResponse() {
|
||||
return grecaptcha.getResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the google captcha ur with google captchas api key
|
||||
*
|
||||
* @param key contains a secret key of a google captchas
|
||||
* @returns string which has google captcha url with google captchas key
|
||||
*/
|
||||
buildCaptchaUrl(key?: string) {
|
||||
const apiUrl = 'https://www.google.com/recaptcha/api.js';
|
||||
return key ? `${apiUrl}?render=${key}` : apiUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append the google captchas script to the document
|
||||
*
|
||||
* @param url contains a script url which will be loaded into page
|
||||
* @returns A promise
|
||||
*/
|
||||
private loadScript(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const script = this.renderer.createElement('script');
|
||||
script.type = 'text/javascript';
|
||||
script.src = url;
|
||||
script.text = ``;
|
||||
script.onload = resolve;
|
||||
script.onerror = reject;
|
||||
this.renderer.appendChild(this._document.head, script);
|
||||
});
|
||||
}
|
||||
|
||||
refreshCaptchaScript = () => {
|
||||
this.loadRecaptchaProperties();
|
||||
};
|
||||
|
||||
}
|
@@ -111,7 +111,6 @@ export class OrcidAuthService {
|
||||
).pipe(
|
||||
map(([authorizeUrl, clientId, scopes]) => {
|
||||
const redirectUri = new URLCombiner(this._window.nativeWindow.origin, encodeURIComponent(this.router.url.split('?')[0]));
|
||||
console.log(redirectUri.toString());
|
||||
return authorizeUrl.values[0] + '?client_id=' + clientId.values[0] + '&redirect_uri=' + redirectUri + '&response_type=code&scope='
|
||||
+ scopes.values.join(' ');
|
||||
}));
|
||||
|
@@ -25,5 +25,12 @@ export class Registration implements UnCacheableObject {
|
||||
* The token linked to the registration
|
||||
*/
|
||||
token: string;
|
||||
|
||||
/**
|
||||
* The token linked to the registration
|
||||
*/
|
||||
groupNames: string[];
|
||||
/**
|
||||
* The token linked to the registration
|
||||
*/
|
||||
groups: string[];
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||
import { BehaviorSubject, combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||
import { distinctUntilChanged, map } from 'rxjs/operators';
|
||||
import { Injectable, InjectionToken } from '@angular/core';
|
||||
import {
|
||||
@@ -26,6 +26,7 @@ const filterStateSelector = (state: SearchFiltersState) => state.searchFilter;
|
||||
|
||||
export const FILTER_CONFIG: InjectionToken<SearchFilterConfig> = new InjectionToken<SearchFilterConfig>('filterConfig');
|
||||
export const IN_PLACE_SEARCH: InjectionToken<boolean> = new InjectionToken<boolean>('inPlaceSearch');
|
||||
export const REFRESH_FILTER: InjectionToken<BehaviorSubject<any>> = new InjectionToken<boolean>('refreshFilters');
|
||||
|
||||
/**
|
||||
* Service that performs all actions that have to do with search filters and facets
|
||||
|
@@ -13,7 +13,6 @@ 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/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/models/search-objects.model';
|
||||
@@ -264,17 +263,26 @@ export class SearchService implements OnDestroy {
|
||||
* @param {number} valuePage The page number of the filter values
|
||||
* @param {SearchOptions} searchOptions The search configuration for the current search
|
||||
* @param {string} filterQuery The optional query used to filter out filter values
|
||||
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
|
||||
* no valid cached version. Defaults to true
|
||||
* @returns {Observable<RemoteData<PaginatedList<FacetValue>>>} Emits the given page of facet values
|
||||
*/
|
||||
getFacetValuesFor(filterConfig: SearchFilterConfig, valuePage: number, searchOptions?: SearchOptions, filterQuery?: string): Observable<RemoteData<FacetValues>> {
|
||||
getFacetValuesFor(filterConfig: SearchFilterConfig, valuePage: number, searchOptions?: PaginatedSearchOptions, filterQuery?: string, useCachedVersionIfAvailable = true): Observable<RemoteData<FacetValues>> {
|
||||
let href;
|
||||
const args: string[] = [`page=${valuePage - 1}`, `size=${filterConfig.pageSize}`];
|
||||
let args: string[] = [];
|
||||
if (hasValue(filterQuery)) {
|
||||
args.push(`prefix=${filterQuery}`);
|
||||
}
|
||||
if (hasValue(searchOptions)) {
|
||||
searchOptions = Object.assign(new PaginatedSearchOptions({}), searchOptions, {
|
||||
pagination: Object.assign({}, searchOptions.pagination, {
|
||||
currentPage: valuePage,
|
||||
pageSize: filterConfig.pageSize
|
||||
})
|
||||
});
|
||||
href = searchOptions.toRestUrl(filterConfig._links.self.href, args);
|
||||
} else {
|
||||
args = [`page=${valuePage - 1}`, `size=${filterConfig.pageSize}`, ...args];
|
||||
href = new URLCombiner(filterConfig._links.self.href, `?${args.join('&')}`).toString();
|
||||
}
|
||||
|
||||
@@ -284,7 +292,7 @@ export class SearchService implements OnDestroy {
|
||||
return FacetValueResponseParsingService;
|
||||
}
|
||||
});
|
||||
this.requestService.send(request, true);
|
||||
this.requestService.send(request, useCachedVersionIfAvailable);
|
||||
|
||||
return this.rdb.buildFromHref(href);
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@ import { deserializeAs, inheritSerialization } from 'cerialize';
|
||||
import { inheritLinkAnnotations, typedObject } from '../../cache/builders/build-decorators';
|
||||
import { IDToUUIDSerializer } from '../../cache/id-to-uuid-serializer';
|
||||
import { SubmissionObject } from './submission-object.model';
|
||||
import { ResourceType } from '../../shared/resource-type';
|
||||
import { WORKSPACEITEM } from '../../eperson/models/workspaceitem.resource-type';
|
||||
|
||||
/**
|
||||
* A model class for a WorkspaceItem.
|
||||
@@ -11,7 +11,7 @@ import { ResourceType } from '../../shared/resource-type';
|
||||
@inheritSerialization(SubmissionObject)
|
||||
@inheritLinkAnnotations(SubmissionObject)
|
||||
export class WorkspaceItem extends SubmissionObject {
|
||||
static type = new ResourceType('workspaceitem');
|
||||
static type = WORKSPACEITEM;
|
||||
|
||||
/**
|
||||
* The universally unique identifier of this WorkspaceItem
|
||||
|
@@ -6,6 +6,10 @@
|
||||
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true">
|
||||
</ds-thumbnail>
|
||||
</a>
|
||||
<span *ngIf="linkType == linkTypes.None" class="lead item-list-title dont-break-out">
|
||||
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true">
|
||||
</ds-thumbnail>
|
||||
</span>
|
||||
</div>
|
||||
<div [ngClass]="showThumbnails ? 'col-9' : 'col-md-12'">
|
||||
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
|
||||
|
@@ -6,6 +6,10 @@
|
||||
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true">
|
||||
</ds-thumbnail>
|
||||
</a>
|
||||
<span *ngIf="linkType == linkTypes.None" class="lead item-list-title dont-break-out">
|
||||
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true">
|
||||
</ds-thumbnail>
|
||||
</span>
|
||||
</div>
|
||||
<div [ngClass]="showThumbnails ? 'col-9' : 'col-md-12'">
|
||||
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
|
||||
|
@@ -5,6 +5,10 @@
|
||||
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true">
|
||||
</ds-thumbnail>
|
||||
</a>
|
||||
<span *ngIf="linkType == linkTypes.None" class="lead item-list-title dont-break-out">
|
||||
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true">
|
||||
</ds-thumbnail>
|
||||
</span>
|
||||
</div>
|
||||
<div [ngClass]="showThumbnails ? 'col-9' : 'col-md-12'">
|
||||
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
|
||||
|
@@ -9,6 +9,13 @@
|
||||
[placeholder]="'thumbnail.orgunit.placeholder'">
|
||||
</ds-thumbnail>
|
||||
</a>
|
||||
<span *ngIf="linkType == linkTypes.None" class="dont-break-out">
|
||||
<ds-thumbnail [thumbnail]="dso?.thumbnail | async"
|
||||
[defaultImage]="'assets/images/orgunit-placeholder.svg'"
|
||||
[alt]="'thumbnail.orgunit.alt'"
|
||||
[placeholder]="'thumbnail.orgunit.placeholder'">
|
||||
</ds-thumbnail>
|
||||
</span>
|
||||
</div>
|
||||
<div [ngClass]="showThumbnails ? 'col-9' : 'col-md-12'">
|
||||
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
|
||||
@@ -16,10 +23,10 @@
|
||||
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
|
||||
rel="noopener noreferrer"
|
||||
[routerLink]="[itemPageRoute]" class="lead"
|
||||
[innerHTML]="dsoTitle"></a>
|
||||
[innerHTML]="dsoTitle || ('orgunit.listelement.no-title' | translate)"></a>
|
||||
<span *ngIf="linkType == linkTypes.None"
|
||||
class="lead"
|
||||
[innerHTML]="dsoTitle"></span>
|
||||
[innerHTML]="dsoTitle || ('orgunit.listelement.no-title' | translate)"></span>
|
||||
<span class="text-muted">
|
||||
<span *ngIf="dso.allMetadata(['dc.description']).length > 0"
|
||||
class="item-list-org-unit-description">
|
||||
|
@@ -10,6 +10,8 @@ import { ItemSearchResult } from '../../../../../shared/object-collection/shared
|
||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
|
||||
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
import { TranslateLoaderMock } from '../../../../../shared/mocks/translate-loader.mock';
|
||||
|
||||
let orgUnitListElementComponent: OrgUnitSearchResultListElementComponent;
|
||||
let fixture: ComponentFixture<OrgUnitSearchResultListElementComponent>;
|
||||
@@ -66,6 +68,13 @@ const enviromentNoThumbs = {
|
||||
describe('OrgUnitSearchResultListElementComponent', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
}
|
||||
)],
|
||||
declarations: [ OrgUnitSearchResultListElementComponent , TruncatePipe],
|
||||
providers: [
|
||||
{ provide: TruncatableService, useValue: {} },
|
||||
@@ -129,6 +138,13 @@ describe('OrgUnitSearchResultListElementComponent', () => {
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
}
|
||||
)],
|
||||
declarations: [OrgUnitSearchResultListElementComponent, TruncatePipe],
|
||||
providers: [
|
||||
{provide: TruncatableService, useValue: {}},
|
||||
|
@@ -9,6 +9,13 @@
|
||||
[placeholder]="'thumbnail.person.placeholder'">
|
||||
</ds-thumbnail>
|
||||
</a>
|
||||
<span *ngIf="linkType == linkTypes.None" class="dont-break-out">
|
||||
<ds-thumbnail [thumbnail]="dso?.thumbnail | async"
|
||||
[defaultImage]="'assets/images/person-placeholder.svg'"
|
||||
[alt]="'thumbnail.person.alt'"
|
||||
[placeholder]="'thumbnail.person.placeholder'">
|
||||
</ds-thumbnail>
|
||||
</span>
|
||||
</div>
|
||||
<div [ngClass]="showThumbnails ? 'col-9 col-md-10' : 'col-12'">
|
||||
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
|
||||
@@ -16,10 +23,10 @@
|
||||
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
|
||||
rel="noopener noreferrer"
|
||||
[routerLink]="[itemPageRoute]" class="lead"
|
||||
[innerHTML]="dsoTitle"></a>
|
||||
[innerHTML]="dsoTitle || ('person.listelement.no-title' | translate)"></a>
|
||||
<span *ngIf="linkType == linkTypes.None"
|
||||
class="lead"
|
||||
[innerHTML]="dsoTitle"></span>
|
||||
[innerHTML]="dsoTitle || ('person.listelement.no-title' | translate)"></span>
|
||||
<span class="text-muted">
|
||||
<ds-truncatable-part [id]="dso.id" [minLines]="1">
|
||||
<span *ngIf="dso.allMetadata(['person.jobTitle']).length > 0"
|
||||
|
@@ -10,6 +10,8 @@ import { TruncatableService } from '../../../../../shared/truncatable/truncatabl
|
||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
|
||||
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
import { TranslateLoaderMock } from '../../../../../shared/mocks/translate-loader.mock';
|
||||
|
||||
let personListElementComponent: PersonSearchResultListElementComponent;
|
||||
let fixture: ComponentFixture<PersonSearchResultListElementComponent>;
|
||||
@@ -66,6 +68,13 @@ const enviromentNoThumbs = {
|
||||
describe('PersonSearchResultListElementComponent', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
}
|
||||
)],
|
||||
declarations: [PersonSearchResultListElementComponent, TruncatePipe],
|
||||
providers: [
|
||||
{ provide: TruncatableService, useValue: {} },
|
||||
@@ -129,6 +138,13 @@ describe('PersonSearchResultListElementComponent', () => {
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
}
|
||||
)],
|
||||
declarations: [PersonSearchResultListElementComponent, TruncatePipe],
|
||||
providers: [
|
||||
{provide: TruncatableService, useValue: {}},
|
||||
|
@@ -9,6 +9,13 @@
|
||||
[placeholder]="'thumbnail.project.placeholder'">
|
||||
</ds-thumbnail>
|
||||
</a>
|
||||
<span *ngIf="linkType == linkTypes.None" class="dont-break-out">
|
||||
<ds-thumbnail [thumbnail]="dso?.thumbnail | async"
|
||||
[defaultImage]="'assets/images/project-placeholder.svg'"
|
||||
[alt]="'thumbnail.project.alt'"
|
||||
[placeholder]="'thumbnail.project.placeholder'">
|
||||
</ds-thumbnail>
|
||||
</span>
|
||||
</div>
|
||||
<div [ngClass]="showThumbnails ? 'col-9' : 'col-md-12'">
|
||||
<ds-truncatable [id]="dso.id">
|
||||
|
@@ -8,6 +8,13 @@
|
||||
[placeholder]="'thumbnail.person.placeholder'">
|
||||
</ds-thumbnail>
|
||||
</a>
|
||||
<span *ngIf="linkType == linkTypes.None" class="dont-break-out">
|
||||
<ds-thumbnail [thumbnail]="dso?.thumbnail | async"
|
||||
[defaultImage]="'assets/images/person-placeholder.svg'"
|
||||
[alt]="'thumbnail.person.alt'"
|
||||
[placeholder]="'thumbnail.person.placeholder'">
|
||||
</ds-thumbnail>
|
||||
</span>
|
||||
</div>
|
||||
<div [ngClass]="showThumbnails ? 'col-9' : 'col-md-12'">
|
||||
<div class="d-flex">
|
||||
|
@@ -22,7 +22,6 @@ describe('ItemPageComponent', () => {
|
||||
|
||||
class AcceptNoneGuard implements CanActivate {
|
||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
|
||||
console.log('BLA');
|
||||
return observableOf(false);
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<div>
|
||||
<div [ngClass]="showThumbnails ? 'hide-modal-thumbnail-column' : ''">
|
||||
<div class="modal-header">{{'virtual-metadata.delete-relationship.modal-head' | translate}}
|
||||
<button type="button" class="close" (click)="close.emit()" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
|
@@ -7,6 +7,8 @@ import { VirtualMetadataComponent } from './virtual-metadata.component';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
||||
import { VarDirective } from '../../../shared/utils/var.directive';
|
||||
import { APP_CONFIG } from '../../../../config/app-config.interface';
|
||||
import { environment } from '../../../../environments/environment';
|
||||
|
||||
describe('VirtualMetadataComponent', () => {
|
||||
|
||||
@@ -46,6 +48,7 @@ describe('VirtualMetadataComponent', () => {
|
||||
declarations: [VirtualMetadataComponent, VarDirective],
|
||||
providers: [
|
||||
{ provide: ObjectUpdatesService, useValue: objectUpdatesService },
|
||||
{ provide: APP_CONFIG, useValue: environment }
|
||||
], schemas: [
|
||||
NO_ERRORS_SCHEMA
|
||||
]
|
||||
|
@@ -1,8 +1,9 @@
|
||||
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
|
||||
import { Component, EventEmitter, Inject, Input, OnInit, Output } from '@angular/core';
|
||||
import {Observable} from 'rxjs';
|
||||
import {Item} from '../../../core/shared/item.model';
|
||||
import {MetadataValue} from '../../../core/shared/metadata.models';
|
||||
import {ObjectUpdatesService} from '../../../core/data/object-updates/object-updates.service';
|
||||
import { APP_CONFIG, AppConfig } from '../../../../config/app-config.interface';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-virtual-metadata',
|
||||
@@ -45,6 +46,12 @@ export class VirtualMetadataComponent implements OnInit {
|
||||
*/
|
||||
@Output() save = new EventEmitter();
|
||||
|
||||
/**
|
||||
* Indicates when thumbnails are required by configuration and therefore
|
||||
* need to be hidden in the modal layout.
|
||||
*/
|
||||
showThumbnails: boolean;
|
||||
|
||||
/**
|
||||
* Get an array of the left and the right item of the relationship to be deleted.
|
||||
*/
|
||||
@@ -56,7 +63,9 @@ export class VirtualMetadataComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
protected objectUpdatesService: ObjectUpdatesService,
|
||||
@Inject(APP_CONFIG) protected appConfig: AppConfig,
|
||||
) {
|
||||
this.showThumbnails = this.appConfig.browseBy.showThumbnails;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -6,6 +6,8 @@ import { By } from '@angular/platform-browser';
|
||||
import { MetadataUriValuesComponent } from './metadata-uri-values.component';
|
||||
import { isNotEmpty } from '../../../shared/empty.util';
|
||||
import { MetadataValue } from '../../../core/shared/metadata.models';
|
||||
import { APP_CONFIG } from '../../../../config/app-config.interface';
|
||||
import { environment } from '../../../../environments/environment';
|
||||
|
||||
let comp: MetadataUriValuesComponent;
|
||||
let fixture: ComponentFixture<MetadataUriValuesComponent>;
|
||||
@@ -33,6 +35,9 @@ describe('MetadataUriValuesComponent', () => {
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
})],
|
||||
providers: [
|
||||
{ provide: APP_CONFIG, useValue: environment },
|
||||
],
|
||||
declarations: [MetadataUriValuesComponent],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(MetadataUriValuesComponent, {
|
||||
|
@@ -5,6 +5,8 @@ import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock
|
||||
import { MetadataValuesComponent } from './metadata-values.component';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { MetadataValue } from '../../../core/shared/metadata.models';
|
||||
import { APP_CONFIG } from '../../../../config/app-config.interface';
|
||||
import { environment } from '../../../../environments/environment';
|
||||
|
||||
let comp: MetadataValuesComponent;
|
||||
let fixture: ComponentFixture<MetadataValuesComponent>;
|
||||
@@ -32,8 +34,11 @@ describe('MetadataValuesComponent', () => {
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
},
|
||||
})],
|
||||
providers: [
|
||||
{ provide: APP_CONFIG, useValue: environment },
|
||||
],
|
||||
declarations: [MetadataValuesComponent],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(MetadataValuesComponent, {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { Component, Inject, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { MetadataValue } from '../../../core/shared/metadata.models';
|
||||
import { environment } from '../../../../environments/environment';
|
||||
import { APP_CONFIG, AppConfig } from '../../../../config/app-config.interface';
|
||||
|
||||
/**
|
||||
* This component renders the configured 'values' into the ds-metadata-field-wrapper component.
|
||||
@@ -13,6 +13,11 @@ import { environment } from '../../../../environments/environment';
|
||||
})
|
||||
export class MetadataValuesComponent implements OnChanges {
|
||||
|
||||
constructor(
|
||||
@Inject(APP_CONFIG) private appConfig: AppConfig,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* The metadata values to display
|
||||
*/
|
||||
@@ -41,6 +46,6 @@ export class MetadataValuesComponent implements OnChanges {
|
||||
renderMarkdown;
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
this.renderMarkdown = !!environment.markdown.enabled && this.enableMarkdown;
|
||||
this.renderMarkdown = !!this.appConfig.markdown.enabled && this.enableMarkdown;
|
||||
}
|
||||
}
|
||||
|
@@ -3,16 +3,14 @@ import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { ItemPageAbstractFieldComponent } from './item-page-abstract-field.component';
|
||||
import { TranslateLoaderMock } from '../../../../../shared/testing/translate-loader.mock';
|
||||
import { MetadataValuesComponent } from '../../../../field-components/metadata-values/metadata-values.component';
|
||||
import { mockItemWithMetadataFieldAndValue } from '../item-page-field.component.spec';
|
||||
import { SharedModule } from '../../../../../shared/shared.module';
|
||||
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
|
||||
import { environment } from '../../../../../../environments/environment';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
let comp: ItemPageAbstractFieldComponent;
|
||||
let fixture: ComponentFixture<ItemPageAbstractFieldComponent>;
|
||||
|
||||
const mockField = 'dc.description.abstract';
|
||||
const mockValue = 'test value';
|
||||
|
||||
describe('ItemPageAbstractFieldComponent', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
@@ -25,7 +23,10 @@ describe('ItemPageAbstractFieldComponent', () => {
|
||||
}),
|
||||
SharedModule,
|
||||
],
|
||||
declarations: [ItemPageAbstractFieldComponent, MetadataValuesComponent],
|
||||
providers: [
|
||||
{ provide: APP_CONFIG, useValue: environment },
|
||||
],
|
||||
declarations: [ItemPageAbstractFieldComponent],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(ItemPageAbstractFieldComponent, {
|
||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||
@@ -33,13 +34,13 @@ describe('ItemPageAbstractFieldComponent', () => {
|
||||
}));
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
|
||||
fixture = TestBed.createComponent(ItemPageAbstractFieldComponent);
|
||||
comp = fixture.componentInstance;
|
||||
comp.item = mockItemWithMetadataFieldAndValue(mockField, mockValue);
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should display display the correct metadata value', () => {
|
||||
expect(fixture.nativeElement.innerHTML).toContain(mockValue);
|
||||
it('should render a ds-metadata-values', () => {
|
||||
expect(fixture.debugElement.query(By.css('ds-metadata-values'))).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
@@ -5,6 +5,8 @@ import { TranslateLoaderMock } from '../../../../../shared/testing/translate-loa
|
||||
import { MetadataValuesComponent } from '../../../../field-components/metadata-values/metadata-values.component';
|
||||
import { mockItemWithMetadataFieldAndValue } from '../item-page-field.component.spec';
|
||||
import { ItemPageAuthorFieldComponent } from './item-page-author-field.component';
|
||||
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
|
||||
import { environment } from '../../../../../../environments/environment';
|
||||
|
||||
let comp: ItemPageAuthorFieldComponent;
|
||||
let fixture: ComponentFixture<ItemPageAuthorFieldComponent>;
|
||||
@@ -21,6 +23,9 @@ describe('ItemPageAuthorFieldComponent', () => {
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
})],
|
||||
providers: [
|
||||
{ provide: APP_CONFIG, useValue: environment },
|
||||
],
|
||||
declarations: [ItemPageAuthorFieldComponent, MetadataValuesComponent],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(ItemPageAuthorFieldComponent, {
|
||||
|
@@ -5,6 +5,8 @@ import { TranslateLoaderMock } from '../../../../../shared/testing/translate-loa
|
||||
import { MetadataValuesComponent } from '../../../../field-components/metadata-values/metadata-values.component';
|
||||
import { mockItemWithMetadataFieldAndValue } from '../item-page-field.component.spec';
|
||||
import { ItemPageDateFieldComponent } from './item-page-date-field.component';
|
||||
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
|
||||
import { environment } from '../../../../../../environments/environment';
|
||||
|
||||
let comp: ItemPageDateFieldComponent;
|
||||
let fixture: ComponentFixture<ItemPageDateFieldComponent>;
|
||||
@@ -21,6 +23,9 @@ describe('ItemPageDateFieldComponent', () => {
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
})],
|
||||
providers: [
|
||||
{ provide: APP_CONFIG, useValue: environment },
|
||||
],
|
||||
declarations: [ItemPageDateFieldComponent, MetadataValuesComponent],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(ItemPageDateFieldComponent, {
|
||||
|
@@ -5,6 +5,8 @@ import { TranslateLoaderMock } from '../../../../../shared/testing/translate-loa
|
||||
import { MetadataValuesComponent } from '../../../../field-components/metadata-values/metadata-values.component';
|
||||
import { mockItemWithMetadataFieldAndValue } from '../item-page-field.component.spec';
|
||||
import { GenericItemPageFieldComponent } from './generic-item-page-field.component';
|
||||
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
|
||||
import { environment } from '../../../../../../environments/environment';
|
||||
|
||||
let comp: GenericItemPageFieldComponent;
|
||||
let fixture: ComponentFixture<GenericItemPageFieldComponent>;
|
||||
@@ -23,6 +25,9 @@ describe('GenericItemPageFieldComponent', () => {
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
})],
|
||||
providers: [
|
||||
{ provide: APP_CONFIG, useValue: environment },
|
||||
],
|
||||
declarations: [GenericItemPageFieldComponent, MetadataValuesComponent],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(GenericItemPageFieldComponent, {
|
||||
|
@@ -23,8 +23,16 @@ const mockLabel = 'test label';
|
||||
const mockFields = [mockField];
|
||||
|
||||
describe('ItemPageFieldComponent', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
|
||||
let appConfig = Object.assign({}, environment, {
|
||||
markdown: {
|
||||
enabled: false,
|
||||
mathjax: false,
|
||||
}
|
||||
});
|
||||
|
||||
const buildTestEnvironment = async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
@@ -35,7 +43,7 @@ describe('ItemPageFieldComponent', () => {
|
||||
SharedModule,
|
||||
],
|
||||
providers: [
|
||||
{ provide: APP_CONFIG, useValue: Object.assign({}, environment) },
|
||||
{ provide: APP_CONFIG, useValue: appConfig },
|
||||
],
|
||||
declarations: [ItemPageFieldComponent, MetadataValuesComponent],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
@@ -43,26 +51,25 @@ describe('ItemPageFieldComponent', () => {
|
||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||
}).compileComponents();
|
||||
markdownSpy = spyOn(MarkdownPipe.prototype, 'transform');
|
||||
}));
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
fixture = TestBed.createComponent(ItemPageFieldComponent);
|
||||
comp = fixture.componentInstance;
|
||||
comp.item = mockItemWithMetadataFieldAndValue(mockField, mockValue);
|
||||
comp.fields = mockFields;
|
||||
comp.label = mockLabel;
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
};
|
||||
|
||||
it('should display display the correct metadata value', () => {
|
||||
it('should display display the correct metadata value', waitForAsync(async () => {
|
||||
await buildTestEnvironment();
|
||||
expect(fixture.nativeElement.innerHTML).toContain(mockValue);
|
||||
});
|
||||
}));
|
||||
|
||||
describe('when markdown is disabled in the environment config', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.inject(APP_CONFIG).markdown.enabled = false;
|
||||
});
|
||||
beforeEach(waitForAsync(async () => {
|
||||
appConfig.markdown.enabled = false;
|
||||
await buildTestEnvironment();
|
||||
}));
|
||||
|
||||
describe('and markdown is disabled in this component', () => {
|
||||
|
||||
@@ -91,9 +98,10 @@ describe('ItemPageFieldComponent', () => {
|
||||
|
||||
describe('when markdown is enabled in the environment config', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.inject(APP_CONFIG).markdown.enabled = true;
|
||||
});
|
||||
beforeEach(waitForAsync(async () => {
|
||||
appConfig.markdown.enabled = true;
|
||||
await buildTestEnvironment();
|
||||
}));
|
||||
|
||||
describe('and markdown is disabled in this component', () => {
|
||||
|
||||
|
@@ -5,6 +5,8 @@ import { TranslateLoaderMock } from '../../../../../shared/testing/translate-loa
|
||||
import { mockItemWithMetadataFieldAndValue } from '../item-page-field.component.spec';
|
||||
import { ItemPageUriFieldComponent } from './item-page-uri-field.component';
|
||||
import { MetadataUriValuesComponent } from '../../../../field-components/metadata-uri-values/metadata-uri-values.component';
|
||||
import { environment } from '../../../../../../environments/environment';
|
||||
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
|
||||
|
||||
let comp: ItemPageUriFieldComponent;
|
||||
let fixture: ComponentFixture<ItemPageUriFieldComponent>;
|
||||
@@ -22,6 +24,9 @@ describe('ItemPageUriFieldComponent', () => {
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
})],
|
||||
providers: [
|
||||
{ provide: APP_CONFIG, useValue: environment },
|
||||
],
|
||||
declarations: [ItemPageUriFieldComponent, MetadataUriValuesComponent],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(ItemPageUriFieldComponent, {
|
||||
|
@@ -259,9 +259,15 @@ describe('MenuResolver', () => {
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||
id: 'import', visible: true,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||
id: 'import_batch', parentID: 'import', visible: true,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||
id: 'export', visible: true,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||
id: 'export_batch', parentID: 'export', visible: true,
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -44,6 +44,9 @@ import {
|
||||
METADATA_IMPORT_SCRIPT_NAME,
|
||||
ScriptDataService
|
||||
} from './core/data/processes/script-data.service';
|
||||
import {
|
||||
ExportBatchSelectorComponent
|
||||
} from './shared/dso-selector/modal-wrappers/export-batch-selector/export-batch-selector.component';
|
||||
|
||||
/**
|
||||
* Creates all of the app's menus
|
||||
@@ -440,6 +443,20 @@ export class MenuResolver implements Resolve<boolean> {
|
||||
} as OnClickMenuItemModel,
|
||||
shouldPersistOnRouteChange: true
|
||||
});
|
||||
this.menuService.addSection(MenuID.ADMIN, {
|
||||
id: 'export_batch',
|
||||
parentID: 'export',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.export_batch',
|
||||
function: () => {
|
||||
this.modalService.open(ExportBatchSelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
shouldPersistOnRouteChange: true
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -448,20 +465,7 @@ export class MenuResolver implements Resolve<boolean> {
|
||||
* the import scripts exist and the current user is allowed to execute them
|
||||
*/
|
||||
createImportMenuSections() {
|
||||
const menuList = [
|
||||
// TODO: enable this menu item once the feature has been implemented
|
||||
// {
|
||||
// id: 'import_batch',
|
||||
// parentID: 'import',
|
||||
// active: false,
|
||||
// visible: true,
|
||||
// model: {
|
||||
// type: MenuItemType.LINK,
|
||||
// text: 'menu.section.import_batch',
|
||||
// link: ''
|
||||
// } as LinkMenuItemModel,
|
||||
// }
|
||||
];
|
||||
const menuList = [];
|
||||
menuList.forEach((menuSection) => this.menuService.addSection(MenuID.ADMIN, menuSection));
|
||||
|
||||
observableCombineLatest([
|
||||
@@ -498,6 +502,18 @@ export class MenuResolver implements Resolve<boolean> {
|
||||
} as LinkMenuItemModel,
|
||||
shouldPersistOnRouteChange: true
|
||||
});
|
||||
this.menuService.addSection(MenuID.ADMIN, {
|
||||
id: 'import_batch',
|
||||
parentID: 'import',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.import_batch',
|
||||
link: '/admin/batch-import'
|
||||
} as LinkMenuItemModel,
|
||||
shouldPersistOnRouteChange: true
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -16,6 +16,7 @@ import { WorkflowItemSearchResultListElementComponent } from '../shared/object-l
|
||||
import { PoolSearchResultDetailElementComponent } from '../shared/object-detail/my-dspace-result-detail-element/pool-search-result/pool-search-result-detail-element.component';
|
||||
import { ClaimedApprovedSearchResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-approved-search-result/claimed-approved-search-result-list-element.component';
|
||||
import { ClaimedDeclinedSearchResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-declined-search-result/claimed-declined-search-result-list-element.component';
|
||||
import { ResearchEntitiesModule } from '../entity-groups/research-entities/research-entities.module';
|
||||
|
||||
const ENTRY_COMPONENTS = [
|
||||
// put only entry components that use custom decorator
|
||||
@@ -38,6 +39,7 @@ const ENTRY_COMPONENTS = [
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
MyDspacePageRoutingModule,
|
||||
ResearchEntitiesModule.withEntryComponents()
|
||||
],
|
||||
declarations: [
|
||||
...ENTRY_COMPONENTS
|
||||
|
@@ -2,7 +2,7 @@
|
||||
<h2>{{MESSAGE_PREFIX + '.header'|translate}}</h2>
|
||||
<p>{{MESSAGE_PREFIX + '.info' | translate}}</p>
|
||||
|
||||
<form [class]="'ng-invalid'" [formGroup]="form" (ngSubmit)="register()">
|
||||
<form [class]="'ng-invalid'" [formGroup]="form">
|
||||
|
||||
<div class="form-group">
|
||||
<div class="row">
|
||||
@@ -28,9 +28,30 @@
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<ds-alert [type]="AlertTypeEnum.Warning" *ngIf="registrationVerification && !isRecaptchaCookieAccepted()">
|
||||
<p class="m-0" [innerHTML]="MESSAGE_PREFIX + '.google-recaptcha.must-accept-cookies' | translate"></p>
|
||||
<p class="m-0"><a href="javascript:void(0);" (click)="this.klaroService.showSettings()">{{ MESSAGE_PREFIX + '.google-recaptcha.open-cookie-settings' | translate }}</a></p>
|
||||
</ds-alert>
|
||||
|
||||
<div class="my-3" *ngIf="isRecaptchaCookieAccepted() && (googleRecaptchaService.captchaVersion() | async) === 'v2'">
|
||||
<ds-google-recaptcha [captchaMode]="(googleRecaptchaService.captchaMode() | async)"
|
||||
(executeRecaptcha)="register($event)" (checkboxChecked)="onCheckboxChecked($event)"
|
||||
(showNotification)="showNotification($event)"></ds-google-recaptcha>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="!((googleRecaptchaService.captchaVersion() | async) === 'v2' && (googleRecaptchaService.captchaMode() | async) === 'invisible'); else v2Invisible">
|
||||
<button class="btn btn-primary" [disabled]="form.invalid || registrationVerification && !isRecaptchaCookieAccepted() || disableUntilChecked" (click)="register()">
|
||||
{{ MESSAGE_PREFIX + '.submit' | translate }}
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #v2Invisible>
|
||||
<button class="btn btn-primary" [disabled]="form.invalid" (click)="executeRecaptcha()">
|
||||
{{ MESSAGE_PREFIX + '.submit' | translate }}
|
||||
</button>
|
||||
</ng-template>
|
||||
</form>
|
||||
|
||||
<button class="btn btn-primary"
|
||||
[disabled]="form.invalid"
|
||||
(click)="register()">{{MESSAGE_PREFIX + '.submit'| translate}}</button>
|
||||
|
||||
</div>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { waitForAsync, ComponentFixture, TestBed, tick, fakeAsync } from '@angular/core/testing';
|
||||
import { of as observableOf, of } from 'rxjs';
|
||||
import { RestResponse } from '../core/cache/response.models';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
@@ -14,6 +14,10 @@ import { RouterStub } from '../shared/testing/router.stub';
|
||||
import { NotificationsServiceStub } from '../shared/testing/notifications-service.stub';
|
||||
import { RegisterEmailFormComponent } from './register-email-form.component';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
|
||||
import { ConfigurationDataService } from '../core/data/configuration-data.service';
|
||||
import { GoogleRecaptchaService } from '../core/google-recaptcha/google-recaptcha.service';
|
||||
import { CookieService } from '../core/services/cookie.service';
|
||||
import { CookieServiceMock } from '../shared/mocks/cookie.service.mock';
|
||||
|
||||
describe('RegisterEmailComponent', () => {
|
||||
|
||||
@@ -24,6 +28,22 @@ describe('RegisterEmailComponent', () => {
|
||||
let epersonRegistrationService: EpersonRegistrationService;
|
||||
let notificationsService;
|
||||
|
||||
const configurationDataService = jasmine.createSpyObj('configurationDataService', {
|
||||
findByPropertyName: jasmine.createSpy('findByPropertyName')
|
||||
});
|
||||
|
||||
const captchaVersion$ = of('v3');
|
||||
const captchaMode$ = of('invisible');
|
||||
const confResponse$ = createSuccessfulRemoteDataObject$({ values: ['true'] });
|
||||
const confResponseDisabled$ = createSuccessfulRemoteDataObject$({ values: ['false'] });
|
||||
|
||||
const googleRecaptchaService = jasmine.createSpyObj('googleRecaptchaService', {
|
||||
getRecaptchaToken: Promise.resolve('googleRecaptchaToken'),
|
||||
executeRecaptcha: Promise.resolve('googleRecaptchaToken'),
|
||||
getRecaptchaTokenResponse: Promise.resolve('googleRecaptchaToken'),
|
||||
captchaVersion: captchaVersion$,
|
||||
captchaMode: captchaMode$,
|
||||
});
|
||||
beforeEach(waitForAsync(() => {
|
||||
|
||||
router = new RouterStub();
|
||||
@@ -39,8 +59,11 @@ describe('RegisterEmailComponent', () => {
|
||||
providers: [
|
||||
{provide: Router, useValue: router},
|
||||
{provide: EpersonRegistrationService, useValue: epersonRegistrationService},
|
||||
{provide: ConfigurationDataService, useValue: configurationDataService},
|
||||
{provide: FormBuilder, useValue: new FormBuilder()},
|
||||
{provide: NotificationsService, useValue: notificationsService},
|
||||
{provide: CookieService, useValue: new CookieServiceMock()},
|
||||
{provide: GoogleRecaptchaService, useValue: googleRecaptchaService},
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
}).compileComponents();
|
||||
@@ -48,6 +71,9 @@ describe('RegisterEmailComponent', () => {
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(RegisterEmailFormComponent);
|
||||
comp = fixture.componentInstance;
|
||||
googleRecaptchaService.captchaVersion$ = captchaVersion$;
|
||||
googleRecaptchaService.captchaMode$ = captchaMode$;
|
||||
configurationDataService.findByPropertyName.and.returnValues(confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$, confResponseDisabled$);
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
@@ -90,4 +116,33 @@ describe('RegisterEmailComponent', () => {
|
||||
expect(router.navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('register with google recaptcha', () => {
|
||||
beforeEach(fakeAsync(() => {
|
||||
configurationDataService.findByPropertyName.and.returnValues(confResponse$, confResponse$, confResponse$, confResponse$, confResponse$, confResponse$, confResponse$, confResponse$, confResponse$, confResponse$);
|
||||
googleRecaptchaService.captchaVersion$ = captchaVersion$;
|
||||
googleRecaptchaService.captchaMode$ = captchaMode$;
|
||||
comp.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should send a registration to the service and on success display a message and return to home', fakeAsync(() => {
|
||||
comp.form.patchValue({email: 'valid@email.org'});
|
||||
comp.register();
|
||||
tick();
|
||||
expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith('valid@email.org', 'googleRecaptchaToken');
|
||||
expect(notificationsService.success).toHaveBeenCalled();
|
||||
expect(router.navigate).toHaveBeenCalledWith(['/home']);
|
||||
}));
|
||||
it('should send a registration to the service and on error display a message', fakeAsync(() => {
|
||||
(epersonRegistrationService.registerEmail as jasmine.Spy).and.returnValue(observableOf(new RestResponse(false, 400, 'Bad Request')));
|
||||
|
||||
comp.form.patchValue({email: 'valid@email.org'});
|
||||
|
||||
comp.register();
|
||||
tick();
|
||||
expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith('valid@email.org', 'googleRecaptchaToken');
|
||||
expect(notificationsService.error).toHaveBeenCalled();
|
||||
expect(router.navigate).not.toHaveBeenCalled();
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { ChangeDetectorRef, Component, Input, OnInit, Optional } from '@angular/core';
|
||||
import { EpersonRegistrationService } from '../core/data/eperson-registration.service';
|
||||
import { NotificationsService } from '../shared/notifications/notifications.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
@@ -6,6 +6,16 @@ import { Router } from '@angular/router';
|
||||
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { Registration } from '../core/shared/registration.model';
|
||||
import { RemoteData } from '../core/data/remote-data';
|
||||
import { ConfigurationDataService } from '../core/data/configuration-data.service';
|
||||
import { getFirstSucceededRemoteDataPayload } from '../core/shared/operators';
|
||||
import { ConfigurationProperty } from '../core/shared/configuration-property.model';
|
||||
import { isNotEmpty } from '../shared/empty.util';
|
||||
import { BehaviorSubject, combineLatest, Observable, of, switchMap } from 'rxjs';
|
||||
import { map, startWith, take } from 'rxjs/operators';
|
||||
import { CAPTCHA_NAME, GoogleRecaptchaService } from '../core/google-recaptcha/google-recaptcha.service';
|
||||
import { AlertType } from '../shared/alert/aletr-type';
|
||||
import { KlaroService } from '../shared/cookies/klaro.service';
|
||||
import { CookieService } from '../core/services/cookie.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-register-email-form',
|
||||
@@ -27,12 +37,40 @@ export class RegisterEmailFormComponent implements OnInit {
|
||||
@Input()
|
||||
MESSAGE_PREFIX: string;
|
||||
|
||||
public AlertTypeEnum = AlertType;
|
||||
|
||||
/**
|
||||
* registration verification configuration
|
||||
*/
|
||||
registrationVerification = false;
|
||||
|
||||
/**
|
||||
* Return true if the user completed the reCaptcha verification (checkbox mode)
|
||||
*/
|
||||
checkboxCheckedSubject$ = new BehaviorSubject<boolean>(false);
|
||||
|
||||
disableUntilChecked = true;
|
||||
|
||||
captchaVersion(): Observable<string> {
|
||||
return this.googleRecaptchaService.captchaVersion();
|
||||
}
|
||||
|
||||
captchaMode(): Observable<string> {
|
||||
return this.googleRecaptchaService.captchaMode();
|
||||
}
|
||||
|
||||
constructor(
|
||||
private epersonRegistrationService: EpersonRegistrationService,
|
||||
private notificationService: NotificationsService,
|
||||
private translateService: TranslateService,
|
||||
private router: Router,
|
||||
private formBuilder: FormBuilder
|
||||
private formBuilder: FormBuilder,
|
||||
private configService: ConfigurationDataService,
|
||||
public googleRecaptchaService: GoogleRecaptchaService,
|
||||
public cookieService: CookieService,
|
||||
@Optional() public klaroService: KlaroService,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private notificationsService: NotificationsService,
|
||||
) {
|
||||
|
||||
}
|
||||
@@ -45,15 +83,70 @@ export class RegisterEmailFormComponent implements OnInit {
|
||||
],
|
||||
})
|
||||
});
|
||||
this.configService.findByPropertyName('registration.verification.enabled').pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
map((res: ConfigurationProperty) => res?.values[0].toLowerCase() === 'true')
|
||||
).subscribe((res: boolean) => {
|
||||
this.registrationVerification = res;
|
||||
});
|
||||
|
||||
this.disableUntilCheckedFcn().subscribe((res) => {
|
||||
this.disableUntilChecked = res;
|
||||
this.changeDetectorRef.detectChanges();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* execute the captcha function for v2 invisible
|
||||
*/
|
||||
executeRecaptcha() {
|
||||
this.googleRecaptchaService.executeRecaptcha();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an email address
|
||||
*/
|
||||
register() {
|
||||
register(tokenV2?) {
|
||||
if (!this.form.invalid) {
|
||||
this.epersonRegistrationService.registerEmail(this.email.value).subscribe((response: RemoteData<Registration>) => {
|
||||
if (this.registrationVerification) {
|
||||
combineLatest([this.captchaVersion(), this.captchaMode()]).pipe(
|
||||
switchMap(([captchaVersion, captchaMode]) => {
|
||||
if (captchaVersion === 'v3') {
|
||||
return this.googleRecaptchaService.getRecaptchaToken('register_email');
|
||||
} else if (captchaVersion === 'v2' && captchaMode === 'checkbox') {
|
||||
return of(this.googleRecaptchaService.getRecaptchaTokenResponse());
|
||||
} else if (captchaVersion === 'v2' && captchaMode === 'invisible') {
|
||||
return of(tokenV2);
|
||||
} else {
|
||||
console.error(`Invalid reCaptcha configuration: version = ${captchaVersion}, mode = ${captchaMode}`);
|
||||
this.showNotification('error');
|
||||
}
|
||||
}),
|
||||
take(1),
|
||||
).subscribe((token) => {
|
||||
if (isNotEmpty(token)) {
|
||||
this.registration(token);
|
||||
} else {
|
||||
console.error('reCaptcha error');
|
||||
this.showNotification('error');
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this.registration();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registration of an email address
|
||||
*/
|
||||
registration(captchaToken = null) {
|
||||
let registerEmail$ = captchaToken ?
|
||||
this.epersonRegistrationService.registerEmail(this.email.value, captchaToken) :
|
||||
this.epersonRegistrationService.registerEmail(this.email.value);
|
||||
registerEmail$.subscribe((response: RemoteData<Registration>) => {
|
||||
if (response.hasSucceeded) {
|
||||
this.notificationService.success(this.translateService.get(`${this.MESSAGE_PREFIX}.success.head`),
|
||||
this.translateService.get(`${this.MESSAGE_PREFIX}.success.content`, {email: this.email.value}));
|
||||
@@ -62,13 +155,55 @@ export class RegisterEmailFormComponent implements OnInit {
|
||||
this.notificationService.error(this.translateService.get(`${this.MESSAGE_PREFIX}.error.head`),
|
||||
this.translateService.get(`${this.MESSAGE_PREFIX}.error.content`, {email: this.email.value}));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the user has accepted the required cookies for reCaptcha
|
||||
*/
|
||||
isRecaptchaCookieAccepted(): boolean {
|
||||
const klaroAnonymousCookie = this.cookieService.get('klaro-anonymous');
|
||||
return isNotEmpty(klaroAnonymousCookie) ? klaroAnonymousCookie[CAPTCHA_NAME] : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the user has not completed the reCaptcha verification (checkbox mode)
|
||||
*/
|
||||
disableUntilCheckedFcn(): Observable<boolean> {
|
||||
const checked$ = this.checkboxCheckedSubject$.asObservable();
|
||||
return combineLatest([this.captchaVersion(), this.captchaMode(), checked$]).pipe(
|
||||
// disable if checkbox is not checked or if reCaptcha is not in v2 checkbox mode
|
||||
switchMap(([captchaVersion, captchaMode, checked]) => captchaVersion === 'v2' && captchaMode === 'checkbox' ? of(!checked) : of(false)),
|
||||
startWith(true),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
get email() {
|
||||
return this.form.get('email');
|
||||
}
|
||||
|
||||
onCheckboxChecked(checked: boolean) {
|
||||
this.checkboxCheckedSubject$.next(checked);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a notification to the user
|
||||
* @param key
|
||||
*/
|
||||
showNotification(key) {
|
||||
const notificationTitle = this.translateService.get(this.MESSAGE_PREFIX + '.google-recaptcha.notification.title');
|
||||
const notificationErrorMsg = this.translateService.get(this.MESSAGE_PREFIX + '.google-recaptcha.notification.message.error');
|
||||
const notificationExpiredMsg = this.translateService.get(this.MESSAGE_PREFIX + '.google-recaptcha.notification.message.expired');
|
||||
switch (key) {
|
||||
case 'expired':
|
||||
this.notificationsService.warning(notificationTitle, notificationExpiredMsg);
|
||||
break;
|
||||
case 'error':
|
||||
this.notificationsService.error(notificationTitle, notificationErrorMsg);
|
||||
break;
|
||||
default:
|
||||
console.warn(`Unimplemented notification '${key}' from reCaptcha service`);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -6,7 +6,7 @@ import { RegisterEmailFormComponent } from './register-email-form.component';
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
SharedModule
|
||||
],
|
||||
declarations: [
|
||||
RegisterEmailFormComponent,
|
||||
|
@@ -21,6 +21,8 @@ describe('BrowserKlaroService', () => {
|
||||
const trackingIdProp = 'google.analytics.key';
|
||||
const trackingIdTestValue = 'mock-tracking-id';
|
||||
const googleAnalytics = 'google-analytics';
|
||||
const recaptchaProp = 'registration.verification.enabled';
|
||||
const recaptchaValue = 'true';
|
||||
let translateService;
|
||||
let ePersonService;
|
||||
let authService;
|
||||
@@ -32,7 +34,7 @@ describe('BrowserKlaroService', () => {
|
||||
const createConfigSuccessSpy = (...values: string[]) => jasmine.createSpyObj('configurationDataService', {
|
||||
findByPropertyName: createSuccessfulRemoteDataObject$({
|
||||
... new ConfigurationProperty(),
|
||||
name: trackingIdProp,
|
||||
name: recaptchaProp,
|
||||
values: values,
|
||||
}),
|
||||
});
|
||||
@@ -57,7 +59,7 @@ describe('BrowserKlaroService', () => {
|
||||
isAuthenticated: observableOf(true),
|
||||
getAuthenticatedUserFromStore: observableOf(user)
|
||||
});
|
||||
configurationDataService = createConfigSuccessSpy(trackingIdTestValue);
|
||||
configurationDataService = createConfigSuccessSpy(recaptchaValue);
|
||||
findByPropertyName = configurationDataService.findByPropertyName;
|
||||
cookieService = jasmine.createSpyObj('cookieService', {
|
||||
get: '{%22token_item%22:true%2C%22impersonation%22:true%2C%22redirect%22:true%2C%22language%22:true%2C%22klaro%22:true%2C%22has_agreed_end_user%22:true%2C%22google-analytics%22:true}',
|
||||
@@ -298,15 +300,18 @@ describe('BrowserKlaroService', () => {
|
||||
|
||||
describe('initialize google analytics configuration', () => {
|
||||
let GOOGLE_ANALYTICS_KEY;
|
||||
let REGISTRATION_VERIFICATION_ENABLED_KEY;
|
||||
beforeEach(() => {
|
||||
GOOGLE_ANALYTICS_KEY = clone((service as any).GOOGLE_ANALYTICS_KEY);
|
||||
configurationDataService.findByPropertyName = findByPropertyName;
|
||||
REGISTRATION_VERIFICATION_ENABLED_KEY = clone((service as any).REGISTRATION_VERIFICATION_ENABLED_KEY);
|
||||
spyOn((service as any), 'getUser$').and.returnValue(observableOf(user));
|
||||
translateService.get.and.returnValue(observableOf('loading...'));
|
||||
spyOn(service, 'addAppMessages');
|
||||
spyOn((service as any), 'initializeUser');
|
||||
spyOn(service, 'translateConfiguration');
|
||||
configurationDataService.findByPropertyName = findByPropertyName;
|
||||
});
|
||||
|
||||
it('should not filter googleAnalytics when servicesToHide are empty', () => {
|
||||
const filteredConfig = (service as any).filterConfigServices([]);
|
||||
expect(filteredConfig).toContain(jasmine.objectContaining({ name: googleAnalytics }));
|
||||
@@ -316,30 +321,74 @@ describe('BrowserKlaroService', () => {
|
||||
expect(filteredConfig).not.toContain(jasmine.objectContaining({ name: googleAnalytics }));
|
||||
});
|
||||
it('should have been initialized with googleAnalytics', () => {
|
||||
configurationDataService.findByPropertyName = jasmine.createSpy('configurationDataService').and.returnValue(
|
||||
createSuccessfulRemoteDataObject$({
|
||||
...new ConfigurationProperty(),
|
||||
name: trackingIdProp,
|
||||
values: [googleAnalytics],
|
||||
})
|
||||
);
|
||||
service.initialize();
|
||||
expect(service.klaroConfig.services).toContain(jasmine.objectContaining({ name: googleAnalytics }));
|
||||
});
|
||||
it('should filter googleAnalytics when empty configuration is retrieved', () => {
|
||||
configurationDataService.findByPropertyName = jasmine.createSpy().withArgs(GOOGLE_ANALYTICS_KEY).and.returnValue(
|
||||
configurationDataService.findByPropertyName =
|
||||
jasmine.createSpy()
|
||||
.withArgs(GOOGLE_ANALYTICS_KEY)
|
||||
.and
|
||||
.returnValue(
|
||||
createSuccessfulRemoteDataObject$({
|
||||
... new ConfigurationProperty(),
|
||||
name: googleAnalytics,
|
||||
values: [],
|
||||
}));
|
||||
}
|
||||
)
|
||||
)
|
||||
.withArgs(REGISTRATION_VERIFICATION_ENABLED_KEY)
|
||||
.and
|
||||
.returnValue(
|
||||
createSuccessfulRemoteDataObject$({
|
||||
... new ConfigurationProperty(),
|
||||
name: trackingIdTestValue,
|
||||
values: ['false'],
|
||||
})
|
||||
);
|
||||
|
||||
service.initialize();
|
||||
expect(service.klaroConfig.services).not.toContain(jasmine.objectContaining({ name: googleAnalytics }));
|
||||
});
|
||||
it('should filter googleAnalytics when an error occurs', () => {
|
||||
configurationDataService.findByPropertyName = jasmine.createSpy().withArgs(GOOGLE_ANALYTICS_KEY).and.returnValue(
|
||||
createFailedRemoteDataObject$('Erro while loading GA')
|
||||
configurationDataService.findByPropertyName =
|
||||
jasmine.createSpy()
|
||||
.withArgs(GOOGLE_ANALYTICS_KEY).and.returnValue(
|
||||
createFailedRemoteDataObject$('Error while loading GA')
|
||||
)
|
||||
.withArgs(REGISTRATION_VERIFICATION_ENABLED_KEY)
|
||||
.and
|
||||
.returnValue(
|
||||
createSuccessfulRemoteDataObject$({
|
||||
... new ConfigurationProperty(),
|
||||
name: trackingIdTestValue,
|
||||
values: ['false'],
|
||||
})
|
||||
);
|
||||
service.initialize();
|
||||
expect(service.klaroConfig.services).not.toContain(jasmine.objectContaining({ name: googleAnalytics }));
|
||||
});
|
||||
it('should filter googleAnalytics when an invalid payload is retrieved', () => {
|
||||
configurationDataService.findByPropertyName = jasmine.createSpy().withArgs(GOOGLE_ANALYTICS_KEY).and.returnValue(
|
||||
configurationDataService.findByPropertyName =
|
||||
jasmine.createSpy()
|
||||
.withArgs(GOOGLE_ANALYTICS_KEY).and.returnValue(
|
||||
createSuccessfulRemoteDataObject$(null)
|
||||
)
|
||||
.withArgs(REGISTRATION_VERIFICATION_ENABLED_KEY)
|
||||
.and
|
||||
.returnValue(
|
||||
createSuccessfulRemoteDataObject$({
|
||||
... new ConfigurationProperty(),
|
||||
name: trackingIdTestValue,
|
||||
values: ['false'],
|
||||
})
|
||||
);
|
||||
service.initialize();
|
||||
expect(service.klaroConfig.services).not.toContain(jasmine.objectContaining({ name: googleAnalytics }));
|
||||
|
@@ -15,6 +15,7 @@ import { ANONYMOUS_STORAGE_NAME_KLARO, klaroConfiguration } from './klaro-config
|
||||
import { Operation } from 'fast-json-patch';
|
||||
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
|
||||
import { ConfigurationDataService } from '../../core/data/configuration-data.service';
|
||||
import { CAPTCHA_NAME } from '../../core/google-recaptcha/google-recaptcha.service';
|
||||
|
||||
/**
|
||||
* Metadata field to store a user's cookie consent preferences in
|
||||
@@ -49,6 +50,8 @@ export class BrowserKlaroService extends KlaroService {
|
||||
|
||||
private readonly GOOGLE_ANALYTICS_KEY = 'google.analytics.key';
|
||||
|
||||
private readonly REGISTRATION_VERIFICATION_ENABLED_KEY = 'registration.verification.enabled';
|
||||
|
||||
private readonly GOOGLE_ANALYTICS_SERVICE_NAME = 'google-analytics';
|
||||
|
||||
/**
|
||||
@@ -78,15 +81,30 @@ export class BrowserKlaroService extends KlaroService {
|
||||
this.klaroConfig.translations.en.consentNotice.description = 'cookies.consent.content-notice.description.no-privacy';
|
||||
}
|
||||
|
||||
const servicesToHide$: Observable<string[]> = this.configService.findByPropertyName(this.GOOGLE_ANALYTICS_KEY).pipe(
|
||||
const hideGoogleAnalytics$ = this.configService.findByPropertyName(this.GOOGLE_ANALYTICS_KEY).pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
map(remoteData => {
|
||||
if (!remoteData.hasSucceeded || !remoteData.payload || isEmpty(remoteData.payload.values)) {
|
||||
return [this.GOOGLE_ANALYTICS_SERVICE_NAME];
|
||||
} else {
|
||||
return [];
|
||||
map(remoteData => !remoteData.hasSucceeded || !remoteData.payload || isEmpty(remoteData.payload.values)),
|
||||
);
|
||||
|
||||
const hideRegistrationVerification$ = this.configService.findByPropertyName(this.REGISTRATION_VERIFICATION_ENABLED_KEY).pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
map((remoteData) =>
|
||||
!remoteData.hasSucceeded || !remoteData.payload || isEmpty(remoteData.payload.values) || remoteData.payload.values[0].toLowerCase() !== 'true'
|
||||
),
|
||||
);
|
||||
|
||||
const servicesToHide$: Observable<string[]> = observableCombineLatest([hideGoogleAnalytics$, hideRegistrationVerification$]).pipe(
|
||||
map(([hideGoogleAnalytics, hideRegistrationVerification]) => {
|
||||
let servicesToHideArray: string[] = [];
|
||||
if (hideGoogleAnalytics) {
|
||||
servicesToHideArray.push(this.GOOGLE_ANALYTICS_SERVICE_NAME);
|
||||
}
|
||||
}),
|
||||
if (hideRegistrationVerification) {
|
||||
servicesToHideArray.push(CAPTCHA_NAME);
|
||||
}
|
||||
console.log(servicesToHideArray);
|
||||
return servicesToHideArray;
|
||||
})
|
||||
);
|
||||
|
||||
this.translateService.setDefaultLang(environment.defaultLanguage);
|
||||
@@ -308,4 +326,5 @@ export class BrowserKlaroService extends KlaroService {
|
||||
private filterConfigServices(servicesToHide: string[]): Pick<typeof klaroConfiguration, 'services'>[] {
|
||||
return this.klaroConfig.services.filter(service => !servicesToHide.some(name => name === service.name));
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { TOKENITEM } from '../../core/auth/models/auth-token-info.model';
|
||||
import { IMPERSONATING_COOKIE, REDIRECT_COOKIE } from '../../core/auth/auth.service';
|
||||
import { LANG_COOKIE } from '../../core/locale/locale.service';
|
||||
import { CAPTCHA_COOKIE, CAPTCHA_NAME } from '../../core/google-recaptcha/google-recaptcha.service';
|
||||
|
||||
/**
|
||||
* Cookie for has_agreed_end_user
|
||||
@@ -157,5 +158,17 @@ export const klaroConfiguration: any = {
|
||||
*/
|
||||
onlyOnce: true,
|
||||
},
|
||||
{
|
||||
name: CAPTCHA_NAME,
|
||||
purposes: ['registration-password-recovery'],
|
||||
required: false,
|
||||
cookies: [
|
||||
[/^klaro-.+$/],
|
||||
CAPTCHA_COOKIE
|
||||
],
|
||||
onAccept: `window.refreshCaptchaScript?.call()`,
|
||||
onDecline: `window.refreshCaptchaScript?.call()`,
|
||||
onlyOnce: true,
|
||||
}
|
||||
],
|
||||
};
|
||||
|
@@ -10,7 +10,9 @@ export enum SelectorActionType {
|
||||
CREATE = 'create',
|
||||
EDIT = 'edit',
|
||||
EXPORT_METADATA = 'export-metadata',
|
||||
SET_SCOPE = 'set-scope'
|
||||
IMPORT_BATCH = 'import-batch',
|
||||
SET_SCOPE = 'set-scope',
|
||||
EXPORT_BATCH = 'export-batch'
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -0,0 +1,210 @@
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
import { DebugElement, NgModule, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { NgbActiveModal, NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { BATCH_EXPORT_SCRIPT_NAME, ScriptDataService } from '../../../../core/data/processes/script-data.service';
|
||||
import { Collection } from '../../../../core/shared/collection.model';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import { ProcessParameter } from '../../../../process-page/processes/process-parameter.model';
|
||||
import { ConfirmationModalComponent } from '../../../confirmation-modal/confirmation-modal.component';
|
||||
import { TranslateLoaderMock } from '../../../mocks/translate-loader.mock';
|
||||
import { NotificationsService } from '../../../notifications/notifications.service';
|
||||
import { NotificationsServiceStub } from '../../../testing/notifications-service.stub';
|
||||
import {
|
||||
createFailedRemoteDataObject$,
|
||||
createSuccessfulRemoteDataObject,
|
||||
createSuccessfulRemoteDataObject$
|
||||
} from '../../../remote-data.utils';
|
||||
import { ExportBatchSelectorComponent } from './export-batch-selector.component';
|
||||
import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service';
|
||||
|
||||
// No way to add entryComponents yet to testbed; alternative implemented; source: https://stackoverflow.com/questions/41689468/how-to-shallow-test-a-component-with-an-entrycomponents
|
||||
@NgModule({
|
||||
imports: [NgbModalModule,
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
}),
|
||||
],
|
||||
exports: [],
|
||||
declarations: [ConfirmationModalComponent],
|
||||
providers: []
|
||||
})
|
||||
class ModelTestModule {
|
||||
}
|
||||
|
||||
describe('ExportBatchSelectorComponent', () => {
|
||||
let component: ExportBatchSelectorComponent;
|
||||
let fixture: ComponentFixture<ExportBatchSelectorComponent>;
|
||||
let debugElement: DebugElement;
|
||||
let modalRef;
|
||||
|
||||
let router;
|
||||
let notificationService: NotificationsServiceStub;
|
||||
let scriptService;
|
||||
let authorizationDataService;
|
||||
|
||||
const mockItem = Object.assign(new Item(), {
|
||||
id: 'fake-id',
|
||||
uuid: 'fake-id',
|
||||
handle: 'fake/handle',
|
||||
lastModified: '2018'
|
||||
});
|
||||
|
||||
const mockCollection: Collection = Object.assign(new Collection(), {
|
||||
id: 'test-collection-1-1',
|
||||
uuid: 'test-collection-1-1',
|
||||
name: 'test-collection-1',
|
||||
metadata: {
|
||||
'dc.identifier.uri': [
|
||||
{
|
||||
language: null,
|
||||
value: 'fake/test-collection-1'
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
const itemRD = createSuccessfulRemoteDataObject(mockItem);
|
||||
const modalStub = jasmine.createSpyObj('modalStub', ['close']);
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
notificationService = new NotificationsServiceStub();
|
||||
router = jasmine.createSpyObj('router', {
|
||||
navigateByUrl: jasmine.createSpy('navigateByUrl')
|
||||
});
|
||||
scriptService = jasmine.createSpyObj('scriptService',
|
||||
{
|
||||
invoke: createSuccessfulRemoteDataObject$({ processId: '45' })
|
||||
}
|
||||
);
|
||||
authorizationDataService = jasmine.createSpyObj('authorizationDataService', {
|
||||
isAuthorized: observableOf(true)
|
||||
});
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), ModelTestModule],
|
||||
declarations: [ExportBatchSelectorComponent],
|
||||
providers: [
|
||||
{ provide: NgbActiveModal, useValue: modalStub },
|
||||
{ provide: NotificationsService, useValue: notificationService },
|
||||
{ provide: ScriptDataService, useValue: scriptService },
|
||||
{ provide: AuthorizationDataService, useValue: authorizationDataService },
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: {
|
||||
root: {
|
||||
snapshot: {
|
||||
data: {
|
||||
dso: itemRD,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: Router, useValue: router
|
||||
}
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ExportBatchSelectorComponent);
|
||||
component = fixture.componentInstance;
|
||||
debugElement = fixture.debugElement;
|
||||
const modalService = TestBed.inject(NgbModal);
|
||||
modalRef = modalService.open(ConfirmationModalComponent);
|
||||
modalRef.componentInstance.response = observableOf(true);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('if item is selected', () => {
|
||||
let scriptRequestSucceeded;
|
||||
beforeEach((done) => {
|
||||
component.navigate(mockItem).subscribe((succeeded: boolean) => {
|
||||
scriptRequestSucceeded = succeeded;
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('should not invoke batch-export script', () => {
|
||||
expect(scriptService.invoke).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('if collection is selected and is admin', () => {
|
||||
let scriptRequestSucceeded;
|
||||
beforeEach((done) => {
|
||||
spyOn((component as any).modalService, 'open').and.returnValue(modalRef);
|
||||
component.navigate(mockCollection).subscribe((succeeded: boolean) => {
|
||||
scriptRequestSucceeded = succeeded;
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('should invoke the batch-export script with option --id uuid option', () => {
|
||||
const parameterValues: ProcessParameter[] = [
|
||||
Object.assign(new ProcessParameter(), { name: '--id', value: mockCollection.uuid }),
|
||||
Object.assign(new ProcessParameter(), { name: '--type', value: 'COLLECTION' }),
|
||||
];
|
||||
expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_EXPORT_SCRIPT_NAME, parameterValues, []);
|
||||
});
|
||||
it('success notification is shown', () => {
|
||||
expect(scriptRequestSucceeded).toBeTrue();
|
||||
expect(notificationService.success).toHaveBeenCalled();
|
||||
});
|
||||
it('redirected to process page', () => {
|
||||
expect(router.navigateByUrl).toHaveBeenCalledWith('/processes/45');
|
||||
});
|
||||
});
|
||||
describe('if collection is selected and is not admin', () => {
|
||||
let scriptRequestSucceeded;
|
||||
beforeEach((done) => {
|
||||
(authorizationDataService.isAuthorized as jasmine.Spy).and.returnValue(observableOf(false));
|
||||
spyOn((component as any).modalService, 'open').and.returnValue(modalRef);
|
||||
component.navigate(mockCollection).subscribe((succeeded: boolean) => {
|
||||
scriptRequestSucceeded = succeeded;
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('should invoke the Batch-export script with option --id uuid without option', () => {
|
||||
const parameterValues: ProcessParameter[] = [
|
||||
Object.assign(new ProcessParameter(), { name: '--id', value: mockCollection.uuid }),
|
||||
Object.assign(new ProcessParameter(), { name: '--type', value: 'COLLECTION' })
|
||||
];
|
||||
expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_EXPORT_SCRIPT_NAME, parameterValues, []);
|
||||
});
|
||||
it('success notification is shown', () => {
|
||||
expect(scriptRequestSucceeded).toBeTrue();
|
||||
expect(notificationService.success).toHaveBeenCalled();
|
||||
});
|
||||
it('redirected to process page', () => {
|
||||
expect(router.navigateByUrl).toHaveBeenCalledWith('/processes/45');
|
||||
});
|
||||
});
|
||||
|
||||
describe('if collection is selected; but script invoke fails', () => {
|
||||
let scriptRequestSucceeded;
|
||||
beforeEach((done) => {
|
||||
spyOn((component as any).modalService, 'open').and.returnValue(modalRef);
|
||||
jasmine.getEnv().allowRespy(true);
|
||||
spyOn(scriptService, 'invoke').and.returnValue(createFailedRemoteDataObject$('Error', 500));
|
||||
component.navigate(mockCollection).subscribe((succeeded: boolean) => {
|
||||
scriptRequestSucceeded = succeeded;
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('error notification is shown', () => {
|
||||
expect(scriptRequestSucceeded).toBeFalse();
|
||||
expect(notificationService.error).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,111 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import { map, switchMap } from 'rxjs/operators';
|
||||
import { BATCH_EXPORT_SCRIPT_NAME, ScriptDataService } from '../../../../core/data/processes/script-data.service';
|
||||
import { Collection } from '../../../../core/shared/collection.model';
|
||||
import { DSpaceObjectType } from '../../../../core/shared/dspace-object-type.model';
|
||||
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
|
||||
import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ProcessParameter } from '../../../../process-page/processes/process-parameter.model';
|
||||
import { ConfirmationModalComponent } from '../../../confirmation-modal/confirmation-modal.component';
|
||||
import { isNotEmpty } from '../../../empty.util';
|
||||
import { NotificationsService } from '../../../notifications/notifications.service';
|
||||
import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils';
|
||||
import { DSOSelectorModalWrapperComponent, SelectorActionType } from '../dso-selector-modal-wrapper.component';
|
||||
import { getFirstCompletedRemoteData } from '../../../../core/shared/operators';
|
||||
import { Process } from '../../../../process-page/processes/process.model';
|
||||
import { RemoteData } from '../../../../core/data/remote-data';
|
||||
import { getProcessDetailRoute } from '../../../../process-page/process-page-routing.paths';
|
||||
import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../../../../core/data/feature-authorization/feature-id';
|
||||
|
||||
/**
|
||||
* Component to wrap a list of existing dso's inside a modal
|
||||
* Used to choose a dso from to export metadata of
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-export-metadata-selector',
|
||||
templateUrl: '../dso-selector-modal-wrapper.component.html',
|
||||
})
|
||||
export class ExportBatchSelectorComponent extends DSOSelectorModalWrapperComponent implements OnInit {
|
||||
objectType = DSpaceObjectType.DSPACEOBJECT;
|
||||
selectorTypes = [DSpaceObjectType.COLLECTION];
|
||||
action = SelectorActionType.EXPORT_BATCH;
|
||||
|
||||
constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute, private router: Router,
|
||||
protected notificationsService: NotificationsService, protected translationService: TranslateService,
|
||||
protected scriptDataService: ScriptDataService,
|
||||
protected authorizationDataService: AuthorizationDataService,
|
||||
private modalService: NgbModal) {
|
||||
super(activeModal, route);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the dso is a collection or community: start export-metadata script & navigate to process if successful
|
||||
* Otherwise show error message
|
||||
*/
|
||||
navigate(dso: DSpaceObject): Observable<boolean> {
|
||||
if (dso instanceof Collection) {
|
||||
const modalRef = this.modalService.open(ConfirmationModalComponent);
|
||||
modalRef.componentInstance.dso = dso;
|
||||
modalRef.componentInstance.headerLabel = 'confirmation-modal.export-batch.header';
|
||||
modalRef.componentInstance.infoLabel = 'confirmation-modal.export-batch.info';
|
||||
modalRef.componentInstance.cancelLabel = 'confirmation-modal.export-batch.cancel';
|
||||
modalRef.componentInstance.confirmLabel = 'confirmation-modal.export-batch.confirm';
|
||||
modalRef.componentInstance.confirmIcon = 'fas fa-file-export';
|
||||
const resp$ = modalRef.componentInstance.response.pipe(switchMap((confirm: boolean) => {
|
||||
if (confirm) {
|
||||
const startScriptSucceeded$ = this.startScriptNotifyAndRedirect(dso);
|
||||
return startScriptSucceeded$.pipe(
|
||||
switchMap((r: boolean) => {
|
||||
return observableOf(r);
|
||||
})
|
||||
);
|
||||
} else {
|
||||
const modalRefExport = this.modalService.open(ExportBatchSelectorComponent);
|
||||
modalRefExport.componentInstance.dsoRD = createSuccessfulRemoteDataObject(dso);
|
||||
}
|
||||
}));
|
||||
resp$.subscribe();
|
||||
return resp$;
|
||||
} else {
|
||||
return observableOf(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start export-metadata script of dso & navigate to process if successful
|
||||
* Otherwise show error message
|
||||
* @param dso Dso to export
|
||||
*/
|
||||
private startScriptNotifyAndRedirect(dso: DSpaceObject): Observable<boolean> {
|
||||
const parameterValues: ProcessParameter[] = [
|
||||
Object.assign(new ProcessParameter(), { name: '--id', value: dso.uuid }),
|
||||
Object.assign(new ProcessParameter(), { name: '--type', value: 'COLLECTION' })
|
||||
];
|
||||
return this.authorizationDataService.isAuthorized(FeatureID.AdministratorOf).pipe(
|
||||
switchMap(() => {
|
||||
return this.scriptDataService.invoke(BATCH_EXPORT_SCRIPT_NAME, parameterValues, []);
|
||||
}),
|
||||
getFirstCompletedRemoteData(),
|
||||
map((rd: RemoteData<Process>) => {
|
||||
if (rd.hasSucceeded) {
|
||||
const title = this.translationService.get('process.new.notification.success.title');
|
||||
const content = this.translationService.get('process.new.notification.success.content');
|
||||
this.notificationsService.success(title, content);
|
||||
if (isNotEmpty(rd.payload)) {
|
||||
this.router.navigateByUrl(getProcessDetailRoute(rd.payload.processId));
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
const title = this.translationService.get('process.new.notification.error.title');
|
||||
const content = this.translationService.get('process.new.notification.error.content');
|
||||
this.notificationsService.error(title, content);
|
||||
return false;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,77 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { Collection } from '../../../../core/shared/collection.model';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import { ImportBatchSelectorComponent } from './import-batch-selector.component';
|
||||
|
||||
describe('ImportBatchSelectorComponent', () => {
|
||||
let component: ImportBatchSelectorComponent;
|
||||
let fixture: ComponentFixture<ImportBatchSelectorComponent>;
|
||||
const mockItem = Object.assign(new Item(), {
|
||||
id: 'fake-id',
|
||||
uuid: 'fake-id',
|
||||
handle: 'fake/handle',
|
||||
lastModified: '2018'
|
||||
});
|
||||
const mockCollection: Collection = Object.assign(new Collection(), {
|
||||
id: 'test-collection-1-1',
|
||||
uuid: 'test-collection-1-1',
|
||||
name: 'test-collection-1',
|
||||
metadata: {
|
||||
'dc.identifier.uri': [
|
||||
{
|
||||
language: null,
|
||||
value: 'fake/test-collection-1'
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
const modalStub = jasmine.createSpyObj('modalStub', ['close']);
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||
declarations: [ImportBatchSelectorComponent],
|
||||
providers: [
|
||||
{ provide: NgbActiveModal, useValue: modalStub },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ImportBatchSelectorComponent);
|
||||
component = fixture.componentInstance;
|
||||
spyOn(component.response, 'emit');
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('if item is selected', () => {
|
||||
beforeEach((done) => {
|
||||
component.navigate(mockItem).subscribe(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('should emit null value', () => {
|
||||
expect(component.response.emit).toHaveBeenCalledWith(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('if collection is selected', () => {
|
||||
beforeEach((done) => {
|
||||
component.navigate(mockCollection).subscribe(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('should emit collection value', () => {
|
||||
expect(component.response.emit).toHaveBeenCalledWith(mockCollection);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@@ -0,0 +1,44 @@
|
||||
import { Component, EventEmitter, OnInit, Output } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Collection } from '../../../../core/shared/collection.model';
|
||||
import { DSpaceObjectType } from '../../../../core/shared/dspace-object-type.model';
|
||||
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { DSOSelectorModalWrapperComponent, SelectorActionType } from '../dso-selector-modal-wrapper.component';
|
||||
import { Observable, of } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Component to wrap a list of existing dso's inside a modal
|
||||
* Used to choose a dso from to import metadata of
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-import-batch-selector',
|
||||
templateUrl: '../dso-selector-modal-wrapper.component.html',
|
||||
})
|
||||
export class ImportBatchSelectorComponent extends DSOSelectorModalWrapperComponent implements OnInit {
|
||||
objectType = DSpaceObjectType.DSPACEOBJECT;
|
||||
selectorTypes = [DSpaceObjectType.COLLECTION];
|
||||
action = SelectorActionType.IMPORT_BATCH;
|
||||
/**
|
||||
* An event fired when the modal is closed
|
||||
*/
|
||||
@Output()
|
||||
response = new EventEmitter<DSpaceObject>();
|
||||
|
||||
constructor(protected activeModal: NgbActiveModal,
|
||||
protected route: ActivatedRoute) {
|
||||
super(activeModal, route);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the dso is a collection:
|
||||
*/
|
||||
navigate(dso: DSpaceObject): Observable<null> {
|
||||
if (dso instanceof Collection) {
|
||||
this.response.emit(dso);
|
||||
return of(null);
|
||||
}
|
||||
this.response.emit(null);
|
||||
return of(null);
|
||||
}
|
||||
}
|
@@ -22,7 +22,7 @@
|
||||
<div *ngIf="context?.index !== null
|
||||
&& (!showErrorMessages || errorMessages.length === 0)" class="clearfix w-100 mb-2"></div>
|
||||
|
||||
<div *ngIf="showErrorMessages" [id]="id + '_errors'"
|
||||
<div *ngIf="!model.hideErrorMessages && showErrorMessages" [id]="id + '_errors'"
|
||||
[ngClass]="[getClass('element', 'errors'), getClass('grid', 'errors')]">
|
||||
<small *ngFor="let message of errorMessages" class="invalid-feedback d-block">{{ message | translate: model.validators }}</small>
|
||||
</div>
|
||||
|
@@ -27,7 +27,7 @@ export interface DsDynamicInputModelConfig extends DynamicInputModelConfig {
|
||||
hasSelectableMetadata: boolean;
|
||||
metadataValue?: FormFieldMetadataValueObject;
|
||||
isModelOfInnerForm?: boolean;
|
||||
|
||||
hideErrorMessages?: boolean;
|
||||
}
|
||||
|
||||
export class DsDynamicInputModel extends DynamicInputModel {
|
||||
@@ -46,6 +46,7 @@ export class DsDynamicInputModel extends DynamicInputModel {
|
||||
@serializable() hasSelectableMetadata: boolean;
|
||||
@serializable() metadataValue: FormFieldMetadataValueObject;
|
||||
@serializable() isModelOfInnerForm: boolean;
|
||||
@serializable() hideErrorMessages?: boolean;
|
||||
|
||||
|
||||
constructor(config: DsDynamicInputModelConfig, layout?: DynamicFormControlLayout) {
|
||||
@@ -61,6 +62,7 @@ export class DsDynamicInputModel extends DynamicInputModel {
|
||||
this.metadataValue = config.metadataValue;
|
||||
this.place = config.place;
|
||||
this.isModelOfInnerForm = (hasValue(config.isModelOfInnerForm) ? config.isModelOfInnerForm : false);
|
||||
this.hideErrorMessages = config.hideErrorMessages;
|
||||
|
||||
this.language = config.language;
|
||||
if (!this.language) {
|
||||
|
@@ -325,6 +325,14 @@ describe('FormBuilderService test suite', () => {
|
||||
typeBindRelations: [{ match: 'VISIBLE', operator: 'OR', when: [{id: 'dc.type', value: 'Book' }]}]
|
||||
},
|
||||
),
|
||||
|
||||
new DynamicConcatModel({
|
||||
id: 'testConcatGroup_CONCAT_GROUP',
|
||||
group: [
|
||||
new DynamicInputModel({ id: 'testConcatGroup_CONCAT_FIRST_INPUT' }),
|
||||
new DynamicInputModel({ id: 'testConcatGroup_CONCAT_SECOND_INPUT' }),
|
||||
]
|
||||
} as any)
|
||||
];
|
||||
|
||||
testFormConfiguration = {
|
||||
@@ -463,6 +471,7 @@ describe('FormBuilderService test suite', () => {
|
||||
expect(service.findById('testTimePicker', testModel) instanceof DynamicTimePickerModel).toBe(true);
|
||||
expect(service.findById('testRating', testModel) instanceof DynamicRatingModel).toBe(true);
|
||||
expect(service.findById('testColorPicker', testModel) instanceof DynamicColorPickerModel).toBe(true);
|
||||
expect(service.findById('testConcatGroup', testModel) instanceof DynamicConcatModel).toBe(true);
|
||||
});
|
||||
|
||||
it('should find a nested dynamic form control model by id', () => {
|
||||
|
@@ -116,8 +116,8 @@ export class FormBuilderService extends DynamicFormService {
|
||||
}
|
||||
|
||||
if (this.isConcatGroup(controlModel)) {
|
||||
if (controlModel.id.match(new RegExp(findId + CONCAT_GROUP_SUFFIX + `_\\d+$`))) {
|
||||
result = (controlModel as DynamicConcatModel).group[0];
|
||||
if (controlModel.id.match(new RegExp(findId + CONCAT_GROUP_SUFFIX))) {
|
||||
result = (controlModel as DynamicConcatModel);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@@ -1,11 +1,7 @@
|
||||
import {Inject} from '@angular/core';
|
||||
import { FormFieldModel } from '../models/form-field.model';
|
||||
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
|
||||
import {
|
||||
DynamicFormControlLayout,
|
||||
DynamicInputModel,
|
||||
DynamicInputModelConfig
|
||||
} from '@ng-dynamic-forms/core';
|
||||
import { DynamicFormControlLayout, } from '@ng-dynamic-forms/core';
|
||||
import {
|
||||
CONCAT_FIRST_INPUT_SUFFIX,
|
||||
CONCAT_GROUP_SUFFIX,
|
||||
@@ -22,6 +18,7 @@ import {
|
||||
PARSER_OPTIONS,
|
||||
SUBMISSION_ID
|
||||
} from './field-parser';
|
||||
import { DsDynamicInputModel, DsDynamicInputModelConfig } from '../ds-dynamic-form-ui/models/ds-dynamic-input.model';
|
||||
|
||||
export class ConcatFieldParser extends FieldParser {
|
||||
|
||||
@@ -58,14 +55,14 @@ export class ConcatFieldParser extends FieldParser {
|
||||
concatGroup.group = [];
|
||||
concatGroup.separator = this.separator;
|
||||
|
||||
const input1ModelConfig: DynamicInputModelConfig = this.initModel(
|
||||
const input1ModelConfig: DsDynamicInputModelConfig = this.initModel(
|
||||
id + CONCAT_FIRST_INPUT_SUFFIX,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
);
|
||||
const input2ModelConfig: DynamicInputModelConfig = this.initModel(
|
||||
const input2ModelConfig: DsDynamicInputModelConfig = this.initModel(
|
||||
id + CONCAT_SECOND_INPUT_SUFFIX,
|
||||
false,
|
||||
true,
|
||||
@@ -73,6 +70,9 @@ export class ConcatFieldParser extends FieldParser {
|
||||
false
|
||||
);
|
||||
|
||||
input1ModelConfig.hideErrorMessages = true;
|
||||
input2ModelConfig.hideErrorMessages = true;
|
||||
|
||||
if (hasNoValue(concatGroup.hint) && hasValue(input1ModelConfig.hint) && hasNoValue(input2ModelConfig.hint)) {
|
||||
concatGroup.hint = input1ModelConfig.hint;
|
||||
input1ModelConfig.hint = undefined;
|
||||
@@ -98,8 +98,8 @@ export class ConcatFieldParser extends FieldParser {
|
||||
input2ModelConfig.placeholder = placeholder[1];
|
||||
}
|
||||
|
||||
const model1 = new DynamicInputModel(input1ModelConfig, clsInput);
|
||||
const model2 = new DynamicInputModel(input2ModelConfig, clsInput);
|
||||
const model1 = new DsDynamicInputModel(input1ModelConfig, clsInput);
|
||||
const model2 = new DsDynamicInputModel(input2ModelConfig, clsInput);
|
||||
concatGroup.group.push(model1);
|
||||
concatGroup.group.push(model2);
|
||||
|
||||
|
@@ -108,8 +108,15 @@ describe('FormService test suite', () => {
|
||||
const title: AbstractControl = new FormControl(undefined, Validators.required);
|
||||
const date: AbstractControl = new FormControl(undefined);
|
||||
const description: AbstractControl = new FormControl(undefined);
|
||||
formGroup = new FormGroup({ author, title, date, description });
|
||||
controls = { author, title, date, description };
|
||||
|
||||
const addressLocation: FormGroup = new FormGroup({
|
||||
zipCode: new FormControl(undefined),
|
||||
state: new FormControl(undefined),
|
||||
city: new FormControl(undefined),
|
||||
});
|
||||
|
||||
formGroup = new FormGroup({ author, title, date, description, addressLocation });
|
||||
controls = { author, title, date, description , addressLocation };
|
||||
service = new FormService(builderService, store);
|
||||
})
|
||||
)
|
||||
@@ -179,6 +186,32 @@ describe('FormService test suite', () => {
|
||||
expect(formGroup.controls.description.touched).toBe(true);
|
||||
});
|
||||
|
||||
it('should add errors to fields of concat group', () => {
|
||||
(builderService as any).isConcatGroup.and.returnValue(true);
|
||||
|
||||
let control = controls.addressLocation;
|
||||
let model = formModel.find((mdl: DynamicFormControlModel) => mdl.id === 'addressLocation');
|
||||
let errorKeys: string[];
|
||||
|
||||
service.addErrorToField(control, model, 'Test error message');
|
||||
|
||||
// the group itself should get an error
|
||||
errorKeys = Object.keys(control.errors);
|
||||
expect(errorKeys.length).toBe(1);
|
||||
expect(control.hasError(errorKeys[0])).toBe(true);
|
||||
|
||||
expect(control.touched).toBe(true);
|
||||
|
||||
// the group's inputs should get an error
|
||||
Object.values(control.controls).forEach((subControl: AbstractControl) => {
|
||||
errorKeys = Object.keys(subControl.errors);
|
||||
expect(errorKeys.length).toBe(1);
|
||||
expect(subControl.hasError(errorKeys[0])).toBe(true);
|
||||
expect(subControl.touched).toBe(true);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should remove error from field', () => {
|
||||
let control = controls.description;
|
||||
let model = formModel.find((mdl: DynamicFormControlModel) => mdl.id === 'description');
|
||||
@@ -209,6 +242,32 @@ describe('FormService test suite', () => {
|
||||
expect(formGroup.controls.description.touched).toBe(false);
|
||||
});
|
||||
|
||||
it('should remove errors from fields of concat group', () => {
|
||||
(builderService as any).isConcatGroup.and.returnValue(true);
|
||||
|
||||
let control = controls.addressLocation;
|
||||
let model = formModel.find((mdl: DynamicFormControlModel) => mdl.id === 'addressLocation');
|
||||
let errorKeys: string[];
|
||||
|
||||
service.addErrorToField(control, model, 'Test error message');
|
||||
errorKeys = Object.keys(control.errors);
|
||||
|
||||
service.removeErrorFromField(control, model, errorKeys[0]);
|
||||
|
||||
// the group itself should no longer have an error
|
||||
expect(errorKeys.length).toBe(1);
|
||||
expect(control.hasError(errorKeys[0])).toBe(false);
|
||||
expect(control.touched).toBe(false);
|
||||
|
||||
// the group's inputs should no longer have an error
|
||||
Object.values(control.controls).forEach((subControl: AbstractControl) => {
|
||||
errorKeys = Object.keys(subControl.errors);
|
||||
expect(errorKeys.length).toBe(1);
|
||||
expect(subControl.hasError(errorKeys[0])).toBe(false);
|
||||
expect(subControl.touched).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should reset form group', () => {
|
||||
const control = controls.author;
|
||||
|
||||
|
@@ -7,7 +7,7 @@ import { select, Store } from '@ngrx/store';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import { formObjectFromIdSelector } from './selectors';
|
||||
import { FormBuilderService } from './builder/form-builder.service';
|
||||
import { DynamicFormControlEvent, DynamicFormControlModel } from '@ng-dynamic-forms/core';
|
||||
import { DynamicFormControlEvent, DynamicFormControlModel, DynamicFormGroupModel } from '@ng-dynamic-forms/core';
|
||||
import { isEmpty, isNotUndefined } from '../empty.util';
|
||||
import { uniqueId } from 'lodash';
|
||||
import {
|
||||
@@ -161,6 +161,15 @@ export class FormService {
|
||||
field.setErrors(error);
|
||||
}
|
||||
|
||||
// if the field in question is a concat group, pass down the error to its fields
|
||||
if (field instanceof FormGroup && model instanceof DynamicFormGroupModel && this.formBuilderService.isConcatGroup(model)) {
|
||||
model.group.forEach((subModel) => {
|
||||
const subField = field.controls[subModel.id];
|
||||
|
||||
this.addErrorToField(subField, subModel, message);
|
||||
});
|
||||
}
|
||||
|
||||
field.markAsTouched();
|
||||
}
|
||||
|
||||
@@ -173,6 +182,15 @@ export class FormService {
|
||||
field.setErrors(error);
|
||||
}
|
||||
|
||||
// if the field in question is a concat group, clear the error from its fields
|
||||
if (field instanceof FormGroup && model instanceof DynamicFormGroupModel && this.formBuilderService.isConcatGroup(model)) {
|
||||
model.group.forEach((subModel) => {
|
||||
const subField = field.controls[subModel.id];
|
||||
|
||||
this.removeErrorFromField(subField, subModel, messageKey);
|
||||
});
|
||||
}
|
||||
|
||||
field.markAsUntouched();
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,6 @@
|
||||
<div class="g-recaptcha"
|
||||
[attr.data-callback]="'dataCallback'"
|
||||
[attr.data-expired-callback]="'expiredCallback'"
|
||||
[attr.data-error-callback]="'errorCallback'"
|
||||
[attr.data-sitekey]="(recaptchaKey$ | async)?.values[0]"
|
||||
[attr.data-size]="captchaMode === 'invisible' ? 'invisible' : null"></div>
|
@@ -0,0 +1,50 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { NativeWindowService } from '../../core/services/window.service';
|
||||
|
||||
import { ConfigurationDataService } from '../../core/data/configuration-data.service';
|
||||
import { NativeWindowMockFactory } from '../mocks/mock-native-window-ref';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
|
||||
import { GoogleRecaptchaComponent } from './google-recaptcha.component';
|
||||
|
||||
describe('GoogleRecaptchaComponent', () => {
|
||||
|
||||
let component: GoogleRecaptchaComponent;
|
||||
|
||||
let fixture: ComponentFixture<GoogleRecaptchaComponent>;
|
||||
|
||||
|
||||
const configurationDataService = jasmine.createSpyObj('configurationDataService', {
|
||||
findByPropertyName: jasmine.createSpy('findByPropertyName')
|
||||
});
|
||||
|
||||
const confResponse$ = createSuccessfulRemoteDataObject$({ values: ['valid-google-recaptcha-key'] });
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ GoogleRecaptchaComponent ],
|
||||
providers: [
|
||||
{ provide: ConfigurationDataService, useValue: configurationDataService },
|
||||
{ provide: NativeWindowService, useFactory: NativeWindowMockFactory },
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(GoogleRecaptchaComponent);
|
||||
component = fixture.componentInstance;
|
||||
configurationDataService.findByPropertyName.and.returnValues(confResponse$);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should rendered google recaptcha.',() => {
|
||||
const container = fixture.debugElement.query(By.css('.g-recaptcha'));
|
||||
expect(container).toBeTruthy();
|
||||
});
|
||||
});
|
@@ -0,0 +1,70 @@
|
||||
import { Component, EventEmitter, Inject, Input, OnInit, Output } from '@angular/core';
|
||||
|
||||
import { ConfigurationDataService } from '../../core/data/configuration-data.service';
|
||||
import { getFirstSucceededRemoteDataPayload } from '../../core/shared/operators';
|
||||
import { Observable } from 'rxjs';
|
||||
import { NativeWindowRef, NativeWindowService } from 'src/app/core/services/window.service';
|
||||
import { isNotEmpty } from '../empty.util';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-google-recaptcha',
|
||||
templateUrl: './google-recaptcha.component.html',
|
||||
styleUrls: ['./google-recaptcha.component.scss'],
|
||||
})
|
||||
export class GoogleRecaptchaComponent implements OnInit {
|
||||
|
||||
@Input() captchaMode: string;
|
||||
|
||||
/**
|
||||
* An EventEmitter that's fired whenever the form is being submitted
|
||||
*/
|
||||
@Output() executeRecaptcha: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
@Output() checkboxChecked: EventEmitter<boolean> = new EventEmitter();
|
||||
|
||||
@Output() showNotification: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
recaptchaKey$: Observable<any>;
|
||||
|
||||
constructor(
|
||||
@Inject(NativeWindowService) private _window: NativeWindowRef,
|
||||
private configService: ConfigurationDataService,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the google recaptcha site key
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.recaptchaKey$ = this.configService.findByPropertyName('google.recaptcha.key.site').pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
);
|
||||
this._window.nativeWindow.dataCallback = this.dataCallbackFcn;
|
||||
this._window.nativeWindow.expiredCallback = this.expiredCallbackFcn;
|
||||
this._window.nativeWindow.errorCallback = this.errorCallbackFcn;
|
||||
}
|
||||
|
||||
dataCallbackFcn = ($event) => {
|
||||
switch (this.captchaMode) {
|
||||
case 'invisible':
|
||||
this.executeRecaptcha.emit($event);
|
||||
break;
|
||||
case 'checkbox':
|
||||
this.checkboxChecked.emit(isNotEmpty($event));
|
||||
break;
|
||||
default:
|
||||
console.error(`Invalid reCaptcha mode '${this.captchaMode}`);
|
||||
this.showNotification.emit('error');
|
||||
}
|
||||
};
|
||||
|
||||
expiredCallbackFcn = () => {
|
||||
this.checkboxChecked.emit(false);
|
||||
this.showNotification.emit('expired');
|
||||
};
|
||||
|
||||
errorCallbackFcn = () => {
|
||||
this.showNotification.emit('error');
|
||||
};
|
||||
|
||||
}
|
@@ -19,6 +19,7 @@ export function getMockFormBuilderService(): FormBuilderService {
|
||||
isQualdropGroup: false,
|
||||
isModelInCustomGroup: true,
|
||||
isRelationGroup: true,
|
||||
isConcatGroup: false,
|
||||
hasArrayGroupValue: true,
|
||||
getTypeBindModel: new DsDynamicInputModel({
|
||||
name: 'dc.type',
|
||||
|
@@ -10,11 +10,11 @@ import { RequestService } from '../../../../core/data/request.service';
|
||||
import { Observable } from 'rxjs';
|
||||
import { RemoteData } from '../../../../core/data/remote-data';
|
||||
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
|
||||
import { switchMap, take } from 'rxjs/operators';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { CLAIMED_TASK } from '../../../../core/tasks/models/claimed-task-object.resource-type';
|
||||
import { getFirstSucceededRemoteDataPayload } from '../../../../core/shared/operators';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import { MyDSpaceReloadableActionsComponent } from '../../mydspace-reloadable-actions';
|
||||
import { isEmpty } from '../../../empty.util';
|
||||
|
||||
/**
|
||||
* Abstract component for rendering a claimed task's action
|
||||
@@ -36,6 +36,11 @@ export abstract class ClaimedTaskActionsAbstractComponent extends MyDSpaceReload
|
||||
|
||||
object: ClaimedTask;
|
||||
|
||||
/**
|
||||
* The item object that belonging to the ClaimedTask object
|
||||
*/
|
||||
item: Item;
|
||||
|
||||
/**
|
||||
* Anchor used to reload the pool task.
|
||||
*/
|
||||
@@ -43,6 +48,11 @@ export abstract class ClaimedTaskActionsAbstractComponent extends MyDSpaceReload
|
||||
|
||||
subs = [];
|
||||
|
||||
/**
|
||||
* The workflowitem object that belonging to the ClaimedTask object
|
||||
*/
|
||||
workflowitem: WorkflowItem;
|
||||
|
||||
protected constructor(protected injector: Injector,
|
||||
protected router: Router,
|
||||
protected notificationsService: NotificationsService,
|
||||
@@ -85,16 +95,10 @@ export abstract class ClaimedTaskActionsAbstractComponent extends MyDSpaceReload
|
||||
* Retrieve the itemUuid.
|
||||
*/
|
||||
initReloadAnchor() {
|
||||
if (!(this.object as any).workflowitem) {
|
||||
if (isEmpty(this.item)) {
|
||||
return;
|
||||
}
|
||||
this.subs.push(this.object.workflowitem.pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
switchMap((workflowItem: WorkflowItem) => workflowItem.item.pipe(getFirstSucceededRemoteDataPayload())
|
||||
))
|
||||
.subscribe((item: Item) => {
|
||||
this.itemUuid = item.uuid;
|
||||
}));
|
||||
this.itemUuid = this.item.uuid;
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
|
@@ -1,15 +1,22 @@
|
||||
<ng-container *ngVar="(actionRD$ | async)?.payload as workflowAction">
|
||||
<div class="mt-1 mb-3 space-children-mr">
|
||||
<ds-claimed-task-actions-loader *ngFor="let option of workflowAction?.options" [option]="option" [object]="object"
|
||||
<ds-claimed-task-actions-loader *ngFor="let option of workflowAction?.options"
|
||||
[item]="item"
|
||||
[option]="option"
|
||||
[object]="object"
|
||||
[workflowitem]="workflowitem"
|
||||
(processCompleted)="this.processCompleted.emit($event)">
|
||||
</ds-claimed-task-actions-loader>
|
||||
|
||||
<button class="btn btn-primary workflow-view" ngbTooltip="{{'submission.workflow.generic.view-help' | translate}}"
|
||||
[routerLink]="[getWorkflowItemViewRoute((workflowitem$ | async))]">
|
||||
[routerLink]="[getWorkflowItemViewRoute(workflowitem)]">
|
||||
<i class="fa fa-info-circle"></i> {{"submission.workflow.generic.view" | translate}}
|
||||
</button>
|
||||
|
||||
<ds-claimed-task-actions-loader [option]="returnToPoolOption" [object]="object"
|
||||
<ds-claimed-task-actions-loader [item]="item"
|
||||
[option]="returnToPoolOption"
|
||||
[object]="object"
|
||||
[workflowitem]="workflowitem"
|
||||
(processCompleted)="this.processCompleted.emit($event)">
|
||||
</ds-claimed-task-actions-loader>
|
||||
</div>
|
||||
|
@@ -3,7 +3,6 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { cold } from 'jasmine-marbles';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
import { TranslateLoaderMock } from '../../mocks/translate-loader.mock';
|
||||
@@ -123,7 +122,9 @@ describe('ClaimedTaskActionsComponent', () => {
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ClaimedTaskActionsComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.item = item;
|
||||
component.object = mockObject;
|
||||
component.workflowitem = workflowitem;
|
||||
notificationsServiceStub = TestBed.inject(NotificationsService as any);
|
||||
router = TestBed.inject(Router as any);
|
||||
fixture.detectChanges();
|
||||
@@ -133,11 +134,11 @@ describe('ClaimedTaskActionsComponent', () => {
|
||||
component.object = null;
|
||||
component.initObjects(mockObject);
|
||||
|
||||
expect(component.item).toEqual(item);
|
||||
|
||||
expect(component.object).toEqual(mockObject);
|
||||
|
||||
expect(component.workflowitem$).toBeObservable(cold('(b|)', {
|
||||
b: rdWorkflowitem.payload
|
||||
}));
|
||||
expect(component.workflowitem).toEqual(workflowitem);
|
||||
});
|
||||
|
||||
it('should reload page on process completed', waitForAsync(() => {
|
||||
|
@@ -2,12 +2,10 @@ import { Component, Injector, Input, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import { filter, map, take } from 'rxjs/operators';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { ClaimedTaskDataService } from '../../../core/tasks/claimed-task-data.service';
|
||||
import { ClaimedTask } from '../../../core/tasks/models/claimed-task-object.model';
|
||||
import { isNotUndefined } from '../../empty.util';
|
||||
import { WorkflowItem } from '../../../core/submission/models/workflowitem.model';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { MyDSpaceActionsComponent } from '../mydspace-actions';
|
||||
@@ -18,6 +16,7 @@ import { WorkflowAction } from '../../../core/tasks/models/workflow-action-objec
|
||||
import { WorkflowActionDataService } from '../../../core/data/workflow-action-data.service';
|
||||
import { WORKFLOW_TASK_OPTION_RETURN_TO_POOL } from './return-to-pool/claimed-task-actions-return-to-pool.component';
|
||||
import { getWorkflowItemViewRoute } from '../../../workflowitems-edit-page/workflowitems-edit-page-routing-paths';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
|
||||
/**
|
||||
* This component represents actions related to ClaimedTask object.
|
||||
@@ -34,10 +33,15 @@ export class ClaimedTaskActionsComponent extends MyDSpaceActionsComponent<Claime
|
||||
*/
|
||||
@Input() object: ClaimedTask;
|
||||
|
||||
/**
|
||||
* The item object that belonging to the ClaimedTask object
|
||||
*/
|
||||
@Input() item: Item;
|
||||
|
||||
/**
|
||||
* The workflowitem object that belonging to the ClaimedTask object
|
||||
*/
|
||||
public workflowitem$: Observable<WorkflowItem>;
|
||||
@Input() workflowitem: WorkflowItem;
|
||||
|
||||
/**
|
||||
* The workflow action available for this task
|
||||
@@ -87,11 +91,6 @@ export class ClaimedTaskActionsComponent extends MyDSpaceActionsComponent<Claime
|
||||
*/
|
||||
initObjects(object: ClaimedTask) {
|
||||
this.object = object;
|
||||
|
||||
this.workflowitem$ = (this.object.workflowitem as Observable<RemoteData<WorkflowItem>>).pipe(
|
||||
filter((rd: RemoteData<WorkflowItem>) => ((!rd.isRequestPending) && isNotUndefined(rd.payload))),
|
||||
map((rd: RemoteData<WorkflowItem>) => rd.payload),
|
||||
take(1));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { ClaimedTaskActionsLoaderComponent } from './claimed-task-actions-loader.component';
|
||||
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { ChangeDetectionStrategy, Injector, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ClaimedTaskActionsDirective } from './claimed-task-actions.directive';
|
||||
import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model';
|
||||
@@ -15,6 +15,8 @@ import { RequestService } from '../../../../core/data/request.service';
|
||||
import { PoolTaskDataService } from '../../../../core/tasks/pool-task-data.service';
|
||||
import { getMockSearchService } from '../../../mocks/search-service.mock';
|
||||
import { getMockRequestService } from '../../../mocks/request.service.mock';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
|
||||
|
||||
const searchService = getMockSearchService();
|
||||
|
||||
@@ -27,6 +29,37 @@ describe('ClaimedTaskActionsLoaderComponent', () => {
|
||||
const option = 'test_option';
|
||||
const object = Object.assign(new ClaimedTask(), { id: 'claimed-task-1' });
|
||||
|
||||
const item = Object.assign(new Item(), {
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
language: 'en_US',
|
||||
value: 'This is just another title'
|
||||
}
|
||||
],
|
||||
'dc.type': [
|
||||
{
|
||||
language: null,
|
||||
value: 'Article'
|
||||
}
|
||||
],
|
||||
'dc.contributor.author': [
|
||||
{
|
||||
language: 'en_US',
|
||||
value: 'Smith, Donald'
|
||||
}
|
||||
],
|
||||
'dc.date.issued': [
|
||||
{
|
||||
language: null,
|
||||
value: '2015-06-26'
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
const workflowitem = Object.assign(new WorkflowItem(), { id: '333' });
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot()],
|
||||
@@ -52,8 +85,10 @@ describe('ClaimedTaskActionsLoaderComponent', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
fixture = TestBed.createComponent(ClaimedTaskActionsLoaderComponent);
|
||||
comp = fixture.componentInstance;
|
||||
comp.item = item;
|
||||
comp.object = object;
|
||||
comp.option = option;
|
||||
comp.workflowitem = workflowitem;
|
||||
spyOn(comp, 'getComponentByWorkflowTaskOption').and.returnValue(ClaimedTaskActionsEditMetadataComponent);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
@@ -15,6 +15,8 @@ import { ClaimedTaskActionsAbstractComponent } from '../abstract/claimed-task-ac
|
||||
import { hasValue } from '../../../empty.util';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { MyDSpaceActionsResult } from '../../mydspace-actions';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-claimed-task-actions-loader',
|
||||
@@ -25,6 +27,11 @@ import { MyDSpaceActionsResult } from '../../mydspace-actions';
|
||||
* Passes on the ClaimedTask to the component and subscribes to the processCompleted output
|
||||
*/
|
||||
export class ClaimedTaskActionsLoaderComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* The item object that belonging to the ClaimedTask object
|
||||
*/
|
||||
@Input() item: Item;
|
||||
|
||||
/**
|
||||
* The ClaimedTask object
|
||||
*/
|
||||
@@ -36,6 +43,11 @@ export class ClaimedTaskActionsLoaderComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
@Input() option: string;
|
||||
|
||||
/**
|
||||
* The workflowitem object that belonging to the ClaimedTask object
|
||||
*/
|
||||
@Input() workflowitem: WorkflowItem;
|
||||
|
||||
/**
|
||||
* Emits the success or failure of a processed action
|
||||
*/
|
||||
@@ -69,7 +81,9 @@ export class ClaimedTaskActionsLoaderComponent implements OnInit, OnDestroy {
|
||||
|
||||
const componentRef = viewContainerRef.createComponent(componentFactory);
|
||||
const componentInstance = (componentRef.instance as ClaimedTaskActionsAbstractComponent);
|
||||
componentInstance.item = this.item;
|
||||
componentInstance.object = this.object;
|
||||
componentInstance.workflowitem = this.workflowitem;
|
||||
if (hasValue(componentInstance.processCompleted)) {
|
||||
this.subs.push(componentInstance.processCompleted.subscribe((result) => this.processCompleted.emit(result)));
|
||||
}
|
||||
|
@@ -12,10 +12,7 @@ import { RouterStub } from '../testing/router.stub';
|
||||
import { getMockSearchService } from '../mocks/search-service.mock';
|
||||
import { getMockRequestService } from '../mocks/request.service.mock';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import {
|
||||
createFailedRemoteDataObject,
|
||||
createSuccessfulRemoteDataObject
|
||||
} from '../remote-data.utils';
|
||||
import { createFailedRemoteDataObject, createSuccessfulRemoteDataObject } from '../remote-data.utils';
|
||||
import { WorkflowItem } from '../../core/submission/models/workflowitem.model';
|
||||
import { TranslateLoaderMock } from '../mocks/translate-loader.mock';
|
||||
import { NotificationsService } from '../notifications/notifications.service';
|
||||
@@ -103,7 +100,9 @@ describe('MyDSpaceReloadableActionsComponent', () => {
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(PoolTaskActionsComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.item = item;
|
||||
component.object = mockObject;
|
||||
component.workflowitem = workflowitem;
|
||||
notificationsServiceStub = TestBed.get(NotificationsService);
|
||||
router = TestBed.get(Router);
|
||||
fixture.detectChanges();
|
||||
|
@@ -8,6 +8,6 @@
|
||||
</button>
|
||||
<button class="btn btn-primary workflow-view ml-1 mt-1 mb-3" data-test="view-btn"
|
||||
ngbTooltip="{{'submission.workflow.generic.view-help' | translate}}"
|
||||
[routerLink]="[getWorkflowItemViewRoute((workflowitem$ | async))]">
|
||||
[routerLink]="[getWorkflowItemViewRoute(workflowitem)]">
|
||||
<i class="fa fa-info-circle"></i> {{"submission.workflow.generic.view" | translate}}
|
||||
</button>
|
||||
|
@@ -4,7 +4,6 @@ import { Router } from '@angular/router';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { cold } from 'jasmine-marbles';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
import { TranslateLoaderMock } from '../../mocks/translate-loader.mock';
|
||||
@@ -105,7 +104,9 @@ describe('PoolTaskActionsComponent', () => {
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(PoolTaskActionsComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.item = item;
|
||||
component.object = mockObject;
|
||||
component.workflowitem = workflowitem;
|
||||
notificationsServiceStub = TestBed.inject(NotificationsService as any);
|
||||
router = TestBed.inject(Router as any);
|
||||
fixture.detectChanges();
|
||||
@@ -120,11 +121,11 @@ describe('PoolTaskActionsComponent', () => {
|
||||
component.object = null;
|
||||
component.initObjects(mockObject);
|
||||
|
||||
expect(component.item).toEqual(item);
|
||||
|
||||
expect(component.object).toEqual(mockObject);
|
||||
|
||||
expect(component.workflowitem$).toBeObservable(cold('(b|)', {
|
||||
b: rdWorkflowitem.payload
|
||||
}));
|
||||
expect(component.workflowitem).toEqual(workflowitem);
|
||||
});
|
||||
|
||||
it('should display claim task button', () => {
|
||||
|
@@ -2,19 +2,17 @@ import { Component, Injector, Input, OnDestroy } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import { filter, map, switchMap, take } from 'rxjs/operators';
|
||||
import { switchMap, take } from 'rxjs/operators';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { WorkflowItem } from '../../../core/submission/models/workflowitem.model';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { PoolTask } from '../../../core/tasks/models/pool-task-object.model';
|
||||
import { PoolTaskDataService } from '../../../core/tasks/pool-task-data.service';
|
||||
import { isNotUndefined } from '../../empty.util';
|
||||
import { NotificationsService } from '../../notifications/notifications.service';
|
||||
import { RequestService } from '../../../core/data/request.service';
|
||||
import { SearchService } from '../../../core/shared/search/search.service';
|
||||
import { ClaimedTaskDataService } from '../../../core/tasks/claimed-task-data.service';
|
||||
import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||
import { MyDSpaceReloadableActionsComponent } from '../mydspace-reloadable-actions';
|
||||
@@ -36,10 +34,15 @@ export class PoolTaskActionsComponent extends MyDSpaceReloadableActionsComponent
|
||||
*/
|
||||
@Input() object: PoolTask;
|
||||
|
||||
/**
|
||||
* The item object that belonging to the PoolTask object
|
||||
*/
|
||||
@Input() item: Item;
|
||||
|
||||
/**
|
||||
* The workflowitem object that belonging to the PoolTask object
|
||||
*/
|
||||
public workflowitem$: Observable<WorkflowItem>;
|
||||
@Input() workflowitem: WorkflowItem;
|
||||
|
||||
/**
|
||||
* Anchor used to reload the pool task.
|
||||
@@ -83,10 +86,6 @@ export class PoolTaskActionsComponent extends MyDSpaceReloadableActionsComponent
|
||||
*/
|
||||
initObjects(object: PoolTask) {
|
||||
this.object = object;
|
||||
this.workflowitem$ = (this.object.workflowitem as Observable<RemoteData<WorkflowItem>>).pipe(
|
||||
filter((rd: RemoteData<WorkflowItem>) => ((!rd.isRequestPending) && isNotUndefined(rd.payload))),
|
||||
map((rd: RemoteData<WorkflowItem>) => rd.payload),
|
||||
take(1));
|
||||
}
|
||||
|
||||
actionExecution(): Observable<ProcessTaskResponse> {
|
||||
@@ -104,13 +103,7 @@ export class PoolTaskActionsComponent extends MyDSpaceReloadableActionsComponent
|
||||
* Retrieve the itemUuid.
|
||||
*/
|
||||
initReloadAnchor() {
|
||||
(this.object as any).workflowitem.pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
switchMap((workflowItem: WorkflowItem) => workflowItem.item.pipe(getFirstSucceededRemoteDataPayload())
|
||||
))
|
||||
.subscribe((item: Item) => {
|
||||
this.itemUuid = item.uuid;
|
||||
});
|
||||
this.itemUuid = this.item.uuid;
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
|
@@ -20,7 +20,7 @@
|
||||
[importable]="importable"
|
||||
[importConfig]="importConfig"
|
||||
(importObject)="importObject.emit($event)"
|
||||
(contentChange)="contentChange.emit()"
|
||||
(contentChange)="contentChange.emit($event)"
|
||||
(prev)="goPrev()"
|
||||
(next)="goNext()"
|
||||
*ngIf="(currentMode$ | async) === viewModeEnum.ListElement">
|
||||
@@ -50,6 +50,7 @@
|
||||
[context]="context"
|
||||
[hidePaginationDetail]="hidePaginationDetail"
|
||||
[showPaginator]="showPaginator"
|
||||
(contentChange)="contentChange.emit($event)"
|
||||
*ngIf="(currentMode$ | async) === viewModeEnum.DetailedListElement">
|
||||
</ds-object-detail>
|
||||
|
||||
|
@@ -59,6 +59,11 @@ export class ObjectCollectionComponent implements OnInit {
|
||||
@Input() hideGear = false;
|
||||
@Input() selectable = false;
|
||||
@Input() selectionConfig: {repeatable: boolean, listId: string};
|
||||
|
||||
/**
|
||||
* Emit custom event for listable object custom actions.
|
||||
*/
|
||||
@Output() customEvent = new EventEmitter<any>();
|
||||
@Output() deselectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();
|
||||
@Output() selectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();
|
||||
|
||||
|
@@ -5,7 +5,9 @@ import { ListableObject } from '../listable-object.model';
|
||||
import { GenericConstructor } from '../../../../core/shared/generic-constructor';
|
||||
import { Context } from '../../../../core/shared/context.model';
|
||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||
import { ItemListElementComponent } from '../../../object-list/item-list-element/item-types/item/item-list-element.component';
|
||||
import {
|
||||
ItemListElementComponent
|
||||
} from '../../../object-list/item-list-element/item-types/item/item-list-element.component';
|
||||
import { ListableObjectDirective } from './listable-object.directive';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { By } from '@angular/platform-browser';
|
||||
@@ -146,7 +148,7 @@ describe('ListableObjectComponentLoaderComponent', () => {
|
||||
expect((comp as any).instantiateComponent).not.toHaveBeenCalled();
|
||||
|
||||
(listableComponent as any).reloadedObject.emit(reloadedObject);
|
||||
tick();
|
||||
tick(200);
|
||||
|
||||
expect((comp as any).instantiateComponent).toHaveBeenCalledWith(reloadedObject);
|
||||
}));
|
||||
@@ -155,7 +157,7 @@ describe('ListableObjectComponentLoaderComponent', () => {
|
||||
expect((comp as any).contentChange.emit).not.toHaveBeenCalled();
|
||||
|
||||
(listableComponent as any).reloadedObject.emit(reloadedObject);
|
||||
tick();
|
||||
tick(200);
|
||||
|
||||
expect((comp as any).contentChange.emit).toHaveBeenCalledWith(reloadedObject);
|
||||
}));
|
||||
|
@@ -1,17 +1,21 @@
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ComponentFactoryResolver,
|
||||
ComponentRef,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
ViewChild,
|
||||
EventEmitter,
|
||||
SimpleChanges,
|
||||
OnChanges,
|
||||
ComponentRef
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
|
||||
import { Subscription } from 'rxjs';
|
||||
import { take } from 'rxjs/operators';
|
||||
|
||||
import { ListableObject } from '../listable-object.model';
|
||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||
import { Context } from '../../../../core/shared/context.model';
|
||||
@@ -20,9 +24,7 @@ import { GenericConstructor } from '../../../../core/shared/generic-constructor'
|
||||
import { ListableObjectDirective } from './listable-object.directive';
|
||||
import { CollectionElementLinkType } from '../../collection-element-link.type';
|
||||
import { hasValue, isNotEmpty } from '../../../empty.util';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { ThemeService } from '../../../theme-support/theme.service';
|
||||
|
||||
@Component({
|
||||
@@ -132,10 +134,7 @@ export class ListableObjectComponentLoaderComponent implements OnInit, OnChanges
|
||||
'contentChange',
|
||||
];
|
||||
|
||||
constructor(
|
||||
private componentFactoryResolver: ComponentFactoryResolver,
|
||||
private themeService: ThemeService
|
||||
) {
|
||||
constructor(private cdr: ChangeDetectorRef, private themeService: ThemeService) {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -166,27 +165,30 @@ export class ListableObjectComponentLoaderComponent implements OnInit, OnChanges
|
||||
|
||||
const component = this.getComponent(object.getRenderTypes(), this.viewMode, this.context);
|
||||
|
||||
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(component);
|
||||
|
||||
const viewContainerRef = this.listableObjectDirective.viewContainerRef;
|
||||
viewContainerRef.clear();
|
||||
|
||||
this.compRef = viewContainerRef.createComponent(
|
||||
componentFactory,
|
||||
0,
|
||||
undefined,
|
||||
[
|
||||
component, {
|
||||
index: 0,
|
||||
injector: undefined,
|
||||
projectableNodes: [
|
||||
[this.badges.nativeElement],
|
||||
]);
|
||||
]
|
||||
}
|
||||
);
|
||||
|
||||
this.connectInputsAndOutputs();
|
||||
|
||||
if ((this.compRef.instance as any).reloadedObject) {
|
||||
(this.compRef.instance as any).reloadedObject.pipe(take(1)).subscribe((reloadedObject: DSpaceObject) => {
|
||||
(this.compRef.instance as any).reloadedObject.pipe(
|
||||
take(1)
|
||||
).subscribe((reloadedObject: DSpaceObject) => {
|
||||
if (reloadedObject) {
|
||||
this.compRef.destroy();
|
||||
this.object = reloadedObject;
|
||||
this.instantiateComponent(reloadedObject);
|
||||
this.cdr.detectChanges();
|
||||
this.contentChange.emit(reloadedObject);
|
||||
}
|
||||
});
|
||||
|
@@ -1,3 +1,3 @@
|
||||
<div class="mt-2 mb-2">
|
||||
<div class="mt-2 mb-2" *ngIf="(submitter$ | async)">
|
||||
<span class="text-muted">{{'submission.workflow.tasks.generic.submitter' | translate}} : <span class="badge badge-info">{{(submitter$ | async)?.name}}</span></span>
|
||||
</div>
|
||||
|
@@ -11,12 +11,11 @@ import { EPersonMock } from '../../../testing/eperson.mock';
|
||||
import { TranslateLoaderMock } from '../../../mocks/translate-loader.mock';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils';
|
||||
import { LinkService } from '../../../../core/cache/builders/link.service';
|
||||
import { getMockLinkService } from '../../../mocks/link-service.mock';
|
||||
|
||||
let component: ItemSubmitterComponent;
|
||||
let fixture: ComponentFixture<ItemSubmitterComponent>;
|
||||
|
||||
const compIndex = 1;
|
||||
|
||||
let mockResultObject: PoolTask;
|
||||
|
||||
const rdSumbitter = createSuccessfulRemoteDataObject(EPersonMock);
|
||||
@@ -36,6 +35,9 @@ describe('ItemSubmitterComponent', () => {
|
||||
})
|
||||
],
|
||||
declarations: [ItemSubmitterComponent],
|
||||
providers: [
|
||||
{ provide: LinkService, useValue: getMockLinkService() },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(ItemSubmitterComponent, {
|
||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||
|
@@ -1,12 +1,15 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import { filter, find, map, mergeMap } from 'rxjs/operators';
|
||||
import { EMPTY, Observable } from 'rxjs';
|
||||
import { map, mergeMap } from 'rxjs/operators';
|
||||
|
||||
import { EPerson } from '../../../../core/eperson/models/eperson.model';
|
||||
import { RemoteData } from '../../../../core/data/remote-data';
|
||||
import { isNotEmpty, isNotUndefined } from '../../../empty.util';
|
||||
import { isNotEmpty } from '../../../empty.util';
|
||||
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
|
||||
import { getFirstCompletedRemoteData } from '../../../../core/shared/operators';
|
||||
import { LinkService } from '../../../../core/cache/builders/link.service';
|
||||
import { followLink } from '../../../utils/follow-link-config.model';
|
||||
|
||||
/**
|
||||
* This component represents a badge with submitter information.
|
||||
@@ -24,18 +27,38 @@ export class ItemSubmitterComponent implements OnInit {
|
||||
@Input() object: any;
|
||||
|
||||
/**
|
||||
* The Eperson object
|
||||
* The submitter object
|
||||
*/
|
||||
submitter$: Observable<EPerson>;
|
||||
|
||||
public constructor(protected linkService: LinkService) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize submitter object
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.linkService.resolveLinks(this.object, followLink('workflowitem', {},
|
||||
followLink('submitter',{})
|
||||
));
|
||||
this.submitter$ = (this.object.workflowitem as Observable<RemoteData<WorkflowItem>>).pipe(
|
||||
filter((rd: RemoteData<WorkflowItem>) => (rd.hasSucceeded && isNotUndefined(rd.payload))),
|
||||
mergeMap((rd: RemoteData<WorkflowItem>) => rd.payload.submitter as Observable<RemoteData<EPerson>>),
|
||||
find((rd: RemoteData<EPerson>) => rd.hasSucceeded && isNotEmpty(rd.payload)),
|
||||
map((rd: RemoteData<EPerson>) => rd.payload));
|
||||
getFirstCompletedRemoteData(),
|
||||
mergeMap((rd: RemoteData<WorkflowItem>) => {
|
||||
if (rd.hasSucceeded && isNotEmpty(rd.payload)) {
|
||||
return (rd.payload.submitter as Observable<RemoteData<EPerson>>).pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
map((rds: RemoteData<EPerson>) => {
|
||||
if (rds.hasSucceeded && isNotEmpty(rds.payload)) {
|
||||
return rds.payload;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
);
|
||||
} else {
|
||||
return EMPTY;
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
@@ -1,10 +1,12 @@
|
||||
<ng-container *ngVar="(workflowitemRD$ | async)?.payload as workflowitem">
|
||||
<ds-item-detail-preview *ngIf="workflowitem"
|
||||
[item]="(workflowitem.item | async)?.payload"
|
||||
<ng-container *ngIf="(workflowitem$ | async) && (item$ | async)">
|
||||
<ds-item-detail-preview [item]="item$?.value"
|
||||
[object]="object"
|
||||
[showSubmitter]="showSubmitter"
|
||||
[status]="status">
|
||||
</ds-item-detail-preview>
|
||||
|
||||
<ds-claimed-task-actions *ngIf="workflowitem" [object]="dso" (processCompleted)="reloadedObject.emit($event.reloadedObject)"></ds-claimed-task-actions>
|
||||
<ds-claimed-task-actions [item]="item$.value"
|
||||
[object]="dso"
|
||||
[workflowitem]="workflowitem$.value"
|
||||
(processCompleted)="reloadedObject.emit($event.reloadedObject)"></ds-claimed-task-actions>
|
||||
</ng-container>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, TestBed, tick, waitForAsync, fakeAsync} from '@angular/core/testing';
|
||||
import { ComponentFixture, fakeAsync, flush, TestBed, tick, waitForAsync } from '@angular/core/testing';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
|
||||
import { of as observableOf } from 'rxjs';
|
||||
@@ -7,7 +7,9 @@ import { of as observableOf } from 'rxjs';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import { ClaimedTaskSearchResultDetailElementComponent } from './claimed-task-search-result-detail-element.component';
|
||||
import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model';
|
||||
import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
|
||||
import {
|
||||
MyDspaceItemStatusType
|
||||
} from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
|
||||
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
|
||||
import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils';
|
||||
import { ClaimedTaskSearchResult } from '../../../object-collection/shared/claimed-task-search-result.model';
|
||||
@@ -15,6 +17,7 @@ import { VarDirective } from '../../../utils/var.directive';
|
||||
import { LinkService } from '../../../../core/cache/builders/link.service';
|
||||
import { getMockLinkService } from '../../../mocks/link-service.mock';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { ObjectCacheService } from '../../../../core/cache/object-cache.service';
|
||||
|
||||
let component: ClaimedTaskSearchResultDetailElementComponent;
|
||||
let fixture: ComponentFixture<ClaimedTaskSearchResultDetailElementComponent>;
|
||||
@@ -58,6 +61,9 @@ const workflowitem = Object.assign(new WorkflowItem(), { item: observableOf(rdIt
|
||||
const rdWorkflowitem = createSuccessfulRemoteDataObject(workflowitem);
|
||||
mockResultObject.indexableObject = Object.assign(new ClaimedTask(), { workflowitem: observableOf(rdWorkflowitem) });
|
||||
const linkService = getMockLinkService();
|
||||
const objectCacheServiceMock = jasmine.createSpyObj('ObjectCacheService', {
|
||||
remove: jasmine.createSpy('remove')
|
||||
});
|
||||
|
||||
describe('ClaimedTaskSearchResultDetailElementComponent', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
@@ -65,7 +71,8 @@ describe('ClaimedTaskSearchResultDetailElementComponent', () => {
|
||||
imports: [NoopAnimationsModule],
|
||||
declarations: [ClaimedTaskSearchResultDetailElementComponent, VarDirective],
|
||||
providers: [
|
||||
{ provide: LinkService, useValue: linkService }
|
||||
{ provide: LinkService, useValue: linkService },
|
||||
{ provide: ObjectCacheService, useValue: objectCacheServiceMock }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(ClaimedTaskSearchResultDetailElementComponent, {
|
||||
@@ -83,18 +90,16 @@ describe('ClaimedTaskSearchResultDetailElementComponent', () => {
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should init workflowitem properly', (done) => {
|
||||
component.workflowitemRD$.subscribe((workflowitemRD) => {
|
||||
// Make sure the necessary links are being resolved
|
||||
it('should init workflowitem properly', fakeAsync(() => {
|
||||
flush();
|
||||
expect(linkService.resolveLinks).toHaveBeenCalledWith(
|
||||
component.dso,
|
||||
jasmine.objectContaining({ name: 'workflowitem' }),
|
||||
jasmine.objectContaining({ name: 'action' })
|
||||
);
|
||||
expect(workflowitemRD.payload).toEqual(workflowitem);
|
||||
done();
|
||||
});
|
||||
});
|
||||
expect(component.workflowitem$.value).toEqual(workflowitem);
|
||||
expect(component.item$.value).toEqual(item);
|
||||
}));
|
||||
|
||||
it('should have properly status', () => {
|
||||
expect(component.status).toEqual(MyDspaceItemStatusType.VALIDATION);
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user