From 4a749cf91de89223b93e301d89bb685f01615c20 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe <kristof.delanghe@atmire.com> Date: Wed, 3 Apr 2019 11:58:45 +0200 Subject: [PATCH] 61142: AbstractItemUpdate component and refactoring of item-metadata and item-relationships --- resources/i18n/en.json | 25 +++ .../abstract-item-update.component.ts | 174 ++++++++++++++++++ .../item-metadata/item-metadata.component.ts | 152 ++------------- .../item-relationships.component.html | 2 +- .../item-relationships.component.ts | 159 ++++------------ 5 files changed, 254 insertions(+), 258 deletions(-) create mode 100644 src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 8b7d15abfd..b64edd42d5 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -273,6 +273,31 @@ "content": "Your changes to this item's metadata were saved." } } + }, + "relationships": { + "discard-button": "Discard", + "reinstate-button": "Undo", + "save-button": "Save", + "edit": { + "buttons": { + "remove": "Remove", + "undo": "Undo changes" + } + }, + "notifications": { + "outdated": { + "title": "Changed outdated", + "content": "The item you're currently working on has been changed by another user. Your current changes are discarded to prevent conflicts" + }, + "discarded": { + "title": "Changed discarded", + "content": "Your changes were discarded. To reinstate your changes click the 'Undo' button" + }, + "saved": { + "title": "Relationships saved", + "content": "Your changes to this item's relationships were saved." + } + } } } }, diff --git a/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts b/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts new file mode 100644 index 0000000000..3cc2a5ed84 --- /dev/null +++ b/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts @@ -0,0 +1,174 @@ +import { Component, Inject, OnInit } from '@angular/core'; +import { FieldUpdate, FieldUpdates } from '../../../core/data/object-updates/object-updates.reducer'; +import { Observable } from 'rxjs/internal/Observable'; +import { Item } from '../../../core/shared/item.model'; +import { ItemDataService } from '../../../core/data/item-data.service'; +import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; +import { GLOBAL_CONFIG, GlobalConfig } from '../../../../config'; +import { first, map } from 'rxjs/operators'; +import { RemoteData } from '../../../core/data/remote-data'; + +@Component({ + selector: 'ds-abstract-item-update', + template: ``, +}) +/** + * Abstract component for managing object updates of an item + */ +export abstract class AbstractItemUpdateComponent implements OnInit { + /** + * The item to display the edit page for + */ + protected item: Item; + /** + * The current values and updates for all this item's metadata fields + */ + protected updates$: Observable<FieldUpdates>; + /** + * The current url of this page + */ + protected url: string; + /** + * Prefix for this component's notification translate keys + */ + protected notificationsPrefix; + /** + * The time span for being able to undo discarding changes + */ + protected discardTimeOut: number; + + constructor( + protected itemService: ItemDataService, + protected objectUpdatesService: ObjectUpdatesService, + protected router: Router, + protected notificationsService: NotificationsService, + protected translateService: TranslateService, + @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, + protected route: ActivatedRoute + ) { + + } + + /** + * Initialize common properties between item-update components + */ + ngOnInit(): void { + this.route.parent.data.pipe(map((data) => data.item)) + .pipe( + first(), + map((data: RemoteData<Item>) => data.payload) + ).subscribe((item: Item) => { + this.item = item; + }); + + this.discardTimeOut = this.EnvConfig.item.edit.undoTimeout; + this.url = this.router.url; + if (this.url.indexOf('?') > 0) { + this.url = this.url.substr(0, this.url.indexOf('?')); + } + this.hasChanges().pipe(first()).subscribe((hasChanges) => { + if (!hasChanges) { + this.initializeOriginalFields(); + } else { + this.checkLastModified(); + } + }); + + this.initializeNotificationsPrefix(); + } + + /** + * Initialize the prefix for notification messages + */ + abstract initializeNotificationsPrefix(): void; + + /** + * Sends all initial values of this item to the object updates service + */ + abstract initializeOriginalFields(): void; + + /** + * Prevent unnecessary rerendering so fields don't lose focus + */ + trackUpdate(index, update: FieldUpdate) { + return update && update.field ? update.field.uuid : undefined; + } + + /** + * Checks whether or not there are currently updates for this item + */ + hasChanges(): Observable<boolean> { + return this.objectUpdatesService.hasUpdates(this.url); + } + + /** + * Check if the current page is entirely valid + */ + protected isValid() { + return this.objectUpdatesService.isValidPage(this.url); + } + + /** + * Checks if the current item is still in sync with the version in the store + * If it's not, a notification is shown and the changes are removed + */ + private checkLastModified() { + const currentVersion = this.item.lastModified; + this.objectUpdatesService.getLastModified(this.url).pipe(first()).subscribe( + (updateVersion: Date) => { + if (updateVersion.getDate() !== currentVersion.getDate()) { + this.notificationsService.warning(this.getNotificationTitle('outdated'), this.getNotificationContent('outdated')); + this.initializeOriginalFields(); + } + } + ); + } + + /** + * Submit the current changes + */ + abstract submit(): void; + + /** + * Request the object updates service to discard all current changes to this item + * Shows a notification to remind the user that they can undo this + */ + discard() { + const undoNotification = this.notificationsService.info(this.getNotificationTitle('discarded'), this.getNotificationContent('discarded'), { timeOut: this.discardTimeOut }); + this.objectUpdatesService.discardFieldUpdates(this.url, undoNotification); + } + + /** + * Request the object updates service to undo discarding all changes to this item + */ + reinstate() { + this.objectUpdatesService.reinstateFieldUpdates(this.url); + } + + /** + * Checks whether or not the item is currently reinstatable + */ + isReinstatable(): Observable<boolean> { + return this.objectUpdatesService.isReinstatable(this.url); + } + + /** + * Get translated notification title + * @param key + */ + protected getNotificationTitle(key: string) { + return this.translateService.instant(this.notificationsPrefix + key + '.title'); + } + + /** + * Get translated notification content + * @param key + */ + protected getNotificationContent(key: string) { + return this.translateService.instant(this.notificationsPrefix + key + '.content'); + + } +} diff --git a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts index 6b3e05c818..7c9202c3b9 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts +++ b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts @@ -6,8 +6,6 @@ import { ActivatedRoute, Router } from '@angular/router'; import { cloneDeep } from 'lodash'; import { Observable } from 'rxjs'; import { - FieldUpdate, - FieldUpdates, Identifiable } from '../../../core/data/object-updates/object-updates.reducer'; import { first, map, switchMap, take, tap } from 'rxjs/operators'; @@ -20,6 +18,7 @@ import { RegistryService } from '../../../core/registry/registry.service'; import { MetadataField } from '../../../core/metadata/metadatafield.model'; import { MetadatumViewModel } from '../../../core/shared/metadata.models'; import { Metadata } from '../../../core/shared/metadata.utils'; +import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component'; @Component({ selector: 'ds-item-metadata', @@ -29,28 +28,7 @@ import { Metadata } from '../../../core/shared/metadata.utils'; /** * Component for displaying an item's metadata edit page */ -export class ItemMetadataComponent implements OnInit { - - /** - * The item to display the edit page for - */ - item: Item; - /** - * The current values and updates for all this item's metadata fields - */ - updates$: Observable<FieldUpdates>; - /** - * The current url of this page - */ - url: string; - /** - * The time span for being able to undo discarding changes - */ - private discardTimeOut: number; - /** - * Prefix for this component's notification translate keys - */ - private notificationsPrefix = 'item.edit.metadata.notifications.'; +export class ItemMetadataComponent extends AbstractItemUpdateComponent { /** * Observable with a list of strings with all existing metadata field keys @@ -58,90 +36,54 @@ export class ItemMetadataComponent implements OnInit { metadataFields$: Observable<string[]>; constructor( - private itemService: ItemDataService, - private objectUpdatesService: ObjectUpdatesService, - private router: Router, - private notificationsService: NotificationsService, - private translateService: TranslateService, + protected itemService: ItemDataService, + protected objectUpdatesService: ObjectUpdatesService, + protected router: Router, + protected notificationsService: NotificationsService, + protected translateService: TranslateService, @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, - private route: ActivatedRoute, - private metadataFieldService: RegistryService, + protected route: ActivatedRoute, + protected metadataFieldService: RegistryService, ) { - + super(itemService, objectUpdatesService, router, notificationsService, translateService, EnvConfig, route); } /** * Set up and initialize all fields */ ngOnInit(): void { + super.ngOnInit(); this.metadataFields$ = this.findMetadataFields(); - this.route.parent.data.pipe(map((data) => data.item)) - .pipe( - first(), - map((data: RemoteData<Item>) => data.payload) - ).subscribe((item: Item) => { - this.item = item; - }); - - this.discardTimeOut = this.EnvConfig.item.edit.undoTimeout; - this.url = this.router.url; - if (this.url.indexOf('?') > 0) { - this.url = this.url.substr(0, this.url.indexOf('?')); - } - this.hasChanges().pipe(first()).subscribe((hasChanges) => { - if (!hasChanges) { - this.initializeOriginalFields(); - } else { - this.checkLastModified(); - } - }); this.updates$ = this.objectUpdatesService.getFieldUpdates(this.url, this.item.metadataAsList); } /** - * Sends a new add update for a field to the object updates service - * @param metadata The metadata to add, if no parameter is supplied, create a new Metadatum + * Initialize the prefix for notification messages */ - add(metadata: MetadatumViewModel = new MetadatumViewModel()) { - this.objectUpdatesService.saveAddFieldUpdate(this.url, metadata); - + public initializeNotificationsPrefix(): void { + this.notificationsPrefix = 'item.edit.metadata.notifications.'; } /** - * Request the object updates service to discard all current changes to this item - * Shows a notification to remind the user that they can undo this - */ - discard() { - const undoNotification = this.notificationsService.info(this.getNotificationTitle('discarded'), this.getNotificationContent('discarded'), { timeOut: this.discardTimeOut }); - this.objectUpdatesService.discardFieldUpdates(this.url, undoNotification); - } - - /** - * Request the object updates service to undo discarding all changes to this item + * Sends a new add update for a field to the object updates service + * @param metadata The metadata to add, if no parameter is supplied, create a new Metadatum */ - reinstate() { - this.objectUpdatesService.reinstateFieldUpdates(this.url); + add(metadata: MetadatumViewModel = new MetadatumViewModel()) { + this.objectUpdatesService.saveAddFieldUpdate(this.url, metadata); } /** * Sends all initial values of this item to the object updates service */ - private initializeOriginalFields() { + public initializeOriginalFields() { this.objectUpdatesService.initialize(this.url, this.item.metadataAsList, this.item.lastModified); } - /** - * Prevent unnecessary rerendering so fields don't lose focus - */ - trackUpdate(index, update: FieldUpdate) { - return update && update.field ? update.field.uuid : undefined; - } - /** * Requests all current metadata for this item and requests the item service to update the item * Makes sure the new version of the item is rendered on the page */ - submit() { + public submit() { this.isValid().pipe(first()).subscribe((isValid) => { if (isValid) { const metadata$: Observable<Identifiable[]> = this.objectUpdatesService.getUpdatedFields(this.url, this.item.metadataAsList) as Observable<MetadatumViewModel[]>; @@ -167,60 +109,6 @@ export class ItemMetadataComponent implements OnInit { }); } - /** - * Checks whether or not there are currently updates for this item - */ - hasChanges(): Observable<boolean> { - return this.objectUpdatesService.hasUpdates(this.url); - } - - /** - * Checks whether or not the item is currently reinstatable - */ - isReinstatable(): Observable<boolean> { - return this.objectUpdatesService.isReinstatable(this.url); - } - - /** - * Checks if the current item is still in sync with the version in the store - * If it's not, a notification is shown and the changes are removed - */ - private checkLastModified() { - const currentVersion = this.item.lastModified; - this.objectUpdatesService.getLastModified(this.url).pipe(first()).subscribe( - (updateVersion: Date) => { - if (updateVersion.getDate() !== currentVersion.getDate()) { - this.notificationsService.warning(this.getNotificationTitle('outdated'), this.getNotificationContent('outdated')); - this.initializeOriginalFields(); - } - } - ); - } - - /** - * Check if the current page is entirely valid - */ - private isValid() { - return this.objectUpdatesService.isValidPage(this.url); - } - - /** - * Get translated notification title - * @param key - */ - private getNotificationTitle(key: string) { - return this.translateService.instant(this.notificationsPrefix + key + '.title'); - } - - /** - * Get translated notification content - * @param key - */ - private getNotificationContent(key: string) { - return this.translateService.instant(this.notificationsPrefix + key + '.content'); - - } - /** * Method to request all metadata fields and convert them to a list of strings */ diff --git a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.html b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.html index c95812b4bb..cfa3b8f415 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.html +++ b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.html @@ -19,7 +19,7 @@ </div> <div *ngFor="let label of relationLabels$ | async" class="mb-2"> <h5>{{label}}</h5> - <div *ngFor="let updateValue of ((getUpdatesByLabel(label) | async)| dsObjectValues)" + <div *ngFor="let updateValue of ((getUpdatesByLabel(label) | async)| dsObjectValues); trackBy: trackUpdate" ds-edit-in-place-relationship [fieldUpdate]="updateValue || {}" [url]="url" diff --git a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts index 0bd1635ae1..66701e22b4 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts +++ b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts @@ -1,51 +1,32 @@ -import { Component, Inject, OnInit } from '@angular/core'; +import { Component } from '@angular/core'; import { Item } from '../../../core/shared/item.model'; -import { FieldUpdate, FieldUpdates } from '../../../core/data/object-updates/object-updates.reducer'; +import { FieldUpdates } from '../../../core/data/object-updates/object-updates.reducer'; import { Observable } from 'rxjs/internal/Observable'; -import { ActivatedRoute, Router } from '@angular/router'; -import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; -import { distinctUntilChanged, filter, first, flatMap, map, startWith, switchMap, tap } from 'rxjs/operators'; +import { distinctUntilChanged, first, flatMap, map, switchMap } from 'rxjs/operators'; import { zip as observableZip } from 'rxjs'; import { RemoteData } from '../../../core/data/remote-data'; import { PaginatedList } from '../../../core/data/paginated-list'; import { Relationship } from '../../../core/shared/item-relationships/relationship.model'; import { hasValue, hasValueOperator } from '../../../shared/empty.util'; import { getRemoteDataPayload, getSucceededRemoteData } from '../../../core/shared/operators'; -import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { RelationshipType } from '../../../core/shared/item-relationships/relationship-type.model'; import { compareArraysUsingIds, filterRelationsByTypeLabel, relationsToItems } from '../../simple/item-types/shared/item.component'; -import { ItemDataService } from '../../../core/data/item-data.service'; import { combineLatest as observableCombineLatest } from 'rxjs/internal/observable/combineLatest'; -import { TranslateService } from '@ngx-translate/core'; -import { GLOBAL_CONFIG, GlobalConfig } from '../../../../config'; +import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component'; @Component({ selector: 'ds-item-relationships', styleUrls: ['./item-relationships.component.scss'], templateUrl: './item-relationships.component.html', }) -export class ItemRelationshipsComponent implements OnInit { - - /** - * The item to display the edit page for - */ - item: Item; - /** - * The current values and updates for all this item's metadata fields - */ - updates$: Observable<FieldUpdates>; - /** - * The current url of this page - */ - url: string; - /** - * Prefix for this component's notification translate keys - */ - private notificationsPrefix = 'item.edit.metadata.notifications.'; +/** + * Component for displaying an item's relationships edit page + */ +export class ItemRelationshipsComponent extends AbstractItemUpdateComponent { /** * The labels of all different relations within this item @@ -56,47 +37,20 @@ export class ItemRelationshipsComponent implements OnInit { * Resolved relationships and types together in one observable */ resolvedRelsAndTypes$: Observable<[Relationship[], RelationshipType[]]>; - /** - * The time span for being able to undo discarding changes - */ - private discardTimeOut: number; - - constructor(private route: ActivatedRoute, - private router: Router, - private translateService: TranslateService, - @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, - private objectUpdatesService: ObjectUpdatesService, - private notificationsService: NotificationsService, - private itemDataService: ItemDataService) { - } ngOnInit(): void { - this.route.parent.data.pipe(map((data) => data.item)) - .pipe( - first(), - map((data: RemoteData<Item>) => data.payload) - ).subscribe((item: Item) => { - this.item = item; - }); - this.discardTimeOut = this.EnvConfig.item.edit.undoTimeout; - this.url = this.router.url; - if (this.url.indexOf('?') > 0) { - this.url = this.url.substr(0, this.url.indexOf('?')); - } - this.hasChanges().pipe(first()).subscribe((hasChanges) => { - if (!hasChanges) { - this.initializeOriginalFields(); - } else { - this.checkLastModified(); - } - }); + super.ngOnInit(); + this.updates$ = this.getRelationships().pipe( - relationsToItems(this.item.id, this.itemDataService), + relationsToItems(this.item.id, this.itemService), switchMap((items: Item[]) => this.objectUpdatesService.getFieldUpdates(this.url, items)) ); this.initRelationshipObservables(); } + /** + * Initialize the item's relationship observables for easier access across the component + */ initRelationshipObservables() { const relationships$ = this.getRelationships(); @@ -119,72 +73,36 @@ export class ItemRelationshipsComponent implements OnInit { } /** - * Prevent unnecessary rerendering so fields don't lose focus - */ - trackUpdate(index, update: FieldUpdate) { - return update && update.field ? update.field.uuid : undefined; - } - - /** - * Checks whether or not there are currently updates for this item - */ - hasChanges(): Observable<boolean> { - return this.objectUpdatesService.hasUpdates(this.url); - } - - /** - * Checks whether or not the item is currently reinstatable - */ - isReinstatable(): Observable<boolean> { - return this.objectUpdatesService.isReinstatable(this.url); - } - - discard(): void { - const undoNotification = this.notificationsService.info(this.getNotificationTitle('discarded'), this.getNotificationContent('discarded'), { timeOut: this.discardTimeOut }); - this.objectUpdatesService.discardFieldUpdates(this.url, undoNotification); - } - - /** - * Request the object updates service to undo discarding all changes to this item + * Initialize the prefix for notification messages */ - reinstate() { - this.objectUpdatesService.reinstateFieldUpdates(this.url); + public initializeNotificationsPrefix(): void { + this.notificationsPrefix = 'item.edit.relationships.notifications.'; } - submit(): void { + public submit(): void { const updatedItems$ = this.getRelationships().pipe( first(), - relationsToItems(this.item.id, this.itemDataService), + relationsToItems(this.item.id, this.itemService), switchMap((items: Item[]) => this.objectUpdatesService.getUpdatedFields(this.url, items) as Observable<Item[]>) ); // TODO: Delete relationships } - private initializeOriginalFields() { + /** + * Sends all initial values of this item to the object updates service + */ + public initializeOriginalFields() { this.getRelationships().pipe( first(), - relationsToItems(this.item.id, this.itemDataService) + relationsToItems(this.item.id, this.itemService) ).subscribe((items: Item[]) => { this.objectUpdatesService.initialize(this.url, items, this.item.lastModified); }); } /** - * Checks if the current item is still in sync with the version in the store - * If it's not, a notification is shown and the changes are removed + * Fetch all the relationships of the item */ - private checkLastModified() { - const currentVersion = this.item.lastModified; - this.objectUpdatesService.getLastModified(this.url).pipe(first()).subscribe( - (updateVersion: Date) => { - if (updateVersion.getDate() !== currentVersion.getDate()) { - this.notificationsService.warning(this.getNotificationTitle('outdated'), this.getNotificationContent('outdated')); - this.initializeOriginalFields(); - } - } - ); - } - public getRelationships(): Observable<Relationship[]> { return this.item.relationships.pipe( getSucceededRemoteData(), @@ -195,34 +113,25 @@ export class ItemRelationshipsComponent implements OnInit { ); } + /** + * Transform the item's relationships of a specific type into related items + * @param label The relationship type's label + */ public getRelatedItemsByLabel(label: string): Observable<Item[]> { return this.resolvedRelsAndTypes$.pipe( filterRelationsByTypeLabel(label), - relationsToItems(this.item.id, this.itemDataService) + relationsToItems(this.item.id, this.itemService) ); } + /** + * Get FieldUpdates for the relationships of a specific type + * @param label The relationship type's label + */ public getUpdatesByLabel(label: string): Observable<FieldUpdates> { return this.getRelatedItemsByLabel(label).pipe( switchMap((items: Item[]) => this.objectUpdatesService.getFieldUpdatesExclusive(this.url, items)) ) } - /** - * Get translated notification title - * @param key - */ - private getNotificationTitle(key: string) { - return this.translateService.instant(this.notificationsPrefix + key + '.title'); - } - - /** - * Get translated notification content - * @param key - */ - private getNotificationContent(key: string) { - return this.translateService.instant(this.notificationsPrefix + key + '.content'); - - } - } -- GitLab