diff --git a/src/app/core/data/version-history-data.service.ts b/src/app/core/data/version-history-data.service.ts index 8f148f168d..9bcedd6372 100644 --- a/src/app/core/data/version-history-data.service.ts +++ b/src/app/core/data/version-history-data.service.ts @@ -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 { return this.versionDataService.findAllByHref(hrefObs, undefined, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } + + createVersion(itemHref: string, summary: string): Observable> { + 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>; + } } diff --git a/src/app/core/shared/version-history.model.ts b/src/app/core/shared/version-history.model.ts index 85578f20fc..1448317427 100644 --- a/src/app/core/shared/version-history.model.ts +++ b/src/app/core/shared/version-history.model.ts @@ -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 */ diff --git a/src/app/item-page/edit-item-page/item-metadata/item-metadata.component.html b/src/app/item-page/edit-item-page/item-metadata/item-metadata.component.html index e154487402..70cd2aaa39 100644 --- a/src/app/item-page/edit-item-page/item-metadata/item-metadata.component.html +++ b/src/app/item-page/edit-item-page/item-metadata/item-metadata.component.html @@ -1,66 +1,69 @@ diff --git a/src/app/item-page/simple/item-types/publication/publication.component.html b/src/app/item-page/simple/item-types/publication/publication.component.html index 9e61f00e48..72824dd3e7 100644 --- a/src/app/item-page/simple/item-types/publication/publication.component.html +++ b/src/app/item-page/simple/item-types/publication/publication.component.html @@ -3,6 +3,7 @@ {{'publication.page.titleprefix' | translate}}
+
diff --git a/src/app/item-page/simple/item-types/shared/item.component.ts b/src/app/item-page/simple/item-types/shared/item.component.ts index 793af180c9..2b507aa921 100644 --- a/src/app/item-page/simple/item-types/shared/item.component.ts +++ b/src/app/item-page/simple/item-types/shared/item.component.ts @@ -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); } diff --git a/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.html b/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.html index b0157dcfee..3ba7ddafd2 100644 --- a/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.html +++ b/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.html @@ -3,6 +3,7 @@
+
diff --git a/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.ts b/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.ts index c1e67561b2..469f04ffd5 100644 --- a/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.ts +++ b/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.ts @@ -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() { diff --git a/src/app/shared/dso-page/dso-page-version-button/dso-page-version-button.component.html b/src/app/shared/dso-page/dso-page-version-button/dso-page-version-button.component.html new file mode 100644 index 0000000000..1538d3bd89 --- /dev/null +++ b/src/app/shared/dso-page/dso-page-version-button/dso-page-version-button.component.html @@ -0,0 +1,7 @@ + diff --git a/src/app/shared/dso-page/dso-page-version-button/dso-page-version-button.component.scss b/src/app/shared/dso-page/dso-page-version-button/dso-page-version-button.component.scss new file mode 100644 index 0000000000..e8b7d689a3 --- /dev/null +++ b/src/app/shared/dso-page/dso-page-version-button/dso-page-version-button.component.scss @@ -0,0 +1,3 @@ +.btn-dark { + background-color: var(--ds-admin-sidebar-bg); +} diff --git a/src/app/shared/dso-page/dso-page-version-button/dso-page-version-button.component.spec.ts b/src/app/shared/dso-page/dso-page-version-button/dso-page-version-button.component.spec.ts new file mode 100644 index 0000000000..0a210c3233 --- /dev/null +++ b/src/app/shared/dso-page/dso-page-version-button/dso-page-version-button.component.spec.ts @@ -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; + + 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(); + }); + }); +}); diff --git a/src/app/shared/dso-page/dso-page-version-button/dso-page-version-button.component.ts b/src/app/shared/dso-page/dso-page-version-button/dso-page-version-button.component.ts new file mode 100644 index 0000000000..7e12f4e1ff --- /dev/null +++ b/src/app/shared/dso-page/dso-page-version-button/dso-page-version-button.component.ts @@ -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; + + 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); + } + +} diff --git a/src/app/shared/item/item-versions/item-versions-delete-modal/item-versions-delete-modal.component.html b/src/app/shared/item/item-versions/item-versions-delete-modal/item-versions-delete-modal.component.html new file mode 100644 index 0000000000..0c0b72272f --- /dev/null +++ b/src/app/shared/item/item-versions/item-versions-delete-modal/item-versions-delete-modal.component.html @@ -0,0 +1,22 @@ +
+ + + +
diff --git a/src/app/shared/item/item-versions/item-versions-delete-modal/item-versions-delete-modal.component.scss b/src/app/shared/item/item-versions/item-versions-delete-modal/item-versions-delete-modal.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/shared/item/item-versions/item-versions-delete-modal/item-versions-delete-modal.component.spec.ts b/src/app/shared/item/item-versions/item-versions-delete-modal/item-versions-delete-modal.component.spec.ts new file mode 100644 index 0000000000..5ef9ae44d2 --- /dev/null +++ b/src/app/shared/item/item-versions/item-versions-delete-modal/item-versions-delete-modal.component.spec.ts @@ -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; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ItemVersionsDeleteModalComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ItemVersionsDeleteModalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/item/item-versions/item-versions-delete-modal/item-versions-delete-modal.component.ts b/src/app/shared/item/item-versions/item-versions-delete-modal/item-versions-delete-modal.component.ts new file mode 100644 index 0000000000..35618390d9 --- /dev/null +++ b/src/app/shared/item/item-versions/item-versions-delete-modal/item-versions-delete-modal.component.ts @@ -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(); + } + +} diff --git a/src/app/shared/item/item-versions/item-versions-summary-modal/item-versions-summary-modal.component.html b/src/app/shared/item/item-versions/item-versions-summary-modal/item-versions-summary-modal.component.html new file mode 100644 index 0000000000..6065cb289e --- /dev/null +++ b/src/app/shared/item/item-versions/item-versions-summary-modal/item-versions-summary-modal.component.html @@ -0,0 +1,30 @@ +
+ + + +
diff --git a/src/app/shared/item/item-versions/item-versions-summary-modal/item-versions-summary-modal.component.scss b/src/app/shared/item/item-versions/item-versions-summary-modal/item-versions-summary-modal.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/shared/item/item-versions/item-versions-summary-modal/item-versions-summary-modal.component.spec.ts b/src/app/shared/item/item-versions/item-versions-summary-modal/item-versions-summary-modal.component.spec.ts new file mode 100644 index 0000000000..3b88b96f42 --- /dev/null +++ b/src/app/shared/item/item-versions/item-versions-summary-modal/item-versions-summary-modal.component.spec.ts @@ -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; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ItemVersionsSummaryModalComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ItemVersionsSummaryModalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/item/item-versions/item-versions-summary-modal/item-versions-summary-modal.component.ts b/src/app/shared/item/item-versions/item-versions-summary-modal/item-versions-summary-modal.component.ts new file mode 100644 index 0000000000..30dd71ca20 --- /dev/null +++ b/src/app/shared/item/item-versions/item-versions-summary-modal/item-versions-summary-modal.component.ts @@ -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); + } + +} diff --git a/src/app/shared/item/item-versions/item-versions.component.html b/src/app/shared/item/item-versions/item-versions.component.html index 34764e7925..4e488a3e23 100644 --- a/src/app/shared/item/item-versions/item-versions.component.html +++ b/src/app/shared/item/item-versions/item-versions.component.html @@ -2,6 +2,9 @@

{{"item.version.history.head" | translate}}

+ + {{ "item.version.history.selected.alert" | translate : {version: itemVersion.version} }} + - +
- - - - - - - + + + + + + + - - - - + + - - - + + + + +
{{"item.version.history.table.version" | translate}}{{"item.version.history.table.item" | translate}}{{"item.version.history.table.editor" | translate}}{{"item.version.history.table.date" | translate}}{{"item.version.history.table.summary" | translate}}
{{"item.version.history.table.version" | translate}}{{"item.version.history.table.editor" | translate}}{{"item.version.history.table.date" | translate}}{{"item.version.history.table.summary" | translate}}{{"item.version.history.table.actions" | translate}}
{{version?.version}} - - {{item?.handle}} - * - - +
{{version?.version}} {{eperson?.name}} - {{version?.created}}{{version?.summary}}
{{version?.created | date : 'yyyy-MM-dd HH:mm:ss'}} + + {{version?.summary}} + + + + +
+ + + + + + + + +
+
* {{"item.version.history.selected" | translate}}
- +
diff --git a/src/app/shared/item/item-versions/item-versions.component.scss b/src/app/shared/item/item-versions/item-versions.component.scss new file mode 100644 index 0000000000..5594e0cafe --- /dev/null +++ b/src/app/shared/item/item-versions/item-versions.component.scss @@ -0,0 +1,9 @@ +.left-column { + float: left; + text-align: left; +} + +.right-column { + float: right; + text-align: right; +} diff --git a/src/app/shared/item/item-versions/item-versions.component.ts b/src/app/shared/item/item-versions/item-versions.component.ts index 268c6f00db..d68c201359 100644 --- a/src/app/shared/item/item-versions/item-versions.component.ts +++ b/src/app/shared/item/item-versions/item-versions.component.ts @@ -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,9 +125,180 @@ 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(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)); + } + }); + }); + }); } /** @@ -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()); + } } diff --git a/src/app/shared/pagination/pagination.component.html b/src/app/shared/pagination/pagination.component.html index 5f002e55d3..2a9aa1a062 100644 --- a/src/app/shared/pagination/pagination.component.html +++ b/src/app/shared/pagination/pagination.component.html @@ -16,7 +16,7 @@ - + diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 9b993e551f..3945fe36e3 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -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 diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 8908ca42f6..16dbd7dba7 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1250,7 +1250,7 @@ "file-section.error.header": "Error obtaining files for this item", - + "footer.copyright": "copyright © 2002-{{ year }}", @@ -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 here.", + "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",