mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge branch 'main' into CST-7216
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -39,3 +39,5 @@ package-lock.json
|
|||||||
/nbproject/
|
/nbproject/
|
||||||
|
|
||||||
junit.xml
|
junit.xml
|
||||||
|
|
||||||
|
/src/mirador-viewer/config.local.js
|
||||||
|
12
server.ts
12
server.ts
@@ -26,7 +26,6 @@ import * as ejs from 'ejs';
|
|||||||
import * as compression from 'compression';
|
import * as compression from 'compression';
|
||||||
import * as expressStaticGzip from 'express-static-gzip';
|
import * as expressStaticGzip from 'express-static-gzip';
|
||||||
/* eslint-enable import/no-namespace */
|
/* eslint-enable import/no-namespace */
|
||||||
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import LRU from 'lru-cache';
|
import LRU from 'lru-cache';
|
||||||
import isbot from 'isbot';
|
import isbot from 'isbot';
|
||||||
@@ -34,7 +33,7 @@ import { createCertificate } from 'pem';
|
|||||||
import { createServer } from 'https';
|
import { createServer } from 'https';
|
||||||
import { json } from 'body-parser';
|
import { json } from 'body-parser';
|
||||||
|
|
||||||
import { existsSync, readFileSync } from 'fs';
|
import { readFileSync } from 'fs';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
|
||||||
import { enableProdMode } from '@angular/core';
|
import { enableProdMode } from '@angular/core';
|
||||||
@@ -180,6 +179,15 @@ export function app() {
|
|||||||
changeOrigin: true
|
changeOrigin: true
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proxy the linksets
|
||||||
|
*/
|
||||||
|
router.use('/signposting**', createProxyMiddleware({
|
||||||
|
target: `${environment.rest.baseUrl}`,
|
||||||
|
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
|
||||||
|
changeOrigin: true
|
||||||
|
}));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the rateLimiter property is present
|
* Checks if the rateLimiter property is present
|
||||||
* When it is present, the rateLimiter will be enabled. When it is undefined, the rateLimiter will be disabled.
|
* When it is present, the rateLimiter will be enabled. When it is undefined, the rateLimiter will be disabled.
|
||||||
|
@@ -287,14 +287,17 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* This method will set everything to stale, which will cause the lists on this page to update.
|
* This method will set everything to stale, which will cause the lists on this page to update.
|
||||||
*/
|
*/
|
||||||
reset() {
|
reset(): void {
|
||||||
this.epersonService.getBrowseEndpoint().pipe(
|
this.epersonService.getBrowseEndpoint().pipe(
|
||||||
take(1)
|
take(1),
|
||||||
).subscribe((href: string) => {
|
switchMap((href: string) => {
|
||||||
this.requestService.setStaleByHrefSubstring(href).pipe(take(1)).subscribe(() => {
|
return this.requestService.setStaleByHrefSubstring(href).pipe(
|
||||||
this.epersonService.cancelEditEPerson();
|
take(1),
|
||||||
this.isEPersonFormShown = false;
|
);
|
||||||
});
|
})
|
||||||
|
).subscribe(()=>{
|
||||||
|
this.epersonService.cancelEditEPerson();
|
||||||
|
this.isEPersonFormShown = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -8,7 +8,7 @@ import {
|
|||||||
} from '@ng-dynamic-forms/core';
|
} from '@ng-dynamic-forms/core';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs';
|
import { combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs';
|
||||||
import { debounceTime, switchMap, take } from 'rxjs/operators';
|
import { debounceTime, finalize, map, switchMap, take } from 'rxjs/operators';
|
||||||
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
|
import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
|
||||||
@@ -463,31 +463,42 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
* Deletes the EPerson from the Repository. The EPerson will be the only that this form is showing.
|
* Deletes the EPerson from the Repository. The EPerson will be the only that this form is showing.
|
||||||
* It'll either show a success or error message depending on whether the delete was successful or not.
|
* It'll either show a success or error message depending on whether the delete was successful or not.
|
||||||
*/
|
*/
|
||||||
delete() {
|
delete(): void {
|
||||||
this.epersonService.getActiveEPerson().pipe(take(1)).subscribe((eperson: EPerson) => {
|
this.epersonService.getActiveEPerson().pipe(
|
||||||
const modalRef = this.modalService.open(ConfirmationModalComponent);
|
take(1),
|
||||||
modalRef.componentInstance.dso = eperson;
|
switchMap((eperson: EPerson) => {
|
||||||
modalRef.componentInstance.headerLabel = 'confirmation-modal.delete-eperson.header';
|
const modalRef = this.modalService.open(ConfirmationModalComponent);
|
||||||
modalRef.componentInstance.infoLabel = 'confirmation-modal.delete-eperson.info';
|
modalRef.componentInstance.dso = eperson;
|
||||||
modalRef.componentInstance.cancelLabel = 'confirmation-modal.delete-eperson.cancel';
|
modalRef.componentInstance.headerLabel = 'confirmation-modal.delete-eperson.header';
|
||||||
modalRef.componentInstance.confirmLabel = 'confirmation-modal.delete-eperson.confirm';
|
modalRef.componentInstance.infoLabel = 'confirmation-modal.delete-eperson.info';
|
||||||
modalRef.componentInstance.brandColor = 'danger';
|
modalRef.componentInstance.cancelLabel = 'confirmation-modal.delete-eperson.cancel';
|
||||||
modalRef.componentInstance.confirmIcon = 'fas fa-trash';
|
modalRef.componentInstance.confirmLabel = 'confirmation-modal.delete-eperson.confirm';
|
||||||
modalRef.componentInstance.response.pipe(take(1)).subscribe((confirm: boolean) => {
|
modalRef.componentInstance.brandColor = 'danger';
|
||||||
if (confirm) {
|
modalRef.componentInstance.confirmIcon = 'fas fa-trash';
|
||||||
if (hasValue(eperson.id)) {
|
|
||||||
this.epersonService.deleteEPerson(eperson).pipe(getFirstCompletedRemoteData()).subscribe((restResponse: RemoteData<NoContent>) => {
|
return modalRef.componentInstance.response.pipe(
|
||||||
if (restResponse.hasSucceeded) {
|
take(1),
|
||||||
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { name: this.dsoNameService.getName(eperson) }));
|
switchMap((confirm: boolean) => {
|
||||||
this.submitForm.emit();
|
if (confirm && hasValue(eperson.id)) {
|
||||||
} else {
|
this.canDelete$ = observableOf(false);
|
||||||
this.notificationsService.error('Error occured when trying to delete EPerson with id: ' + eperson.id + ' with code: ' + restResponse.statusCode + ' and message: ' + restResponse.errorMessage);
|
return this.epersonService.deleteEPerson(eperson).pipe(
|
||||||
}
|
getFirstCompletedRemoteData(),
|
||||||
this.cancelForm.emit();
|
map((restResponse: RemoteData<NoContent>) => ({ restResponse, eperson }))
|
||||||
});
|
);
|
||||||
}
|
} else {
|
||||||
}
|
return observableOf(null);
|
||||||
});
|
}
|
||||||
|
}),
|
||||||
|
finalize(() => this.canDelete$ = observableOf(true))
|
||||||
|
);
|
||||||
|
})
|
||||||
|
).subscribe(({ restResponse, eperson }: { restResponse: RemoteData<NoContent> | null, eperson: EPerson }) => {
|
||||||
|
if (restResponse?.hasSucceeded) {
|
||||||
|
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { name: this.dsoNameService.getName(eperson) }));
|
||||||
|
} else {
|
||||||
|
this.notificationsService.error(`Error occurred when trying to delete EPerson with id: ${eperson?.id} with code: ${restResponse?.statusCode} and message: ${restResponse?.errorMessage}`);
|
||||||
|
}
|
||||||
|
this.cancelForm.emit();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -523,7 +534,6 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
* Cancel the current edit when component is destroyed & unsub all subscriptions
|
* Cancel the current edit when component is destroyed & unsub all subscriptions
|
||||||
*/
|
*/
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.onCancel();
|
|
||||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||||
this.paginationService.clearPagination(this.config.id);
|
this.paginationService.clearPagination(this.config.id);
|
||||||
if (hasValue(this.emailValueChangeSubscribe)) {
|
if (hasValue(this.emailValueChangeSubscribe)) {
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing';
|
import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
|
||||||
import { MetadataSchemaFormComponent } from './metadata-schema-form.component';
|
import { MetadataSchemaFormComponent } from './metadata-schema-form.component';
|
||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
@@ -29,14 +28,16 @@ describe('MetadataSchemaFormComponent', () => {
|
|||||||
createFormGroup: () => {
|
createFormGroup: () => {
|
||||||
return {
|
return {
|
||||||
patchValue: () => {
|
patchValue: () => {
|
||||||
}
|
},
|
||||||
|
reset(_value?: any, _options?: { onlySelf?: boolean; emitEvent?: boolean; }): void {
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
|
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
return TestBed.configureTestingModule({
|
||||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
||||||
declarations: [MetadataSchemaFormComponent, EnumKeysPipe],
|
declarations: [MetadataSchemaFormComponent, EnumKeysPipe],
|
||||||
providers: [
|
providers: [
|
||||||
@@ -64,7 +65,7 @@ describe('MetadataSchemaFormComponent', () => {
|
|||||||
const expected = Object.assign(new MetadataSchema(), {
|
const expected = Object.assign(new MetadataSchema(), {
|
||||||
namespace: namespace,
|
namespace: namespace,
|
||||||
prefix: prefix
|
prefix: prefix
|
||||||
});
|
} as MetadataSchema);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(component.submitForm, 'emit');
|
spyOn(component.submitForm, 'emit');
|
||||||
@@ -79,11 +80,10 @@ describe('MetadataSchemaFormComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should emit a new schema using the correct values', waitForAsync(() => {
|
it('should emit a new schema using the correct values', async () => {
|
||||||
fixture.whenStable().then(() => {
|
await fixture.whenStable();
|
||||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expected);
|
expect(component.submitForm.emit).toHaveBeenCalledWith(expected);
|
||||||
});
|
});
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('with an active schema', () => {
|
describe('with an active schema', () => {
|
||||||
@@ -91,7 +91,7 @@ describe('MetadataSchemaFormComponent', () => {
|
|||||||
id: 1,
|
id: 1,
|
||||||
namespace: namespace,
|
namespace: namespace,
|
||||||
prefix: prefix
|
prefix: prefix
|
||||||
});
|
} as MetadataSchema);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(expectedWithId));
|
spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(expectedWithId));
|
||||||
@@ -99,11 +99,10 @@ describe('MetadataSchemaFormComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should edit the existing schema using the correct values', waitForAsync(() => {
|
it('should edit the existing schema using the correct values', async () => {
|
||||||
fixture.whenStable().then(() => {
|
await fixture.whenStable();
|
||||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expectedWithId);
|
expect(component.submitForm.emit).toHaveBeenCalledWith(expectedWithId);
|
||||||
});
|
});
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -77,19 +77,24 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
combineLatest(
|
combineLatest([
|
||||||
this.translateService.get(`${this.messagePrefix}.name`),
|
this.translateService.get(`${this.messagePrefix}.name`),
|
||||||
this.translateService.get(`${this.messagePrefix}.namespace`)
|
this.translateService.get(`${this.messagePrefix}.namespace`)
|
||||||
).subscribe(([name, namespace]) => {
|
]).subscribe(([name, namespace]) => {
|
||||||
this.name = new DynamicInputModel({
|
this.name = new DynamicInputModel({
|
||||||
id: 'name',
|
id: 'name',
|
||||||
label: name,
|
label: name,
|
||||||
name: 'name',
|
name: 'name',
|
||||||
validators: {
|
validators: {
|
||||||
required: null,
|
required: null,
|
||||||
pattern: '^[^ ,_]{1,32}$'
|
pattern: '^[^. ,]*$',
|
||||||
|
maxLength: 32,
|
||||||
},
|
},
|
||||||
required: true,
|
required: true,
|
||||||
|
errorMessages: {
|
||||||
|
pattern: 'error.validation.metadata.name.invalid-pattern',
|
||||||
|
maxLength: 'error.validation.metadata.name.max-length',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
this.namespace = new DynamicInputModel({
|
this.namespace = new DynamicInputModel({
|
||||||
id: 'namespace',
|
id: 'namespace',
|
||||||
@@ -97,8 +102,12 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
|||||||
name: 'namespace',
|
name: 'namespace',
|
||||||
validators: {
|
validators: {
|
||||||
required: null,
|
required: null,
|
||||||
|
maxLength: 256,
|
||||||
},
|
},
|
||||||
required: true,
|
required: true,
|
||||||
|
errorMessages: {
|
||||||
|
maxLength: 'error.validation.metadata.namespace.max-length',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
this.formModel = [
|
this.formModel = [
|
||||||
new DynamicFormGroupModel(
|
new DynamicFormGroupModel(
|
||||||
@@ -108,13 +117,18 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
|||||||
})
|
})
|
||||||
];
|
];
|
||||||
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
||||||
this.registryService.getActiveMetadataSchema().subscribe((schema) => {
|
this.registryService.getActiveMetadataSchema().subscribe((schema: MetadataSchema) => {
|
||||||
this.formGroup.patchValue({
|
if (schema == null) {
|
||||||
metadatadataschemagroup:{
|
this.clearFields();
|
||||||
name: schema != null ? schema.prefix : '',
|
} else {
|
||||||
namespace: schema != null ? schema.namespace : ''
|
this.formGroup.patchValue({
|
||||||
}
|
metadatadataschemagroup: {
|
||||||
});
|
name: schema.prefix,
|
||||||
|
namespace: schema.namespace,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.name.disabled = true;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -132,10 +146,10 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
|||||||
* When the schema has no id attached -> Create new schema
|
* When the schema has no id attached -> Create new schema
|
||||||
* Emit the updated/created schema using the EventEmitter submitForm
|
* Emit the updated/created schema using the EventEmitter submitForm
|
||||||
*/
|
*/
|
||||||
onSubmit() {
|
onSubmit(): void {
|
||||||
this.registryService.clearMetadataSchemaRequests().subscribe();
|
this.registryService.clearMetadataSchemaRequests().subscribe();
|
||||||
this.registryService.getActiveMetadataSchema().pipe(take(1)).subscribe(
|
this.registryService.getActiveMetadataSchema().pipe(take(1)).subscribe(
|
||||||
(schema) => {
|
(schema: MetadataSchema) => {
|
||||||
const values = {
|
const values = {
|
||||||
prefix: this.name.value,
|
prefix: this.name.value,
|
||||||
namespace: this.namespace.value
|
namespace: this.namespace.value
|
||||||
@@ -147,9 +161,9 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
|||||||
} else {
|
} else {
|
||||||
this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), schema, {
|
this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), schema, {
|
||||||
id: schema.id,
|
id: schema.id,
|
||||||
prefix: (values.prefix ? values.prefix : schema.prefix),
|
prefix: schema.prefix,
|
||||||
namespace: (values.namespace ? values.namespace : schema.namespace)
|
namespace: values.namespace,
|
||||||
})).subscribe((updatedSchema) => {
|
})).subscribe((updatedSchema: MetadataSchema) => {
|
||||||
this.submitForm.emit(updatedSchema);
|
this.submitForm.emit(updatedSchema);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -162,13 +176,9 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* Reset all input-fields to be empty
|
* Reset all input-fields to be empty
|
||||||
*/
|
*/
|
||||||
clearFields() {
|
clearFields(): void {
|
||||||
this.formGroup.patchValue({
|
this.formGroup.reset('metadatadataschemagroup');
|
||||||
metadatadataschemagroup:{
|
this.name.disabled = false;
|
||||||
prefix: '',
|
|
||||||
namespace: ''
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -39,14 +39,16 @@ describe('MetadataFieldFormComponent', () => {
|
|||||||
createFormGroup: () => {
|
createFormGroup: () => {
|
||||||
return {
|
return {
|
||||||
patchValue: () => {
|
patchValue: () => {
|
||||||
}
|
},
|
||||||
|
reset(_value?: any, _options?: { onlySelf?: boolean; emitEvent?: boolean; }): void {
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
|
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
return TestBed.configureTestingModule({
|
||||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
||||||
declarations: [MetadataFieldFormComponent, EnumKeysPipe],
|
declarations: [MetadataFieldFormComponent, EnumKeysPipe],
|
||||||
providers: [
|
providers: [
|
||||||
@@ -98,11 +100,10 @@ describe('MetadataFieldFormComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should emit a new field using the correct values', waitForAsync(() => {
|
it('should emit a new field using the correct values', async () => {
|
||||||
fixture.whenStable().then(() => {
|
await fixture.whenStable();
|
||||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expected);
|
expect(component.submitForm.emit).toHaveBeenCalledWith(expected);
|
||||||
});
|
});
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('with an active field', () => {
|
describe('with an active field', () => {
|
||||||
@@ -120,11 +121,10 @@ describe('MetadataFieldFormComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should edit the existing field using the correct values', waitForAsync(() => {
|
it('should edit the existing field using the correct values', async () => {
|
||||||
fixture.whenStable().then(() => {
|
await fixture.whenStable();
|
||||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expectedWithId);
|
expect(component.submitForm.emit).toHaveBeenCalledWith(expectedWithId);
|
||||||
});
|
});
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -98,25 +98,39 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
|
|||||||
* Initialize the component, setting up the necessary Models for the dynamic form
|
* Initialize the component, setting up the necessary Models for the dynamic form
|
||||||
*/
|
*/
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
combineLatest(
|
combineLatest([
|
||||||
this.translateService.get(`${this.messagePrefix}.element`),
|
this.translateService.get(`${this.messagePrefix}.element`),
|
||||||
this.translateService.get(`${this.messagePrefix}.qualifier`),
|
this.translateService.get(`${this.messagePrefix}.qualifier`),
|
||||||
this.translateService.get(`${this.messagePrefix}.scopenote`)
|
this.translateService.get(`${this.messagePrefix}.scopenote`)
|
||||||
).subscribe(([element, qualifier, scopenote]) => {
|
]).subscribe(([element, qualifier, scopenote]) => {
|
||||||
this.element = new DynamicInputModel({
|
this.element = new DynamicInputModel({
|
||||||
id: 'element',
|
id: 'element',
|
||||||
label: element,
|
label: element,
|
||||||
name: 'element',
|
name: 'element',
|
||||||
validators: {
|
validators: {
|
||||||
required: null,
|
required: null,
|
||||||
|
pattern: '^[^. ,]*$',
|
||||||
|
maxLength: 64,
|
||||||
},
|
},
|
||||||
required: true,
|
required: true,
|
||||||
|
errorMessages: {
|
||||||
|
pattern: 'error.validation.metadata.element.invalid-pattern',
|
||||||
|
maxLength: 'error.validation.metadata.element.max-length',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
this.qualifier = new DynamicInputModel({
|
this.qualifier = new DynamicInputModel({
|
||||||
id: 'qualifier',
|
id: 'qualifier',
|
||||||
label: qualifier,
|
label: qualifier,
|
||||||
name: 'qualifier',
|
name: 'qualifier',
|
||||||
|
validators: {
|
||||||
|
pattern: '^[^. ,]*$',
|
||||||
|
maxLength: 64,
|
||||||
|
},
|
||||||
required: false,
|
required: false,
|
||||||
|
errorMessages: {
|
||||||
|
pattern: 'error.validation.metadata.qualifier.invalid-pattern',
|
||||||
|
maxLength: 'error.validation.metadata.qualifier.max-length',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
this.scopeNote = new DynamicInputModel({
|
this.scopeNote = new DynamicInputModel({
|
||||||
id: 'scopeNote',
|
id: 'scopeNote',
|
||||||
@@ -132,14 +146,20 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
|
|||||||
})
|
})
|
||||||
];
|
];
|
||||||
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
||||||
this.registryService.getActiveMetadataField().subscribe((field) => {
|
this.registryService.getActiveMetadataField().subscribe((field: MetadataField): void => {
|
||||||
this.formGroup.patchValue({
|
if (field == null) {
|
||||||
metadatadatafieldgroup: {
|
this.clearFields();
|
||||||
element: field != null ? field.element : '',
|
} else {
|
||||||
qualifier: field != null ? field.qualifier : '',
|
this.formGroup.patchValue({
|
||||||
scopeNote: field != null ? field.scopeNote : ''
|
metadatadatafieldgroup: {
|
||||||
}
|
element: field.element,
|
||||||
});
|
qualifier: field.qualifier,
|
||||||
|
scopeNote: field.scopeNote,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.element.disabled = true;
|
||||||
|
this.qualifier.disabled = true;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -157,25 +177,24 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
|
|||||||
* When the field has no id attached -> Create new field
|
* When the field has no id attached -> Create new field
|
||||||
* Emit the updated/created field using the EventEmitter submitForm
|
* Emit the updated/created field using the EventEmitter submitForm
|
||||||
*/
|
*/
|
||||||
onSubmit() {
|
onSubmit(): void {
|
||||||
this.registryService.getActiveMetadataField().pipe(take(1)).subscribe(
|
this.registryService.getActiveMetadataField().pipe(take(1)).subscribe(
|
||||||
(field) => {
|
(field: MetadataField) => {
|
||||||
const values = {
|
|
||||||
element: this.element.value,
|
|
||||||
qualifier: this.qualifier.value,
|
|
||||||
scopeNote: this.scopeNote.value
|
|
||||||
};
|
|
||||||
if (field == null) {
|
if (field == null) {
|
||||||
this.registryService.createMetadataField(Object.assign(new MetadataField(), values), this.metadataSchema).subscribe((newField) => {
|
this.registryService.createMetadataField(Object.assign(new MetadataField(), {
|
||||||
|
element: this.element.value,
|
||||||
|
qualifier: this.qualifier.value,
|
||||||
|
scopeNote: this.scopeNote.value,
|
||||||
|
}), this.metadataSchema).subscribe((newField: MetadataField) => {
|
||||||
this.submitForm.emit(newField);
|
this.submitForm.emit(newField);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.registryService.updateMetadataField(Object.assign(new MetadataField(), field, {
|
this.registryService.updateMetadataField(Object.assign(new MetadataField(), field, {
|
||||||
id: field.id,
|
id: field.id,
|
||||||
element: (values.element ? values.element : field.element),
|
element: field.element,
|
||||||
qualifier: (values.qualifier ? values.qualifier : field.qualifier),
|
qualifier: field.qualifier,
|
||||||
scopeNote: (values.scopeNote ? values.scopeNote : field.scopeNote)
|
scopeNote: this.scopeNote.value,
|
||||||
})).subscribe((updatedField) => {
|
})).subscribe((updatedField: MetadataField) => {
|
||||||
this.submitForm.emit(updatedField);
|
this.submitForm.emit(updatedField);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -188,14 +207,10 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* Reset all input-fields to be empty
|
* Reset all input-fields to be empty
|
||||||
*/
|
*/
|
||||||
clearFields() {
|
clearFields(): void {
|
||||||
this.formGroup.patchValue({
|
this.formGroup.reset('metadatadatafieldgroup');
|
||||||
metadatadatafieldgroup: {
|
this.element.disabled = false;
|
||||||
element: '',
|
this.qualifier.disabled = false;
|
||||||
qualifier: '',
|
|
||||||
scopeNote: ''
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -11,6 +11,9 @@ import { ActivatedRoute, Router } from '@angular/router';
|
|||||||
import { getForbiddenRoute } from '../../app-routing-paths';
|
import { getForbiddenRoute } from '../../app-routing-paths';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { SignpostingDataService } from '../../core/data/signposting-data.service';
|
||||||
|
import { ServerResponseService } from '../../core/services/server-response.service';
|
||||||
|
import { PLATFORM_ID } from '@angular/core';
|
||||||
|
|
||||||
describe('BitstreamDownloadPageComponent', () => {
|
describe('BitstreamDownloadPageComponent', () => {
|
||||||
let component: BitstreamDownloadPageComponent;
|
let component: BitstreamDownloadPageComponent;
|
||||||
@@ -24,6 +27,20 @@ describe('BitstreamDownloadPageComponent', () => {
|
|||||||
let router;
|
let router;
|
||||||
|
|
||||||
let bitstream: Bitstream;
|
let bitstream: Bitstream;
|
||||||
|
let serverResponseService: jasmine.SpyObj<ServerResponseService>;
|
||||||
|
let signpostingDataService: jasmine.SpyObj<SignpostingDataService>;
|
||||||
|
|
||||||
|
const mocklink = {
|
||||||
|
href: 'http://test.org',
|
||||||
|
rel: 'test',
|
||||||
|
type: 'test'
|
||||||
|
};
|
||||||
|
|
||||||
|
const mocklink2 = {
|
||||||
|
href: 'http://test2.org',
|
||||||
|
rel: 'test',
|
||||||
|
type: 'test'
|
||||||
|
};
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
authService = jasmine.createSpyObj('authService', {
|
authService = jasmine.createSpyObj('authService', {
|
||||||
@@ -44,8 +61,8 @@ describe('BitstreamDownloadPageComponent', () => {
|
|||||||
bitstream = Object.assign(new Bitstream(), {
|
bitstream = Object.assign(new Bitstream(), {
|
||||||
uuid: 'bitstreamUuid',
|
uuid: 'bitstreamUuid',
|
||||||
_links: {
|
_links: {
|
||||||
content: {href: 'bitstream-content-link'},
|
content: { href: 'bitstream-content-link' },
|
||||||
self: {href: 'bitstream-self-link'},
|
self: { href: 'bitstream-self-link' },
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -54,10 +71,21 @@ describe('BitstreamDownloadPageComponent', () => {
|
|||||||
bitstream: createSuccessfulRemoteDataObject(
|
bitstream: createSuccessfulRemoteDataObject(
|
||||||
bitstream
|
bitstream
|
||||||
)
|
)
|
||||||
|
}),
|
||||||
|
params: observableOf({
|
||||||
|
id: 'testid'
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
router = jasmine.createSpyObj('router', ['navigateByUrl']);
|
router = jasmine.createSpyObj('router', ['navigateByUrl']);
|
||||||
|
|
||||||
|
serverResponseService = jasmine.createSpyObj('ServerResponseService', {
|
||||||
|
setHeader: jasmine.createSpy('setHeader'),
|
||||||
|
});
|
||||||
|
|
||||||
|
signpostingDataService = jasmine.createSpyObj('SignpostingDataService', {
|
||||||
|
getLinks: observableOf([mocklink, mocklink2])
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function initTestbed() {
|
function initTestbed() {
|
||||||
@@ -65,12 +93,15 @@ describe('BitstreamDownloadPageComponent', () => {
|
|||||||
imports: [CommonModule, TranslateModule.forRoot()],
|
imports: [CommonModule, TranslateModule.forRoot()],
|
||||||
declarations: [BitstreamDownloadPageComponent],
|
declarations: [BitstreamDownloadPageComponent],
|
||||||
providers: [
|
providers: [
|
||||||
{provide: ActivatedRoute, useValue: activatedRoute},
|
{ provide: ActivatedRoute, useValue: activatedRoute },
|
||||||
{provide: Router, useValue: router},
|
{ provide: Router, useValue: router },
|
||||||
{provide: AuthorizationDataService, useValue: authorizationService},
|
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||||
{provide: AuthService, useValue: authService},
|
{ provide: AuthService, useValue: authService },
|
||||||
{provide: FileService, useValue: fileService},
|
{ provide: FileService, useValue: fileService },
|
||||||
{provide: HardRedirectService, useValue: hardRedirectService},
|
{ provide: HardRedirectService, useValue: hardRedirectService },
|
||||||
|
{ provide: ServerResponseService, useValue: serverResponseService },
|
||||||
|
{ provide: SignpostingDataService, useValue: signpostingDataService },
|
||||||
|
{ provide: PLATFORM_ID, useValue: 'server' }
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
.compileComponents();
|
.compileComponents();
|
||||||
@@ -107,6 +138,9 @@ describe('BitstreamDownloadPageComponent', () => {
|
|||||||
it('should redirect to the content link', () => {
|
it('should redirect to the content link', () => {
|
||||||
expect(hardRedirectService.redirect).toHaveBeenCalledWith('bitstream-content-link');
|
expect(hardRedirectService.redirect).toHaveBeenCalledWith('bitstream-content-link');
|
||||||
});
|
});
|
||||||
|
it('should add the signposting links', () => {
|
||||||
|
expect(serverResponseService.setHeader).toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
describe('when the user is authorized and logged in', () => {
|
describe('when the user is authorized and logged in', () => {
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
@@ -134,7 +168,7 @@ describe('BitstreamDownloadPageComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
it('should navigate to the forbidden route', () => {
|
it('should navigate to the forbidden route', () => {
|
||||||
expect(router.navigateByUrl).toHaveBeenCalledWith(getForbiddenRoute(), {skipLocationChange: true});
|
expect(router.navigateByUrl).toHaveBeenCalledWith(getForbiddenRoute(), { skipLocationChange: true });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('when the user is not authorized and not logged in', () => {
|
describe('when the user is not authorized and not logged in', () => {
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, Inject, OnInit, PLATFORM_ID } from '@angular/core';
|
||||||
import { filter, map, switchMap, take } from 'rxjs/operators';
|
import { filter, map, switchMap, take } from 'rxjs/operators';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||||
import { getRemoteDataPayload} from '../../core/shared/operators';
|
import { getRemoteDataPayload } from '../../core/shared/operators';
|
||||||
import { Bitstream } from '../../core/shared/bitstream.model';
|
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
@@ -13,8 +13,11 @@ import { HardRedirectService } from '../../core/services/hard-redirect.service';
|
|||||||
import { getForbiddenRoute } from '../../app-routing-paths';
|
import { getForbiddenRoute } from '../../app-routing-paths';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { redirectOn4xx } from '../../core/shared/authorized.operators';
|
import { redirectOn4xx } from '../../core/shared/authorized.operators';
|
||||||
import { Location } from '@angular/common';
|
import { isPlatformServer, Location } from '@angular/common';
|
||||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||||
|
import { SignpostingDataService } from '../../core/data/signposting-data.service';
|
||||||
|
import { ServerResponseService } from '../../core/services/server-response.service';
|
||||||
|
import { SignpostingLink } from '../../core/data/signposting-links.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-bitstream-download-page',
|
selector: 'ds-bitstream-download-page',
|
||||||
@@ -28,7 +31,6 @@ export class BitstreamDownloadPageComponent implements OnInit {
|
|||||||
bitstream$: Observable<Bitstream>;
|
bitstream$: Observable<Bitstream>;
|
||||||
bitstreamRD$: Observable<RemoteData<Bitstream>>;
|
bitstreamRD$: Observable<RemoteData<Bitstream>>;
|
||||||
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
@@ -38,8 +40,11 @@ export class BitstreamDownloadPageComponent implements OnInit {
|
|||||||
private hardRedirectService: HardRedirectService,
|
private hardRedirectService: HardRedirectService,
|
||||||
private location: Location,
|
private location: Location,
|
||||||
public dsoNameService: DSONameService,
|
public dsoNameService: DSONameService,
|
||||||
|
private signpostingDataService: SignpostingDataService,
|
||||||
|
private responseService: ServerResponseService,
|
||||||
|
@Inject(PLATFORM_ID) protected platformId: string
|
||||||
) {
|
) {
|
||||||
|
this.initPageLinks();
|
||||||
}
|
}
|
||||||
|
|
||||||
back(): void {
|
back(): void {
|
||||||
@@ -89,4 +94,26 @@ export class BitstreamDownloadPageComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create page links if any are retrieved by signposting endpoint
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private initPageLinks(): void {
|
||||||
|
if (isPlatformServer(this.platformId)) {
|
||||||
|
this.route.params.subscribe(params => {
|
||||||
|
this.signpostingDataService.getLinks(params.id).pipe(take(1)).subscribe((signpostingLinks: SignpostingLink[]) => {
|
||||||
|
let links = '';
|
||||||
|
|
||||||
|
signpostingLinks.forEach((link: SignpostingLink) => {
|
||||||
|
links = links + (isNotEmpty(links) ? ', ' : '') + `<${link.href}> ; rel="${link.rel}"` + (isNotEmpty(link.type) ? ` ; type="${link.type}" ` : ' ');
|
||||||
|
links = links + (isNotEmpty(links) ? ', ' : '') + `<${link.href}> ; rel="${link.rel}" ; type="${link.type}" `;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.responseService.setHeader('Link', links);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,8 +2,8 @@ import { first } from 'rxjs/operators';
|
|||||||
import { BrowseByGuard } from './browse-by-guard';
|
import { BrowseByGuard } from './browse-by-guard';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
|
||||||
import { BrowseDefinition } from '../core/shared/browse-definition.model';
|
|
||||||
import { BrowseByDataType } from './browse-by-switcher/browse-by-decorator';
|
import { BrowseByDataType } from './browse-by-switcher/browse-by-decorator';
|
||||||
|
import { ValueListBrowseDefinition } from '../core/shared/value-list-browse-definition.model';
|
||||||
import { DSONameServiceMock } from '../shared/mocks/dso-name.service.mock';
|
import { DSONameServiceMock } from '../shared/mocks/dso-name.service.mock';
|
||||||
import { DSONameService } from '../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../core/breadcrumbs/dso-name.service';
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ describe('BrowseByGuard', () => {
|
|||||||
const id = 'author';
|
const id = 'author';
|
||||||
const scope = '1234-65487-12354-1235';
|
const scope = '1234-65487-12354-1235';
|
||||||
const value = 'Filter';
|
const value = 'Filter';
|
||||||
const browseDefinition = Object.assign(new BrowseDefinition(), { type: BrowseByDataType.Metadata, metadataKeys: ['dc.contributor'] });
|
const browseDefinition = Object.assign(new ValueListBrowseDefinition(), { type: BrowseByDataType.Metadata, metadataKeys: ['dc.contributor'] });
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
dsoService = {
|
dsoService = {
|
||||||
|
@@ -26,7 +26,7 @@ const map = new Map();
|
|||||||
* @param browseByType The type of page
|
* @param browseByType The type of page
|
||||||
* @param theme The optional theme for the component
|
* @param theme The optional theme for the component
|
||||||
*/
|
*/
|
||||||
export function rendersBrowseBy(browseByType: BrowseByDataType, theme = DEFAULT_THEME) {
|
export function rendersBrowseBy(browseByType: string, theme = DEFAULT_THEME) {
|
||||||
return function decorator(component: any) {
|
return function decorator(component: any) {
|
||||||
if (hasNoValue(map.get(browseByType))) {
|
if (hasNoValue(map.get(browseByType))) {
|
||||||
map.set(browseByType, new Map());
|
map.set(browseByType, new Map());
|
||||||
|
@@ -3,9 +3,11 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
|||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { BROWSE_BY_COMPONENT_FACTORY, BrowseByDataType } from './browse-by-decorator';
|
import { BROWSE_BY_COMPONENT_FACTORY, BrowseByDataType } from './browse-by-decorator';
|
||||||
import { BrowseDefinition } from '../../core/shared/browse-definition.model';
|
|
||||||
import { BehaviorSubject } from 'rxjs';
|
import { BehaviorSubject } from 'rxjs';
|
||||||
import { ThemeService } from '../../shared/theme-support/theme.service';
|
import { ThemeService } from '../../shared/theme-support/theme.service';
|
||||||
|
import { FlatBrowseDefinition } from '../../core/shared/flat-browse-definition.model';
|
||||||
|
import { ValueListBrowseDefinition } from '../../core/shared/value-list-browse-definition.model';
|
||||||
|
import { NonHierarchicalBrowseDefinition } from '../../core/shared/non-hierarchical-browse-definition';
|
||||||
|
|
||||||
describe('BrowseBySwitcherComponent', () => {
|
describe('BrowseBySwitcherComponent', () => {
|
||||||
let comp: BrowseBySwitcherComponent;
|
let comp: BrowseBySwitcherComponent;
|
||||||
@@ -13,33 +15,33 @@ describe('BrowseBySwitcherComponent', () => {
|
|||||||
|
|
||||||
const types = [
|
const types = [
|
||||||
Object.assign(
|
Object.assign(
|
||||||
new BrowseDefinition(), {
|
new FlatBrowseDefinition(), {
|
||||||
id: 'title',
|
id: 'title',
|
||||||
dataType: BrowseByDataType.Title,
|
dataType: BrowseByDataType.Title,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
Object.assign(
|
Object.assign(
|
||||||
new BrowseDefinition(), {
|
new FlatBrowseDefinition(), {
|
||||||
id: 'dateissued',
|
id: 'dateissued',
|
||||||
dataType: BrowseByDataType.Date,
|
dataType: BrowseByDataType.Date,
|
||||||
metadataKeys: ['dc.date.issued']
|
metadataKeys: ['dc.date.issued']
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
Object.assign(
|
Object.assign(
|
||||||
new BrowseDefinition(), {
|
new ValueListBrowseDefinition(), {
|
||||||
id: 'author',
|
id: 'author',
|
||||||
dataType: BrowseByDataType.Metadata,
|
dataType: BrowseByDataType.Metadata,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
Object.assign(
|
Object.assign(
|
||||||
new BrowseDefinition(), {
|
new ValueListBrowseDefinition(), {
|
||||||
id: 'subject',
|
id: 'subject',
|
||||||
dataType: BrowseByDataType.Metadata,
|
dataType: BrowseByDataType.Metadata,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
const data = new BehaviorSubject(createDataWithBrowseDefinition(new BrowseDefinition()));
|
const data = new BehaviorSubject(createDataWithBrowseDefinition(new FlatBrowseDefinition()));
|
||||||
|
|
||||||
const activatedRouteStub = {
|
const activatedRouteStub = {
|
||||||
data
|
data
|
||||||
@@ -70,7 +72,7 @@ describe('BrowseBySwitcherComponent', () => {
|
|||||||
comp = fixture.componentInstance;
|
comp = fixture.componentInstance;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
types.forEach((type: BrowseDefinition) => {
|
types.forEach((type: NonHierarchicalBrowseDefinition) => {
|
||||||
describe(`when switching to a browse-by page for "${type.id}"`, () => {
|
describe(`when switching to a browse-by page for "${type.id}"`, () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
data.next(createDataWithBrowseDefinition(type));
|
data.next(createDataWithBrowseDefinition(type));
|
||||||
|
@@ -31,7 +31,7 @@ export class BrowseBySwitcherComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.browseByComponent = this.route.data.pipe(
|
this.browseByComponent = this.route.data.pipe(
|
||||||
map((data: { browseDefinition: BrowseDefinition }) => this.getComponentByBrowseByType(data.browseDefinition.dataType, this.themeService.getThemeName()))
|
map((data: { browseDefinition: BrowseDefinition }) => this.getComponentByBrowseByType(data.browseDefinition.getRenderType(), this.themeService.getThemeName()))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -0,0 +1,14 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="mb-3">
|
||||||
|
<ds-vocabulary-treeview [vocabularyOptions]=vocabularyOptions
|
||||||
|
[multiSelect]="true"
|
||||||
|
(select)="onSelect($event)"
|
||||||
|
(deselect)="onDeselect($event)">
|
||||||
|
</ds-vocabulary-treeview>
|
||||||
|
</div>
|
||||||
|
<a class="btn btn-primary"
|
||||||
|
[routerLink]="['/search']"
|
||||||
|
[queryParams]="queryParams"
|
||||||
|
[queryParamsHandling]="'merge'">
|
||||||
|
{{ 'browse.taxonomy.button' | translate }}</a>
|
||||||
|
</div>
|
@@ -0,0 +1,91 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { BrowseByTaxonomyPageComponent } from './browse-by-taxonomy-page.component';
|
||||||
|
import { VocabularyEntryDetail } from '../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { BehaviorSubject } from 'rxjs';
|
||||||
|
import { createDataWithBrowseDefinition } from '../browse-by-switcher/browse-by-switcher.component.spec';
|
||||||
|
import { HierarchicalBrowseDefinition } from '../../core/shared/hierarchical-browse-definition.model';
|
||||||
|
import { ThemeService } from '../../shared/theme-support/theme.service';
|
||||||
|
|
||||||
|
describe('BrowseByTaxonomyPageComponent', () => {
|
||||||
|
let component: BrowseByTaxonomyPageComponent;
|
||||||
|
let fixture: ComponentFixture<BrowseByTaxonomyPageComponent>;
|
||||||
|
let themeService: ThemeService;
|
||||||
|
let detail1: VocabularyEntryDetail;
|
||||||
|
let detail2: VocabularyEntryDetail;
|
||||||
|
|
||||||
|
const data = new BehaviorSubject(createDataWithBrowseDefinition(new HierarchicalBrowseDefinition()));
|
||||||
|
const activatedRouteStub = {
|
||||||
|
data
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
themeService = jasmine.createSpyObj('themeService', {
|
||||||
|
getThemeName: 'dspace',
|
||||||
|
});
|
||||||
|
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ TranslateModule.forRoot() ],
|
||||||
|
declarations: [ BrowseByTaxonomyPageComponent ],
|
||||||
|
providers: [
|
||||||
|
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||||
|
{ provide: ThemeService, useValue: themeService },
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(BrowseByTaxonomyPageComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
detail1 = new VocabularyEntryDetail();
|
||||||
|
detail2 = new VocabularyEntryDetail();
|
||||||
|
detail1.value = 'HUMANITIES and RELIGION';
|
||||||
|
detail2.value = 'TECHNOLOGY';
|
||||||
|
detail1.id = 'id-1';
|
||||||
|
detail2.id = 'id-2';
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle select event', () => {
|
||||||
|
component.onSelect(detail1);
|
||||||
|
expect(component.selectedItems.length).toBe(1);
|
||||||
|
expect(component.selectedItems).toContain(detail1);
|
||||||
|
expect(component.selectedItems.length).toBe(1);
|
||||||
|
expect(component.filterValues).toEqual(['HUMANITIES and RELIGION,equals'] );
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle select event with multiple selected items', () => {
|
||||||
|
component.onSelect(detail1);
|
||||||
|
component.onSelect(detail2);
|
||||||
|
expect(component.selectedItems.length).toBe(2);
|
||||||
|
expect(component.selectedItems).toContain(detail1, detail2);
|
||||||
|
expect(component.selectedItems.length).toBe(2);
|
||||||
|
expect(component.filterValues).toEqual(['HUMANITIES and RELIGION,equals', 'TECHNOLOGY,equals'] );
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle deselect event', () => {
|
||||||
|
component.onSelect(detail1);
|
||||||
|
component.onSelect(detail2);
|
||||||
|
expect(component.selectedItems.length).toBe(2);
|
||||||
|
expect(component.selectedItems.length).toBe(2);
|
||||||
|
component.onDeselect(detail1);
|
||||||
|
expect(component.selectedItems.length).toBe(1);
|
||||||
|
expect(component.selectedItems).toContain(detail2);
|
||||||
|
expect(component.selectedItems.length).toBe(1);
|
||||||
|
expect(component.filterValues).toEqual(['TECHNOLOGY,equals'] );
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fixture.destroy();
|
||||||
|
component = null;
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,118 @@
|
|||||||
|
import { Component, OnInit, Inject, OnDestroy } from '@angular/core';
|
||||||
|
import { VocabularyOptions } from '../../core/submission/vocabularies/models/vocabulary-options.model';
|
||||||
|
import { VocabularyEntryDetail } from '../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { Observable, Subscription } from 'rxjs';
|
||||||
|
import { BrowseDefinition } from '../../core/shared/browse-definition.model';
|
||||||
|
import { GenericConstructor } from '../../core/shared/generic-constructor';
|
||||||
|
import { BROWSE_BY_COMPONENT_FACTORY } from '../browse-by-switcher/browse-by-decorator';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
import { ThemeService } from 'src/app/shared/theme-support/theme.service';
|
||||||
|
import { HierarchicalBrowseDefinition } from '../../core/shared/hierarchical-browse-definition.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-browse-by-taxonomy-page',
|
||||||
|
templateUrl: './browse-by-taxonomy-page.component.html',
|
||||||
|
styleUrls: ['./browse-by-taxonomy-page.component.scss']
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Component for browsing items by metadata in a hierarchical controlled vocabulary
|
||||||
|
*/
|
||||||
|
export class BrowseByTaxonomyPageComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link VocabularyOptions} object
|
||||||
|
*/
|
||||||
|
vocabularyOptions: VocabularyOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The selected vocabulary entries
|
||||||
|
*/
|
||||||
|
selectedItems: VocabularyEntryDetail[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The query parameters, contain the selected entries
|
||||||
|
*/
|
||||||
|
filterValues: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The facet the use when filtering
|
||||||
|
*/
|
||||||
|
facetType: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The used vocabulary
|
||||||
|
*/
|
||||||
|
vocabularyName: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The parameters used in the URL
|
||||||
|
*/
|
||||||
|
queryParams: any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolved browse-by component
|
||||||
|
*/
|
||||||
|
browseByComponent: Observable<any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscriptions to track
|
||||||
|
*/
|
||||||
|
browseByComponentSubs: Subscription[] = [];
|
||||||
|
|
||||||
|
public constructor( protected route: ActivatedRoute,
|
||||||
|
protected themeService: ThemeService,
|
||||||
|
@Inject(BROWSE_BY_COMPONENT_FACTORY) private getComponentByBrowseByType: (browseByType, theme) => GenericConstructor<any>) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.browseByComponent = this.route.data.pipe(
|
||||||
|
map((data: { browseDefinition: BrowseDefinition }) => {
|
||||||
|
this.getComponentByBrowseByType(data.browseDefinition.getRenderType(), this.themeService.getThemeName());
|
||||||
|
return data.browseDefinition;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.browseByComponentSubs.push(this.browseByComponent.subscribe((browseDefinition: HierarchicalBrowseDefinition) => {
|
||||||
|
this.facetType = browseDefinition.facetType;
|
||||||
|
this.vocabularyName = browseDefinition.vocabulary;
|
||||||
|
this.vocabularyOptions = { name: this.vocabularyName, closed: true };
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds detail to selectedItems, transforms it to be used as query parameter
|
||||||
|
* and adds that to filterValues.
|
||||||
|
*
|
||||||
|
* @param detail VocabularyEntryDetail to be added
|
||||||
|
*/
|
||||||
|
onSelect(detail: VocabularyEntryDetail): void {
|
||||||
|
this.selectedItems.push(detail);
|
||||||
|
this.filterValues = this.selectedItems
|
||||||
|
.map((item: VocabularyEntryDetail) => `${item.value},equals`);
|
||||||
|
this.updateQueryParams();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes detail from selectedItems and filterValues.
|
||||||
|
*
|
||||||
|
* @param detail VocabularyEntryDetail to be removed
|
||||||
|
*/
|
||||||
|
onDeselect(detail: VocabularyEntryDetail): void {
|
||||||
|
this.selectedItems = this.selectedItems.filter((entry: VocabularyEntryDetail) => { return entry.id !== detail.id; });
|
||||||
|
this.filterValues = this.filterValues.filter((value: string) => { return value !== `${detail.value},equals`; });
|
||||||
|
this.updateQueryParams();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates queryParams based on the current facetType and filterValues.
|
||||||
|
*/
|
||||||
|
private updateQueryParams(): void {
|
||||||
|
this.queryParams = {
|
||||||
|
['f.' + this.facetType]: this.filterValues
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.browseByComponentSubs.forEach((sub: Subscription) => sub.unsubscribe());
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,28 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { ThemedComponent } from '../../shared/theme-support/themed.component';
|
||||||
|
import { rendersBrowseBy } from '../browse-by-switcher/browse-by-decorator';
|
||||||
|
import { BrowseByTaxonomyPageComponent } from './browse-by-taxonomy-page.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-themed-browse-by-taxonomy-page',
|
||||||
|
templateUrl: '../../shared/theme-support/themed.component.html',
|
||||||
|
styleUrls: []
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Themed wrapper for BrowseByTaxonomyPageComponent
|
||||||
|
*/
|
||||||
|
@rendersBrowseBy('hierarchy')
|
||||||
|
export class ThemedBrowseByTaxonomyPageComponent extends ThemedComponent<BrowseByTaxonomyPageComponent>{
|
||||||
|
|
||||||
|
protected getComponentName(): string {
|
||||||
|
return 'BrowseByTaxonomyPageComponent';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importThemedComponent(themeName: string): Promise<any> {
|
||||||
|
return import(`../../../themes/${themeName}/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component`);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importUnthemedComponent(): Promise<any> {
|
||||||
|
return import(`./browse-by-taxonomy-page.component`);
|
||||||
|
}
|
||||||
|
}
|
@@ -4,24 +4,28 @@ import { BrowseByTitlePageComponent } from './browse-by-title-page/browse-by-tit
|
|||||||
import { BrowseByMetadataPageComponent } from './browse-by-metadata-page/browse-by-metadata-page.component';
|
import { BrowseByMetadataPageComponent } from './browse-by-metadata-page/browse-by-metadata-page.component';
|
||||||
import { BrowseByDatePageComponent } from './browse-by-date-page/browse-by-date-page.component';
|
import { BrowseByDatePageComponent } from './browse-by-date-page/browse-by-date-page.component';
|
||||||
import { BrowseBySwitcherComponent } from './browse-by-switcher/browse-by-switcher.component';
|
import { BrowseBySwitcherComponent } from './browse-by-switcher/browse-by-switcher.component';
|
||||||
|
import { BrowseByTaxonomyPageComponent } from './browse-by-taxonomy-page/browse-by-taxonomy-page.component';
|
||||||
import { ThemedBrowseBySwitcherComponent } from './browse-by-switcher/themed-browse-by-switcher.component';
|
import { ThemedBrowseBySwitcherComponent } from './browse-by-switcher/themed-browse-by-switcher.component';
|
||||||
import { ComcolModule } from '../shared/comcol/comcol.module';
|
import { ComcolModule } from '../shared/comcol/comcol.module';
|
||||||
import { ThemedBrowseByMetadataPageComponent } from './browse-by-metadata-page/themed-browse-by-metadata-page.component';
|
import { ThemedBrowseByMetadataPageComponent } from './browse-by-metadata-page/themed-browse-by-metadata-page.component';
|
||||||
import { ThemedBrowseByDatePageComponent } from './browse-by-date-page/themed-browse-by-date-page.component';
|
import { ThemedBrowseByDatePageComponent } from './browse-by-date-page/themed-browse-by-date-page.component';
|
||||||
import { ThemedBrowseByTitlePageComponent } from './browse-by-title-page/themed-browse-by-title-page.component';
|
import { ThemedBrowseByTitlePageComponent } from './browse-by-title-page/themed-browse-by-title-page.component';
|
||||||
|
import { ThemedBrowseByTaxonomyPageComponent } from './browse-by-taxonomy-page/themed-browse-by-taxonomy-page.component';
|
||||||
import { SharedBrowseByModule } from '../shared/browse-by/shared-browse-by.module';
|
import { SharedBrowseByModule } from '../shared/browse-by/shared-browse-by.module';
|
||||||
import { DsoPageModule } from '../shared/dso-page/dso-page.module';
|
import { DsoPageModule } from '../shared/dso-page/dso-page.module';
|
||||||
|
import { FormModule } from '../shared/form/form.module';
|
||||||
|
|
||||||
const ENTRY_COMPONENTS = [
|
const ENTRY_COMPONENTS = [
|
||||||
// put only entry components that use custom decorator
|
// put only entry components that use custom decorator
|
||||||
BrowseByTitlePageComponent,
|
BrowseByTitlePageComponent,
|
||||||
BrowseByMetadataPageComponent,
|
BrowseByMetadataPageComponent,
|
||||||
BrowseByDatePageComponent,
|
BrowseByDatePageComponent,
|
||||||
|
BrowseByTaxonomyPageComponent,
|
||||||
|
|
||||||
ThemedBrowseByMetadataPageComponent,
|
ThemedBrowseByMetadataPageComponent,
|
||||||
ThemedBrowseByDatePageComponent,
|
ThemedBrowseByDatePageComponent,
|
||||||
ThemedBrowseByTitlePageComponent,
|
ThemedBrowseByTitlePageComponent,
|
||||||
|
ThemedBrowseByTaxonomyPageComponent,
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@@ -29,7 +33,8 @@ const ENTRY_COMPONENTS = [
|
|||||||
SharedBrowseByModule,
|
SharedBrowseByModule,
|
||||||
CommonModule,
|
CommonModule,
|
||||||
ComcolModule,
|
ComcolModule,
|
||||||
DsoPageModule
|
DsoPageModule,
|
||||||
|
FormModule,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
BrowseBySwitcherComponent,
|
BrowseBySwitcherComponent,
|
||||||
|
@@ -1,20 +1,60 @@
|
|||||||
|
// eslint-disable-next-line max-classes-per-file
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { BROWSE_DEFINITION } from '../shared/browse-definition.resource-type';
|
import { BROWSE_DEFINITION } from '../shared/browse-definition.resource-type';
|
||||||
import { BrowseDefinition } from '../shared/browse-definition.model';
|
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
import { RemoteData } from '../data/remote-data';
|
import { RemoteData } from '../data/remote-data';
|
||||||
import { PaginatedList } from '../data/paginated-list.model';
|
import { PaginatedList } from '../data/paginated-list.model';
|
||||||
import { FindListOptions } from '../data/find-list-options.model';
|
import { FindListOptions } from '../data/find-list-options.model';
|
||||||
import { IdentifiableDataService } from '../data/base/identifiable-data.service';
|
import { IdentifiableDataService } from '../data/base/identifiable-data.service';
|
||||||
import { FindAllData, FindAllDataImpl } from '../data/base/find-all-data';
|
import { FindAllData, FindAllDataImpl } from '../data/base/find-all-data';
|
||||||
import { dataService } from '../data/base/data-service.decorator';
|
import { dataService } from '../data/base/data-service.decorator';
|
||||||
|
import { isNotEmpty, isNotEmptyOperator, hasValue } from '../../shared/empty.util';
|
||||||
|
import { take } from 'rxjs/operators';
|
||||||
|
import { BrowseDefinitionRestRequest } from '../data/request.models';
|
||||||
import { RequestParam } from '../cache/models/request-param.model';
|
import { RequestParam } from '../cache/models/request-param.model';
|
||||||
import { SearchData, SearchDataImpl } from '../data/base/search-data';
|
import { SearchData, SearchDataImpl } from '../data/base/search-data';
|
||||||
|
import { BrowseDefinition } from '../shared/browse-definition.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a GET request for the given href, and send it.
|
||||||
|
* Use a GET request specific for BrowseDefinitions.
|
||||||
|
*/
|
||||||
|
export const createAndSendBrowseDefinitionGetRequest = (requestService: RequestService,
|
||||||
|
responseMsToLive: number,
|
||||||
|
href$: string | Observable<string>,
|
||||||
|
useCachedVersionIfAvailable: boolean = true): void => {
|
||||||
|
if (isNotEmpty(href$)) {
|
||||||
|
if (typeof href$ === 'string') {
|
||||||
|
href$ = observableOf(href$);
|
||||||
|
}
|
||||||
|
|
||||||
|
href$.pipe(
|
||||||
|
isNotEmptyOperator(),
|
||||||
|
take(1)
|
||||||
|
).subscribe((href: string) => {
|
||||||
|
const requestId = requestService.generateRequestId();
|
||||||
|
const request = new BrowseDefinitionRestRequest(requestId, href);
|
||||||
|
if (hasValue(responseMsToLive)) {
|
||||||
|
request.responseMsToLive = responseMsToLive;
|
||||||
|
}
|
||||||
|
requestService.send(request, useCachedVersionIfAvailable);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom extension of {@link FindAllDataImpl} to be able to send BrowseDefinitionRestRequests
|
||||||
|
*/
|
||||||
|
class BrowseDefinitionFindAllDataImpl extends FindAllDataImpl<BrowseDefinition> {
|
||||||
|
createAndSendGetRequest(href$: string | Observable<string>, useCachedVersionIfAvailable: boolean = true) {
|
||||||
|
createAndSendBrowseDefinitionGetRequest(this.requestService, this.responseMsToLive, href$, useCachedVersionIfAvailable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data service responsible for retrieving browse definitions from the REST server
|
* Data service responsible for retrieving browse definitions from the REST server
|
||||||
@@ -24,7 +64,7 @@ import { SearchData, SearchDataImpl } from '../data/base/search-data';
|
|||||||
})
|
})
|
||||||
@dataService(BROWSE_DEFINITION)
|
@dataService(BROWSE_DEFINITION)
|
||||||
export class BrowseDefinitionDataService extends IdentifiableDataService<BrowseDefinition> implements FindAllData<BrowseDefinition>, SearchData<BrowseDefinition> {
|
export class BrowseDefinitionDataService extends IdentifiableDataService<BrowseDefinition> implements FindAllData<BrowseDefinition>, SearchData<BrowseDefinition> {
|
||||||
private findAllData: FindAllDataImpl<BrowseDefinition>;
|
private findAllData: BrowseDefinitionFindAllDataImpl;
|
||||||
private searchData: SearchDataImpl<BrowseDefinition>;
|
private searchData: SearchDataImpl<BrowseDefinition>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -35,7 +75,7 @@ export class BrowseDefinitionDataService extends IdentifiableDataService<BrowseD
|
|||||||
) {
|
) {
|
||||||
super('browses', requestService, rdbService, objectCache, halService);
|
super('browses', requestService, rdbService, objectCache, halService);
|
||||||
this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
|
this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
|
||||||
this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
|
this.findAllData = new BrowseDefinitionFindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -121,5 +161,8 @@ export class BrowseDefinitionDataService extends IdentifiableDataService<BrowseD
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createAndSendGetRequest(href$: string | Observable<string>, useCachedVersionIfAvailable: boolean = true) {
|
||||||
|
createAndSendBrowseDefinitionGetRequest(this.requestService, this.responseMsToLive, href$, useCachedVersionIfAvailable);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -6,13 +6,15 @@ import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
|||||||
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
|
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { BrowseDefinition } from '../shared/browse-definition.model';
|
|
||||||
import { BrowseEntrySearchOptions } from './browse-entry-search-options.model';
|
import { BrowseEntrySearchOptions } from './browse-entry-search-options.model';
|
||||||
import { BrowseService } from './browse.service';
|
import { BrowseService } from './browse.service';
|
||||||
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
import { createPaginatedList, getFirstUsedArgumentOfSpyMethod } from '../../shared/testing/utils.test';
|
import { createPaginatedList, getFirstUsedArgumentOfSpyMethod } from '../../shared/testing/utils.test';
|
||||||
import { getMockHrefOnlyDataService } from '../../shared/mocks/href-only-data.service.mock';
|
import { getMockHrefOnlyDataService } from '../../shared/mocks/href-only-data.service.mock';
|
||||||
import { RequestEntry } from '../data/request-entry.model';
|
import { RequestEntry } from '../data/request-entry.model';
|
||||||
|
import { FlatBrowseDefinition } from '../shared/flat-browse-definition.model';
|
||||||
|
import { ValueListBrowseDefinition } from '../shared/value-list-browse-definition.model';
|
||||||
|
import { HierarchicalBrowseDefinition } from '../shared/hierarchical-browse-definition.model';
|
||||||
|
|
||||||
describe('BrowseService', () => {
|
describe('BrowseService', () => {
|
||||||
let scheduler: TestScheduler;
|
let scheduler: TestScheduler;
|
||||||
@@ -23,9 +25,9 @@ describe('BrowseService', () => {
|
|||||||
const browsesEndpointURL = 'https://rest.api/browses';
|
const browsesEndpointURL = 'https://rest.api/browses';
|
||||||
const halService: any = new HALEndpointServiceStub(browsesEndpointURL);
|
const halService: any = new HALEndpointServiceStub(browsesEndpointURL);
|
||||||
const browseDefinitions = [
|
const browseDefinitions = [
|
||||||
Object.assign(new BrowseDefinition(), {
|
Object.assign(new FlatBrowseDefinition(), {
|
||||||
id: 'date',
|
id: 'date',
|
||||||
metadataBrowse: false,
|
browseType: 'flatBrowse',
|
||||||
sortOptions: [
|
sortOptions: [
|
||||||
{
|
{
|
||||||
name: 'title',
|
name: 'title',
|
||||||
@@ -50,9 +52,9 @@ describe('BrowseService', () => {
|
|||||||
items: { href: 'https://rest.api/discover/browses/dateissued/items' }
|
items: { href: 'https://rest.api/discover/browses/dateissued/items' }
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
Object.assign(new BrowseDefinition(), {
|
Object.assign(new ValueListBrowseDefinition(), {
|
||||||
id: 'author',
|
id: 'author',
|
||||||
metadataBrowse: true,
|
browseType: 'valueList',
|
||||||
sortOptions: [
|
sortOptions: [
|
||||||
{
|
{
|
||||||
name: 'title',
|
name: 'title',
|
||||||
@@ -78,7 +80,23 @@ describe('BrowseService', () => {
|
|||||||
entries: { href: 'https://rest.api/discover/browses/author/entries' },
|
entries: { href: 'https://rest.api/discover/browses/author/entries' },
|
||||||
items: { href: 'https://rest.api/discover/browses/author/items' }
|
items: { href: 'https://rest.api/discover/browses/author/items' }
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
|
Object.assign(new HierarchicalBrowseDefinition(), {
|
||||||
|
id: 'srsc',
|
||||||
|
browseType: 'hierarchicalBrowse',
|
||||||
|
facetType: 'subject',
|
||||||
|
vocabulary: 'srsc',
|
||||||
|
type: 'browse',
|
||||||
|
metadata: [
|
||||||
|
'dc.subject'
|
||||||
|
],
|
||||||
|
_links: {
|
||||||
|
vocabulary: { 'href': 'https://rest.api/submission/vocabularies/srsc/' },
|
||||||
|
items: { 'href': 'https://rest.api/discover/browses/srsc/items' },
|
||||||
|
entries: { 'href': 'https://rest.api/discover/browses/srsc/entries' },
|
||||||
|
self: { 'href': 'https://rest.api/discover/browses/srsc' }
|
||||||
|
}
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
let browseDefinitionDataService;
|
let browseDefinitionDataService;
|
||||||
@@ -140,7 +158,7 @@ describe('BrowseService', () => {
|
|||||||
|
|
||||||
describe('when getBrowseEntriesFor is called with a valid browse definition id', () => {
|
describe('when getBrowseEntriesFor is called with a valid browse definition id', () => {
|
||||||
it('should call hrefOnlyDataService.findListByHref with the expected href', () => {
|
it('should call hrefOnlyDataService.findListByHref with the expected href', () => {
|
||||||
const expected = browseDefinitions[1]._links.entries.href;
|
const expected = (browseDefinitions[1] as ValueListBrowseDefinition)._links.entries.href;
|
||||||
|
|
||||||
scheduler.schedule(() => service.getBrowseEntriesFor(new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe());
|
scheduler.schedule(() => service.getBrowseEntriesFor(new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe());
|
||||||
scheduler.flush();
|
scheduler.flush();
|
||||||
|
@@ -7,6 +7,7 @@ import { PaginatedList } from '../data/paginated-list.model';
|
|||||||
import { RemoteData } from '../data/remote-data';
|
import { RemoteData } from '../data/remote-data';
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { BrowseDefinition } from '../shared/browse-definition.model';
|
import { BrowseDefinition } from '../shared/browse-definition.model';
|
||||||
|
import { FlatBrowseDefinition } from '../shared/flat-browse-definition.model';
|
||||||
import { BrowseEntry } from '../shared/browse-entry.model';
|
import { BrowseEntry } from '../shared/browse-entry.model';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { Item } from '../shared/item.model';
|
import { Item } from '../shared/item.model';
|
||||||
@@ -240,7 +241,12 @@ export class BrowseService {
|
|||||||
getPaginatedListPayload(),
|
getPaginatedListPayload(),
|
||||||
map((browseDefinitions: BrowseDefinition[]) => browseDefinitions
|
map((browseDefinitions: BrowseDefinition[]) => browseDefinitions
|
||||||
.find((def: BrowseDefinition) => {
|
.find((def: BrowseDefinition) => {
|
||||||
const matchingKeys = def.metadataKeys.find((key: string) => searchKeyArray.indexOf(key) >= 0);
|
let matchingKeys = '';
|
||||||
|
|
||||||
|
if (Array.isArray((def as FlatBrowseDefinition).metadataKeys)) {
|
||||||
|
matchingKeys = (def as FlatBrowseDefinition).metadataKeys.find((key: string) => searchKeyArray.indexOf(key) >= 0);
|
||||||
|
}
|
||||||
|
|
||||||
return isNotEmpty(matchingKeys);
|
return isNotEmpty(matchingKeys);
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
|
@@ -177,6 +177,10 @@ import { IdentifierData } from '../shared/object-list/identifier-data/identifier
|
|||||||
import { Subscription } from '../shared/subscriptions/models/subscription.model';
|
import { Subscription } from '../shared/subscriptions/models/subscription.model';
|
||||||
import { SupervisionOrderDataService } from './supervision-order/supervision-order-data.service';
|
import { SupervisionOrderDataService } from './supervision-order/supervision-order-data.service';
|
||||||
import { ItemRequest } from './shared/item-request.model';
|
import { ItemRequest } from './shared/item-request.model';
|
||||||
|
import { HierarchicalBrowseDefinition } from './shared/hierarchical-browse-definition.model';
|
||||||
|
import { FlatBrowseDefinition } from './shared/flat-browse-definition.model';
|
||||||
|
import { ValueListBrowseDefinition } from './shared/value-list-browse-definition.model';
|
||||||
|
import { NonHierarchicalBrowseDefinition } from './shared/non-hierarchical-browse-definition';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When not in production, endpoint responses can be mocked for testing purposes
|
* When not in production, endpoint responses can be mocked for testing purposes
|
||||||
@@ -333,6 +337,10 @@ export const models =
|
|||||||
AuthStatus,
|
AuthStatus,
|
||||||
BrowseEntry,
|
BrowseEntry,
|
||||||
BrowseDefinition,
|
BrowseDefinition,
|
||||||
|
NonHierarchicalBrowseDefinition,
|
||||||
|
FlatBrowseDefinition,
|
||||||
|
ValueListBrowseDefinition,
|
||||||
|
HierarchicalBrowseDefinition,
|
||||||
ClaimedTask,
|
ClaimedTask,
|
||||||
TaskObject,
|
TaskObject,
|
||||||
PoolTask,
|
PoolTask,
|
||||||
|
@@ -1,13 +1,14 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
import { BitstreamDataService } from './bitstream-data.service';
|
import { BitstreamDataService } from './bitstream-data.service';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
import { Bitstream } from '../shared/bitstream.model';
|
import { Bitstream } from '../shared/bitstream.model';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { BitstreamFormatDataService } from './bitstream-format-data.service';
|
import { BitstreamFormatDataService } from './bitstream-format-data.service';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
import { BitstreamFormat } from '../shared/bitstream-format.model';
|
import { BitstreamFormat } from '../shared/bitstream-format.model';
|
||||||
import { BitstreamFormatSupportLevel } from '../shared/bitstream-format-support-level';
|
import { BitstreamFormatSupportLevel } from '../shared/bitstream-format-support-level';
|
||||||
import { PutRequest } from './request.models';
|
import { PatchRequest, PutRequest } from './request.models';
|
||||||
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
||||||
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
|
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
@@ -15,6 +16,11 @@ import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-bu
|
|||||||
import { testSearchDataImplementation } from './base/search-data.spec';
|
import { testSearchDataImplementation } from './base/search-data.spec';
|
||||||
import { testPatchDataImplementation } from './base/patch-data.spec';
|
import { testPatchDataImplementation } from './base/patch-data.spec';
|
||||||
import { testDeleteDataImplementation } from './base/delete-data.spec';
|
import { testDeleteDataImplementation } from './base/delete-data.spec';
|
||||||
|
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import objectContaining = jasmine.objectContaining;
|
||||||
|
import { RemoteData } from './remote-data';
|
||||||
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
|
|
||||||
describe('BitstreamDataService', () => {
|
describe('BitstreamDataService', () => {
|
||||||
let service: BitstreamDataService;
|
let service: BitstreamDataService;
|
||||||
@@ -25,10 +31,18 @@ describe('BitstreamDataService', () => {
|
|||||||
let rdbService: RemoteDataBuildService;
|
let rdbService: RemoteDataBuildService;
|
||||||
const bitstreamFormatHref = 'rest-api/bitstreamformats';
|
const bitstreamFormatHref = 'rest-api/bitstreamformats';
|
||||||
|
|
||||||
const bitstream = Object.assign(new Bitstream(), {
|
const bitstream1 = Object.assign(new Bitstream(), {
|
||||||
uuid: 'fake-bitstream',
|
id: 'fake-bitstream1',
|
||||||
|
uuid: 'fake-bitstream1',
|
||||||
_links: {
|
_links: {
|
||||||
self: { href: 'fake-bitstream-self' }
|
self: { href: 'fake-bitstream1-self' }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const bitstream2 = Object.assign(new Bitstream(), {
|
||||||
|
id: 'fake-bitstream2',
|
||||||
|
uuid: 'fake-bitstream2',
|
||||||
|
_links: {
|
||||||
|
self: { href: 'fake-bitstream2-self' }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const format = Object.assign(new BitstreamFormat(), {
|
const format = Object.assign(new BitstreamFormat(), {
|
||||||
@@ -50,7 +64,18 @@ describe('BitstreamDataService', () => {
|
|||||||
});
|
});
|
||||||
rdbService = getMockRemoteDataBuildService();
|
rdbService = getMockRemoteDataBuildService();
|
||||||
|
|
||||||
service = new BitstreamDataService(requestService, rdbService, objectCache, halService, null, bitstreamFormatService, null, null);
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
{ provide: ObjectCacheService, useValue: objectCache },
|
||||||
|
{ provide: RequestService, useValue: requestService },
|
||||||
|
{ provide: HALEndpointService, useValue: halService },
|
||||||
|
{ provide: BitstreamFormatDataService, useValue: bitstreamFormatService },
|
||||||
|
{ provide: RemoteDataBuildService, useValue: rdbService },
|
||||||
|
{ provide: DSOChangeAnalyzer, useValue: {} },
|
||||||
|
{ provide: NotificationsService, useValue: {} },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
service = TestBed.inject(BitstreamDataService);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('composition', () => {
|
describe('composition', () => {
|
||||||
@@ -62,11 +87,49 @@ describe('BitstreamDataService', () => {
|
|||||||
|
|
||||||
describe('when updating the bitstream\'s format', () => {
|
describe('when updating the bitstream\'s format', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
service.updateFormat(bitstream, format);
|
service.updateFormat(bitstream1, format);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should send a put request', () => {
|
it('should send a put request', () => {
|
||||||
expect(requestService.send).toHaveBeenCalledWith(jasmine.any(PutRequest));
|
expect(requestService.send).toHaveBeenCalledWith(jasmine.any(PutRequest));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('removeMultiple', () => {
|
||||||
|
function mockBuildFromRequestUUIDAndAwait(requestUUID$: string | Observable<string>, callback: (rd?: RemoteData<any>) => Observable<unknown>, ..._linksToFollow: FollowLinkConfig<any>[]): Observable<RemoteData<any>> {
|
||||||
|
callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(service, 'invalidateByHref');
|
||||||
|
spyOn(rdbService, 'buildFromRequestUUIDAndAwait').and.callFake((requestUUID$: string | Observable<string>, callback: (rd?: RemoteData<any>) => Observable<unknown>, ...linksToFollow: FollowLinkConfig<any>[]) => mockBuildFromRequestUUIDAndAwait(requestUUID$, callback, ...linksToFollow));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be able to 1 bitstream', () => {
|
||||||
|
service.removeMultiple([bitstream1]);
|
||||||
|
|
||||||
|
expect(requestService.send).toHaveBeenCalledWith(objectContaining({
|
||||||
|
href: `${url}/bitstreams`,
|
||||||
|
body: [
|
||||||
|
{ op: 'remove', path: '/bitstreams/fake-bitstream1' },
|
||||||
|
],
|
||||||
|
} as PatchRequest));
|
||||||
|
expect(service.invalidateByHref).toHaveBeenCalledWith('fake-bitstream1-self');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be able to delete multiple bitstreams', () => {
|
||||||
|
service.removeMultiple([bitstream1, bitstream2]);
|
||||||
|
|
||||||
|
expect(requestService.send).toHaveBeenCalledWith(objectContaining({
|
||||||
|
href: `${url}/bitstreams`,
|
||||||
|
body: [
|
||||||
|
{ op: 'remove', path: '/bitstreams/fake-bitstream1' },
|
||||||
|
{ op: 'remove', path: '/bitstreams/fake-bitstream2' },
|
||||||
|
],
|
||||||
|
} as PatchRequest));
|
||||||
|
expect(service.invalidateByHref).toHaveBeenCalledWith('fake-bitstream1-self');
|
||||||
|
expect(service.invalidateByHref).toHaveBeenCalledWith('fake-bitstream2-self');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { HttpHeaders } from '@angular/common/http';
|
import { HttpHeaders } from '@angular/common/http';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||||
import { map, switchMap, take } from 'rxjs/operators';
|
import { find, map, switchMap, take } from 'rxjs/operators';
|
||||||
import { hasValue } from '../../shared/empty.util';
|
import { hasValue } from '../../shared/empty.util';
|
||||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
@@ -14,7 +14,7 @@ import { Item } from '../shared/item.model';
|
|||||||
import { BundleDataService } from './bundle-data.service';
|
import { BundleDataService } from './bundle-data.service';
|
||||||
import { buildPaginatedList, PaginatedList } from './paginated-list.model';
|
import { buildPaginatedList, PaginatedList } from './paginated-list.model';
|
||||||
import { RemoteData } from './remote-data';
|
import { RemoteData } from './remote-data';
|
||||||
import { PutRequest } from './request.models';
|
import { PatchRequest, PutRequest } from './request.models';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
import { BitstreamFormatDataService } from './bitstream-format-data.service';
|
import { BitstreamFormatDataService } from './bitstream-format-data.service';
|
||||||
import { BitstreamFormat } from '../shared/bitstream-format.model';
|
import { BitstreamFormat } from '../shared/bitstream-format.model';
|
||||||
@@ -33,7 +33,7 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
|
|||||||
import { NoContent } from '../shared/NoContent.model';
|
import { NoContent } from '../shared/NoContent.model';
|
||||||
import { IdentifiableDataService } from './base/identifiable-data.service';
|
import { IdentifiableDataService } from './base/identifiable-data.service';
|
||||||
import { dataService } from './base/data-service.decorator';
|
import { dataService } from './base/data-service.decorator';
|
||||||
import { Operation } from 'fast-json-patch';
|
import { Operation, RemoveOperation } from 'fast-json-patch';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A service to retrieve {@link Bitstream}s from the REST API
|
* A service to retrieve {@link Bitstream}s from the REST API
|
||||||
@@ -277,4 +277,34 @@ export class BitstreamDataService extends IdentifiableDataService<Bitstream> imp
|
|||||||
deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
|
deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
|
||||||
return this.deleteData.deleteByHref(href, copyVirtualMetadata);
|
return this.deleteData.deleteByHref(href, copyVirtualMetadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete multiple {@link Bitstream}s at once by sending a PATCH request to the backend
|
||||||
|
*
|
||||||
|
* @param bitstreams The bitstreams that should be removed
|
||||||
|
*/
|
||||||
|
removeMultiple(bitstreams: Bitstream[]): Observable<RemoteData<NoContent>> {
|
||||||
|
const operations: RemoveOperation[] = bitstreams.map((bitstream: Bitstream) => {
|
||||||
|
return {
|
||||||
|
op: 'remove',
|
||||||
|
path: `/bitstreams/${bitstream.id}`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const requestId: string = this.requestService.generateRequestId();
|
||||||
|
|
||||||
|
const hrefObs: Observable<string> = this.halService.getEndpoint(this.linkPath);
|
||||||
|
|
||||||
|
hrefObs.pipe(
|
||||||
|
find((href: string) => hasValue(href)),
|
||||||
|
).subscribe((href: string) => {
|
||||||
|
const request = new PatchRequest(requestId, href, operations);
|
||||||
|
if (hasValue(this.responseMsToLive)) {
|
||||||
|
request.responseMsToLive = this.responseMsToLive;
|
||||||
|
}
|
||||||
|
this.requestService.send(request);
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.rdbService.buildFromRequestUUIDAndAwait(requestId, () => observableCombineLatest(bitstreams.map((bitstream: Bitstream) => this.invalidateByHref(bitstream._links.self.href))));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
64
src/app/core/data/browse-response-parsing.service.spec.ts
Normal file
64
src/app/core/data/browse-response-parsing.service.spec.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { getMockObjectCacheService } from '../../shared/mocks/object-cache.service.mock';
|
||||||
|
import { BrowseResponseParsingService } from './browse-response-parsing.service';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { HIERARCHICAL_BROWSE_DEFINITION } from '../shared/hierarchical-browse-definition.resource-type';
|
||||||
|
import { FLAT_BROWSE_DEFINITION } from '../shared/flat-browse-definition.resource-type';
|
||||||
|
import { VALUE_LIST_BROWSE_DEFINITION } from '../shared/value-list-browse-definition.resource-type';
|
||||||
|
|
||||||
|
class TestService extends BrowseResponseParsingService {
|
||||||
|
constructor(protected objectCache: ObjectCacheService) {
|
||||||
|
super(objectCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overwrite method to make it public for testing
|
||||||
|
public deserialize<ObjectDomain>(obj): any {
|
||||||
|
return super.deserialize(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('BrowseResponseParsingService', () => {
|
||||||
|
let service: TestService;
|
||||||
|
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service = new TestService(getMockObjectCacheService());
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('', () => {
|
||||||
|
const mockFlatBrowse = {
|
||||||
|
id: 'title',
|
||||||
|
browseType: 'flatBrowse',
|
||||||
|
type: 'browse',
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockValueList = {
|
||||||
|
id: 'author',
|
||||||
|
browseType: 'valueList',
|
||||||
|
type: 'browse',
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockHierarchicalBrowse = {
|
||||||
|
id: 'srsc',
|
||||||
|
browseType: 'hierarchicalBrowse',
|
||||||
|
type: 'browse',
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should deserialize flatBrowses correctly', () => {
|
||||||
|
let deserialized = service.deserialize(mockFlatBrowse);
|
||||||
|
expect(deserialized.type).toBe(FLAT_BROWSE_DEFINITION);
|
||||||
|
expect(deserialized.id).toBe(mockFlatBrowse.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should deserialize valueList browses correctly', () => {
|
||||||
|
let deserialized = service.deserialize(mockValueList);
|
||||||
|
expect(deserialized.type).toBe(VALUE_LIST_BROWSE_DEFINITION);
|
||||||
|
expect(deserialized.id).toBe(mockValueList.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should deserialize hierarchicalBrowses correctly', () => {
|
||||||
|
let deserialized = service.deserialize(mockHierarchicalBrowse);
|
||||||
|
expect(deserialized.type).toBe(HIERARCHICAL_BROWSE_DEFINITION);
|
||||||
|
expect(deserialized.id).toBe(mockHierarchicalBrowse.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
48
src/app/core/data/browse-response-parsing.service.ts
Normal file
48
src/app/core/data/browse-response-parsing.service.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { hasValue } from '../../shared/empty.util';
|
||||||
|
import {
|
||||||
|
HIERARCHICAL_BROWSE_DEFINITION
|
||||||
|
} from '../shared/hierarchical-browse-definition.resource-type';
|
||||||
|
import { FLAT_BROWSE_DEFINITION } from '../shared/flat-browse-definition.resource-type';
|
||||||
|
import { HierarchicalBrowseDefinition } from '../shared/hierarchical-browse-definition.model';
|
||||||
|
import { FlatBrowseDefinition } from '../shared/flat-browse-definition.model';
|
||||||
|
import { DspaceRestResponseParsingService } from './dspace-rest-response-parsing.service';
|
||||||
|
import { Serializer } from '../serializer';
|
||||||
|
import { BrowseDefinition } from '../shared/browse-definition.model';
|
||||||
|
import { BROWSE_DEFINITION } from '../shared/browse-definition.resource-type';
|
||||||
|
import { ValueListBrowseDefinition } from '../shared/value-list-browse-definition.model';
|
||||||
|
import { VALUE_LIST_BROWSE_DEFINITION } from '../shared/value-list-browse-definition.resource-type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A ResponseParsingService used to parse a REST API response to a BrowseDefinition object
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class BrowseResponseParsingService extends DspaceRestResponseParsingService {
|
||||||
|
constructor(
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
) {
|
||||||
|
super(objectCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected deserialize<ObjectDomain>(obj): any {
|
||||||
|
const browseType: string = obj.browseType;
|
||||||
|
if (obj.type === BROWSE_DEFINITION.value && hasValue(browseType)) {
|
||||||
|
let serializer: Serializer<BrowseDefinition>;
|
||||||
|
if (browseType === HIERARCHICAL_BROWSE_DEFINITION.value) {
|
||||||
|
serializer = new this.serializerConstructor(HierarchicalBrowseDefinition);
|
||||||
|
} else if (browseType === FLAT_BROWSE_DEFINITION.value) {
|
||||||
|
serializer = new this.serializerConstructor(FlatBrowseDefinition);
|
||||||
|
} else if (browseType === VALUE_LIST_BROWSE_DEFINITION.value) {
|
||||||
|
serializer = new this.serializerConstructor(ValueListBrowseDefinition);
|
||||||
|
} else {
|
||||||
|
throw new Error('An error occurred while retrieving the browse definitions.');
|
||||||
|
}
|
||||||
|
return serializer.deserialize(obj);
|
||||||
|
} else {
|
||||||
|
throw new Error('An error occurred while retrieving the browse definitions.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -10,6 +10,10 @@ import { hasNoValue, hasValue } from '../../shared/empty.util';
|
|||||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||||
import { RestRequest } from './rest-request.model';
|
import { RestRequest } from './rest-request.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use DspaceRestResponseParsingService for new code, this is only left to support a
|
||||||
|
* few legacy use cases, and should get removed eventually
|
||||||
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DSOResponseParsingService extends BaseResponseParsingService implements ResponseParsingService {
|
export class DSOResponseParsingService extends BaseResponseParsingService implements ResponseParsingService {
|
||||||
protected toCache = true;
|
protected toCache = true;
|
||||||
|
@@ -11,6 +11,7 @@ import { TaskResponseParsingService } from '../tasks/task-response-parsing.servi
|
|||||||
import { ContentSourceResponseParsingService } from './content-source-response-parsing.service';
|
import { ContentSourceResponseParsingService } from './content-source-response-parsing.service';
|
||||||
import { RestRequestWithResponseParser } from './rest-request-with-response-parser.model';
|
import { RestRequestWithResponseParser } from './rest-request-with-response-parser.model';
|
||||||
import { DspaceRestResponseParsingService } from './dspace-rest-response-parsing.service';
|
import { DspaceRestResponseParsingService } from './dspace-rest-response-parsing.service';
|
||||||
|
import { BrowseResponseParsingService } from './browse-response-parsing.service';
|
||||||
import { FindListOptions } from './find-list-options.model';
|
import { FindListOptions } from './find-list-options.model';
|
||||||
|
|
||||||
|
|
||||||
@@ -118,6 +119,15 @@ export class PatchRequest extends DSpaceRestRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing a BrowseDefinition HTTP Rest request object
|
||||||
|
*/
|
||||||
|
export class BrowseDefinitionRestRequest extends DSpaceRestRequest {
|
||||||
|
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||||
|
return BrowseResponseParsingService;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class FindListRequest extends GetRequest {
|
export class FindListRequest extends GetRequest {
|
||||||
constructor(
|
constructor(
|
||||||
uuid: string,
|
uuid: string,
|
||||||
|
@@ -331,7 +331,7 @@ export class RequestService {
|
|||||||
map((request: RequestEntry) => isStale(request.state)),
|
map((request: RequestEntry) => isStale(request.state)),
|
||||||
filter((stale: boolean) => stale),
|
filter((stale: boolean) => stale),
|
||||||
take(1),
|
take(1),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
97
src/app/core/data/signposting-data.service.spec.ts
Normal file
97
src/app/core/data/signposting-data.service.spec.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import { fakeAsync, TestBed, tick } from '@angular/core/testing';
|
||||||
|
import { SignpostingDataService } from './signposting-data.service';
|
||||||
|
import { DspaceRestService } from '../dspace-rest/dspace-rest.service';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
import { SignpostingLink } from './signposting-links.model';
|
||||||
|
|
||||||
|
describe('SignpostingDataService', () => {
|
||||||
|
let service: SignpostingDataService;
|
||||||
|
let restServiceSpy: jasmine.SpyObj<DspaceRestService>;
|
||||||
|
let halServiceSpy: jasmine.SpyObj<HALEndpointService>;
|
||||||
|
const mocklink = {
|
||||||
|
href: 'http://test.org',
|
||||||
|
rel: 'test',
|
||||||
|
type: 'test'
|
||||||
|
};
|
||||||
|
|
||||||
|
const mocklink2 = {
|
||||||
|
href: 'http://test2.org',
|
||||||
|
rel: 'test',
|
||||||
|
type: 'test'
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockResponse: any = {
|
||||||
|
statusCode: 200,
|
||||||
|
payload: [mocklink, mocklink2]
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockErrResponse: any = {
|
||||||
|
statusCode: 500
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const restSpy = jasmine.createSpyObj('DspaceRestService', ['get', 'getWithHeaders']);
|
||||||
|
const halSpy = jasmine.createSpyObj('HALEndpointService', ['getRootHref']);
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
SignpostingDataService,
|
||||||
|
{ provide: DspaceRestService, useValue: restSpy },
|
||||||
|
{ provide: HALEndpointService, useValue: halSpy }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
service = TestBed.inject(SignpostingDataService);
|
||||||
|
restServiceSpy = TestBed.inject(DspaceRestService) as jasmine.SpyObj<DspaceRestService>;
|
||||||
|
halServiceSpy = TestBed.inject(HALEndpointService) as jasmine.SpyObj<HALEndpointService>;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return signposting links', fakeAsync(() => {
|
||||||
|
const uuid = '123';
|
||||||
|
const baseUrl = 'http://localhost:8080';
|
||||||
|
|
||||||
|
halServiceSpy.getRootHref.and.returnValue(`${baseUrl}/api`);
|
||||||
|
|
||||||
|
restServiceSpy.get.and.returnValue(of(mockResponse));
|
||||||
|
|
||||||
|
let result: SignpostingLink[];
|
||||||
|
|
||||||
|
const expectedResult: SignpostingLink[] = [mocklink, mocklink2];
|
||||||
|
|
||||||
|
service.getLinks(uuid).subscribe((links) => {
|
||||||
|
result = links;
|
||||||
|
});
|
||||||
|
|
||||||
|
tick();
|
||||||
|
|
||||||
|
expect(result).toEqual(expectedResult);
|
||||||
|
expect(halServiceSpy.getRootHref).toHaveBeenCalled();
|
||||||
|
expect(restServiceSpy.get).toHaveBeenCalledWith(`${baseUrl}/signposting/links/${uuid}`);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should handle error and return an empty array', fakeAsync(() => {
|
||||||
|
const uuid = '123';
|
||||||
|
const baseUrl = 'http://localhost:8080';
|
||||||
|
|
||||||
|
halServiceSpy.getRootHref.and.returnValue(`${baseUrl}/api`);
|
||||||
|
|
||||||
|
restServiceSpy.get.and.returnValue(of(mockErrResponse));
|
||||||
|
|
||||||
|
let result: any;
|
||||||
|
|
||||||
|
service.getLinks(uuid).subscribe((data) => {
|
||||||
|
result = data;
|
||||||
|
});
|
||||||
|
|
||||||
|
tick();
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
expect(halServiceSpy.getRootHref).toHaveBeenCalled();
|
||||||
|
expect(restServiceSpy.get).toHaveBeenCalledWith(`${baseUrl}/signposting/links/${uuid}`);
|
||||||
|
}));
|
||||||
|
});
|
38
src/app/core/data/signposting-data.service.ts
Normal file
38
src/app/core/data/signposting-data.service.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { catchError, map } from 'rxjs/operators';
|
||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
import { DspaceRestService } from '../dspace-rest/dspace-rest.service';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { RawRestResponse } from '../dspace-rest/raw-rest-response.model';
|
||||||
|
import { SignpostingLink } from './signposting-links.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service responsible for handling requests related to the Signposting endpoint
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class SignpostingDataService {
|
||||||
|
|
||||||
|
constructor(private restService: DspaceRestService, protected halService: HALEndpointService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the list of signposting links related to the given resource's id
|
||||||
|
*
|
||||||
|
* @param uuid
|
||||||
|
*/
|
||||||
|
getLinks(uuid: string): Observable<SignpostingLink[]> {
|
||||||
|
const baseUrl = this.halService.getRootHref().replace('/api', '');
|
||||||
|
|
||||||
|
return this.restService.get(`${baseUrl}/signposting/links/${uuid}`).pipe(
|
||||||
|
catchError((err) => {
|
||||||
|
return observableOf([]);
|
||||||
|
}),
|
||||||
|
map((res: RawRestResponse) => res.statusCode === 200 ? res.payload as SignpostingLink[] : [])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
8
src/app/core/data/signposting-links.model.ts
Normal file
8
src/app/core/data/signposting-links.model.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* Represents the link object received by the signposting endpoint
|
||||||
|
*/
|
||||||
|
export interface SignpostingLink {
|
||||||
|
href?: string,
|
||||||
|
rel?: string,
|
||||||
|
type?: string
|
||||||
|
}
|
@@ -46,6 +46,7 @@ export class ServerHardRedirectService extends HardRedirectService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Redirecting from ${this.req.url} to ${url} with ${status}`);
|
console.log(`Redirecting from ${this.req.url} to ${url} with ${status}`);
|
||||||
|
|
||||||
this.res.redirect(status, url);
|
this.res.redirect(status, url);
|
||||||
this.res.end();
|
this.res.end();
|
||||||
// I haven't found a way to correctly stop Angular rendering.
|
// I haven't found a way to correctly stop Angular rendering.
|
||||||
|
@@ -1,7 +1,11 @@
|
|||||||
import { RESPONSE } from '@nguniversal/express-engine/tokens';
|
import { RESPONSE } from '@nguniversal/express-engine/tokens';
|
||||||
import { Inject, Injectable, Optional } from '@angular/core';
|
import { Inject, Injectable, Optional } from '@angular/core';
|
||||||
|
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service responsible to provide method to manage the response object
|
||||||
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ServerResponseService {
|
export class ServerResponseService {
|
||||||
private response: Response;
|
private response: Response;
|
||||||
@@ -10,6 +14,12 @@ export class ServerResponseService {
|
|||||||
this.response = response;
|
this.response = response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a status code to response
|
||||||
|
*
|
||||||
|
* @param code
|
||||||
|
* @param message
|
||||||
|
*/
|
||||||
setStatus(code: number, message?: string): this {
|
setStatus(code: number, message?: string): this {
|
||||||
if (this.response) {
|
if (this.response) {
|
||||||
this.response.statusCode = code;
|
this.response.statusCode = code;
|
||||||
@@ -20,19 +30,51 @@ export class ServerResponseService {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set Unauthorized status
|
||||||
|
*
|
||||||
|
* @param message
|
||||||
|
*/
|
||||||
setUnauthorized(message = 'Unauthorized'): this {
|
setUnauthorized(message = 'Unauthorized'): this {
|
||||||
return this.setStatus(401, message);
|
return this.setStatus(401, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set Forbidden status
|
||||||
|
*
|
||||||
|
* @param message
|
||||||
|
*/
|
||||||
setForbidden(message = 'Forbidden'): this {
|
setForbidden(message = 'Forbidden'): this {
|
||||||
return this.setStatus(403, message);
|
return this.setStatus(403, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set Not found status
|
||||||
|
*
|
||||||
|
* @param message
|
||||||
|
*/
|
||||||
setNotFound(message = 'Not found'): this {
|
setNotFound(message = 'Not found'): this {
|
||||||
return this.setStatus(404, message);
|
return this.setStatus(404, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set Internal Server Error status
|
||||||
|
*
|
||||||
|
* @param message
|
||||||
|
*/
|
||||||
setInternalServerError(message = 'Internal Server Error'): this {
|
setInternalServerError(message = 'Internal Server Error'): this {
|
||||||
return this.setStatus(500, message);
|
return this.setStatus(500, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a response's header
|
||||||
|
*
|
||||||
|
* @param header
|
||||||
|
* @param content
|
||||||
|
*/
|
||||||
|
setHeader(header: string, content: string) {
|
||||||
|
if (this.response) {
|
||||||
|
this.response.setHeader(header, content);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,50 +1,16 @@
|
|||||||
import { autoserialize, autoserializeAs, deserialize } from 'cerialize';
|
import { autoserialize } from 'cerialize';
|
||||||
import { typedObject } from '../cache/builders/build-decorators';
|
|
||||||
import { excludeFromEquals } from '../utilities/equals.decorators';
|
|
||||||
import { BROWSE_DEFINITION } from './browse-definition.resource-type';
|
|
||||||
import { HALLink } from './hal-link.model';
|
|
||||||
import { ResourceType } from './resource-type';
|
|
||||||
import { SortOption } from './sort-option.model';
|
|
||||||
import { CacheableObject } from '../cache/cacheable-object.model';
|
import { CacheableObject } from '../cache/cacheable-object.model';
|
||||||
import { BrowseByDataType } from '../../browse-by/browse-by-switcher/browse-by-decorator';
|
|
||||||
|
|
||||||
@typedObject
|
/**
|
||||||
export class BrowseDefinition extends CacheableObject {
|
* Base class for BrowseDefinition models
|
||||||
static type = BROWSE_DEFINITION;
|
*/
|
||||||
|
export abstract class BrowseDefinition extends CacheableObject {
|
||||||
/**
|
|
||||||
* The object type
|
|
||||||
*/
|
|
||||||
@excludeFromEquals
|
|
||||||
@autoserialize
|
|
||||||
type: ResourceType;
|
|
||||||
|
|
||||||
@autoserialize
|
@autoserialize
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
@autoserialize
|
/**
|
||||||
metadataBrowse: boolean;
|
* Get the render type of the BrowseDefinition model
|
||||||
|
*/
|
||||||
@autoserialize
|
abstract getRenderType(): string;
|
||||||
sortOptions: SortOption[];
|
|
||||||
|
|
||||||
@autoserializeAs('order')
|
|
||||||
defaultSortOrder: string;
|
|
||||||
|
|
||||||
@autoserializeAs('metadata')
|
|
||||||
metadataKeys: string[];
|
|
||||||
|
|
||||||
@autoserialize
|
|
||||||
dataType: BrowseByDataType;
|
|
||||||
|
|
||||||
get self(): string {
|
|
||||||
return this._links.self.href;
|
|
||||||
}
|
|
||||||
|
|
||||||
@deserialize
|
|
||||||
_links: {
|
|
||||||
self: HALLink;
|
|
||||||
entries: HALLink;
|
|
||||||
items: HALLink;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
36
src/app/core/shared/flat-browse-definition.model.ts
Normal file
36
src/app/core/shared/flat-browse-definition.model.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { inheritSerialization, deserialize } from 'cerialize';
|
||||||
|
import { typedObject } from '../cache/builders/build-decorators';
|
||||||
|
import { excludeFromEquals } from '../utilities/equals.decorators';
|
||||||
|
import { FLAT_BROWSE_DEFINITION } from './flat-browse-definition.resource-type';
|
||||||
|
import { ResourceType } from './resource-type';
|
||||||
|
import { NonHierarchicalBrowseDefinition } from './non-hierarchical-browse-definition';
|
||||||
|
import { HALLink } from './hal-link.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BrowseDefinition model for browses of type 'flatBrowse'
|
||||||
|
*/
|
||||||
|
@typedObject
|
||||||
|
@inheritSerialization(NonHierarchicalBrowseDefinition)
|
||||||
|
export class FlatBrowseDefinition extends NonHierarchicalBrowseDefinition {
|
||||||
|
static type = FLAT_BROWSE_DEFINITION;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The object type
|
||||||
|
*/
|
||||||
|
@excludeFromEquals
|
||||||
|
type: ResourceType = FLAT_BROWSE_DEFINITION;
|
||||||
|
|
||||||
|
get self(): string {
|
||||||
|
return this._links.self.href;
|
||||||
|
}
|
||||||
|
|
||||||
|
@deserialize
|
||||||
|
_links: {
|
||||||
|
self: HALLink;
|
||||||
|
items: HALLink;
|
||||||
|
};
|
||||||
|
|
||||||
|
getRenderType(): string {
|
||||||
|
return this.dataType;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,9 @@
|
|||||||
|
import { ResourceType } from './resource-type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The resource type for FlatBrowseDefinition
|
||||||
|
*
|
||||||
|
* Needs to be in a separate file to prevent circular
|
||||||
|
* dependencies in webpack.
|
||||||
|
*/
|
||||||
|
export const FLAT_BROWSE_DEFINITION = new ResourceType('flatBrowse');
|
45
src/app/core/shared/hierarchical-browse-definition.model.ts
Normal file
45
src/app/core/shared/hierarchical-browse-definition.model.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { autoserialize, autoserializeAs, deserialize, inheritSerialization } from 'cerialize';
|
||||||
|
import { typedObject } from '../cache/builders/build-decorators';
|
||||||
|
import { excludeFromEquals } from '../utilities/equals.decorators';
|
||||||
|
import { HIERARCHICAL_BROWSE_DEFINITION } from './hierarchical-browse-definition.resource-type';
|
||||||
|
import { HALLink } from './hal-link.model';
|
||||||
|
import { ResourceType } from './resource-type';
|
||||||
|
import { BrowseDefinition } from './browse-definition.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BrowseDefinition model for browses of type 'hierarchicalBrowse'
|
||||||
|
*/
|
||||||
|
@typedObject
|
||||||
|
@inheritSerialization(BrowseDefinition)
|
||||||
|
export class HierarchicalBrowseDefinition extends BrowseDefinition {
|
||||||
|
static type = HIERARCHICAL_BROWSE_DEFINITION;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The object type
|
||||||
|
*/
|
||||||
|
@excludeFromEquals
|
||||||
|
type: ResourceType = HIERARCHICAL_BROWSE_DEFINITION;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
facetType: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
vocabulary: string;
|
||||||
|
|
||||||
|
@autoserializeAs('metadata')
|
||||||
|
metadataKeys: string[];
|
||||||
|
|
||||||
|
get self(): string {
|
||||||
|
return this._links.self.href;
|
||||||
|
}
|
||||||
|
|
||||||
|
@deserialize
|
||||||
|
_links: {
|
||||||
|
self: HALLink;
|
||||||
|
vocabulary: HALLink;
|
||||||
|
};
|
||||||
|
|
||||||
|
getRenderType(): string {
|
||||||
|
return 'hierarchy';
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,9 @@
|
|||||||
|
import { ResourceType } from './resource-type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The resource type for HierarchicalBrowseDefinition
|
||||||
|
*
|
||||||
|
* Needs to be in a separate file to prevent circular
|
||||||
|
* dependencies in webpack.
|
||||||
|
*/
|
||||||
|
export const HIERARCHICAL_BROWSE_DEFINITION = new ResourceType('hierarchicalBrowse');
|
24
src/app/core/shared/non-hierarchical-browse-definition.ts
Normal file
24
src/app/core/shared/non-hierarchical-browse-definition.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
|
||||||
|
import { SortOption } from './sort-option.model';
|
||||||
|
import { BrowseByDataType } from '../../browse-by/browse-by-switcher/browse-by-decorator';
|
||||||
|
import { BrowseDefinition } from './browse-definition.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Super class for NonHierarchicalBrowseDefinition models,
|
||||||
|
* e.g. FlatBrowseDefinition and ValueListBrowseDefinition
|
||||||
|
*/
|
||||||
|
@inheritSerialization(BrowseDefinition)
|
||||||
|
export abstract class NonHierarchicalBrowseDefinition extends BrowseDefinition {
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
sortOptions: SortOption[];
|
||||||
|
|
||||||
|
@autoserializeAs('order')
|
||||||
|
defaultSortOrder: string;
|
||||||
|
|
||||||
|
@autoserializeAs('metadata')
|
||||||
|
metadataKeys: string[];
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
dataType: BrowseByDataType;
|
||||||
|
}
|
36
src/app/core/shared/value-list-browse-definition.model.ts
Normal file
36
src/app/core/shared/value-list-browse-definition.model.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { inheritSerialization, deserialize } from 'cerialize';
|
||||||
|
import { typedObject } from '../cache/builders/build-decorators';
|
||||||
|
import { excludeFromEquals } from '../utilities/equals.decorators';
|
||||||
|
import { VALUE_LIST_BROWSE_DEFINITION } from './value-list-browse-definition.resource-type';
|
||||||
|
import { ResourceType } from './resource-type';
|
||||||
|
import { NonHierarchicalBrowseDefinition } from './non-hierarchical-browse-definition';
|
||||||
|
import { HALLink } from './hal-link.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BrowseDefinition model for browses of type 'valueList'
|
||||||
|
*/
|
||||||
|
@typedObject
|
||||||
|
@inheritSerialization(NonHierarchicalBrowseDefinition)
|
||||||
|
export class ValueListBrowseDefinition extends NonHierarchicalBrowseDefinition {
|
||||||
|
static type = VALUE_LIST_BROWSE_DEFINITION;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The object type
|
||||||
|
*/
|
||||||
|
@excludeFromEquals
|
||||||
|
type: ResourceType = VALUE_LIST_BROWSE_DEFINITION;
|
||||||
|
|
||||||
|
get self(): string {
|
||||||
|
return this._links.self.href;
|
||||||
|
}
|
||||||
|
|
||||||
|
@deserialize
|
||||||
|
_links: {
|
||||||
|
self: HALLink;
|
||||||
|
entries: HALLink;
|
||||||
|
};
|
||||||
|
|
||||||
|
getRenderType(): string {
|
||||||
|
return this.dataType;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,9 @@
|
|||||||
|
import { ResourceType } from './resource-type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The resource type for ValueListBrowseDefinition
|
||||||
|
*
|
||||||
|
* Needs to be in a separate file to prevent circular
|
||||||
|
* dependencies in webpack.
|
||||||
|
*/
|
||||||
|
export const VALUE_LIST_BROWSE_DEFINITION = new ResourceType('valueList');
|
@@ -223,13 +223,15 @@ export class VocabularyService {
|
|||||||
* no valid cached version. Defaults to true
|
* no valid cached version. Defaults to true
|
||||||
* @param reRequestOnStale Whether or not the request should automatically be re-
|
* @param reRequestOnStale Whether or not the request should automatically be re-
|
||||||
* requested after the response becomes stale
|
* requested after the response becomes stale
|
||||||
|
* @param constructId Whether constructing the full vocabularyDetail ID
|
||||||
|
* ({vocabularyName}:{detailName}) is still necessary
|
||||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
|
||||||
* {@link HALLink}s should be automatically resolved
|
* {@link HALLink}s should be automatically resolved
|
||||||
* @return {Observable<RemoteData<VocabularyEntryDetail>>}
|
* @return {Observable<RemoteData<VocabularyEntryDetail>>}
|
||||||
* Return an observable that emits VocabularyEntryDetail object
|
* Return an observable that emits VocabularyEntryDetail object
|
||||||
*/
|
*/
|
||||||
findEntryDetailById(id: string, name: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<VocabularyEntryDetail>[]): Observable<RemoteData<VocabularyEntryDetail>> {
|
findEntryDetailById(id: string, name: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, constructId: boolean = true, ...linksToFollow: FollowLinkConfig<VocabularyEntryDetail>[]): Observable<RemoteData<VocabularyEntryDetail>> {
|
||||||
const findId = `${name}:${id}`;
|
const findId: string = (constructId ? `${name}:${id}` : id);
|
||||||
return this.vocabularyEntryDetailDataService.findById(findId, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
return this.vocabularyEntryDetailDataService.findById(findId, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -12,6 +12,7 @@ import { createPaginatedList } from '../../../shared/testing/utils.test';
|
|||||||
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
|
||||||
|
|
||||||
describe('MetadataFieldSelectorComponent', () => {
|
describe('MetadataFieldSelectorComponent', () => {
|
||||||
let component: MetadataFieldSelectorComponent;
|
let component: MetadataFieldSelectorComponent;
|
||||||
@@ -79,7 +80,7 @@ describe('MetadataFieldSelectorComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should query the registry service for metadata fields and include the schema', () => {
|
it('should query the registry service for metadata fields and include the schema', () => {
|
||||||
expect(registryService.queryMetadataFields).toHaveBeenCalledWith(query, null, true, false, followLink('schema'));
|
expect(registryService.queryMetadataFields).toHaveBeenCalledWith(query, { elementsPerPage: 10, sort: new SortOptions('fieldName', SortDirection.ASC) }, true, false, followLink('schema'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -9,10 +9,11 @@ import {
|
|||||||
Output,
|
Output,
|
||||||
ViewChild
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { switchMap, debounceTime, distinctUntilChanged, map, tap, take } from 'rxjs/operators';
|
import { debounceTime, distinctUntilChanged, map, switchMap, take, tap } from 'rxjs/operators';
|
||||||
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||||
import {
|
import {
|
||||||
getAllSucceededRemoteData, getFirstCompletedRemoteData,
|
getAllSucceededRemoteData,
|
||||||
|
getFirstCompletedRemoteData,
|
||||||
metadataFieldsToString
|
metadataFieldsToString
|
||||||
} from '../../../core/shared/operators';
|
} from '../../../core/shared/operators';
|
||||||
import { Observable } from 'rxjs/internal/Observable';
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
@@ -24,6 +25,7 @@ import { Subscription } from 'rxjs/internal/Subscription';
|
|||||||
import { of } from 'rxjs/internal/observable/of';
|
import { of } from 'rxjs/internal/observable/of';
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-metadata-field-selector',
|
selector: 'ds-metadata-field-selector',
|
||||||
@@ -127,7 +129,7 @@ export class MetadataFieldSelectorComponent implements OnInit, OnDestroy, AfterV
|
|||||||
switchMap((query: string) => {
|
switchMap((query: string) => {
|
||||||
this.showInvalid = false;
|
this.showInvalid = false;
|
||||||
if (query !== null) {
|
if (query !== null) {
|
||||||
return this.registryService.queryMetadataFields(query, null, true, false, followLink('schema')).pipe(
|
return this.registryService.queryMetadataFields(query, { elementsPerPage: 10, sort: new SortOptions('fieldName', SortDirection.ASC) }, true, false, followLink('schema')).pipe(
|
||||||
getAllSucceededRemoteData(),
|
getAllSucceededRemoteData(),
|
||||||
metadataFieldsToString(),
|
metadataFieldsToString(),
|
||||||
);
|
);
|
||||||
|
@@ -25,6 +25,7 @@ import { getMockRequestService } from '../../../shared/mocks/request.service.moc
|
|||||||
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||||
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||||
import { FieldChangeType } from '../../../core/data/object-updates/field-change-type.model';
|
import { FieldChangeType } from '../../../core/data/object-updates/field-change-type.model';
|
||||||
|
import { BitstreamDataServiceStub } from '../../../shared/testing/bitstream-data-service.stub';
|
||||||
|
|
||||||
let comp: ItemBitstreamsComponent;
|
let comp: ItemBitstreamsComponent;
|
||||||
let fixture: ComponentFixture<ItemBitstreamsComponent>;
|
let fixture: ComponentFixture<ItemBitstreamsComponent>;
|
||||||
@@ -71,7 +72,7 @@ let objectUpdatesService: ObjectUpdatesService;
|
|||||||
let router: any;
|
let router: any;
|
||||||
let route: ActivatedRoute;
|
let route: ActivatedRoute;
|
||||||
let notificationsService: NotificationsService;
|
let notificationsService: NotificationsService;
|
||||||
let bitstreamService: BitstreamDataService;
|
let bitstreamService: BitstreamDataServiceStub;
|
||||||
let objectCache: ObjectCacheService;
|
let objectCache: ObjectCacheService;
|
||||||
let requestService: RequestService;
|
let requestService: RequestService;
|
||||||
let searchConfig: SearchConfigurationService;
|
let searchConfig: SearchConfigurationService;
|
||||||
@@ -112,9 +113,7 @@ describe('ItemBitstreamsComponent', () => {
|
|||||||
success: successNotification
|
success: successNotification
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
bitstreamService = jasmine.createSpyObj('bitstreamService', {
|
bitstreamService = new BitstreamDataServiceStub();
|
||||||
delete: jasmine.createSpy('delete')
|
|
||||||
});
|
|
||||||
objectCache = jasmine.createSpyObj('objectCache', {
|
objectCache = jasmine.createSpyObj('objectCache', {
|
||||||
remove: jasmine.createSpy('remove')
|
remove: jasmine.createSpy('remove')
|
||||||
});
|
});
|
||||||
@@ -179,15 +178,16 @@ describe('ItemBitstreamsComponent', () => {
|
|||||||
|
|
||||||
describe('when submit is called', () => {
|
describe('when submit is called', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
spyOn(bitstreamService, 'removeMultiple').and.callThrough();
|
||||||
comp.submit();
|
comp.submit();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call delete on the bitstreamService for the marked field', () => {
|
it('should call removeMultiple on the bitstreamService for the marked field', () => {
|
||||||
expect(bitstreamService.delete).toHaveBeenCalledWith(bitstream2.id);
|
expect(bitstreamService.removeMultiple).toHaveBeenCalledWith([bitstream2]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not call delete on the bitstreamService for the unmarked field', () => {
|
it('should not call removeMultiple on the bitstreamService for the unmarked field', () => {
|
||||||
expect(bitstreamService.delete).not.toHaveBeenCalledWith(bitstream1.id);
|
expect(bitstreamService.removeMultiple).not.toHaveBeenCalledWith([bitstream1]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -210,7 +210,6 @@ describe('ItemBitstreamsComponent', () => {
|
|||||||
comp.dropBitstream(bundle, {
|
comp.dropBitstream(bundle, {
|
||||||
fromIndex: 0,
|
fromIndex: 0,
|
||||||
toIndex: 50,
|
toIndex: 50,
|
||||||
// eslint-disable-next-line no-empty, @typescript-eslint/no-empty-function
|
|
||||||
finish: () => {
|
finish: () => {
|
||||||
done();
|
done();
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { ChangeDetectorRef, Component, NgZone, OnDestroy } from '@angular/core';
|
import { ChangeDetectorRef, Component, NgZone, OnDestroy } from '@angular/core';
|
||||||
import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
|
import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
|
||||||
import { filter, map, switchMap, take } from 'rxjs/operators';
|
import { filter, map, switchMap, take } from 'rxjs/operators';
|
||||||
import { Observable, of as observableOf, Subscription, zip as observableZip } from 'rxjs';
|
import { Observable, Subscription, zip as observableZip } from 'rxjs';
|
||||||
import { ItemDataService } from '../../../core/data/item-data.service';
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
@@ -133,20 +133,16 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Send out delete requests for all deleted bitstreams
|
// Send out delete requests for all deleted bitstreams
|
||||||
const removedResponses$ = removedBitstreams$.pipe(
|
const removedResponses$: Observable<RemoteData<NoContent>> = removedBitstreams$.pipe(
|
||||||
take(1),
|
take(1),
|
||||||
switchMap((removedBistreams: Bitstream[]) => {
|
switchMap((removedBitstreams: Bitstream[]) => {
|
||||||
if (isNotEmpty(removedBistreams)) {
|
return this.bitstreamService.removeMultiple(removedBitstreams);
|
||||||
return observableZip(...removedBistreams.map((bitstream: Bitstream) => this.bitstreamService.delete(bitstream.id)));
|
|
||||||
} else {
|
|
||||||
return observableOf(undefined);
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Perform the setup actions from above in order and display notifications
|
// Perform the setup actions from above in order and display notifications
|
||||||
removedResponses$.pipe(take(1)).subscribe((responses: RemoteData<NoContent>[]) => {
|
removedResponses$.subscribe((responses: RemoteData<NoContent>) => {
|
||||||
this.displayNotifications('item.edit.bitstreams.notifications.remove', responses);
|
this.displayNotifications('item.edit.bitstreams.notifications.remove', [responses]);
|
||||||
this.submitting = false;
|
this.submitting = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -3,6 +3,7 @@ import { MetadataValue } from '../../../core/shared/metadata.models';
|
|||||||
import { APP_CONFIG, AppConfig } from '../../../../config/app-config.interface';
|
import { APP_CONFIG, AppConfig } from '../../../../config/app-config.interface';
|
||||||
import { BrowseDefinition } from '../../../core/shared/browse-definition.model';
|
import { BrowseDefinition } from '../../../core/shared/browse-definition.model';
|
||||||
import { hasValue } from '../../../shared/empty.util';
|
import { hasValue } from '../../../shared/empty.util';
|
||||||
|
import { VALUE_LIST_BROWSE_DEFINITION } from '../../../core/shared/value-list-browse-definition.resource-type';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders the configured 'values' into the ds-metadata-field-wrapper component.
|
* This component renders the configured 'values' into the ds-metadata-field-wrapper component.
|
||||||
@@ -84,7 +85,7 @@ export class MetadataValuesComponent implements OnChanges {
|
|||||||
*/
|
*/
|
||||||
getQueryParams(value) {
|
getQueryParams(value) {
|
||||||
let queryParams = {startsWith: value};
|
let queryParams = {startsWith: value};
|
||||||
if (this.browseDefinition.metadataBrowse) {
|
if (this.browseDefinition.getRenderType() === VALUE_LIST_BROWSE_DEFINITION.value) {
|
||||||
return {value: value};
|
return {value: value};
|
||||||
}
|
}
|
||||||
return queryParams;
|
return queryParams;
|
||||||
|
@@ -2,7 +2,7 @@ import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/cor
|
|||||||
import { ItemDataService } from '../../core/data/item-data.service';
|
import { ItemDataService } from '../../core/data/item-data.service';
|
||||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
|
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
|
||||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA, PLATFORM_ID } from '@angular/core';
|
||||||
import { TruncatePipe } from '../../shared/utils/truncate.pipe';
|
import { TruncatePipe } from '../../shared/utils/truncate.pipe';
|
||||||
import { FullItemPageComponent } from './full-item-page.component';
|
import { FullItemPageComponent } from './full-item-page.component';
|
||||||
import { MetadataService } from '../../core/metadata/metadata.service';
|
import { MetadataService } from '../../core/metadata/metadata.service';
|
||||||
@@ -20,6 +20,9 @@ import { createPaginatedList } from '../../shared/testing/utils.test';
|
|||||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
import { createRelationshipsObservable } from '../simple/item-types/shared/item.component.spec';
|
import { createRelationshipsObservable } from '../simple/item-types/shared/item.component.spec';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { ServerResponseService } from '../../core/services/server-response.service';
|
||||||
|
import { SignpostingDataService } from '../../core/data/signposting-data.service';
|
||||||
|
import { LinkHeadService } from '../../core/services/link-head.service';
|
||||||
|
|
||||||
const mockItem: Item = Object.assign(new Item(), {
|
const mockItem: Item = Object.assign(new Item(), {
|
||||||
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||||
@@ -55,8 +58,21 @@ describe('FullItemPageComponent', () => {
|
|||||||
let routeStub: ActivatedRouteStub;
|
let routeStub: ActivatedRouteStub;
|
||||||
let routeData;
|
let routeData;
|
||||||
let authorizationDataService: AuthorizationDataService;
|
let authorizationDataService: AuthorizationDataService;
|
||||||
|
let serverResponseService: jasmine.SpyObj<ServerResponseService>;
|
||||||
|
let signpostingDataService: jasmine.SpyObj<SignpostingDataService>;
|
||||||
|
let linkHeadService: jasmine.SpyObj<LinkHeadService>;
|
||||||
|
|
||||||
|
const mocklink = {
|
||||||
|
href: 'http://test.org',
|
||||||
|
rel: 'test',
|
||||||
|
type: 'test'
|
||||||
|
};
|
||||||
|
|
||||||
|
const mocklink2 = {
|
||||||
|
href: 'http://test2.org',
|
||||||
|
rel: 'test',
|
||||||
|
type: 'test'
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
authService = jasmine.createSpyObj('authService', {
|
authService = jasmine.createSpyObj('authService', {
|
||||||
@@ -76,6 +92,19 @@ describe('FullItemPageComponent', () => {
|
|||||||
isAuthorized: observableOf(false),
|
isAuthorized: observableOf(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
serverResponseService = jasmine.createSpyObj('ServerResponseService', {
|
||||||
|
setHeader: jasmine.createSpy('setHeader'),
|
||||||
|
});
|
||||||
|
|
||||||
|
signpostingDataService = jasmine.createSpyObj('SignpostingDataService', {
|
||||||
|
getLinks: observableOf([mocklink, mocklink2]),
|
||||||
|
});
|
||||||
|
|
||||||
|
linkHeadService = jasmine.createSpyObj('LinkHeadService', {
|
||||||
|
addTag: jasmine.createSpy('setHeader'),
|
||||||
|
removeTag: jasmine.createSpy('removeTag'),
|
||||||
|
});
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot({
|
imports: [TranslateModule.forRoot({
|
||||||
loader: {
|
loader: {
|
||||||
@@ -90,8 +119,11 @@ describe('FullItemPageComponent', () => {
|
|||||||
{ provide: MetadataService, useValue: metadataServiceStub },
|
{ provide: MetadataService, useValue: metadataServiceStub },
|
||||||
{ provide: AuthService, useValue: authService },
|
{ provide: AuthService, useValue: authService },
|
||||||
{ provide: AuthorizationDataService, useValue: authorizationDataService },
|
{ provide: AuthorizationDataService, useValue: authorizationDataService },
|
||||||
|
{ provide: ServerResponseService, useValue: serverResponseService },
|
||||||
|
{ provide: SignpostingDataService, useValue: signpostingDataService },
|
||||||
|
{ provide: LinkHeadService, useValue: linkHeadService },
|
||||||
|
{ provide: PLATFORM_ID, useValue: 'server' }
|
||||||
],
|
],
|
||||||
|
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).overrideComponent(FullItemPageComponent, {
|
}).overrideComponent(FullItemPageComponent, {
|
||||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||||
@@ -143,6 +175,11 @@ describe('FullItemPageComponent', () => {
|
|||||||
const objectLoader = fixture.debugElement.query(By.css('.full-item-info'));
|
const objectLoader = fixture.debugElement.query(By.css('.full-item-info'));
|
||||||
expect(objectLoader.nativeElement).not.toBeNull();
|
expect(objectLoader.nativeElement).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should add the signposting links', () => {
|
||||||
|
expect(serverResponseService.setHeader).toHaveBeenCalled();
|
||||||
|
expect(linkHeadService.addTag).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
describe('when the item is withdrawn and the user is not an admin', () => {
|
describe('when the item is withdrawn and the user is not an admin', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -167,6 +204,11 @@ describe('FullItemPageComponent', () => {
|
|||||||
const objectLoader = fixture.debugElement.query(By.css('.full-item-info'));
|
const objectLoader = fixture.debugElement.query(By.css('.full-item-info'));
|
||||||
expect(objectLoader).not.toBeNull();
|
expect(objectLoader).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should add the signposting links', () => {
|
||||||
|
expect(serverResponseService.setHeader).toHaveBeenCalled();
|
||||||
|
expect(linkHeadService.addTag).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the item is not withdrawn and the user is not an admin', () => {
|
describe('when the item is not withdrawn and the user is not an admin', () => {
|
||||||
@@ -179,5 +221,10 @@ describe('FullItemPageComponent', () => {
|
|||||||
const objectLoader = fixture.debugElement.query(By.css('.full-item-info'));
|
const objectLoader = fixture.debugElement.query(By.css('.full-item-info'));
|
||||||
expect(objectLoader).not.toBeNull();
|
expect(objectLoader).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should add the signposting links', () => {
|
||||||
|
expect(serverResponseService.setHeader).toHaveBeenCalled();
|
||||||
|
expect(linkHeadService.addTag).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { filter, map } from 'rxjs/operators';
|
import { filter, map } from 'rxjs/operators';
|
||||||
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
|
||||||
import { ActivatedRoute, Data, Router } from '@angular/router';
|
import { ActivatedRoute, Data, Router } from '@angular/router';
|
||||||
|
|
||||||
import { BehaviorSubject, Observable } from 'rxjs';
|
import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
@@ -16,7 +16,9 @@ import { hasValue } from '../../shared/empty.util';
|
|||||||
import { AuthService } from '../../core/auth/auth.service';
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
import { Location } from '@angular/common';
|
import { Location } from '@angular/common';
|
||||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { ServerResponseService } from '../../core/services/server-response.service';
|
||||||
|
import { SignpostingDataService } from '../../core/data/signposting-data.service';
|
||||||
|
import { LinkHeadService } from '../../core/services/link-head.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders a full item page.
|
* This component renders a full item page.
|
||||||
@@ -43,13 +45,19 @@ export class FullItemPageComponent extends ItemPageComponent implements OnInit,
|
|||||||
|
|
||||||
subs = [];
|
subs = [];
|
||||||
|
|
||||||
constructor(protected route: ActivatedRoute,
|
constructor(
|
||||||
router: Router,
|
protected route: ActivatedRoute,
|
||||||
items: ItemDataService,
|
protected router: Router,
|
||||||
authService: AuthService,
|
protected items: ItemDataService,
|
||||||
authorizationService: AuthorizationDataService,
|
protected authService: AuthService,
|
||||||
private _location: Location) {
|
protected authorizationService: AuthorizationDataService,
|
||||||
super(route, router, items, authService, authorizationService);
|
protected _location: Location,
|
||||||
|
protected responseService: ServerResponseService,
|
||||||
|
protected signpostingDataService: SignpostingDataService,
|
||||||
|
protected linkHeadService: LinkHeadService,
|
||||||
|
@Inject(PLATFORM_ID) protected platformId: string,
|
||||||
|
) {
|
||||||
|
super(route, router, items, authService, authorizationService, responseService, signpostingDataService, linkHeadService, platformId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*** AoT inheritance fix, will hopefully be resolved in the near future **/
|
/*** AoT inheritance fix, will hopefully be resolved in the near future **/
|
||||||
|
@@ -34,11 +34,13 @@
|
|||||||
<div class="btn-group edit-field">
|
<div class="btn-group edit-field">
|
||||||
<button [ngbTooltip]="getOperationTooltip(entry) | translate" container="body"
|
<button [ngbTooltip]="getOperationTooltip(entry) | translate" container="body"
|
||||||
class="btn btn-outline-primary my-1 col-md" (click)="send(entry)">
|
class="btn btn-outline-primary my-1 col-md" (click)="send(entry)">
|
||||||
<i [ngClass]="getOperationClass(entry)"></i>
|
<span [ngClass]="getOperationClass(entry)"></span>
|
||||||
|
<span class="sr-only">{{ getOperationTooltip(entry) | translate }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button [ngbTooltip]="'person.page.orcid.sync-queue.discard' | translate" container="body"
|
<button [ngbTooltip]="'person.page.orcid.sync-queue.discard' | translate" container="body"
|
||||||
class="btn btn-outline-danger my-1 col-md" (click)="discardEntry(entry)">
|
class="btn btn-outline-danger my-1 col-md" (click)="discardEntry(entry)">
|
||||||
<i class="fas fa-unlink"></i>
|
<span class="fas fa-unlink"></span>
|
||||||
|
<span class="sr-only">{{ 'person.page.orcid.sync-queue.discard' | translate }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@@ -2,7 +2,7 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
|||||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
|
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
|
||||||
import { ItemDataService } from '../../core/data/item-data.service';
|
import { ItemDataService } from '../../core/data/item-data.service';
|
||||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA, PLATFORM_ID } from '@angular/core';
|
||||||
import { ItemPageComponent } from './item-page.component';
|
import { ItemPageComponent } from './item-page.component';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
|
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
|
||||||
@@ -22,6 +22,10 @@ import {
|
|||||||
import { AuthService } from '../../core/auth/auth.service';
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
import { createPaginatedList } from '../../shared/testing/utils.test';
|
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { ServerResponseService } from '../../core/services/server-response.service';
|
||||||
|
import { SignpostingDataService } from '../../core/data/signposting-data.service';
|
||||||
|
import { LinkDefinition, LinkHeadService } from '../../core/services/link-head.service';
|
||||||
|
import { SignpostingLink } from '../../core/data/signposting-links.model';
|
||||||
|
|
||||||
const mockItem: Item = Object.assign(new Item(), {
|
const mockItem: Item = Object.assign(new Item(), {
|
||||||
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||||
@@ -36,11 +40,28 @@ const mockWithdrawnItem: Item = Object.assign(new Item(), {
|
|||||||
isWithdrawn: true
|
isWithdrawn: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const mocklink = {
|
||||||
|
href: 'http://test.org',
|
||||||
|
rel: 'rel1',
|
||||||
|
type: 'type1'
|
||||||
|
};
|
||||||
|
|
||||||
|
const mocklink2 = {
|
||||||
|
href: 'http://test2.org',
|
||||||
|
rel: 'rel2',
|
||||||
|
type: undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockSignpostingLinks: SignpostingLink[] = [mocklink, mocklink2];
|
||||||
|
|
||||||
describe('ItemPageComponent', () => {
|
describe('ItemPageComponent', () => {
|
||||||
let comp: ItemPageComponent;
|
let comp: ItemPageComponent;
|
||||||
let fixture: ComponentFixture<ItemPageComponent>;
|
let fixture: ComponentFixture<ItemPageComponent>;
|
||||||
let authService: AuthService;
|
let authService: AuthService;
|
||||||
let authorizationDataService: AuthorizationDataService;
|
let authorizationDataService: AuthorizationDataService;
|
||||||
|
let serverResponseService: jasmine.SpyObj<ServerResponseService>;
|
||||||
|
let signpostingDataService: jasmine.SpyObj<SignpostingDataService>;
|
||||||
|
let linkHeadService: jasmine.SpyObj<LinkHeadService>;
|
||||||
|
|
||||||
const mockMetadataService = {
|
const mockMetadataService = {
|
||||||
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
|
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
|
||||||
@@ -60,6 +81,18 @@ describe('ItemPageComponent', () => {
|
|||||||
authorizationDataService = jasmine.createSpyObj('authorizationDataService', {
|
authorizationDataService = jasmine.createSpyObj('authorizationDataService', {
|
||||||
isAuthorized: observableOf(false),
|
isAuthorized: observableOf(false),
|
||||||
});
|
});
|
||||||
|
serverResponseService = jasmine.createSpyObj('ServerResponseService', {
|
||||||
|
setHeader: jasmine.createSpy('setHeader'),
|
||||||
|
});
|
||||||
|
|
||||||
|
signpostingDataService = jasmine.createSpyObj('SignpostingDataService', {
|
||||||
|
getLinks: observableOf([mocklink, mocklink2]),
|
||||||
|
});
|
||||||
|
|
||||||
|
linkHeadService = jasmine.createSpyObj('LinkHeadService', {
|
||||||
|
addTag: jasmine.createSpy('setHeader'),
|
||||||
|
removeTag: jasmine.createSpy('removeTag'),
|
||||||
|
});
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot({
|
imports: [TranslateModule.forRoot({
|
||||||
@@ -76,6 +109,10 @@ describe('ItemPageComponent', () => {
|
|||||||
{ provide: Router, useValue: {} },
|
{ provide: Router, useValue: {} },
|
||||||
{ provide: AuthService, useValue: authService },
|
{ provide: AuthService, useValue: authService },
|
||||||
{ provide: AuthorizationDataService, useValue: authorizationDataService },
|
{ provide: AuthorizationDataService, useValue: authorizationDataService },
|
||||||
|
{ provide: ServerResponseService, useValue: serverResponseService },
|
||||||
|
{ provide: SignpostingDataService, useValue: signpostingDataService },
|
||||||
|
{ provide: LinkHeadService, useValue: linkHeadService },
|
||||||
|
{ provide: PLATFORM_ID, useValue: 'server' },
|
||||||
],
|
],
|
||||||
|
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
@@ -126,6 +163,33 @@ describe('ItemPageComponent', () => {
|
|||||||
const objectLoader = fixture.debugElement.query(By.css('ds-listable-object-component-loader'));
|
const objectLoader = fixture.debugElement.query(By.css('ds-listable-object-component-loader'));
|
||||||
expect(objectLoader.nativeElement).toBeDefined();
|
expect(objectLoader.nativeElement).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should add the signposting links', () => {
|
||||||
|
expect(serverResponseService.setHeader).toHaveBeenCalled();
|
||||||
|
expect(linkHeadService.addTag).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should add link tags correctly', () => {
|
||||||
|
|
||||||
|
expect(comp.signpostingLinks).toEqual([mocklink, mocklink2]);
|
||||||
|
|
||||||
|
// Check if linkHeadService.addTag() was called with the correct arguments
|
||||||
|
expect(linkHeadService.addTag).toHaveBeenCalledTimes(mockSignpostingLinks.length);
|
||||||
|
let expected: LinkDefinition = mockSignpostingLinks[0] as LinkDefinition;
|
||||||
|
expect(linkHeadService.addTag).toHaveBeenCalledWith(expected);
|
||||||
|
expected = {
|
||||||
|
href: 'http://test2.org',
|
||||||
|
rel: 'rel2'
|
||||||
|
};
|
||||||
|
expect(linkHeadService.addTag).toHaveBeenCalledWith(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set Link header on the server', () => {
|
||||||
|
|
||||||
|
expect(serverResponseService.setHeader).toHaveBeenCalledWith('Link', '<http://test.org> ; rel="rel1" ; type="type1" , <http://test2.org> ; rel="rel2" ');
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
describe('when the item is withdrawn and the user is not an admin', () => {
|
describe('when the item is withdrawn and the user is not an admin', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -150,6 +214,11 @@ describe('ItemPageComponent', () => {
|
|||||||
const objectLoader = fixture.debugElement.query(By.css('ds-listable-object-component-loader'));
|
const objectLoader = fixture.debugElement.query(By.css('ds-listable-object-component-loader'));
|
||||||
expect(objectLoader.nativeElement).toBeDefined();
|
expect(objectLoader.nativeElement).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should add the signposting links', () => {
|
||||||
|
expect(serverResponseService.setHeader).toHaveBeenCalled();
|
||||||
|
expect(linkHeadService.addTag).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the item is not withdrawn and the user is not an admin', () => {
|
describe('when the item is not withdrawn and the user is not an admin', () => {
|
||||||
@@ -162,6 +231,11 @@ describe('ItemPageComponent', () => {
|
|||||||
const objectLoader = fixture.debugElement.query(By.css('ds-listable-object-component-loader'));
|
const objectLoader = fixture.debugElement.query(By.css('ds-listable-object-component-loader'));
|
||||||
expect(objectLoader.nativeElement).toBeDefined();
|
expect(objectLoader.nativeElement).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should add the signposting links', () => {
|
||||||
|
expect(serverResponseService.setHeader).toHaveBeenCalled();
|
||||||
|
expect(linkHeadService.addTag).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -1,8 +1,9 @@
|
|||||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { isPlatformServer } from '@angular/common';
|
||||||
|
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { map } from 'rxjs/operators';
|
import { map, take } from 'rxjs/operators';
|
||||||
|
|
||||||
import { ItemDataService } from '../../core/data/item-data.service';
|
import { ItemDataService } from '../../core/data/item-data.service';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
@@ -15,6 +16,11 @@ import { getItemPageRoute } from '../item-page-routing-paths';
|
|||||||
import { redirectOn4xx } from '../../core/shared/authorized.operators';
|
import { redirectOn4xx } from '../../core/shared/authorized.operators';
|
||||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
|
import { ServerResponseService } from '../../core/services/server-response.service';
|
||||||
|
import { SignpostingDataService } from '../../core/data/signposting-data.service';
|
||||||
|
import { SignpostingLink } from '../../core/data/signposting-links.model';
|
||||||
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { LinkDefinition, LinkHeadService } from '../../core/services/link-head.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders a simple item page.
|
* This component renders a simple item page.
|
||||||
@@ -28,7 +34,7 @@ import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
animations: [fadeInOut]
|
animations: [fadeInOut]
|
||||||
})
|
})
|
||||||
export class ItemPageComponent implements OnInit {
|
export class ItemPageComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The item's id
|
* The item's id
|
||||||
@@ -57,13 +63,23 @@ export class ItemPageComponent implements OnInit {
|
|||||||
|
|
||||||
itemUrl: string;
|
itemUrl: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains a list of SignpostingLink related to the item
|
||||||
|
*/
|
||||||
|
signpostingLinks: SignpostingLink[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected route: ActivatedRoute,
|
protected route: ActivatedRoute,
|
||||||
private router: Router,
|
protected router: Router,
|
||||||
private items: ItemDataService,
|
protected items: ItemDataService,
|
||||||
private authService: AuthService,
|
protected authService: AuthService,
|
||||||
private authorizationService: AuthorizationDataService
|
protected authorizationService: AuthorizationDataService,
|
||||||
|
protected responseService: ServerResponseService,
|
||||||
|
protected signpostingDataService: SignpostingDataService,
|
||||||
|
protected linkHeadService: LinkHeadService,
|
||||||
|
@Inject(PLATFORM_ID) protected platformId: string
|
||||||
) {
|
) {
|
||||||
|
this.initPageLinks();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -82,4 +98,42 @@ export class ItemPageComponent implements OnInit {
|
|||||||
this.isAdmin$ = this.authorizationService.isAuthorized(FeatureID.AdministratorOf);
|
this.isAdmin$ = this.authorizationService.isAuthorized(FeatureID.AdministratorOf);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create page links if any are retrieved by signposting endpoint
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private initPageLinks(): void {
|
||||||
|
this.route.params.subscribe(params => {
|
||||||
|
this.signpostingDataService.getLinks(params.id).pipe(take(1)).subscribe((signpostingLinks: SignpostingLink[]) => {
|
||||||
|
let links = '';
|
||||||
|
this.signpostingLinks = signpostingLinks;
|
||||||
|
|
||||||
|
signpostingLinks.forEach((link: SignpostingLink) => {
|
||||||
|
links = links + (isNotEmpty(links) ? ', ' : '') + `<${link.href}> ; rel="${link.rel}"` + (isNotEmpty(link.type) ? ` ; type="${link.type}" ` : ' ');
|
||||||
|
let tag: LinkDefinition = {
|
||||||
|
href: link.href,
|
||||||
|
rel: link.rel
|
||||||
|
};
|
||||||
|
if (isNotEmpty(link.type)) {
|
||||||
|
tag = Object.assign(tag, {
|
||||||
|
type: link.type
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.linkHeadService.addTag(tag);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isPlatformServer(this.platformId)) {
|
||||||
|
this.responseService.setHeader('Link', links);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.signpostingLinks.forEach((link: SignpostingLink) => {
|
||||||
|
this.linkHeadService.removeTag(`href='${link.href}'`);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -16,7 +16,6 @@ import { RouterTestingModule } from '@angular/router/testing';
|
|||||||
import { BrowseService } from '../core/browse/browse.service';
|
import { BrowseService } from '../core/browse/browse.service';
|
||||||
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
|
||||||
import { buildPaginatedList } from '../core/data/paginated-list.model';
|
import { buildPaginatedList } from '../core/data/paginated-list.model';
|
||||||
import { BrowseDefinition } from '../core/shared/browse-definition.model';
|
|
||||||
import { BrowseByDataType } from '../browse-by/browse-by-switcher/browse-by-decorator';
|
import { BrowseByDataType } from '../browse-by/browse-by-switcher/browse-by-decorator';
|
||||||
import { Item } from '../core/shared/item.model';
|
import { Item } from '../core/shared/item.model';
|
||||||
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
||||||
@@ -28,6 +27,9 @@ import { authReducer } from '../core/auth/auth.reducer';
|
|||||||
import { provideMockStore } from '@ngrx/store/testing';
|
import { provideMockStore } from '@ngrx/store/testing';
|
||||||
import { AuthTokenInfo } from '../core/auth/models/auth-token-info.model';
|
import { AuthTokenInfo } from '../core/auth/models/auth-token-info.model';
|
||||||
import { EPersonMock } from '../shared/testing/eperson.mock';
|
import { EPersonMock } from '../shared/testing/eperson.mock';
|
||||||
|
import { FlatBrowseDefinition } from '../core/shared/flat-browse-definition.model';
|
||||||
|
import { ValueListBrowseDefinition } from '../core/shared/value-list-browse-definition.model';
|
||||||
|
import { HierarchicalBrowseDefinition } from '../core/shared/hierarchical-browse-definition.model';
|
||||||
|
|
||||||
let comp: NavbarComponent;
|
let comp: NavbarComponent;
|
||||||
let fixture: ComponentFixture<NavbarComponent>;
|
let fixture: ComponentFixture<NavbarComponent>;
|
||||||
@@ -66,30 +68,35 @@ describe('NavbarComponent', () => {
|
|||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
browseDefinitions = [
|
browseDefinitions = [
|
||||||
Object.assign(
|
Object.assign(
|
||||||
new BrowseDefinition(), {
|
new FlatBrowseDefinition(), {
|
||||||
id: 'title',
|
id: 'title',
|
||||||
dataType: BrowseByDataType.Title,
|
dataType: BrowseByDataType.Title,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
Object.assign(
|
Object.assign(
|
||||||
new BrowseDefinition(), {
|
new FlatBrowseDefinition(), {
|
||||||
id: 'dateissued',
|
id: 'dateissued',
|
||||||
dataType: BrowseByDataType.Date,
|
dataType: BrowseByDataType.Date,
|
||||||
metadataKeys: ['dc.date.issued']
|
metadataKeys: ['dc.date.issued']
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
Object.assign(
|
Object.assign(
|
||||||
new BrowseDefinition(), {
|
new ValueListBrowseDefinition(), {
|
||||||
id: 'author',
|
id: 'author',
|
||||||
dataType: BrowseByDataType.Metadata,
|
dataType: BrowseByDataType.Metadata,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
Object.assign(
|
Object.assign(
|
||||||
new BrowseDefinition(), {
|
new ValueListBrowseDefinition(), {
|
||||||
id: 'subject',
|
id: 'subject',
|
||||||
dataType: BrowseByDataType.Metadata,
|
dataType: BrowseByDataType.Metadata,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
Object.assign(
|
||||||
|
new HierarchicalBrowseDefinition(), {
|
||||||
|
id: 'srsc',
|
||||||
|
}
|
||||||
|
),
|
||||||
];
|
];
|
||||||
initialState = {
|
initialState = {
|
||||||
core: {
|
core: {
|
||||||
|
@@ -1,10 +1,15 @@
|
|||||||
<div class="container" *ngVar="(processRD$ | async)?.payload as process">
|
<div class="container" *ngIf="(processRD$ | async)?.payload as process">
|
||||||
<div class="d-flex">
|
<div class="row">
|
||||||
<h2 class="flex-grow-1">{{'process.detail.title' | translate:{
|
<div class="col-10">
|
||||||
id: process?.processId,
|
<h2 class="flex-grow-1">
|
||||||
name: process?.scriptName
|
{{ 'process.detail.title' | translate:{ id: process?.processId, name: process?.scriptName } }}
|
||||||
} }}</h2>
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="refreshCounter$ | async as seconds" class="col-2 refresh-counter">
|
||||||
|
Refreshing in {{ seconds }}s <i class="fas fa-sync-alt fa-spin"></i>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ds-process-detail-field id="process-name" [title]="'process.detail.script'">
|
<ds-process-detail-field id="process-name" [title]="'process.detail.script'">
|
||||||
<div>{{ process?.scriptName }}</div>
|
<div>{{ process?.scriptName }}</div>
|
||||||
</ds-process-detail-field>
|
</ds-process-detail-field>
|
||||||
@@ -17,10 +22,12 @@
|
|||||||
<div *ngVar="(filesRD$ | async)?.payload?.page as files">
|
<div *ngVar="(filesRD$ | async)?.payload?.page as files">
|
||||||
<ds-process-detail-field *ngIf="files && files?.length > 0" id="process-files"
|
<ds-process-detail-field *ngIf="files && files?.length > 0" id="process-files"
|
||||||
[title]="'process.detail.output-files'">
|
[title]="'process.detail.output-files'">
|
||||||
|
<div class="d-flex flex-column">
|
||||||
<ds-themed-file-download-link *ngFor="let file of files; let last=last;" [bitstream]="file">
|
<ds-themed-file-download-link *ngFor="let file of files; let last=last;" [bitstream]="file">
|
||||||
<span>{{getFileName(file)}}</span>
|
<span>{{getFileName(file)}}</span>
|
||||||
<span>({{(file?.sizeBytes) | dsFileSize }})</span>
|
<span>({{(file?.sizeBytes) | dsFileSize }})</span>
|
||||||
</ds-themed-file-download-link>
|
</ds-themed-file-download-link>
|
||||||
|
</div>
|
||||||
</ds-process-detail-field>
|
</ds-process-detail-field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -70,7 +77,7 @@
|
|||||||
|
|
||||||
<ng-template #deleteModal >
|
<ng-template #deleteModal >
|
||||||
|
|
||||||
<div *ngVar="(processRD$ | async)?.payload as process">
|
<div *ngIf="(processRD$ | async)?.payload as process">
|
||||||
|
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<div>
|
<div>
|
||||||
|
@@ -35,6 +35,7 @@ import { NotificationsServiceStub } from '../../shared/testing/notifications-ser
|
|||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { getProcessListRoute } from '../process-page-routing.paths';
|
import { getProcessListRoute } from '../process-page-routing.paths';
|
||||||
|
import {ProcessStatus} from '../processes/process-status.model';
|
||||||
|
|
||||||
describe('ProcessDetailComponent', () => {
|
describe('ProcessDetailComponent', () => {
|
||||||
let component: ProcessDetailComponent;
|
let component: ProcessDetailComponent;
|
||||||
@@ -44,6 +45,7 @@ describe('ProcessDetailComponent', () => {
|
|||||||
let nameService: DSONameService;
|
let nameService: DSONameService;
|
||||||
let bitstreamDataService: BitstreamDataService;
|
let bitstreamDataService: BitstreamDataService;
|
||||||
let httpClient: HttpClient;
|
let httpClient: HttpClient;
|
||||||
|
let route: ActivatedRoute;
|
||||||
|
|
||||||
let process: Process;
|
let process: Process;
|
||||||
let fileName: string;
|
let fileName: string;
|
||||||
@@ -106,7 +108,8 @@ describe('ProcessDetailComponent', () => {
|
|||||||
});
|
});
|
||||||
processService = jasmine.createSpyObj('processService', {
|
processService = jasmine.createSpyObj('processService', {
|
||||||
getFiles: createSuccessfulRemoteDataObject$(createPaginatedList(files)),
|
getFiles: createSuccessfulRemoteDataObject$(createPaginatedList(files)),
|
||||||
delete: createSuccessfulRemoteDataObject$(null)
|
delete: createSuccessfulRemoteDataObject$(null),
|
||||||
|
findById: createSuccessfulRemoteDataObject$(process),
|
||||||
});
|
});
|
||||||
bitstreamDataService = jasmine.createSpyObj('bitstreamDataService', {
|
bitstreamDataService = jasmine.createSpyObj('bitstreamDataService', {
|
||||||
findByHref: createSuccessfulRemoteDataObject$(logBitstream)
|
findByHref: createSuccessfulRemoteDataObject$(logBitstream)
|
||||||
@@ -127,6 +130,13 @@ describe('ProcessDetailComponent', () => {
|
|||||||
router = jasmine.createSpyObj('router', {
|
router = jasmine.createSpyObj('router', {
|
||||||
navigateByUrl:{}
|
navigateByUrl:{}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
route = jasmine.createSpyObj('route', {
|
||||||
|
data: observableOf({ process: createSuccessfulRemoteDataObject(process) }),
|
||||||
|
snapshot: {
|
||||||
|
params: { id: process.processId }
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
@@ -263,4 +273,92 @@ describe('ProcessDetailComponent', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('refresh counter', () => {
|
||||||
|
const queryRefreshCounter = () => fixture.debugElement.query(By.css('.refresh-counter'));
|
||||||
|
|
||||||
|
describe('if process is completed', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
process.processStatus = ProcessStatus.COMPLETED;
|
||||||
|
route.data = observableOf({process: createSuccessfulRemoteDataObject(process)});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show', () => {
|
||||||
|
spyOn(component, 'startRefreshTimer');
|
||||||
|
|
||||||
|
const refreshCounter = queryRefreshCounter();
|
||||||
|
expect(refreshCounter).toBeNull();
|
||||||
|
|
||||||
|
expect(component.startRefreshTimer).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if process is not finished', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
process.processStatus = ProcessStatus.RUNNING;
|
||||||
|
route.data = observableOf({process: createSuccessfulRemoteDataObject(process)});
|
||||||
|
fixture.detectChanges();
|
||||||
|
component.stopRefreshTimer();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call startRefreshTimer', () => {
|
||||||
|
spyOn(component, 'startRefreshTimer');
|
||||||
|
|
||||||
|
component.ngOnInit();
|
||||||
|
fixture.detectChanges(); // subscribe to process observable with async pipe
|
||||||
|
|
||||||
|
expect(component.startRefreshTimer).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call refresh method every 5 seconds, until process is completed', fakeAsync(() => {
|
||||||
|
spyOn(component, 'refresh');
|
||||||
|
spyOn(component, 'stopRefreshTimer');
|
||||||
|
|
||||||
|
process.processStatus = ProcessStatus.COMPLETED;
|
||||||
|
// set findbyId to return a completed process
|
||||||
|
(processService.findById as jasmine.Spy).and.returnValue(observableOf(createSuccessfulRemoteDataObject(process)));
|
||||||
|
|
||||||
|
component.ngOnInit();
|
||||||
|
fixture.detectChanges(); // subscribe to process observable with async pipe
|
||||||
|
|
||||||
|
expect(component.refresh).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
expect(component.refreshCounter$.value).toBe(0);
|
||||||
|
|
||||||
|
tick(1001); // 1 second + 1 ms by the setTimeout
|
||||||
|
expect(component.refreshCounter$.value).toBe(5); // 5 - 0
|
||||||
|
|
||||||
|
tick(2001); // 2 seconds + 1 ms by the setTimeout
|
||||||
|
expect(component.refreshCounter$.value).toBe(3); // 5 - 2
|
||||||
|
|
||||||
|
tick(2001); // 2 seconds + 1 ms by the setTimeout
|
||||||
|
expect(component.refreshCounter$.value).toBe(1); // 3 - 2
|
||||||
|
|
||||||
|
tick(1001); // 1 second + 1 ms by the setTimeout
|
||||||
|
expect(component.refreshCounter$.value).toBe(0); // 1 - 1
|
||||||
|
|
||||||
|
tick(1000); // 1 second
|
||||||
|
|
||||||
|
expect(component.refresh).toHaveBeenCalledTimes(1);
|
||||||
|
expect(component.stopRefreshTimer).toHaveBeenCalled();
|
||||||
|
|
||||||
|
expect(component.refreshCounter$.value).toBe(0);
|
||||||
|
|
||||||
|
tick(1001); // 1 second + 1 ms by the setTimeout
|
||||||
|
// startRefreshTimer not called again
|
||||||
|
expect(component.refreshCounter$.value).toBe(0);
|
||||||
|
|
||||||
|
discardPeriodicTasks(); // discard any periodic tasks that have not yet executed
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should show if refreshCounter is different from 0', () => {
|
||||||
|
component.refreshCounter$.next(1);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const refreshCounter = queryRefreshCounter();
|
||||||
|
expect(refreshCounter).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Component, NgZone, OnInit } from '@angular/core';
|
import { Component, Inject, NgZone, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { BehaviorSubject, Observable } from 'rxjs';
|
import { BehaviorSubject, interval, Observable, shareReplay, Subscription } from 'rxjs';
|
||||||
import { finalize, map, switchMap, take, tap } from 'rxjs/operators';
|
import { finalize, map, switchMap, take, tap } from 'rxjs/operators';
|
||||||
import { AuthService } from '../../core/auth/auth.service';
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||||
@@ -26,6 +26,8 @@ import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
|||||||
import { getProcessListRoute } from '../process-page-routing.paths';
|
import { getProcessListRoute } from '../process-page-routing.paths';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { followLink } from '../../shared/utils/follow-link-config.model';
|
||||||
|
import { isPlatformBrowser } from '@angular/common';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-process-detail',
|
selector: 'ds-process-detail',
|
||||||
@@ -34,7 +36,7 @@ import { TranslateService } from '@ngx-translate/core';
|
|||||||
/**
|
/**
|
||||||
* A component displaying detailed information about a DSpace Process
|
* A component displaying detailed information about a DSpace Process
|
||||||
*/
|
*/
|
||||||
export class ProcessDetailComponent implements OnInit {
|
export class ProcessDetailComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The AlertType enumeration
|
* The AlertType enumeration
|
||||||
@@ -65,48 +67,58 @@ export class ProcessDetailComponent implements OnInit {
|
|||||||
/**
|
/**
|
||||||
* Boolean on whether or not to show the output logs
|
* Boolean on whether or not to show the output logs
|
||||||
*/
|
*/
|
||||||
showOutputLogs;
|
showOutputLogs = false;
|
||||||
/**
|
/**
|
||||||
* When it's retrieving the output logs from backend, to show loading component
|
* When it's retrieving the output logs from backend, to show loading component
|
||||||
*/
|
*/
|
||||||
retrievingOutputLogs$: BehaviorSubject<boolean>;
|
retrievingOutputLogs$ = new BehaviorSubject<boolean>(false);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Date format to use for start and end time of processes
|
* Date format to use for start and end time of processes
|
||||||
*/
|
*/
|
||||||
dateFormat = 'yyyy-MM-dd HH:mm:ss ZZZZ';
|
dateFormat = 'yyyy-MM-dd HH:mm:ss ZZZZ';
|
||||||
|
|
||||||
|
refreshCounter$ = new BehaviorSubject(0);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reference to NgbModal
|
* Reference to NgbModal
|
||||||
*/
|
*/
|
||||||
protected modalRef: NgbModalRef;
|
protected modalRef: NgbModalRef;
|
||||||
|
|
||||||
constructor(protected route: ActivatedRoute,
|
private refreshTimerSub?: Subscription;
|
||||||
protected router: Router,
|
|
||||||
protected processService: ProcessDataService,
|
constructor(
|
||||||
protected bitstreamDataService: BitstreamDataService,
|
@Inject(PLATFORM_ID) protected platformId: object,
|
||||||
protected nameService: DSONameService,
|
protected route: ActivatedRoute,
|
||||||
private zone: NgZone,
|
protected router: Router,
|
||||||
protected authService: AuthService,
|
protected processService: ProcessDataService,
|
||||||
protected http: HttpClient,
|
protected bitstreamDataService: BitstreamDataService,
|
||||||
protected modalService: NgbModal,
|
protected nameService: DSONameService,
|
||||||
protected notificationsService: NotificationsService,
|
private zone: NgZone,
|
||||||
protected translateService: TranslateService
|
protected authService: AuthService,
|
||||||
) {
|
protected http: HttpClient,
|
||||||
}
|
protected modalService: NgbModal,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected translateService: TranslateService
|
||||||
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize component properties
|
* Initialize component properties
|
||||||
* Display a 404 if the process doesn't exist
|
* Display a 404 if the process doesn't exist
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.showOutputLogs = false;
|
|
||||||
this.retrievingOutputLogs$ = new BehaviorSubject<boolean>(false);
|
|
||||||
this.processRD$ = this.route.data.pipe(
|
this.processRD$ = this.route.data.pipe(
|
||||||
map((data) => {
|
map((data) => {
|
||||||
|
if (isPlatformBrowser(this.platformId)) {
|
||||||
|
if (!this.isProcessFinished(data.process.payload)) {
|
||||||
|
this.startRefreshTimer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return data.process as RemoteData<Process>;
|
return data.process as RemoteData<Process>;
|
||||||
}),
|
}),
|
||||||
redirectOn4xx(this.router, this.authService)
|
redirectOn4xx(this.router, this.authService),
|
||||||
|
shareReplay(1)
|
||||||
);
|
);
|
||||||
|
|
||||||
this.filesRD$ = this.processRD$.pipe(
|
this.filesRD$ = this.processRD$.pipe(
|
||||||
@@ -115,6 +127,53 @@ export class ProcessDetailComponent implements OnInit {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
this.processRD$ = this.processService.findById(
|
||||||
|
this.route.snapshot.params.id,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
followLink('script')
|
||||||
|
).pipe(
|
||||||
|
getFirstSucceededRemoteData(),
|
||||||
|
redirectOn4xx(this.router, this.authService),
|
||||||
|
tap((processRemoteData: RemoteData<Process>) => {
|
||||||
|
if (!this.isProcessFinished(processRemoteData.payload)) {
|
||||||
|
this.startRefreshTimer();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
shareReplay(1)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.filesRD$ = this.processRD$.pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
switchMap((process: Process) => this.processService.getFiles(process.processId))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
startRefreshTimer() {
|
||||||
|
this.refreshCounter$.next(0);
|
||||||
|
|
||||||
|
this.refreshTimerSub = interval(1000).subscribe(
|
||||||
|
value => {
|
||||||
|
if (value > 5) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.refresh();
|
||||||
|
this.stopRefreshTimer();
|
||||||
|
this.refreshCounter$.next(0);
|
||||||
|
}, 1);
|
||||||
|
} else {
|
||||||
|
this.refreshCounter$.next(5 - value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
stopRefreshTimer() {
|
||||||
|
if (hasValue(this.refreshTimerSub)) {
|
||||||
|
this.refreshTimerSub.unsubscribe();
|
||||||
|
this.refreshTimerSub = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the name of a bitstream
|
* Get the name of a bitstream
|
||||||
* @param bitstream
|
* @param bitstream
|
||||||
@@ -210,6 +269,7 @@ export class ProcessDetailComponent implements OnInit {
|
|||||||
openDeleteModal(content) {
|
openDeleteModal(content) {
|
||||||
this.modalRef = this.modalService.open(content);
|
this.modalRef = this.modalService.open(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Close the modal.
|
* Close the modal.
|
||||||
*/
|
*/
|
||||||
@@ -217,4 +277,7 @@ export class ProcessDetailComponent implements OnInit {
|
|||||||
this.modalRef.close();
|
this.modalRef.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.stopRefreshTimer();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -50,27 +50,32 @@ export class DSOEditMenuResolver implements Resolve<{ [key: string]: MenuSection
|
|||||||
if (hasNoValue(id) && hasValue(route.queryParams.scope)) {
|
if (hasNoValue(id) && hasValue(route.queryParams.scope)) {
|
||||||
id = route.queryParams.scope;
|
id = route.queryParams.scope;
|
||||||
}
|
}
|
||||||
return this.dSpaceObjectDataService.findById(id, true, false).pipe(
|
if (hasNoValue(id)) {
|
||||||
getFirstCompletedRemoteData(),
|
// If there's no ID, we're not on a DSO homepage, so pass on any pre-existing menu route data
|
||||||
switchMap((dsoRD) => {
|
return observableOf({ ...route.data?.menu });
|
||||||
if (dsoRD.hasSucceeded) {
|
} else {
|
||||||
const dso = dsoRD.payload;
|
return this.dSpaceObjectDataService.findById(id, true, false).pipe(
|
||||||
return combineLatest(this.getDsoMenus(dso, route, state)).pipe(
|
getFirstCompletedRemoteData(),
|
||||||
// Menu sections are retrieved as an array of arrays and flattened into a single array
|
switchMap((dsoRD) => {
|
||||||
map((combinedMenus) => [].concat.apply([], combinedMenus)),
|
if (dsoRD.hasSucceeded) {
|
||||||
map((menus) => this.addDsoUuidToMenuIDs(menus, dso)),
|
const dso = dsoRD.payload;
|
||||||
map((menus) => {
|
return combineLatest(this.getDsoMenus(dso, route, state)).pipe(
|
||||||
return {
|
// Menu sections are retrieved as an array of arrays and flattened into a single array
|
||||||
...route.data?.menu,
|
map((combinedMenus) => [].concat.apply([], combinedMenus)),
|
||||||
[MenuID.DSO_EDIT]: menus
|
map((menus) => this.addDsoUuidToMenuIDs(menus, dso)),
|
||||||
};
|
map((menus) => {
|
||||||
})
|
return {
|
||||||
);
|
...route.data?.menu,
|
||||||
} else {
|
[MenuID.DSO_EDIT]: menus
|
||||||
return observableOf({...route.data?.menu});
|
};
|
||||||
}
|
})
|
||||||
})
|
);
|
||||||
);
|
} else {
|
||||||
|
return observableOf({...route.data?.menu});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -30,8 +30,8 @@ import { VocabularyEntry } from '../../../../../../core/submission/vocabularies/
|
|||||||
import { PageInfo } from '../../../../../../core/shared/page-info.model';
|
import { PageInfo } from '../../../../../../core/shared/page-info.model';
|
||||||
import { DsDynamicVocabularyComponent } from '../dynamic-vocabulary.component';
|
import { DsDynamicVocabularyComponent } from '../dynamic-vocabulary.component';
|
||||||
import { Vocabulary } from '../../../../../../core/submission/vocabularies/models/vocabulary.model';
|
import { Vocabulary } from '../../../../../../core/submission/vocabularies/models/vocabulary.model';
|
||||||
import { VocabularyTreeviewComponent } from '../../../../vocabulary-treeview/vocabulary-treeview.component';
|
|
||||||
import { VocabularyEntryDetail } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
|
import { VocabularyEntryDetail } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
|
||||||
|
import { VocabularyTreeviewModalComponent } from '../../../../vocabulary-treeview-modal/vocabulary-treeview-modal.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component representing a onebox input field.
|
* Component representing a onebox input field.
|
||||||
@@ -222,10 +222,10 @@ export class DsDynamicOneboxComponent extends DsDynamicVocabularyComponent imple
|
|||||||
map((vocabulary: Vocabulary) => vocabulary.preloadLevel),
|
map((vocabulary: Vocabulary) => vocabulary.preloadLevel),
|
||||||
take(1)
|
take(1)
|
||||||
).subscribe((preloadLevel) => {
|
).subscribe((preloadLevel) => {
|
||||||
const modalRef: NgbModalRef = this.modalService.open(VocabularyTreeviewComponent, { size: 'lg', windowClass: 'treeview' });
|
const modalRef: NgbModalRef = this.modalService.open(VocabularyTreeviewModalComponent, { size: 'lg', windowClass: 'treeview' });
|
||||||
modalRef.componentInstance.vocabularyOptions = this.model.vocabularyOptions;
|
modalRef.componentInstance.vocabularyOptions = this.model.vocabularyOptions;
|
||||||
modalRef.componentInstance.preloadLevel = preloadLevel;
|
modalRef.componentInstance.preloadLevel = preloadLevel;
|
||||||
modalRef.componentInstance.selectedItem = this.currentValue ? this.currentValue : '';
|
modalRef.componentInstance.selectedItems = this.currentValue ? [this.currentValue.value] : [];
|
||||||
modalRef.result.then((result: VocabularyEntryDetail) => {
|
modalRef.result.then((result: VocabularyEntryDetail) => {
|
||||||
if (result) {
|
if (result) {
|
||||||
this.currentValue = result;
|
this.currentValue = result;
|
||||||
|
@@ -32,7 +32,7 @@ import { NumberPickerComponent } from './number-picker/number-picker.component';
|
|||||||
import { AuthorityConfidenceStateDirective } from './directives/authority-confidence-state.directive';
|
import { AuthorityConfidenceStateDirective } from './directives/authority-confidence-state.directive';
|
||||||
import { SortablejsModule } from 'ngx-sortablejs';
|
import { SortablejsModule } from 'ngx-sortablejs';
|
||||||
import { VocabularyTreeviewComponent } from './vocabulary-treeview/vocabulary-treeview.component';
|
import { VocabularyTreeviewComponent } from './vocabulary-treeview/vocabulary-treeview.component';
|
||||||
import { VocabularyTreeviewService } from './vocabulary-treeview/vocabulary-treeview.service';
|
import { VocabularyTreeviewModalComponent } from './vocabulary-treeview-modal/vocabulary-treeview-modal.component';
|
||||||
import { FormBuilderService } from './builder/form-builder.service';
|
import { FormBuilderService } from './builder/form-builder.service';
|
||||||
import { DsDynamicTypeBindRelationService } from './builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service';
|
import { DsDynamicTypeBindRelationService } from './builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service';
|
||||||
import { FormService } from './form.service';
|
import { FormService } from './form.service';
|
||||||
@@ -71,7 +71,8 @@ const COMPONENTS = [
|
|||||||
ChipsComponent,
|
ChipsComponent,
|
||||||
NumberPickerComponent,
|
NumberPickerComponent,
|
||||||
VocabularyTreeviewComponent,
|
VocabularyTreeviewComponent,
|
||||||
ThemedExternalSourceEntryImportModalComponent
|
VocabularyTreeviewModalComponent,
|
||||||
|
ThemedExternalSourceEntryImportModalComponent,
|
||||||
];
|
];
|
||||||
|
|
||||||
const DIRECTIVES = [
|
const DIRECTIVES = [
|
||||||
@@ -105,7 +106,6 @@ const DIRECTIVES = [
|
|||||||
provide: DYNAMIC_FORM_CONTROL_MAP_FN,
|
provide: DYNAMIC_FORM_CONTROL_MAP_FN,
|
||||||
useValue: dsDynamicFormControlMapFn
|
useValue: dsDynamicFormControlMapFn
|
||||||
},
|
},
|
||||||
VocabularyTreeviewService,
|
|
||||||
DynamicFormLayoutService,
|
DynamicFormLayoutService,
|
||||||
DynamicFormService,
|
DynamicFormService,
|
||||||
DynamicFormValidationService,
|
DynamicFormValidationService,
|
||||||
|
@@ -0,0 +1,16 @@
|
|||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title">{{'vocabulary-treeview.header' | translate}}</h4>
|
||||||
|
<button type="button" class="close" aria-label="Close" (click)="activeModal.dismiss('Cross click')">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="p-3">
|
||||||
|
<ds-vocabulary-treeview [vocabularyOptions]="vocabularyOptions"
|
||||||
|
[preloadLevel]="preloadLevel"
|
||||||
|
[selectedItems]="selectedItems"
|
||||||
|
[multiSelect]="multiSelect"
|
||||||
|
(select)="onSelect($event)">
|
||||||
|
</ds-vocabulary-treeview>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1,33 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { VocabularyTreeviewModalComponent } from './vocabulary-treeview-modal.component';
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
describe('VocabularyTreeviewModalComponent', () => {
|
||||||
|
let component: VocabularyTreeviewModalComponent;
|
||||||
|
let fixture: ComponentFixture<VocabularyTreeviewModalComponent>;
|
||||||
|
|
||||||
|
const modalStub = jasmine.createSpyObj('modalStub', ['close']);
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ TranslateModule.forRoot() ],
|
||||||
|
declarations: [ VocabularyTreeviewModalComponent ],
|
||||||
|
providers: [
|
||||||
|
{ provide: NgbActiveModal, useValue: modalStub },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(VocabularyTreeviewModalComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,51 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { VocabularyOptions } from '../../../core/submission/vocabularies/models/vocabulary-options.model';
|
||||||
|
import { VocabularyEntryDetail } from '../../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-vocabulary-treeview-modal',
|
||||||
|
templateUrl: './vocabulary-treeview-modal.component.html',
|
||||||
|
styleUrls: ['./vocabulary-treeview-modal.component.scss']
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Component that contains a modal to display a VocabularyTreeviewComponent
|
||||||
|
*/
|
||||||
|
export class VocabularyTreeviewModalComponent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link VocabularyOptions} object
|
||||||
|
*/
|
||||||
|
@Input() vocabularyOptions: VocabularyOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Representing how many tree level load at initialization
|
||||||
|
*/
|
||||||
|
@Input() preloadLevel = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The vocabulary entries already selected, if any
|
||||||
|
*/
|
||||||
|
@Input() selectedItems: string[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to allow selecting multiple values with checkboxes
|
||||||
|
*/
|
||||||
|
@Input() multiSelect = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize instance variables
|
||||||
|
*
|
||||||
|
* @param {NgbActiveModal} activeModal
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
public activeModal: NgbActiveModal,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method called on entry select
|
||||||
|
*/
|
||||||
|
onSelect(item: VocabularyEntryDetail) {
|
||||||
|
this.activeModal.close(item);
|
||||||
|
}
|
||||||
|
}
|
@@ -21,7 +21,8 @@ export class TreeviewNode {
|
|||||||
public pageInfo: PageInfo = new PageInfo(),
|
public pageInfo: PageInfo = new PageInfo(),
|
||||||
public loadMoreParentItem: VocabularyEntryDetail | null = null,
|
public loadMoreParentItem: VocabularyEntryDetail | null = null,
|
||||||
public isSearchNode = false,
|
public isSearchNode = false,
|
||||||
public isInInitValueHierarchy = false) {
|
public isInInitValueHierarchy = false,
|
||||||
|
public isSelected = false) {
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePageInfo(pageInfo: PageInfo) {
|
updatePageInfo(pageInfo: PageInfo) {
|
||||||
@@ -38,7 +39,8 @@ export class TreeviewFlatNode {
|
|||||||
public pageInfo: PageInfo = new PageInfo(),
|
public pageInfo: PageInfo = new PageInfo(),
|
||||||
public loadMoreParentItem: VocabularyEntryDetail | null = null,
|
public loadMoreParentItem: VocabularyEntryDetail | null = null,
|
||||||
public isSearchNode = false,
|
public isSearchNode = false,
|
||||||
public isInInitValueHierarchy = false) {
|
public isInInitValueHierarchy = false,
|
||||||
|
public isSelected = false) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,77 +1,98 @@
|
|||||||
<div class="modal-header">
|
<ds-alert *ngIf="description | async" [content]="description | async" [type]="'alert-info'"></ds-alert>
|
||||||
<h4 class="modal-title">{{'vocabulary-treeview.header' | translate}}</h4>
|
<div class="treeview-header row">
|
||||||
<button type="button" class="close" aria-label="Close" (click)="activeModal.dismiss('Cross click')">
|
<div class="col-12">
|
||||||
<span aria-hidden="true">×</span>
|
<div class="input-group">
|
||||||
</button>
|
<input type="text" class="form-control" [(ngModel)]="searchText" (keyup.enter)="search()">
|
||||||
</div>
|
<div class="input-group-append" id="button-addon4">
|
||||||
<div class="modal-body">
|
<button class="btn btn-outline-primary" type="button" (click)="search()" [disabled]="!isSearchEnabled()">
|
||||||
<div class="p-3">
|
{{'vocabulary-treeview.search.form.search' | translate}}
|
||||||
<ds-alert *ngIf="description | async" [content]="description | async" [type]="'alert-info'"></ds-alert>
|
</button>
|
||||||
<div class="treeview-header row">
|
<button class="btn btn-outline-secondary" type="button" (click)="reset()">
|
||||||
<div class="col-12">
|
{{'vocabulary-treeview.search.form.reset' | translate}}
|
||||||
<div class="input-group">
|
</button>
|
||||||
<input type="text" class="form-control" [(ngModel)]="searchText" (keyup.enter)="search()">
|
|
||||||
<div class="input-group-append" id="button-addon4">
|
|
||||||
<button class="btn btn-outline-primary" type="button" (click)="search()" [disabled]="!isSearchEnabled()">
|
|
||||||
{{'vocabulary-treeview.search.form.search' | translate}}
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-outline-secondary" type="button" (click)="reset()">
|
|
||||||
{{'vocabulary-treeview.search.form.reset' | translate}}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="treeview-container">
|
|
||||||
<ds-themed-loading *ngIf="loading | async" [showMessage]="false"></ds-themed-loading>
|
|
||||||
<h4 *ngIf="!(loading | async) && dataSource.data.length === 0" class="text-center text-muted mt-4" >
|
|
||||||
<span>{{'vocabulary-treeview.search.no-result' | translate}}</span>
|
|
||||||
</h4>
|
|
||||||
<cdk-tree [dataSource]="dataSource" [treeControl]="treeControl">
|
|
||||||
<!-- Leaf node -->
|
|
||||||
<cdk-tree-node *cdkTreeNodeDef="let node" cdkTreeNodePadding class="d-flex">
|
|
||||||
<button type="button" class="btn btn-default" cdkTreeNodeToggle>
|
|
||||||
<span class="fas fa-angle-right fa-2x invisible" aria-hidden="true"></span>
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-outline-link btn-sm text-left"
|
|
||||||
[class.text-success]="node.item?.value === selectedItem?.value"
|
|
||||||
[disabled]="!node.item?.selectable"
|
|
||||||
[ngbTooltip]="node.item?.otherInformation?.note"
|
|
||||||
[openDelay]="500"
|
|
||||||
container="body"
|
|
||||||
(click)="onSelect(node.item)">{{node.item.display}}</button>
|
|
||||||
</cdk-tree-node>
|
|
||||||
|
|
||||||
<!-- expandable node -->
|
|
||||||
<cdk-tree-node *cdkTreeNodeDef="let node; when: hasChildren" cdkTreeNodePadding class="d-flex">
|
|
||||||
<button type="button" class="btn btn-default" cdkTreeNodeToggle
|
|
||||||
[attr.aria-label]="'toggle ' + node.name"
|
|
||||||
(click)="loadChildren(node)">
|
|
||||||
<span class="fas {{treeControl.isExpanded(node) ? 'fa-angle-down' : 'fa-angle-right'}} fa-2x"
|
|
||||||
aria-hidden="true"></span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="btn btn-outline-link btn-sm text-left"
|
|
||||||
[class.text-success]="node.item?.value === selectedItem?.value"
|
|
||||||
[disabled]="!node.item?.selectable"
|
|
||||||
[ngbTooltip]="node.item?.otherInformation?.note"
|
|
||||||
[openDelay]="500"
|
|
||||||
container="body"
|
|
||||||
(click)="onSelect(node.item)">{{node.item.display}}</button>
|
|
||||||
</cdk-tree-node>
|
|
||||||
|
|
||||||
<cdk-tree-node *cdkTreeNodeDef="let node; when: isLoadMore" cdkTreeNodePadding>
|
|
||||||
<button class="btn btn-outline-secondary btn-sm" (click)="loadMore(node.loadMoreParentItem)">
|
|
||||||
{{'vocabulary-treeview.load-more' | translate}}...
|
|
||||||
</button>
|
|
||||||
</cdk-tree-node>
|
|
||||||
|
|
||||||
<cdk-tree-node *cdkTreeNodeDef="let node; when: isLoadMoreRoot">
|
|
||||||
<button class="btn btn-outline-secondary btn-sm" (click)="loadMoreRoot(node)">
|
|
||||||
{{'vocabulary-treeview.load-more' | translate}}...
|
|
||||||
</button>
|
|
||||||
</cdk-tree-node>
|
|
||||||
</cdk-tree>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="treeview-container">
|
||||||
|
<ds-themed-loading *ngIf="loading | async" [showMessage]="false"></ds-themed-loading>
|
||||||
|
<h4 *ngIf="!(loading | async) && dataSource.data.length === 0" class="text-center text-muted mt-4" >
|
||||||
|
<span>{{'vocabulary-treeview.search.no-result' | translate}}</span>
|
||||||
|
</h4>
|
||||||
|
<cdk-tree [dataSource]="dataSource" [treeControl]="treeControl">
|
||||||
|
<!-- Leaf node -->
|
||||||
|
<cdk-tree-node *cdkTreeNodeDef="let node" cdkTreeNodePadding class="d-flex">
|
||||||
|
<button type="button" class="btn btn-default" cdkTreeNodeToggle>
|
||||||
|
<span class="fas fa-angle-right invisible" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
<label *ngIf="multiSelect" class="d-flex align-items-center m-0 p-0 form-check"
|
||||||
|
[class.text-success]="node.isSelected"
|
||||||
|
[ngbTooltip]="node.item?.otherInformation?.note"
|
||||||
|
[openDelay]="500"
|
||||||
|
container="body"
|
||||||
|
>
|
||||||
|
<input class="mr-2" type="checkbox"
|
||||||
|
[disabled]="!node.item?.selectable"
|
||||||
|
[(ngModel)]="node.isSelected"
|
||||||
|
[checked]="node.isSelected"
|
||||||
|
(change)="onSelect(node.item)"
|
||||||
|
>
|
||||||
|
<span>{{node.item.display}}</span>
|
||||||
|
</label>
|
||||||
|
<button *ngIf="!multiSelect" class="btn btn-outline-link btn-sm text-left"
|
||||||
|
[class.text-success]="node.isSelected"
|
||||||
|
[disabled]="!node.item?.selectable"
|
||||||
|
[ngbTooltip]="node.item?.otherInformation?.note"
|
||||||
|
[openDelay]="500"
|
||||||
|
container="body"
|
||||||
|
(click)="onSelect(node.item)">
|
||||||
|
<span>{{node.item.display}}</span>
|
||||||
|
</button>
|
||||||
|
</cdk-tree-node>
|
||||||
|
|
||||||
|
<!-- expandable node -->
|
||||||
|
<cdk-tree-node *cdkTreeNodeDef="let node; when: hasChildren" cdkTreeNodePadding class="d-flex">
|
||||||
|
<button type="button" class="btn btn-default" cdkTreeNodeToggle
|
||||||
|
[attr.aria-label]="'toggle ' + node.name"
|
||||||
|
(click)="loadChildren(node)">
|
||||||
|
<span class="fas {{treeControl.isExpanded(node) ? 'fa-angle-down' : 'fa-angle-right'}}"
|
||||||
|
aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<label *ngIf="multiSelect" class="d-flex align-items-center m-0 p-0 form-check"
|
||||||
|
[class.text-success]="node.isSelected"
|
||||||
|
[ngbTooltip]="node.item?.otherInformation?.note"
|
||||||
|
[openDelay]="500"
|
||||||
|
container="body">
|
||||||
|
<input class="mr-2" type="checkbox"
|
||||||
|
[disabled]="!node.item?.selectable"
|
||||||
|
[(ngModel)]="node.isSelected"
|
||||||
|
[checked]="node.isSelected"
|
||||||
|
(change)="onSelect(node.item)"
|
||||||
|
>
|
||||||
|
<span>{{node.item.display}}</span>
|
||||||
|
</label>
|
||||||
|
<button *ngIf="!multiSelect" class="btn btn-outline-link btn-sm text-left"
|
||||||
|
[class.text-success]="node.isSelected"
|
||||||
|
[disabled]="!node.item?.selectable"
|
||||||
|
[ngbTooltip]="node.item?.otherInformation?.note"
|
||||||
|
[openDelay]="500"
|
||||||
|
container="body"
|
||||||
|
(click)="onSelect(node.item)">
|
||||||
|
<span>{{node.item.display}}</span>
|
||||||
|
</button>
|
||||||
|
</cdk-tree-node>
|
||||||
|
|
||||||
|
<cdk-tree-node *cdkTreeNodeDef="let node; when: isLoadMore" cdkTreeNodePadding>
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" (click)="loadMore(node.loadMoreParentItem)">
|
||||||
|
{{'vocabulary-treeview.load-more' | translate}}...
|
||||||
|
</button>
|
||||||
|
</cdk-tree-node>
|
||||||
|
|
||||||
|
<cdk-tree-node *cdkTreeNodeDef="let node; when: isLoadMoreRoot">
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" (click)="loadMoreRoot(node)">
|
||||||
|
{{'vocabulary-treeview.load-more' | translate}}...
|
||||||
|
</button>
|
||||||
|
</cdk-tree-node>
|
||||||
|
</cdk-tree>
|
||||||
|
</div>
|
||||||
|
@@ -5,3 +5,7 @@
|
|||||||
cdk-tree .btn:focus {
|
cdk-tree .btn:focus {
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
@@ -12,7 +12,7 @@ import { createTestComponent } from '../../testing/utils.test';
|
|||||||
import { VocabularyTreeviewComponent } from './vocabulary-treeview.component';
|
import { VocabularyTreeviewComponent } from './vocabulary-treeview.component';
|
||||||
import { VocabularyTreeviewService } from './vocabulary-treeview.service';
|
import { VocabularyTreeviewService } from './vocabulary-treeview.service';
|
||||||
import { VocabularyEntryDetail } from '../../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
|
import { VocabularyEntryDetail } from '../../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
|
||||||
import { TreeviewFlatNode } from './vocabulary-treeview-node.model';
|
import { TreeviewFlatNode, TreeviewNode } from './vocabulary-treeview-node.model';
|
||||||
import { FormFieldMetadataValueObject } from '../builder/models/form-field-metadata-value.model';
|
import { FormFieldMetadataValueObject } from '../builder/models/form-field-metadata-value.model';
|
||||||
import { VocabularyOptions } from '../../../core/submission/vocabularies/models/vocabulary-options.model';
|
import { VocabularyOptions } from '../../../core/submission/vocabularies/models/vocabulary-options.model';
|
||||||
import { PageInfo } from '../../../core/shared/page-info.model';
|
import { PageInfo } from '../../../core/shared/page-info.model';
|
||||||
@@ -20,6 +20,8 @@ import { VocabularyEntry } from '../../../core/submission/vocabularies/models/vo
|
|||||||
import { AuthTokenInfo } from '../../../core/auth/models/auth-token-info.model';
|
import { AuthTokenInfo } from '../../../core/auth/models/auth-token-info.model';
|
||||||
import { authReducer } from '../../../core/auth/auth.reducer';
|
import { authReducer } from '../../../core/auth/auth.reducer';
|
||||||
import { storeModuleConfig } from '../../../app.reducer';
|
import { storeModuleConfig } from '../../../app.reducer';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { VocabularyService } from '../../../core/submission/vocabularies/vocabulary.service';
|
||||||
|
|
||||||
describe('VocabularyTreeviewComponent test suite', () => {
|
describe('VocabularyTreeviewComponent test suite', () => {
|
||||||
|
|
||||||
@@ -27,6 +29,7 @@ describe('VocabularyTreeviewComponent test suite', () => {
|
|||||||
let compAsAny: any;
|
let compAsAny: any;
|
||||||
let fixture: ComponentFixture<VocabularyTreeviewComponent>;
|
let fixture: ComponentFixture<VocabularyTreeviewComponent>;
|
||||||
let initialState;
|
let initialState;
|
||||||
|
let de;
|
||||||
|
|
||||||
const item = new VocabularyEntryDetail();
|
const item = new VocabularyEntryDetail();
|
||||||
item.id = 'node1';
|
item.id = 'node1';
|
||||||
@@ -47,6 +50,14 @@ describe('VocabularyTreeviewComponent test suite', () => {
|
|||||||
restoreNodes: jasmine.createSpy('restoreNodes'),
|
restoreNodes: jasmine.createSpy('restoreNodes'),
|
||||||
cleanTree: jasmine.createSpy('cleanTree'),
|
cleanTree: jasmine.createSpy('cleanTree'),
|
||||||
});
|
});
|
||||||
|
const vocabularyServiceStub = jasmine.createSpyObj('VocabularyService', {
|
||||||
|
getVocabularyEntriesByValue: jasmine.createSpy('getVocabularyEntriesByValue'),
|
||||||
|
getEntryDetailParent: jasmine.createSpy('getEntryDetailParent'),
|
||||||
|
findEntryDetailById: jasmine.createSpy('findEntryDetailById'),
|
||||||
|
searchTopEntries: jasmine.createSpy('searchTopEntries'),
|
||||||
|
getEntryDetailChildren: jasmine.createSpy('getEntryDetailChildren'),
|
||||||
|
clearSearchTopRequests: jasmine.createSpy('clearSearchTopRequests')
|
||||||
|
});
|
||||||
|
|
||||||
initialState = {
|
initialState = {
|
||||||
core: {
|
core: {
|
||||||
@@ -75,6 +86,7 @@ describe('VocabularyTreeviewComponent test suite', () => {
|
|||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: VocabularyTreeviewService, useValue: vocabularyTreeviewServiceStub },
|
{ provide: VocabularyTreeviewService, useValue: vocabularyTreeviewServiceStub },
|
||||||
|
{ provide: VocabularyService, useValue: vocabularyServiceStub },
|
||||||
{ provide: NgbActiveModal, useValue: modalStub },
|
{ provide: NgbActiveModal, useValue: modalStub },
|
||||||
provideMockStore({ initialState }),
|
provideMockStore({ initialState }),
|
||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
@@ -117,13 +129,7 @@ describe('VocabularyTreeviewComponent test suite', () => {
|
|||||||
comp = fixture.componentInstance;
|
comp = fixture.componentInstance;
|
||||||
compAsAny = comp;
|
compAsAny = comp;
|
||||||
comp.vocabularyOptions = vocabularyOptions;
|
comp.vocabularyOptions = vocabularyOptions;
|
||||||
comp.selectedItem = null;
|
comp.selectedItems = [];
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
fixture.destroy();
|
|
||||||
comp = null;
|
|
||||||
compAsAny = null;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should should init component properly', () => {
|
it('should should init component properly', () => {
|
||||||
@@ -138,10 +144,10 @@ describe('VocabularyTreeviewComponent test suite', () => {
|
|||||||
currentValue.otherInformation = {
|
currentValue.otherInformation = {
|
||||||
id: 'entryID'
|
id: 'entryID'
|
||||||
};
|
};
|
||||||
comp.selectedItem = currentValue;
|
comp.selectedItems = [currentValue.value];
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(comp.dataSource.data).toEqual([]);
|
expect(comp.dataSource.data).toEqual([]);
|
||||||
expect(vocabularyTreeviewServiceStub.initialize).toHaveBeenCalledWith(comp.vocabularyOptions, new PageInfo(), null);
|
expect(vocabularyTreeviewServiceStub.initialize).toHaveBeenCalledWith(comp.vocabularyOptions, new PageInfo(), ['testValue'], null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should should init component properly with init value as VocabularyEntry', () => {
|
it('should should init component properly with init value as VocabularyEntry', () => {
|
||||||
@@ -150,30 +156,30 @@ describe('VocabularyTreeviewComponent test suite', () => {
|
|||||||
currentValue.otherInformation = {
|
currentValue.otherInformation = {
|
||||||
id: 'entryID'
|
id: 'entryID'
|
||||||
};
|
};
|
||||||
comp.selectedItem = currentValue;
|
comp.selectedItems = [currentValue.value];
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(comp.dataSource.data).toEqual([]);
|
expect(comp.dataSource.data).toEqual([]);
|
||||||
expect(vocabularyTreeviewServiceStub.initialize).toHaveBeenCalledWith(comp.vocabularyOptions, new PageInfo(), null);
|
expect(vocabularyTreeviewServiceStub.initialize).toHaveBeenCalledWith(comp.vocabularyOptions, new PageInfo(), ['testValue'], null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call loadMore function', () => {
|
it('should call loadMore function', () => {
|
||||||
comp.loadMore(item);
|
comp.loadMore(item);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(vocabularyTreeviewServiceStub.loadMore).toHaveBeenCalledWith(item);
|
expect(vocabularyTreeviewServiceStub.loadMore).toHaveBeenCalledWith(item, []);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call loadMoreRoot function', () => {
|
it('should call loadMoreRoot function', () => {
|
||||||
const node = new TreeviewFlatNode(item);
|
const node = new TreeviewFlatNode(item);
|
||||||
comp.loadMoreRoot(node);
|
comp.loadMoreRoot(node);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(vocabularyTreeviewServiceStub.loadMoreRoot).toHaveBeenCalledWith(node);
|
expect(vocabularyTreeviewServiceStub.loadMoreRoot).toHaveBeenCalledWith(node, []);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call loadChildren function', () => {
|
it('should call loadChildren function', () => {
|
||||||
const node = new TreeviewFlatNode(item);
|
const node = new TreeviewFlatNode(item);
|
||||||
comp.loadChildren(node);
|
comp.loadChildren(node);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(vocabularyTreeviewServiceStub.loadMore).toHaveBeenCalledWith(node.item, true);
|
expect(vocabularyTreeviewServiceStub.loadMore).toHaveBeenCalledWith(node.item, [], true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should emit select event', () => {
|
it('should emit select event', () => {
|
||||||
@@ -188,7 +194,7 @@ describe('VocabularyTreeviewComponent test suite', () => {
|
|||||||
comp.nodeMap.set('test', new TreeviewFlatNode(item));
|
comp.nodeMap.set('test', new TreeviewFlatNode(item));
|
||||||
comp.search();
|
comp.search();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(vocabularyTreeviewServiceStub.searchByQuery).toHaveBeenCalledWith('test search');
|
expect(vocabularyTreeviewServiceStub.searchByQuery).toHaveBeenCalledWith('test search', []);
|
||||||
expect(comp.storedNodeMap).toEqual(nodeMap);
|
expect(comp.storedNodeMap).toEqual(nodeMap);
|
||||||
expect(comp.nodeMap).toEqual(emptyNodeMap);
|
expect(comp.nodeMap).toEqual(emptyNodeMap);
|
||||||
});
|
});
|
||||||
@@ -199,7 +205,7 @@ describe('VocabularyTreeviewComponent test suite', () => {
|
|||||||
comp.storedNodeMap.set('test', new TreeviewFlatNode(item2));
|
comp.storedNodeMap.set('test', new TreeviewFlatNode(item2));
|
||||||
comp.search();
|
comp.search();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(vocabularyTreeviewServiceStub.searchByQuery).toHaveBeenCalledWith('test search');
|
expect(vocabularyTreeviewServiceStub.searchByQuery).toHaveBeenCalledWith('test search', []);
|
||||||
expect(comp.storedNodeMap).toEqual(storedNodeMap);
|
expect(comp.storedNodeMap).toEqual(storedNodeMap);
|
||||||
expect(comp.nodeMap).toEqual(emptyNodeMap);
|
expect(comp.nodeMap).toEqual(emptyNodeMap);
|
||||||
});
|
});
|
||||||
@@ -229,6 +235,50 @@ describe('VocabularyTreeviewComponent test suite', () => {
|
|||||||
expect(vocabularyTreeviewServiceStub.cleanTree).toHaveBeenCalled();
|
expect(vocabularyTreeviewServiceStub.cleanTree).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vocabularyTreeviewServiceStub.getData.and.returnValue(observableOf([
|
||||||
|
{
|
||||||
|
'item': {
|
||||||
|
'id': 'srsc:SCB11',
|
||||||
|
'display': 'HUMANITIES and RELIGION'
|
||||||
|
}
|
||||||
|
} as TreeviewNode,
|
||||||
|
{
|
||||||
|
'item': {
|
||||||
|
'id': 'srsc:SCB12',
|
||||||
|
'display': 'LAW/JURISPRUDENCE'
|
||||||
|
}
|
||||||
|
} as TreeviewNode,
|
||||||
|
{
|
||||||
|
'item': {
|
||||||
|
'id': 'srsc:SCB13',
|
||||||
|
'display': 'SOCIAL SCIENCES'
|
||||||
|
}
|
||||||
|
} as TreeviewNode,
|
||||||
|
]));
|
||||||
|
fixture = TestBed.createComponent(VocabularyTreeviewComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
compAsAny = comp;
|
||||||
|
comp.vocabularyOptions = vocabularyOptions;
|
||||||
|
comp.selectedItems = [];
|
||||||
|
de = fixture.debugElement;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not display checkboxes by default', async () => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(de.query(By.css('input[type=checkbox]'))).toBeNull();
|
||||||
|
expect(de.queryAll(By.css('cdk-tree-node')).length).toEqual(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display checkboxes if multiSelect is true', async () => {
|
||||||
|
comp.multiSelect = true;
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(de.queryAll(By.css('input[type=checkbox]')).length).toEqual(3);
|
||||||
|
expect(de.queryAll(By.css('cdk-tree-node')).length).toEqual(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// declare a test component
|
// declare a test component
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
import { FlatTreeControl } from '@angular/cdk/tree';
|
import { FlatTreeControl } from '@angular/cdk/tree';
|
||||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||||
|
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
import { Observable, Subscription } from 'rxjs';
|
import { Observable, Subscription } from 'rxjs';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
@@ -18,9 +17,11 @@ import { VocabularyTreeFlattener } from './vocabulary-tree-flattener';
|
|||||||
import { VocabularyTreeFlatDataSource } from './vocabulary-tree-flat-data-source';
|
import { VocabularyTreeFlatDataSource } from './vocabulary-tree-flat-data-source';
|
||||||
import { CoreState } from '../../../core/core-state.model';
|
import { CoreState } from '../../../core/core-state.model';
|
||||||
import { lowerCase } from 'lodash/string';
|
import { lowerCase } from 'lodash/string';
|
||||||
|
import { VocabularyService } from '../../../core/submission/vocabularies/vocabulary.service';
|
||||||
|
import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component that show a hierarchical vocabulary in a tree view
|
* Component that shows a hierarchical vocabulary in a tree view
|
||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-vocabulary-treeview',
|
selector: 'ds-vocabulary-treeview',
|
||||||
@@ -40,9 +41,14 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit {
|
|||||||
@Input() preloadLevel = 2;
|
@Input() preloadLevel = 2;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The vocabulary entry already selected, if any
|
* The vocabulary entries already selected, if any
|
||||||
*/
|
*/
|
||||||
@Input() selectedItem: any = null;
|
@Input() selectedItems: string[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to allow selecting multiple values with checkboxes
|
||||||
|
*/
|
||||||
|
@Input() multiSelect = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contain a descriptive message for this vocabulary retrieved from i18n files
|
* Contain a descriptive message for this vocabulary retrieved from i18n files
|
||||||
@@ -90,6 +96,12 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit {
|
|||||||
*/
|
*/
|
||||||
@Output() select: EventEmitter<VocabularyEntryDetail> = new EventEmitter<VocabularyEntryDetail>(null);
|
@Output() select: EventEmitter<VocabularyEntryDetail> = new EventEmitter<VocabularyEntryDetail>(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An event fired when a vocabulary entry is deselected.
|
||||||
|
* Event's payload equals to {@link VocabularyEntryDetail} deselected.
|
||||||
|
*/
|
||||||
|
@Output() deselect: EventEmitter<VocabularyEntryDetail> = new EventEmitter<VocabularyEntryDetail>(null);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A boolean representing if user is authenticated
|
* A boolean representing if user is authenticated
|
||||||
*/
|
*/
|
||||||
@@ -103,14 +115,14 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit {
|
|||||||
/**
|
/**
|
||||||
* Initialize instance variables
|
* Initialize instance variables
|
||||||
*
|
*
|
||||||
* @param {NgbActiveModal} activeModal
|
|
||||||
* @param {VocabularyTreeviewService} vocabularyTreeviewService
|
* @param {VocabularyTreeviewService} vocabularyTreeviewService
|
||||||
|
* @param {vocabularyService} vocabularyService
|
||||||
* @param {Store<CoreState>} store
|
* @param {Store<CoreState>} store
|
||||||
* @param {TranslateService} translate
|
* @param {TranslateService} translate
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
public activeModal: NgbActiveModal,
|
|
||||||
private vocabularyTreeviewService: VocabularyTreeviewService,
|
private vocabularyTreeviewService: VocabularyTreeviewService,
|
||||||
|
private vocabularyService: VocabularyService,
|
||||||
private store: Store<CoreState>,
|
private store: Store<CoreState>,
|
||||||
private translate: TranslateService
|
private translate: TranslateService
|
||||||
) {
|
) {
|
||||||
@@ -148,7 +160,8 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit {
|
|||||||
node.pageInfo,
|
node.pageInfo,
|
||||||
node.loadMoreParentItem,
|
node.loadMoreParentItem,
|
||||||
node.isSearchNode,
|
node.isSearchNode,
|
||||||
node.isInInitValueHierarchy
|
node.isInInitValueHierarchy,
|
||||||
|
node.isSelected
|
||||||
);
|
);
|
||||||
this.nodeMap.set(node.item.id, newNode);
|
this.nodeMap.set(node.item.id, newNode);
|
||||||
|
|
||||||
@@ -211,7 +224,7 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit {
|
|||||||
|
|
||||||
this.loading = this.vocabularyTreeviewService.isLoading();
|
this.loading = this.vocabularyTreeviewService.isLoading();
|
||||||
|
|
||||||
this.vocabularyTreeviewService.initialize(this.vocabularyOptions, new PageInfo(), null);
|
this.vocabularyTreeviewService.initialize(this.vocabularyOptions, new PageInfo(), this.selectedItems, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -219,7 +232,7 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit {
|
|||||||
* @param item The VocabularyEntryDetail for which to load more nodes
|
* @param item The VocabularyEntryDetail for which to load more nodes
|
||||||
*/
|
*/
|
||||||
loadMore(item: VocabularyEntryDetail) {
|
loadMore(item: VocabularyEntryDetail) {
|
||||||
this.vocabularyTreeviewService.loadMore(item);
|
this.vocabularyTreeviewService.loadMore(item, this.selectedItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -227,7 +240,7 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit {
|
|||||||
* @param node The TreeviewFlatNode for which to load more nodes
|
* @param node The TreeviewFlatNode for which to load more nodes
|
||||||
*/
|
*/
|
||||||
loadMoreRoot(node: TreeviewFlatNode) {
|
loadMoreRoot(node: TreeviewFlatNode) {
|
||||||
this.vocabularyTreeviewService.loadMoreRoot(node);
|
this.vocabularyTreeviewService.loadMoreRoot(node, this.selectedItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -235,16 +248,20 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit {
|
|||||||
* @param node The TreeviewFlatNode for which to load children nodes
|
* @param node The TreeviewFlatNode for which to load children nodes
|
||||||
*/
|
*/
|
||||||
loadChildren(node: TreeviewFlatNode) {
|
loadChildren(node: TreeviewFlatNode) {
|
||||||
this.vocabularyTreeviewService.loadMore(node.item, true);
|
this.vocabularyTreeviewService.loadMore(node.item, this.selectedItems, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method called on entry select
|
* Method called on entry select/deselect
|
||||||
* Emit a new select Event
|
|
||||||
*/
|
*/
|
||||||
onSelect(item: VocabularyEntryDetail) {
|
onSelect(item: VocabularyEntryDetail) {
|
||||||
this.select.emit(item);
|
if (!this.selectedItems.includes(item.id)) {
|
||||||
this.activeModal.close(item);
|
this.selectedItems.push(item.id);
|
||||||
|
this.select.emit(item);
|
||||||
|
} else {
|
||||||
|
this.selectedItems = this.selectedItems.filter((detail: string) => { return detail !== item.id; });
|
||||||
|
this.deselect.emit(item);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -256,7 +273,7 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit {
|
|||||||
this.storedNodeMap = this.nodeMap;
|
this.storedNodeMap = this.nodeMap;
|
||||||
}
|
}
|
||||||
this.nodeMap = new Map<string, TreeviewFlatNode>();
|
this.nodeMap = new Map<string, TreeviewFlatNode>();
|
||||||
this.vocabularyTreeviewService.searchByQuery(this.searchText);
|
this.vocabularyTreeviewService.searchByQuery(this.searchText, this.selectedItems);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,13 +288,22 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit {
|
|||||||
* Reset tree resulting from a previous search
|
* Reset tree resulting from a previous search
|
||||||
*/
|
*/
|
||||||
reset() {
|
reset() {
|
||||||
|
this.searchText = '';
|
||||||
|
for (const item of this.selectedItems) {
|
||||||
|
this.subs.push(this.vocabularyService.findEntryDetailById(item, this.vocabularyOptions.name, true, true, false).pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
).subscribe((detail: VocabularyEntryDetail) => {
|
||||||
|
this.deselect.emit(detail);
|
||||||
|
}));
|
||||||
|
this.nodeMap.get(item).isSelected = false;
|
||||||
|
}
|
||||||
|
this.selectedItems = [];
|
||||||
|
|
||||||
if (isNotEmpty(this.storedNodeMap)) {
|
if (isNotEmpty(this.storedNodeMap)) {
|
||||||
this.nodeMap = this.storedNodeMap;
|
this.nodeMap = this.storedNodeMap;
|
||||||
this.storedNodeMap = new Map<string, TreeviewFlatNode>();
|
this.storedNodeMap = new Map<string, TreeviewFlatNode>();
|
||||||
this.vocabularyTreeviewService.restoreNodes();
|
this.vocabularyTreeviewService.restoreNodes();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.searchText = '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -192,7 +192,7 @@ describe('VocabularyTreeviewService test suite', () => {
|
|||||||
a: createSuccessfulRemoteDataObject(buildPaginatedList(pageInfo, [item, item2, item3]))
|
a: createSuccessfulRemoteDataObject(buildPaginatedList(pageInfo, [item, item2, item3]))
|
||||||
}));
|
}));
|
||||||
|
|
||||||
scheduler.schedule(() => service.initialize(vocabularyOptions, pageInfo));
|
scheduler.schedule(() => service.initialize(vocabularyOptions, pageInfo, []));
|
||||||
scheduler.flush();
|
scheduler.flush();
|
||||||
|
|
||||||
expect(serviceAsAny.vocabularyName).toEqual(vocabularyOptions.name);
|
expect(serviceAsAny.vocabularyName).toEqual(vocabularyOptions.name);
|
||||||
@@ -214,7 +214,7 @@ describe('VocabularyTreeviewService test suite', () => {
|
|||||||
b: createSuccessfulRemoteDataObject(item)
|
b: createSuccessfulRemoteDataObject(item)
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
scheduler.schedule(() => service.initialize(vocabularyOptions, pageInfo, 'root2'));
|
scheduler.schedule(() => service.initialize(vocabularyOptions, pageInfo, [], 'root2'));
|
||||||
scheduler.flush();
|
scheduler.flush();
|
||||||
|
|
||||||
expect(serviceAsAny.vocabularyName).toEqual(vocabularyOptions.name);
|
expect(serviceAsAny.vocabularyName).toEqual(vocabularyOptions.name);
|
||||||
@@ -233,11 +233,11 @@ describe('VocabularyTreeviewService test suite', () => {
|
|||||||
describe('loadMoreRoot', () => {
|
describe('loadMoreRoot', () => {
|
||||||
it('should call retrieveTopNodes properly', () => {
|
it('should call retrieveTopNodes properly', () => {
|
||||||
spyOn(serviceAsAny, 'retrieveTopNodes');
|
spyOn(serviceAsAny, 'retrieveTopNodes');
|
||||||
service.initialize(vocabularyOptions, new PageInfo());
|
service.initialize(vocabularyOptions, new PageInfo(), []);
|
||||||
serviceAsAny.dataChange.next(treeNodeListWithLoadMoreRoot);
|
serviceAsAny.dataChange.next(treeNodeListWithLoadMoreRoot);
|
||||||
service.loadMoreRoot(loadMoreRootFlatNode);
|
service.loadMoreRoot(loadMoreRootFlatNode, []);
|
||||||
|
|
||||||
expect(serviceAsAny.retrieveTopNodes).toHaveBeenCalledWith(loadMoreRootFlatNode.pageInfo, treeNodeList);
|
expect(serviceAsAny.retrieveTopNodes).toHaveBeenCalledWith(loadMoreRootFlatNode.pageInfo, treeNodeList, []);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -263,7 +263,7 @@ describe('VocabularyTreeviewService test suite', () => {
|
|||||||
serviceAsAny.nodeMap = nodeMapWithChildren;
|
serviceAsAny.nodeMap = nodeMapWithChildren;
|
||||||
treeNodeListWithChildren.push(new TreeviewNode(child2, false, new PageInfo(), item));
|
treeNodeListWithChildren.push(new TreeviewNode(child2, false, new PageInfo(), item));
|
||||||
|
|
||||||
scheduler.schedule(() => service.loadMore(item));
|
scheduler.schedule(() => service.loadMore(item, []));
|
||||||
scheduler.flush();
|
scheduler.flush();
|
||||||
|
|
||||||
expect(serviceAsAny.dataChange.value).toEqual(treeNodeListWithChildren);
|
expect(serviceAsAny.dataChange.value).toEqual(treeNodeListWithChildren);
|
||||||
@@ -285,7 +285,7 @@ describe('VocabularyTreeviewService test suite', () => {
|
|||||||
treeNodeListWithChildren.push(childNode2);
|
treeNodeListWithChildren.push(childNode2);
|
||||||
treeNodeListWithChildren.push(loadMoreNode);
|
treeNodeListWithChildren.push(loadMoreNode);
|
||||||
|
|
||||||
scheduler.schedule(() => service.loadMore(item));
|
scheduler.schedule(() => service.loadMore(item, []));
|
||||||
scheduler.flush();
|
scheduler.flush();
|
||||||
|
|
||||||
expect(serviceAsAny.dataChange.value).toEqual(treeNodeListWithChildren);
|
expect(serviceAsAny.dataChange.value).toEqual(treeNodeListWithChildren);
|
||||||
@@ -319,7 +319,7 @@ describe('VocabularyTreeviewService test suite', () => {
|
|||||||
);
|
);
|
||||||
vocabularyOptions.query = 'root1-child1-child1';
|
vocabularyOptions.query = 'root1-child1-child1';
|
||||||
|
|
||||||
scheduler.schedule(() => service.searchByQuery(vocabularyOptions));
|
scheduler.schedule(() => service.searchByQuery(vocabularyOptions, []));
|
||||||
scheduler.flush();
|
scheduler.flush();
|
||||||
|
|
||||||
// We can't check the tree by comparing root TreeviewNodes directly in this particular test;
|
// We can't check the tree by comparing root TreeviewNodes directly in this particular test;
|
||||||
|
@@ -25,7 +25,9 @@ import { VocabularyEntryDetail } from '../../../core/submission/vocabularies/mod
|
|||||||
/**
|
/**
|
||||||
* A service that provides methods to deal with vocabulary tree
|
* A service that provides methods to deal with vocabulary tree
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
export class VocabularyTreeviewService {
|
export class VocabularyTreeviewService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -101,21 +103,22 @@ export class VocabularyTreeviewService {
|
|||||||
*
|
*
|
||||||
* @param options The {@link VocabularyOptions} object
|
* @param options The {@link VocabularyOptions} object
|
||||||
* @param pageInfo The {@link PageInfo} object
|
* @param pageInfo The {@link PageInfo} object
|
||||||
|
* @param selectedItems The currently selected items
|
||||||
* @param initValueId The entry id of the node to mark as selected, if any
|
* @param initValueId The entry id of the node to mark as selected, if any
|
||||||
*/
|
*/
|
||||||
initialize(options: VocabularyOptions, pageInfo: PageInfo, initValueId?: string): void {
|
initialize(options: VocabularyOptions, pageInfo: PageInfo, selectedItems: string[], initValueId?: string): void {
|
||||||
this.loading.next(true);
|
this.loading.next(true);
|
||||||
this.vocabularyOptions = options;
|
this.vocabularyOptions = options;
|
||||||
this.vocabularyName = options.name;
|
this.vocabularyName = options.name;
|
||||||
this.pageInfo = pageInfo;
|
this.pageInfo = pageInfo;
|
||||||
if (isNotEmpty(initValueId)) {
|
if (isNotEmpty(initValueId)) {
|
||||||
this.getNodeHierarchyById(initValueId)
|
this.getNodeHierarchyById(initValueId, selectedItems)
|
||||||
.subscribe((hierarchy: string[]) => {
|
.subscribe((hierarchy: string[]) => {
|
||||||
this.initValueHierarchy = hierarchy;
|
this.initValueHierarchy = hierarchy;
|
||||||
this.retrieveTopNodes(pageInfo, []);
|
this.retrieveTopNodes(pageInfo, [], selectedItems);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.retrieveTopNodes(pageInfo, []);
|
this.retrieveTopNodes(pageInfo, [], selectedItems);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,19 +132,21 @@ export class VocabularyTreeviewService {
|
|||||||
/**
|
/**
|
||||||
* Expand the root node whose children are not loaded
|
* Expand the root node whose children are not loaded
|
||||||
* @param node The root node
|
* @param node The root node
|
||||||
|
* @param selectedItems The currently selected items
|
||||||
*/
|
*/
|
||||||
loadMoreRoot(node: TreeviewFlatNode) {
|
loadMoreRoot(node: TreeviewFlatNode, selectedItems: string[]) {
|
||||||
const nodes = this.dataChange.value;
|
const nodes = this.dataChange.value;
|
||||||
nodes.pop();
|
nodes.pop();
|
||||||
this.retrieveTopNodes(node.pageInfo, nodes);
|
this.retrieveTopNodes(node.pageInfo, nodes, selectedItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Expand a node whose children are not loaded
|
* Expand a node whose children are not loaded
|
||||||
* @param item
|
* @param item
|
||||||
|
* @param selectedItems
|
||||||
* @param onlyFirstTime
|
* @param onlyFirstTime
|
||||||
*/
|
*/
|
||||||
loadMore(item: VocabularyEntryDetail, onlyFirstTime = false) {
|
loadMore(item: VocabularyEntryDetail, selectedItems: string[], onlyFirstTime = false) {
|
||||||
if (!this.nodeMap.has(item.otherInformation.id)) {
|
if (!this.nodeMap.has(item.otherInformation.id)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -154,7 +159,7 @@ export class VocabularyTreeviewService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newNodes: TreeviewNode[] = list.page.map((entry) => this._generateNode(entry));
|
const newNodes: TreeviewNode[] = list.page.map((entry) => this._generateNode(entry, selectedItems));
|
||||||
children.push(...newNodes);
|
children.push(...newNodes);
|
||||||
|
|
||||||
if ((list.pageInfo.currentPage + 1) <= list.pageInfo.totalPages) {
|
if ((list.pageInfo.currentPage + 1) <= list.pageInfo.totalPages) {
|
||||||
@@ -183,7 +188,7 @@ export class VocabularyTreeviewService {
|
|||||||
/**
|
/**
|
||||||
* Perform a search operation by query
|
* Perform a search operation by query
|
||||||
*/
|
*/
|
||||||
searchByQuery(query: string) {
|
searchByQuery(query: string, selectedItems: string[]) {
|
||||||
this.loading.next(true);
|
this.loading.next(true);
|
||||||
if (isEmpty(this.storedNodes)) {
|
if (isEmpty(this.storedNodes)) {
|
||||||
this.storedNodes = this.dataChange.value;
|
this.storedNodes = this.dataChange.value;
|
||||||
@@ -200,7 +205,7 @@ export class VocabularyTreeviewService {
|
|||||||
getFirstSucceededRemoteDataPayload()
|
getFirstSucceededRemoteDataPayload()
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
mergeMap((entry: VocabularyEntryDetail) => this.getNodeHierarchy(entry)),
|
mergeMap((entry: VocabularyEntryDetail) => this.getNodeHierarchy(entry, selectedItems)),
|
||||||
scan((acc: TreeviewNode[], value: TreeviewNode) => {
|
scan((acc: TreeviewNode[], value: TreeviewNode) => {
|
||||||
if (isEmpty(value) || findIndex(acc, (node) => node.item.otherInformation.id === value.item.otherInformation.id) !== -1) {
|
if (isEmpty(value) || findIndex(acc, (node) => node.item.otherInformation.id === value.item.otherInformation.id) !== -1) {
|
||||||
return acc;
|
return acc;
|
||||||
@@ -231,11 +236,12 @@ export class VocabularyTreeviewService {
|
|||||||
* Generate a {@link TreeviewNode} object from vocabulary entry
|
* Generate a {@link TreeviewNode} object from vocabulary entry
|
||||||
*
|
*
|
||||||
* @param entry The vocabulary entry detail
|
* @param entry The vocabulary entry detail
|
||||||
|
* @param selectedItems An array containing the currently selected items
|
||||||
* @param isSearchNode A Boolean representing if given entry is the result of a search
|
* @param isSearchNode A Boolean representing if given entry is the result of a search
|
||||||
* @param toStore A Boolean representing if the node created is to store or not
|
* @param toStore A Boolean representing if the node created is to store or not
|
||||||
* @return TreeviewNode
|
* @return TreeviewNode
|
||||||
*/
|
*/
|
||||||
private _generateNode(entry: VocabularyEntryDetail, isSearchNode = false, toStore = true): TreeviewNode {
|
private _generateNode(entry: VocabularyEntryDetail, selectedItems: string[], isSearchNode = false, toStore = true): TreeviewNode {
|
||||||
const entryId = entry.otherInformation.id;
|
const entryId = entry.otherInformation.id;
|
||||||
if (this.nodeMap.has(entryId)) {
|
if (this.nodeMap.has(entryId)) {
|
||||||
return this.nodeMap.get(entryId)!;
|
return this.nodeMap.get(entryId)!;
|
||||||
@@ -243,13 +249,15 @@ export class VocabularyTreeviewService {
|
|||||||
const hasChildren = entry.hasOtherInformation() && (entry.otherInformation as any)!.hasChildren === 'true';
|
const hasChildren = entry.hasOtherInformation() && (entry.otherInformation as any)!.hasChildren === 'true';
|
||||||
const pageInfo: PageInfo = this.pageInfo;
|
const pageInfo: PageInfo = this.pageInfo;
|
||||||
const isInInitValueHierarchy = this.initValueHierarchy.includes(entryId);
|
const isInInitValueHierarchy = this.initValueHierarchy.includes(entryId);
|
||||||
|
const isSelected: boolean = selectedItems.some(() => selectedItems.includes(entry.id));
|
||||||
const result = new TreeviewNode(
|
const result = new TreeviewNode(
|
||||||
entry,
|
entry,
|
||||||
hasChildren,
|
hasChildren,
|
||||||
pageInfo,
|
pageInfo,
|
||||||
null,
|
null,
|
||||||
isSearchNode,
|
isSearchNode,
|
||||||
isInInitValueHierarchy);
|
isInInitValueHierarchy,
|
||||||
|
isSelected);
|
||||||
|
|
||||||
if (toStore) {
|
if (toStore) {
|
||||||
this.nodeMap.set(entryId, result);
|
this.nodeMap.set(entryId, result);
|
||||||
@@ -260,12 +268,13 @@ export class VocabularyTreeviewService {
|
|||||||
/**
|
/**
|
||||||
* Return the node Hierarchy by a given node's id
|
* Return the node Hierarchy by a given node's id
|
||||||
* @param id The node id
|
* @param id The node id
|
||||||
|
* @param selectedItems The currently selected items
|
||||||
* @return Observable<string[]>
|
* @return Observable<string[]>
|
||||||
*/
|
*/
|
||||||
private getNodeHierarchyById(id: string): Observable<string[]> {
|
private getNodeHierarchyById(id: string, selectedItems: string[]): Observable<string[]> {
|
||||||
return this.getById(id).pipe(
|
return this.getById(id).pipe(
|
||||||
mergeMap((entry: VocabularyEntryDetail) => this.getNodeHierarchy(entry, [], false)),
|
mergeMap((entry: VocabularyEntryDetail) => this.getNodeHierarchy(entry, selectedItems,[], false)),
|
||||||
map((node: TreeviewNode) => this.getNodeHierarchyIds(node))
|
map((node: TreeviewNode) => this.getNodeHierarchyIds(node, selectedItems))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,13 +315,14 @@ export class VocabularyTreeviewService {
|
|||||||
* Retrieve the top level vocabulary entries
|
* Retrieve the top level vocabulary entries
|
||||||
* @param pageInfo The {@link PageInfo} object
|
* @param pageInfo The {@link PageInfo} object
|
||||||
* @param nodes The top level nodes already loaded, if any
|
* @param nodes The top level nodes already loaded, if any
|
||||||
|
* @param selectedItems The currently selected items
|
||||||
*/
|
*/
|
||||||
private retrieveTopNodes(pageInfo: PageInfo, nodes: TreeviewNode[]): void {
|
private retrieveTopNodes(pageInfo: PageInfo, nodes: TreeviewNode[], selectedItems: string[]): void {
|
||||||
this.vocabularyService.searchTopEntries(this.vocabularyName, pageInfo).pipe(
|
this.vocabularyService.searchTopEntries(this.vocabularyName, pageInfo).pipe(
|
||||||
getFirstSucceededRemoteDataPayload()
|
getFirstSucceededRemoteDataPayload()
|
||||||
).subscribe((list: PaginatedList<VocabularyEntryDetail>) => {
|
).subscribe((list: PaginatedList<VocabularyEntryDetail>) => {
|
||||||
this.vocabularyService.clearSearchTopRequests();
|
this.vocabularyService.clearSearchTopRequests();
|
||||||
const newNodes: TreeviewNode[] = list.page.map((entry: VocabularyEntryDetail) => this._generateNode(entry));
|
const newNodes: TreeviewNode[] = list.page.map((entry: VocabularyEntryDetail) => this._generateNode(entry, selectedItems));
|
||||||
nodes.push(...newNodes);
|
nodes.push(...newNodes);
|
||||||
|
|
||||||
if ((list.pageInfo.currentPage + 1) <= list.pageInfo.totalPages) {
|
if ((list.pageInfo.currentPage + 1) <= list.pageInfo.totalPages) {
|
||||||
@@ -334,15 +344,16 @@ export class VocabularyTreeviewService {
|
|||||||
* Build and return the tree node hierarchy by a given vocabulary entry
|
* Build and return the tree node hierarchy by a given vocabulary entry
|
||||||
*
|
*
|
||||||
* @param item The vocabulary entry
|
* @param item The vocabulary entry
|
||||||
|
* @param selectedItems The currently selected items
|
||||||
* @param children The vocabulary entry
|
* @param children The vocabulary entry
|
||||||
* @param toStore A Boolean representing if the node created is to store or not
|
* @param toStore A Boolean representing if the node created is to store or not
|
||||||
* @return Observable<string[]>
|
* @return Observable<string[]>
|
||||||
*/
|
*/
|
||||||
private getNodeHierarchy(item: VocabularyEntryDetail, children?: TreeviewNode[], toStore = true): Observable<TreeviewNode> {
|
private getNodeHierarchy(item: VocabularyEntryDetail, selectedItems: string[], children?: TreeviewNode[], toStore = true): Observable<TreeviewNode> {
|
||||||
if (isEmpty(item)) {
|
if (isEmpty(item)) {
|
||||||
return observableOf(null);
|
return observableOf(null);
|
||||||
}
|
}
|
||||||
const node = this._generateNode(item, toStore, toStore);
|
const node = this._generateNode(item, selectedItems, toStore, toStore);
|
||||||
|
|
||||||
if (isNotEmpty(children)) {
|
if (isNotEmpty(children)) {
|
||||||
const newChildren = children
|
const newChildren = children
|
||||||
@@ -357,7 +368,7 @@ export class VocabularyTreeviewService {
|
|||||||
|
|
||||||
if (node.item.hasOtherInformation() && isNotEmpty(node.item.otherInformation.parent)) {
|
if (node.item.hasOtherInformation() && isNotEmpty(node.item.otherInformation.parent)) {
|
||||||
return this.getParentNode(node.item.otherInformation.id).pipe(
|
return this.getParentNode(node.item.otherInformation.id).pipe(
|
||||||
mergeMap((parentItem: VocabularyEntryDetail) => this.getNodeHierarchy(parentItem, [node], toStore))
|
mergeMap((parentItem: VocabularyEntryDetail) => this.getNodeHierarchy(parentItem, selectedItems, [node], toStore))
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return observableOf(node);
|
return observableOf(node);
|
||||||
@@ -368,15 +379,16 @@ export class VocabularyTreeviewService {
|
|||||||
* Build and return the node Hierarchy ids by a given node
|
* Build and return the node Hierarchy ids by a given node
|
||||||
*
|
*
|
||||||
* @param node The given node
|
* @param node The given node
|
||||||
|
* @param selectedItems The currently selected items
|
||||||
* @param hierarchyIds The ids already present in the Hierarchy's array
|
* @param hierarchyIds The ids already present in the Hierarchy's array
|
||||||
* @return string[]
|
* @return string[]
|
||||||
*/
|
*/
|
||||||
private getNodeHierarchyIds(node: TreeviewNode, hierarchyIds: string[] = []): string[] {
|
private getNodeHierarchyIds(node: TreeviewNode, selectedItems: string[], hierarchyIds: string[] = []): string[] {
|
||||||
if (!hierarchyIds.includes(node.item.otherInformation.id)) {
|
if (!hierarchyIds.includes(node.item.otherInformation.id)) {
|
||||||
hierarchyIds.push(node.item.otherInformation.id);
|
hierarchyIds.push(node.item.otherInformation.id);
|
||||||
}
|
}
|
||||||
if (isNotEmpty(node.children)) {
|
if (isNotEmpty(node.children)) {
|
||||||
return this.getNodeHierarchyIds(node.children[0], hierarchyIds);
|
return this.getNodeHierarchyIds(node.children[0], selectedItems, hierarchyIds);
|
||||||
} else {
|
} else {
|
||||||
return hierarchyIds;
|
return hierarchyIds;
|
||||||
}
|
}
|
||||||
|
@@ -2,6 +2,7 @@ import { MetadataRepresentationType } from '../../../../core/shared/metadata-rep
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { MetadataRepresentationListElementComponent } from '../metadata-representation-list-element.component';
|
import { MetadataRepresentationListElementComponent } from '../metadata-representation-list-element.component';
|
||||||
import { metadataRepresentationComponent } from '../../../metadata-representation/metadata-representation.decorator';
|
import { metadataRepresentationComponent } from '../../../metadata-representation/metadata-representation.decorator';
|
||||||
|
import { VALUE_LIST_BROWSE_DEFINITION } from '../../../../core/shared/value-list-browse-definition.resource-type';
|
||||||
//@metadataRepresentationComponent('Publication', MetadataRepresentationType.PlainText)
|
//@metadataRepresentationComponent('Publication', MetadataRepresentationType.PlainText)
|
||||||
// For now, authority controlled fields are rendered the same way as plain text fields
|
// For now, authority controlled fields are rendered the same way as plain text fields
|
||||||
//@metadataRepresentationComponent('Publication', MetadataRepresentationType.AuthorityControlled)
|
//@metadataRepresentationComponent('Publication', MetadataRepresentationType.AuthorityControlled)
|
||||||
@@ -21,7 +22,7 @@ export class BrowseLinkMetadataListElementComponent extends MetadataRepresentati
|
|||||||
*/
|
*/
|
||||||
getQueryParams() {
|
getQueryParams() {
|
||||||
let queryParams = {startsWith: this.mdRepresentation.getValue()};
|
let queryParams = {startsWith: this.mdRepresentation.getValue()};
|
||||||
if (this.mdRepresentation.browseDefinition.metadataBrowse) {
|
if (this.mdRepresentation.browseDefinition.getRenderType() === VALUE_LIST_BROWSE_DEFINITION.value) {
|
||||||
return {value: this.mdRepresentation.getValue()};
|
return {value: this.mdRepresentation.getValue()};
|
||||||
}
|
}
|
||||||
return queryParams;
|
return queryParams;
|
||||||
|
@@ -2,6 +2,7 @@ import { MetadataRepresentationType } from '../../../../core/shared/metadata-rep
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { MetadataRepresentationListElementComponent } from '../metadata-representation-list-element.component';
|
import { MetadataRepresentationListElementComponent } from '../metadata-representation-list-element.component';
|
||||||
import { metadataRepresentationComponent } from '../../../metadata-representation/metadata-representation.decorator';
|
import { metadataRepresentationComponent } from '../../../metadata-representation/metadata-representation.decorator';
|
||||||
|
import { VALUE_LIST_BROWSE_DEFINITION } from '../../../../core/shared/value-list-browse-definition.resource-type';
|
||||||
|
|
||||||
@metadataRepresentationComponent('Publication', MetadataRepresentationType.PlainText)
|
@metadataRepresentationComponent('Publication', MetadataRepresentationType.PlainText)
|
||||||
// For now, authority controlled fields are rendered the same way as plain text fields
|
// For now, authority controlled fields are rendered the same way as plain text fields
|
||||||
@@ -21,7 +22,7 @@ export class PlainTextMetadataListElementComponent extends MetadataRepresentatio
|
|||||||
*/
|
*/
|
||||||
getQueryParams() {
|
getQueryParams() {
|
||||||
let queryParams = {startsWith: this.mdRepresentation.getValue()};
|
let queryParams = {startsWith: this.mdRepresentation.getValue()};
|
||||||
if (this.mdRepresentation.browseDefinition.metadataBrowse) {
|
if (this.mdRepresentation.browseDefinition.getRenderType() === VALUE_LIST_BROWSE_DEFINITION.value) {
|
||||||
return {value: this.mdRepresentation.getValue()};
|
return {value: this.mdRepresentation.getValue()};
|
||||||
}
|
}
|
||||||
return queryParams;
|
return queryParams;
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
<button (click)="toggle()" (focusin)="focusBox = true" (focusout)="focusBox = false"
|
<button (click)="toggle()" (focusin)="focusBox = true" (focusout)="focusBox = false"
|
||||||
class="filter-name d-flex" [attr.aria-controls]="regionId" [id]="toggleId"
|
class="filter-name d-flex" [attr.aria-controls]="regionId" [id]="toggleId"
|
||||||
[attr.aria-expanded]="false"
|
[attr.aria-expanded]="false"
|
||||||
[attr.aria-label]="((collapsed$ | async) ? 'search.filters.filter.expand' : 'search.filters.filter.collapse') | translate"
|
[attr.aria-label]="(((collapsed$ | async) ? 'search.filters.filter.expand' : 'search.filters.filter.collapse') | translate) + ' ' + (('search.filters.filter.' + filter.name + '.head') | translate | lowercase)"
|
||||||
[attr.data-test]="'filter-toggle' | dsBrowserOnly"
|
[attr.data-test]="'filter-toggle' | dsBrowserOnly"
|
||||||
>
|
>
|
||||||
<h5 class="d-inline-block mb-0">
|
<h5 class="d-inline-block mb-0">
|
||||||
|
@@ -3,7 +3,6 @@ import { renderFacetFor } from '../search-filter-type-decorator';
|
|||||||
import { FilterType } from '../../../models/filter-type.model';
|
import { FilterType } from '../../../models/filter-type.model';
|
||||||
import { facetLoad, SearchFacetFilterComponent } from '../search-facet-filter/search-facet-filter.component';
|
import { facetLoad, SearchFacetFilterComponent } from '../search-facet-filter/search-facet-filter.component';
|
||||||
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { VocabularyTreeviewComponent } from '../../../../form/vocabulary-treeview/vocabulary-treeview.component';
|
|
||||||
import {
|
import {
|
||||||
VocabularyEntryDetail
|
VocabularyEntryDetail
|
||||||
} from '../../../../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
|
} from '../../../../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
|
||||||
@@ -26,6 +25,7 @@ import { Observable, BehaviorSubject } from 'rxjs';
|
|||||||
import { PageInfo } from '../../../../../core/shared/page-info.model';
|
import { PageInfo } from '../../../../../core/shared/page-info.model';
|
||||||
import { environment } from '../../../../../../environments/environment';
|
import { environment } from '../../../../../../environments/environment';
|
||||||
import { addOperatorToFilterValue } from '../../../search.utils';
|
import { addOperatorToFilterValue } from '../../../search.utils';
|
||||||
|
import { VocabularyTreeviewModalComponent } from '../../../../form/vocabulary-treeview-modal/vocabulary-treeview-modal.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-search-hierarchy-filter',
|
selector: 'ds-search-hierarchy-filter',
|
||||||
@@ -83,7 +83,7 @@ export class SearchHierarchyFilterComponent extends SearchFacetFilterComponent i
|
|||||||
* When an entry is selected, add the filter query to the search options.
|
* When an entry is selected, add the filter query to the search options.
|
||||||
*/
|
*/
|
||||||
showVocabularyTree() {
|
showVocabularyTree() {
|
||||||
const modalRef: NgbModalRef = this.modalService.open(VocabularyTreeviewComponent, {
|
const modalRef: NgbModalRef = this.modalService.open(VocabularyTreeviewModalComponent, {
|
||||||
size: 'lg',
|
size: 'lg',
|
||||||
windowClass: 'treeview'
|
windowClass: 'treeview'
|
||||||
});
|
});
|
||||||
@@ -91,7 +91,7 @@ export class SearchHierarchyFilterComponent extends SearchFacetFilterComponent i
|
|||||||
name: this.getVocabularyEntry(),
|
name: this.getVocabularyEntry(),
|
||||||
closed: true
|
closed: true
|
||||||
};
|
};
|
||||||
modalRef.componentInstance.select.subscribe((detail: VocabularyEntryDetail) => {
|
modalRef.result.then((detail: VocabularyEntryDetail) => {
|
||||||
this.selectedValues$
|
this.selectedValues$
|
||||||
.pipe(take(1))
|
.pipe(take(1))
|
||||||
.subscribe((selectedValues) => {
|
.subscribe((selectedValues) => {
|
||||||
@@ -106,7 +106,7 @@ export class SearchHierarchyFilterComponent extends SearchFacetFilterComponent i
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
}).catch();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -67,7 +67,7 @@ export class SearchComponent implements OnInit {
|
|||||||
* The configuration to use for the search options
|
* The configuration to use for the search options
|
||||||
* If empty, 'default' is used
|
* If empty, 'default' is used
|
||||||
*/
|
*/
|
||||||
@Input() configuration = 'default';
|
@Input() configuration;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The actual query for the fixed filter.
|
* The actual query for the fixed filter.
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
<form class="w-100" [formGroup]="formData" (ngSubmit)="submitForm(formData.value)">
|
<form class="w-100" [formGroup]="formData" (ngSubmit)="submitForm(formData.value)">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="form-group input-group col-8 col-sm-12 col-md-6">
|
<div class="form-group input-group col-8 col-sm-12 col-md-6">
|
||||||
<input class="form-control" placeholder="{{'browse.startsWith.type_text' | translate}}" type="text" name="startsWith" formControlName="startsWith" [value]="getStartsWith()" />
|
<input class="form-control" [attr.aria-label]="'browse.startsWith.input' | translate" placeholder="{{'browse.startsWith.type_text' | translate}}" type="text" name="startsWith" formControlName="startsWith" [value]="getStartsWith()" />
|
||||||
<span class="input-group-append">
|
<span class="input-group-append">
|
||||||
<button class="btn btn-primary" type="submit"><i class="fas fa-book-open"></i> {{'browse.startsWith.submit' | translate}}</button>
|
<button class="btn btn-primary" type="submit"><i class="fas fa-book-open"></i> {{'browse.startsWith.submit' | translate}}</button>
|
||||||
</span>
|
</span>
|
||||||
|
13
src/app/shared/testing/bitstream-data-service.stub.ts
Normal file
13
src/app/shared/testing/bitstream-data-service.stub.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { NoContent } from '../../core/shared/NoContent.model';
|
||||||
|
import { RequestEntryState } from '../../core/data/request-entry-state.model';
|
||||||
|
|
||||||
|
export class BitstreamDataServiceStub {
|
||||||
|
|
||||||
|
removeMultiple(_bitstreams: Bitstream[]): Observable<RemoteData<NoContent>> {
|
||||||
|
return observableOf(new RemoteData(0, 0, 0, RequestEntryState.Success));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -5,12 +5,14 @@ import { BrowseDefinition } from '../../core/shared/browse-definition.model';
|
|||||||
import { BrowseService } from '../../core/browse/browse.service';
|
import { BrowseService } from '../../core/browse/browse.service';
|
||||||
import { createSuccessfulRemoteDataObject } from '../remote-data.utils';
|
import { createSuccessfulRemoteDataObject } from '../remote-data.utils';
|
||||||
import { PageInfo } from '../../core/shared/page-info.model';
|
import { PageInfo } from '../../core/shared/page-info.model';
|
||||||
|
import { FlatBrowseDefinition } from '../../core/shared/flat-browse-definition.model';
|
||||||
|
import { ValueListBrowseDefinition } from '../../core/shared/value-list-browse-definition.model';
|
||||||
|
|
||||||
// This data is in post-serialized form (metadata -> metadataKeys)
|
// This data is in post-serialized form (metadata -> metadataKeys)
|
||||||
export const mockData: BrowseDefinition[] = [
|
export const mockData: BrowseDefinition[] = [
|
||||||
Object.assign(new BrowseDefinition, {
|
Object.assign(new FlatBrowseDefinition(), {
|
||||||
'id' : 'dateissued',
|
'id' : 'dateissued',
|
||||||
'metadataBrowse' : false,
|
'browseType': 'flatBrowse',
|
||||||
'dataType' : 'date',
|
'dataType' : 'date',
|
||||||
'sortOptions' : EMPTY,
|
'sortOptions' : EMPTY,
|
||||||
'order' : 'ASC',
|
'order' : 'ASC',
|
||||||
@@ -18,9 +20,9 @@ export const mockData: BrowseDefinition[] = [
|
|||||||
'metadataKeys' : [ 'dc.date.issued' ],
|
'metadataKeys' : [ 'dc.date.issued' ],
|
||||||
'_links' : EMPTY
|
'_links' : EMPTY
|
||||||
}),
|
}),
|
||||||
Object.assign(new BrowseDefinition, {
|
Object.assign(new ValueListBrowseDefinition(), {
|
||||||
'id' : 'author',
|
'id' : 'author',
|
||||||
'metadataBrowse' : true,
|
'browseType' : 'valueList',
|
||||||
'dataType' : 'text',
|
'dataType' : 'text',
|
||||||
'sortOptions' : EMPTY,
|
'sortOptions' : EMPTY,
|
||||||
'order' : 'ASC',
|
'order' : 'ASC',
|
||||||
|
@@ -175,7 +175,7 @@ describe('SubmissionSectionUploadFileComponent test suite', () => {
|
|||||||
it('should init file data properly', () => {
|
it('should init file data properly', () => {
|
||||||
uploadService.getFileData.and.returnValue(observableOf(fileData));
|
uploadService.getFileData.and.returnValue(observableOf(fileData));
|
||||||
|
|
||||||
comp.ngOnChanges();
|
comp.ngOnChanges({});
|
||||||
|
|
||||||
expect(comp.fileData).toEqual(fileData);
|
expect(comp.fileData).toEqual(fileData);
|
||||||
});
|
});
|
||||||
|
@@ -1,4 +1,13 @@
|
|||||||
import { ChangeDetectorRef, Component, Input, OnChanges, OnInit, ViewChild } from '@angular/core';
|
import {
|
||||||
|
ChangeDetectorRef,
|
||||||
|
Component,
|
||||||
|
Input,
|
||||||
|
OnChanges,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
SimpleChanges,
|
||||||
|
ViewChild
|
||||||
|
} from '@angular/core';
|
||||||
|
|
||||||
import { BehaviorSubject, Subscription } from 'rxjs';
|
import { BehaviorSubject, Subscription } from 'rxjs';
|
||||||
import { filter } from 'rxjs/operators';
|
import { filter } from 'rxjs/operators';
|
||||||
@@ -27,7 +36,7 @@ import { NgbModalOptions } from '@ng-bootstrap/ng-bootstrap/modal/modal-config';
|
|||||||
styleUrls: ['./section-upload-file.component.scss'],
|
styleUrls: ['./section-upload-file.component.scss'],
|
||||||
templateUrl: './section-upload-file.component.html',
|
templateUrl: './section-upload-file.component.html',
|
||||||
})
|
})
|
||||||
export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit {
|
export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit, OnDestroy {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The list of available access condition
|
* The list of available access condition
|
||||||
@@ -168,13 +177,13 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit {
|
|||||||
/**
|
/**
|
||||||
* Retrieve bitstream's metadata
|
* Retrieve bitstream's metadata
|
||||||
*/
|
*/
|
||||||
ngOnChanges() {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
if (this.availableAccessConditionOptions) {
|
if (this.availableAccessConditionOptions) {
|
||||||
// Retrieve file state
|
// Retrieve file state
|
||||||
this.subscriptions.push(
|
this.subscriptions.push(
|
||||||
this.uploadService
|
this.uploadService
|
||||||
.getFileData(this.submissionId, this.sectionId, this.fileId).pipe(
|
.getFileData(this.submissionId, this.sectionId, this.fileId)
|
||||||
filter((bitstream) => isNotUndefined(bitstream)))
|
.pipe(filter((bitstream) => isNotUndefined(bitstream)))
|
||||||
.subscribe((bitstream) => {
|
.subscribe((bitstream) => {
|
||||||
this.fileData = bitstream;
|
this.fileData = bitstream;
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,93 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { SubmissionFormsModel } from 'src/app/core/config/models/config-submission-forms.model';
|
||||||
|
import { ThemedComponent } from 'src/app/shared/theme-support/themed.component';
|
||||||
|
import { SubmissionSectionUploadFileComponent } from './section-upload-file.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-themed-submission-upload-section-file',
|
||||||
|
styleUrls: [],
|
||||||
|
templateUrl: '../../../../shared/theme-support/themed.component.html'
|
||||||
|
})
|
||||||
|
export class ThemedSubmissionSectionUploadFileComponent
|
||||||
|
extends ThemedComponent<SubmissionSectionUploadFileComponent> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of available access condition
|
||||||
|
* @type {Array}
|
||||||
|
*/
|
||||||
|
@Input() availableAccessConditionOptions: any[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The submission id
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
@Input() collectionId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define if collection access conditions policy type :
|
||||||
|
* POLICY_DEFAULT_NO_LIST : is not possible to define additional access group/s for the single file
|
||||||
|
* POLICY_DEFAULT_WITH_LIST : is possible to define additional access group/s for the single file
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
@Input() collectionPolicyType: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The configuration for the bitstream's metadata form
|
||||||
|
* @type {SubmissionFormsModel}
|
||||||
|
*/
|
||||||
|
@Input() configMetadataForm: SubmissionFormsModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The bitstream id
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
@Input() fileId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The bitstream array key
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
@Input() fileIndex: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The bitstream id
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
@Input() fileName: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The section id
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
@Input() sectionId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The submission id
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
@Input() submissionId: string;
|
||||||
|
|
||||||
|
protected inAndOutputNames: (keyof SubmissionSectionUploadFileComponent & keyof this)[] = [
|
||||||
|
'availableAccessConditionOptions',
|
||||||
|
'collectionId',
|
||||||
|
'collectionPolicyType',
|
||||||
|
'configMetadataForm',
|
||||||
|
'fileId',
|
||||||
|
'fileIndex',
|
||||||
|
'fileName',
|
||||||
|
'sectionId',
|
||||||
|
'submissionId'
|
||||||
|
];
|
||||||
|
|
||||||
|
protected getComponentName(): string {
|
||||||
|
return 'SubmissionSectionUploadFileComponent';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importThemedComponent(themeName: string): Promise<any> {
|
||||||
|
return import(`../../../../../themes/${themeName}/app/submission/sections/upload/file/section-upload-file.component`);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importUnthemedComponent(): Promise<any> {
|
||||||
|
return import(`./section-upload-file.component`);
|
||||||
|
}
|
||||||
|
}
|
@@ -28,7 +28,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-container *ngFor="let fileEntry of fileList">
|
<ng-container *ngFor="let fileEntry of fileList">
|
||||||
<ds-submission-upload-section-file
|
<ds-themed-submission-upload-section-file
|
||||||
[availableAccessConditionOptions]="availableAccessConditionOptions"
|
[availableAccessConditionOptions]="availableAccessConditionOptions"
|
||||||
[collectionId]="collectionId"
|
[collectionId]="collectionId"
|
||||||
[collectionPolicyType]="collectionPolicyType"
|
[collectionPolicyType]="collectionPolicyType"
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
[fileIndex]="fileList.indexOf(fileEntry)"
|
[fileIndex]="fileList.indexOf(fileEntry)"
|
||||||
[fileName]="fileNames[fileList.indexOf(fileEntry)]"
|
[fileName]="fileNames[fileList.indexOf(fileEntry)]"
|
||||||
[sectionId]="sectionData.id"
|
[sectionId]="sectionData.id"
|
||||||
[submissionId]="submissionId"></ds-submission-upload-section-file>
|
[submissionId]="submissionId"></ds-themed-submission-upload-section-file>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<hr/>
|
<hr/>
|
||||||
|
@@ -49,6 +49,7 @@ import { ResearchEntitiesModule } from '../entity-groups/research-entities/resea
|
|||||||
import { ThemedSubmissionEditComponent } from './edit/themed-submission-edit.component';
|
import { ThemedSubmissionEditComponent } from './edit/themed-submission-edit.component';
|
||||||
import { ThemedSubmissionSubmitComponent } from './submit/themed-submission-submit.component';
|
import { ThemedSubmissionSubmitComponent } from './submit/themed-submission-submit.component';
|
||||||
import { ThemedSubmissionImportExternalComponent } from './import-external/themed-submission-import-external.component';
|
import { ThemedSubmissionImportExternalComponent } from './import-external/themed-submission-import-external.component';
|
||||||
|
import { ThemedSubmissionSectionUploadFileComponent } from './sections/upload/file/themed-section-upload-file.component';
|
||||||
import { FormModule } from '../shared/form/form.module';
|
import { FormModule } from '../shared/form/form.module';
|
||||||
import { NgbAccordionModule, NgbCollapseModule, NgbModalModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbAccordionModule, NgbCollapseModule, NgbModalModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { SubmissionSectionAccessesComponent } from './sections/accesses/section-accesses.component';
|
import { SubmissionSectionAccessesComponent } from './sections/accesses/section-accesses.component';
|
||||||
@@ -104,6 +105,7 @@ const DECLARATIONS = [
|
|||||||
PublisherPolicyComponent,
|
PublisherPolicyComponent,
|
||||||
PublicationInformationComponent,
|
PublicationInformationComponent,
|
||||||
MetadataInformationComponent,
|
MetadataInformationComponent,
|
||||||
|
ThemedSubmissionSectionUploadFileComponent,
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@@ -785,6 +785,8 @@
|
|||||||
|
|
||||||
"browse.comcol.by.subject": "By Subject",
|
"browse.comcol.by.subject": "By Subject",
|
||||||
|
|
||||||
|
"browse.comcol.by.srsc": "By Subject Category",
|
||||||
|
|
||||||
"browse.comcol.by.title": "By Title",
|
"browse.comcol.by.title": "By Title",
|
||||||
|
|
||||||
"browse.comcol.head": "Browse",
|
"browse.comcol.head": "Browse",
|
||||||
@@ -805,6 +807,8 @@
|
|||||||
|
|
||||||
"browse.metadata.subject.breadcrumbs": "Browse by Subject",
|
"browse.metadata.subject.breadcrumbs": "Browse by Subject",
|
||||||
|
|
||||||
|
"browse.metadata.srsc.breadcrumbs": "Browse by Subject Category",
|
||||||
|
|
||||||
"browse.metadata.title.breadcrumbs": "Browse by Title",
|
"browse.metadata.title.breadcrumbs": "Browse by Title",
|
||||||
|
|
||||||
"pagination.next.button": "Next",
|
"pagination.next.button": "Next",
|
||||||
@@ -859,6 +863,10 @@
|
|||||||
|
|
||||||
"browse.startsWith.type_text": "Filter results by typing the first few letters",
|
"browse.startsWith.type_text": "Filter results by typing the first few letters",
|
||||||
|
|
||||||
|
"browse.startsWith.input": "Filter",
|
||||||
|
|
||||||
|
"browse.taxonomy.button": "Browse",
|
||||||
|
|
||||||
"browse.title": "Browsing {{ collection }} by {{ field }}{{ startsWith }} {{ value }}",
|
"browse.title": "Browsing {{ collection }} by {{ field }}{{ startsWith }} {{ value }}",
|
||||||
|
|
||||||
"browse.title.page": "Browsing {{ collection }} by {{ field }} {{ value }}",
|
"browse.title.page": "Browsing {{ collection }} by {{ field }} {{ value }}",
|
||||||
@@ -1632,6 +1640,19 @@
|
|||||||
|
|
||||||
"error.validation.groupExists": "This group already exists",
|
"error.validation.groupExists": "This group already exists",
|
||||||
|
|
||||||
|
"error.validation.metadata.name.invalid-pattern": "This field cannot contain dots, commas or spaces. Please use the Element & Qualifier fields instead",
|
||||||
|
|
||||||
|
"error.validation.metadata.name.max-length": "This field may not contain more than 32 characters",
|
||||||
|
|
||||||
|
"error.validation.metadata.namespace.max-length": "This field may not contain more than 256 characters",
|
||||||
|
|
||||||
|
"error.validation.metadata.element.invalid-pattern": "This field cannot contain dots, commas or spaces. Please use the Qualifier field instead",
|
||||||
|
|
||||||
|
"error.validation.metadata.element.max-length": "This field may not contain more than 64 characters",
|
||||||
|
|
||||||
|
"error.validation.metadata.qualifier.invalid-pattern": "This field cannot contain dots, commas or spaces",
|
||||||
|
|
||||||
|
"error.validation.metadata.qualifier.max-length": "This field may not contain more than 64 characters",
|
||||||
|
|
||||||
"feed.description": "Syndication feed",
|
"feed.description": "Syndication feed",
|
||||||
|
|
||||||
@@ -2544,6 +2565,8 @@
|
|||||||
|
|
||||||
"item.preview.dc.identifier.doi": "DOI",
|
"item.preview.dc.identifier.doi": "DOI",
|
||||||
|
|
||||||
|
"item.preview.dc.publisher": "Publisher:",
|
||||||
|
|
||||||
"item.preview.person.familyName": "Surname:",
|
"item.preview.person.familyName": "Surname:",
|
||||||
|
|
||||||
"item.preview.person.givenName": "Name:",
|
"item.preview.person.givenName": "Name:",
|
||||||
@@ -2915,6 +2938,8 @@
|
|||||||
|
|
||||||
"menu.section.browse_global_by_subject": "By Subject",
|
"menu.section.browse_global_by_subject": "By Subject",
|
||||||
|
|
||||||
|
"menu.section.browse_global_by_srsc": "By Subject Category",
|
||||||
|
|
||||||
"menu.section.browse_global_by_title": "By Title",
|
"menu.section.browse_global_by_title": "By Title",
|
||||||
|
|
||||||
"menu.section.browse_global_communities_and_collections": "Communities & Collections",
|
"menu.section.browse_global_communities_and_collections": "Communities & Collections",
|
||||||
@@ -4071,7 +4096,7 @@
|
|||||||
|
|
||||||
"submission.general.cancel": "Cancel",
|
"submission.general.cancel": "Cancel",
|
||||||
|
|
||||||
"submission.general.cannot_submit": "You have not the privilege to make a new submission.",
|
"submission.general.cannot_submit": "You don't have permission to make a new submission.",
|
||||||
|
|
||||||
"submission.general.deposit": "Deposit",
|
"submission.general.deposit": "Deposit",
|
||||||
|
|
||||||
|
@@ -1,4 +1,17 @@
|
|||||||
import Mirador from 'mirador/dist/es/src/index';
|
import Mirador from 'mirador/dist/es/src/index';
|
||||||
|
|
||||||
|
// You can modify this default Mirador configuration file. However,
|
||||||
|
// you should consider creating a copy of this file named
|
||||||
|
// 'config.local.js'. If that file exists it will be used to build
|
||||||
|
// your local Mirador instance. This allows you to keep local
|
||||||
|
// Mirador configuration separate from this default distribution
|
||||||
|
// copy.
|
||||||
|
|
||||||
|
// For an example of all Mirador configuration options, see
|
||||||
|
// https://github.com/ProjectMirador/mirador/blob/master/src/config/settings.js
|
||||||
|
|
||||||
|
// You can add or remove plugins. When adding new plugins be sure to also
|
||||||
|
// import them into the project via your package.json dependencies.
|
||||||
import miradorShareDialogPlugin from 'mirador-share-plugin/es/MiradorShareDialog';
|
import miradorShareDialogPlugin from 'mirador-share-plugin/es/MiradorShareDialog';
|
||||||
import miradorSharePlugin from 'mirador-share-plugin/es/miradorSharePlugin';
|
import miradorSharePlugin from 'mirador-share-plugin/es/miradorSharePlugin';
|
||||||
import miradorDownloadPlugin from 'mirador-dl-plugin/es/miradorDownloadPlugin';
|
import miradorDownloadPlugin from 'mirador-dl-plugin/es/miradorDownloadPlugin';
|
@@ -31,7 +31,6 @@ import { GoogleAnalyticsService } from '../../app/statistics/google-analytics.se
|
|||||||
import { AuthRequestService } from '../../app/core/auth/auth-request.service';
|
import { AuthRequestService } from '../../app/core/auth/auth-request.service';
|
||||||
import { BrowserAuthRequestService } from '../../app/core/auth/browser-auth-request.service';
|
import { BrowserAuthRequestService } from '../../app/core/auth/browser-auth-request.service';
|
||||||
import { BrowserInitService } from './browser-init.service';
|
import { BrowserInitService } from './browser-init.service';
|
||||||
import { VocabularyTreeviewService } from 'src/app/shared/form/vocabulary-treeview/vocabulary-treeview.service';
|
|
||||||
|
|
||||||
export const REQ_KEY = makeStateKey<string>('req');
|
export const REQ_KEY = makeStateKey<string>('req');
|
||||||
|
|
||||||
@@ -111,10 +110,6 @@ export function getRequest(transferState: TransferState): any {
|
|||||||
{
|
{
|
||||||
provide: LocationToken,
|
provide: LocationToken,
|
||||||
useFactory: locationProvider,
|
useFactory: locationProvider,
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: VocabularyTreeviewService,
|
|
||||||
useClass: VocabularyTreeviewService,
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
@@ -38,7 +38,7 @@ export class ServerInitService extends InitService {
|
|||||||
protected metadata: MetadataService,
|
protected metadata: MetadataService,
|
||||||
protected breadcrumbsService: BreadcrumbsService,
|
protected breadcrumbsService: BreadcrumbsService,
|
||||||
protected themeService: ThemeService,
|
protected themeService: ThemeService,
|
||||||
protected menuService: MenuService,
|
protected menuService: MenuService
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
store,
|
store,
|
||||||
|
@@ -0,0 +1,15 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { BrowseByTaxonomyPageComponent as BaseComponent } from '../../../../../app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-browse-by-taxonomy-page',
|
||||||
|
// templateUrl: './browse-by-taxonomy-page.component.html',
|
||||||
|
templateUrl: '../../../../../app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.html',
|
||||||
|
// styleUrls: ['./browse-by-taxonomy-page.component.scss'],
|
||||||
|
styleUrls: ['../../../../../app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.scss'],
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Component for browsing items by metadata in a hierarchical controlled vocabulary
|
||||||
|
*/
|
||||||
|
export class BrowseByTaxonomyPageComponent extends BaseComponent {
|
||||||
|
}
|
@@ -0,0 +1,18 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import {
|
||||||
|
SubmissionSectionUploadFileComponent as BaseComponent
|
||||||
|
} from 'src/app/submission/sections/upload/file/section-upload-file.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component represents a single bitstream contained in the submission
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-submission-upload-section-file',
|
||||||
|
// styleUrls: ['./section-upload-file.component.scss'],
|
||||||
|
styleUrls: ['../../../../../../../app/submission/sections/upload/file/section-upload-file.component.scss'],
|
||||||
|
// templateUrl: './section-upload-file.component.html'
|
||||||
|
templateUrl: '../../../../../../../app/submission/sections/upload/file/section-upload-file.component.html'
|
||||||
|
})
|
||||||
|
export class SubmissionSectionUploadFileComponent
|
||||||
|
extends BaseComponent {
|
||||||
|
}
|
@@ -101,6 +101,7 @@ import { ObjectListComponent } from './app/shared/object-list/object-list.compon
|
|||||||
import { BrowseByMetadataPageComponent } from './app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component';
|
import { BrowseByMetadataPageComponent } from './app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component';
|
||||||
import { BrowseByDatePageComponent } from './app/browse-by/browse-by-date-page/browse-by-date-page.component';
|
import { BrowseByDatePageComponent } from './app/browse-by/browse-by-date-page/browse-by-date-page.component';
|
||||||
import { BrowseByTitlePageComponent } from './app/browse-by/browse-by-title-page/browse-by-title-page.component';
|
import { BrowseByTitlePageComponent } from './app/browse-by/browse-by-title-page/browse-by-title-page.component';
|
||||||
|
import { BrowseByTaxonomyPageComponent } from './app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component';
|
||||||
import {
|
import {
|
||||||
ExternalSourceEntryImportModalComponent
|
ExternalSourceEntryImportModalComponent
|
||||||
} from './app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component';
|
} from './app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component';
|
||||||
@@ -141,6 +142,7 @@ import {
|
|||||||
import { NgxGalleryModule } from '@kolkov/ngx-gallery';
|
import { NgxGalleryModule } from '@kolkov/ngx-gallery';
|
||||||
import { WorkspaceItemsDeletePageComponent } from './app/workspace-items-delete-page/workspace-items-delete/workspace-items-delete.component';
|
import { WorkspaceItemsDeletePageComponent } from './app/workspace-items-delete-page/workspace-items-delete/workspace-items-delete.component';
|
||||||
import { ThumbnailComponent } from './app/thumbnail/thumbnail.component';
|
import { ThumbnailComponent } from './app/thumbnail/thumbnail.component';
|
||||||
|
import { SubmissionSectionUploadFileComponent } from './app/submission/sections/upload/file/section-upload-file.component';
|
||||||
import { ItemStatusComponent } from './app/item-page/edit-item-page/item-status/item-status.component';
|
import { ItemStatusComponent } from './app/item-page/edit-item-page/item-status/item-status.component';
|
||||||
import { EditBitstreamPageComponent } from './app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component';
|
import { EditBitstreamPageComponent } from './app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component';
|
||||||
import { FormModule } from '../../app/shared/form/form.module';
|
import { FormModule } from '../../app/shared/form/form.module';
|
||||||
@@ -199,6 +201,7 @@ const DECLARATIONS = [
|
|||||||
BrowseByMetadataPageComponent,
|
BrowseByMetadataPageComponent,
|
||||||
BrowseByDatePageComponent,
|
BrowseByDatePageComponent,
|
||||||
BrowseByTitlePageComponent,
|
BrowseByTitlePageComponent,
|
||||||
|
BrowseByTaxonomyPageComponent,
|
||||||
ExternalSourceEntryImportModalComponent,
|
ExternalSourceEntryImportModalComponent,
|
||||||
SearchFiltersComponent,
|
SearchFiltersComponent,
|
||||||
SearchSidebarComponent,
|
SearchSidebarComponent,
|
||||||
@@ -220,6 +223,7 @@ const DECLARATIONS = [
|
|||||||
MediaViewerVideoComponent,
|
MediaViewerVideoComponent,
|
||||||
WorkspaceItemsDeletePageComponent,
|
WorkspaceItemsDeletePageComponent,
|
||||||
ThumbnailComponent,
|
ThumbnailComponent,
|
||||||
|
SubmissionSectionUploadFileComponent,
|
||||||
ItemStatusComponent,
|
ItemStatusComponent,
|
||||||
EditBitstreamPageComponent,
|
EditBitstreamPageComponent,
|
||||||
];
|
];
|
||||||
|
@@ -1,10 +1,13 @@
|
|||||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
// @ts-ignore
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
mode: 'production',
|
mode: 'production',
|
||||||
entry: {
|
entry: {
|
||||||
mirador: './src/mirador-viewer/index.js'
|
mirador: fs.existsSync('./src/mirador-viewer/config.local.js')? './src/mirador-viewer/config.local.js' :
|
||||||
|
'./src/mirador-viewer/config.default.js'
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
path: path.resolve(__dirname, '..' , 'dist/iiif/mirador'),
|
path: path.resolve(__dirname, '..' , 'dist/iiif/mirador'),
|
||||||
|
Reference in New Issue
Block a user