Skip to content
Snippets Groups Projects
Unverified Commit 103605c3 authored by Tim Donohue's avatar Tim Donohue Committed by GitHub
Browse files

Merge pull request #585 from atmire/List-version-history

List item version history
parents c911ec90 e8049aac
No related merge requests found
Showing
with 499 additions and 2 deletions
......@@ -1005,6 +1005,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",
......@@ -1084,6 +1090,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",
......
......@@ -22,6 +22,7 @@ import { EditRelationshipComponent } from './item-relationships/edit-relationshi
import { EditRelationshipListComponent } from './item-relationships/edit-relationship-list/edit-relationship-list.component';
import { ItemMoveComponent } from './item-move/item-move.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
......@@ -47,6 +48,7 @@ import { VirtualMetadataComponent } from './virtual-metadata/virtual-metadata.co
ItemMetadataComponent,
ItemRelationshipsComponent,
ItemBitstreamsComponent,
ItemVersionHistoryComponent,
EditInPlaceFieldComponent,
EditRelationshipComponent,
EditRelationshipListComponent,
......
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 }
}
]
},
......
<div class="mt-4">
<ds-alert [content]="'item.edit.tabs.versionhistory.under-construction'" [type]="AlertTypeEnum.Warning"></ds-alert>
</div>
<div class="mt-2" *ngVar="(itemRD$ | async)?.payload as item">
<ds-item-versions *ngIf="item" [item]="item" [displayWhenEmpty]="true" [displayTitle]="false"></ds-item-versions>
</div>
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<ItemVersionHistoryComponent>;
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();
});
});
});
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<RemoteData<Item>>;
/**
* 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<RemoteData<Item>>;
}
}
......@@ -21,6 +21,7 @@
</table>
<ds-item-page-full-file-section [item]="item"></ds-item-page-full-file-section>
<ds-item-page-collections [item]="item"></ds-item-page-collections>
<ds-item-versions class="mt-2" [item]="item"></ds-item-versions>
</div>
</div>
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.item' | translate}}"></ds-error>
......
......@@ -59,7 +59,7 @@ import { AbstractIncrementalListComponent } from './simple/abstract-incremental-
MetadataRepresentationListComponent,
RelatedEntitiesSearchComponent,
TabbedRelatedEntitiesSearchComponent,
AbstractIncrementalListComponent
AbstractIncrementalListComponent,
],
exports: [
ItemComponent,
......
......@@ -28,6 +28,7 @@ export class ItemPageResolver implements Resolve<RemoteData<Item>> {
followLink('owningCollection'),
followLink('bundles'),
followLink('relationships'),
followLink('version', undefined, true, followLink('versionhistory')),
).pipe(
find((RD) => hasValue(RD.error) || RD.hasSucceeded),
);
......
......@@ -3,6 +3,7 @@
<div *ngIf="itemRD?.payload as item">
<ds-view-tracker [object]="item"></ds-view-tracker>
<ds-listable-object-component-loader [object]="item" [viewMode]="viewMode"></ds-listable-object-component-loader>
<ds-item-versions class="mt-2" [item]="item"></ds-item-versions>
</div>
</div>
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.item' | translate}}"></ds-error>
......
......@@ -142,6 +142,10 @@ import { PoolTask } from './tasks/models/pool-task-object.model';
import { TaskObject } from './tasks/models/task-object.model';
import { PoolTaskDataService } from './tasks/pool-task-data.service';
import { TaskResponseParsingService } from './tasks/task-response-parsing.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
......@@ -255,6 +259,8 @@ const PROVIDERS = [
RelationshipTypeService,
ExternalSourceService,
LookupRelationService,
VersionDataService,
VersionHistoryDataService,
LicenseDataService,
ItemTypeDataService,
// register AuthInterceptor as HttpInterceptor
......@@ -305,6 +311,8 @@ export const models =
ItemType,
ExternalSource,
ExternalSourceEntry,
Version,
VersionHistory
];
@NgModule({
......
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<Version> {
protected linkPath = 'versions';
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected store: Store<CoreState>,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected comparator: DefaultChangeAnalyzer<Version>) {
super();
}
/**
* Get the endpoint for browsing versions
*/
getBrowseEndpoint(options: FindListOptions = {}, linkPath?: string): Observable<string> {
return this.halService.getEndpoint(this.linkPath);
}
}
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);
}
});
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<VersionHistory> {
protected linkPath = 'versionhistories';
protected versionsEndpoint = 'versions';
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected store: Store<CoreState>,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected comparator: DefaultChangeAnalyzer<VersionHistory>) {
super();
}
/**
* Get the endpoint for browsing versions
*/
getBrowseEndpoint(options: FindListOptions = {}, linkPath?: string): Observable<string> {
return this.halService.getEndpoint(this.linkPath);
}
/**
* Get the versions endpoint for a version history
* @param versionHistoryId
*/
getVersionsEndpoint(versionHistoryId: string): Observable<string> {
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<FollowLinkConfig<Version>>): Observable<RemoteData<PaginatedList<Version>>> {
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<Version>(hrefObs, ...linksToFollow);
}
}
......@@ -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<RemoteData<Collection>>;
/**
* The version this item represents in its history
* Will be undefined unless the version {@link HALLink} has been resolved.
*/
@link(VERSION)
version?: Observable<RemoteData<Version>>;
/**
* The list of Bundles inside this Item
* Will be undefined unless the bundles {@link HALLink} has been resolved.
......
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<RemoteData<PaginatedList<Version>>>;
}
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');
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<RemoteData<VersionHistory>>;
/**
* The item this version represents
*/
@excludeFromEquals
@link(ITEM)
item: Observable<RemoteData<Item>>;
/**
* The e-person who created this version
*/
@excludeFromEquals
@link(EPERSON)
eperson: Observable<RemoteData<EPerson>>;
}
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');
<div *ngVar="(versionsRD$ | async)?.payload as versions">
<div *ngVar="(versionRD$ | async)?.payload as itemVersion">
<div class="mb-2" *ngIf="versions?.page?.length > 0 || displayWhenEmpty">
<h2 *ngIf="displayTitle">{{"item.version.history.head" | translate}}</h2>
<ds-pagination *ngIf="versions?.page?.length > 0"
[hideGear]="true"
[hidePagerWhenSinglePage]="true"
[paginationOptions]="options"
[pageInfoState]="versions"
[collectionSize]="versions?.totalElements"
[disableRouteParameterUpdate]="true"
(pageChange)="switchPage($event)">
<table class="table table-striped my-2">
<thead>
<tr>
<th scope="col">{{"item.version.history.table.version" | translate}}</th>
<th scope="col">{{"item.version.history.table.item" | translate}}</th>
<th scope="col" *ngIf="(hasEpersons$ | async)">{{"item.version.history.table.editor" | translate}}</th>
<th scope="col">{{"item.version.history.table.date" | translate}}</th>
<th scope="col">{{"item.version.history.table.summary" | translate}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let version of versions?.page" [id]="'version-row-' + version.id">
<td class="version-row-element-version">{{version?.version}}</td>
<td class="version-row-element-item">
<span *ngVar="(version?.item | async)?.payload as item">
<a *ngIf="item" [routerLink]="['/items', item?.id]">{{item?.handle}}</a>
<span *ngIf="version?.id === itemVersion?.id">*</span>
</span>
</td>
<td *ngIf="(hasEpersons$ | async)" class="version-row-element-editor">
<span *ngVar="(version?.eperson | async)?.payload as eperson">
<a *ngIf="eperson" [href]="'mailto:' + eperson?.email">{{eperson?.name}}</a>
</span>
</td>
<td class="version-row-element-date">{{version?.created}}</td>
<td class="version-row-element-summary">{{version?.summary}}</td>
</tr>
</tbody>
</table>
<div>*&nbsp;{{"item.version.history.selected" | translate}}</div>
</ds-pagination>
<ds-alert *ngIf="!itemVersion || versions?.page?.length === 0" [content]="'item.version.history.empty'" [type]="AlertTypeEnum.Info"></ds-alert>
</div>
</div>
</div>
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment