61142: AbstractItemUpdate component and refactoring of item-metadata and item-relationships

This commit is contained in:
Kristof De Langhe
2019-04-03 11:58:45 +02:00
parent cc862af249
commit 4a749cf91d
5 changed files with 254 additions and 258 deletions

View File

@@ -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."
}
}
}
}
},

View File

@@ -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');
}
}

View File

@@ -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);
}
/**
* Initialize the prefix for notification messages
*/
public initializeNotificationsPrefix(): void {
this.notificationsPrefix = 'item.edit.metadata.notifications.';
}
/**
* 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
*/
add(metadata: MetadatumViewModel = new MetadatumViewModel()) {
this.objectUpdatesService.saveAddFieldUpdate(this.url, metadata);
}
/**
* 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);
}
/**
* 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
*/

View File

@@ -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"

View File

@@ -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
/**
* Component for displaying an item's relationships edit page
*/
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.';
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
* Initialize the prefix for notification messages
*/
trackUpdate(index, update: FieldUpdate) {
return update && update.field ? update.field.uuid : undefined;
public initializeNotificationsPrefix(): void {
this.notificationsPrefix = 'item.edit.relationships.notifications.';
}
/**
* 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
*/
reinstate() {
this.objectUpdatesService.reinstateFieldUpdates(this.url);
}
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');
}
}