PR-530 Keep virtual metadata on relationship delete - community feedback

This commit is contained in:
Samuel
2020-01-09 13:42:50 +01:00
parent a434b5706e
commit 4e679ec6db
16 changed files with 586 additions and 432 deletions

View File

@@ -1,16 +1,15 @@
<ng-container *ngVar="(updates$ | async) as updates"> <h5>{{getRelationshipMessageKey() | async | translate}}</h5>
<div *ngIf="updates"> <ng-container *ngVar="updates$ | async as updates">
<h5>{{getRelationshipMessageKey(relationshipLabel) | translate}}</h5> <ng-container *ngIf="updates">
<ng-container *ngVar="(updates | dsObjectValues) as updateValues"> <ng-container *ngVar="updates | dsObjectValues as updateValues">
<div *ngFor="let updateValue of updateValues; trackBy: trackUpdate" <ds-edit-relationship *ngFor="let updateValue of updateValues; trackBy: trackUpdate"
ds-edit-relationship class="relationship-row d-block"
class="relationship-row d-block" [fieldUpdate]="updateValue"
[fieldUpdate]="updateValue || {}" [url]="url"
[url]="url" [editItem]="item"
[editItem]="item" [ngClass]="{'alert alert-danger': updateValue?.changeType === 2}">
[ngClass]="{'alert alert-danger': updateValue.changeType === 2}"> </ds-edit-relationship>
</div> </ng-container>
<ds-loading *ngIf="updateValues.length == 0" message="{{'loading.items' | translate}}"></ds-loading>
</ng-container> </ng-container>
</div> <div *ngIf="!updates">no relationships</div>
</ng-container> </ng-container>

View File

@@ -1,27 +1,26 @@
import { EditRelationshipListComponent } from './edit-relationship-list.component'; import {EditRelationshipListComponent} from './edit-relationship-list.component';
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model'; import {RelationshipType} from '../../../../core/shared/item-relationships/relationship-type.model';
import { ResourceType } from '../../../../core/shared/resource-type'; import {Relationship} from '../../../../core/shared/item-relationships/relationship.model';
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model'; import {of as observableOf} from 'rxjs/internal/observable/of';
import { of as observableOf } from 'rxjs/internal/observable/of'; import {RemoteData} from '../../../../core/data/remote-data';
import { RemoteData } from '../../../../core/data/remote-data'; import {Item} from '../../../../core/shared/item.model';
import { Item } from '../../../../core/shared/item.model'; import {PaginatedList} from '../../../../core/data/paginated-list';
import { PaginatedList } from '../../../../core/data/paginated-list'; import {PageInfo} from '../../../../core/shared/page-info.model';
import { PageInfo } from '../../../../core/shared/page-info.model'; import {FieldChangeType} from '../../../../core/data/object-updates/object-updates.actions';
import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions'; import {SharedModule} from '../../../../shared/shared.module';
import { SharedModule } from '../../../../shared/shared.module'; import {TranslateModule} from '@ngx-translate/core';
import { TranslateModule } from '@ngx-translate/core'; import {ObjectUpdatesService} from '../../../../core/data/object-updates/object-updates.service';
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; import {DebugElement, NO_ERRORS_SCHEMA} from '@angular/core';
import { RelationshipService } from '../../../../core/data/relationship.service'; import {By} from '@angular/platform-browser';
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; import {ItemType} from '../../../../core/shared/item-relationships/item-type.model';
import { By } from '@angular/platform-browser';
let comp: EditRelationshipListComponent; let comp: EditRelationshipListComponent;
let fixture: ComponentFixture<EditRelationshipListComponent>; let fixture: ComponentFixture<EditRelationshipListComponent>;
let de: DebugElement; let de: DebugElement;
let objectUpdatesService; let objectUpdatesService;
let relationshipService; let entityTypeService;
const url = 'http://test-url.com/test-url'; const url = 'http://test-url.com/test-url';
@@ -30,42 +29,65 @@ let author1;
let author2; let author2;
let fieldUpdate1; let fieldUpdate1;
let fieldUpdate2; let fieldUpdate2;
let relationships; let relationship1;
let relationship2;
let relationshipType; let relationshipType;
let entityType;
let relatedEntityType;
describe('EditRelationshipListComponent', () => { describe('EditRelationshipListComponent', () => {
beforeEach(async(() => { beforeEach(async(() => {
relationshipType = Object.assign(new RelationshipType(), { relationshipType = Object.assign(new RelationshipType(), {
id: '1', id: '1',
uuid: '1', uuid: '1',
leftwardType: 'isAuthorOfPublication', leftwardType: 'isAuthorOfPublication',
rightwardType: 'isPublicationOfAuthor' rightwardType: 'isPublicationOfAuthor',
leftType: observableOf(new RemoteData(false, false, true, undefined, entityType)),
rightType: observableOf(new RemoteData(false, false, true, undefined, relatedEntityType)),
}); });
relationships = [ relationship1 = Object.assign(new Relationship(), {
Object.assign(new Relationship(), { self: url + '/2',
self: url + '/2', id: '2',
id: '2', uuid: '2',
uuid: '2', leftId: 'author1',
leftId: 'author1', rightId: 'publication',
rightId: 'publication', leftItem: observableOf(new RemoteData(false, false, true, undefined, item)),
relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType)) rightItem: observableOf(new RemoteData(false, false, true, undefined, author1)),
}), relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType))
Object.assign(new Relationship(), { });
self: url + '/3',
id: '3', relationship2 = Object.assign(new Relationship(), {
uuid: '3', self: url + '/3',
leftId: 'author2', id: '3',
rightId: 'publication', uuid: '3',
relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType)) leftId: 'author2',
}) rightId: 'publication',
]; leftItem: observableOf(new RemoteData(false, false, true, undefined, item)),
rightItem: observableOf(new RemoteData(false, false, true, undefined, author2)),
relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType))
});
item = Object.assign(new Item(), { item = Object.assign(new Item(), {
self: 'fake-item-url/publication', self: 'fake-item-url/publication',
id: 'publication', id: 'publication',
uuid: 'publication', uuid: 'publication',
relationships: observableOf(new RemoteData(false, false, true, undefined, new PaginatedList(new PageInfo(), relationships))) relationships: observableOf(new RemoteData(
false,
false,
true,
undefined,
new PaginatedList(new PageInfo(), [relationship1, relationship2])
))
});
entityType = Object.assign(new ItemType(), {
id: 'entityType',
});
relatedEntityType = Object.assign(new ItemType(), {
id: 'relatedEntityType',
}); });
author1 = Object.assign(new Item(), { author1 = Object.assign(new Item(), {
@@ -88,17 +110,29 @@ describe('EditRelationshipListComponent', () => {
objectUpdatesService = jasmine.createSpyObj('objectUpdatesService', objectUpdatesService = jasmine.createSpyObj('objectUpdatesService',
{ {
getFieldUpdatesExclusive: observableOf({ getFieldUpdates: observableOf({
[author1.uuid]: fieldUpdate1, [author1.uuid]: fieldUpdate1,
[author2.uuid]: fieldUpdate2 [author2.uuid]: fieldUpdate2
}) })
} }
); );
relationshipService = jasmine.createSpyObj('relationshipService', entityTypeService = jasmine.createSpyObj('entityTypeService',
{ {
getRelatedItemsByLabel: observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), [author1, author2]))), getEntityTypeByLabel: observableOf(new RemoteData(
getItemRelationshipsByLabel: observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), relationships))), false,
false,
true,
null,
entityType,
)),
getEntityTypeRelationships: observableOf(new RemoteData(
false,
false,
true,
null,
new PaginatedList(new PageInfo(), [relationshipType]),
)),
} }
); );
@@ -107,7 +141,6 @@ describe('EditRelationshipListComponent', () => {
declarations: [EditRelationshipListComponent], declarations: [EditRelationshipListComponent],
providers: [ providers: [
{ provide: ObjectUpdatesService, useValue: objectUpdatesService }, { provide: ObjectUpdatesService, useValue: objectUpdatesService },
{ provide: RelationshipService, useValue: relationshipService }
], schemas: [ ], schemas: [
NO_ERRORS_SCHEMA NO_ERRORS_SCHEMA
] ]
@@ -120,7 +153,7 @@ describe('EditRelationshipListComponent', () => {
de = fixture.debugElement; de = fixture.debugElement;
comp.item = item; comp.item = item;
comp.url = url; comp.url = url;
comp.relationshipLabel = relationshipType.leftwardType; comp.relationshipType = relationshipType;
fixture.detectChanges(); fixture.detectChanges();
}); });

View File

@@ -1,12 +1,15 @@
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; import {Component, Input, OnInit} from '@angular/core';
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; import {ObjectUpdatesService} from '../../../../core/data/object-updates/object-updates.service';
import { Observable } from 'rxjs/internal/Observable'; import {Observable} from 'rxjs/internal/Observable';
import { FieldUpdate, FieldUpdates } from '../../../../core/data/object-updates/object-updates.reducer'; import {FieldUpdate, FieldUpdates} from '../../../../core/data/object-updates/object-updates.reducer';
import { RelationshipService } from '../../../../core/data/relationship.service'; import {Item} from '../../../../core/shared/item.model';
import { Item } from '../../../../core/shared/item.model'; import {map, switchMap} from 'rxjs/operators';
import { map, switchMap} from 'rxjs/operators'; import {hasValue} from '../../../../shared/empty.util';
import { hasValue } from '../../../../shared/empty.util';
import {Relationship} from '../../../../core/shared/item-relationships/relationship.model'; import {Relationship} from '../../../../core/shared/item-relationships/relationship.model';
import {RelationshipType} from '../../../../core/shared/item-relationships/relationship-type.model';
import {getRemoteDataPayload, getSucceededRemoteData} from '../../../../core/shared/operators';
import {combineLatest as observableCombineLatest, combineLatest} from 'rxjs';
import {ItemType} from '../../../../core/shared/item-relationships/item-type.model';
@Component({ @Component({
selector: 'ds-edit-relationship-list', selector: 'ds-edit-relationship-list',
@@ -17,12 +20,15 @@ import {Relationship} from '../../../../core/shared/item-relationships/relations
* A component creating a list of editable relationships of a certain type * A component creating a list of editable relationships of a certain type
* The relationships are rendered as a list of related items * The relationships are rendered as a list of related items
*/ */
export class EditRelationshipListComponent implements OnInit, OnChanges { export class EditRelationshipListComponent implements OnInit {
/** /**
* The item to display related items for * The item to display related items for
*/ */
@Input() item: Item; @Input() item: Item;
@Input() itemType: ItemType;
/** /**
* The URL to the current page * The URL to the current page
* Used to fetch updates for the current item from the store * Used to fetch updates for the current item from the store
@@ -32,7 +38,7 @@ export class EditRelationshipListComponent implements OnInit, OnChanges {
/** /**
* The label of the relationship-type we're rendering a list for * The label of the relationship-type we're rendering a list for
*/ */
@Input() relationshipLabel: string; @Input() relationshipType: RelationshipType;
/** /**
* The FieldUpdates for the relationships in question * The FieldUpdates for the relationships in question
@@ -41,48 +47,42 @@ export class EditRelationshipListComponent implements OnInit, OnChanges {
constructor( constructor(
protected objectUpdatesService: ObjectUpdatesService, protected objectUpdatesService: ObjectUpdatesService,
protected relationshipService: RelationshipService
) { ) {
} }
ngOnInit(): void {
this.initUpdates();
}
ngOnChanges(changes: SimpleChanges): void {
this.initUpdates();
}
/** /**
* Initialize the FieldUpdates using the related items * Get the i18n message key for this relationship type
*/ */
initUpdates() { public getRelationshipMessageKey(): Observable<string> {
this.updates$ = this.getUpdatesByLabel(this.relationshipLabel);
}
/** return this.getLabel().pipe(
* Get FieldUpdates for the relationships of a specific type map((label) => {
* @param label The relationship type's label if (hasValue(label) && label.indexOf('Of') > -1) {
*/ return `relationships.${label.substring(0, label.indexOf('Of') + 2)}`
public getUpdatesByLabel(label: string): Observable<FieldUpdates> { } else {
return this.relationshipService.getItemRelationshipsByLabel(this.item, label).pipe( return label;
map((relationsRD) => relationsRD.payload.page.map((relationship) => }
Object.assign(new Relationship(), relationship, {uuid: relationship.id}) }),
)),
switchMap((initialFields) => this.objectUpdatesService.getFieldUpdatesExclusive(this.url, initialFields)),
); );
} }
/** /**
* Get the i18n message key for a relationship * Get the relevant label for this relationship type
* @param label The relationship type's label
*/ */
public getRelationshipMessageKey(label: string): string { private getLabel(): Observable<string> {
if (hasValue(label) && label.indexOf('Of') > -1) {
return `relationships.${label.substring(0, label.indexOf('Of') + 2)}` return combineLatest([
} else { this.relationshipType.leftType,
return label; this.relationshipType.rightType,
} ].map((itemTypeRD) => itemTypeRD.pipe(
getSucceededRemoteData(),
getRemoteDataPayload(),
))).pipe(
map((itemTypes) => [
this.relationshipType.leftwardType,
this.relationshipType.rightwardType,
][itemTypes.findIndex((itemType) => itemType.id === this.itemType.id)]),
);
} }
/** /**
@@ -91,4 +91,27 @@ export class EditRelationshipListComponent implements OnInit, OnChanges {
trackUpdate(index, update: FieldUpdate) { trackUpdate(index, update: FieldUpdate) {
return update && update.field ? update.field.uuid : undefined; return update && update.field ? update.field.uuid : undefined;
} }
ngOnInit(): void {
this.updates$ = this.item.relationships.pipe(
map((relationships) => relationships.payload.page.filter((relationship) => relationship)),
switchMap((itemRelationships) =>
observableCombineLatest(
itemRelationships
.map((relationship) => relationship.relationshipType.pipe(
getSucceededRemoteData(),
getRemoteDataPayload(),
))
).pipe(
map((relationshipTypes) => itemRelationships.filter(
(relationship, index) => relationshipTypes[index].id === this.relationshipType.id)
),
map((relationships) => relationships.map((relationship) =>
Object.assign(new Relationship(), relationship, {uuid: relationship.id})
)),
)
),
switchMap((initialFields) => this.objectUpdatesService.getFieldUpdates(this.url, initialFields)),
);
}
} }

View File

@@ -19,7 +19,9 @@
</div> </div>
<ng-template #virtualMetadataModal> <ng-template #virtualMetadataModal>
<ds-virtual-metadata <ds-virtual-metadata
[relationship]="relationship" [relationshipId]="relationship.id"
[leftItem]="leftItem$ | async"
[rightItem]="rightItem$ | async"
[url]="url" [url]="url"
(close)="closeVirtualMetadataModal()" (close)="closeVirtualMetadataModal()"
(save)="remove()" (save)="remove()"

View File

@@ -13,7 +13,7 @@ import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap';
@Component({ @Component({
// tslint:disable-next-line:component-selector // tslint:disable-next-line:component-selector
selector: '[ds-edit-relationship]', selector: 'ds-edit-relationship',
styleUrls: ['./edit-relationship.component.scss'], styleUrls: ['./edit-relationship.component.scss'],
templateUrl: './edit-relationship.component.html', templateUrl: './edit-relationship.component.html',
}) })

View File

@@ -17,8 +17,13 @@
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.save-button" | translate}}</span> <span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.save-button" | translate}}</span>
</button> </button>
</div> </div>
<div *ngFor="let label of relationLabels$ | async" class="mb-4"> <div *ngFor="let relationshipType of relationshipTypes$ | async" class="mb-4">
<ds-edit-relationship-list [item]="item" [url]="url" [relationshipLabel]="label" ></ds-edit-relationship-list> <ds-edit-relationship-list
[url]="url"
[item]="item"
[itemType]="entityType$ | async"
[relationshipType]="relationshipType"
></ds-edit-relationship-list>
</div> </div>
<div class="button-row bottom"> <div class="button-row bottom">
<div class="float-right"> <div class="float-right">

View File

@@ -22,10 +22,12 @@ import { ErrorResponse, RestResponse } from '../../../core/cache/response.models
import { isNotEmptyOperator } from '../../../shared/empty.util'; import { isNotEmptyOperator } from '../../../shared/empty.util';
import { RemoteData } from '../../../core/data/remote-data'; import { RemoteData } from '../../../core/data/remote-data';
import { ObjectCacheService } from '../../../core/cache/object-cache.service'; import { ObjectCacheService } from '../../../core/cache/object-cache.service';
import { getSucceededRemoteData} from '../../../core/shared/operators'; import {getRemoteDataPayload, getSucceededRemoteData} from '../../../core/shared/operators';
import { RequestService } from '../../../core/data/request.service'; import { RequestService } from '../../../core/data/request.service';
import { Subscription } from 'rxjs/internal/Subscription'; import { Subscription } from 'rxjs/internal/Subscription';
import { getRelationsByRelatedItemIds } from '../../simple/item-types/shared/item-relationships-utils'; import {RelationshipType} from '../../../core/shared/item-relationships/relationship-type.model';
import {ItemType} from '../../../core/shared/item-relationships/item-type.model';
import {EntityTypeService} from '../../../core/data/entity-type.service';
@Component({ @Component({
selector: 'ds-item-relationships', selector: 'ds-item-relationships',
@@ -40,13 +42,14 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl
/** /**
* The labels of all different relations within this item * The labels of all different relations within this item
*/ */
relationLabels$: Observable<string[]>; relationshipTypes$: Observable<RelationshipType[]>;
/** /**
* A subscription that checks when the item is deleted in cache and reloads the item by sending a new request * A subscription that checks when the item is deleted in cache and reloads the item by sending a new request
* This is used to update the item in cache after relationships are deleted * This is used to update the item in cache after relationships are deleted
*/ */
itemUpdateSubscription: Subscription; itemUpdateSubscription: Subscription;
entityType$: Observable<ItemType>;
constructor( constructor(
protected itemService: ItemDataService, protected itemService: ItemDataService,
@@ -59,7 +62,7 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl
protected relationshipService: RelationshipService, protected relationshipService: RelationshipService,
protected objectCache: ObjectCacheService, protected objectCache: ObjectCacheService,
protected requestService: RequestService, protected requestService: RequestService,
protected cdRef: ChangeDetectorRef protected entityTypeService: EntityTypeService,
) { ) {
super(itemService, objectUpdatesService, router, notificationsService, translateService, EnvConfig, route); super(itemService, objectUpdatesService, router, notificationsService, translateService, EnvConfig, route);
} }
@@ -69,21 +72,13 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl
*/ */
ngOnInit(): void { ngOnInit(): void {
super.ngOnInit(); super.ngOnInit();
this.relationLabels$ = this.relationshipService.getItemRelationshipLabels(this.item);
this.initializeItemUpdate();
}
/**
* Update the item (and view) when it's removed in the request cache
*/
public initializeItemUpdate(): void {
this.itemUpdateSubscription = this.requestService.hasByHrefObservable(this.item.self).pipe( this.itemUpdateSubscription = this.requestService.hasByHrefObservable(this.item.self).pipe(
filter((exists: boolean) => !exists), filter((exists: boolean) => !exists),
switchMap(() => this.itemService.findById(this.item.uuid)), switchMap(() => this.itemService.findById(this.item.uuid)),
getSucceededRemoteData(), getSucceededRemoteData(),
).subscribe((itemRD: RemoteData<Item>) => { ).subscribe((itemRD: RemoteData<Item>) => {
this.item = itemRD.payload; this.item = itemRD.payload;
this.cdRef.detectChanges(); this.initializeUpdates();
}); });
} }
@@ -91,8 +86,22 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl
* Initialize the values and updates of the current item's relationship fields * Initialize the values and updates of the current item's relationship fields
*/ */
public initializeUpdates(): void { public initializeUpdates(): void {
this.updates$ = this.relationshipService.getRelatedItems(this.item).pipe(
switchMap((items: Item[]) => this.objectUpdatesService.getFieldUpdates(this.url, items)) this.entityType$ = this.entityTypeService.getEntityTypeByLabel(
this.item.firstMetadataValue('relationship.type')
).pipe(
getSucceededRemoteData(),
getRemoteDataPayload(),
);
this.relationshipTypes$ = this.entityType$.pipe(
switchMap((entityType) =>
this.entityTypeService.getEntityTypeRelationships(entityType.id).pipe(
getSucceededRemoteData(),
getRemoteDataPayload(),
map((relationshipTypes) => relationshipTypes.page),
)
),
); );
} }
@@ -142,8 +151,9 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl
)) ))
), ),
).subscribe((responses: RestResponse[]) => { ).subscribe((responses: RestResponse[]) => {
this.displayNotifications(responses); this.itemUpdateSubscription.add(() => {
this.reset(); this.displayNotifications(responses);
});
}); });
} }
@@ -165,22 +175,12 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl
} }
} }
/**
* Re-initialize fields and subscriptions
*/
reset() {
this.initializeOriginalFields();
this.initializeUpdates();
this.initializeItemUpdate();
}
/** /**
* Sends all initial values of this item to the object updates service * Sends all initial values of this item to the object updates service
*/ */
public initializeOriginalFields() { public initializeOriginalFields() {
this.relationshipService.getRelatedItems(this.item).pipe(take(1)).subscribe((items: Item[]) => { const initialFields = [];
this.objectUpdatesService.initialize(this.url, items, this.item.lastModified); this.objectUpdatesService.initialize(this.url, initialFields, this.item.lastModified);
});
} }
/** /**
@@ -189,5 +189,4 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl
ngOnDestroy(): void { ngOnDestroy(): void {
this.itemUpdateSubscription.unsubscribe(); this.itemUpdateSubscription.unsubscribe();
} }
} }

View File

@@ -5,37 +5,33 @@
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div *ngFor="let item$ of [leftItem$, rightItem$]"> <ng-container *ngFor="let item of items; trackBy: trackItem">
<div *ngVar="item$ | async as item"> <div *ngVar="(isSelectedVirtualMetadataItem(item) | async) as selected"
<div *ngVar="(isSelectedVirtualMetadataItem(item) | async) as selected" (click)="setSelectedVirtualMetadataItem(item, !selected)"
(click)="setSelectedVirtualMetadataItem(item, !selected)" class="d-flex flex-row">
class="d-flex flex-row"> <div class="m-2">
<div class="m-2"> <label>
<label> <input class="select" type="checkbox">
<input type="checkbox" [checked]="selected"> </label>
</label> </div>
</div> <div class="flex-column">
<div class="flex-column"> <ds-listable-object-component-loader [object]="item">
<ds-listable-object-component-loader </ds-listable-object-component-loader>
[object]="item$ | async"></ds-listable-object-component-loader> <div *ngFor="let metadata of virtualMetadata.get(item.uuid)">
<div *ngFor="let metadata of getVirtualMetadata(relationship, item$ | async)"> <div class="font-weight-bold">
<div> {{metadata.metadataField}}
<div class="font-weight-bold"> </div>
{{metadata.metadataField}} <div>
</div> {{metadata.metadataValue.value}}
<div>
{{metadata.metadataValue.value}}
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </ng-container>
<div class="d-flex flex-row-reverse m-2"> <div class="d-flex flex-row-reverse m-2">
<button class="btn btn-primary" <button class="btn btn-primary save"
(click)="save.emit()"><i (click)="save.emit()">
class="fas fa-save"></i> {{"item.edit.metadata.save-button" | translate}} <i class="fas fa-save"></i> {{"item.edit.metadata.save-button" | translate}}
</button> </button>
</div> </div>
</div> </div>

View File

@@ -1,172 +1,102 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import { Item } from '../../../core/shared/item.model'; import {of as observableOf} from 'rxjs/internal/observable/of';
import { RouterStub } from '../../../shared/testing/router-stub'; import {TranslateModule} from '@ngx-translate/core';
import { CommonModule } from '@angular/common'; import {DebugElement, NO_ERRORS_SCHEMA} from '@angular/core';
import { RouterTestingModule } from '@angular/router/testing'; import {By} from '@angular/platform-browser';
import { TranslateModule } from '@ngx-translate/core'; import {VirtualMetadataComponent} from './virtual-metadata.component';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import {Item} from '../../../core/shared/item.model';
import { ActivatedRoute, Router } from '@angular/router'; import {ObjectUpdatesService} from '../../../core/data/object-updates/object-updates.service';
import { VirtualMetadataComponent } from './virtual-metadata.component'; import {VarDirective} from '../../../shared/utils/var.directive';
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { SearchService } from '../../../+search-page/search-service/search.service';
import { of as observableOf } from 'rxjs';
import { FormsModule } from '@angular/forms';
import { ItemDataService } from '../../../core/data/item-data.service';
import { RemoteData } from '../../../core/data/remote-data';
import { PaginatedList } from '../../../core/data/paginated-list';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { RestResponse } from '../../../core/cache/response.models';
import { Collection } from '../../../core/shared/collection.model';
// describe('ItemMoveComponent', () => { fdescribe('VirtualMetadataComponent', () => {
// let comp: VirtualMetadataComponent;
// let fixture: ComponentFixture<VirtualMetadataComponent>; let comp: VirtualMetadataComponent;
// let fixture: ComponentFixture<VirtualMetadataComponent>;
// const mockItem = Object.assign(new Item(), { let de: DebugElement;
// id: 'fake-id',
// handle: 'fake/handle', let objectUpdatesService;
// lastModified: '2018'
// }); const url = 'http://test-url.com/test-url';
//
// const itemPageUrl = `fake-url/${mockItem.id}`; let item;
// const routerStub = Object.assign(new RouterStub(), { let relatedItem;
// url: `${itemPageUrl}/edit` let relationshipId;
// });
// beforeEach(() => {
// const mockItemDataService = jasmine.createSpyObj({
// moveToCollection: observableOf(new RestResponse(true, 200, 'Success')) relationshipId = 'relationship id';
// });
// item = Object.assign(new Item(), {
// const mockItemDataServiceFail = jasmine.createSpyObj({ uuid: 'publication',
// moveToCollection: observableOf(new RestResponse(false, 500, 'Internal server error')) metadata: [],
// }); });
//
// const routeStub = { relatedItem = Object.assign(new Item(), {
// data: observableOf({ uuid: 'relatedItem',
// item: new RemoteData(false, false, true, null, { metadata: [],
// id: 'item1' });
// })
// }) objectUpdatesService = jasmine.createSpyObj('objectUpdatesService', {
// }; isSelectedVirtualMetadata: observableOf(false),
// setSelectedVirtualMetadata: null,
// const collection1 = Object.assign(new Collection(),{ });
// uuid: 'collection-uuid-1',
// name: 'Test collection 1', TestBed.configureTestingModule({
// self: 'self-link-1', imports: [TranslateModule.forRoot()],
// }); declarations: [VirtualMetadataComponent, VarDirective],
// providers: [
// const collection2 = Object.assign(new Collection(),{ {provide: ObjectUpdatesService, useValue: objectUpdatesService},
// uuid: 'collection-uuid-2', ], schemas: [
// name: 'Test collection 2', NO_ERRORS_SCHEMA
// self: 'self-link-2', ]
// }); }).compileComponents();
// fixture = TestBed.createComponent(VirtualMetadataComponent);
// const mockSearchService = { comp = fixture.componentInstance;
// search: () => { de = fixture.debugElement;
// return observableOf(new RemoteData(false, false, true, null,
// new PaginatedList(null, [ comp.url = url;
// { comp.leftItem = item;
// indexableObject: collection1, comp.rightItem = relatedItem;
// hitHighlights: {} comp.relationshipId = relationshipId;
// }, {
// indexableObject: collection2, fixture.detectChanges();
// hitHighlights: {} });
// }
// ]))); describe('when clicking the save button', () => {
// } it('should emit a save event', () => {
// };
// spyOn(comp.save, 'emit');
// const notificationsServiceStub = new NotificationsServiceStub(); fixture.debugElement
// .query(By.css('button.save'))
// describe('ItemMoveComponent success', () => { .triggerEventHandler('click', null);
// beforeEach(async(() => { expect(comp.save.emit).toHaveBeenCalled();
// TestBed.configureTestingModule({ });
// imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()], });
// declarations: [VirtualMetadataComponent],
// providers: [ describe('when clicking the close button', () => {
// {provide: ActivatedRoute, useValue: routeStub}, it('should emit a close event', () => {
// {provide: Router, useValue: routerStub},
// {provide: ItemDataService, useValue: mockItemDataService}, spyOn(comp.close, 'emit');
// {provide: NotificationsService, useValue: notificationsServiceStub}, fixture.debugElement
// {provide: SearchService, useValue: mockSearchService}, .query(By.css('button.close'))
// ], schemas: [ .triggerEventHandler('click', null);
// CUSTOM_ELEMENTS_SCHEMA expect(comp.close.emit).toHaveBeenCalled();
// ] });
// }).compileComponents(); });
// }));
// describe('when selecting an item', () => {
// beforeEach(() => { it('should call the updates service setSelectedVirtualMetadata method', () => {
// fixture = TestBed.createComponent(VirtualMetadataComponent); fixture.debugElement
// comp = fixture.componentInstance; .query(By.css('input.select'))
// fixture.detectChanges(); .triggerEventHandler('click', null);
// }); fixture.whenStable().then(() =>
// it('should load suggestions', () => { expect(objectUpdatesService.setSelectedVirtualMetadata).toHaveBeenCalledWith(
// const expected = [ url,
// collection1, relationshipId,
// collection2 item.uuid,
// ]; true
// )
// comp.collectionSearchResults.subscribe((value) => { );
// expect(value).toEqual(expected); });
// } })
// ); });
// });
// it('should get current url ', () => {
// expect(comp.getCurrentUrl()).toEqual('fake-url/fake-id/edit');
// });
// it('should on click select the correct collection name and id', () => {
// const data = collection1;
//
// comp.onClick(data);
//
// expect(comp.selectedCollectionName).toEqual('Test collection 1');
// expect(comp.selectedCollection).toEqual(collection1);
// });
// describe('moveCollection', () => {
// it('should call itemDataService.moveToCollection', () => {
// comp.itemId = 'item-id';
// comp.selectedCollectionName = 'selected-collection-id';
// comp.selectedCollection = collection1;
// comp.moveCollection();
//
// expect(mockItemDataService.moveToCollection).toHaveBeenCalledWith('item-id', collection1);
// });
// it('should call notificationsService success message on success', () => {
// comp.moveCollection();
//
// expect(notificationsServiceStub.success).toHaveBeenCalled();
// });
// });
// });
//
// describe('ItemMoveComponent fail', () => {
// beforeEach(async(() => {
// TestBed.configureTestingModule({
// imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
// declarations: [VirtualMetadataComponent],
// providers: [
// {provide: ActivatedRoute, useValue: routeStub},
// {provide: Router, useValue: routerStub},
// {provide: ItemDataService, useValue: mockItemDataServiceFail},
// {provide: NotificationsService, useValue: notificationsServiceStub},
// {provide: SearchService, useValue: mockSearchService},
// ], schemas: [
// CUSTOM_ELEMENTS_SCHEMA
// ]
// }).compileComponents();
// }));
//
// beforeEach(() => {
// fixture = TestBed.createComponent(VirtualMetadataComponent);
// comp = fixture.componentInstance;
// fixture.detectChanges();
// });
//
// it('should call notificationsService error message on fail', () => {
// comp.moveCollection();
//
// expect(notificationsServiceStub.error).toHaveBeenCalled();
// });
// });
// });

View File

@@ -1,10 +1,7 @@
import {Component, EventEmitter, Input, OnChanges, Output} from '@angular/core'; import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
import {ActivatedRoute} from '@angular/router';
import {Observable} from 'rxjs'; import {Observable} from 'rxjs';
import {Item} from '../../../core/shared/item.model'; import {Item} from '../../../core/shared/item.model';
import {Relationship} from '../../../core/shared/item-relationships/relationship.model';
import {MetadataValue} from '../../../core/shared/metadata.models'; import {MetadataValue} from '../../../core/shared/metadata.models';
import {getRemoteDataPayload, getSucceededRemoteData} from '../../../core/shared/operators';
import {ObjectUpdatesService} from '../../../core/data/object-updates/object-updates.service'; import {ObjectUpdatesService} from '../../../core/data/object-updates/object-updates.service';
@Component({ @Component({
@@ -12,47 +9,67 @@ import {ObjectUpdatesService} from '../../../core/data/object-updates/object-upd
templateUrl: './virtual-metadata.component.html' templateUrl: './virtual-metadata.component.html'
}) })
/** /**
* Component that handles the moving of an item to a different collection * Component that lists both items of a relationship, along with their virtual metadata of the relationship.
* The component is shown when a relationship is marked to be deleted.
* Each item has a checkbox to indicate whether its virtual metadata should be saved as real metadata.
*/ */
export class VirtualMetadataComponent implements OnChanges { export class VirtualMetadataComponent implements OnInit {
/** /**
* The current url of this page * The current url of this page
*/ */
@Input() url: string; @Input() url: string;
@Input() relationship: Relationship; /**
* The id of the relationship to be deleted.
*/
@Input() relationshipId: string;
/**
* The left item of the relationship to be deleted.
*/
@Input() leftItem: Item;
/**
* The right item of the relationship to be deleted.
*/
@Input() rightItem: Item;
/**
* Emits when the close button is pressed.
*/
@Output() close = new EventEmitter(); @Output() close = new EventEmitter();
/**
* Emits when the save button is pressed.
*/
@Output() save = new EventEmitter(); @Output() save = new EventEmitter();
leftItem$: Observable<Item>; /**
rightItem$: Observable<Item>; * Get an array of the left and the right item of the relationship to be deleted.
*/
get items() {
return [this.leftItem, this.rightItem];
}
private virtualMetadata: Map<string, VirtualMetadata[]> = new Map<string, VirtualMetadata[]>();
constructor( constructor(
protected route: ActivatedRoute,
protected objectUpdatesService: ObjectUpdatesService, protected objectUpdatesService: ObjectUpdatesService,
) { ) {
} }
ngOnChanges(): void { /**
this.leftItem$ = this.relationship.leftItem.pipe( * Get the virtual metadata of a given item corresponding to this relationship.
getSucceededRemoteData(), * @param item the item to get the virtual metadata for
getRemoteDataPayload(), */
); getVirtualMetadata(item: Item): VirtualMetadata[] {
this.rightItem$ = this.relationship.rightItem.pipe(
getSucceededRemoteData(),
getRemoteDataPayload(),
);
}
getVirtualMetadata(relationship: Relationship, relatedItem: Item): VirtualMetadata[] { return Object.entries(item.metadata)
return Object.entries(relatedItem.metadata)
.map(([key, value]) => .map(([key, value]) =>
value value
.filter((metadata: MetadataValue) => .filter((metadata: MetadataValue) =>
metadata.authority && metadata.authority.endsWith(relationship.id)) !key.startsWith('relation') && metadata.authority && metadata.authority.endsWith(this.relationshipId))
.map((metadata: MetadataValue) => { .map((metadata: MetadataValue) => {
return { return {
metadataField: key, metadataField: key,
@@ -60,18 +77,43 @@ export class VirtualMetadataComponent implements OnChanges {
} }
}) })
) )
.reduce((previous, current) => previous.concat(current)); .reduce((previous, current) => previous.concat(current), []);
} }
/**
* Select/deselect the virtual metadata of an item to be saved as real metadata.
* @param item the item for which (not) to save the virtual metadata as real metadata
* @param selected whether or not to save the virtual metadata as real metadata
*/
setSelectedVirtualMetadataItem(item: Item, selected: boolean) { setSelectedVirtualMetadataItem(item: Item, selected: boolean) {
this.objectUpdatesService.setSelectedVirtualMetadata(this.url, this.relationship.id, item.uuid, selected); this.objectUpdatesService.setSelectedVirtualMetadata(this.url, this.relationshipId, item.uuid, selected);
} }
/**
* Check whether the virtual metadata of a given item is selected to be saved as real metadata
* @param item the item for which to check whether the virtual metadata is selected to be saved as real metadata
*/
isSelectedVirtualMetadataItem(item: Item): Observable<boolean> { isSelectedVirtualMetadataItem(item: Item): Observable<boolean> {
return this.objectUpdatesService.isSelectedVirtualMetadata(this.url, this.relationship.id, item.uuid); return this.objectUpdatesService.isSelectedVirtualMetadata(this.url, this.relationshipId, item.uuid);
}
/**
* Prevent unnecessary rerendering so fields don't lose focus
*/
trackItem(index, item: Item) {
return item && item.uuid;
}
ngOnInit(): void {
this.items.forEach((item) => {
this.virtualMetadata.set(item.uuid, this.getVirtualMetadata(item));
});
} }
} }
/**
* Represents a virtual metadata entry.
*/
export interface VirtualMetadata { export interface VirtualMetadata {
metadataField: string, metadataField: string,
metadataValue: MetadataValue, metadataValue: MetadataValue,

View File

@@ -121,6 +121,7 @@ import { NormalizedBrowseEntry } from './shared/normalized-browse-entry.model';
import { BrowseDefinition } from './shared/browse-definition.model'; import { BrowseDefinition } from './shared/browse-definition.model';
import { MappedCollectionsReponseParsingService } from './data/mapped-collections-reponse-parsing.service'; import { MappedCollectionsReponseParsingService } from './data/mapped-collections-reponse-parsing.service';
import { ObjectSelectService } from '../shared/object-select/object-select.service'; import { ObjectSelectService } from '../shared/object-select/object-select.service';
import {EntityTypeService} from './data/entity-type.service';
const IMPORTS = [ const IMPORTS = [
CommonModule, CommonModule,
@@ -211,6 +212,7 @@ const PROVIDERS = [
TaskResponseParsingService, TaskResponseParsingService,
ClaimedTaskDataService, ClaimedTaskDataService,
PoolTaskDataService, PoolTaskDataService,
EntityTypeService,
// register AuthInterceptor as HttpInterceptor // register AuthInterceptor as HttpInterceptor
{ {
provide: HTTP_INTERCEPTORS, provide: HTTP_INTERCEPTORS,

View File

@@ -0,0 +1,103 @@
import { DataService } from './data.service';
import { RequestService } from './request.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
import { Store } from '@ngrx/store';
import { CoreState } from '../core.reducers';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { HttpClient } from '@angular/common/http';
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
import { Injectable } from '@angular/core';
import { GetRequest } from './request.models';
import { Observable } from 'rxjs/internal/Observable';
import {switchMap, take, tap} from 'rxjs/operators';
import { RemoteData } from './remote-data';
import {RelationshipType} from '../shared/item-relationships/relationship-type.model';
import {PaginatedList} from './paginated-list';
import {ItemType} from '../shared/item-relationships/item-type.model';
/**
* Service handling all ItemType requests
*/
@Injectable()
export class EntityTypeService extends DataService<ItemType> {
protected linkPath = 'entitytypes';
protected forceBypassCache = false;
constructor(protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected dataBuildService: NormalizedObjectBuildService,
protected store: Store<CoreState>,
protected halService: HALEndpointService,
protected objectCache: ObjectCacheService,
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected comparator: DefaultChangeAnalyzer<ItemType>) {
super();
}
getBrowseEndpoint(options, linkPath?: string): Observable<string> {
return this.halService.getEndpoint(this.linkPath);
}
/**
* Get the endpoint for the item type's allowed relationship types
* @param entityTypeId
*/
getRelationshipTypesEndpoint(entityTypeId: string): Observable<string> {
return this.halService.getEndpoint(this.linkPath).pipe(
switchMap((href) => this.halService.getEndpoint('relationshiptypes', `${href}/${entityTypeId}`))
);
}
/**
* Get the allowed relationship types for an entity type
* @param entityTypeId
*/
getEntityTypeRelationships(entityTypeId: string): Observable<RemoteData<PaginatedList<RelationshipType>>> {
const href$ = this.getRelationshipTypesEndpoint(entityTypeId);
href$.pipe(take(1)).subscribe((href) => {
const request = new GetRequest(this.requestService.generateRequestId(), href);
this.requestService.configure(request);
});
return this.rdbService.buildList(href$);
}
/**
* Get an entity type by their label
* @param label
*/
getEntityTypeByLabel(label: string): Observable<RemoteData<ItemType>> {
// TODO: Remove mock data once REST API supports this
/*
href$.pipe(take(1)).subscribe((href) => {
const request = new GetRequest(this.requestService.generateRequestId(), href);
this.requestService.configure(request);
});
return this.rdbService.buildSingle<EntityType>(href$);
*/
// Mock:
const index = [
'Publication',
'Person',
'Project',
'OrgUnit',
'Journal',
'JournalVolume',
'JournalIssue',
'DataPackage',
'DataFile',
].indexOf(label);
return this.findById((index + 1) + '');
}
}

View File

@@ -84,6 +84,9 @@ export class AddFieldUpdateAction implements Action {
} }
} }
/**
* An ngrx action to select/deselect virtual metadata in the ObjectUpdates state for a certain page url
*/
export class SelectVirtualMetadataAction implements Action { export class SelectVirtualMetadataAction implements Action {
type = ObjectUpdatesActionTypes.SELECT_VIRTUAL_METADATA; type = ObjectUpdatesActionTypes.SELECT_VIRTUAL_METADATA;
@@ -95,12 +98,16 @@ export class SelectVirtualMetadataAction implements Action {
}; };
/** /**
* Create a new AddFieldUpdateAction * Create a new SelectVirtualMetadataAction
* *
* @param url * @param url
* the unique url of the page for which a field update is added * the unique url of the page for which a field update is added
* @param field The identifiable field of which a new update is added * @param source
* @param changeType The update's change type * the id of the relationship which adds the virtual metadata
* @param uuid
* the id of the item which has the virtual metadata
* @param select
* whether to select or deselect the virtual metadata to be saved as real metadata
*/ */
constructor( constructor(
url: string, url: string,

View File

@@ -23,13 +23,9 @@ import {
SetEditableFieldUpdateAction, SetEditableFieldUpdateAction,
SetValidFieldUpdateAction SetValidFieldUpdateAction
} from './object-updates.actions'; } from './object-updates.actions';
import {distinctUntilChanged, filter, map} from 'rxjs/operators'; import {distinctUntilChanged, filter, map, switchMap} from 'rxjs/operators';
import {hasNoValue, hasValue, isEmpty, isNotEmpty} from '../../../shared/empty.util'; import {hasNoValue, hasValue, isEmpty, isNotEmpty} from '../../../shared/empty.util';
import {INotification} from '../../../shared/notifications/models/notification.model'; import {INotification} from '../../../shared/notifications/models/notification.model';
import {Item} from '../../shared/item.model';
import {Relationship} from '../../shared/item-relationships/relationship.model';
import {MetadataValue} from '../../shared/metadata.models';
import {VirtualMetadata} from '../../../+item-page/edit-item-page/virtual-metadata/virtual-metadata.component';
function objectUpdatesStateSelector(): MemoizedSelector<CoreState, ObjectUpdatesState> { function objectUpdatesStateSelector(): MemoizedSelector<CoreState, ObjectUpdatesState> {
return createSelector(coreSelector, (state: CoreState) => state['cache/object-updates']); return createSelector(coreSelector, (state: CoreState) => state['cache/object-updates']);
@@ -101,18 +97,22 @@ export class ObjectUpdatesService {
*/ */
getFieldUpdates(url: string, initialFields: Identifiable[]): Observable<FieldUpdates> { getFieldUpdates(url: string, initialFields: Identifiable[]): Observable<FieldUpdates> {
const objectUpdates = this.getObjectEntry(url); const objectUpdates = this.getObjectEntry(url);
return objectUpdates.pipe(map((objectEntry) => { return objectUpdates.pipe(
const fieldUpdates: FieldUpdates = {}; switchMap((objectEntry) => {
Object.keys(objectEntry.fieldStates).forEach((uuid) => { const fieldUpdates: FieldUpdates = {};
let fieldUpdate = objectEntry.fieldUpdates[uuid]; Object.keys(objectEntry.fieldStates).forEach((uuid) => {
if (isEmpty(fieldUpdate)) { fieldUpdates[uuid] = objectEntry.fieldUpdates[uuid];
const identifiable = initialFields.find((object: Identifiable) => object.uuid === uuid); });
fieldUpdate = { field: identifiable, changeType: undefined }; return this.getFieldUpdatesExclusive(url, initialFields).pipe(
} map((fieldUpdatesExclusive) => {
fieldUpdates[uuid] = fieldUpdate; Object.keys(fieldUpdatesExclusive).forEach((uuid) => {
}); fieldUpdates[uuid] = fieldUpdatesExclusive[uuid];
return fieldUpdates; });
})) return fieldUpdates;
})
);
}),
);
} }
/** /**
@@ -204,6 +204,15 @@ export class ObjectUpdatesService {
saveChangeFieldUpdate(url: string, field: Identifiable) { saveChangeFieldUpdate(url: string, field: Identifiable) {
this.saveFieldUpdate(url, field, FieldChangeType.UPDATE); this.saveFieldUpdate(url, field, FieldChangeType.UPDATE);
} }
/**
* Check whether the virtual metadata of a given item is selected to be saved as real metadata
* @param url The URL of the page on which the field resides
* @param relationship The id of the relationship for which to check whether the virtual metadata is selected to be
* saved as real metadata
* @param item The id of the item for which to check whether the virtual metadata is selected to be
* saved as real metadata
*/
isSelectedVirtualMetadata(url: string, relationship: string, item: string): Observable<boolean> { isSelectedVirtualMetadata(url: string, relationship: string, item: string): Observable<boolean> {
return this.store return this.store

View File

@@ -1,39 +1,40 @@
import { Injectable } from '@angular/core'; import {Injectable} from '@angular/core';
import { RequestService } from './request.service'; import {RequestService} from './request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import {HALEndpointService} from '../shared/hal-endpoint.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import {RemoteDataBuildService} from '../cache/builders/remote-data-build.service';
import { hasValue, hasValueOperator, isNotEmptyOperator } from '../../shared/empty.util'; import {hasValue, hasValueOperator, isNotEmptyOperator} from '../../shared/empty.util';
import { distinctUntilChanged, filter, flatMap, map, switchMap, take, tap } from 'rxjs/operators'; import {distinctUntilChanged, filter, flatMap, map, switchMap, take, tap} from 'rxjs/operators';
import { import {
configureRequest, configureRequest,
filterSuccessfulResponses, getRemoteDataPayload,
getRemoteDataPayload, getResponseFromEntry, getResponseFromEntry,
getSucceededRemoteData getSucceededRemoteData
} from '../shared/operators'; } from '../shared/operators';
import { DeleteRequest, FindAllOptions, RestRequest } from './request.models'; import {DeleteRequest, FindAllOptions, RestRequest} from './request.models';
import { Observable } from 'rxjs/internal/Observable'; import {Observable} from 'rxjs/internal/Observable';
import { RestResponse } from '../cache/response.models'; import {RestResponse} from '../cache/response.models';
import { Item } from '../shared/item.model'; import {Item} from '../shared/item.model';
import { Relationship } from '../shared/item-relationships/relationship.model'; import {Relationship} from '../shared/item-relationships/relationship.model';
import { RelationshipType } from '../shared/item-relationships/relationship-type.model'; import {RelationshipType} from '../shared/item-relationships/relationship-type.model';
import { RemoteData } from './remote-data'; import {RemoteData} from './remote-data';
import { combineLatest as observableCombineLatest } from 'rxjs/internal/observable/combineLatest'; import {combineLatest as observableCombineLatest} from 'rxjs/internal/observable/combineLatest';
import { zip as observableZip } from 'rxjs'; import {zip as observableZip} from 'rxjs';
import { PaginatedList } from './paginated-list'; import {PaginatedList} from './paginated-list';
import { ItemDataService } from './item-data.service'; import {ItemDataService} from './item-data.service';
import { import {
compareArraysUsingIds, filterRelationsByTypeLabel, paginatedRelationsToItems, compareArraysUsingIds,
paginatedRelationsToItems,
relationsToItems relationsToItems
} from '../../+item-page/simple/item-types/shared/item-relationships-utils'; } from '../../+item-page/simple/item-types/shared/item-relationships-utils';
import { ObjectCacheService } from '../cache/object-cache.service'; import {ObjectCacheService} from '../cache/object-cache.service';
import { DataService } from './data.service'; import {DataService} from './data.service';
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; import {NormalizedObjectBuildService} from '../cache/builders/normalized-object-build.service';
import { Store } from '@ngrx/store'; import {Store} from '@ngrx/store';
import { CoreState } from '../core.reducers'; import {CoreState} from '../core.reducers';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import {NotificationsService} from '../../shared/notifications/notifications.service';
import {HttpClient} from '@angular/common/http'; import {HttpClient} from '@angular/common/http';
import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; import {DefaultChangeAnalyzer} from './default-change-analyzer.service';
import { SearchParam } from '../cache/models/search-param.model'; import {SearchParam} from '../cache/models/search-param.model';
/** /**
* The service handling all relationship requests * The service handling all relationship requests
@@ -93,19 +94,12 @@ export class RelationshipService extends DataService<Relationship> {
configureRequest(this.requestService), configureRequest(this.requestService),
switchMap((restRequest: RestRequest) => this.requestService.getByUUID(restRequest.uuid)), switchMap((restRequest: RestRequest) => this.requestService.getByUUID(restRequest.uuid)),
getResponseFromEntry(), getResponseFromEntry(),
tap(() => this.clearRelatedCache(uuid)) take(1),
); switchMap((response) =>
} this.clearRelatedCache(uuid).pipe(
map(() => response),
/** )
* Get a combined observable containing an array of all relationships in an item, as well as an array of the relationships their types ),
* This is used for easier access of a relationship's type because they exist as observables
* @param item
*/
getItemResolvedRelsAndTypes(item: Item): Observable<[Relationship[], RelationshipType[]]> {
return observableCombineLatest(
this.getItemRelationshipsArray(item),
this.getItemRelationshipTypesArray(item)
); );
} }
@@ -252,23 +246,33 @@ export class RelationshipService extends DataService<Relationship> {
} else { } else {
findAllOptions.searchParams = searchParams; findAllOptions.searchParams = searchParams;
} }
return this.searchBy('byLabel', findAllOptions); return this.searchBy('byLabel', findAllOptions).pipe(
tap((relationshipsRD) => relationshipsRD.payload.page.forEach(
(relationship) => console.log('relationship: ' + relationship.id))
),
);
} }
/** /**
* Clear object and request caches of the items related to a relationship (left and right items) * Clear object and request caches of the items related to a relationship (left and right items)
* @param uuid * @param uuid
*/ */
clearRelatedCache(uuid: string) { clearRelatedCache(uuid: string): Observable<void> {
this.findById(uuid).pipe( return this.findById(uuid).pipe(
getSucceededRemoteData(), getSucceededRemoteData(),
flatMap((rd: RemoteData<Relationship>) => observableCombineLatest(rd.payload.leftItem.pipe(getSucceededRemoteData()), rd.payload.rightItem.pipe(getSucceededRemoteData()))), switchMap((rd: RemoteData<Relationship>) =>
take(1) observableCombineLatest(
).subscribe(([leftItem, rightItem]) => { rd.payload.leftItem.pipe(getSucceededRemoteData()),
this.objectCache.remove(leftItem.payload.self); rd.payload.rightItem.pipe(getSucceededRemoteData())
this.objectCache.remove(rightItem.payload.self); )
this.requestService.removeByHrefSubstring(leftItem.payload.self); ),
this.requestService.removeByHrefSubstring(rightItem.payload.self); take(1),
}); map(([leftItem, rightItem]) => {
this.objectCache.remove(leftItem.payload.self);
this.objectCache.remove(rightItem.payload.self);
this.requestService.removeByHrefSubstring(leftItem.payload.self);
this.requestService.removeByHrefSubstring(rightItem.payload.self);
}),
);
} }
} }

View File

@@ -43,8 +43,8 @@ export class HALEndpointService {
); );
} }
public getEndpoint(linkPath: string): Observable<string> { public getEndpoint(linkPath: string, startHref?: string): Observable<string> {
return this.getEndpointAt(this.getRootHref(), ...linkPath.split('/')); return this.getEndpointAt(startHref || this.getRootHref(), ...linkPath.split('/'));
} }
/** /**