From 77b2506112e4750f05f1065dd3527859333921e3 Mon Sep 17 00:00:00 2001 From: Marie Verdonck Date: Thu, 16 Jul 2020 15:56:07 +0200 Subject: [PATCH 01/13] 71894: MD field suggestions on edit item page --- .../edit-in-place-field.component.ts | 62 ++++++++++++++----- .../core/data/metadata-field-data.service.ts | 25 ++++++++ src/app/core/metadata/metadata-field.model.ts | 33 +++++++++- src/app/core/registry/registry.service.ts | 9 +-- 4 files changed, 106 insertions(+), 23 deletions(-) diff --git a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts index 5a905fc7ea1..cff1310f226 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts +++ b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts @@ -1,9 +1,13 @@ import { Component, Input, OnChanges, OnInit } from '@angular/core'; +import { MetadataSchemaDataService } from '../../../../core/data/metadata-schema-data.service'; +import { PaginatedList } from '../../../../core/data/paginated-list'; +import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model'; +import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators'; import { hasValue, isNotEmpty } from '../../../../shared/empty.util'; import { RegistryService } from '../../../../core/registry/registry.service'; import { cloneDeep } from 'lodash'; import { BehaviorSubject, Observable, of as observableOf } from 'rxjs'; -import { map, take } from 'rxjs/operators'; +import { map, switchMap, take, tap } from 'rxjs/operators'; import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions'; import { FieldUpdate } from '../../../../core/data/object-updates/object-updates.reducer'; import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; @@ -11,6 +15,7 @@ import { NgModel } from '@angular/forms'; import { MetadatumViewModel } from '../../../../core/shared/metadata.models'; import { MetadataField } from '../../../../core/metadata/metadata-field.model'; import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model'; +import { followLink } from '../../../../shared/utils/follow-link-config.model'; @Component({ // tslint:disable-next-line:component-selector @@ -60,6 +65,7 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges { constructor( private registryService: RegistryService, private objectUpdatesService: ObjectUpdatesService, + private metadataSchemaService: MetadataSchemaDataService ) { } @@ -128,25 +134,53 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges { */ findMetadataFieldSuggestions(query: string): void { if (isNotEmpty(query)) { - this.registryService.queryMetadataFields(query).pipe( - // getSucceededRemoteData(), + this.registryService.queryMetadataFields(query, null, followLink('schema')).pipe( + getSucceededRemoteData(), take(1), - map((data) => data.payload.page) - ).subscribe( - (fields: MetadataField[]) => this.metadataFieldSuggestions.next( - fields.map((field: MetadataField) => { - return { - displayValue: field.toString().split('.').join('.​'), - value: field.toString() - }; - }) - ) - ); + map((data) => data.payload.page), + switchMap((fields: MetadataField[]) => { + return fields.map((field: MetadataField, index: number) => { + // Resolve the metadata field's schema if not already the case, to be able to form complete MD field name + if (!hasValue(field.schemaResolved)) { + field.schema.pipe( + getSucceededRemoteData(), + getRemoteDataPayload(), + map((schema: MetadataSchema) => { + field.schemaResolved = schema; + if (index == fields.length - 1) { + this.setInputSuggestions(fields); + } + }), + take(1) + ).subscribe() + } else { + this.setInputSuggestions(fields); + } + }); + } + ), + take(1)).subscribe(); } else { this.metadataFieldSuggestions.next([]); } } + /** + * Set the list of input suggestion with the given Metadata fields, which all require a resolved MetadataSchema + * @param fields list of Metadata fields, which all require a resolved MetadataSchema + */ + setInputSuggestions(fields: MetadataField[]) { + this.metadataFieldSuggestions.next( + fields.map((field: MetadataField) => { + const fieldNameWhole = field.schemaResolved.prefix + '.' + field.toString(); + return { + displayValue: fieldNameWhole.split('.').join('.​'), + value: fieldNameWhole + }; + }) + ); + } + /** * Check if a user should be allowed to edit this field * @return an observable that emits true when the user should be able to edit this field and false when they should not diff --git a/src/app/core/data/metadata-field-data.service.ts b/src/app/core/data/metadata-field-data.service.ts index 1f7a4b90893..dddde2b93e8 100644 --- a/src/app/core/data/metadata-field-data.service.ts +++ b/src/app/core/data/metadata-field-data.service.ts @@ -1,4 +1,5 @@ import { Injectable } from '@angular/core'; +import { hasValue } from '../../shared/empty.util'; import { dataService } from '../cache/builders/build-decorators'; import { DataService } from './data.service'; import { RequestService } from './request.service'; @@ -27,6 +28,7 @@ import { RequestParam } from '../cache/models/request-param.model'; export class MetadataFieldDataService extends DataService { protected linkPath = 'metadatafields'; protected searchBySchemaLinkPath = 'bySchema'; + protected searchByFieldNameLinkPath = 'byFieldName'; constructor( protected requestService: RequestService, @@ -53,6 +55,29 @@ export class MetadataFieldDataService extends DataService { return this.searchBy(this.searchBySchemaLinkPath, optionsWithSchema, ...linksToFollow); } + /** + * Find metadata fields with either the partial metadata field name (e.g. "dc.ti") as query or an exact match to + * at least the schema, element or qualifier + * @param schema optional; an exact match of the prefix of the metadata schema (e.g. "dc", "dcterms", "eperson") + * @param element optional; an exact match of the field's element (e.g. "contributor", "title") + * @param qualifier optional; an exact match of the field's qualifier (e.g. "author", "alternative") + * @param query optional (if any of schema, element or qualifier used) - part of the fully qualified field, + * should start with the start of the schema (e.g. "dc.ti") + * @param options The options info used to retrieve the fields + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + */ + findByFieldName(schema: string, element: string, qualifier: string, query: string, options: FindListOptions = {}, ...linksToFollow: Array>) { + const optionParams = Object.assign(new FindListOptions(), options, { + searchParams: [ + new RequestParam('schema', hasValue(schema) ? schema : ''), + new RequestParam('element', hasValue(element) ? element : ''), + new RequestParam('qualifier', hasValue(qualifier) ? qualifier : ''), + new RequestParam('query', hasValue(query) ? query : '') + ] + }); + return this.searchBy(this.searchByFieldNameLinkPath, optionParams, ...linksToFollow); + } + /** * Clear all metadata field requests * Used for refreshing lists after adding/updating/removing a metadata field from a metadata schema diff --git a/src/app/core/metadata/metadata-field.model.ts b/src/app/core/metadata/metadata-field.model.ts index 66171141c51..41dfa784a4e 100644 --- a/src/app/core/metadata/metadata-field.model.ts +++ b/src/app/core/metadata/metadata-field.model.ts @@ -1,10 +1,13 @@ import { autoserialize, deserialize } from 'cerialize'; -import { isNotEmpty } from '../../shared/empty.util'; +import { map } from 'rxjs/operators'; +import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { ListableObject } from '../../shared/object-collection/shared/listable-object.model'; import { link, typedObject } from '../cache/builders/build-decorators'; +import { MetadataSchemaDataService } from '../data/metadata-schema-data.service'; import { GenericConstructor } from '../shared/generic-constructor'; import { HALLink } from '../shared/hal-link.model'; import { HALResource } from '../shared/hal-resource.model'; +import { getRemoteDataPayload, getSucceededRemoteData } from '../shared/operators'; import { ResourceType } from '../shared/resource-type'; import { excludeFromEquals } from '../utilities/equals.decorators'; import { METADATA_FIELD } from './metadata-field.resource-type'; @@ -67,9 +70,11 @@ export class MetadataField extends ListableObject implements HALResource { @link(METADATA_SCHEMA) schema?: Observable>; + schemaResolved?: MetadataSchema; + /** - * Method to print this metadata field as a string - * @param separator The separator between the schema, element and qualifier in the string + * Method to print this metadata field as a string without the schema + * @param separator The separator between element and qualifier in the string */ toString(separator: string = '.'): string { let key = this.element; @@ -79,6 +84,28 @@ export class MetadataField extends ListableObject implements HALResource { return key; } + /** + * Method to print this metadata field as a string + * @param separator The separator between the schema, element and qualifier in the string + */ + toStringWithSchema(separator: string = '.', schemaService: MetadataSchemaDataService): Observable { + let schemaObject: Observable> = this.schema; + if (!hasValue(this.schema)) { + schemaObject = schemaService.findByHref(this._links.schema.href); + } + return schemaObject.pipe( + getSucceededRemoteData(), + getRemoteDataPayload(), + map((schemaPayload: MetadataSchema) => { + let key = this.element; + if (isNotEmpty(this.qualifier)) { + key += separator + this.qualifier; + } + return schemaPayload.namespace + separator + key; + }) + ) + } + /** * Method that returns as which type of object this object should be rendered */ diff --git a/src/app/core/registry/registry.service.ts b/src/app/core/registry/registry.service.ts index 72de6ec7933..726ae4613d4 100644 --- a/src/app/core/registry/registry.service.ts +++ b/src/app/core/registry/registry.service.ts @@ -297,13 +297,10 @@ export class RegistryService { /** * Retrieve a filtered paginated list of metadata fields - * @param query {string} The query to filter the field names by + * @param query {string} The query to use for the metadata field name, can be partial (e.g. "dc.ti") * @returns an observable that emits a remote data object with a page of metadata fields that match the query */ - // TODO this is temporarily disabled. The performance is too bad. - // Querying metadatafields will need to be implemented as a search endpoint on the rest api, - // not by downloading everything and preforming the query client side. - queryMetadataFields(query: string): Observable>> { - return createSuccessfulRemoteDataObject$(new PaginatedList(null, [])); + queryMetadataFields(query: string, options: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { + return this.metadataFieldService.findByFieldName(null, null, null, query, options, ...linksToFollow); } } From a6c0a60bd741fbc9edd8dd95282acae4f2d7e772 Mon Sep 17 00:00:00 2001 From: Marie Verdonck Date: Thu, 16 Jul 2020 18:07:21 +0200 Subject: [PATCH 02/13] 71894: Start of mdField validator; todo: show error & some backend changes --- .../edit-in-place-field.component.html | 1 - .../edit-in-place-field.component.ts | 6 -- .../item-metadata.component.html | 1 - .../item-metadata/item-metadata.component.ts | 17 ----- src/app/core/registry/registry.service.ts | 14 ---- .../filter-input-suggestions.component.html | 5 +- src/app/shared/shared.module.ts | 4 +- .../metadatafield-validator.directive.ts | 66 +++++++++++++++++++ 8 files changed, 72 insertions(+), 42 deletions(-) create mode 100644 src/app/shared/utils/metadatafield-validator.directive.ts diff --git a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.html b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.html index 80c78941c87..9f409d11bd6 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.html +++ b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.html @@ -12,7 +12,6 @@ (dsClickOutside)="checkValidity(suggestionControl)" (findSuggestions)="findMetadataFieldSuggestions($event)" #suggestionControl="ngModel" - [dsInListValidator]="metadataFields" [valid]="(valid | async) !== false" dsAutoFocus autoFocusSelector=".suggestion_input" [ngModelOptions]="{standalone: true}" diff --git a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts index cff1310f226..1bd4ebf341a 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts +++ b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts @@ -37,11 +37,6 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges { */ @Input() url: string; - /** - * List of strings with all metadata field keys available - */ - @Input() metadataFields: string[]; - /** * The metadatum of this field */ @@ -65,7 +60,6 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges { constructor( private registryService: RegistryService, private objectUpdatesService: ObjectUpdatesService, - private metadataSchemaService: MetadataSchemaDataService ) { } diff --git a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.html b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.html index 366f6fffe2b..042ed2d339a 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.html +++ b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.html @@ -33,7 +33,6 @@ + [ngModelOptions]="{standalone: true}" autocomplete="off" + dsMetadataFieldValidator /> - \ No newline at end of file + diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 66bdea92177..bcdf8b2fd27 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -21,6 +21,7 @@ import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import { EnumKeysPipe } from './utils/enum-keys-pipe'; import { FileSizePipe } from './utils/file-size-pipe'; +import { MetadataFieldValidator } from './utils/metadatafield-validator.directive'; import { SafeUrlPipe } from './utils/safe-url-pipe'; import { ConsolePipe } from './utils/console.pipe'; @@ -516,7 +517,8 @@ const DIRECTIVES = [ FileValueAccessorDirective, FileValidator, ClaimedTaskActionsDirective, - NgForTrackByIdDirective + NgForTrackByIdDirective, + MetadataFieldValidator ]; @NgModule({ diff --git a/src/app/shared/utils/metadatafield-validator.directive.ts b/src/app/shared/utils/metadatafield-validator.directive.ts new file mode 100644 index 00000000000..f4ee756fcb7 --- /dev/null +++ b/src/app/shared/utils/metadatafield-validator.directive.ts @@ -0,0 +1,66 @@ +import { Directive, Injectable } from '@angular/core'; +import { AbstractControl, AsyncValidator, NG_VALIDATORS, ValidationErrors } from '@angular/forms'; +import { map, switchMap, take } from 'rxjs/operators'; +import { of as observableOf, timer as observableTimer, Observable } from 'rxjs'; +import { MetadataFieldDataService } from '../../core/data/metadata-field-data.service'; +import { getSucceededRemoteData } from '../../core/shared/operators'; + +/** + * Directive for validating if a ngModel value is a valid metadata field + */ +@Directive({ + selector: '[ngModel][dsMetadataFieldValidator]', + // We add our directive to the list of existing validators + providers: [ + { provide: NG_VALIDATORS, useExisting: MetadataFieldValidator, multi: true } + ] +}) +@Injectable({ providedIn: 'root' }) +export class MetadataFieldValidator implements AsyncValidator { + + constructor(private metadataFieldService: MetadataFieldDataService) { + } + + /** + * The function that checks if the form control's value is currently valid + * @param control The FormControl + */ + validate(control: AbstractControl): Observable { + const resTimer = observableTimer(500).pipe( + switchMap(() => { + console.log('control', control) + if (!control.value) { + return observableOf({ invalidMetadataField: { value: control.value } }); + } + const mdFieldNameParts = control.value.split('.'); + if (mdFieldNameParts.length < 2) { + console.log('not enough parts') + return observableOf({ invalidMetadataField: { value: control.value } }); + } + + const res = this.metadataFieldService.findByFieldName(mdFieldNameParts[0], mdFieldNameParts[1], mdFieldNameParts.length == 3 ? mdFieldNameParts[2] : '', '') + .pipe( + getSucceededRemoteData(), + map((results) => { + console.log('results', results) + // TODO: - Currently it’s valid if the search returns at least one matching mdField; but this does mean that if for example a mdField named schema.elementEx.qualifierEx exists, but you fill in schema. or schema.elementEx then there is at least one result, but this doesn’t mean this is the whole of the field (the suggestion does show the options); alternatively it is valid if exact one matching field but then dc.title isn’t valid because the search schema=dc & element=title also returns for example dc.title.alternative + // - So the endpoint / restcontract should probably be changed to accommodate and exact search? Or was that already what was wanted and did I interpret it wrong? + if (results.payload.totalElements > 0) { + console.log('VALID') + return null; + } else { + console.log('NOT VALID') + return { invalidMetadataField: { value: control.value } }; + } + }) + ); + + res.pipe(take(1)).subscribe(); + + return res; + }) + ); + resTimer.pipe(take(1)).subscribe(); + return resTimer; + } +} From 4a0444d669d89eb7852a9f701e5c0367111593b3 Mon Sep 17 00:00:00 2001 From: Marie Verdonck Date: Fri, 17 Jul 2020 17:41:46 +0200 Subject: [PATCH 03/13] 71894: mdField validation; TODO: fix EditInPlace tests --- .../edit-in-place-field.component.html | 2 - .../edit-in-place-field.component.spec.ts | 21 ++++++---- .../edit-in-place-field.component.ts | 4 +- src/app/core/core.module.ts | 2 + .../core/data/metadata-field-data.service.ts | 41 ++++++++++++++++--- .../metadatafield-response-parsing.service.ts | 19 +++++++++ src/app/core/data/request.models.ts | 11 +++++ src/app/core/registry/registry.service.ts | 18 +++++--- .../filter-input-suggestions.component.html | 38 +++++++++-------- ...filter-input-suggestions.component.spec.ts | 15 ++++--- .../filter-input-suggestions.component.ts | 23 +++++++++-- .../metadatafield-validator.directive.ts | 18 +++----- 12 files changed, 150 insertions(+), 62 deletions(-) create mode 100644 src/app/core/data/metadatafield-response-parsing.service.ts diff --git a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.html b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.html index 9f409d11bd6..de8b74c312e 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.html +++ b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.html @@ -17,8 +17,6 @@ [ngModelOptions]="{standalone: true}" > - {{"item.edit.metadata.metadatafield.invalid" | translate}} diff --git a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.spec.ts b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.spec.ts index 19765f0e095..2936119d499 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.spec.ts @@ -6,6 +6,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { getTestScheduler } from 'jasmine-marbles'; import { of as observableOf } from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; +import { MetadataFieldDataService } from '../../../../core/data/metadata-field-data.service'; import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions'; import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; import { PaginatedList } from '../../../../core/data/paginated-list'; @@ -29,13 +30,18 @@ const mdSchema = Object.assign(new MetadataSchema(), { prefix: 'dc' }) const mdField1 = Object.assign(new MetadataField(), { schema: mdSchema, element: 'contributor', - qualifier: 'author' + qualifier: 'author', + schemaResolved: mdSchema, }); -const mdField2 = Object.assign(new MetadataField(), { schema: mdSchema, element: 'title' }); +const mdField2 = Object.assign(new MetadataField(), { + schema: mdSchema, + element: 'title', + schemaResolved: mdSchema, }); const mdField3 = Object.assign(new MetadataField(), { schema: mdSchema, element: 'description', - qualifier: 'abstract' + qualifier: 'abstract', + schemaResolved: mdSchema, }); const metadatum = Object.assign(new MetadatumViewModel(), { @@ -51,7 +57,7 @@ const fieldUpdate = { }; let scheduler: TestScheduler; -describe('EditInPlaceFieldComponent', () => { +fdescribe('EditInPlaceFieldComponent', () => { beforeEach(async(() => { scheduler = getTestScheduler(); @@ -79,6 +85,7 @@ describe('EditInPlaceFieldComponent', () => { providers: [ { provide: RegistryService, useValue: metadataFieldService }, { provide: ObjectUpdatesService, useValue: objectUpdatesService }, + { provide: MetadataFieldDataService, useValue: {} } ], schemas: [ CUSTOM_ELEMENTS_SCHEMA ] @@ -192,9 +199,9 @@ describe('EditInPlaceFieldComponent', () => { const metadataFieldSuggestions: InputSuggestion[] = [ - { displayValue: mdField1.toString().split('.').join('.​'), value: mdField1.toString() }, - { displayValue: mdField2.toString().split('.').join('.​'), value: mdField2.toString() }, - { displayValue: mdField3.toString().split('.').join('.​'), value: mdField3.toString() } + { displayValue: (mdField1.schemaResolved.prefix + '.' + mdField1.toString()).split('.').join('.​'), value: (mdField1.schemaResolved.prefix + '.' + mdField1.toString()) }, + { displayValue: (mdField2.schemaResolved.prefix + '.' + mdField2.toString()).split('.').join('.​'), value: (mdField2.schemaResolved.prefix + '.' + mdField2.toString()) }, + { displayValue: (mdField3.schemaResolved.prefix + '.' + mdField3.toString()).split('.').join('.​'), value: (mdField3.schemaResolved.prefix + '.' + mdField3.toString()) } ]; beforeEach(() => { diff --git a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts index 1bd4ebf341a..3dd5e2091d8 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts +++ b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts @@ -1,13 +1,11 @@ import { Component, Input, OnChanges, OnInit } from '@angular/core'; -import { MetadataSchemaDataService } from '../../../../core/data/metadata-schema-data.service'; -import { PaginatedList } from '../../../../core/data/paginated-list'; import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model'; import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators'; import { hasValue, isNotEmpty } from '../../../../shared/empty.util'; import { RegistryService } from '../../../../core/registry/registry.service'; import { cloneDeep } from 'lodash'; import { BehaviorSubject, Observable, of as observableOf } from 'rxjs'; -import { map, switchMap, take, tap } from 'rxjs/operators'; +import { map, switchMap, take } from 'rxjs/operators'; import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions'; import { FieldUpdate } from '../../../../core/data/object-updates/object-updates.reducer'; import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 5aa462d5e0e..299b04fe263 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -68,6 +68,7 @@ import { ItemDataService } from './data/item-data.service'; import { LicenseDataService } from './data/license-data.service'; import { LookupRelationService } from './data/lookup-relation.service'; import { MappedCollectionsReponseParsingService } from './data/mapped-collections-reponse-parsing.service'; +import { MetadatafieldParsingService } from './data/metadatafield-response-parsing.service'; import { MyDSpaceResponseParsingService } from './data/mydspace-response-parsing.service'; import { ObjectUpdatesService } from './data/object-updates/object-updates.service'; import { RelationshipTypeService } from './data/relationship-type.service'; @@ -289,6 +290,7 @@ const PROVIDERS = [ MetadataSchemaDataService, MetadataFieldDataService, TokenResponseParsingService, + MetadatafieldParsingService, // register AuthInterceptor as HttpInterceptor { provide: HTTP_INTERCEPTORS, diff --git a/src/app/core/data/metadata-field-data.service.ts b/src/app/core/data/metadata-field-data.service.ts index dddde2b93e8..ceaace024b1 100644 --- a/src/app/core/data/metadata-field-data.service.ts +++ b/src/app/core/data/metadata-field-data.service.ts @@ -1,7 +1,10 @@ import { Injectable } from '@angular/core'; -import { hasValue } from '../../shared/empty.util'; +import { hasValue, isNotEmptyOperator } from '../../shared/empty.util'; import { dataService } from '../cache/builders/build-decorators'; +import { RestResponse } from '../cache/response.models'; +import { configureRequest } from '../shared/operators'; import { DataService } from './data.service'; +import { RequestEntry } from './request.reducer'; import { RequestService } from './request.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { Store } from '@ngrx/store'; @@ -14,10 +17,14 @@ import { NotificationsService } from '../../shared/notifications/notifications.s import { METADATA_FIELD } from '../metadata/metadata-field.resource-type'; import { MetadataField } from '../metadata/metadata-field.model'; import { MetadataSchema } from '../metadata/metadata-schema.model'; -import { FindListOptions } from './request.models'; +import { + FindListOptions, + GetMetadataFieldRequest, + RestRequest +} from './request.models'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { Observable } from 'rxjs/internal/Observable'; -import { tap } from 'rxjs/operators'; +import { distinctUntilChanged, find, map, switchMap, tap } from 'rxjs/operators'; import { RequestParam } from '../cache/models/request-param.model'; /** @@ -62,11 +69,11 @@ export class MetadataFieldDataService extends DataService { * @param element optional; an exact match of the field's element (e.g. "contributor", "title") * @param qualifier optional; an exact match of the field's qualifier (e.g. "author", "alternative") * @param query optional (if any of schema, element or qualifier used) - part of the fully qualified field, - * should start with the start of the schema (e.g. "dc.ti") + * should start with the start of the schema, element or qualifier (e.g. “dc.ti”, “contributor”, “auth”, “contributor.ot”) * @param options The options info used to retrieve the fields * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved */ - findByFieldName(schema: string, element: string, qualifier: string, query: string, options: FindListOptions = {}, ...linksToFollow: Array>) { + searchByFieldNameParams(schema: string, element: string, qualifier: string, query: string, options: FindListOptions = {}, ...linksToFollow: Array>) { const optionParams = Object.assign(new FindListOptions(), options, { searchParams: [ new RequestParam('schema', hasValue(schema) ? schema : ''), @@ -78,6 +85,30 @@ export class MetadataFieldDataService extends DataService { return this.searchBy(this.searchByFieldNameLinkPath, optionParams, ...linksToFollow); } + /** + * Finds a specific metadata field by name. There's always at most one metadata field per name. + * If the metadata field can be found it is in the response, otherwise the response will have code 404 + * @param exactFieldName Exact metadata field name (ex. dc.title, dc.contributor.other) + */ + findByExactFieldName(exactFieldName: string): Observable { + const request$ = this.halService.getEndpoint(this.linkPath).pipe( + isNotEmptyOperator(), + distinctUntilChanged(), + map((endpoint: string) => `${endpoint}/name/${exactFieldName}`), + map((endpointURL: string) => new GetMetadataFieldRequest(this.requestService.generateRequestId(), endpointURL)), + configureRequest(this.requestService) + ); + + const requestEntry$ = request$.pipe( + switchMap((request: RestRequest) => this.requestService.getByHref(request.href)) + ); + + return requestEntry$.pipe( + find((request: RequestEntry) => request.completed), + map((request: RequestEntry) => request.response) + ); + } + /** * Clear all metadata field requests * Used for refreshing lists after adding/updating/removing a metadata field from a metadata schema diff --git a/src/app/core/data/metadatafield-response-parsing.service.ts b/src/app/core/data/metadatafield-response-parsing.service.ts new file mode 100644 index 00000000000..6eddfded79c --- /dev/null +++ b/src/app/core/data/metadatafield-response-parsing.service.ts @@ -0,0 +1,19 @@ +import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; +import { MetadataField } from '../metadata/metadata-field.model'; +import { RestRequest } from './request.models'; +import { ResponseParsingService } from './parsing.service'; +import { Injectable } from '@angular/core'; +import { MetadatafieldSuccessResponse, RestResponse } from '../cache/response.models'; + +@Injectable() +export class MetadatafieldParsingService implements ResponseParsingService { + + parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { + const payload = data.payload; + + const deserialized = new DSpaceSerializer(MetadataField).deserialize(payload); + return new MetadatafieldSuccessResponse(deserialized, data.statusCode, data.statusText); + } + +} diff --git a/src/app/core/data/request.models.ts b/src/app/core/data/request.models.ts index bd497d4ddb1..d3f60fe0670 100644 --- a/src/app/core/data/request.models.ts +++ b/src/app/core/data/request.models.ts @@ -2,6 +2,7 @@ import { SortOptions } from '../cache/models/sort-options.model'; import { GenericConstructor } from '../shared/generic-constructor'; import { BrowseEntriesResponseParsingService } from './browse-entries-response-parsing.service'; import { DSOResponseParsingService } from './dso-response-parsing.service'; +import { MetadatafieldParsingService } from './metadatafield-response-parsing.service'; import { ResponseParsingService } from './parsing.service'; import { EndpointMapResponseParsingService } from './endpoint-map-response-parsing.service'; import { BrowseResponseParsingService } from './browse-response-parsing.service'; @@ -429,4 +430,14 @@ export class RequestError extends Error { statusCode: number; statusText: string; } + +export class GetMetadataFieldRequest extends GetRequest { + constructor(uuid: string, href: string, public options?: HttpOptions) { + super(uuid, href, null, options); + } + + getResponseParser(): GenericConstructor { + return MetadatafieldParsingService; + } +} /* tslint:enable:max-classes-per-file */ diff --git a/src/app/core/registry/registry.service.ts b/src/app/core/registry/registry.service.ts index ec4b5f8a5ad..899d1582def 100644 --- a/src/app/core/registry/registry.service.ts +++ b/src/app/core/registry/registry.service.ts @@ -30,7 +30,6 @@ import { MetadataSchemaDataService } from '../data/metadata-schema-data.service' import { MetadataFieldDataService } from '../data/metadata-field-data.service'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { RequestParam } from '../cache/models/request-param.model'; -import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; const metadataRegistryStateSelector = (state: AppState) => state.metadataRegistry; const editMetadataSchemaSelector = createSelector(metadataRegistryStateSelector, (metadataState: MetadataRegistryState) => metadataState.editSchema); @@ -137,6 +136,7 @@ export class RegistryService { public getSelectedMetadataSchemas(): Observable { return this.store.pipe(select(selectedMetadataSchemasSelector)); } + /** * Method to start editing a metadata field, dispatches an edit field action * @param field The field that's being edited @@ -151,12 +151,14 @@ export class RegistryService { public cancelEditMetadataField() { this.store.dispatch(new MetadataRegistryCancelFieldAction()); } + /** * Method to retrieve the metadata field that are currently being edited */ public getActiveMetadataField(): Observable { return this.store.pipe(select(editMetadataFieldSelector)); } + /** * Method to select a metadata field, dispatches a select field action * @param field The field that's being selected @@ -164,6 +166,7 @@ export class RegistryService { public selectMetadataField(field: MetadataField) { this.store.dispatch(new MetadataRegistrySelectFieldAction(field)); } + /** * Method to deselect a metadata field, dispatches a deselect field action * @param field The field that's it being deselected @@ -171,6 +174,7 @@ export class RegistryService { public deselectMetadataField(field: MetadataField) { this.store.dispatch(new MetadataRegistryDeselectFieldAction(field)); } + /** * Method to deselect all currently selected metadata fields, dispatches a deselect all field action */ @@ -199,7 +203,7 @@ export class RegistryService { getFirstSucceededRemoteDataPayload(), hasValueOperator(), tap(() => { - this.showNotifications(true, isUpdate, false, {prefix: schema.prefix}); + this.showNotifications(true, isUpdate, false, { prefix: schema.prefix }); }) ); } @@ -230,7 +234,7 @@ export class RegistryService { getFirstSucceededRemoteDataPayload(), hasValueOperator(), tap(() => { - this.showNotifications(true, false, true, {field: field.toString()}); + this.showNotifications(true, false, true, { field: field.toString() }); }) ); } @@ -245,7 +249,7 @@ export class RegistryService { getFirstSucceededRemoteDataPayload(), hasValueOperator(), tap(() => { - this.showNotifications(true, true, true, {field: field.toString()}); + this.showNotifications(true, true, true, { field: field.toString() }); }) ); } @@ -257,6 +261,7 @@ export class RegistryService { public deleteMetadataField(id: number): Observable { return this.metadataFieldService.delete(`${id}`); } + /** * Method that clears a cached metadata field request and returns its REST url */ @@ -283,10 +288,11 @@ export class RegistryService { /** * Retrieve a filtered paginated list of metadata fields - * @param query {string} The query to use for the metadata field name, can be partial (e.g. "dc.ti") + * @param query {string} The query to use for the metadata field name, can be part of the fully qualified field, + * should start with the start of the schema, element or qualifier (e.g. “dc.ti”, “contributor”, “auth”, “contributor.ot”) * @returns an observable that emits a remote data object with a page of metadata fields that match the query */ queryMetadataFields(query: string, options: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { - return this.metadataFieldService.findByFieldName(null, null, null, query, options, ...linksToFollow); + return this.metadataFieldService.searchByFieldNameParams(null, null, null, query, options, ...linksToFollow); } } diff --git a/src/app/shared/input-suggestions/filter-suggestions/filter-input-suggestions.component.html b/src/app/shared/input-suggestions/filter-suggestions/filter-input-suggestions.component.html index 2467dfb5699..e09a869091c 100644 --- a/src/app/shared/input-suggestions/filter-suggestions/filter-input-suggestions.component.html +++ b/src/app/shared/input-suggestions/filter-suggestions/filter-input-suggestions.component.html @@ -1,23 +1,27 @@ -
- - -
+ {{"item.edit.metadata.metadatafield.invalid" | translate}} diff --git a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.spec.ts b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.spec.ts index 2936119d499..e4040e34fbd 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.spec.ts @@ -17,6 +17,7 @@ import { MetadatumViewModel } from '../../../../core/shared/metadata.models'; import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model'; import { SharedModule } from '../../../../shared/shared.module'; import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; +import { followLink } from '../../../../shared/utils/follow-link-config.model'; import { EditInPlaceFieldComponent } from './edit-in-place-field.component'; let comp: EditInPlaceFieldComponent; @@ -57,7 +58,7 @@ const fieldUpdate = { }; let scheduler: TestScheduler; -fdescribe('EditInPlaceFieldComponent', () => { +describe('EditInPlaceFieldComponent', () => { beforeEach(async(() => { scheduler = getTestScheduler(); @@ -211,7 +212,7 @@ fdescribe('EditInPlaceFieldComponent', () => { it('it should call queryMetadataFields on the metadataFieldService with the correct query', () => { - expect(metadataFieldService.queryMetadataFields).toHaveBeenCalledWith(query); + expect(metadataFieldService.queryMetadataFields).toHaveBeenCalledWith(query, null, followLink('schema')); }); it('it should set metadataFieldSuggestions to the right value', () => { diff --git a/src/app/shared/input-suggestions/filter-suggestions/filter-input-suggestions.component.html b/src/app/shared/input-suggestions/filter-suggestions/filter-input-suggestions.component.html index e09a869091c..783201b6a11 100644 --- a/src/app/shared/input-suggestions/filter-suggestions/filter-input-suggestions.component.html +++ b/src/app/shared/input-suggestions/filter-suggestions/filter-input-suggestions.component.html @@ -8,6 +8,7 @@ [ngClass]="{'is-invalid': !valid}" [dsDebounce]="debounceTime" (onDebounce)="find($event)" [placeholder]="placeholder" + ng-model-options="{standalone: true}" autocomplete="off"> Date: Mon, 27 Jul 2020 14:43:41 +0200 Subject: [PATCH 05/13] 71894: refactor with new operator metadataFieldsToString instead of schemaResolved --- .../edit-in-place-field.component.spec.ts | 40 ++++++++------- .../edit-in-place-field.component.ts | 49 +++++-------------- src/app/core/metadata/metadata-field.model.ts | 24 --------- src/app/core/shared/operators.ts | 30 +++++++++++- 4 files changed, 62 insertions(+), 81 deletions(-) diff --git a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.spec.ts b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.spec.ts index e4040e34fbd..b89a0a846c2 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.spec.ts @@ -1,5 +1,5 @@ import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core'; -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; import { FormsModule } from '@angular/forms'; import { By } from '@angular/platform-browser'; import { TranslateModule } from '@ngx-translate/core'; @@ -16,7 +16,10 @@ import { RegistryService } from '../../../../core/registry/registry.service'; import { MetadatumViewModel } from '../../../../core/shared/metadata.models'; import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model'; import { SharedModule } from '../../../../shared/shared.module'; -import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; +import { + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$ +} from '../../../../shared/remote-data.utils'; import { followLink } from '../../../../shared/utils/follow-link-config.model'; import { EditInPlaceFieldComponent } from './edit-in-place-field.component'; @@ -27,22 +30,21 @@ let el: HTMLElement; let metadataFieldService; let objectUpdatesService; let paginatedMetadataFields; -const mdSchema = Object.assign(new MetadataSchema(), { prefix: 'dc' }) +const mdSchema = Object.assign(new MetadataSchema(), { prefix: 'dc' }); +const mdSchemaRD$ = createSuccessfulRemoteDataObject$(mdSchema); const mdField1 = Object.assign(new MetadataField(), { - schema: mdSchema, + schema: mdSchemaRD$, element: 'contributor', - qualifier: 'author', - schemaResolved: mdSchema, + qualifier: 'author' }); const mdField2 = Object.assign(new MetadataField(), { - schema: mdSchema, - element: 'title', - schemaResolved: mdSchema, }); + schema: mdSchemaRD$, + element: 'title' +}); const mdField3 = Object.assign(new MetadataField(), { - schema: mdSchema, + schema: mdSchemaRD$, element: 'description', qualifier: 'abstract', - schemaResolved: mdSchema, }); const metadatum = Object.assign(new MetadatumViewModel(), { @@ -58,7 +60,7 @@ const fieldUpdate = { }; let scheduler: TestScheduler; -describe('EditInPlaceFieldComponent', () => { +fdescribe('EditInPlaceFieldComponent', () => { beforeEach(async(() => { scheduler = getTestScheduler(); @@ -200,18 +202,18 @@ describe('EditInPlaceFieldComponent', () => { const metadataFieldSuggestions: InputSuggestion[] = [ - { displayValue: (mdField1.schemaResolved.prefix + '.' + mdField1.toString()).split('.').join('.​'), value: (mdField1.schemaResolved.prefix + '.' + mdField1.toString()) }, - { displayValue: (mdField2.schemaResolved.prefix + '.' + mdField2.toString()).split('.').join('.​'), value: (mdField2.schemaResolved.prefix + '.' + mdField2.toString()) }, - { displayValue: (mdField3.schemaResolved.prefix + '.' + mdField3.toString()).split('.').join('.​'), value: (mdField3.schemaResolved.prefix + '.' + mdField3.toString()) } + { displayValue: ('dc.' + mdField1.toString()).split('.').join('.​'), value: ('dc.' + mdField1.toString()) }, + { displayValue: ('dc.' + mdField2.toString()).split('.').join('.​'), value: ('dc.' + mdField2.toString()) }, + { displayValue: ('dc.' + mdField3.toString()).split('.').join('.​'), value: ('dc.' + mdField3.toString()) } ]; - beforeEach(() => { + beforeEach(fakeAsync(() => { comp.findMetadataFieldSuggestions(query); - - }); + tick(); + fixture.detectChanges(); + })); it('it should call queryMetadataFields on the metadataFieldService with the correct query', () => { - expect(metadataFieldService.queryMetadataFields).toHaveBeenCalledWith(query, null, followLink('schema')); }); diff --git a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts index 3dd5e2091d8..7218ae10b09 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts +++ b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts @@ -1,17 +1,15 @@ import { Component, Input, OnChanges, OnInit } from '@angular/core'; -import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model'; -import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators'; +import { metadataFieldsToString } from '../../../../core/shared/operators'; import { hasValue, isNotEmpty } from '../../../../shared/empty.util'; import { RegistryService } from '../../../../core/registry/registry.service'; import { cloneDeep } from 'lodash'; import { BehaviorSubject, Observable, of as observableOf } from 'rxjs'; -import { map, switchMap, take } from 'rxjs/operators'; +import { map, take } from 'rxjs/operators'; import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions'; import { FieldUpdate } from '../../../../core/data/object-updates/object-updates.reducer'; import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; import { NgModel } from '@angular/forms'; import { MetadatumViewModel } from '../../../../core/shared/metadata.models'; -import { MetadataField } from '../../../../core/metadata/metadata-field.model'; import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model'; import { followLink } from '../../../../shared/utils/follow-link-config.model'; @@ -124,34 +122,14 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges { * Ignores fields from metadata schemas "relation" and "relationship" * @param query The query to look for */ - findMetadataFieldSuggestions(query: string): void { + findMetadataFieldSuggestions(query: string) { if (isNotEmpty(query)) { - this.registryService.queryMetadataFields(query, null, followLink('schema')).pipe( - getSucceededRemoteData(), - take(1), - map((data) => data.payload.page), - switchMap((fields: MetadataField[]) => { - return fields.map((field: MetadataField, index: number) => { - // Resolve the metadata field's schema if not already the case, to be able to form complete MD field name - if (!hasValue(field.schemaResolved)) { - field.schema.pipe( - getSucceededRemoteData(), - getRemoteDataPayload(), - map((schema: MetadataSchema) => { - field.schemaResolved = schema; - if (index == fields.length - 1) { - this.setInputSuggestions(fields); - } - }), - take(1) - ).subscribe() - } else { - this.setInputSuggestions(fields); - } - }); - } - ), - take(1)).subscribe(); + return this.registryService.queryMetadataFields(query, null, followLink('schema')).pipe( + metadataFieldsToString(), + take(1)) + .subscribe((fieldNames: string[]) => { + this.setInputSuggestions(fieldNames); + }) } else { this.metadataFieldSuggestions.next([]); } @@ -161,13 +139,12 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges { * Set the list of input suggestion with the given Metadata fields, which all require a resolved MetadataSchema * @param fields list of Metadata fields, which all require a resolved MetadataSchema */ - setInputSuggestions(fields: MetadataField[]) { + setInputSuggestions(fields: string[]) { this.metadataFieldSuggestions.next( - fields.map((field: MetadataField) => { - const fieldNameWhole = field.schemaResolved.prefix + '.' + field.toString(); + fields.map((fieldName: string) => { return { - displayValue: fieldNameWhole.split('.').join('.​'), - value: fieldNameWhole + displayValue: fieldName.split('.').join('.​'), + value: fieldName }; }) ); diff --git a/src/app/core/metadata/metadata-field.model.ts b/src/app/core/metadata/metadata-field.model.ts index 41dfa784a4e..a24d1864ac5 100644 --- a/src/app/core/metadata/metadata-field.model.ts +++ b/src/app/core/metadata/metadata-field.model.ts @@ -70,8 +70,6 @@ export class MetadataField extends ListableObject implements HALResource { @link(METADATA_SCHEMA) schema?: Observable>; - schemaResolved?: MetadataSchema; - /** * Method to print this metadata field as a string without the schema * @param separator The separator between element and qualifier in the string @@ -84,28 +82,6 @@ export class MetadataField extends ListableObject implements HALResource { return key; } - /** - * Method to print this metadata field as a string - * @param separator The separator between the schema, element and qualifier in the string - */ - toStringWithSchema(separator: string = '.', schemaService: MetadataSchemaDataService): Observable { - let schemaObject: Observable> = this.schema; - if (!hasValue(this.schema)) { - schemaObject = schemaService.findByHref(this._links.schema.href); - } - return schemaObject.pipe( - getSucceededRemoteData(), - getRemoteDataPayload(), - map((schemaPayload: MetadataSchema) => { - let key = this.element; - if (isNotEmpty(this.qualifier)) { - key += separator + this.qualifier; - } - return schemaPayload.namespace + separator + key; - }) - ) - } - /** * Method that returns as which type of object this object should be rendered */ diff --git a/src/app/core/shared/operators.ts b/src/app/core/shared/operators.ts index b8120d47656..0afd1d97b1b 100644 --- a/src/app/core/shared/operators.ts +++ b/src/app/core/shared/operators.ts @@ -1,6 +1,6 @@ import { Router, UrlTree } from '@angular/router'; -import { Observable } from 'rxjs'; -import { filter, find, flatMap, map, take, tap } from 'rxjs/operators'; +import { Observable, combineLatest as observableCombineLatest } from 'rxjs'; +import { filter, find, flatMap, map, switchMap, take, tap } from 'rxjs/operators'; import { hasValue, hasValueOperator, isNotEmpty } from '../../shared/empty.util'; import { SearchResult } from '../../shared/search/search-result.model'; import { DSOSuccessResponse, RestResponse } from '../cache/response.models'; @@ -9,6 +9,8 @@ import { RemoteData } from '../data/remote-data'; import { RestRequest } from '../data/request.models'; import { RequestEntry } from '../data/request.reducer'; import { RequestService } from '../data/request.service'; +import { MetadataField } from '../metadata/metadata-field.model'; +import { MetadataSchema } from '../metadata/metadata-schema.model'; import { BrowseDefinition } from './browse-definition.model'; import { DSpaceObject } from './dspace-object.model'; import { getUnauthorizedRoute } from '../../app-routing-paths'; @@ -250,3 +252,27 @@ export const paginatedListToArray = () => hasValueOperator(), map((objectRD: RemoteData>) => objectRD.payload.page.filter((object: T) => hasValue(object))) ); + +/** + * Operator for turning a list of metadata fields into an array of string representing their schema.element.qualifier string + */ +export const metadataFieldsToString = () => + (source: Observable>>): Observable => + source.pipe( + hasValueOperator(), + map((fieldRD: RemoteData>) => { + return fieldRD.payload.page.filter((object: MetadataField) => hasValue(object)) + }), + switchMap((fields: MetadataField[])=> { + const fieldSchemaArray = fields.map((field: MetadataField) => { + return field.schema.pipe( + getFirstSucceededRemoteDataPayload(), + map((schema: MetadataSchema) => ({ field, schema })) + ); + }); + return observableCombineLatest(fieldSchemaArray); + }), + map((fieldSchemaArray: {field: MetadataField, schema: MetadataSchema}[]): string[] => { + return fieldSchemaArray.map((fieldSchema: {field: MetadataField, schema: MetadataSchema}) => fieldSchema.schema.prefix + '.' + fieldSchema.field.toString()) + }) + ); From 10b314cf2020af63e8cd0b10dc4ed8aab60185d4 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Fri, 28 Aug 2020 18:29:38 +0200 Subject: [PATCH 06/13] fix tests --- .../edit-in-place-field.component.html | 4 +- .../edit-in-place-field.component.spec.ts | 55 +++++++++++++------ 2 files changed, 39 insertions(+), 20 deletions(-) diff --git a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.html b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.html index af23ad05010..43621b8551e 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.html +++ b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.html @@ -45,12 +45,12 @@
- - - -