93746: Edit metadata redesign - Virtual metadata

This commit is contained in:
Kristof De Langhe
2022-12-13 14:38:53 +01:00
parent 454bfd2f9f
commit 50f7211947
8 changed files with 130 additions and 57 deletions

View File

@@ -1,7 +1,7 @@
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Inject, Injectable } from '@angular/core';
import { MemoizedSelector, select, Store } from '@ngrx/store';
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
import { distinctUntilChanged, filter, map, mergeMap, startWith, switchMap, take, tap } from 'rxjs/operators';
import {
compareArraysUsingIds, PAGINATED_RELATIONS_TO_ITEMS_OPERATOR,
@@ -44,6 +44,11 @@ import { DeleteRequest, FindListOptions, PostRequest, RestRequest } from './requ
import { RequestService } from './request.service';
import { RequestEntryState } from './request.reducer';
import { NoContent } from '../shared/NoContent.model';
import { MetadataValue } from '../shared/metadata.models';
import { MetadataRepresentation } from '../shared/metadata-representation/metadata-representation.model';
import { MetadatumRepresentation } from '../shared/metadata-representation/metadatum/metadatum-representation.model';
import { ItemMetadataRepresentation } from '../shared/metadata-representation/item/item-metadata-representation.model';
import { DSpaceObject } from '../shared/dspace-object.model';
const relationshipListsStateSelector = (state: AppState) => state.relationshipLists;
@@ -523,4 +528,34 @@ export class RelationshipService extends DataService<Relationship> {
}) as Observable<RemoteData<PaginatedList<Relationship>>>;
}
/**
* Resolve a {@link MetadataValue} into a {@link MetadataRepresentation} of the correct type
* @param metadatum {@link MetadataValue} to resolve
* @param parentItem Parent dspace object the metadata value belongs to
* @param itemType The type of item this metadata value represents (will only be used when no related item can be found, as a fallback)
*/
resolveMetadataRepresentation(metadatum: MetadataValue, parentItem: DSpaceObject, itemType: string): Observable<MetadataRepresentation> {
if (metadatum.isVirtual) {
return this.findById(metadatum.virtualValue, true, false, followLink('leftItem'), followLink('rightItem')).pipe(
getFirstSucceededRemoteData(),
switchMap((relRD: RemoteData<Relationship>) =>
observableCombineLatest(relRD.payload.leftItem, relRD.payload.rightItem).pipe(
filter(([leftItem, rightItem]) => leftItem.hasCompleted && rightItem.hasCompleted),
map(([leftItem, rightItem]) => {
if (!leftItem.hasSucceeded || !rightItem.hasSucceeded) {
return observableOf(Object.assign(new MetadatumRepresentation(itemType), metadatum));
} else if (rightItem.hasSucceeded && leftItem.payload.id === parentItem.id) {
return rightItem.payload;
} else if (rightItem.payload.id === parentItem.id) {
return leftItem.payload;
}
}),
map((item: Item) => Object.assign(new ItemMetadataRepresentation(metadatum), item))
)
));
} else {
return observableOf(Object.assign(new MetadatumRepresentation(itemType), metadatum));
}
}
}

View File

@@ -1,5 +1,6 @@
<div class="flex-grow-1 ds-drop-list">
<div class="flex-grow-1 ds-drop-list h-100">
<ds-dso-edit-metadata-value *ngFor="let mdValue of form.fields[mdField]; let idx = index"
[dso]="dso"
[mdValue]="mdValue"
[dsoType]="dsoType"
[saving$]="saving$"

View File

@@ -1,6 +1,7 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { DsoEditMetadataChangeType, DsoEditMetadataForm } from '../dso-edit-metadata-form';
import { Observable } from 'rxjs/internal/Observable';
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
@Component({
selector: 'ds-dso-edit-metadata-field-values',
@@ -11,6 +12,11 @@ import { Observable } from 'rxjs/internal/Observable';
* Component displaying table rows for each value for a certain metadata field within a form
*/
export class DsoEditMetadataFieldValuesComponent {
/**
* The parent {@link DSpaceObject} to display a metadata form for
* Also used to determine metadata-representations in case of virtual metadata
*/
@Input() dso: DSpaceObject;
/**
* A dynamic form object containing all information about the metadata and the changes made to them, see {@link DsoEditMetadataForm}
*/

View File

@@ -1,9 +1,13 @@
<div class="d-flex flex-row ds-value-row"
<div class="d-flex flex-row ds-value-row" *ngVar="mdValue.newValue.isVirtual as isVirtual"
[ngClass]="{ 'ds-warning': mdValue.change === DsoEditMetadataChangeTypeEnum.UPDATE, 'ds-danger': mdValue.change === DsoEditMetadataChangeTypeEnum.REMOVE, 'ds-success': mdValue.change === DsoEditMetadataChangeTypeEnum.ADD, 'h-100': isOnlyValue }">
<div class="flex-grow-1 ds-flex-cell ds-value-cell d-flex align-items-center">
<div class="dont-break-out preserve-line-breaks" *ngIf="!mdValue.editing">{{ mdValue.newValue.value }}</div>
<textarea class="form-control" rows="2" *ngIf="mdValue.editing" [(ngModel)]="mdValue.newValue.value"
<div class="flex-grow-1 ds-flex-cell ds-value-cell d-flex align-items-center" *ngVar="(mdRepresentation$ | async) as mdRepresentation">
<div class="dont-break-out preserve-line-breaks" *ngIf="!mdValue.editing && !mdRepresentation">{{ mdValue.newValue.value }}</div>
<textarea class="form-control" rows="2" *ngIf="mdValue.editing && !mdRepresentation" [(ngModel)]="mdValue.newValue.value"
[dsDebounce]="300" (onDebounce)="confirm.emit(false)"></textarea>
<div class="d-flex" *ngIf="mdRepresentation">
<a class="mr-2" target="_blank" [routerLink]="mdRepresentationItemRoute$ | async">{{ mdRepresentationName$ | async }}</a>
<ds-type-badge [object]="mdRepresentation"></ds-type-badge>
</div>
</div>
<div class="ds-flex-cell ds-lang-cell">
<div class="dont-break-out preserve-line-breaks" *ngIf="!mdValue.editing">{{ mdValue.newValue.language }}</div>
@@ -12,22 +16,24 @@
</div>
<div class="text-center ds-flex-cell ds-edit-cell">
<div class="btn-group edit-field">
<button class="btn btn-outline-primary btn-sm ng-star-inserted" ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.edit' | translate }}" *ngIf="!mdValue.editing"
[disabled]="mdValue.change === DsoEditMetadataChangeTypeEnum.REMOVE || (saving$ | async)" (click)="edit.emit()">
<i class="fas fa-edit fa-fw"></i>
</button>
<button class="btn btn-outline-success btn-sm ng-star-inserted" ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.confirm' | translate }}" *ngIf="mdValue.editing"
[disabled]="(saving$ | async)" (click)="confirm.emit(true)">
<i class="fas fa-check fa-fw"></i>
</button>
<button class="btn btn-outline-danger btn-sm" ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.remove' | translate }}"
[disabled]="mdValue.change || mdValue.editing || (saving$ | async)" (click)="remove.emit()">
<i class="fas fa-trash-alt fa-fw"></i>
</button>
<button class="btn btn-outline-warning btn-sm" ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.undo' | translate }}"
[disabled]="(!mdValue.change && !mdValue.editing) || (saving$ | async)" (click)="undo.emit()">
<i class="fas fa-undo-alt fa-fw"></i>
</button>
<div class="btn-group" [ngbTooltip]="isVirtual ? (dsoType + '.edit.metadata.edit.buttons.virtual' | translate) : null">
<button class="btn btn-outline-primary btn-sm ng-star-inserted" ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.edit' | translate }}" *ngIf="!mdValue.editing"
[disabled]="isVirtual || mdValue.change === DsoEditMetadataChangeTypeEnum.REMOVE || (saving$ | async)" (click)="edit.emit()">
<i class="fas fa-edit fa-fw"></i>
</button>
<button class="btn btn-outline-success btn-sm ng-star-inserted" ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.confirm' | translate }}" *ngIf="mdValue.editing"
[disabled]="isVirtual || (saving$ | async)" (click)="confirm.emit(true)">
<i class="fas fa-check fa-fw"></i>
</button>
<button class="btn btn-outline-danger btn-sm" ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.remove' | translate }}"
[disabled]="isVirtual || mdValue.change || mdValue.editing || (saving$ | async)" (click)="remove.emit()">
<i class="fas fa-trash-alt fa-fw"></i>
</button>
<button class="btn btn-outline-warning btn-sm" ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.undo' | translate }}"
[disabled]="isVirtual || (!mdValue.change && !mdValue.editing) || (saving$ | async)" (click)="undo.emit()">
<i class="fas fa-undo-alt fa-fw"></i>
</button>
</div>
<!-- TODO: Enable drag -->
<button class="btn btn-outline-secondary ds-drag-handle btn-sm"
[ngClass]="{'disabled': isOnlyValue || (saving$ | async)}" ngbTooltip="{{ dsoType + '.edit.metadata.edit.buttons.drag' | translate }}" [disabled]="isOnlyValue || (saving$ | async)">

View File

@@ -1,6 +1,14 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { DsoEditMetadataChangeType, DsoEditMetadataValue } from '../dso-edit-metadata-form';
import { Observable } from 'rxjs/internal/Observable';
import { MetadataRepresentationType } from '../../../core/shared/metadata-representation/metadata-representation.model';
import { RelationshipService } from '../../../core/data/relationship.service';
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { of } from 'rxjs/internal/observable/of';
import { ItemMetadataRepresentation } from '../../../core/shared/metadata-representation/item/item-metadata-representation.model';
import { map } from 'rxjs/operators';
import { getItemPageRoute } from '../../../item-page/item-page-routing-paths';
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
@Component({
selector: 'ds-dso-edit-metadata-value',
@@ -10,7 +18,13 @@ import { Observable } from 'rxjs/internal/Observable';
/**
* Component displaying a single editable row for a metadata value
*/
export class DsoEditMetadataValueComponent {
export class DsoEditMetadataValueComponent implements OnInit {
/**
* The parent {@link DSpaceObject} to display a metadata form for
* Also used to determine metadata-representations in case of virtual metadata
*/
@Input() dso: DSpaceObject;
/**
* Editable metadata value to show
*/
@@ -59,4 +73,42 @@ export class DsoEditMetadataValueComponent {
* @type {DsoEditMetadataChangeType}
*/
public DsoEditMetadataChangeTypeEnum = DsoEditMetadataChangeType;
/**
* The item this metadata value represents in case it's virtual (if any, otherwise null)
*/
mdRepresentation$: Observable<ItemMetadataRepresentation | null>;
/**
* The route to the item represented by this virtual metadata value (otherwise null)
*/
mdRepresentationItemRoute$: Observable<string | null>;
/**
* The name of the item represented by this virtual metadata value (otherwise null)
*/
mdRepresentationName$: Observable<string | null>;
constructor(protected relationshipService: RelationshipService,
protected dsoNameService: DSONameService) {
}
ngOnInit(): void {
this.initVirtualProperties();
}
/**
* Initialise potential properties of a virtual metadata value
*/
initVirtualProperties() {
this.mdRepresentation$ = this.mdValue.newValue.isVirtual ?
this.relationshipService.resolveMetadataRepresentation(this.mdValue.newValue, this.dso, 'Item')
.pipe(map((mdRepresentation) => mdRepresentation.representationType === MetadataRepresentationType.Item ? mdRepresentation : null)) : of(null);
this.mdRepresentationItemRoute$ = this.mdRepresentation$.pipe(
map((mdRepresentation) => mdRepresentation ? getItemPageRoute(mdRepresentation) : null),
);
this.mdRepresentationName$ = this.mdRepresentation$.pipe(
map((mdRepresentation) => mdRepresentation ? this.dsoNameService.getName(mdRepresentation) : null),
);
}
}

View File

@@ -28,7 +28,8 @@
</ds-metadata-field-selector>
</div>
<div class="flex-grow-1 ds-drop-list">
<ds-dso-edit-metadata-value [mdValue]="form.newValue"
<ds-dso-edit-metadata-value [dso]="dso"
[mdValue]="form.newValue"
[dsoType]="dsoType"
[saving$]="savingOrLoadingFieldValidation$"
[isOnlyValue]="true"
@@ -43,6 +44,7 @@
<span class="dont-break-out preserve-line-breaks">{{ mdField }}</span>
</div>
<ds-dso-edit-metadata-field-values class="flex-grow-1"
[dso]="dso"
[form]="form"
[dsoType]="dsoType"
[saving$]="saving$"

View File

@@ -1,21 +1,12 @@
import { Component, Input } from '@angular/core';
import { MetadataRepresentation } from '../../../core/shared/metadata-representation/metadata-representation.model';
import {
combineLatest as observableCombineLatest,
Observable,
of as observableOf,
zip as observableZip
} from 'rxjs';
import { RelationshipService } from '../../../core/data/relationship.service';
import { MetadataValue } from '../../../core/shared/metadata.models';
import { getFirstSucceededRemoteData } from '../../../core/shared/operators';
import { filter, map, switchMap } from 'rxjs/operators';
import { RemoteData } from '../../../core/data/remote-data';
import { Relationship } from '../../../core/shared/item-relationships/relationship.model';
import { Item } from '../../../core/shared/item.model';
import { MetadatumRepresentation } from '../../../core/shared/metadata-representation/metadatum/metadatum-representation.model';
import { ItemMetadataRepresentation } from '../../../core/shared/metadata-representation/item/item-metadata-representation.model';
import { followLink } from '../../../shared/utils/follow-link-config.model';
import { AbstractIncrementalListComponent } from '../abstract-incremental-list/abstract-incremental-list.component';
@Component({
@@ -85,29 +76,7 @@ export class MetadataRepresentationListComponent extends AbstractIncrementalList
...metadata
.slice((this.objects.length * this.incrementBy), (this.objects.length * this.incrementBy) + this.incrementBy)
.map((metadatum: any) => Object.assign(new MetadataValue(), metadatum))
.map((metadatum: MetadataValue) => {
if (metadatum.isVirtual) {
return this.relationshipService.findById(metadatum.virtualValue, true, false, followLink('leftItem'), followLink('rightItem')).pipe(
getFirstSucceededRemoteData(),
switchMap((relRD: RemoteData<Relationship>) =>
observableCombineLatest(relRD.payload.leftItem, relRD.payload.rightItem).pipe(
filter(([leftItem, rightItem]) => leftItem.hasCompleted && rightItem.hasCompleted),
map(([leftItem, rightItem]) => {
if (!leftItem.hasSucceeded || !rightItem.hasSucceeded) {
return observableOf(Object.assign(new MetadatumRepresentation(this.itemType), metadatum));
} else if (rightItem.hasSucceeded && leftItem.payload.id === this.parentItem.id) {
return rightItem.payload;
} else if (rightItem.payload.id === this.parentItem.id) {
return leftItem.payload;
}
}),
map((item: Item) => Object.assign(new ItemMetadataRepresentation(metadatum), item))
)
));
} else {
return observableOf(Object.assign(new MetadatumRepresentation(this.itemType), metadatum));
}
})
.map((metadatum: MetadataValue) => this.relationshipService.resolveMetadataRepresentation(metadatum, this.parentItem, this.itemType)),
);
}
}

View File

@@ -1803,6 +1803,8 @@
"item.edit.metadata.edit.buttons.unedit": "Stop editing",
"item.edit.metadata.edit.buttons.virtual": "This is a virtual metadata value, i.e. a value inherited from a related entity. It cant be modified directly. Add or remove the corresponding relationship in the \"Relationships\" tab",
"item.edit.metadata.empty": "The item currently doesn't contain any metadata. Click Add to start adding a metadata value.",
"item.edit.metadata.headers.edit": "Edit",