diff --git a/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.spec.ts b/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.spec.ts index 8d416c2df8..b758767ddb 100644 --- a/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.spec.ts +++ b/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.spec.ts @@ -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); + }); }); }); }); diff --git a/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.ts b/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.ts index 6d3138987a..1992289ff7 100644 --- a/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.ts +++ b/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.ts @@ -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; } /** diff --git a/src/app/admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.spec.ts b/src/app/admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.spec.ts index e13180d633..ad7b54945d 100644 --- a/src/app/admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.spec.ts +++ b/src/app/admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.spec.ts @@ -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); + }); }); }); }); diff --git a/src/app/admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.ts b/src/app/admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.ts index 0beb306d6c..773e0600fb 100644 --- a/src/app/admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.ts +++ b/src/app/admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.ts @@ -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; } /** diff --git a/src/app/browse-by/browse-by-guard.spec.ts b/src/app/browse-by/browse-by-guard.spec.ts index 671e098762..9ef76b1212 100644 --- a/src/app/browse-by/browse-by-guard.spec.ts +++ b/src/app/browse-by/browse-by-guard.spec.ts @@ -2,8 +2,8 @@ import { first } from 'rxjs/operators'; import { BrowseByGuard } from './browse-by-guard'; import { of as observableOf } from 'rxjs'; 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 { ValueListBrowseDefinition } from '../core/shared/value-list-browse-definition.model'; import { DSONameServiceMock } from '../shared/mocks/dso-name.service.mock'; import { DSONameService } from '../core/breadcrumbs/dso-name.service'; @@ -20,7 +20,7 @@ describe('BrowseByGuard', () => { const id = 'author'; const scope = '1234-65487-12354-1235'; 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(() => { dsoService = { diff --git a/src/app/browse-by/browse-by-switcher/browse-by-decorator.ts b/src/app/browse-by/browse-by-switcher/browse-by-decorator.ts index ceb4c6a6c6..b59a46cae1 100644 --- a/src/app/browse-by/browse-by-switcher/browse-by-decorator.ts +++ b/src/app/browse-by/browse-by-switcher/browse-by-decorator.ts @@ -26,7 +26,7 @@ const map = new Map(); * @param browseByType The type of page * @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) { if (hasNoValue(map.get(browseByType))) { map.set(browseByType, new Map()); diff --git a/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.spec.ts b/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.spec.ts index c2e1c9cb68..c13405dd4d 100644 --- a/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.spec.ts +++ b/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.spec.ts @@ -3,9 +3,11 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { BROWSE_BY_COMPONENT_FACTORY, BrowseByDataType } from './browse-by-decorator'; -import { BrowseDefinition } from '../../core/shared/browse-definition.model'; import { BehaviorSubject } from 'rxjs'; 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', () => { let comp: BrowseBySwitcherComponent; @@ -13,33 +15,33 @@ describe('BrowseBySwitcherComponent', () => { const types = [ Object.assign( - new BrowseDefinition(), { + new FlatBrowseDefinition(), { id: 'title', dataType: BrowseByDataType.Title, } ), Object.assign( - new BrowseDefinition(), { + new FlatBrowseDefinition(), { id: 'dateissued', dataType: BrowseByDataType.Date, metadataKeys: ['dc.date.issued'] } ), Object.assign( - new BrowseDefinition(), { + new ValueListBrowseDefinition(), { id: 'author', dataType: BrowseByDataType.Metadata, } ), Object.assign( - new BrowseDefinition(), { + new ValueListBrowseDefinition(), { id: 'subject', dataType: BrowseByDataType.Metadata, } ), ]; - const data = new BehaviorSubject(createDataWithBrowseDefinition(new BrowseDefinition())); + const data = new BehaviorSubject(createDataWithBrowseDefinition(new FlatBrowseDefinition())); const activatedRouteStub = { data @@ -70,7 +72,7 @@ describe('BrowseBySwitcherComponent', () => { comp = fixture.componentInstance; })); - types.forEach((type: BrowseDefinition) => { + types.forEach((type: NonHierarchicalBrowseDefinition) => { describe(`when switching to a browse-by page for "${type.id}"`, () => { beforeEach(() => { data.next(createDataWithBrowseDefinition(type)); diff --git a/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.ts b/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.ts index 0d3a35bebf..35e4edf900 100644 --- a/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.ts +++ b/src/app/browse-by/browse-by-switcher/browse-by-switcher.component.ts @@ -31,7 +31,7 @@ export class BrowseBySwitcherComponent implements OnInit { */ ngOnInit(): void { 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())) ); } diff --git a/src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.html b/src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.html new file mode 100644 index 0000000000..87c7937b1b --- /dev/null +++ b/src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.html @@ -0,0 +1,10 @@ +
+
+ + +
+ {{ 'browse.taxonomy.button' | translate }} +
diff --git a/src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.scss b/src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.spec.ts b/src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.spec.ts new file mode 100644 index 0000000000..c724017b1f --- /dev/null +++ b/src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.spec.ts @@ -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; + 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; + }); +}); diff --git a/src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.ts b/src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.ts new file mode 100644 index 0000000000..cf6345bf39 --- /dev/null +++ b/src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.ts @@ -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; + + /** + * Subscriptions to track + */ + browseByComponentSubs: Subscription[] = []; + + public constructor( protected route: ActivatedRoute, + protected themeService: ThemeService, + @Inject(BROWSE_BY_COMPONENT_FACTORY) private getComponentByBrowseByType: (browseByType, theme) => GenericConstructor) { + } + + 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()); + } +} diff --git a/src/app/browse-by/browse-by-taxonomy-page/themed-browse-by-taxonomy-page.component.ts b/src/app/browse-by/browse-by-taxonomy-page/themed-browse-by-taxonomy-page.component.ts new file mode 100644 index 0000000000..212044b853 --- /dev/null +++ b/src/app/browse-by/browse-by-taxonomy-page/themed-browse-by-taxonomy-page.component.ts @@ -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{ + + protected getComponentName(): string { + return 'BrowseByTaxonomyPageComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../../themes/${themeName}/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component`); + } + + protected importUnthemedComponent(): Promise { + return import(`./browse-by-taxonomy-page.component`); + } +} diff --git a/src/app/browse-by/browse-by.module.ts b/src/app/browse-by/browse-by.module.ts index 6c652376d5..c0e2d3f9ff 100644 --- a/src/app/browse-by/browse-by.module.ts +++ b/src/app/browse-by/browse-by.module.ts @@ -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 { BrowseByDatePageComponent } from './browse-by-date-page/browse-by-date-page.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 { ComcolModule } from '../shared/comcol/comcol.module'; 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 { 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 { DsoPageModule } from '../shared/dso-page/dso-page.module'; +import { FormModule } from '../shared/form/form.module'; const ENTRY_COMPONENTS = [ // put only entry components that use custom decorator BrowseByTitlePageComponent, BrowseByMetadataPageComponent, BrowseByDatePageComponent, + BrowseByTaxonomyPageComponent, ThemedBrowseByMetadataPageComponent, ThemedBrowseByDatePageComponent, ThemedBrowseByTitlePageComponent, - + ThemedBrowseByTaxonomyPageComponent, ]; @NgModule({ @@ -29,7 +33,8 @@ const ENTRY_COMPONENTS = [ SharedBrowseByModule, CommonModule, ComcolModule, - DsoPageModule + DsoPageModule, + FormModule, ], declarations: [ BrowseBySwitcherComponent, diff --git a/src/app/core/browse/browse-definition-data.service.ts b/src/app/core/browse/browse-definition-data.service.ts index 88d070000e..bc495a51f4 100644 --- a/src/app/core/browse/browse-definition-data.service.ts +++ b/src/app/core/browse/browse-definition-data.service.ts @@ -1,20 +1,60 @@ +// eslint-disable-next-line max-classes-per-file import { Injectable } from '@angular/core'; import { BROWSE_DEFINITION } from '../shared/browse-definition.resource-type'; -import { BrowseDefinition } from '../shared/browse-definition.model'; import { RequestService } from '../data/request.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; 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 { PaginatedList } from '../data/paginated-list.model'; import { FindListOptions } from '../data/find-list-options.model'; import { IdentifiableDataService } from '../data/base/identifiable-data.service'; import { FindAllData, FindAllDataImpl } from '../data/base/find-all-data'; 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 { 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, + 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 { + createAndSendGetRequest(href$: string | Observable, useCachedVersionIfAvailable: boolean = true) { + createAndSendBrowseDefinitionGetRequest(this.requestService, this.responseMsToLive, href$, useCachedVersionIfAvailable); + } +} /** * 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) export class BrowseDefinitionDataService extends IdentifiableDataService implements FindAllData, SearchData { - private findAllData: FindAllDataImpl; + private findAllData: BrowseDefinitionFindAllDataImpl; private searchData: SearchDataImpl; constructor( @@ -35,7 +75,7 @@ export class BrowseDefinitionDataService extends IdentifiableDataService, useCachedVersionIfAvailable: boolean = true) { + createAndSendBrowseDefinitionGetRequest(this.requestService, this.responseMsToLive, href$, useCachedVersionIfAvailable); + } } diff --git a/src/app/core/browse/browse.service.spec.ts b/src/app/core/browse/browse.service.spec.ts index 46ac8c44f4..9f166e3d19 100644 --- a/src/app/core/browse/browse.service.spec.ts +++ b/src/app/core/browse/browse.service.spec.ts @@ -6,13 +6,15 @@ import { getMockRequestService } from '../../shared/mocks/request.service.mock'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RequestService } from '../data/request.service'; -import { BrowseDefinition } from '../shared/browse-definition.model'; import { BrowseEntrySearchOptions } from './browse-entry-search-options.model'; import { BrowseService } from './browse.service'; import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { createPaginatedList, getFirstUsedArgumentOfSpyMethod } from '../../shared/testing/utils.test'; import { getMockHrefOnlyDataService } from '../../shared/mocks/href-only-data.service.mock'; 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', () => { let scheduler: TestScheduler; @@ -23,9 +25,9 @@ describe('BrowseService', () => { const browsesEndpointURL = 'https://rest.api/browses'; const halService: any = new HALEndpointServiceStub(browsesEndpointURL); const browseDefinitions = [ - Object.assign(new BrowseDefinition(), { + Object.assign(new FlatBrowseDefinition(), { id: 'date', - metadataBrowse: false, + browseType: 'flatBrowse', sortOptions: [ { name: 'title', @@ -50,9 +52,9 @@ describe('BrowseService', () => { items: { href: 'https://rest.api/discover/browses/dateissued/items' } } }), - Object.assign(new BrowseDefinition(), { + Object.assign(new ValueListBrowseDefinition(), { id: 'author', - metadataBrowse: true, + browseType: 'valueList', sortOptions: [ { name: 'title', @@ -78,7 +80,23 @@ describe('BrowseService', () => { entries: { href: 'https://rest.api/discover/browses/author/entries' }, 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; @@ -140,7 +158,7 @@ describe('BrowseService', () => { describe('when getBrowseEntriesFor is called with a valid browse definition id', () => { 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.flush(); diff --git a/src/app/core/browse/browse.service.ts b/src/app/core/browse/browse.service.ts index 989213a978..b210b34949 100644 --- a/src/app/core/browse/browse.service.ts +++ b/src/app/core/browse/browse.service.ts @@ -7,6 +7,7 @@ import { PaginatedList } from '../data/paginated-list.model'; import { RemoteData } from '../data/remote-data'; import { RequestService } from '../data/request.service'; import { BrowseDefinition } from '../shared/browse-definition.model'; +import { FlatBrowseDefinition } from '../shared/flat-browse-definition.model'; import { BrowseEntry } from '../shared/browse-entry.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { Item } from '../shared/item.model'; @@ -240,7 +241,12 @@ export class BrowseService { getPaginatedListPayload(), map((browseDefinitions: BrowseDefinition[]) => browseDefinitions .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); }) ), diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 5b95a96796..dbca773375 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -177,6 +177,10 @@ import { IdentifierData } from '../shared/object-list/identifier-data/identifier import { Subscription } from '../shared/subscriptions/models/subscription.model'; import { SupervisionOrderDataService } from './supervision-order/supervision-order-data.service'; 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'; import { BulkAccessConditionOptions } from './config/models/bulk-access-condition-options.model'; /** @@ -334,6 +338,10 @@ export const models = AuthStatus, BrowseEntry, BrowseDefinition, + NonHierarchicalBrowseDefinition, + FlatBrowseDefinition, + ValueListBrowseDefinition, + HierarchicalBrowseDefinition, ClaimedTask, TaskObject, PoolTask, diff --git a/src/app/core/data/bitstream-data.service.spec.ts b/src/app/core/data/bitstream-data.service.spec.ts index c67eaa2d68..89178f8dd2 100644 --- a/src/app/core/data/bitstream-data.service.spec.ts +++ b/src/app/core/data/bitstream-data.service.spec.ts @@ -1,13 +1,14 @@ +import { TestBed } from '@angular/core/testing'; import { BitstreamDataService } from './bitstream-data.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { RequestService } from './request.service'; import { Bitstream } from '../shared/bitstream.model'; import { HALEndpointService } from '../shared/hal-endpoint.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 { 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 { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; 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 { testPatchDataImplementation } from './base/patch-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', () => { let service: BitstreamDataService; @@ -25,10 +31,18 @@ describe('BitstreamDataService', () => { let rdbService: RemoteDataBuildService; const bitstreamFormatHref = 'rest-api/bitstreamformats'; - const bitstream = Object.assign(new Bitstream(), { - uuid: 'fake-bitstream', + const bitstream1 = Object.assign(new Bitstream(), { + id: 'fake-bitstream1', + uuid: 'fake-bitstream1', _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(), { @@ -50,7 +64,18 @@ describe('BitstreamDataService', () => { }); 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', () => { @@ -62,11 +87,49 @@ describe('BitstreamDataService', () => { describe('when updating the bitstream\'s format', () => { beforeEach(() => { - service.updateFormat(bitstream, format); + service.updateFormat(bitstream1, format); }); it('should send a put request', () => { expect(requestService.send).toHaveBeenCalledWith(jasmine.any(PutRequest)); }); }); + + describe('removeMultiple', () => { + function mockBuildFromRequestUUIDAndAwait(requestUUID$: string | Observable, callback: (rd?: RemoteData) => Observable, ..._linksToFollow: FollowLinkConfig[]): Observable> { + callback(); + return; + } + + beforeEach(() => { + spyOn(service, 'invalidateByHref'); + spyOn(rdbService, 'buildFromRequestUUIDAndAwait').and.callFake((requestUUID$: string | Observable, callback: (rd?: RemoteData) => Observable, ...linksToFollow: FollowLinkConfig[]) => 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'); + }); + }); }); diff --git a/src/app/core/data/bitstream-data.service.ts b/src/app/core/data/bitstream-data.service.ts index 6bdcefe187..bb4ec28166 100644 --- a/src/app/core/data/bitstream-data.service.ts +++ b/src/app/core/data/bitstream-data.service.ts @@ -1,7 +1,7 @@ import { HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; 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 { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; 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 { buildPaginatedList, PaginatedList } from './paginated-list.model'; import { RemoteData } from './remote-data'; -import { PutRequest } from './request.models'; +import { PatchRequest, PutRequest } from './request.models'; import { RequestService } from './request.service'; import { BitstreamFormatDataService } from './bitstream-format-data.service'; 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 { IdentifiableDataService } from './base/identifiable-data.service'; 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 @@ -277,4 +277,34 @@ export class BitstreamDataService extends IdentifiableDataService imp deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable> { 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> { + const operations: RemoveOperation[] = bitstreams.map((bitstream: Bitstream) => { + return { + op: 'remove', + path: `/bitstreams/${bitstream.id}`, + }; + }); + const requestId: string = this.requestService.generateRequestId(); + + const hrefObs: Observable = 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)))); + } + } diff --git a/src/app/core/data/browse-response-parsing.service.spec.ts b/src/app/core/data/browse-response-parsing.service.spec.ts new file mode 100644 index 0000000000..9fa7239ef7 --- /dev/null +++ b/src/app/core/data/browse-response-parsing.service.spec.ts @@ -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(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); + }); + }); +}); diff --git a/src/app/core/data/browse-response-parsing.service.ts b/src/app/core/data/browse-response-parsing.service.ts new file mode 100644 index 0000000000..a568cdb617 --- /dev/null +++ b/src/app/core/data/browse-response-parsing.service.ts @@ -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(obj): any { + const browseType: string = obj.browseType; + if (obj.type === BROWSE_DEFINITION.value && hasValue(browseType)) { + let serializer: Serializer; + 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.'); + } + } +} diff --git a/src/app/core/data/dso-response-parsing.service.ts b/src/app/core/data/dso-response-parsing.service.ts index fd5a22fae9..74117e79d3 100644 --- a/src/app/core/data/dso-response-parsing.service.ts +++ b/src/app/core/data/dso-response-parsing.service.ts @@ -10,6 +10,10 @@ import { hasNoValue, hasValue } from '../../shared/empty.util'; import { DSpaceObject } from '../shared/dspace-object.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() export class DSOResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { protected toCache = true; diff --git a/src/app/core/data/request.models.ts b/src/app/core/data/request.models.ts index 6ab3f180d3..9809bc0fde 100644 --- a/src/app/core/data/request.models.ts +++ b/src/app/core/data/request.models.ts @@ -11,6 +11,7 @@ import { TaskResponseParsingService } from '../tasks/task-response-parsing.servi import { ContentSourceResponseParsingService } from './content-source-response-parsing.service'; import { RestRequestWithResponseParser } from './rest-request-with-response-parser.model'; import { DspaceRestResponseParsingService } from './dspace-rest-response-parsing.service'; +import { BrowseResponseParsingService } from './browse-response-parsing.service'; 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 { + return BrowseResponseParsingService; + } +} + export class FindListRequest extends GetRequest { constructor( uuid: string, diff --git a/src/app/core/shared/browse-definition.model.ts b/src/app/core/shared/browse-definition.model.ts index 863f454422..a5bed53c9f 100644 --- a/src/app/core/shared/browse-definition.model.ts +++ b/src/app/core/shared/browse-definition.model.ts @@ -1,50 +1,16 @@ -import { autoserialize, autoserializeAs, deserialize } 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 { autoserialize } from 'cerialize'; import { CacheableObject } from '../cache/cacheable-object.model'; -import { BrowseByDataType } from '../../browse-by/browse-by-switcher/browse-by-decorator'; -@typedObject -export class BrowseDefinition extends CacheableObject { - static type = BROWSE_DEFINITION; - - /** - * The object type - */ - @excludeFromEquals - @autoserialize - type: ResourceType; +/** + * Base class for BrowseDefinition models + */ +export abstract class BrowseDefinition extends CacheableObject { @autoserialize id: string; - @autoserialize - metadataBrowse: boolean; - - @autoserialize - 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; - }; + /** + * Get the render type of the BrowseDefinition model + */ + abstract getRenderType(): string; } diff --git a/src/app/core/shared/flat-browse-definition.model.ts b/src/app/core/shared/flat-browse-definition.model.ts new file mode 100644 index 0000000000..086fca891b --- /dev/null +++ b/src/app/core/shared/flat-browse-definition.model.ts @@ -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; + } +} diff --git a/src/app/core/shared/flat-browse-definition.resource-type.ts b/src/app/core/shared/flat-browse-definition.resource-type.ts new file mode 100644 index 0000000000..bfb01cd98c --- /dev/null +++ b/src/app/core/shared/flat-browse-definition.resource-type.ts @@ -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'); diff --git a/src/app/core/shared/hierarchical-browse-definition.model.ts b/src/app/core/shared/hierarchical-browse-definition.model.ts new file mode 100644 index 0000000000..d561fff643 --- /dev/null +++ b/src/app/core/shared/hierarchical-browse-definition.model.ts @@ -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'; + } +} diff --git a/src/app/core/shared/hierarchical-browse-definition.resource-type.ts b/src/app/core/shared/hierarchical-browse-definition.resource-type.ts new file mode 100644 index 0000000000..df06d67c7a --- /dev/null +++ b/src/app/core/shared/hierarchical-browse-definition.resource-type.ts @@ -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'); diff --git a/src/app/core/shared/non-hierarchical-browse-definition.ts b/src/app/core/shared/non-hierarchical-browse-definition.ts new file mode 100644 index 0000000000..d5481fcc8d --- /dev/null +++ b/src/app/core/shared/non-hierarchical-browse-definition.ts @@ -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; +} diff --git a/src/app/core/shared/value-list-browse-definition.model.ts b/src/app/core/shared/value-list-browse-definition.model.ts new file mode 100644 index 0000000000..3378263962 --- /dev/null +++ b/src/app/core/shared/value-list-browse-definition.model.ts @@ -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; + } +} diff --git a/src/app/core/shared/value-list-browse-definition.resource-type.ts b/src/app/core/shared/value-list-browse-definition.resource-type.ts new file mode 100644 index 0000000000..8904dc472f --- /dev/null +++ b/src/app/core/shared/value-list-browse-definition.resource-type.ts @@ -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'); diff --git a/src/app/core/submission/vocabularies/vocabulary.service.ts b/src/app/core/submission/vocabularies/vocabulary.service.ts index f2c1747658..1ff5b30ee0 100644 --- a/src/app/core/submission/vocabularies/vocabulary.service.ts +++ b/src/app/core/submission/vocabularies/vocabulary.service.ts @@ -223,13 +223,15 @@ export class VocabularyService { * no valid cached version. Defaults to true * @param reRequestOnStale Whether or not the request should automatically be re- * 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 * {@link HALLink}s should be automatically resolved * @return {Observable>} * Return an observable that emits VocabularyEntryDetail object */ - findEntryDetailById(id: string, name: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> { - const findId = `${name}:${id}`; + findEntryDetailById(id: string, name: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, constructId: boolean = true, ...linksToFollow: FollowLinkConfig[]): Observable> { + const findId: string = (constructId ? `${name}:${id}` : id); return this.vocabularyEntryDetailDataService.findById(findId, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } diff --git a/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.spec.ts b/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.spec.ts index e0fde0e8f2..de8736df94 100644 --- a/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.spec.ts +++ b/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.spec.ts @@ -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')); }); }); diff --git a/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.ts b/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.ts index 5f76d87265..fc0d57046d 100644 --- a/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.ts +++ b/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.ts @@ -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(), ); diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts index 67d047d776..10e1812131 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts @@ -25,6 +25,7 @@ import { getMockRequestService } from '../../../shared/mocks/request.service.moc import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { createPaginatedList } from '../../../shared/testing/utils.test'; import { FieldChangeType } from '../../../core/data/object-updates/field-change-type.model'; +import { BitstreamDataServiceStub } from '../../../shared/testing/bitstream-data-service.stub'; let comp: ItemBitstreamsComponent; let fixture: ComponentFixture; @@ -71,7 +72,7 @@ let objectUpdatesService: ObjectUpdatesService; let router: any; let route: ActivatedRoute; let notificationsService: NotificationsService; -let bitstreamService: BitstreamDataService; +let bitstreamService: BitstreamDataServiceStub; let objectCache: ObjectCacheService; let requestService: RequestService; let searchConfig: SearchConfigurationService; @@ -112,9 +113,7 @@ describe('ItemBitstreamsComponent', () => { success: successNotification } ); - bitstreamService = jasmine.createSpyObj('bitstreamService', { - delete: jasmine.createSpy('delete') - }); + bitstreamService = new BitstreamDataServiceStub(); objectCache = jasmine.createSpyObj('objectCache', { remove: jasmine.createSpy('remove') }); @@ -179,15 +178,16 @@ describe('ItemBitstreamsComponent', () => { describe('when submit is called', () => { beforeEach(() => { + spyOn(bitstreamService, 'removeMultiple').and.callThrough(); comp.submit(); }); - it('should call delete on the bitstreamService for the marked field', () => { - expect(bitstreamService.delete).toHaveBeenCalledWith(bitstream2.id); + it('should call removeMultiple on the bitstreamService for the marked field', () => { + expect(bitstreamService.removeMultiple).toHaveBeenCalledWith([bitstream2]); }); - it('should not call delete on the bitstreamService for the unmarked field', () => { - expect(bitstreamService.delete).not.toHaveBeenCalledWith(bitstream1.id); + it('should not call removeMultiple on the bitstreamService for the unmarked field', () => { + expect(bitstreamService.removeMultiple).not.toHaveBeenCalledWith([bitstream1]); }); }); @@ -210,7 +210,6 @@ describe('ItemBitstreamsComponent', () => { comp.dropBitstream(bundle, { fromIndex: 0, toIndex: 50, - // eslint-disable-next-line no-empty, @typescript-eslint/no-empty-function finish: () => { done(); } diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts index 0c7dfb1e34..ee53bd919c 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectorRef, Component, NgZone, OnDestroy } from '@angular/core'; import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component'; 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 { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; import { ActivatedRoute, Router } from '@angular/router'; @@ -133,20 +133,16 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme ); // Send out delete requests for all deleted bitstreams - const removedResponses$ = removedBitstreams$.pipe( + const removedResponses$: Observable> = removedBitstreams$.pipe( take(1), - switchMap((removedBistreams: Bitstream[]) => { - if (isNotEmpty(removedBistreams)) { - return observableZip(...removedBistreams.map((bitstream: Bitstream) => this.bitstreamService.delete(bitstream.id))); - } else { - return observableOf(undefined); - } + switchMap((removedBitstreams: Bitstream[]) => { + return this.bitstreamService.removeMultiple(removedBitstreams); }) ); // Perform the setup actions from above in order and display notifications - removedResponses$.pipe(take(1)).subscribe((responses: RemoteData[]) => { - this.displayNotifications('item.edit.bitstreams.notifications.remove', responses); + removedResponses$.subscribe((responses: RemoteData) => { + this.displayNotifications('item.edit.bitstreams.notifications.remove', [responses]); this.submitting = false; }); } diff --git a/src/app/item-page/field-components/metadata-values/metadata-values.component.ts b/src/app/item-page/field-components/metadata-values/metadata-values.component.ts index 49ce453fbe..cbbae9006d 100644 --- a/src/app/item-page/field-components/metadata-values/metadata-values.component.ts +++ b/src/app/item-page/field-components/metadata-values/metadata-values.component.ts @@ -3,6 +3,7 @@ import { MetadataValue } from '../../../core/shared/metadata.models'; import { APP_CONFIG, AppConfig } from '../../../../config/app-config.interface'; import { BrowseDefinition } from '../../../core/shared/browse-definition.model'; 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. @@ -84,7 +85,7 @@ export class MetadataValuesComponent implements OnChanges { */ getQueryParams(value) { let queryParams = {startsWith: value}; - if (this.browseDefinition.metadataBrowse) { + if (this.browseDefinition.getRenderType() === VALUE_LIST_BROWSE_DEFINITION.value) { return {value: value}; } return queryParams; diff --git a/src/app/item-page/orcid-page/orcid-queue/orcid-queue.component.html b/src/app/item-page/orcid-page/orcid-queue/orcid-queue.component.html index 9358bcf835..6ba318f7fd 100644 --- a/src/app/item-page/orcid-page/orcid-queue/orcid-queue.component.html +++ b/src/app/item-page/orcid-page/orcid-queue/orcid-queue.component.html @@ -34,11 +34,13 @@
diff --git a/src/app/navbar/navbar.component.spec.ts b/src/app/navbar/navbar.component.spec.ts index ada9be9d0b..983eace055 100644 --- a/src/app/navbar/navbar.component.spec.ts +++ b/src/app/navbar/navbar.component.spec.ts @@ -16,7 +16,6 @@ import { RouterTestingModule } from '@angular/router/testing'; import { BrowseService } from '../core/browse/browse.service'; import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; 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 { Item } from '../core/shared/item.model'; 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 { AuthTokenInfo } from '../core/auth/models/auth-token-info.model'; 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 fixture: ComponentFixture; @@ -66,30 +68,35 @@ describe('NavbarComponent', () => { beforeEach(waitForAsync(() => { browseDefinitions = [ Object.assign( - new BrowseDefinition(), { + new FlatBrowseDefinition(), { id: 'title', dataType: BrowseByDataType.Title, } ), Object.assign( - new BrowseDefinition(), { + new FlatBrowseDefinition(), { id: 'dateissued', dataType: BrowseByDataType.Date, metadataKeys: ['dc.date.issued'] } ), Object.assign( - new BrowseDefinition(), { + new ValueListBrowseDefinition(), { id: 'author', dataType: BrowseByDataType.Metadata, } ), Object.assign( - new BrowseDefinition(), { + new ValueListBrowseDefinition(), { id: 'subject', dataType: BrowseByDataType.Metadata, } ), + Object.assign( + new HierarchicalBrowseDefinition(), { + id: 'srsc', + } + ), ]; initialState = { core: { diff --git a/src/app/process-page/detail/process-detail.component.html b/src/app/process-page/detail/process-detail.component.html index 29cbfc113f..5f905cbfff 100644 --- a/src/app/process-page/detail/process-detail.component.html +++ b/src/app/process-page/detail/process-detail.component.html @@ -1,10 +1,15 @@ -
-
-

{{'process.detail.title' | translate:{ - id: process?.processId, - name: process?.scriptName - } }}

+
+
+
+

+ {{ 'process.detail.title' | translate:{ id: process?.processId, name: process?.scriptName } }} +

+
+
+ Refreshing in {{ seconds }}s +
+
{{ process?.scriptName }}
@@ -17,10 +22,12 @@
+
- {{getFileName(file)}} - ({{(file?.sizeBytes) | dsFileSize }}) + {{getFileName(file)}} + ({{(file?.sizeBytes) | dsFileSize }}) +
@@ -70,7 +77,7 @@ -
+