From f64eff42f4977a5d5d0c44b2e16a76c043f9a929 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Fri, 3 Apr 2020 17:28:13 +0200 Subject: [PATCH] fix a number of issues with name variants --- src/app/core/data/data.service.ts | 47 ++++++++++----- src/app/core/data/relationship.service.ts | 59 +++++++++---------- .../org-unit-input-suggestions.component.scss | 3 +- ...esult-list-submission-element.component.ts | 2 +- .../person-input-suggestions.component.scss | 1 + ...ynamic-form-control-container.component.ts | 37 +++++++++++- ...xisting-metadata-list-element.component.ts | 1 + .../models/ds-dynamic-row-array-model.ts | 3 + .../relationship.effects.ts | 15 +++-- .../form/builder/form-builder.service.spec.ts | 3 +- .../models/relationship-options.model.ts | 2 +- .../form/builder/parsers/field-parser.ts | 1 + src/app/shared/mocks/mock-form-models.ts | 4 +- 13 files changed, 116 insertions(+), 62 deletions(-) diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index 267cc547ea..9fc9658138 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -44,11 +44,12 @@ import { FindByIDRequest, FindListOptions, FindListRequest, - GetRequest, PatchRequest + GetRequest, PatchRequest, PutRequest } from './request.models'; import { RequestEntry } from './request.reducer'; import { RequestService } from './request.service'; import { RestRequestMethod } from './rest-request-method'; +import { GenericConstructor } from '../shared/generic-constructor'; export abstract class DataService { protected abstract requestService: RequestService; @@ -307,24 +308,23 @@ export abstract class DataService { * Return an observable that emits response from the server */ searchBy(searchMethod: string, options: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { - + const requestId = this.requestService.generateRequestId(); const hrefObs = this.getSearchByHref(searchMethod, options, ...linksToFollow); - return hrefObs.pipe( - find((href: string) => hasValue(href)), - tap((href: string) => { - this.requestService.removeByHrefSubstring(href); - const request = new FindListRequest(this.requestService.generateRequestId(), href, options); - request.responseMsToLive = 10 * 1000; + hrefObs.pipe( + find((href: string) => hasValue(href)) + ).subscribe((href: string) => { + this.requestService.removeByHrefSubstring(href); + const request = new FindListRequest(requestId, href, options); + request.responseMsToLive = 10 * 1000; + this.requestService.configure(request); + }); - this.requestService.configure(request); - } + return this.requestService.getByUUID(requestId).pipe( + find((requestEntry) => hasValue(requestEntry) && requestEntry.completed), + switchMap((requestEntry) => + this.rdbService.buildList(requestEntry.request.href, ...linksToFollow) ), - switchMap((href) => this.requestService.getByHref(href)), - skipWhile((requestEntry) => hasValue(requestEntry) && requestEntry.completed), - switchMap((href) => - this.rdbService.buildList(hrefObs, ...linksToFollow) as Observable>> - ) ); } @@ -353,6 +353,23 @@ export abstract class DataService { ); } + /** + * Send a PUT request for the specified object + * + * @param object The object to send a put request for. + */ + put(object: T): Observable> { + const requestId = this.requestService.generateRequestId(); + const serializedObject = new DSpaceSerializer(object.constructor as GenericConstructor<{}>).serialize(object); + const request = new PutRequest(requestId, object._links.self.href, serializedObject); + this.requestService.configure(request); + + return this.requestService.getByUUID(requestId).pipe( + find((request: RequestEntry) => hasValue(request) && request.completed), + switchMap(() => this.findByHref(object._links.self.href)) + ); + } + /** * Add a new patch to the object cache * The patch is derived from the differences between the given object and its version in the object cache diff --git a/src/app/core/data/relationship.service.ts b/src/app/core/data/relationship.service.ts index 0b8d569ce2..61bd717518 100644 --- a/src/app/core/data/relationship.service.ts +++ b/src/app/core/data/relationship.service.ts @@ -1,11 +1,12 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { MemoizedSelector, select, Store } from '@ngrx/store'; -import { combineLatest as observableCombineLatest } from 'rxjs'; +import { combineLatest as observableCombineLatest, OperatorFunction } from 'rxjs'; import { Observable } from 'rxjs/internal/Observable'; import { distinctUntilChanged, filter, + find, map, startWith, switchMap, @@ -44,7 +45,7 @@ import { configureRequest, getFirstSucceededRemoteDataPayload, getRemoteDataPayload, getResponseFromEntry, - getSucceededRemoteData + getSucceededRemoteData, getPaginatedListPayload } from '../shared/operators'; import { DataService } from './data.service'; import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; @@ -53,6 +54,7 @@ import { PaginatedList } from './paginated-list'; import { RemoteData, RemoteDataState } from './remote-data'; import { DeleteRequest, FindListOptions, PostRequest, RestRequest } from './request.models'; import { RequestService } from './request.service'; +import has = Reflect.has; const relationshipListsStateSelector = (state: AppState) => state.relationshipLists; @@ -167,7 +169,7 @@ export class RelationshipService extends DataService { * Method to remove an item that's part of a relationship from the cache * @param item The item to remove from the cache */ - private refreshRelationshipItemsInCache(item) { + public refreshRelationshipItemsInCache(item) { this.objectCache.remove(item._links.self.href); this.requestService.removeByHrefSubstring(item.uuid); observableCombineLatest( @@ -310,24 +312,29 @@ export class RelationshipService extends DataService { * @param label The rightward or leftward type of the relationship */ getRelationshipByItemsAndLabel(item1: Item, item2: Item, label: string, options?: FindListOptions): Observable { - return this.getItemRelationshipsByLabel(item1, label, options, followLink('relationshipType'), followLink('leftItem'), followLink('rightItem')) - .pipe( + console.log('getRelationshipByItemsAndLabel', item1, item2, label, options); + return this.getItemRelationshipsByLabel( + item1, + label, + options, + followLink('relationshipType'), + followLink('leftItem'), + followLink('rightItem') + ).pipe( getSucceededRemoteData(), - isNotEmptyOperator(), - map((relationshipListRD: RemoteData>) => relationshipListRD.payload.page), - mergeMap((relationships: Relationship[]) => { - return observableCombineLatest(...relationships.map((relationship: Relationship) => { - return observableCombineLatest( - this.isItemMatchWithItemRD(this.itemService.findByHref(relationship._links.leftItem.href), item2), - this.isItemMatchWithItemRD(this.itemService.findByHref(relationship._links.rightItem.href), item2) - ).pipe( - map(([isLeftItem, isRightItem]) => isLeftItem || isRightItem), - map((isMatch) => isMatch ? relationship : undefined) - ); - }) - ) + // the mergemap below will emit all elements of the list as separate events + mergeMap((relationshipListRD: RemoteData>) => relationshipListRD.payload.page), + mergeMap((relationship: Relationship) => { + return observableCombineLatest([ + this.isItemMatchWithItemRD(this.itemService.findByHref(relationship._links.leftItem.href), item2), + this.isItemMatchWithItemRD(this.itemService.findByHref(relationship._links.rightItem.href), item2) + ]).pipe( + map(([isLeftItem, isRightItem]) => isLeftItem || isRightItem), + map((isMatch) => isMatch ? relationship : undefined) + ); }), - map((relationships: Relationship[]) => relationships.find(((relationship) => hasValue(relationship)))) + filter((relationship) => hasValue(relationship)), + take(1) ) } @@ -384,11 +391,8 @@ export class RelationshipService extends DataService { * @param nameVariant The name variant to set for the matching relationship */ public updateNameVariant(item1: Item, item2: Item, relationshipLabel: string, nameVariant: string): Observable> { - let count = 0 - const update$: Observable> = this.getRelationshipByItemsAndLabel(item1, item2, relationshipLabel) + return this.getRelationshipByItemsAndLabel(item1, item2, relationshipLabel) .pipe( - tap((v) => console.log('updateNameVariant after getRelationshipByItemsAndLabel', v)), - filter((relation: Relationship) => hasValue(relation)), switchMap((relation: Relationship) => relation.relationshipType.pipe( getSucceededRemoteData(), @@ -399,7 +403,6 @@ export class RelationshipService extends DataService { ) ), switchMap((relationshipAndType: { relation: Relationship, type: RelationshipType }) => { - console.log('updateNameVariant switchMap', relationshipAndType); const { relation, type } = relationshipAndType; let updatedRelationship; if (relationshipLabel === type.leftwardType) { @@ -409,15 +412,7 @@ export class RelationshipService extends DataService { } return this.update(updatedRelationship); }), - tap((relationshipRD: RemoteData) => { - if (relationshipRD.hasSucceeded && count < 1) { - count++; - this.refreshRelationshipItemsInCache(item1); - this.refreshRelationshipItemsInCache(item2); - } - }) ); - return update$ } /** diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-suggestions/org-unit-input-suggestions.component.scss b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-suggestions/org-unit-input-suggestions.component.scss index 8301e12c5f..20b48c805b 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-suggestions/org-unit-input-suggestions.component.scss +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-suggestions/org-unit-input-suggestions.component.scss @@ -1,6 +1,7 @@ form { z-index: 1; &:before { + pointer-events: none; // prevent the icon from ‘catching‘ the click position: absolute; font-weight: 900; font-family: "Font Awesome 5 Free"; @@ -15,4 +16,4 @@ form { input.suggestion_input { background: transparent; } -} \ No newline at end of file +} diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.ts b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.ts index 42f8d72c70..fa27e62a6b 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.ts +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.ts @@ -55,7 +55,7 @@ export class PersonSearchResultListSubmissionElementComponent extends SearchResu this.relationshipService.getNameVariant(this.listID, this.dso.uuid) .pipe(take(1)) .subscribe((nameVariant: string) => { - this.selectedName = nameVariant || defaultValue; + this.selectedName = nameVariant || defaultValue; } ); } diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-suggestions/person-input-suggestions.component.scss b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-suggestions/person-input-suggestions.component.scss index d55951dd91..86233c473a 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-suggestions/person-input-suggestions.component.scss +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-suggestions/person-input-suggestions.component.scss @@ -1,5 +1,6 @@ form { &:before { + pointer-events: none; // prevent the icon from ‘catching‘ the click position: absolute; font-weight: 900; font-family: "Font Awesome 5 Free"; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts index 24cbc488b6..e72af14091 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts @@ -83,7 +83,12 @@ import { SelectableListService } from '../../../object-list/selectable-list/sele import { DsDynamicDisabledComponent } from './models/disabled/dynamic-disabled.component'; import { DYNAMIC_FORM_CONTROL_TYPE_DISABLED } from './models/disabled/dynamic-disabled.model'; import { DsDynamicLookupRelationModalComponent } from './relation-lookup-modal/dynamic-lookup-relation-modal.component'; -import { getAllSucceededRemoteData, getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators'; +import { + getAllSucceededRemoteData, + getRemoteDataPayload, + getSucceededRemoteData, + getAllSucceededRemoteDataPayload, getPaginatedListPayload, getFirstSucceededRemoteDataPayload +} from '../../../../core/shared/operators'; import { RemoteData } from '../../../../core/data/remote-data'; import { Item } from '../../../../core/shared/item.model'; import { ItemDataService } from '../../../../core/data/item-data.service'; @@ -95,11 +100,13 @@ import { PaginatedList } from '../../../../core/data/paginated-list'; import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model'; import { Relationship } from '../../../../core/shared/item-relationships/relationship.model'; import { Collection } from '../../../../core/shared/collection.model'; -import { MetadataValue } from '../../../../core/shared/metadata.models'; +import { MetadataValue, VIRTUAL_METADATA_PREFIX } from '../../../../core/shared/metadata.models'; import { FormService } from '../../form.service'; import { SelectableListState } from '../../../object-list/selectable-list/selectable-list.reducer'; import { SubmissionService } from '../../../../submission/submission.service'; import { followLink } from '../../../utils/follow-link-config.model'; +import { paginatedRelationsToItems } from '../../../../+item-page/simple/item-types/shared/item-relationships-utils'; +import { RelationshipOptions } from '../models/relationship-options.model'; export function dsDynamicFormControlMapFn(model: DynamicFormControlModel): Type | null { switch (model.type) { @@ -261,13 +268,37 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo } } if (this.model.relationshipConfig) { + const relationshipOptions = Object.assign(new RelationshipOptions(), this.model.relationshipConfig); this.listId = 'list-' + this.model.relationshipConfig.relationshipType; this.setItem(); const subscription = this.selectableListService.getSelectableList(this.listId).pipe( find((list: SelectableListState) => hasNoValue(list)), switchMap(() => this.item$.pipe(take(1))), switchMap((item) => { - return this.relationshipService.getRelatedItemsByLabel(item, this.model.relationshipConfig.relationshipType).pipe( + const relationshipsRD$ = this.relationshipService.getItemRelationshipsByLabel(item, + this.model.relationshipConfig.relationshipType, + undefined, + followLink('leftItem'), + followLink('rightItem'), + followLink('relationshipType') + ); + + relationshipsRD$.pipe( + getFirstSucceededRemoteDataPayload(), + getPaginatedListPayload() + ).subscribe((relationships: Relationship[]) => { + // set initial namevariants for pre-existing relationships + relationships.forEach((relationship: Relationship) => { + const relationshipMD: MetadataValue = item.firstMetadata(relationshipOptions.metadataField, { authority: `${VIRTUAL_METADATA_PREFIX}${relationship.id}` }); + const nameVariantMD: MetadataValue = item.firstMetadata(this.model.metadataFields, { authority: `${VIRTUAL_METADATA_PREFIX}${relationship.id}` }); + if (hasValue(relationshipMD) && isNotEmpty(relationshipMD.value) && hasValue(nameVariantMD) && isNotEmpty(nameVariantMD.value)) { + this.relationshipService.setNameVariant(this.listId, relationshipMD.value, nameVariantMD.value); + } + }); + }); + + return relationshipsRD$.pipe( + paginatedRelationsToItems(item.uuid), getSucceededRemoteData(), map((items: RemoteData>) => items.payload.page.map((i) => Object.assign(new ItemSearchResult(), { indexableObject: i }))), ) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts index c0d3e5ea3d..e29dc4e3f1 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts @@ -208,6 +208,7 @@ export class ExistingMetadataListElementComponent implements OnInit, OnChanges, const relationMD: MetadataValue = this.submissionItem.firstMetadata(this.relationshipOptions.metadataField, { value: this.relatedItem.uuid }); if (hasValue(relationMD)) { const metadataRepresentationMD: MetadataValue = this.submissionItem.firstMetadata(this.metadataFields, { authority: relationMD.authority }); + const nextValue = Object.assign( new ItemMetadataRepresentation(metadataRepresentationMD), this.relatedItem diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-array-model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-array-model.ts index 3c547fc142..bacedfb087 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-array-model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-array-model.ts @@ -7,6 +7,7 @@ export interface DynamicRowArrayModelConfig extends DynamicFormArrayModelConfig submissionId: string; relationshipConfig: RelationshipOptions; metadataKey: string; + metadataFields: string[]; } export class DynamicRowArrayModel extends DynamicFormArrayModel { @@ -15,6 +16,7 @@ export class DynamicRowArrayModel extends DynamicFormArrayModel { @serializable() submissionId: string; @serializable() relationshipConfig: RelationshipOptions; @serializable() metadataKey: string; + @serializable() metadataFields: string[]; isRowArray = true; constructor(config: DynamicRowArrayModelConfig, layout?: DynamicFormControlLayout) { @@ -24,5 +26,6 @@ export class DynamicRowArrayModel extends DynamicFormArrayModel { this.submissionId = config.submissionId; this.relationshipConfig = config.relationshipConfig; this.metadataKey = config.metadataKey; + this.metadataFields = config.metadataFields; } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.ts index d6f8103814..3ec505450d 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.ts @@ -20,6 +20,7 @@ import { RequestService } from '../../../../../core/data/request.service'; import { ServerSyncBufferActionTypes } from '../../../../../core/cache/server-sync-buffer.actions'; import { CommitPatchOperationsAction, JsonPatchOperationsActionTypes, PatchOperationsActions } from '../../../../../core/json-patch/json-patch-operations.actions'; import { followLink } from '../../../../utils/follow-link-config.model'; +import { RemoteData } from '../../../../../core/data/remote-data'; const DEBOUNCE_TIME = 500; @@ -101,10 +102,14 @@ export class RelationshipEffects { if (inProgress) { this.nameVariantUpdates[identifier] = nameVariant; } else { - this.relationshipService.updateNameVariant(item1, item2, relationshipType, nameVariant).pipe(take(1)) - .subscribe(() => { - this.updateAfterPatchSubmissionId = submissionId; - }); + this.relationshipService.updateNameVariant(item1, item2, relationshipType, nameVariant).pipe( + filter((relationshipRD: RemoteData) => hasValue(relationshipRD.payload)), + take(1) + ).subscribe(() => { + this.updateAfterPatchSubmissionId = submissionId; + this.relationshipService.refreshRelationshipItemsInCache(item1); + this.relationshipService.refreshRelationshipItemsInCache(item2); + }); } } ) @@ -161,8 +166,6 @@ export class RelationshipEffects { private removeRelationship(item1: Item, item2: Item, relationshipType: string, submissionId: string) { this.relationshipService.getRelationshipByItemsAndLabel(item1, item2, relationshipType).pipe( - take(1), - hasValueOperator(), mergeMap((relationship: Relationship) => this.relationshipService.deleteRelationship(relationship.id, 'none')), take(1), switchMap(() => this.refreshWorkspaceItemInCache(submissionId)), diff --git a/src/app/shared/form/builder/form-builder.service.spec.ts b/src/app/shared/form/builder/form-builder.service.spec.ts index 77378437aa..1e6ac85625 100644 --- a/src/app/shared/form/builder/form-builder.service.spec.ts +++ b/src/app/shared/form/builder/form-builder.service.spec.ts @@ -268,7 +268,8 @@ describe('FormBuilderService test suite', () => { ]; }, required: false, - metadataKey: 'dc.contributor.author' + metadataKey: 'dc.contributor.author', + metadataFields: ['dc.contributor.author'] }, ), ]; diff --git a/src/app/shared/form/builder/models/relationship-options.model.ts b/src/app/shared/form/builder/models/relationship-options.model.ts index f1d3d0ae7a..f062ef3102 100644 --- a/src/app/shared/form/builder/models/relationship-options.model.ts +++ b/src/app/shared/form/builder/models/relationship-options.model.ts @@ -1,4 +1,4 @@ -const RELATION_METADATA_PREFIX = 'relation.' +const RELATION_METADATA_PREFIX = 'relation.'; /** * The submission options for fields that can represent relationships diff --git a/src/app/shared/form/builder/parsers/field-parser.ts b/src/app/shared/form/builder/parsers/field-parser.ts index f8b22cc6d1..91583d70cd 100644 --- a/src/app/shared/form/builder/parsers/field-parser.ts +++ b/src/app/shared/form/builder/parsers/field-parser.ts @@ -55,6 +55,7 @@ export abstract class FieldParser { required: JSON.parse( this.configData.mandatory), submissionId: this.submissionId, metadataKey, + metadataFields: this.getAllFieldIds(), groupFactory: () => { let model; let isFirstModelInArray = true; diff --git a/src/app/shared/mocks/mock-form-models.ts b/src/app/shared/mocks/mock-form-models.ts index e466ed1a67..3106ba5492 100644 --- a/src/app/shared/mocks/mock-form-models.ts +++ b/src/app/shared/mocks/mock-form-models.ts @@ -10,7 +10,6 @@ import { AuthorityValue } from '../../core/integration/models/authority.value'; import { FormFieldMetadataValueObject } from '../form/builder/models/form-field-metadata-value.model'; import { DynamicRowGroupModel } from '../form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-group-model'; import { FormRowModel } from '../../core/config/models/config-submission-form.model'; -import { WorkspaceItem } from '../../core/submission/models/workspaceitem.model'; export const qualdropSelectConfig = { name: 'dc.identifier_QUALDROP_METADATA', @@ -82,7 +81,8 @@ const rowArrayQualdropConfig = { }, required: false, submissionId: '1234', - metadataKey: 'dc.some.key' + metadataKey: 'dc.some.key', + metadataFields: ['dc.some.key'] } as DynamicRowArrayModelConfig; export const MockRowArrayQualdropModel: DynamicRowArrayModel = new DynamicRowArrayModel(rowArrayQualdropConfig);