mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-16 22:43:03 +00:00
Merge branch 'main' into bugfix/addressing-#2276
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { MetadataSchemaFormComponent } from './metadata-schema-form.component';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
@@ -29,14 +28,16 @@ describe('MetadataSchemaFormComponent', () => {
|
||||
createFormGroup: () => {
|
||||
return {
|
||||
patchValue: () => {
|
||||
}
|
||||
},
|
||||
reset(_value?: any, _options?: { onlySelf?: boolean; emitEvent?: boolean; }): void {
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
return TestBed.configureTestingModule({
|
||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
||||
declarations: [MetadataSchemaFormComponent, EnumKeysPipe],
|
||||
providers: [
|
||||
@@ -64,7 +65,7 @@ describe('MetadataSchemaFormComponent', () => {
|
||||
const expected = Object.assign(new MetadataSchema(), {
|
||||
namespace: namespace,
|
||||
prefix: prefix
|
||||
});
|
||||
} as MetadataSchema);
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(component.submitForm, 'emit');
|
||||
@@ -79,11 +80,10 @@ describe('MetadataSchemaFormComponent', () => {
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should emit a new schema using the correct values', waitForAsync(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expected);
|
||||
});
|
||||
}));
|
||||
it('should emit a new schema using the correct values', async () => {
|
||||
await fixture.whenStable();
|
||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an active schema', () => {
|
||||
@@ -91,7 +91,7 @@ describe('MetadataSchemaFormComponent', () => {
|
||||
id: 1,
|
||||
namespace: namespace,
|
||||
prefix: prefix
|
||||
});
|
||||
} as MetadataSchema);
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(expectedWithId));
|
||||
@@ -99,11 +99,10 @@ describe('MetadataSchemaFormComponent', () => {
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should edit the existing schema using the correct values', waitForAsync(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expectedWithId);
|
||||
});
|
||||
}));
|
||||
it('should edit the existing schema using the correct values', async () => {
|
||||
await fixture.whenStable();
|
||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expectedWithId);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -77,19 +77,24 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
combineLatest(
|
||||
combineLatest([
|
||||
this.translateService.get(`${this.messagePrefix}.name`),
|
||||
this.translateService.get(`${this.messagePrefix}.namespace`)
|
||||
).subscribe(([name, namespace]) => {
|
||||
]).subscribe(([name, namespace]) => {
|
||||
this.name = new DynamicInputModel({
|
||||
id: 'name',
|
||||
label: name,
|
||||
name: 'name',
|
||||
validators: {
|
||||
required: null,
|
||||
pattern: '^[^ ,_]{1,32}$'
|
||||
pattern: '^[^. ,]*$',
|
||||
maxLength: 32,
|
||||
},
|
||||
required: true,
|
||||
errorMessages: {
|
||||
pattern: 'error.validation.metadata.name.invalid-pattern',
|
||||
maxLength: 'error.validation.metadata.name.max-length',
|
||||
},
|
||||
});
|
||||
this.namespace = new DynamicInputModel({
|
||||
id: 'namespace',
|
||||
@@ -97,8 +102,12 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
||||
name: 'namespace',
|
||||
validators: {
|
||||
required: null,
|
||||
maxLength: 256,
|
||||
},
|
||||
required: true,
|
||||
errorMessages: {
|
||||
maxLength: 'error.validation.metadata.namespace.max-length',
|
||||
},
|
||||
});
|
||||
this.formModel = [
|
||||
new DynamicFormGroupModel(
|
||||
@@ -108,13 +117,18 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
||||
})
|
||||
];
|
||||
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
||||
this.registryService.getActiveMetadataSchema().subscribe((schema) => {
|
||||
this.formGroup.patchValue({
|
||||
metadatadataschemagroup:{
|
||||
name: schema != null ? schema.prefix : '',
|
||||
namespace: schema != null ? schema.namespace : ''
|
||||
}
|
||||
});
|
||||
this.registryService.getActiveMetadataSchema().subscribe((schema: MetadataSchema) => {
|
||||
if (schema == null) {
|
||||
this.clearFields();
|
||||
} else {
|
||||
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
|
||||
* Emit the updated/created schema using the EventEmitter submitForm
|
||||
*/
|
||||
onSubmit() {
|
||||
onSubmit(): void {
|
||||
this.registryService.clearMetadataSchemaRequests().subscribe();
|
||||
this.registryService.getActiveMetadataSchema().pipe(take(1)).subscribe(
|
||||
(schema) => {
|
||||
(schema: MetadataSchema) => {
|
||||
const values = {
|
||||
prefix: this.name.value,
|
||||
namespace: this.namespace.value
|
||||
@@ -147,9 +161,9 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
||||
} else {
|
||||
this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), schema, {
|
||||
id: schema.id,
|
||||
prefix: (values.prefix ? values.prefix : schema.prefix),
|
||||
namespace: (values.namespace ? values.namespace : schema.namespace)
|
||||
})).subscribe((updatedSchema) => {
|
||||
prefix: schema.prefix,
|
||||
namespace: values.namespace,
|
||||
})).subscribe((updatedSchema: MetadataSchema) => {
|
||||
this.submitForm.emit(updatedSchema);
|
||||
});
|
||||
}
|
||||
@@ -162,13 +176,9 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* Reset all input-fields to be empty
|
||||
*/
|
||||
clearFields() {
|
||||
this.formGroup.patchValue({
|
||||
metadatadataschemagroup:{
|
||||
prefix: '',
|
||||
namespace: ''
|
||||
}
|
||||
});
|
||||
clearFields(): void {
|
||||
this.formGroup.reset('metadatadataschemagroup');
|
||||
this.name.disabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -39,14 +39,16 @@ describe('MetadataFieldFormComponent', () => {
|
||||
createFormGroup: () => {
|
||||
return {
|
||||
patchValue: () => {
|
||||
}
|
||||
},
|
||||
reset(_value?: any, _options?: { onlySelf?: boolean; emitEvent?: boolean; }): void {
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
return TestBed.configureTestingModule({
|
||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
||||
declarations: [MetadataFieldFormComponent, EnumKeysPipe],
|
||||
providers: [
|
||||
@@ -98,11 +100,10 @@ describe('MetadataFieldFormComponent', () => {
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should emit a new field using the correct values', waitForAsync(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expected);
|
||||
});
|
||||
}));
|
||||
it('should emit a new field using the correct values', async () => {
|
||||
await fixture.whenStable();
|
||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an active field', () => {
|
||||
@@ -120,11 +121,10 @@ describe('MetadataFieldFormComponent', () => {
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should edit the existing field using the correct values', waitForAsync(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expectedWithId);
|
||||
});
|
||||
}));
|
||||
it('should edit the existing field using the correct values', async () => {
|
||||
await fixture.whenStable();
|
||||
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
|
||||
*/
|
||||
ngOnInit() {
|
||||
combineLatest(
|
||||
combineLatest([
|
||||
this.translateService.get(`${this.messagePrefix}.element`),
|
||||
this.translateService.get(`${this.messagePrefix}.qualifier`),
|
||||
this.translateService.get(`${this.messagePrefix}.scopenote`)
|
||||
).subscribe(([element, qualifier, scopenote]) => {
|
||||
]).subscribe(([element, qualifier, scopenote]) => {
|
||||
this.element = new DynamicInputModel({
|
||||
id: 'element',
|
||||
label: element,
|
||||
name: 'element',
|
||||
validators: {
|
||||
required: null,
|
||||
pattern: '^[^. ,]*$',
|
||||
maxLength: 64,
|
||||
},
|
||||
required: true,
|
||||
errorMessages: {
|
||||
pattern: 'error.validation.metadata.element.invalid-pattern',
|
||||
maxLength: 'error.validation.metadata.element.max-length',
|
||||
},
|
||||
});
|
||||
this.qualifier = new DynamicInputModel({
|
||||
id: 'qualifier',
|
||||
label: qualifier,
|
||||
name: 'qualifier',
|
||||
validators: {
|
||||
pattern: '^[^. ,]*$',
|
||||
maxLength: 64,
|
||||
},
|
||||
required: false,
|
||||
errorMessages: {
|
||||
pattern: 'error.validation.metadata.qualifier.invalid-pattern',
|
||||
maxLength: 'error.validation.metadata.qualifier.max-length',
|
||||
},
|
||||
});
|
||||
this.scopeNote = new DynamicInputModel({
|
||||
id: 'scopeNote',
|
||||
@@ -132,14 +146,20 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
|
||||
})
|
||||
];
|
||||
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
||||
this.registryService.getActiveMetadataField().subscribe((field) => {
|
||||
this.formGroup.patchValue({
|
||||
metadatadatafieldgroup: {
|
||||
element: field != null ? field.element : '',
|
||||
qualifier: field != null ? field.qualifier : '',
|
||||
scopeNote: field != null ? field.scopeNote : ''
|
||||
}
|
||||
});
|
||||
this.registryService.getActiveMetadataField().subscribe((field: MetadataField): void => {
|
||||
if (field == null) {
|
||||
this.clearFields();
|
||||
} else {
|
||||
this.formGroup.patchValue({
|
||||
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
|
||||
* Emit the updated/created field using the EventEmitter submitForm
|
||||
*/
|
||||
onSubmit() {
|
||||
onSubmit(): void {
|
||||
this.registryService.getActiveMetadataField().pipe(take(1)).subscribe(
|
||||
(field) => {
|
||||
const values = {
|
||||
element: this.element.value,
|
||||
qualifier: this.qualifier.value,
|
||||
scopeNote: this.scopeNote.value
|
||||
};
|
||||
(field: MetadataField) => {
|
||||
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);
|
||||
});
|
||||
} else {
|
||||
this.registryService.updateMetadataField(Object.assign(new MetadataField(), field, {
|
||||
id: field.id,
|
||||
element: (values.element ? values.element : field.element),
|
||||
qualifier: (values.qualifier ? values.qualifier : field.qualifier),
|
||||
scopeNote: (values.scopeNote ? values.scopeNote : field.scopeNote)
|
||||
})).subscribe((updatedField) => {
|
||||
element: field.element,
|
||||
qualifier: field.qualifier,
|
||||
scopeNote: this.scopeNote.value,
|
||||
})).subscribe((updatedField: MetadataField) => {
|
||||
this.submitForm.emit(updatedField);
|
||||
});
|
||||
}
|
||||
@@ -188,14 +207,10 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* Reset all input-fields to be empty
|
||||
*/
|
||||
clearFields() {
|
||||
this.formGroup.patchValue({
|
||||
metadatadatafieldgroup: {
|
||||
element: '',
|
||||
qualifier: '',
|
||||
scopeNote: ''
|
||||
}
|
||||
});
|
||||
clearFields(): void {
|
||||
this.formGroup.reset('metadatadatafieldgroup');
|
||||
this.element.disabled = false;
|
||||
this.qualifier.disabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -12,6 +12,7 @@ import { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
|
||||
|
||||
describe('MetadataFieldSelectorComponent', () => {
|
||||
let component: MetadataFieldSelectorComponent;
|
||||
@@ -79,7 +80,7 @@ describe('MetadataFieldSelectorComponent', () => {
|
||||
});
|
||||
|
||||
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,
|
||||
ViewChild
|
||||
} 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 {
|
||||
getAllSucceededRemoteData, getFirstCompletedRemoteData,
|
||||
getAllSucceededRemoteData,
|
||||
getFirstCompletedRemoteData,
|
||||
metadataFieldsToString
|
||||
} from '../../../core/shared/operators';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
@@ -24,6 +25,7 @@ import { Subscription } from 'rxjs/internal/Subscription';
|
||||
import { of } from 'rxjs/internal/observable/of';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-metadata-field-selector',
|
||||
@@ -127,7 +129,7 @@ export class MetadataFieldSelectorComponent implements OnInit, OnDestroy, AfterV
|
||||
switchMap((query: string) => {
|
||||
this.showInvalid = false;
|
||||
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(),
|
||||
metadataFieldsToString(),
|
||||
);
|
||||
|
@@ -1,10 +1,15 @@
|
||||
<div class="container" *ngVar="(processRD$ | async)?.payload as process">
|
||||
<div class="d-flex">
|
||||
<h2 class="flex-grow-1">{{'process.detail.title' | translate:{
|
||||
id: process?.processId,
|
||||
name: process?.scriptName
|
||||
} }}</h2>
|
||||
<div class="container" *ngIf="(processRD$ | async)?.payload as process">
|
||||
<div class="row">
|
||||
<div class="col-10">
|
||||
<h2 class="flex-grow-1">
|
||||
{{ 'process.detail.title' | translate:{ id: process?.processId, name: process?.scriptName } }}
|
||||
</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>
|
||||
|
||||
<ds-process-detail-field id="process-name" [title]="'process.detail.script'">
|
||||
<div>{{ process?.scriptName }}</div>
|
||||
</ds-process-detail-field>
|
||||
@@ -17,10 +22,12 @@
|
||||
<div *ngVar="(filesRD$ | async)?.payload?.page as files">
|
||||
<ds-process-detail-field *ngIf="files && files?.length > 0" id="process-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">
|
||||
<span>{{getFileName(file)}}</span>
|
||||
<span>({{(file?.sizeBytes) | dsFileSize }})</span>
|
||||
<span>{{getFileName(file)}}</span>
|
||||
<span>({{(file?.sizeBytes) | dsFileSize }})</span>
|
||||
</ds-themed-file-download-link>
|
||||
</div>
|
||||
</ds-process-detail-field>
|
||||
</div>
|
||||
|
||||
@@ -70,7 +77,7 @@
|
||||
|
||||
<ng-template #deleteModal >
|
||||
|
||||
<div *ngVar="(processRD$ | async)?.payload as process">
|
||||
<div *ngIf="(processRD$ | async)?.payload as process">
|
||||
|
||||
<div class="modal-header">
|
||||
<div>
|
||||
|
@@ -35,6 +35,7 @@ import { NotificationsServiceStub } from '../../shared/testing/notifications-ser
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { getProcessListRoute } from '../process-page-routing.paths';
|
||||
import {ProcessStatus} from '../processes/process-status.model';
|
||||
|
||||
describe('ProcessDetailComponent', () => {
|
||||
let component: ProcessDetailComponent;
|
||||
@@ -44,6 +45,7 @@ describe('ProcessDetailComponent', () => {
|
||||
let nameService: DSONameService;
|
||||
let bitstreamDataService: BitstreamDataService;
|
||||
let httpClient: HttpClient;
|
||||
let route: ActivatedRoute;
|
||||
|
||||
let process: Process;
|
||||
let fileName: string;
|
||||
@@ -106,7 +108,8 @@ describe('ProcessDetailComponent', () => {
|
||||
});
|
||||
processService = jasmine.createSpyObj('processService', {
|
||||
getFiles: createSuccessfulRemoteDataObject$(createPaginatedList(files)),
|
||||
delete: createSuccessfulRemoteDataObject$(null)
|
||||
delete: createSuccessfulRemoteDataObject$(null),
|
||||
findById: createSuccessfulRemoteDataObject$(process),
|
||||
});
|
||||
bitstreamDataService = jasmine.createSpyObj('bitstreamDataService', {
|
||||
findByHref: createSuccessfulRemoteDataObject$(logBitstream)
|
||||
@@ -127,6 +130,13 @@ describe('ProcessDetailComponent', () => {
|
||||
router = jasmine.createSpyObj('router', {
|
||||
navigateByUrl:{}
|
||||
});
|
||||
|
||||
route = jasmine.createSpyObj('route', {
|
||||
data: observableOf({ process: createSuccessfulRemoteDataObject(process) }),
|
||||
snapshot: {
|
||||
params: { id: process.processId }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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 { Component, NgZone, OnInit } from '@angular/core';
|
||||
import { Component, Inject, NgZone, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
|
||||
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 { AuthService } from '../../core/auth/auth.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 { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { followLink } from '../../shared/utils/follow-link-config.model';
|
||||
import { isPlatformBrowser } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-process-detail',
|
||||
@@ -34,7 +36,7 @@ import { TranslateService } from '@ngx-translate/core';
|
||||
/**
|
||||
* A component displaying detailed information about a DSpace Process
|
||||
*/
|
||||
export class ProcessDetailComponent implements OnInit {
|
||||
export class ProcessDetailComponent implements OnInit, OnDestroy {
|
||||
|
||||
/**
|
||||
* The AlertType enumeration
|
||||
@@ -65,48 +67,58 @@ export class ProcessDetailComponent implements OnInit {
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
retrievingOutputLogs$: BehaviorSubject<boolean>;
|
||||
retrievingOutputLogs$ = new BehaviorSubject<boolean>(false);
|
||||
|
||||
/**
|
||||
* Date format to use for start and end time of processes
|
||||
*/
|
||||
dateFormat = 'yyyy-MM-dd HH:mm:ss ZZZZ';
|
||||
|
||||
refreshCounter$ = new BehaviorSubject(0);
|
||||
|
||||
/**
|
||||
* Reference to NgbModal
|
||||
*/
|
||||
protected modalRef: NgbModalRef;
|
||||
|
||||
constructor(protected route: ActivatedRoute,
|
||||
protected router: Router,
|
||||
protected processService: ProcessDataService,
|
||||
protected bitstreamDataService: BitstreamDataService,
|
||||
protected nameService: DSONameService,
|
||||
private zone: NgZone,
|
||||
protected authService: AuthService,
|
||||
protected http: HttpClient,
|
||||
protected modalService: NgbModal,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected translateService: TranslateService
|
||||
) {
|
||||
}
|
||||
private refreshTimerSub?: Subscription;
|
||||
|
||||
constructor(
|
||||
@Inject(PLATFORM_ID) protected platformId: object,
|
||||
protected route: ActivatedRoute,
|
||||
protected router: Router,
|
||||
protected processService: ProcessDataService,
|
||||
protected bitstreamDataService: BitstreamDataService,
|
||||
protected nameService: DSONameService,
|
||||
private zone: NgZone,
|
||||
protected authService: AuthService,
|
||||
protected http: HttpClient,
|
||||
protected modalService: NgbModal,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected translateService: TranslateService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Initialize component properties
|
||||
* Display a 404 if the process doesn't exist
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.showOutputLogs = false;
|
||||
this.retrievingOutputLogs$ = new BehaviorSubject<boolean>(false);
|
||||
this.processRD$ = this.route.data.pipe(
|
||||
map((data) => {
|
||||
if (isPlatformBrowser(this.platformId)) {
|
||||
if (!this.isProcessFinished(data.process.payload)) {
|
||||
this.startRefreshTimer();
|
||||
}
|
||||
}
|
||||
|
||||
return data.process as RemoteData<Process>;
|
||||
}),
|
||||
redirectOn4xx(this.router, this.authService)
|
||||
redirectOn4xx(this.router, this.authService),
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
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
|
||||
* @param bitstream
|
||||
@@ -210,6 +269,7 @@ export class ProcessDetailComponent implements OnInit {
|
||||
openDeleteModal(content) {
|
||||
this.modalRef = this.modalService.open(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the modal.
|
||||
*/
|
||||
@@ -217,4 +277,7 @@ export class ProcessDetailComponent implements OnInit {
|
||||
this.modalRef.close();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.stopRefreshTimer();
|
||||
}
|
||||
}
|
||||
|
@@ -1628,6 +1628,19 @@
|
||||
|
||||
"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",
|
||||
|
||||
|
Reference in New Issue
Block a user