diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index f165ea6ce2..015f4225b3 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -1128,6 +1128,12 @@ "item.edit.tabs.status.title": "Item Edit - Status", + "item.edit.tabs.versionhistory.head": "Version History", + + "item.edit.tabs.versionhistory.title": "Item Edit - Version History", + + "item.edit.tabs.versionhistory.under-construction": "Editing or adding new versions is not yet possible in this user interface.", + "item.edit.tabs.view.head": "View Item", "item.edit.tabs.view.title": "Item Edit - View", @@ -1207,6 +1213,25 @@ "item.select.table.title": "Title", + "item.version.history.empty": "There are no other versions for this item yet.", + + "item.version.history.head": "Version History", + + "item.version.history.return": "Return", + + "item.version.history.selected": "Selected version", + + "item.version.history.table.version": "Version", + + "item.version.history.table.item": "Item", + + "item.version.history.table.editor": "Editor", + + "item.version.history.table.date": "Date", + + "item.version.history.table.summary": "Summary", + + "journal.listelement.badge": "Journal", diff --git a/src/app/+item-page/edit-item-page/edit-item-page.module.ts b/src/app/+item-page/edit-item-page/edit-item-page.module.ts index 880fd93ec9..d02aafcfa1 100644 --- a/src/app/+item-page/edit-item-page/edit-item-page.module.ts +++ b/src/app/+item-page/edit-item-page/edit-item-page.module.ts @@ -29,6 +29,7 @@ import { DragDropModule } from '@angular/cdk/drag-drop'; import { ItemEditBitstreamDragHandleComponent } from './item-bitstreams/item-edit-bitstream-drag-handle/item-edit-bitstream-drag-handle.component'; import { PaginatedDragAndDropBitstreamListComponent } from './item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component'; import { VirtualMetadataComponent } from './virtual-metadata/virtual-metadata.component'; +import { ItemVersionHistoryComponent } from './item-version-history/item-version-history.component'; /** * Module that contains all components related to the Edit Item page administrator functionality @@ -56,6 +57,7 @@ import { VirtualMetadataComponent } from './virtual-metadata/virtual-metadata.co ItemMetadataComponent, ItemRelationshipsComponent, ItemBitstreamsComponent, + ItemVersionHistoryComponent, EditInPlaceFieldComponent, ItemEditBitstreamComponent, ItemEditBitstreamBundleComponent, diff --git a/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts b/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts index 84cdeec3d0..e4b1b06730 100644 --- a/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts +++ b/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts @@ -1,4 +1,3 @@ -import { ItemPageResolver } from '../item-page.resolver'; import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { EditItemPageComponent } from './edit-item-page.component'; @@ -14,6 +13,7 @@ import { ItemCollectionMapperComponent } from './item-collection-mapper/item-col import { ItemMoveComponent } from './item-move/item-move.component'; import { ItemRelationshipsComponent } from './item-relationships/item-relationships.component'; import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { ItemVersionHistoryComponent } from './item-version-history/item-version-history.component'; export const ITEM_EDIT_WITHDRAW_PATH = 'withdraw'; export const ITEM_EDIT_REINSTATE_PATH = 'reinstate'; @@ -75,6 +75,11 @@ export const ITEM_EDIT_MOVE_PATH = 'move'; /* TODO - change when curate page exists */ component: ItemBitstreamsComponent, data: { title: 'item.edit.tabs.curate.title', showBreadcrumbs: true } + }, + { + path: 'versionhistory', + component: ItemVersionHistoryComponent, + data: { title: 'item.edit.tabs.versionhistory.title', showBreadcrumbs: true } } ] }, diff --git a/src/app/+item-page/edit-item-page/item-version-history/item-version-history.component.html b/src/app/+item-page/edit-item-page/item-version-history/item-version-history.component.html new file mode 100644 index 0000000000..acabbd1010 --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-version-history/item-version-history.component.html @@ -0,0 +1,6 @@ +
+ +
+
+ +
diff --git a/src/app/+item-page/edit-item-page/item-version-history/item-version-history.component.spec.ts b/src/app/+item-page/edit-item-page/item-version-history/item-version-history.component.spec.ts new file mode 100644 index 0000000000..9bc39649f4 --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-version-history/item-version-history.component.spec.ts @@ -0,0 +1,44 @@ +import { ItemVersionHistoryComponent } from './item-version-history.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { VarDirective } from '../../../shared/utils/var.directive'; +import { RouterTestingModule } from '@angular/router/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { Item } from '../../../core/shared/item.model'; +import { ActivatedRoute } from '@angular/router'; +import { of as observableOf } from 'rxjs'; +import { createSuccessfulRemoteDataObject } from '../../../shared/testing/utils'; + +describe('ItemVersionHistoryComponent', () => { + let component: ItemVersionHistoryComponent; + let fixture: ComponentFixture; + + const item = Object.assign(new Item(), { + uuid: 'item-identifier-1', + handle: '123456789/1', + }); + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ItemVersionHistoryComponent, VarDirective], + imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])], + providers: [ + { provide: ActivatedRoute, useValue: { parent: { data: observableOf({ item: createSuccessfulRemoteDataObject(item) }) } } } + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ItemVersionHistoryComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should initialize the itemRD$ from the route\'s data', (done) => { + component.itemRD$.subscribe((itemRD) => { + expect(itemRD.payload).toBe(item); + done(); + }); + }); +}); diff --git a/src/app/+item-page/edit-item-page/item-version-history/item-version-history.component.ts b/src/app/+item-page/edit-item-page/item-version-history/item-version-history.component.ts new file mode 100644 index 0000000000..ce662c5753 --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-version-history/item-version-history.component.ts @@ -0,0 +1,35 @@ +import { Component } from '@angular/core'; +import { Observable } from 'rxjs/internal/Observable'; +import { RemoteData } from '../../../core/data/remote-data'; +import { Item } from '../../../core/shared/item.model'; +import { map } from 'rxjs/operators'; +import { getSucceededRemoteData } from '../../../core/shared/operators'; +import { ActivatedRoute } from '@angular/router'; +import { AlertType } from '../../../shared/alert/aletr-type'; + +@Component({ + selector: 'ds-item-version-history', + templateUrl: './item-version-history.component.html' +}) +/** + * Component for listing and managing an item's version history + */ +export class ItemVersionHistoryComponent { + /** + * The item to display the version history for + */ + itemRD$: Observable>; + + /** + * The AlertType enumeration + * @type {AlertType} + */ + AlertTypeEnum = AlertType; + + constructor(private route: ActivatedRoute) { + } + + ngOnInit(): void { + this.itemRD$ = this.route.parent.data.pipe(map((data) => data.item)).pipe(getSucceededRemoteData()) as Observable>; + } +} diff --git a/src/app/+item-page/full/full-item-page.component.html b/src/app/+item-page/full/full-item-page.component.html index c453df6bff..b93b8f1e12 100644 --- a/src/app/+item-page/full/full-item-page.component.html +++ b/src/app/+item-page/full/full-item-page.component.html @@ -21,6 +21,7 @@ + diff --git a/src/app/+item-page/item-page.module.ts b/src/app/+item-page/item-page.module.ts index be7885340f..4c3a64e117 100644 --- a/src/app/+item-page/item-page.module.ts +++ b/src/app/+item-page/item-page.module.ts @@ -61,7 +61,7 @@ import { AbstractIncrementalListComponent } from './simple/abstract-incremental- RelatedEntitiesSearchComponent, UploadBitstreamComponent, TabbedRelatedEntitiesSearchComponent, - AbstractIncrementalListComponent + AbstractIncrementalListComponent, ], exports: [ ItemComponent, diff --git a/src/app/+item-page/item-page.resolver.ts b/src/app/+item-page/item-page.resolver.ts index 0f73dc6170..501bb34d2c 100644 --- a/src/app/+item-page/item-page.resolver.ts +++ b/src/app/+item-page/item-page.resolver.ts @@ -28,6 +28,7 @@ export class ItemPageResolver implements Resolve> { followLink('owningCollection'), followLink('bundles'), followLink('relationships'), + followLink('version', undefined, true, followLink('versionhistory')), ).pipe( find((RD) => hasValue(RD.error) || RD.hasSucceeded), ); diff --git a/src/app/+item-page/simple/item-page.component.html b/src/app/+item-page/simple/item-page.component.html index b4b32fb05c..3b942f9d33 100644 --- a/src/app/+item-page/simple/item-page.component.html +++ b/src/app/+item-page/simple/item-page.component.html @@ -3,6 +3,7 @@
+
diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 9180b0882a..83ff85d55f 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -144,6 +144,10 @@ import { PoolTaskDataService } from './tasks/pool-task-data.service'; import { TaskResponseParsingService } from './tasks/task-response-parsing.service'; import { ArrayMoveChangeAnalyzer } from './data/array-move-change-analyzer.service'; import { BitstreamDataService } from './data/bitstream-data.service'; +import { VersionDataService } from './data/version-data.service'; +import { VersionHistoryDataService } from './data/version-history-data.service'; +import { Version } from './shared/version.model'; +import { VersionHistory } from './shared/version-history.model'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -259,6 +263,8 @@ const PROVIDERS = [ RelationshipTypeService, ExternalSourceService, LookupRelationService, + VersionDataService, + VersionHistoryDataService, LicenseDataService, ItemTypeDataService, // register AuthInterceptor as HttpInterceptor @@ -309,6 +315,8 @@ export const models = ItemType, ExternalSource, ExternalSourceEntry, + Version, + VersionHistory ]; @NgModule({ diff --git a/src/app/core/data/version-data.service.ts b/src/app/core/data/version-data.service.ts new file mode 100644 index 0000000000..80fb4f5b80 --- /dev/null +++ b/src/app/core/data/version-data.service.ts @@ -0,0 +1,44 @@ +import { Injectable } from '@angular/core'; +import { DataService } from './data.service'; +import { Version } from '../shared/version.model'; +import { RequestService } from './request.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { Store } from '@ngrx/store'; +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 { DefaultChangeAnalyzer } from './default-change-analyzer.service'; +import { FindListOptions } from './request.models'; +import { Observable } from 'rxjs/internal/Observable'; +import { dataService } from '../cache/builders/build-decorators'; +import { VERSION } from '../shared/version.resource-type'; + +/** + * Service responsible for handling requests related to the Version object + */ +@Injectable() +@dataService(VERSION) +export class VersionDataService extends DataService { + protected linkPath = 'versions'; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DefaultChangeAnalyzer) { + super(); + } + + /** + * Get the endpoint for browsing versions + */ + getBrowseEndpoint(options: FindListOptions = {}, linkPath?: string): Observable { + return this.halService.getEndpoint(this.linkPath); + } +} diff --git a/src/app/core/data/version-history-data.service.spec.ts b/src/app/core/data/version-history-data.service.spec.ts new file mode 100644 index 0000000000..6728df71f1 --- /dev/null +++ b/src/app/core/data/version-history-data.service.spec.ts @@ -0,0 +1,54 @@ +import { RequestService } from './request.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { VersionHistoryDataService } from './version-history-data.service'; +import { NotificationsServiceStub } from '../../shared/testing/notifications-service-stub'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; +import { getMockRequestService } from '../../shared/mocks/mock-request.service'; +import { GetRequest } from './request.models'; + +const url = 'fake-url'; + +describe('VersionHistoryDataService', () => { + let service: VersionHistoryDataService; + + let requestService: RequestService; + let notificationsService: any; + let rdbService: RemoteDataBuildService; + let objectCache: ObjectCacheService; + let halService: any; + + beforeEach(() => { + createService(); + }); + + describe('getVersions', () => { + let result; + + beforeEach(() => { + result = service.getVersions('1'); + }); + + it('should configure a GET request', () => { + expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(GetRequest)); + }); + }); + + /** + * Create a VersionHistoryDataService used for testing + * @param requestEntry$ Supply a requestEntry to be returned by the REST API (optional) + */ + function createService(requestEntry$?) { + requestService = getMockRequestService(requestEntry$); + rdbService = jasmine.createSpyObj('rdbService', { + buildList: jasmine.createSpy('buildList') + }); + objectCache = jasmine.createSpyObj('objectCache', { + remove: jasmine.createSpy('remove') + }); + halService = new HALEndpointServiceStub(url); + notificationsService = new NotificationsServiceStub(); + + service = new VersionHistoryDataService(requestService, rdbService, null, objectCache, halService, notificationsService, null, null); + } +}); diff --git a/src/app/core/data/version-history-data.service.ts b/src/app/core/data/version-history-data.service.ts new file mode 100644 index 0000000000..b4107d629d --- /dev/null +++ b/src/app/core/data/version-history-data.service.ts @@ -0,0 +1,81 @@ +import { DataService } from './data.service'; +import { VersionHistory } from '../shared/version-history.model'; +import { Injectable } from '@angular/core'; +import { RequestService } from './request.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { Store } from '@ngrx/store'; +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 { DefaultChangeAnalyzer } from './default-change-analyzer.service'; +import { FindListOptions, GetRequest } from './request.models'; +import { Observable } from 'rxjs/internal/Observable'; +import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; +import { RemoteData } from './remote-data'; +import { PaginatedList } from './paginated-list'; +import { Version } from '../shared/version.model'; +import { map, switchMap, take } from 'rxjs/operators'; +import { dataService } from '../cache/builders/build-decorators'; +import { VERSION_HISTORY } from '../shared/version-history.resource-type'; +import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; + +/** + * Service responsible for handling requests related to the VersionHistory object + */ +@Injectable() +@dataService(VERSION_HISTORY) +export class VersionHistoryDataService extends DataService { + protected linkPath = 'versionhistories'; + protected versionsEndpoint = 'versions'; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DefaultChangeAnalyzer) { + super(); + } + + /** + * Get the endpoint for browsing versions + */ + getBrowseEndpoint(options: FindListOptions = {}, linkPath?: string): Observable { + return this.halService.getEndpoint(this.linkPath); + } + + /** + * Get the versions endpoint for a version history + * @param versionHistoryId + */ + getVersionsEndpoint(versionHistoryId: string): Observable { + return this.getBrowseEndpoint().pipe( + switchMap((href: string) => this.halService.getEndpoint(this.versionsEndpoint, `${href}/${versionHistoryId}`)) + ); + } + + /** + * Get a version history's versions using paginated search options + * @param versionHistoryId The version history's ID + * @param searchOptions The search options to use + * @param linksToFollow HAL Links to follow on the Versions + */ + getVersions(versionHistoryId: string, searchOptions?: PaginatedSearchOptions, ...linksToFollow: Array>): Observable>> { + const hrefObs = this.getVersionsEndpoint(versionHistoryId).pipe( + map((href) => searchOptions ? searchOptions.toRestUrl(href) : href) + ); + hrefObs.pipe( + take(1) + ).subscribe((href) => { + const request = new GetRequest(this.requestService.generateRequestId(), href); + this.requestService.configure(request); + }); + + return this.rdbService.buildList(hrefObs, ...linksToFollow); + } +} diff --git a/src/app/core/shared/item.model.ts b/src/app/core/shared/item.model.ts index e7f0ae9e10..7f6cf9fe13 100644 --- a/src/app/core/shared/item.model.ts +++ b/src/app/core/shared/item.model.ts @@ -18,6 +18,8 @@ import { Relationship } from './item-relationships/relationship.model'; import { RELATIONSHIP } from './item-relationships/relationship.resource-type'; import { ITEM } from './item.resource-type'; import { ChildHALResource } from './child-hal-resource.model'; +import { Version } from './version.model'; +import { VERSION } from './version.resource-type'; /** * Class representing a DSpace Item @@ -67,6 +69,7 @@ export class Item extends DSpaceObject implements ChildHALResource { bundles: HALLink; owningCollection: HALLink; templateItemOf: HALLink; + version: HALLink; self: HALLink; }; @@ -77,6 +80,13 @@ export class Item extends DSpaceObject implements ChildHALResource { @link(COLLECTION) owningCollection?: Observable>; + /** + * The version this item represents in its history + * Will be undefined unless the version {@link HALLink} has been resolved. + */ + @link(VERSION) + version?: Observable>; + /** * The list of Bundles inside this Item * Will be undefined unless the bundles {@link HALLink} has been resolved. diff --git a/src/app/core/shared/version-history.model.ts b/src/app/core/shared/version-history.model.ts new file mode 100644 index 0000000000..a8ce982fb2 --- /dev/null +++ b/src/app/core/shared/version-history.model.ts @@ -0,0 +1,39 @@ +import { deserialize, autoserialize, inheritSerialization } from 'cerialize'; +import { excludeFromEquals } from '../utilities/equals.decorators'; +import { Observable } from 'rxjs/internal/Observable'; +import { RemoteData } from '../data/remote-data'; +import { PaginatedList } from '../data/paginated-list'; +import { Version } from './version.model'; +import { VERSION_HISTORY } from './version-history.resource-type'; +import { link, typedObject } from '../cache/builders/build-decorators'; +import { DSpaceObject } from './dspace-object.model'; +import { HALLink } from './hal-link.model'; +import { VERSION } from './version.resource-type'; + +/** + * Class representing a DSpace Version History + */ +@typedObject +@inheritSerialization(DSpaceObject) +export class VersionHistory extends DSpaceObject { + static type = VERSION_HISTORY; + + @deserialize + _links: { + self: HALLink; + versions: HALLink; + }; + + /** + * The identifier of this Version History + */ + @autoserialize + id: string; + + /** + * The list of versions within this history + */ + @excludeFromEquals + @link(VERSION, true) + versions: Observable>>; +} diff --git a/src/app/core/shared/version-history.resource-type.ts b/src/app/core/shared/version-history.resource-type.ts new file mode 100644 index 0000000000..c6d92ce138 --- /dev/null +++ b/src/app/core/shared/version-history.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from './resource-type'; + +/** + * The resource type for VersionHistory + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const VERSION_HISTORY = new ResourceType('versionhistory'); diff --git a/src/app/core/shared/version.model.ts b/src/app/core/shared/version.model.ts new file mode 100644 index 0000000000..6e109ba9c2 --- /dev/null +++ b/src/app/core/shared/version.model.ts @@ -0,0 +1,76 @@ +import { deserialize, autoserialize, inheritSerialization } from 'cerialize'; +import { excludeFromEquals } from '../utilities/equals.decorators'; +import { Item } from './item.model'; +import { RemoteData } from '../data/remote-data'; +import { Observable } from 'rxjs/internal/Observable'; +import { VersionHistory } from './version-history.model'; +import { EPerson } from '../eperson/models/eperson.model'; +import { VERSION } from './version.resource-type'; +import { HALLink } from './hal-link.model'; +import { link, typedObject } from '../cache/builders/build-decorators'; +import { VERSION_HISTORY } from './version-history.resource-type'; +import { ITEM } from './item.resource-type'; +import { EPERSON } from '../eperson/models/eperson.resource-type'; +import { DSpaceObject } from './dspace-object.model'; + +/** + * Class representing a DSpace Version + */ +@typedObject +@inheritSerialization(DSpaceObject) +export class Version extends DSpaceObject { + static type = VERSION; + + @deserialize + _links: { + self: HALLink; + item: HALLink; + versionhistory: HALLink; + eperson: HALLink; + }; + + /** + * The identifier of this Version + */ + @autoserialize + id: string; + + /** + * The version number of the version's history this version represents + */ + @autoserialize + version: number; + + /** + * The summary for the changes made in this version + */ + @autoserialize + summary: string; + + /** + * The Date this version was created + */ + @deserialize + created: Date; + + /** + * The full version history this version is apart of + */ + @excludeFromEquals + @link(VERSION_HISTORY) + versionhistory: Observable>; + + /** + * The item this version represents + */ + @excludeFromEquals + @link(ITEM) + item: Observable>; + + /** + * The e-person who created this version + */ + @excludeFromEquals + @link(EPERSON) + eperson: Observable>; +} diff --git a/src/app/core/shared/version.resource-type.ts b/src/app/core/shared/version.resource-type.ts new file mode 100644 index 0000000000..ac0f56239e --- /dev/null +++ b/src/app/core/shared/version.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from './resource-type'; + +/** + * The resource type for Version + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const VERSION = new ResourceType('version'); diff --git a/src/app/shared/item/item-versions/item-versions.component.html b/src/app/shared/item/item-versions/item-versions.component.html new file mode 100644 index 0000000000..6e93f4c7ca --- /dev/null +++ b/src/app/shared/item/item-versions/item-versions.component.html @@ -0,0 +1,47 @@ +
+
+
+

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

+ + + + + + + + + + + + + + + + + + + + +
{{"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}}
{{version?.version}} + + {{item?.handle}} + * + + + + {{eperson?.name}} + + {{version?.created}}{{version?.summary}}
+
* {{"item.version.history.selected" | translate}}
+
+ +
+
+
diff --git a/src/app/shared/item/item-versions/item-versions.component.spec.ts b/src/app/shared/item/item-versions/item-versions.component.spec.ts new file mode 100644 index 0000000000..18fa4cf983 --- /dev/null +++ b/src/app/shared/item/item-versions/item-versions.component.spec.ts @@ -0,0 +1,121 @@ +import { ItemVersionsComponent } from './item-versions.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { VarDirective } from '../../utils/var.directive'; +import { TranslateModule } from '@ngx-translate/core'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { Item } from '../../../core/shared/item.model'; +import { Version } from '../../../core/shared/version.model'; +import { VersionHistory } from '../../../core/shared/version-history.model'; +import { createPaginatedList, createSuccessfulRemoteDataObject$ } from '../../testing/utils'; +import { VersionHistoryDataService } from '../../../core/data/version-history-data.service'; +import { By } from '@angular/platform-browser'; + +describe('ItemVersionsComponent', () => { + let component: ItemVersionsComponent; + let fixture: ComponentFixture; + + const versionHistory = Object.assign(new VersionHistory(), { + id: '1' + }); + const version1 = Object.assign(new Version(), { + id: '1', + version: 1, + created: new Date(2020, 1, 1), + summary: 'first version', + versionhistory: createSuccessfulRemoteDataObject$(versionHistory) + }); + const version2 = Object.assign(new Version(), { + id: '2', + version: 2, + summary: 'second version', + created: new Date(2020, 1, 2), + versionhistory: createSuccessfulRemoteDataObject$(versionHistory) + }); + const versions = [version1, version2]; + versionHistory.versions = createSuccessfulRemoteDataObject$(createPaginatedList(versions)); + const item1 = Object.assign(new Item(), { + uuid: 'item-identifier-1', + handle: '123456789/1', + version: createSuccessfulRemoteDataObject$(version1) + }); + const item2 = Object.assign(new Item(), { + uuid: 'item-identifier-2', + handle: '123456789/2', + version: createSuccessfulRemoteDataObject$(version2) + }); + const items = [item1, item2]; + version1.item = createSuccessfulRemoteDataObject$(item1); + version2.item = createSuccessfulRemoteDataObject$(item2); + const versionHistoryService = jasmine.createSpyObj('versionHistoryService', { + getVersions: createSuccessfulRemoteDataObject$(createPaginatedList(versions)) + }); + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ItemVersionsComponent, VarDirective], + imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])], + providers: [ + { provide: VersionHistoryDataService, useValue: versionHistoryService } + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ItemVersionsComponent); + component = fixture.componentInstance; + component.item = item1; + fixture.detectChanges(); + }); + + it(`should display ${versions.length} rows`, () => { + const rows = fixture.debugElement.queryAll(By.css('tbody tr')); + expect(rows.length).toBe(versions.length); + }); + + versions.forEach((version: Version, index: number) => { + const versionItem = items[index]; + + it(`should display version ${version.version} in the correct column for version ${version.id}`, () => { + const id = fixture.debugElement.query(By.css(`#version-row-${version.id} .version-row-element-version`)); + expect(id.nativeElement.textContent).toEqual('' + version.version); + }); + + it(`should display item handle ${versionItem.handle} in the correct column for version ${version.id}`, () => { + const item = fixture.debugElement.query(By.css(`#version-row-${version.id} .version-row-element-item`)); + expect(item.nativeElement.textContent).toContain(versionItem.handle); + }); + + // This version's item is equal to the component's item (the selected item) + // Check if the handle contains an asterisk + if (item1.uuid === versionItem.uuid) { + it('should add an asterisk to the handle of the selected item', () => { + const item = fixture.debugElement.query(By.css(`#version-row-${version.id} .version-row-element-item`)); + expect(item.nativeElement.textContent).toContain('*'); + }); + } + + it(`should display date ${version.created} in the correct column for version ${version.id}`, () => { + const date = fixture.debugElement.query(By.css(`#version-row-${version.id} .version-row-element-date`)); + expect(date.nativeElement.textContent).toEqual('' + version.created); + }); + + it(`should display summary ${version.summary} in the correct column for version ${version.id}`, () => { + const summary = fixture.debugElement.query(By.css(`#version-row-${version.id} .version-row-element-summary`)); + expect(summary.nativeElement.textContent).toEqual(version.summary); + }); + }); + + describe('switchPage', () => { + const page = 5; + + beforeEach(() => { + component.switchPage(page); + }); + + it('should set the option\'s currentPage to the new page', () => { + expect(component.options.currentPage).toEqual(page); + }); + }); +}); diff --git a/src/app/shared/item/item-versions/item-versions.component.ts b/src/app/shared/item/item-versions/item-versions.component.ts new file mode 100644 index 0000000000..684599d3b5 --- /dev/null +++ b/src/app/shared/item/item-versions/item-versions.component.ts @@ -0,0 +1,130 @@ +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 { Observable } from 'rxjs/internal/Observable'; +import { VersionHistory } from '../../../core/shared/version-history.model'; +import { getAllSucceededRemoteData, getRemoteDataPayload } from '../../../core/shared/operators'; +import { map, startWith, switchMap } from 'rxjs/operators'; +import { combineLatest as observableCombineLatest } from 'rxjs'; +import { PaginatedList } from '../../../core/data/paginated-list'; +import { PaginationComponentOptions } from '../../pagination/pagination-component-options.model'; +import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; +import { VersionHistoryDataService } from '../../../core/data/version-history-data.service'; +import { PaginatedSearchOptions } from '../../search/paginated-search-options.model'; +import { AlertType } from '../../alert/aletr-type'; +import { followLink } from '../../utils/follow-link-config.model'; + +@Component({ + selector: 'ds-item-versions', + templateUrl: './item-versions.component.html' +}) +/** + * 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 + */ + @Input() item: Item; + + /** + * An option to display the list of versions, even when there aren't any. + * Instead of the table, an alert will be displayed, notifying the user there are no other versions present + * for the current item. + */ + @Input() displayWhenEmpty = false; + + /** + * Whether or not to display the title + */ + @Input() displayTitle = true; + + /** + * The AlertType enumeration + * @type {AlertType} + */ + AlertTypeEnum = AlertType; + + /** + * The item's version + */ + versionRD$: Observable>; + + /** + * The item's full version history + */ + versionHistoryRD$: Observable>; + + /** + * The version history's list of versions + */ + versionsRD$: Observable>>; + + /** + * Verify if the list of versions has at least one e-person to display + * Used to hide the "Editor" column when no e-persons are present to display + */ + hasEpersons$: Observable; + + /** + * The amount of versions to display per page + */ + pageSize = 10; + + /** + * 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(),{ + id: 'item-versions-options', + currentPage: 1, + pageSize: this.pageSize + }); + + /** + * The current page being displayed + */ + currentPage$ = new BehaviorSubject(1); + + constructor(private versionHistoryService: VersionHistoryDataService) { + } + + /** + * Initialize all observables + */ + ngOnInit(): void { + this.versionRD$ = this.item.version; + this.versionHistoryRD$ = this.versionRD$.pipe( + getAllSucceededRemoteData(), + getRemoteDataPayload(), + switchMap((version: Version) => version.versionhistory) + ); + const versionHistory$ = this.versionHistoryRD$.pipe( + getAllSucceededRemoteData(), + getRemoteDataPayload(), + ); + this.versionsRD$ = observableCombineLatest(versionHistory$, this.currentPage$).pipe( + switchMap(([versionHistory, page]: [VersionHistory, number]) => + this.versionHistoryService.getVersions(versionHistory.id, + new PaginatedSearchOptions({pagination: Object.assign({}, this.options, { currentPage: page })}), + followLink('item'), followLink('eperson'))) + ); + this.hasEpersons$ = this.versionsRD$.pipe( + getAllSucceededRemoteData(), + getRemoteDataPayload(), + map((versions: PaginatedList) => versions.page.filter((version: Version) => version.eperson !== undefined).length > 0), + startWith(false) + ); + } + + /** + * Update the current page + * @param page + */ + switchPage(page: number) { + this.options.currentPage = page; + this.currentPage$.next(page); + } + +} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 8771be805f..b8cf064bc4 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -176,6 +176,7 @@ import { ExternalSourceEntryImportModalComponent } from './form/builder/ds-dynam import { ImportableListItemControlComponent } from './object-collection/shared/importable-list-item-control/importable-list-item-control.component'; import { DragDropModule } from '@angular/cdk/drag-drop'; import { ExistingMetadataListElementComponent } from './form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component'; +import { ItemVersionsComponent } from './item/item-versions/item-versions.component'; import { SortablejsModule } from 'ngx-sortablejs'; import { CustomSwitchComponent } from './form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component'; import { BundleListElementComponent } from './object-list/bundle-list-element/bundle-list-element.component'; @@ -349,6 +350,7 @@ const COMPONENTS = [ ExternalSourceEntryImportModalComponent, ImportableListItemControlComponent, ExistingMetadataListElementComponent, + ItemVersionsComponent, PublicationSearchResultListElementComponent, ]; @@ -414,6 +416,7 @@ const ENTRY_COMPONENTS = [ DsDynamicLookupRelationSelectionTabComponent, DsDynamicLookupRelationExternalSourceTabComponent, ExternalSourceEntryImportModalComponent, + ItemVersionsComponent, BundleListElementComponent ];