mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
93746: Edit metadata redesign - Virtual metadata
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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$"
|
||||
|
@@ -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}
|
||||
*/
|
||||
|
@@ -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)">
|
||||
|
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -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$"
|
||||
|
@@ -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)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -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 can’t 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",
|
||||
|
Reference in New Issue
Block a user