1
0

[CST-4499] Version history (WIP) - Missing tests and auth features

This commit is contained in:
Alessandro Martelli
2021-09-02 13:30:13 +02:00
committed by Davide Negretti
parent adb40d8712
commit ce399cb764
28 changed files with 815 additions and 104 deletions

View File

@@ -8,19 +8,21 @@ import { CoreState } from '../core.reducers';
import { ObjectCacheService } from '../cache/object-cache.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { HttpClient } from '@angular/common/http';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
import { FindListOptions } from './request.models';
import { FindListOptions, PostRequest, RestRequest } from './request.models';
import { Observable } from 'rxjs';
import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model';
import { RemoteData } from './remote-data';
import { PaginatedList } from './paginated-list.model';
import { Version } from '../shared/version.model';
import { map, switchMap } from 'rxjs/operators';
import { map, switchMap, take } from 'rxjs/operators';
import { dataService } from '../cache/builders/build-decorators';
import { VERSION_HISTORY } from '../shared/version-history.resource-type';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { VersionDataService } from './version-data.service';
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
import { getFirstCompletedRemoteData, sendRequest } from '../shared/operators';
/**
* Service responsible for handling requests related to the VersionHistory object
@@ -79,4 +81,20 @@ export class VersionHistoryDataService extends DataService<VersionHistory> {
return this.versionDataService.findAllByHref(hrefObs, undefined, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}
createVersion(itemHref: string, summary: string): Observable<RemoteData<Version>> {
const requestOptions: HttpOptions = Object.create({});
let requestHeaders = new HttpHeaders();
requestHeaders = requestHeaders.append('Content-Type', 'text/uri-list');
requestOptions.headers = requestHeaders;
return this.halService.getEndpoint(this.versionsEndpoint).pipe(
take(1),
map((endpointUrl: string) => (summary?.length > 0) ? `${endpointUrl}?summary=${summary}` : `${endpointUrl}`),
map((endpointURL: string) => new PostRequest(this.requestService.generateRequestId(), endpointURL, itemHref, requestOptions)),
sendRequest(this.requestService),
switchMap((restRequest: RestRequest) => this.rdbService.buildFromRequestUUID(restRequest.uuid)),
getFirstCompletedRemoteData()
) as Observable<RemoteData<Version>>;
}
}

View File

@@ -30,6 +30,18 @@ export class VersionHistory extends DSpaceObject {
@autoserialize
id: string;
/**
* The summary of this Version History
*/
@autoserialize
summary: string;
/**
* The name of the submitter of this Version History
*/
@autoserialize
submitterName: string;
/**
* The list of versions within this history
*/

View File

@@ -22,14 +22,17 @@
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.discard-button" | translate}}</span>
</button>
</div>
<table class="table table-responsive table-striped table-bordered" *ngIf="((updates$ | async)| dsObjectValues).length > 0">
<tbody>
<table class="table table-responsive table-striped table-bordered"
*ngIf="((updates$ | async)| dsObjectValues).length > 0">
<thead>
<tr>
<th><span id="fieldName">{{'item.edit.metadata.headers.field' | translate}}</span></th>
<th><span id="fieldValue">{{'item.edit.metadata.headers.value' | translate}}</span></th>
<th class="text-center"><span id="fieldLang">{{'item.edit.metadata.headers.language' | translate}}</span></th>
<th class="text-center">{{'item.edit.metadata.headers.edit' | translate}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let updateValue of ((updates$ | async)| dsObjectValues); trackBy: trackUpdate"
ds-edit-in-place-field
[fieldUpdate]="updateValue || {}"

View File

@@ -1,6 +1,4 @@
<div class="mt-4">
<ds-alert [content]="'item.edit.tabs.versionhistory.under-construction'" [type]="AlertTypeEnum.Warning"></ds-alert>
</div>
<div class="mt-2" *ngVar="(itemRD$ | async)?.payload as item">
<ds-item-versions *ngIf="item" [item]="item" [displayWhenEmpty]="true" [displayTitle]="false"></ds-item-versions>
<ds-item-versions *ngIf="item" [item]="item" [displayWhenEmpty]="true" [displayTitle]="false"
[displayActions]="true"></ds-item-versions>
</div>

View File

@@ -30,6 +30,6 @@ export class ItemVersionHistoryComponent {
}
ngOnInit(): void {
this.itemRD$ = this.route.parent.data.pipe(map((data) => data.dso)).pipe(getFirstSucceededRemoteData()) as Observable<RemoteData<Item>>;
this.itemRD$ = this.route.parent.parent.data.pipe(map((data) => data.dso)).pipe(getFirstSucceededRemoteData()) as Observable<RemoteData<Item>>;
}
}

View File

@@ -5,7 +5,7 @@
<ds-item-versions-notice [item]="item"></ds-item-versions-notice>
<ds-view-tracker [object]="item"></ds-view-tracker>
<ds-listable-object-component-loader [object]="item" [viewMode]="viewMode"></ds-listable-object-component-loader>
<ds-item-versions class="mt-2" [item]="item"></ds-item-versions>
<ds-item-versions class="mt-2" [item]="item" [displayActions]="false"></ds-item-versions>
</div>
</div>
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.item' | translate}}"></ds-error>

View File

@@ -3,6 +3,7 @@
{{'publication.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="object?.allMetadata(['dc.title'])"></ds-metadata-values>
</h2>
<div class="pl-2">
<ds-dso-page-version-button (newVersionEvent)="createNewVersion()" [dso]="object" [tooltipMsg]="'item.page.version'"></ds-dso-page-version-button>
<ds-dso-page-edit-button [pageRoute]="itemPageRoute" [dso]="object" [tooltipMsg]="'publication.page.edit'"></ds-dso-page-edit-button>
</div>
</div>

View File

@@ -2,6 +2,14 @@ import { Component, Input, OnInit } from '@angular/core';
import { environment } from '../../../../../environments/environment';
import { Item } from '../../../../core/shared/item.model';
import { getItemPageRoute } from '../../../item-page-routing-paths';
import { ItemVersionsSummaryModalComponent } from '../../../../shared/item/item-versions/item-versions-summary-modal/item-versions-summary-modal.component';
import { getFirstCompletedRemoteData } from '../../../../core/shared/operators';
import { take } from 'rxjs/operators';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { VersionHistoryDataService } from '../../../../core/data/version-history-data.service';
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
import { VersionDataService } from '../../../../core/data/version-data.service';
@Component({
selector: 'ds-item',
@@ -20,6 +28,49 @@ export class ItemComponent implements OnInit {
mediaViewer = environment.mediaViewer;
constructor(
private modalService: NgbModal,
private versionHistoryService: VersionHistoryDataService,
private notificationsService: NotificationsService,
private translateService: TranslateService,
// private itemService: ItemDataService,
private versionService: VersionDataService,
) {
}
createNewVersion() {
const successMessageKey = 'item.version.create.notification.success';
const failureMessageKey = 'item.version.create.notification.failure';
const item = this.object;
// Open modal
const activeModal = this.modalService.open(ItemVersionsSummaryModalComponent);
this.versionService.findByHref(item._links.version.href).pipe(getFirstCompletedRemoteData()).subscribe(
(res) => {
// TODO check serve async?
activeModal.componentInstance.firstVersion = res.hasNoContent;
activeModal.componentInstance.versionNumber = (res.hasNoContent ? undefined : res.payload.version);
}
);
// On modal submit/dismiss
activeModal.result.then((modalResult) => {
const summary = modalResult;
const itemHref = item._links.self.href;
this.versionHistoryService.createVersion(itemHref, summary).pipe(take(1)).subscribe((postResult) => {
if (postResult.hasSucceeded) {
const newVersionNumber = postResult.payload.version;
this.notificationsService.success(null, this.translateService.get(successMessageKey, {version: newVersionNumber}));
} else {
this.notificationsService.error(null, this.translateService.get(failureMessageKey));
}
});
});
}
ngOnInit(): void {
this.itemPageRoute = getItemPageRoute(this.object);
}

View File

@@ -3,6 +3,7 @@
<ds-metadata-values [mdValues]="object?.allMetadata(['dc.title'])"></ds-metadata-values>
</h2>
<div class="pl-2">
<ds-dso-page-version-button (newVersionEvent)="createNewVersion()" [dso]="object" [tooltipMsg]="'item.page.version'"></ds-dso-page-version-button>
<ds-dso-page-edit-button [pageRoute]="itemPageRoute" [dso]="object" [tooltipMsg]="'item.page.edit'"></ds-dso-page-edit-button>
</div>
</div>

View File

@@ -119,7 +119,7 @@ export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit {
}
/**
* Method called on clicking the button "New Submition", It opens a dialog for
* Method called on clicking the button "New Submission", It opens a dialog for
* select a collection.
*/
openDialog() {

View File

@@ -0,0 +1,7 @@
<button *ngIf="isAuthorized$ | async"
class="edit-button btn btn-dark btn-sm"
(click)="createNewVersion()"
[ngbTooltip]="tooltipMsg | translate"
role="button" [title]="tooltipMsg |translate" [attr.aria-label]="tooltipMsg |translate">
<i class="fas fa-code-branch fa-fw"></i>
</button>

View File

@@ -0,0 +1,3 @@
.btn-dark {
background-color: var(--ds-admin-sidebar-bg);
}

View File

@@ -0,0 +1,76 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { DsoPageVersionButtonComponent } from './dso-page-version-button.component';
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { Item } from '../../../core/shared/item.model';
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
import { of as observableOf } from 'rxjs';
import { TranslateModule } from '@ngx-translate/core';
import { RouterTestingModule } from '@angular/router/testing';
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
import { By } from '@angular/platform-browser';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
describe('DsoPageEditButtonComponent', () => {
let component: DsoPageVersionButtonComponent;
let fixture: ComponentFixture<DsoPageVersionButtonComponent>;
let authorizationService: AuthorizationDataService;
let dso: DSpaceObject;
beforeEach(waitForAsync(() => {
dso = Object.assign(new Item(), {
id: 'test-item',
_links: {
self: { href: 'test-item-selflink' }
}
});
authorizationService = jasmine.createSpyObj('authorizationService', {
isAuthorized: observableOf(true)
});
TestBed.configureTestingModule({
declarations: [DsoPageVersionButtonComponent],
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NgbModule],
providers: [
{ provide: AuthorizationDataService, useValue: authorizationService }
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DsoPageVersionButtonComponent);
component = fixture.componentInstance;
component.dso = dso;
component.pageRoute = 'test';
fixture.detectChanges();
});
it('should check the authorization of the current user', () => {
expect(authorizationService.isAuthorized).toHaveBeenCalledWith(FeatureID.CanEditMetadata, dso.self);
});
describe('when the user is authorized', () => {
beforeEach(() => {
(authorizationService.isAuthorized as jasmine.Spy).and.returnValue(observableOf(true));
component.ngOnInit();
fixture.detectChanges();
});
it('should render a link', () => {
const link = fixture.debugElement.query(By.css('a'));
expect(link).not.toBeNull();
});
});
describe('when the user is not authorized', () => {
beforeEach(() => {
(authorizationService.isAuthorized as jasmine.Spy).and.returnValue(observableOf(false));
component.ngOnInit();
fixture.detectChanges();
});
it('should not render a link', () => {
const link = fixture.debugElement.query(By.css('a'));
expect(link).toBeNull();
});
});
});

View File

@@ -0,0 +1,52 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
import { Observable } from 'rxjs/internal/Observable';
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
@Component({
selector: 'ds-dso-page-version-button',
templateUrl: './dso-page-version-button.component.html',
styleUrls: ['./dso-page-version-button.component.scss']
})
/**
* Display a button linking to the edit page of a DSpaceObject
*/
export class DsoPageVersionButtonComponent implements OnInit {
/**
* The DSpaceObject to display a button to the edit page for
*/
@Input() dso: DSpaceObject;
/**
* A message for the tooltip on the button
* Supports i18n keys
*/
@Input() tooltipMsg: string;
/**
* Emits an event that triggers the creation of the new version
*/
@Output() newVersionEvent = new EventEmitter();
/**
* Whether or not the current user is authorized to create a new version of the DSpaceObject
*/
isAuthorized$: Observable<boolean>;
constructor(protected authorizationService: AuthorizationDataService) {
}
/**
* Creates a new version for the current item
*/
createNewVersion() {
this.newVersionEvent.emit();
}
ngOnInit() {
// TODO show if user can view history
this.isAuthorized$ = this.authorizationService.isAuthorized(FeatureID.CanEditMetadata, this.dso.self);
}
}

View File

@@ -0,0 +1,22 @@
<div>
<div class="modal-header">{{'item.version.delete.modal.header' | translate}}
<button type="button" class="close" (click)="onModalClose()" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<p class="pb-2">{{ "item.version.delete.modal.text" | translate : {version: versionNumber} }}</p>
</div>
<div class="modal-footer">
<button class="btn btn-outline-secondary btn-sm"
(click)="onModalClose()"
title="{{'item.version.delete.modal.button.cancel.tooltip' | translate}}">
<i class="fas fa-times fa-fw"></i> {{'item.version.delete.modal.button.cancel' | translate}}
</button>
<button class="btn btn-danger btn-sm"
(click)="onModalSubmit()"
title="{{'item.version.delete.modal.button.confirm.tooltip' | translate}}">
<i class="fas fa-check fa-fw"></i> {{'item.version.delete.modal.button.confirm' | translate}}
</button>
</div>
</div>

View File

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ItemVersionsDeleteModalComponent } from './item-versions-delete-modal.component';
describe('ItemVersionsDeleteModalComponent', () => {
let component: ItemVersionsDeleteModalComponent;
let fixture: ComponentFixture<ItemVersionsDeleteModalComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ ItemVersionsDeleteModalComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ItemVersionsDeleteModalComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,25 @@
import { Component } from '@angular/core';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
@Component({
selector: 'ds-item-versions-delete-modal',
templateUrl: './item-versions-delete-modal.component.html',
styleUrls: ['./item-versions-delete-modal.component.scss']
})
export class ItemVersionsDeleteModalComponent {
versionNumber: number;
constructor(
protected activeModal: NgbActiveModal,) {
}
onModalClose() {
this.activeModal.dismiss();
}
onModalSubmit() {
this.activeModal.close();
}
}

View File

@@ -0,0 +1,30 @@
<div>
<div class="modal-header">{{'item.version.create.modal.header' | translate}}
<button type="button" class="close" (click)="onModalClose()" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<p class="pb-2">
{{ "item.version.create.modal.text" | translate }}
<span *ngIf="!firstVersion">{{ "item.version.create.modal.text.startingFrom" | translate : {version: versionNumber} }}</span>
</p>
<div class="form-group">
<label for="summary">{{'item.version.create.modal.form.summary.label' | translate }}:</label>
<input type="text" id="summary" required class="form-control" [(ngModel)]="newVersionSummary"
placeholder="{{'item.version.create.modal.form.summary.placeholder' | translate }}"/>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-outline-secondary btn-sm"
(click)="onModalClose()"
title="{{'item.version.create.modal.button.cancel.tooltip' | translate}}">
<i class="fas fa-times fa-fw"></i> {{'item.version.create.modal.button.cancel' | translate}}
</button>
<button class="btn btn-success btn-sm"
(click)="onModalSubmit()"
title="{{'item.version.create.modal.button.confirm.tooltip' | translate}}">
<i class="fas fa-check fa-fw"></i> {{'item.version.create.modal.button.confirm' | translate}}
</button>
</div>
</div>

View File

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ItemVersionsSummaryModalComponent } from './item-versions-summary-modal.component';
describe('ItemVersionsSummaryModalComponent', () => {
let component: ItemVersionsSummaryModalComponent;
let fixture: ComponentFixture<ItemVersionsSummaryModalComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ ItemVersionsSummaryModalComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ItemVersionsSummaryModalComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,28 @@
import { Component } from '@angular/core';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
@Component({
selector: 'ds-item-versions-summary-modal',
templateUrl: './item-versions-summary-modal.component.html',
styleUrls: ['./item-versions-summary-modal.component.scss']
})
export class ItemVersionsSummaryModalComponent {
versionNumber: number;
newVersionSummary: string;
firstVersion: boolean;
constructor(
protected activeModal: NgbActiveModal,
) {
}
onModalClose() {
this.activeModal.dismiss();
}
onModalSubmit() {
this.activeModal.close(this.newVersionSummary);
}
}

View File

@@ -2,6 +2,9 @@
<div *ngVar="(versionRD$ | async)?.payload as itemVersion">
<div class="mb-2" *ngIf="versions?.page?.length > 0 || displayWhenEmpty">
<h2 *ngIf="displayTitle">{{"item.version.history.head" | translate}}</h2>
<ds-alert [type]="AlertTypeEnum.Info" *ngIf="itemVersion">
{{ "item.version.history.selected.alert" | translate : {version: itemVersion.version} }}
</ds-alert>
<ds-pagination *ngIf="versions?.page?.length > 0"
[hideGear]="true"
[hidePagerWhenSinglePage]="true"
@@ -9,38 +12,79 @@
[pageInfoState]="versions"
[collectionSize]="versions?.totalElements"
[retainScrollPosition]="true">
<table class="table table-striped my-2">
<table class="table table-striped table-bordered align-middle my-2">
<thead>
<tr>
<th scope="col">{{"item.version.history.table.version" | translate}}</th>
<th scope="col">{{"item.version.history.table.item" | translate}}</th>
<th scope="col" *ngIf="(hasEpersons$ | async)">{{"item.version.history.table.editor" | translate}}</th>
<th scope="col">{{"item.version.history.table.date" | translate}}</th>
<th scope="col">{{"item.version.history.table.summary" | translate}}</th>
<th scope="col" *ngIf="displayActions">{{"item.version.history.table.actions" | translate}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let version of versions?.page" [id]="'version-row-' + version.id">
<td class="version-row-element-version">{{version?.version}}</td>
<td class="version-row-element-item">
<span *ngVar="(version?.item | async)?.payload as item">
<a *ngIf="item" [routerLink]="[(itemPageRoutes$ | async)[item?.id]]">{{item?.handle}}</a>
<span *ngIf="version?.id === itemVersion?.id">*</span>
</span>
</td>
<td *ngIf="(hasEpersons$ | async)" class="version-row-element-editor">
<span *ngVar="(version?.eperson | async)?.payload as eperson">
<a *ngIf="eperson" [href]="'mailto:' + eperson?.email">{{eperson?.name}}</a>
</span>
</td>
<td class="version-row-element-date">{{version?.created}}</td>
<td class="version-row-element-summary">{{version?.summary}}</td>
<td class="version-row-element-date">{{version?.created | date : 'yyyy-MM-dd HH:mm:ss'}}</td>
<td class="version-row-element-summary">
<ng-container *ngIf="isThisBeingEdited(version); then editSummary else showSummary"></ng-container>
<ng-template #showSummary>{{version?.summary}}</ng-template>
<ng-template #editSummary>
<input class="form-control" type="text" [(ngModel)]="versionBeingEditedSummary"/>
</ng-template>
</td>
<td class="version-row-element-actions" *ngIf="displayActions">
<div class="btn-group edit-field">
<!--EDIT / SAVE-->
<button class="btn btn-outline-primary btn-sm"
*ngIf="!isThisBeingEdited(version)"
[disabled]="isAnyBeingEdited()"
(click)="enableVersionEditing(version)"
title="{{'item.version.history.table.action.editSummary' | translate}}">
<i class="fas fa-edit fa-fw"></i>
</button>
<button class="btn btn-outline-success btn-sm"
*ngIf="isThisBeingEdited(version)"
(click)="onSummarySubmit()"
title="{{'item.version.history.table.action.saveSummary' | translate}}">
<i class="fas fa-check fa-fw"></i>
</button>
<!--CREATE-->
<button class="btn btn-outline-primary btn-sm"
[disabled]="isAnyBeingEdited()"
(click)="createNewVersion(version)"
title="{{'item.version.history.table.action.newVersion' | translate}}">
<i class="fas fa-code-branch fa-fw"></i>
</button>
<!--DELETE-->
<button class="btn btn-sm"
[ngClass]="isAnyBeingEdited() ? 'btn-outline-primary' : 'btn-outline-danger'"
[disabled]="isAnyBeingEdited()"
(click)="deleteVersion(version)"
title="{{'item.version.history.table.action.deleteVersion' | translate}}">
<i class="fas fa-trash fa-fw"></i>
</button>
<button class="btn btn-sm"
[ngClass]="isThisBeingEdited(version) ? 'btn-outline-warning' : 'btn-outline-primary'"
[disabled]="!isAnyBeingEdited() || !isThisBeingEdited(version)"
(click)="disableSummaryEditing()"
title="{{'item.version.history.table.action.discardSummary' | translate}}">
<i class="fas fa-undo-alt fa-fw"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
<div>*&nbsp;{{"item.version.history.selected" | translate}}</div>
</ds-pagination>
<ds-alert *ngIf="!itemVersion || versions?.page?.length === 0" [content]="'item.version.history.empty'" [type]="AlertTypeEnum.Info"></ds-alert>
<ds-alert *ngIf="!itemVersion || versions?.page?.length === 0" [content]="'item.version.history.empty'"
[type]="AlertTypeEnum.Info"></ds-alert>
</div>
</div>
</div>

View File

@@ -0,0 +1,9 @@
.left-column {
float: left;
text-align: left;
}
.right-column {
float: right;
text-align: right;
}

View File

@@ -2,14 +2,17 @@ import { Component, Input, OnInit } from '@angular/core';
import { Item } from '../../../core/shared/item.model';
import { Version } from '../../../core/shared/version.model';
import { RemoteData } from '../../../core/data/remote-data';
import { BehaviorSubject, combineLatest as observableCombineLatest, Observable } from 'rxjs';
import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs';
import { VersionHistory } from '../../../core/shared/version-history.model';
import {
getAllSucceededRemoteData,
getAllSucceededRemoteDataPayload,
getFirstCompletedRemoteData,
getFirstSucceededRemoteData,
getFirstSucceededRemoteDataPayload,
getRemoteDataPayload
} from '../../../core/shared/operators';
import { map, startWith, switchMap } from 'rxjs/operators';
import { map, startWith, switchMap, take } from 'rxjs/operators';
import { PaginatedList } from '../../../core/data/paginated-list.model';
import { PaginationComponentOptions } from '../../pagination/pagination-component-options.model';
import { VersionHistoryDataService } from '../../../core/data/version-history-data.service';
@@ -19,15 +22,26 @@ import { followLink } from '../../utils/follow-link-config.model';
import { hasValue, hasValueOperator } from '../../empty.util';
import { PaginationService } from '../../../core/pagination/pagination.service';
import { getItemPageRoute } from '../../../item-page/item-page-routing-paths';
import { FormBuilder } from '@angular/forms';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { ItemVersionsSummaryModalComponent } from './item-versions-summary-modal/item-versions-summary-modal.component';
import { NotificationsService } from '../../notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
import { ItemVersionsDeleteModalComponent } from './item-versions-delete-modal/item-versions-delete-modal.component';
import { VersionDataService } from '../../../core/data/version-data.service';
import { ItemDataService } from '../../../core/data/item-data.service';
@Component({
selector: 'ds-item-versions',
templateUrl: './item-versions.component.html'
templateUrl: './item-versions.component.html',
styleUrls: ['./item-versions.component.scss']
})
/**
* Component listing all available versions of the history the provided item is a part of
*/
export class ItemVersionsComponent implements OnInit {
/**
* The item to display a version history for
*/
@@ -45,6 +59,16 @@ export class ItemVersionsComponent implements OnInit {
*/
@Input() displayTitle = true;
/**
* Whether or not to display the action buttons (delete/create/edit version)
*/
@Input() displayActions: boolean;
/**
* Array of active subscriptions
*/
subs: Subscription[] = [];
/**
* The AlertType enumeration
* @type {AlertType}
@@ -81,7 +105,7 @@ export class ItemVersionsComponent implements OnInit {
* The page options to use for fetching the versions
* Start at page 1 and always use the set page size
*/
options = Object.assign(new PaginationComponentOptions(),{
options = Object.assign(new PaginationComponentOptions(), {
id: 'ivo',
currentPage: 1,
pageSize: this.pageSize
@@ -101,11 +125,182 @@ export class ItemVersionsComponent implements OnInit {
[itemId: string]: string
}>;
/**
* Emits when the versionsRD$ must be refreshed
* (should be used when a new version has been created)
*/
refreshSubject = new BehaviorSubject<any>(null);
/**
* The number of the version whose summary is currently being edited
*/
versionBeingEditedNumber: string;
/**
* The id of the version whose summary is currently being edited
*/
versionBeingEditedId: string;
/**
* The summary currently being edited
*/
versionBeingEditedSummary: string;
constructor(private versionHistoryService: VersionHistoryDataService,
private paginationService: PaginationService
private versionService: VersionDataService,
private itemService: ItemDataService,
private paginationService: PaginationService,
private formBuilder: FormBuilder,
private modalService: NgbModal,
private notificationsService: NotificationsService,
private translateService: TranslateService,
// private cacheService: ObjectCacheService,
) {
}
/**
* True when a version is being edited
* (used to disable buttons for other versions)
*/
isAnyBeingEdited(): boolean {
return this.versionBeingEditedNumber != null;
}
/**
* True if the specified version is being edited
* (used to show input field and to change buttons for specified version)
*/
isThisBeingEdited(version): boolean {
return version?.version === this.versionBeingEditedNumber;
}
/**
* Enables editing for the specified version
*/
enableVersionEditing(version): void {
this.versionBeingEditedSummary = version?.summary;
this.versionBeingEditedNumber = version?.version;
this.versionBeingEditedId = version?.id;
}
/**
* Disables editing for the specified version and discards all pending changes
*/
disableSummaryEditing(): void {
this.versionBeingEditedSummary = undefined;
this.versionBeingEditedNumber = undefined;
this.versionBeingEditedId = undefined;
}
/**
* Applies changes to version currently being edited
*/
onSummarySubmit() {
const successMessageKey = 'item.version.edit.notification.success';
const failureMessageKey = 'item.version.edit.notification.failure';
this.versionService.findById(this.versionBeingEditedId).pipe(getFirstSucceededRemoteData()).subscribe(
(findRes) => {
const updatedVersion =
Object.assign({}, findRes.payload, {
summary: this.versionBeingEditedSummary,
});
this.versionService.update(updatedVersion).pipe(take(1)).subscribe(
(updateRes) => {
// TODO check
if (updateRes.hasSucceeded) {
this.notificationsService.success(null, this.translateService.get(successMessageKey, {'version': this.versionBeingEditedNumber}));
} else {
this.notificationsService.warning(null, this.translateService.get(failureMessageKey, {'version': this.versionBeingEditedNumber}));
}
this.disableSummaryEditing();
}
);
}
);
}
/**
* Deletes the specified version
* @param version the version to be deleted
*/
deleteVersion(version) {
const successMessageKey = 'item.version.delete.notification.success';
const failureMessageKey = 'item.version.delete.notification.failure';
const versionNumber = version.version;
// Open modal
const activeModal = this.modalService.open(ItemVersionsDeleteModalComponent);
activeModal.componentInstance.versionNumber = version.version;
activeModal.componentInstance.firstVersion = false;
// On modal submit/dismiss
activeModal.result.then(() => {
version.item.pipe(getFirstSucceededRemoteDataPayload()).subscribe((getItemRes) => {
this.itemService.delete(getItemRes.id).pipe(getFirstCompletedRemoteData()).subscribe(
(deleteItemRes) => {
console.log(JSON.stringify(deleteItemRes));
if (deleteItemRes.hasSucceeded) {
this.notificationsService.success(null, this.translateService.get(successMessageKey, {'version': versionNumber}));
this.refreshSubject.next(null);
} else {
this.notificationsService.error(null, this.translateService.get(failureMessageKey, {'version': versionNumber}));
}
}
);
});
/*version.item.pipe(
getFirstSucceededRemoteDataPayload(),
switchMap((getItemRes) => this.itemService.delete(getItemRes.id))
).subscribe(
getFirstCompletedRemoteData(),
map((deleteItemRes) => {
console.log(JSON.stringify(deleteItemRes));
if (deleteItemRes.hasSucceeded) {
this.notificationsService.success(null, this.translateService.get(successMessageKey, {'version': versionNumber}));
} else {
this.notificationsService.warning(null, this.translateService.get(failureMessageKey, {'version': versionNumber}));
}
}
)
);*/
});
}
/**
* Creates a new version starting from the specified one
* @param version the version from which a new one will be created
*/
createNewVersion(version) {
const successMessageKey = 'item.version.create.notification.success';
const failureMessageKey = 'item.version.create.notification.failure';
const versionNumber = version.version;
// Open modal
const activeModal = this.modalService.open(ItemVersionsSummaryModalComponent);
activeModal.componentInstance.versionNumber = versionNumber;
// On modal submit/dismiss
activeModal.result.then((modalResult) => {
const summary = modalResult;
version.item.pipe(getFirstSucceededRemoteDataPayload()).subscribe((item) => {
const itemHref = item._links.self.href;
this.versionHistoryService.createVersion(itemHref, summary).pipe(take(1)).subscribe((postResult) => {
if (postResult.hasSucceeded) {
const newVersionNumber = postResult.payload.version;
this.notificationsService.success(null, this.translateService.get(successMessageKey, {version: newVersionNumber}));
this.refreshSubject.next(null);
} else {
this.notificationsService.error(null, this.translateService.get(failureMessageKey));
}
});
});
});
}
/**
* Initialize all observables
*/
@@ -124,12 +319,24 @@ export class ItemVersionsComponent implements OnInit {
hasValueOperator(),
);
const currentPagination = this.paginationService.getCurrentPagination(this.options.id, this.options);
this.versionsRD$ = observableCombineLatest(versionHistory$, currentPagination).pipe(
switchMap(([versionHistory, options]: [VersionHistory, PaginationComponentOptions]) =>
this.versionHistoryService.getVersions(versionHistory.id,
new PaginatedSearchOptions({pagination: Object.assign({}, options, { currentPage: options.currentPage })}),
true, true, followLink('item'), followLink('eperson')))
this.versionsRD$ = observableCombineLatest([versionHistory$, currentPagination]).pipe(
switchMap(([versionHistory, options]: [VersionHistory, PaginationComponentOptions]) => {
return this.versionHistoryService.getVersions(versionHistory.id,
new PaginatedSearchOptions({pagination: Object.assign({}, options, {currentPage: options.currentPage})}),
true, true, followLink('item'), followLink('eperson'));
})
);
// Refresh the table when refreshSubject emits
this.subs.push(this.refreshSubject.pipe(switchMap(() => {
return observableCombineLatest([versionHistory$, currentPagination]).pipe(
take(1),
switchMap(([versionHistory, options]: [VersionHistory, PaginationComponentOptions]) => {
return this.versionHistoryService.getVersions(versionHistory.id,
new PaginatedSearchOptions({pagination: Object.assign({}, options, {currentPage: options.currentPage})}),
false, true, followLink('item'), followLink('eperson'));
})
);
})).subscribe());
this.hasEpersons$ = this.versionsRD$.pipe(
getAllSucceededRemoteData(),
getRemoteDataPayload(),
@@ -150,8 +357,15 @@ export class ItemVersionsComponent implements OnInit {
}
ngOnDestroy(): void {
this.cleanupSubscribes();
this.paginationService.clearPagination(this.options.id);
}
/**
* Unsub all subscriptions
*/
cleanupSubscribes() {
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
}
}

View File

@@ -211,6 +211,7 @@ import { CollectionSidebarSearchListElementComponent } from './object-list/sideb
import { CommunitySidebarSearchListElementComponent } from './object-list/sidebar-search-list-element/community/community-sidebar-search-list-element.component';
import { AuthorizedCollectionSelectorComponent } from './dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component';
import { DsoPageEditButtonComponent } from './dso-page/dso-page-edit-button/dso-page-edit-button.component';
import { DsoPageVersionButtonComponent } from './dso-page/dso-page-version-button/dso-page-version-button.component';
import { HoverClassDirective } from './hover-class.directive';
import { ValidationSuggestionsComponent } from './input-suggestions/validation-suggestions/validation-suggestions.component';
import { ItemAlertsComponent } from './item/item-alerts/item-alerts.component';
@@ -233,6 +234,8 @@ import { OnClickMenuItemComponent } from './menu/menu-item/onclick-menu-item.com
import { TextMenuItemComponent } from './menu/menu-item/text-menu-item.component';
import { ThemedConfigurationSearchPageComponent } from '../search-page/themed-configuration-search-page.component';
import { SearchNavbarComponent } from '../search-navbar/search-navbar.component';
import { ItemVersionsSummaryModalComponent } from './item/item-versions/item-versions-summary-modal/item-versions-summary-modal.component';
import { ItemVersionsDeleteModalComponent } from './item/item-versions/item-versions-delete-modal/item-versions-delete-modal.component';
/**
* Declaration needed to make sure all decorator functions are called in time
@@ -534,6 +537,7 @@ const SHARED_ITEM_PAGE_COMPONENTS = [
MetadataFieldWrapperComponent,
MetadataValuesComponent,
DsoPageEditButtonComponent,
DsoPageVersionButtonComponent,
ItemAlertsComponent,
GenericItemPageFieldComponent,
MetadataRepresentationListComponent,
@@ -584,7 +588,9 @@ const DIRECTIVES = [
...COMPONENTS,
...DIRECTIVES,
...SHARED_ITEM_PAGE_COMPONENTS,
...SHARED_SEARCH_PAGE_COMPONENTS
...SHARED_SEARCH_PAGE_COMPONENTS,
ItemVersionsSummaryModalComponent,
ItemVersionsDeleteModalComponent
],
providers: [
...PROVIDERS

View File

@@ -1910,6 +1910,8 @@
"item.page.return": "Back",
"item.page.version": "Create new version",
"item.preview.dc.identifier.uri": "Identifier:",
"item.preview.dc.contributor.author": "Authors:",
@@ -1965,6 +1967,8 @@
"item.version.history.selected": "Selected version",
"item.version.history.selected.alert": "You are currently viewing version {{version}} of the item.",
"item.version.history.table.version": "Version",
"item.version.history.table.item": "Item",
@@ -1975,11 +1979,68 @@
"item.version.history.table.summary": "Summary",
"item.version.history.table.actions": "Action",
"item.version.history.table.action.editSummary": "Edit summary",
"item.version.history.table.action.saveSummary": "Save summary edits",
"item.version.history.table.action.discardSummary": "Discard summary edits",
"item.version.history.table.action.newVersion": "Create new version from this one",
"item.version.history.table.action.deleteVersion": "Delete version",
"item.version.notice": "This is not the latest version of this item. The latest version can be found <a href='{{destination}}'>here</a>.",
"item.version.create.modal.header": "New version",
"item.version.create.modal.text": "Create a new version for this item",
"item.version.create.modal.text.startingFrom": "starting from version {{version}}",
"item.version.create.modal.button.confirm": "Create",
"item.version.create.modal.button.confirm.tooltip": "Create new version",
"item.version.create.modal.button.cancel": "Cancel",
"item.version.create.modal.button.cancel.tooltip": "Do not create new version",
"item.version.create.modal.form.summary.label": "Summary",
"item.version.create.modal.form.summary.placeholder": "Insert the summary for the new version",
"item.version.create.notification.success" : "New version has been created with version number {{version}}",
"item.version.create.notification.failure" : "New version has not been created",
"item.version.delete.modal.header": "Delete version",
"item.version.delete.modal.text": "Do you want to delete version {{version}}?",
"item.version.delete.modal.button.confirm": "Delete",
"item.version.delete.modal.button.confirm.tooltip": "Delete this version",
"item.version.delete.modal.button.cancel": "Cancel",
"item.version.delete.modal.button.cancel.tooltip": "Do not delete this version",
"item.version.delete.notification.success" : "Version number {{version}} has been deleted",
"item.version.delete.notification.failure" : "Version number {{version}} has not been deleted",
"item.version.edit.notification.success" : "The summary of version number {{version}} has been changed",
"item.version.edit.notification.failure" : "The summary of version number {{version}} has not been changed",
"journal.listelement.badge": "Journal",