import { Component, Input, OnDestroy, OnInit, } from '@angular/core'; import { ActivatedRoute, Data, Router, } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; import { combineLatest as observableCombineLatest, Observable, Subscription, } from 'rxjs'; import { map, switchMap, take, tap, } from 'rxjs/operators'; import { environment } from '../../../../environments/environment'; import { ItemDataService } from '../../../core/data/item-data.service'; import { FieldUpdate } from '../../../core/data/object-updates/field-update.model'; import { FieldUpdates } from '../../../core/data/object-updates/field-updates.model'; import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; import { RemoteData } from '../../../core/data/remote-data'; import { Item } from '../../../core/shared/item.model'; import { getAllSucceededRemoteData } from '../../../core/shared/operators'; import { hasValue } from '../../../shared/empty.util'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { AbstractTrackableComponent } from '../../../shared/trackable/abstract-trackable.component'; import { getItemPageLinksToFollow } from '../../item.resolver'; import { getItemPageRoute } from '../../item-page-routing-paths'; @Component({ selector: 'ds-abstract-item-update', template: '', standalone: true, }) /** * Abstract component for managing object updates of an item */ export class AbstractItemUpdateComponent extends AbstractTrackableComponent implements OnInit, OnDestroy { /** * The item to display the edit page for */ @Input() item: Item; /** * The current values and updates for all this item's fields * Should be initialized in the initializeUpdates method of the child component */ updates$: Observable; /** * Route to the item's page */ itemPageRoute: string; /** * 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 bitstreams are deleted */ itemUpdateSubscription: Subscription; constructor( public itemService: ItemDataService, public objectUpdatesService: ObjectUpdatesService, public router: Router, public notificationsService: NotificationsService, public translateService: TranslateService, public route: ActivatedRoute, ) { super(objectUpdatesService, notificationsService, translateService, router); } /** * Initialize common properties between item-update components */ ngOnInit(): void { if (hasValue(this.item)) { this.setItem(this.item); } else { // The item wasn't provided through an input, retrieve it from the route instead. this.itemUpdateSubscription = observableCombineLatest([this.route.data, this.route.parent.data]).pipe( map(([data, parentData]: [Data, Data]) => Object.assign({}, data, parentData)), map((data: any) => data.dso), tap((rd: RemoteData) => { this.item = rd.payload; }), switchMap((rd: RemoteData) => { return this.itemService.findByHref(rd.payload._links.self.href, true, true, ...getItemPageLinksToFollow()); }), getAllSucceededRemoteData(), ).subscribe((rd: RemoteData) => { this.setItem(rd.payload); }); } super.ngOnInit(); this.discardTimeOut = environment.item.edit.undoTimeout; this.hasChanges().pipe(take(1)).subscribe((hasChanges) => { if (!hasChanges) { this.initializeOriginalFields(); } else { this.checkLastModified(); } }); this.initializeNotificationsPrefix(); this.initializeUpdates(); } setItem(item: Item) { this.item = item; this.itemPageRoute = getItemPageRoute(this.item); this.postItemInit(); this.initializeUpdates(); } ngOnDestroy() { if (hasValue(this.itemUpdateSubscription)) { this.itemUpdateSubscription.unsubscribe(); } } /** * Actions to perform after the item has been initialized * Abstract method: Should be overwritten in the sub class */ postItemInit(): void { // Overwrite in subclasses } /** * Initialize the values and updates of the current item's fields * Abstract method: Should be overwritten in the sub class */ initializeUpdates(): void { // Overwrite in subclasses } /** * Initialize the prefix for notification messages * Abstract method: Should be overwritten in the sub class */ initializeNotificationsPrefix(): void { // Overwrite in subclasses } /** * Sends all initial values of this item to the object updates service * Abstract method: Should be overwritten in the sub class */ initializeOriginalFields(): void { // Overwrite in subclasses } /** * Submit the current changes * Abstract method: Should be overwritten in the sub class */ submit(): void { // Overwrite in subclasses } /** * Prevent unnecessary rerendering so fields don't lose focus */ trackUpdate(index, update: FieldUpdate) { return update && update.field ? update.field.uuid : undefined; } /** * Check if the current page is entirely valid */ public 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(take(1)).subscribe( (updateVersion: Date) => { if (updateVersion.getDate() !== currentVersion.getDate()) { this.notificationsService.warning(this.getNotificationTitle('outdated'), this.getNotificationContent('outdated')); this.initializeOriginalFields(); } }, ); } }