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 @@
+
+
+
0 || displayWhenEmpty">
+
{{"item.version.history.head" | translate}}
+
0"
+ [hideGear]="true"
+ [hidePagerWhenSinglePage]="true"
+ [paginationOptions]="options"
+ [pageInfoState]="versions"
+ [collectionSize]="versions?.totalElements"
+ [disableRouteParameterUpdate]="true"
+ (pageChange)="switchPage($event)">
+
+
+
+ {{"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
];