Merge pull request #1318 from 4Science/CST-4499

Create new Item Version (basic Items only)
This commit is contained in:
Tim Donohue
2021-10-20 11:17:57 -05:00
committed by GitHub
57 changed files with 2661 additions and 222 deletions

View File

@@ -22,4 +22,7 @@ export enum FeatureID {
CanManagePolicies = 'canManagePolicies',
CanMakePrivate = 'canMakePrivate',
CanMove = 'canMove',
CanEditVersion = 'canEditVersion',
CanDeleteVersion = 'canDeleteVersion',
CanCreateVersion = 'canCreateVersion',
}

View File

@@ -31,7 +31,7 @@ describe('ItemDataService', () => {
},
removeByHrefSubstring(href: string) {
// Do nothing
}
},
}) as RequestService;
const rdbService = getMockRemoteDataBuildService();
@@ -184,4 +184,14 @@ describe('ItemDataService', () => {
});
});
describe('when cache is invalidated', () => {
beforeEach(() => {
service = initTestService();
});
it('should call setStaleByHrefSubstring', () => {
service.invalidateItemCache('uuid');
expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith('item/uuid');
});
});
});

View File

@@ -59,6 +59,7 @@ export class ItemDataService extends DataService<Item> {
* Get the endpoint for browsing items
* (When options.sort.field is empty, the default field to browse by will be 'dc.date.issued')
* @param {FindListOptions} options
* @param linkPath
* @returns {Observable<string>}
*/
public getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable<string> {
@@ -287,4 +288,13 @@ export class ItemDataService extends DataService<Item> {
switchMap((url: string) => this.halService.getEndpoint('bitstreams', `${url}/${itemId}`))
);
}
/**
* Invalidate the cache of the item
* @param itemUUID
*/
invalidateItemCache(itemUUID: string) {
this.requestService.setStaleByHrefSubstring('item/' + itemUUID);
}
}

View File

@@ -0,0 +1,181 @@
import { HttpClient } from '@angular/common/http';
import { of as observableOf } from 'rxjs';
import { TestScheduler } from 'rxjs/testing';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { RequestService } from './request.service';
import { PageInfo } from '../shared/page-info.model';
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { RequestEntry } from './request.reducer';
import { HrefOnlyDataService } from './href-only-data.service';
import { getMockHrefOnlyDataService } from '../../shared/mocks/href-only-data.service.mock';
import { Store } from '@ngrx/store';
import { CoreState } from '../core.reducers';
import { RestResponse } from '../cache/response.models';
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
import { Item } from '../shared/item.model';
import { VersionDataService } from './version-data.service';
import { Version } from '../shared/version.model';
import { VersionHistory } from '../shared/version-history.model';
import { followLink } from '../../shared/utils/follow-link-config.model';
describe('VersionDataService test', () => {
let scheduler: TestScheduler;
let service: VersionDataService;
let requestService: RequestService;
let rdbService: RemoteDataBuildService;
let objectCache: ObjectCacheService;
let halService: HALEndpointService;
let hrefOnlyDataService: HrefOnlyDataService;
let responseCacheEntry: RequestEntry;
const item = Object.assign(new Item(), {
id: '1234-1234',
uuid: '1234-1234',
bundles: observableOf({}),
metadata: {
'dc.title': [
{
language: 'en_US',
value: 'This is just another title'
}
],
'dc.type': [
{
language: null,
value: 'Article'
}
],
'dc.contributor.author': [
{
language: 'en_US',
value: 'Smith, Donald'
}
],
'dc.date.issued': [
{
language: null,
value: '2015-06-26'
}
]
}
});
const itemRD = createSuccessfulRemoteDataObject(item);
const versionHistory = Object.assign(new VersionHistory(), {
id: '1',
draftVersion: true,
});
const mockVersion: Version = Object.assign(new Version(), {
item: createSuccessfulRemoteDataObject$(item),
versionhistory: createSuccessfulRemoteDataObject$(versionHistory),
version: 1,
});
const mockVersionRD = createSuccessfulRemoteDataObject(mockVersion);
const endpointURL = `https://rest.api/rest/api/versioning/versions`;
const findByIdRequestURL = `https://rest.api/rest/api/versioning/versions/${mockVersion.id}`;
const findByIdRequestURL$ = observableOf(findByIdRequestURL);
const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a';
objectCache = {} as ObjectCacheService;
const notificationsService = {} as NotificationsService;
const http = {} as HttpClient;
const comparator = {} as any;
const comparatorEntry = {} as any;
const store = {} as Store<CoreState>;
const pageInfo = new PageInfo();
function initTestService() {
hrefOnlyDataService = getMockHrefOnlyDataService();
return new VersionDataService(
requestService,
rdbService,
store,
objectCache,
halService,
notificationsService,
http,
comparatorEntry
);
}
describe('', () => {
beforeEach(() => {
scheduler = getTestScheduler();
halService = jasmine.createSpyObj('halService', {
getEndpoint: cold('a', { a: endpointURL })
});
responseCacheEntry = new RequestEntry();
responseCacheEntry.request = { href: 'https://rest.api/' } as any;
responseCacheEntry.response = new RestResponse(true, 200, 'Success');
requestService = jasmine.createSpyObj('requestService', {
generateRequestId: requestUUID,
send: true,
removeByHrefSubstring: {},
getByHref: observableOf(responseCacheEntry),
getByUUID: observableOf(responseCacheEntry),
});
rdbService = jasmine.createSpyObj('rdbService', {
buildSingle: hot('(a|)', {
a: mockVersionRD
})
});
service = initTestService();
spyOn((service as any), 'findByHref').and.callThrough();
spyOn((service as any), 'getIDHrefObs').and.returnValue(findByIdRequestURL$);
});
afterEach(() => {
service = null;
});
describe('getHistoryFromVersion', () => {
it('should proxy the call to DataService.findByHref', () => {
scheduler.schedule(() => service.getHistoryFromVersion(mockVersion, true, true));
scheduler.flush();
expect((service as any).findByHref).toHaveBeenCalledWith(findByIdRequestURL$, true, true, followLink('versionhistory'));
});
it('should return a VersionHistory', () => {
const result = service.getHistoryFromVersion(mockVersion, true, true);
const expected = cold('(a|)', {
a: versionHistory
});
expect(result).toBeObservable(expected);
});
it('should return an EMPTY observable when version is not given', () => {
const result = service.getHistoryFromVersion(null);
const expected = cold('|');
expect(result).toBeObservable(expected);
});
});
describe('getHistoryIdFromVersion', () => {
it('should return the version history id', () => {
spyOn((service as any), 'getHistoryFromVersion').and.returnValue(observableOf(versionHistory));
const result = service.getHistoryIdFromVersion(mockVersion);
const expected = cold('(a|)', {
a: versionHistory.id
});
expect(result).toBeObservable(expected);
});
});
});
});

View File

@@ -10,10 +10,14 @@ 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';
import { EMPTY, Observable } from 'rxjs';
import { dataService } from '../cache/builders/build-decorators';
import { VERSION } from '../shared/version.resource-type';
import { VersionHistory } from '../shared/version-history.model';
import { followLink } from '../../shared/utils/follow-link-config.model';
import { getFirstSucceededRemoteDataPayload } from '../shared/operators';
import { map, switchMap } from 'rxjs/operators';
import { isNotEmpty } from '../../shared/empty.util';
/**
* Service responsible for handling requests related to the Version object
@@ -36,9 +40,29 @@ export class VersionDataService extends DataService<Version> {
}
/**
* Get the endpoint for browsing versions
* Get the version history for the given version
* @param version
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* no valid cached version. Defaults to true
* @param reRequestOnStale Whether or not the request should automatically be re-
* requested after the response becomes stale
*/
getBrowseEndpoint(options: FindListOptions = {}, linkPath?: string): Observable<string> {
return this.halService.getEndpoint(this.linkPath);
getHistoryFromVersion(version: Version, useCachedVersionIfAvailable = false, reRequestOnStale = true): Observable<VersionHistory> {
return isNotEmpty(version) ? this.findById(version.id, useCachedVersionIfAvailable, reRequestOnStale, followLink('versionhistory')).pipe(
getFirstSucceededRemoteDataPayload(),
switchMap((res: Version) => res.versionhistory),
getFirstSucceededRemoteDataPayload(),
) : EMPTY;
}
/**
* Get the ID of the version history for the given version
* @param version
*/
getHistoryIdFromVersion(version: Version): Observable<string> {
return this.getHistoryFromVersion(version).pipe(
map((versionHistory: VersionHistory) => versionHistory.id),
);
}
}

View File

@@ -6,6 +6,14 @@ import { NotificationsServiceStub } from '../../shared/testing/notifications-ser
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
import { VersionDataService } from './version-data.service';
import { fakeAsync, waitForAsync } from '@angular/core/testing';
import { VersionHistory } from '../shared/version-history.model';
import { Version } from '../shared/version.model';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { createPaginatedList } from '../../shared/testing/utils.test';
import { Item } from '../shared/item.model';
import { of } from 'rxjs';
import SpyObj = jasmine.SpyObj;
const url = 'fake-url';
@@ -16,9 +24,97 @@ describe('VersionHistoryDataService', () => {
let notificationsService: any;
let rdbService: RemoteDataBuildService;
let objectCache: ObjectCacheService;
let versionService: VersionDataService;
let versionService: SpyObj<VersionDataService>;
let halService: any;
const versionHistoryId = 'version-history-id';
const versionHistoryDraftId = 'version-history-draft-id';
const version1Id = 'version-1-id';
const version2Id = 'version-1-id';
const item1Uuid = 'item-1-uuid';
const item2Uuid = 'item-2-uuid';
const versionHistory = Object.assign(new VersionHistory(), {
id: versionHistoryId,
draftVersion: false,
});
const versionHistoryDraft = Object.assign(new VersionHistory(), {
id: versionHistoryDraftId,
draftVersion: true,
});
const version1 = Object.assign(new Version(), {
id: version1Id,
version: 1,
created: new Date(2020, 1, 1),
summary: 'first version',
versionhistory: createSuccessfulRemoteDataObject$(versionHistory),
_links: {
self: {
href: 'version1-url',
},
},
});
const version2 = Object.assign(new Version(), {
id: version2Id,
version: 2,
summary: 'second version',
created: new Date(2020, 1, 2),
versionhistory: createSuccessfulRemoteDataObject$(versionHistory),
_links: {
self: {
href: 'version2-url',
},
},
});
const versions = [version1, version2];
versionHistory.versions = createSuccessfulRemoteDataObject$(createPaginatedList(versions));
const item1 = Object.assign(new Item(), {
uuid: item1Uuid,
handle: '123456789/1',
version: createSuccessfulRemoteDataObject$(version1),
_links: {
self: {
href: '/items/' + item2Uuid,
}
}
});
const item2 = Object.assign(new Item(), {
uuid: item2Uuid,
handle: '123456789/2',
version: createSuccessfulRemoteDataObject$(version2),
_links: {
self: {
href: '/items/' + item2Uuid,
}
}
});
const items = [item1, item2];
version1.item = createSuccessfulRemoteDataObject$(item1);
version2.item = createSuccessfulRemoteDataObject$(item2);
/**
* 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'),
buildFromRequestUUID: jasmine.createSpy('buildFromRequestUUID'),
});
objectCache = jasmine.createSpyObj('objectCache', {
remove: jasmine.createSpy('remove')
});
versionService = jasmine.createSpyObj('objectCache', {
findByHref: jasmine.createSpy('findByHref'),
findAllByHref: jasmine.createSpy('findAllByHref'),
getHistoryFromVersion: jasmine.createSpy('getHistoryFromVersion'),
});
halService = new HALEndpointServiceStub(url);
notificationsService = new NotificationsServiceStub();
service = new VersionHistoryDataService(requestService, rdbService, null, objectCache, halService, notificationsService, versionService, null, null);
}
beforeEach(() => {
createService();
});
@@ -35,24 +131,70 @@ describe('VersionHistoryDataService', () => {
});
});
/**
* 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')
describe('when getVersions is called', () => {
beforeEach(waitForAsync(() => {
service.getVersions(versionHistoryId);
}));
it('findAllByHref should have been called', () => {
expect(versionService.findAllByHref).toHaveBeenCalled();
});
objectCache = jasmine.createSpyObj('objectCache', {
remove: jasmine.createSpy('remove')
});
versionService = jasmine.createSpyObj('objectCache', {
findAllByHref: jasmine.createSpy('findAllByHref')
});
halService = new HALEndpointServiceStub(url);
notificationsService = new NotificationsServiceStub();
});
describe('when getBrowseEndpoint is called', () => {
it('should return the correct value', () => {
service.getBrowseEndpoint().subscribe((res) => {
expect(res).toBe(url + '/versionhistories');
});
});
});
describe('when getVersionsEndpoint is called', () => {
it('should return the correct value', () => {
service.getVersionsEndpoint(versionHistoryId).subscribe((res) => {
expect(res).toBe(url + '/versions');
});
});
});
describe('when cache is invalidated', () => {
it('should call setStaleByHrefSubstring', () => {
service.invalidateVersionHistoryCache(versionHistoryId);
expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith('versioning/versionhistories/' + versionHistoryId);
});
});
describe('isLatest$', () => {
beforeEach(waitForAsync(() => {
spyOn(service, 'getLatestVersion$').and.returnValue(of(version2));
}));
it('should return false for version1', () => {
service.isLatest$(version1).subscribe((res) => {
expect(res).toBe(false);
});
});
it('should return true for version2', () => {
service.isLatest$(version2).subscribe((res) => {
expect(res).toBe(true);
});
});
});
describe('hasDraftVersion$', () => {
beforeEach(waitForAsync(() => {
versionService.findByHref.and.returnValue(createSuccessfulRemoteDataObject$<Version>(version1));
}));
it('should return false if draftVersion is false', fakeAsync(() => {
versionService.getHistoryFromVersion.and.returnValue(of(versionHistory));
service.hasDraftVersion$('href').subscribe((res) => {
expect(res).toBeFalse();
});
}));
it('should return true if draftVersion is true', fakeAsync(() => {
versionService.getHistoryFromVersion.and.returnValue(of(versionHistoryDraft));
service.hasDraftVersion$('href').subscribe((res) => {
expect(res).toBeTrue();
});
}));
});
service = new VersionHistoryDataService(requestService, rdbService, null, objectCache, halService, notificationsService, versionService, null, null);
}
});

View File

@@ -8,19 +8,30 @@ import { CoreState } from '../core.reducers';
import { ObjectCacheService } from '../cache/object-cache.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { HttpClient } from '@angular/common/http';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
import { FindListOptions } from './request.models';
import { Observable } from 'rxjs';
import { FindListOptions, PostRequest, RestRequest } from './request.models';
import { Observable, of } from 'rxjs';
import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model';
import { RemoteData } from './remote-data';
import { PaginatedList } from './paginated-list.model';
import { Version } from '../shared/version.model';
import { map, switchMap } from 'rxjs/operators';
import { filter, map, switchMap, take } from 'rxjs/operators';
import { dataService } from '../cache/builders/build-decorators';
import { VERSION_HISTORY } from '../shared/version-history.resource-type';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { VersionDataService } from './version-data.service';
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
import {
getAllSucceededRemoteData,
getFirstCompletedRemoteData,
getFirstSucceededRemoteDataPayload,
getRemoteDataPayload,
sendRequest
} from '../shared/operators';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { hasValueOperator } from '../../shared/empty.util';
import { Item } from '../shared/item.model';
/**
* Service responsible for handling requests related to the VersionHistory object
@@ -79,4 +90,129 @@ export class VersionHistoryDataService extends DataService<VersionHistory> {
return this.versionDataService.findAllByHref(hrefObs, undefined, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}
/**
* Create a new version for an item
* @param itemHref the item for which create a new version
* @param summary the summary of the new version
*/
createVersion(itemHref: string, summary: string): Observable<RemoteData<Version>> {
const requestOptions: HttpOptions = Object.create({});
let requestHeaders = new HttpHeaders();
requestHeaders = requestHeaders.append('Content-Type', 'text/uri-list');
requestOptions.headers = requestHeaders;
return this.halService.getEndpoint(this.versionsEndpoint).pipe(
take(1),
map((endpointUrl: string) => (summary?.length > 0) ? `${endpointUrl}?summary=${summary}` : `${endpointUrl}`),
map((endpointURL: string) => new PostRequest(this.requestService.generateRequestId(), endpointURL, itemHref, requestOptions)),
sendRequest(this.requestService),
switchMap((restRequest: RestRequest) => this.rdbService.buildFromRequestUUID(restRequest.uuid)),
getFirstCompletedRemoteData()
) as Observable<RemoteData<Version>>;
}
/**
* Get the latest version in a version history
* @param versionHistory
*/
getLatestVersionFromHistory$(versionHistory: VersionHistory): Observable<Version> {
// Pagination options to fetch a single version on the first page (this is the latest version in the history)
const latestVersionOptions = Object.assign(new PaginationComponentOptions(), {
id: 'item-newest-version-options',
currentPage: 1,
pageSize: 1
});
const latestVersionSearch = new PaginatedSearchOptions({pagination: latestVersionOptions});
return this.getVersions(versionHistory.id, latestVersionSearch, false, true, followLink('item')).pipe(
getAllSucceededRemoteData(),
getRemoteDataPayload(),
hasValueOperator(),
filter((versions) => versions.page.length > 0),
map((versions) => versions.page[0])
);
}
/**
* Get the latest version (return null if the specified version is null)
* @param version
*/
getLatestVersion$(version: Version): Observable<Version> {
// retrieve again version, including with versionHistory
return version.id ? this.versionDataService.findById(version.id, false, true, followLink('versionhistory')).pipe(
getFirstSucceededRemoteDataPayload(),
switchMap((res) => res.versionhistory),
getFirstSucceededRemoteDataPayload(),
switchMap((versionHistoryRD) => this.getLatestVersionFromHistory$(versionHistoryRD)),
) : of(null);
}
/**
* Check if the given version is the latest (return null if `version` is null)
* @param version
* @returns `true` if the specified version is the latest one, `false` otherwise, or `null` if the specified version is null
*/
isLatest$(version: Version): Observable<boolean> {
return version ? this.getLatestVersion$(version).pipe(
take(1),
switchMap((latestVersion) => of(version.version === latestVersion.version))
) : of(null);
}
/**
* Check if a worskpace item exists in the version history (return null if there is no version history)
* @param versionHref the href of the version
* @returns `true` if a workspace item exists, `false` otherwise, or `null` if a version history does not exist
*/
hasDraftVersion$(versionHref: string): Observable<boolean> {
return this.versionDataService.findByHref(versionHref, true, true, followLink('versionhistory')).pipe(
getFirstCompletedRemoteData(),
switchMap((res) => {
if (res.hasSucceeded && !res.hasNoContent) {
return of(res).pipe(
getFirstSucceededRemoteDataPayload(),
switchMap((version) => this.versionDataService.getHistoryFromVersion(version)),
map((versionHistory) => versionHistory ? versionHistory.draftVersion : false),
);
} else {
return of(false);
}
}),
);
}
/**
* Get the item of the latest version in a version history
* @param versionHistory
*/
getLatestVersionItemFromHistory$(versionHistory: VersionHistory): Observable<Item> {
return this.getLatestVersionFromHistory$(versionHistory).pipe(
switchMap((newLatestVersion) => newLatestVersion.item),
getFirstSucceededRemoteDataPayload(),
);
}
/**
* Get the item of the latest version from any version in the version history
* @param version
*/
getVersionHistoryFromVersion$(version: Version): Observable<VersionHistory> {
return this.versionDataService.getHistoryIdFromVersion(version).pipe(
take(1),
switchMap((res) => this.findById(res)),
getFirstSucceededRemoteDataPayload(),
);
}
/**
* Invalidate the cache of the version history
* @param versionHistoryID
*/
invalidateVersionHistoryCache(versionHistoryID: string) {
this.requestService.setStaleByHrefSubstring('versioning/versionhistories/' + versionHistoryID);
}
}

View File

@@ -22,6 +22,7 @@ export class VersionHistory extends DSpaceObject {
_links: {
self: HALLink;
versions: HALLink;
draftVersion: HALLink;
};
/**
@@ -30,6 +31,24 @@ export class VersionHistory extends DSpaceObject {
@autoserialize
id: string;
/**
* The summary of this Version History
*/
@autoserialize
summary: string;
/**
* The name of the submitter of this Version History
*/
@autoserialize
submitterName: string;
/**
* Whether exist a workspace item
*/
@autoserialize
draftVersion: boolean;
/**
* The list of versions within this history
*/

View File

@@ -0,0 +1,150 @@
import { HttpClient } from '@angular/common/http';
import { of as observableOf } from 'rxjs';
import { TestScheduler } from 'rxjs/testing';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { RequestService } from '../data/request.service';
import { PageInfo } from '../shared/page-info.model';
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
import { RequestEntry } from '../data/request.reducer';
import { HrefOnlyDataService } from '../data/href-only-data.service';
import { getMockHrefOnlyDataService } from '../../shared/mocks/href-only-data.service.mock';
import { Store } from '@ngrx/store';
import { CoreState } from '../core.reducers';
import { RestResponse } from '../cache/response.models';
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
import { Item } from '../shared/item.model';
import { WorkflowItemDataService } from './workflowitem-data.service';
import { WorkflowItem } from './models/workflowitem.model';
describe('WorkflowItemDataService test', () => {
let scheduler: TestScheduler;
let service: WorkflowItemDataService;
let requestService: RequestService;
let rdbService: RemoteDataBuildService;
let objectCache: ObjectCacheService;
let halService: HALEndpointService;
let hrefOnlyDataService: HrefOnlyDataService;
let responseCacheEntry: RequestEntry;
const item = Object.assign(new Item(), {
id: '1234-1234',
uuid: '1234-1234',
bundles: observableOf({}),
metadata: {
'dc.title': [
{
language: 'en_US',
value: 'This is just another title'
}
],
'dc.type': [
{
language: null,
value: 'Article'
}
],
'dc.contributor.author': [
{
language: 'en_US',
value: 'Smith, Donald'
}
],
'dc.date.issued': [
{
language: null,
value: '2015-06-26'
}
]
}
});
const itemRD = createSuccessfulRemoteDataObject(item);
const wsi = Object.assign(new WorkflowItem(), { item: observableOf(itemRD), id: '1234', uuid: '1234' });
const wsiRD = createSuccessfulRemoteDataObject(wsi);
const endpointURL = `https://rest.api/rest/api/submission/workspaceitems`;
const searchRequestURL = `https://rest.api/rest/api/submission/workspaceitems/search/item?uuid=1234-1234`;
const searchRequestURL$ = observableOf(searchRequestURL);
const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a';
objectCache = {} as ObjectCacheService;
const notificationsService = {} as NotificationsService;
const http = {} as HttpClient;
const comparator = {} as any;
const comparatorEntry = {} as any;
const store = {} as Store<CoreState>;
const pageInfo = new PageInfo();
function initTestService() {
hrefOnlyDataService = getMockHrefOnlyDataService();
return new WorkflowItemDataService(
comparatorEntry,
halService,
http,
notificationsService,
requestService,
rdbService,
objectCache,
store
);
}
describe('', () => {
beforeEach(() => {
scheduler = getTestScheduler();
halService = jasmine.createSpyObj('halService', {
getEndpoint: cold('a', { a: endpointURL })
});
responseCacheEntry = new RequestEntry();
responseCacheEntry.request = { href: 'https://rest.api/' } as any;
responseCacheEntry.response = new RestResponse(true, 200, 'Success');
requestService = jasmine.createSpyObj('requestService', {
generateRequestId: requestUUID,
send: true,
removeByHrefSubstring: {},
getByHref: observableOf(responseCacheEntry),
getByUUID: observableOf(responseCacheEntry),
});
rdbService = jasmine.createSpyObj('rdbService', {
buildSingle: hot('a|', {
a: wsiRD
})
});
service = initTestService();
spyOn((service as any), 'findByHref').and.callThrough();
spyOn((service as any), 'getSearchByHref').and.returnValue(searchRequestURL$);
});
afterEach(() => {
service = null;
});
describe('findByItem', () => {
it('should proxy the call to DataService.findByHref', () => {
scheduler.schedule(() => service.findByItem('1234-1234', true, true, pageInfo));
scheduler.flush();
expect((service as any).findByHref).toHaveBeenCalledWith(searchRequestURL$, true, true);
});
it('should return a RemoteData<WorkspaceItem> for the search', () => {
const result = service.findByItem('1234-1234', true, true, pageInfo);
const expected = cold('a|', {
a: wsiRD
});
expect(result).toBeObservable(expected);
});
});
});
});

View File

@@ -9,7 +9,7 @@ import { DataService } from '../data/data.service';
import { RequestService } from '../data/request.service';
import { WorkflowItem } from './models/workflowitem.model';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { DeleteByIDRequest } from '../data/request.models';
import { DeleteByIDRequest, FindListOptions } from '../data/request.models';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
@@ -19,6 +19,9 @@ import { hasValue } from '../../shared/empty.util';
import { RemoteData } from '../data/remote-data';
import { NoContent } from '../shared/NoContent.model';
import { getFirstCompletedRemoteData } from '../shared/operators';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { WorkspaceItem } from './models/workspaceitem.model';
import { RequestParam } from '../cache/models/request-param.model';
/**
* A service that provides methods to make REST requests with workflow items endpoint.
@@ -27,6 +30,7 @@ import { getFirstCompletedRemoteData } from '../shared/operators';
@dataService(WorkflowItem.type)
export class WorkflowItemDataService extends DataService<WorkflowItem> {
protected linkPath = 'workflowitems';
protected searchByItemLinkPath = 'item';
protected responseMsToLive = 10 * 1000;
constructor(
@@ -86,4 +90,23 @@ export class WorkflowItemDataService extends DataService<WorkflowItem> {
return this.rdbService.buildFromRequestUUID(requestId);
}
/**
* Return the WorkflowItem object found through the UUID of an item
*
* @param uuid The uuid of the item
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* no valid cached version. Defaults to true
* @param reRequestOnStale Whether or not the request should automatically be re-
* requested after the response becomes stale
* @param options The {@link FindListOptions} object
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
*/
public findByItem(uuid: string, useCachedVersionIfAvailable = false, reRequestOnStale = true, options: FindListOptions = {}, ...linksToFollow: FollowLinkConfig<WorkspaceItem>[]): Observable<RemoteData<WorkspaceItem>> {
const findListOptions = new FindListOptions();
findListOptions.searchParams = [new RequestParam('uuid', encodeURIComponent(uuid))];
const href$ = this.getSearchByHref(this.searchByItemLinkPath, findListOptions, ...linksToFollow);
return this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}
}

View File

@@ -0,0 +1,150 @@
import { HttpClient } from '@angular/common/http';
import { of as observableOf } from 'rxjs';
import { TestScheduler } from 'rxjs/testing';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { RequestService } from '../data/request.service';
import { PageInfo } from '../shared/page-info.model';
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
import { RequestEntry } from '../data/request.reducer';
import { HrefOnlyDataService } from '../data/href-only-data.service';
import { getMockHrefOnlyDataService } from '../../shared/mocks/href-only-data.service.mock';
import { WorkspaceitemDataService } from './workspaceitem-data.service';
import { Store } from '@ngrx/store';
import { CoreState } from '../core.reducers';
import { RestResponse } from '../cache/response.models';
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
import { Item } from '../shared/item.model';
import { WorkspaceItem } from './models/workspaceitem.model';
describe('WorkspaceitemDataService test', () => {
let scheduler: TestScheduler;
let service: WorkspaceitemDataService;
let requestService: RequestService;
let rdbService: RemoteDataBuildService;
let objectCache: ObjectCacheService;
let halService: HALEndpointService;
let hrefOnlyDataService: HrefOnlyDataService;
let responseCacheEntry: RequestEntry;
const item = Object.assign(new Item(), {
id: '1234-1234',
uuid: '1234-1234',
bundles: observableOf({}),
metadata: {
'dc.title': [
{
language: 'en_US',
value: 'This is just another title'
}
],
'dc.type': [
{
language: null,
value: 'Article'
}
],
'dc.contributor.author': [
{
language: 'en_US',
value: 'Smith, Donald'
}
],
'dc.date.issued': [
{
language: null,
value: '2015-06-26'
}
]
}
});
const itemRD = createSuccessfulRemoteDataObject(item);
const wsi = Object.assign(new WorkspaceItem(), { item: observableOf(itemRD), id: '1234', uuid: '1234' });
const wsiRD = createSuccessfulRemoteDataObject(wsi);
const endpointURL = `https://rest.api/rest/api/submission/workspaceitems`;
const searchRequestURL = `https://rest.api/rest/api/submission/workspaceitems/search/item?uuid=1234-1234`;
const searchRequestURL$ = observableOf(searchRequestURL);
const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a';
objectCache = {} as ObjectCacheService;
const notificationsService = {} as NotificationsService;
const http = {} as HttpClient;
const comparator = {} as any;
const comparatorEntry = {} as any;
const store = {} as Store<CoreState>;
const pageInfo = new PageInfo();
function initTestService() {
hrefOnlyDataService = getMockHrefOnlyDataService();
return new WorkspaceitemDataService(
comparatorEntry,
halService,
http,
notificationsService,
requestService,
rdbService,
objectCache,
store
);
}
describe('', () => {
beforeEach(() => {
scheduler = getTestScheduler();
halService = jasmine.createSpyObj('halService', {
getEndpoint: cold('a', { a: endpointURL })
});
responseCacheEntry = new RequestEntry();
responseCacheEntry.request = { href: 'https://rest.api/' } as any;
responseCacheEntry.response = new RestResponse(true, 200, 'Success');
requestService = jasmine.createSpyObj('requestService', {
generateRequestId: requestUUID,
send: true,
removeByHrefSubstring: {},
getByHref: observableOf(responseCacheEntry),
getByUUID: observableOf(responseCacheEntry),
});
rdbService = jasmine.createSpyObj('rdbService', {
buildSingle: hot('a|', {
a: wsiRD
})
});
service = initTestService();
spyOn((service as any), 'findByHref').and.callThrough();
spyOn((service as any), 'getSearchByHref').and.returnValue(searchRequestURL$);
});
afterEach(() => {
service = null;
});
describe('findByItem', () => {
it('should proxy the call to DataService.findByHref', () => {
scheduler.schedule(() => service.findByItem('1234-1234', true, true, pageInfo));
scheduler.flush();
expect((service as any).findByHref).toHaveBeenCalledWith(searchRequestURL$, true, true);
});
it('should return a RemoteData<WorkspaceItem> for the search', () => {
const result = service.findByItem('1234-1234', true, true, pageInfo);
const expected = cold('a|', {
a: wsiRD
});
expect(result).toBeObservable(expected);
});
});
});
});

View File

@@ -12,6 +12,11 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
import { ObjectCacheService } from '../cache/object-cache.service';
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
import { WorkspaceItem } from './models/workspaceitem.model';
import { Observable } from 'rxjs';
import { RemoteData } from '../data/remote-data';
import { FindListOptions } from '../data/request.models';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { RequestParam } from '../cache/models/request-param.model';
/**
* A service that provides methods to make REST requests with workspaceitems endpoint.
@@ -20,6 +25,7 @@ import { WorkspaceItem } from './models/workspaceitem.model';
@dataService(WorkspaceItem.type)
export class WorkspaceitemDataService extends DataService<WorkspaceItem> {
protected linkPath = 'workspaceitems';
protected searchByItemLinkPath = 'item';
constructor(
protected comparator: DSOChangeAnalyzer<WorkspaceItem>,
@@ -33,4 +39,22 @@ export class WorkspaceitemDataService extends DataService<WorkspaceItem> {
super();
}
/**
* Return the WorkspaceItem object found through the UUID of an item
*
* @param uuid The uuid of the item
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* no valid cached version. Defaults to true
* @param reRequestOnStale Whether or not the request should automatically be re-
* requested after the response becomes stale
* @param options The {@link FindListOptions} object
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
*/
public findByItem(uuid: string, useCachedVersionIfAvailable = false, reRequestOnStale = true, options: FindListOptions = {}, ...linksToFollow: FollowLinkConfig<WorkspaceItem>[]): Observable<RemoteData<WorkspaceItem>> {
const findListOptions = new FindListOptions();
findListOptions.searchParams = [new RequestParam('uuid', encodeURIComponent(uuid))];
const href$ = this.getSearchByHref(this.searchByItemLinkPath, findListOptions, ...linksToFollow);
return this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}
}

View File

@@ -1,66 +1,69 @@
<div class="item-metadata">
<div class="button-row top d-flex mb-2">
<button class="mr-auto btn btn-success"
(click)="add()"><i
class="fas fa-plus"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.add-button" | translate}}</span>
</button>
<button class="btn btn-warning" *ngIf="isReinstatable() | async"
(click)="reinstate()"><i
class="fas fa-undo-alt"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.reinstate-button" | translate}}</span>
</button>
<button class="btn btn-primary" [disabled]="!(hasChanges() | async) || !(isValid() | async)"
(click)="submit()"><i
class="fas fa-save"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.save-button" | translate}}</span>
</button>
<button class="btn btn-danger" *ngIf="!(isReinstatable() | async)"
[disabled]="!(hasChanges() | async)"
(click)="discard()"><i
class="fas fa-times"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.discard-button" | translate}}</span>
</button>
</div>
<table class="table table-responsive table-striped table-bordered" *ngIf="((updates$ | async)| dsObjectValues).length > 0">
<tbody>
<tr>
<th><span id="fieldName">{{'item.edit.metadata.headers.field' | translate}}</span></th>
<th><span id="fieldValue">{{'item.edit.metadata.headers.value' | translate}}</span></th>
<th class="text-center"><span id="fieldLang">{{'item.edit.metadata.headers.language' | translate}}</span></th>
<th class="text-center">{{'item.edit.metadata.headers.edit' | translate}}</th>
</tr>
<tr *ngFor="let updateValue of ((updates$ | async)| dsObjectValues); trackBy: trackUpdate"
ds-edit-in-place-field
[fieldUpdate]="updateValue || {}"
[url]="url"
[ngClass]="{
<div class="button-row top d-flex mb-2">
<button class="mr-auto btn btn-success"
(click)="add()"><i
class="fas fa-plus"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.add-button" | translate}}</span>
</button>
<button class="btn btn-warning" *ngIf="isReinstatable() | async"
(click)="reinstate()"><i
class="fas fa-undo-alt"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.reinstate-button" | translate}}</span>
</button>
<button class="btn btn-primary" [disabled]="!(hasChanges() | async) || !(isValid() | async)"
(click)="submit()"><i
class="fas fa-save"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.save-button" | translate}}</span>
</button>
<button class="btn btn-danger" *ngIf="!(isReinstatable() | async)"
[disabled]="!(hasChanges() | async)"
(click)="discard()"><i
class="fas fa-times"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.discard-button" | translate}}</span>
</button>
</div>
<table class="table table-responsive table-striped table-bordered"
*ngIf="((updates$ | async)| dsObjectValues).length > 0">
<thead>
<tr>
<th><span id="fieldName">{{'item.edit.metadata.headers.field' | translate}}</span></th>
<th><span id="fieldValue">{{'item.edit.metadata.headers.value' | translate}}</span></th>
<th class="text-center"><span id="fieldLang">{{'item.edit.metadata.headers.language' | translate}}</span></th>
<th class="text-center">{{'item.edit.metadata.headers.edit' | translate}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let updateValue of ((updates$ | async)| dsObjectValues); trackBy: trackUpdate"
ds-edit-in-place-field
[fieldUpdate]="updateValue || {}"
[url]="url"
[ngClass]="{
'table-warning': updateValue.changeType === 0,
'table-danger': updateValue.changeType === 2,
'table-success': updateValue.changeType === 1
}">
</tr>
</tbody>
</table>
<div *ngIf="((updates$ | async)| dsObjectValues).length == 0">
<ds-alert [content]="'item.edit.metadata.empty'" [type]="AlertTypeEnum.Info"></ds-alert>
</tr>
</tbody>
</table>
<div *ngIf="((updates$ | async)| dsObjectValues).length == 0">
<ds-alert [content]="'item.edit.metadata.empty'" [type]="AlertTypeEnum.Info"></ds-alert>
</div>
<div class="button-row bottom">
<div class="mt-2 float-right">
<button class="btn btn-warning" *ngIf="isReinstatable() | async"
(click)="reinstate()"><i
class="fas fa-undo-alt"></i> {{"item.edit.metadata.reinstate-button" | translate}}
</button>
<button class="btn btn-primary mr-0" [disabled]="!(hasChanges() | async)"
(click)="submit()"><i
class="fas fa-save"></i> {{"item.edit.metadata.save-button" | translate}}
</button>
<button class="btn btn-danger" *ngIf="!(isReinstatable() | async)"
[disabled]="!(hasChanges() | async)"
(click)="discard()"><i
class="fas fa-times"></i> {{"item.edit.metadata.discard-button" | translate}}
</button>
</div>
<div class="button-row bottom">
<div class="mt-2 float-right">
<button class="btn btn-warning" *ngIf="isReinstatable() | async"
(click)="reinstate()"><i
class="fas fa-undo-alt"></i> {{"item.edit.metadata.reinstate-button" | translate}}
</button>
<button class="btn btn-primary mr-0" [disabled]="!(hasChanges() | async)"
(click)="submit()"><i
class="fas fa-save"></i> {{"item.edit.metadata.save-button" | translate}}
</button>
<button class="btn btn-danger" *ngIf="!(isReinstatable() | async)"
[disabled]="!(hasChanges() | async)"
(click)="discard()"><i
class="fas fa-times"></i> {{"item.edit.metadata.discard-button" | translate}}
</button>
</div>
</div>
</div>
</div>

View File

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

View File

@@ -1,5 +1,5 @@
import { ItemVersionHistoryComponent } from './item-version-history.component';
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { VarDirective } from '../../../shared/utils/var.directive';
import { RouterTestingModule } from '@angular/router/testing';
import { TranslateModule } from '@ngx-translate/core';
@@ -18,12 +18,20 @@ describe('ItemVersionHistoryComponent', () => {
handle: '123456789/1',
});
const activatedRoute = {
parent: {
parent: {
data: observableOf({dso: createSuccessfulRemoteDataObject(item)})
}
}
};
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ItemVersionHistoryComponent, VarDirective],
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
providers: [
{ provide: ActivatedRoute, useValue: { parent: { data: observableOf({ dso: createSuccessfulRemoteDataObject(item) }) } } }
{ provide: ActivatedRoute, useValue: activatedRoute }
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();

View File

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

View File

@@ -22,6 +22,10 @@ export function getItemEditRoute(item: Item) {
return new URLCombiner(getItemPageRoute(item), ITEM_EDIT_PATH).toString();
}
export function getItemEditVersionhistoryRoute(item: Item) {
return new URLCombiner(getItemPageRoute(item), ITEM_EDIT_PATH, ITEM_EDIT_VERSIONHISTORY_PATH).toString();
}
export function getEntityPageRoute(entityType: string, itemId: string) {
if (isNotEmpty(entityType)) {
return new URLCombiner('/entities', encodeURIComponent(entityType.toLowerCase()), itemId).toString();
@@ -34,5 +38,15 @@ export function getEntityEditRoute(entityType: string, itemId: string) {
return new URLCombiner(getEntityPageRoute(entityType, itemId), ITEM_EDIT_PATH).toString();
}
/**
* Get the route to an item's version
* @param versionId the ID of the version for which the route will be retrieved
*/
export function getItemVersionRoute(versionId: string) {
return new URLCombiner(getItemModuleRoute(), ITEM_VERSION_PATH, versionId).toString();
}
export const ITEM_EDIT_PATH = 'edit';
export const ITEM_EDIT_VERSIONHISTORY_PATH = 'versionhistory';
export const ITEM_VERSION_PATH = 'version';
export const UPLOAD_BITSTREAM_PATH = 'bitstreams/new';

View File

@@ -3,6 +3,7 @@ import { RouterModule } from '@angular/router';
import { ItemPageResolver } from './item-page.resolver';
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
import { ItemBreadcrumbResolver } from '../core/breadcrumbs/item-breadcrumb.resolver';
import { VersionResolver } from './version-page/version.resolver';
import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service';
import { LinkService } from '../core/cache/builders/link.service';
import { UploadBitstreamComponent } from './bitstreams/upload/upload-bitstream.component';
@@ -12,6 +13,7 @@ import { MenuItemType } from '../shared/menu/initial-menus-state';
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
import { ThemedItemPageComponent } from './simple/themed-item-page.component';
import { ThemedFullItemPageComponent } from './full/themed-full-item-page.component';
import { VersionPageComponent } from './version-page/version-page/version-page.component';
import { BitstreamRequestACopyPageComponent } from '../shared/bitstream-request-a-copy-page/bitstream-request-a-copy-page.component';
@NgModule({
@@ -63,6 +65,18 @@ import { BitstreamRequestACopyPageComponent } from '../shared/bitstream-request-
}],
},
},
},
{
path: 'version',
children: [
{
path: ':id',
component: VersionPageComponent,
resolve: {
dso: VersionResolver,
},
}
],
}
])
],
@@ -72,6 +86,7 @@ import { BitstreamRequestACopyPageComponent } from '../shared/bitstream-request-
DSOBreadcrumbsService,
LinkService,
ItemPageAdministratorGuard,
VersionResolver,
]
})

View File

@@ -31,6 +31,8 @@ import { MediaViewerComponent } from './media-viewer/media-viewer.component';
import { MediaViewerVideoComponent } from './media-viewer/media-viewer-video/media-viewer-video.component';
import { MediaViewerImageComponent } from './media-viewer/media-viewer-image/media-viewer-image.component';
import { NgxGalleryModule } from '@kolkov/ngx-gallery';
import { VersionPageComponent } from './version-page/version-page/version-page.component';
import { VersionedItemComponent } from './simple/item-types/versioned-item/versioned-item.component';
import { ThemedFileSectionComponent} from './simple/field-components/file-section/themed-file-section.component';
const ENTRY_COMPONENTS = [
@@ -62,7 +64,8 @@ const DECLARATIONS = [
AbstractIncrementalListComponent,
MediaViewerComponent,
MediaViewerVideoComponent,
MediaViewerImageComponent
MediaViewerImageComponent,
VersionPageComponent,
];
@NgModule({
@@ -74,10 +77,11 @@ const DECLARATIONS = [
StatisticsModule.forRoot(),
JournalEntitiesModule.withEntryComponents(),
ResearchEntitiesModule.withEntryComponents(),
NgxGalleryModule,
NgxGalleryModule,
],
declarations: [
...DECLARATIONS
...DECLARATIONS,
VersionedItemComponent
],
exports: [
...DECLARATIONS

View File

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

View File

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

View File

@@ -29,6 +29,12 @@ import { TruncatePipe } from '../../../../shared/utils/truncate.pipe';
import { GenericItemPageFieldComponent } from '../../field-components/specific-field/generic/generic-item-page-field.component';
import { createRelationshipsObservable } from '../shared/item.component.spec';
import { UntypedItemComponent } from './untyped-item.component';
import { VersionHistoryDataService } from '../../../../core/data/version-history-data.service';
import { VersionDataService } from '../../../../core/data/version-data.service';
import { RouterTestingModule } from '@angular/router/testing';
import { WorkspaceitemDataService } from '../../../../core/submission/workspaceitem-data.service';
import { SearchService } from '../../../../core/shared/search/search.service';
import { ItemVersionsSharedService } from '../../../../shared/item/item-versions/item-versions-shared.service';
const mockItem: Item = Object.assign(new Item(), {
bundles: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])),
@@ -47,13 +53,16 @@ describe('UntypedItemComponent', () => {
}
};
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateLoaderMock
}
})],
declarations: [UntypedItemComponent, GenericItemPageFieldComponent, TruncatePipe],
imports: [
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateLoaderMock
}
}),
RouterTestingModule,
],
declarations: [UntypedItemComponent, GenericItemPageFieldComponent, TruncatePipe ],
providers: [
{ provide: ItemDataService, useValue: {} },
{ provide: TruncatableService, useValue: {} },
@@ -68,9 +77,14 @@ describe('UntypedItemComponent', () => {
{ provide: HttpClient, useValue: {} },
{ provide: DSOChangeAnalyzer, useValue: {} },
{ provide: DefaultChangeAnalyzer, useValue: {} },
{ provide: VersionHistoryDataService, useValue: {} },
{ provide: VersionDataService, useValue: {} },
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
{ provide: WorkspaceitemDataService, useValue: {} },
{ provide: SearchService, useValue: {} },
{ provide: ItemDataService, useValue: {} },
{ provide: ItemVersionsSharedService, useValue: {} },
],
schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(UntypedItemComponent, {
set: { changeDetection: ChangeDetectionStrategy.Default }

View File

@@ -1,8 +1,8 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Item } from '../../../../core/shared/item.model';
import { ItemComponent } from '../shared/item.component';
import { ViewMode } from '../../../../core/shared/view-mode.model';
import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
import { VersionedItemComponent } from '../versioned-item/versioned-item.component';
/**
* Component that represents a publication Item page
@@ -15,6 +15,6 @@ import { listableObjectComponent } from '../../../../shared/object-collection/sh
templateUrl: './untyped-item.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UntypedItemComponent extends ItemComponent {
export class UntypedItemComponent extends VersionedItemComponent {
}

View File

@@ -0,0 +1,93 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { VersionedItemComponent } from './versioned-item.component';
import { VersionHistoryDataService } from '../../../../core/data/version-history-data.service';
import { TranslateService } from '@ngx-translate/core';
import { VersionDataService } from '../../../../core/data/version-data.service';
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { ItemVersionsSharedService } from '../../../../shared/item/item-versions/item-versions-shared.service';
import { Item } from '../../../../core/shared/item.model';
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
import { buildPaginatedList } from '../../../../core/data/paginated-list.model';
import { PageInfo } from '../../../../core/shared/page-info.model';
import { MetadataMap } from '../../../../core/shared/metadata.models';
import { createRelationshipsObservable } from '../shared/item.component.spec';
import { RouterTestingModule } from '@angular/router/testing';
import { Component } from '@angular/core';
import { WorkspaceitemDataService } from '../../../../core/submission/workspaceitem-data.service';
import { SearchService } from '../../../../core/shared/search/search.service';
import { ItemDataService } from '../../../../core/data/item-data.service';
import { Version } from '../../../../core/shared/version.model';
const mockItem: Item = Object.assign(new Item(), {
bundles: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])),
metadata: new MetadataMap(),
relationships: createRelationshipsObservable(),
_links: {
self: {
href: 'item-href'
},
version: {
href: 'version-href'
}
}
});
@Component({template: ''})
class DummyComponent {
}
describe('VersionedItemComponent', () => {
let component: VersionedItemComponent;
let fixture: ComponentFixture<VersionedItemComponent>;
let versionService: VersionDataService;
let versionHistoryService: VersionHistoryDataService;
const versionServiceSpy = jasmine.createSpyObj('versionService', {
findByHref: createSuccessfulRemoteDataObject$<Version>(new Version()),
});
const versionHistoryServiceSpy = jasmine.createSpyObj('versionHistoryService', {
createVersion: createSuccessfulRemoteDataObject$<Version>(new Version()),
});
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [VersionedItemComponent, DummyComponent],
imports: [RouterTestingModule],
providers: [
{ provide: VersionHistoryDataService, useValue: versionHistoryServiceSpy },
{ provide: TranslateService, useValue: {} },
{ provide: VersionDataService, useValue: versionServiceSpy },
{ provide: NotificationsService, useValue: {} },
{ provide: ItemVersionsSharedService, useValue: {} },
{ provide: WorkspaceitemDataService, useValue: {} },
{ provide: SearchService, useValue: {} },
{ provide: ItemDataService, useValue: {} },
]
}).compileComponents();
versionService = TestBed.inject(VersionDataService);
versionHistoryService = TestBed.inject(VersionHistoryDataService);
});
beforeEach(() => {
fixture = TestBed.createComponent(VersionedItemComponent);
component = fixture.componentInstance;
component.object = mockItem;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
describe('when onCreateNewVersion() is called', () => {
it('should call versionService.findByHref', () => {
component.onCreateNewVersion();
expect(versionService.findByHref).toHaveBeenCalledWith('version-href');
});
});
});

View File

@@ -0,0 +1,78 @@
import { Component } from '@angular/core';
import { ItemComponent } from '../shared/item.component';
import { ItemVersionsSummaryModalComponent } from '../../../../shared/item/item-versions/item-versions-summary-modal/item-versions-summary-modal.component';
import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../../../core/shared/operators';
import { RemoteData } from '../../../../core/data/remote-data';
import { Version } from '../../../../core/shared/version.model';
import { switchMap, tap } from 'rxjs/operators';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { VersionHistoryDataService } from '../../../../core/data/version-history-data.service';
import { TranslateService } from '@ngx-translate/core';
import { VersionDataService } from '../../../../core/data/version-data.service';
import { ItemVersionsSharedService } from '../../../../shared/item/item-versions/item-versions-shared.service';
import { Router } from '@angular/router';
import { WorkspaceitemDataService } from '../../../../core/submission/workspaceitem-data.service';
import { SearchService } from '../../../../core/shared/search/search.service';
import { Item } from '../../../../core/shared/item.model';
import { ItemDataService } from '../../../../core/data/item-data.service';
import { WorkspaceItem } from '../../../../core/submission/models/workspaceitem.model';
@Component({
selector: 'ds-versioned-item',
templateUrl: './versioned-item.component.html',
styleUrls: ['./versioned-item.component.scss']
})
export class VersionedItemComponent extends ItemComponent {
constructor(
private modalService: NgbModal,
private versionHistoryService: VersionHistoryDataService,
private translateService: TranslateService,
private versionService: VersionDataService,
private itemVersionShared: ItemVersionsSharedService,
private router: Router,
private workspaceItemDataService: WorkspaceitemDataService,
private searchService: SearchService,
private itemService: ItemDataService,
) {
super();
}
/**
* Open a modal that allows to create a new version starting from the specified item, with optional summary
*/
onCreateNewVersion(): void {
const item = this.object;
const versionHref = item._links.version.href;
// Open modal
const activeModal = this.modalService.open(ItemVersionsSummaryModalComponent);
// Show current version in modal
this.versionService.findByHref(versionHref).pipe(getFirstCompletedRemoteData()).subscribe((res: RemoteData<Version>) => {
// if res.hasNoContent then the item is unversioned
activeModal.componentInstance.firstVersion = res.hasNoContent;
activeModal.componentInstance.versionNumber = (res.hasNoContent ? undefined : res.payload.version);
});
// On createVersionEvent emitted create new version and notify
activeModal.componentInstance.createVersionEvent.pipe(
switchMap((summary: string) => this.versionHistoryService.createVersion(item._links.self.href, summary)),
getFirstCompletedRemoteData(),
// show success/failure notification
tap((res: RemoteData<Version>) => { this.itemVersionShared.notifyCreateNewVersion(res); }),
// get workspace item
getFirstSucceededRemoteDataPayload<Version>(),
switchMap((newVersion: Version) => this.itemService.findByHref(newVersion._links.item.href)),
getFirstSucceededRemoteDataPayload<Item>(),
switchMap((newVersionItem: Item) => this.workspaceItemDataService.findByItem(newVersionItem.uuid, true, false)),
getFirstSucceededRemoteDataPayload<WorkspaceItem>(),
).subscribe((wsItem) => {
const wsiId = wsItem.id;
const route = 'workspaceitems/' + wsiId + '/edit';
this.router.navigateByUrl(route);
});
}
}

View File

@@ -0,0 +1,68 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { VersionPageComponent } from './version-page.component';
import { ActivatedRoute } from '@angular/router';
import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub';
import { of as observableOf } from 'rxjs';
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { Item } from '../../../core/shared/item.model';
import { createPaginatedList } from '../../../shared/testing/utils.test';
import { createRelationshipsObservable } from '../../simple/item-types/shared/item.component.spec';
import { VersionDataService } from '../../../core/data/version-data.service';
import { AuthService } from '../../../core/auth/auth.service';
import { Version } from '../../../core/shared/version.model';
import { RouterTestingModule } from '@angular/router/testing';
import { Component } from '@angular/core';
const mockItem: Item = Object.assign(new Item(), {
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
metadata: [],
relationships: createRelationshipsObservable(),
uuid: 'item-uuid',
});
const mockVersion: Version = Object.assign(new Version(), {
item: createSuccessfulRemoteDataObject$(mockItem),
version: 1,
});
@Component({ template: '' })
class DummyComponent {
}
describe('VersionPageComponent', () => {
let component: VersionPageComponent;
let fixture: ComponentFixture<VersionPageComponent>;
let authService: AuthService;
const mockRoute = Object.assign(new ActivatedRouteStub(), {
data: observableOf({dso: createSuccessfulRemoteDataObject(mockVersion)})
});
beforeEach(waitForAsync(() => {
authService = jasmine.createSpyObj('authService', {
isAuthenticated: observableOf(true),
setRedirectUrl: {}
});
TestBed.configureTestingModule({
declarations: [VersionPageComponent, DummyComponent],
imports: [RouterTestingModule.withRoutes([{ path: 'items/item-uuid', component: DummyComponent, pathMatch: 'full' }])],
providers: [
{ provide: ActivatedRoute, useValue: mockRoute },
{ provide: VersionDataService, useValue: {} },
{ provide: AuthService, useValue: authService },
],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(VersionPageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,56 @@
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { RemoteData } from '../../../core/data/remote-data';
import { ActivatedRoute, Router } from '@angular/router';
import { AuthService } from '../../../core/auth/auth.service';
import { map, switchMap } from 'rxjs/operators';
import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload, redirectOn4xx } from '../../../core/shared/operators';
import { VersionDataService } from '../../../core/data/version-data.service';
import { Version } from '../../../core/shared/version.model';
import { Item } from '../../../core/shared/item.model';
import { getItemPageRoute } from '../../item-page-routing-paths';
import { getPageNotFoundRoute } from '../../../app-routing-paths';
@Component({
selector: 'ds-version-page',
templateUrl: './version-page.component.html',
styleUrls: ['./version-page.component.scss']
})
export class VersionPageComponent implements OnInit {
versionRD$: Observable<RemoteData<Version>>;
itemRD$: Observable<RemoteData<Item>>;
constructor(
protected route: ActivatedRoute,
private router: Router,
private versionService: VersionDataService,
private authService: AuthService,
) {
}
ngOnInit(): void {
/* Retrieve version from resolver or redirect on 4xx */
this.versionRD$ = this.route.data.pipe(
map((data) => data.dso as RemoteData<Version>),
redirectOn4xx(this.router, this.authService),
);
/* Retrieve item from version and reroute to item's page or handle missing item */
this.versionRD$.pipe(
getFirstSucceededRemoteDataPayload(),
switchMap((version) => version.item),
redirectOn4xx(this.router, this.authService),
getFirstCompletedRemoteData(),
).subscribe((itemRD) => {
if (itemRD.hasNoContent) {
this.router.navigateByUrl(getPageNotFoundRoute(), { skipLocationChange: true });
} else {
const itemUrl = getItemPageRoute(itemRD.payload);
this.router.navigateByUrl(itemUrl);
}
});
}
}

View File

@@ -0,0 +1,54 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { RemoteData } from '../../core/data/remote-data';
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
import { Store } from '@ngrx/store';
import { ResolvedAction } from '../../core/resolving/resolver.actions';
import { Version } from '../../core/shared/version.model';
import { VersionDataService } from '../../core/data/version-data.service';
/**
* The self links defined in this list are expected to be requested somewhere in the near future
* Requesting them as embeds will limit the number of requests
*/
export const VERSION_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig<Version>[] = [
followLink('item'),
];
/**
* This class represents a resolver that requests a specific version before the route is activated
*/
@Injectable()
export class VersionResolver implements Resolve<RemoteData<Version>> {
constructor(
protected versionService: VersionDataService,
protected store: Store<any>,
protected router: Router
) {
}
/**
* Method for resolving a version based on the parameters in the current route
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @returns Observable<<RemoteData<Item>> Emits the found item based on the parameters in the current route,
* or an error if something went wrong
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Version>> {
const versionRD$ = this.versionService.findById(route.params.id,
true,
false,
...VERSION_PAGE_LINKS_TO_FOLLOW
).pipe(
getFirstCompletedRemoteData(),
);
versionRD$.subscribe((versionRD: RemoteData<Version>) => {
this.store.dispatch(new ResolvedAction(state.url, versionRD.payload));
});
return versionRD$;
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,96 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { DsoPageVersionButtonComponent } from './dso-page-version-button.component';
import { Item } from '../../../core/shared/item.model';
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
import { Observable, of, of as observableOf } from 'rxjs';
import { TranslateModule } from '@ngx-translate/core';
import { RouterTestingModule } from '@angular/router/testing';
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
import { By } from '@angular/platform-browser';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { VersionHistoryDataService } from '../../../core/data/version-history-data.service';
describe('DsoPageVersionButtonComponent', () => {
let component: DsoPageVersionButtonComponent;
let fixture: ComponentFixture<DsoPageVersionButtonComponent>;
let authorizationService: AuthorizationDataService;
let versionHistoryService: VersionHistoryDataService;
let dso: Item;
let tooltipMsg: Observable<string>;
const authorizationServiceSpy = jasmine.createSpyObj('authorizationService', ['isAuthorized']);
const versionHistoryServiceSpy = jasmine.createSpyObj('versionHistoryService',
['getVersions', 'getLatestVersionFromHistory$', 'isLatest$', 'hasDraftVersion$']
);
beforeEach(waitForAsync(() => {
dso = Object.assign(new Item(), {
id: 'test-item',
_links: {
self: { href: 'test-item-selflink' },
version: { href: 'test-item-version-selflink' },
},
});
tooltipMsg = of('tooltip-msg');
TestBed.configureTestingModule({
declarations: [DsoPageVersionButtonComponent],
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NgbModule],
providers: [
{ provide: AuthorizationDataService, useValue: authorizationServiceSpy },
{ provide: VersionHistoryDataService, useValue: versionHistoryServiceSpy },
]
}).compileComponents();
authorizationService = TestBed.inject(AuthorizationDataService);
versionHistoryService = TestBed.inject(VersionHistoryDataService);
versionHistoryServiceSpy.hasDraftVersion$.and.returnValue(observableOf(true));
}));
beforeEach(() => {
fixture = TestBed.createComponent(DsoPageVersionButtonComponent);
component = fixture.componentInstance;
component.dso = dso;
component.tooltipMsg$ = tooltipMsg;
fixture.detectChanges();
});
it('should check the authorization of the current user', () => {
expect(authorizationService.isAuthorized).toHaveBeenCalledWith(FeatureID.CanCreateVersion, dso.self);
});
it('should check if the item has a draft version', () => {
expect(versionHistoryServiceSpy.hasDraftVersion$).toHaveBeenCalledWith(dso._links.version.href);
});
describe('when the user is authorized', () => {
beforeEach(() => {
authorizationServiceSpy.isAuthorized.and.returnValue(observableOf(true));
component.ngOnInit();
fixture.detectChanges();
});
it('should render a button', () => {
const button = fixture.debugElement.query(By.css('button'));
expect(button).not.toBeNull();
});
});
describe('when the user is not authorized', () => {
beforeEach(() => {
authorizationServiceSpy.isAuthorized.and.returnValue(observableOf(false));
component.ngOnInit();
fixture.detectChanges();
});
it('should render a button', () => {
const button = fixture.debugElement.query(By.css('button'));
expect(button).toBeNull();
});
});
});

View File

@@ -0,0 +1,78 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
import { Observable } from 'rxjs/internal/Observable';
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
import { VersionHistoryDataService } from '../../../core/data/version-history-data.service';
import { Item } from '../../../core/shared/item.model';
import { map, startWith, switchMap } from 'rxjs/operators';
import { of } from 'rxjs';
@Component({
selector: 'ds-dso-page-version-button',
templateUrl: './dso-page-version-button.component.html',
styleUrls: ['./dso-page-version-button.component.scss']
})
/**
* Display a button linking to the edit page of a DSpaceObject
*/
export class DsoPageVersionButtonComponent implements OnInit {
/**
* The item for which display a button to create a new version
*/
@Input() dso: Item;
/**
* A message for the tooltip on the button
* Supports i18n keys
*/
@Input() tooltipMsgCreate: string;
/**
* A message for the tooltip on the button (when is disabled)
* Supports i18n keys
*/
@Input() tooltipMsgHasDraft: string;
/**
* Emits an event that triggers the creation of the new version
*/
@Output() newVersionEvent = new EventEmitter();
/**
* Whether or not the current user is authorized to create a new version of the DSpaceObject
*/
isAuthorized$: Observable<boolean>;
disableNewVersionButton$: Observable<boolean>;
tooltipMsg$: Observable<string>;
constructor(
protected authorizationService: AuthorizationDataService,
protected versionHistoryService: VersionHistoryDataService,
) {
}
/**
* Creates a new version for the current item
*/
createNewVersion() {
this.newVersionEvent.emit();
}
ngOnInit() {
this.isAuthorized$ = this.authorizationService.isAuthorized(FeatureID.CanCreateVersion, this.dso.self);
this.disableNewVersionButton$ = this.versionHistoryService.hasDraftVersion$(this.dso._links.version.href).pipe(
// button is disabled if hasDraftVersion = true, and enabled if hasDraftVersion = false or null
// (hasDraftVersion is null when a version history does not exist)
map((res) => Boolean(res)),
startWith(true),
);
this.tooltipMsg$ = this.disableNewVersionButton$.pipe(
switchMap((hasDraftVersion) => of(hasDraftVersion ? this.tooltipMsgHasDraft : this.tooltipMsgCreate)),
);
}
}

View File

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

View File

@@ -0,0 +1,31 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ItemVersionsDeleteModalComponent } from './item-versions-delete-modal.component';
import { TranslateModule } from '@ngx-translate/core';
import { RouterTestingModule } from '@angular/router/testing';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
describe('ItemVersionsDeleteModalComponent', () => {
let component: ItemVersionsDeleteModalComponent;
let fixture: ComponentFixture<ItemVersionsDeleteModalComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ItemVersionsDeleteModalComponent],
imports: [ TranslateModule.forRoot(), RouterTestingModule.withRoutes([]) ],
providers: [
{ provide: NgbActiveModal },
]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ItemVersionsDeleteModalComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

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

View File

@@ -0,0 +1,56 @@
import { TestBed } from '@angular/core/testing';
import { ItemVersionsSharedService } from './item-versions-shared.service';
import { ActivatedRoute } from '@angular/router';
import { VersionDataService } from '../../../core/data/version-data.service';
import { AuthService } from '../../../core/auth/auth.service';
import { NotificationsService } from '../../notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
import { VersionHistoryDataService } from '../../../core/data/version-history-data.service';
import { WorkspaceitemDataService } from '../../../core/submission/workspaceitem-data.service';
import { WorkflowItemDataService } from '../../../core/submission/workflowitem-data.service';
import { createFailedRemoteDataObject, createSuccessfulRemoteDataObject } from '../../remote-data.utils';
import { Version } from '../../../core/shared/version.model';
describe('ItemVersionsSharedService', () => {
let service: ItemVersionsSharedService;
let notificationService: NotificationsService;
const successfulVersionRD = createSuccessfulRemoteDataObject<Version>(new Version());
const failedVersionRD = createFailedRemoteDataObject<Version>();
const notificationsServiceSpy = jasmine.createSpyObj('notificationsServiceSpy', ['success', 'error']);
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
{ provide: ActivatedRoute, useValue: {} },
{ provide: VersionDataService, useValue: {} },
{ provide: VersionHistoryDataService, useValue: {} },
{ provide: AuthService, useValue: {} },
{ provide: NotificationsService, useValue: notificationsServiceSpy },
{ provide: TranslateService, useValue: { get: () => undefined, } },
{ provide: WorkspaceitemDataService, useValue: {} },
{ provide: WorkflowItemDataService, useValue: {} },
],
});
service = TestBed.inject(ItemVersionsSharedService);
notificationService = TestBed.inject(NotificationsService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
describe('when notifyCreateNewVersion is called', () => {
it('should notify when successful', () => {
service.notifyCreateNewVersion(successfulVersionRD);
expect(notificationService.success).toHaveBeenCalled();
});
it('should notify when not successful', () => {
service.notifyCreateNewVersion(failedVersionRD);
expect(notificationService.error).toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,35 @@
import { Injectable } from '@angular/core';
import { NotificationsService } from '../../notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
import { RemoteData } from '../../../core/data/remote-data';
import { Version } from '../../../core/shared/version.model';
@Injectable({
providedIn: 'root'
})
export class ItemVersionsSharedService {
constructor(
private notificationsService: NotificationsService,
private translateService: TranslateService,
) {
}
private static msg(key: string): string {
const translationPrefix = 'item.version.create.notification';
return translationPrefix + '.' + key;
}
/**
* Notify success/failure after creating a new version.
*
* @param newVersionRD the new version that has been created
*/
public notifyCreateNewVersion(newVersionRD: RemoteData<Version>): void {
const newVersionNumber = newVersionRD?.payload?.version;
newVersionRD.hasSucceeded ?
this.notificationsService.success(null, this.translateService.get(ItemVersionsSharedService.msg('success'), {version: newVersionNumber})) :
this.notificationsService.error(null, this.translateService.get(ItemVersionsSharedService.msg(newVersionRD?.statusCode === 422 ? 'inProgress' : 'failure')));
}
}

View File

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

View File

@@ -0,0 +1,31 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ItemVersionsSummaryModalComponent } from './item-versions-summary-modal.component';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule } from '@ngx-translate/core';
import { RouterTestingModule } from '@angular/router/testing';
describe('ItemVersionsSummaryModalComponent', () => {
let component: ItemVersionsSummaryModalComponent;
let fixture: ComponentFixture<ItemVersionsSummaryModalComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ ItemVersionsSummaryModalComponent ],
imports: [ TranslateModule.forRoot(), RouterTestingModule.withRoutes([]) ],
providers: [
{ provide: NgbActiveModal },
]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ItemVersionsSummaryModalComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

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

View File

@@ -2,45 +2,146 @@
<div *ngVar="(versionRD$ | async)?.payload as itemVersion">
<div class="mb-2" *ngIf="versions?.page?.length > 0 || displayWhenEmpty">
<h2 *ngIf="displayTitle">{{"item.version.history.head" | translate}}</h2>
<ds-alert [type]="AlertTypeEnum.Info" *ngIf="itemVersion">
{{ "item.version.history.selected.alert" | translate : {version: itemVersion.version} }}
</ds-alert>
<ds-pagination *ngIf="versions?.page?.length > 0"
(paginationChange)="onPageChange()"
[hideGear]="true"
[hidePagerWhenSinglePage]="true"
[paginationOptions]="options"
[pageInfoState]="versions"
[collectionSize]="versions?.totalElements"
[retainScrollPosition]="true">
<table class="table table-striped my-2">
<table class="table table-striped table-bordered align-middle my-2">
<thead>
<tr>
<th scope="col">{{"item.version.history.table.version" | translate}}</th>
<th scope="col">{{"item.version.history.table.item" | translate}}</th>
<th scope="col" *ngIf="(hasEpersons$ | async)">{{"item.version.history.table.editor" | translate}}</th>
<th scope="col">{{"item.version.history.table.date" | translate}}</th>
<th scope="col">{{"item.version.history.table.summary" | translate}}</th>
</tr>
<tr>
<th scope="col">{{"item.version.history.table.version" | 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]="[(itemPageRoutes$ | async)[item?.id]]">{{item?.handle}}</a>
<span *ngIf="version?.id === itemVersion?.id">*</span>
</span>
</td>
<td *ngIf="(hasEpersons$ | async)" class="version-row-element-editor">
<tr *ngFor="let version of versions?.page" [id]="'version-row-' + version.id">
<td class="version-row-element-version">
<!-- Get the ID of the workspace/workflow item (`undefined` if they don't exist).
Conditionals inside *ngVar are needed in order to avoid useless calls. -->
<ng-container *ngVar="((hasDraftVersion$ | async) ? getWorkspaceId(version?.item) : undefined) as workspaceId$">
<ng-container *ngVar=" ((workspaceId$ | async) ? undefined : getWorkflowId(version?.item)) as workflowId$">
<div class="left-column">
<span *ngIf="(workspaceId$ | async) || (workflowId$ | async); then versionNumberWithoutLink else versionNumberWithLink"></span>
<ng-template #versionNumberWithLink>
<a [routerLink]="getVersionRoute(version.id)">{{version.version}}</a>
</ng-template>
<ng-template #versionNumberWithoutLink>
{{version.version}}
</ng-template>
<span *ngIf="version?.id === itemVersion?.id">*</span>
<span *ngIf="workspaceId$ | async" class="text-light badge badge-primary ml-3">
{{ "item.version.history.table.workspaceItem" | translate }}
</span>
<span *ngIf="workflowId$ | async" class="text-light badge badge-info ml-3">
{{ "item.version.history.table.workflowItem" | translate }}
</span>
</div>
<div class="right-column">
<div class="btn-group edit-field" *ngIf="displayActions">
<!--EDIT WORKSPACE ITEM-->
<button class="btn btn-outline-primary btn-sm version-row-element-edit"
*ngIf="workspaceId$ | async"
(click)="editWorkspaceItem(workspaceId$)"
title="{{'item.version.history.table.action.editWorkspaceItem' | translate }}">
<i class="fas fa-pencil-alt fa-fw"></i>
</button>
<!--CREATE-->
<ng-container *ngIf="canCreateVersion$ | async">
<button class="btn btn-outline-primary btn-sm version-row-element-create"
[disabled]="isAnyBeingEdited() || (hasDraftVersion$ | async)"
(click)="createNewVersion(version)"
title="{{createVersionTitle$ | async | translate }}">
<i class="fas fa-code-branch fa-fw"></i>
</button>
</ng-container>
<!--DELETE-->
<ng-container *ngIf="canDeleteVersion$(version) | async">
<button class="btn btn-sm version-row-element-delete"
[ngClass]="isAnyBeingEdited() ? 'btn-outline-primary' : 'btn-outline-danger'"
[disabled]="isAnyBeingEdited()"
(click)="deleteVersion(version, version.id==itemVersion.id)"
title="{{'item.version.history.table.action.deleteVersion' | translate}}">
<i class="fas fa-trash fa-fw"></i>
</button>
</ng-container>
</div>
</div>
</ng-container>
</ng-container>
</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>
</td>
<td class="version-row-element-date">
{{version?.created | date : 'yyyy-MM-dd HH:mm:ss'}}
</td>
<td class="version-row-element-summary">
<div class="float-left">
<ng-container *ngIf="isThisBeingEdited(version); then editSummary else showSummary"></ng-container>
<ng-template #showSummary>{{version?.summary}}</ng-template>
<ng-template #editSummary>
<input class="form-control" type="text" [(ngModel)]="versionBeingEditedSummary"
(keyup.enter)="onSummarySubmit()"/>
</ng-template>
</div>
<div class="float-right btn-group edit-field" *ngIf="displayActions">
<!--DISCARD EDIT -->
<ng-container *ngIf="(canEditVersion$(version) | async) && isThisBeingEdited(version)">
<button class="btn btn-sm"
[ngClass]="isThisBeingEdited(version) ? 'btn-outline-warning' : 'btn-outline-primary'"
(click)="disableVersionEditing()"
title="{{'item.version.history.table.action.discardSummary' | translate}}">
<i class="fas fa-undo-alt fa-fw"></i>
</button>
</ng-container>
<!--EDIT / SAVE-->
<ng-container *ngIf="canEditVersion$(version) | async">
<button class="btn btn-outline-primary btn-sm version-row-element-edit"
*ngIf="!isThisBeingEdited(version)"
[disabled]="isAnyBeingEdited()"
(click)="enableVersionEditing(version)"
title="{{'item.version.history.table.action.editSummary' | translate}}">
<i class="fas fa-edit fa-fw"></i>
</button>
<button class="btn btn-outline-success btn-sm"
*ngIf="isThisBeingEdited(version)"
(click)="onSummarySubmit()"
title="{{'item.version.history.table.action.saveSummary' | translate}}">
<i class="fas fa-check fa-fw"></i>
</button>
</ng-container>
</div>
</td>
</tr>
</tbody>
</table>
<div>*&nbsp;{{"item.version.history.selected" | translate}}</div>
</ds-pagination>
<ds-alert *ngIf="!itemVersion || versions?.page?.length === 0" [content]="'item.version.history.empty'" [type]="AlertTypeEnum.Info"></ds-alert>
<ds-alert *ngIf="!itemVersion || versions?.page?.length === 0" [content]="'item.version.history.empty'"
[type]="AlertTypeEnum.Info"></ds-alert>
</div>
</div>
</div>

View File

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

View File

@@ -11,70 +11,138 @@ import { VersionHistoryDataService } from '../../../core/data/version-history-da
import { By } from '@angular/platform-browser';
import { createSuccessfulRemoteDataObject$ } from '../../remote-data.utils';
import { createPaginatedList } from '../../testing/utils.test';
import { PaginationComponentOptions } from '../../pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
import { of as observableOf } from 'rxjs';
import { EMPTY, of, of as observableOf } from 'rxjs';
import { PaginationService } from '../../../core/pagination/pagination.service';
import { PaginationServiceStub } from '../../testing/pagination-service.stub';
import { AuthService } from '../../../core/auth/auth.service';
import { VersionDataService } from '../../../core/data/version-data.service';
import { ItemDataService } from '../../../core/data/item-data.service';
import { FormBuilder } from '@angular/forms';
import { NotificationsService } from '../../notifications/notifications.service';
import { NotificationsServiceStub } from '../../testing/notifications-service.stub';
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
import { WorkspaceitemDataService } from '../../../core/submission/workspaceitem-data.service';
import { WorkflowItemDataService } from '../../../core/submission/workflowitem-data.service';
describe('ItemVersionsComponent', () => {
let component: ItemVersionsComponent;
let fixture: ComponentFixture<ItemVersionsComponent>;
let authenticationService: AuthService;
let authorizationService: AuthorizationDataService;
let versionHistoryService: VersionHistoryDataService;
let workspaceItemDataService: WorkspaceitemDataService;
let workflowItemDataService: WorkflowItemDataService;
let versionService: VersionDataService;
const versionHistory = Object.assign(new VersionHistory(), {
id: '1'
id: '1',
draftVersion: true,
});
const version1 = Object.assign(new Version(), {
id: '1',
version: 1,
created: new Date(2020, 1, 1),
summary: 'first version',
versionhistory: createSuccessfulRemoteDataObject$(versionHistory)
versionhistory: createSuccessfulRemoteDataObject$(versionHistory),
_links: {
self: {
href: 'version2-url',
},
},
});
const version2 = Object.assign(new Version(), {
id: '2',
version: 2,
summary: 'second version',
created: new Date(2020, 1, 2),
versionhistory: createSuccessfulRemoteDataObject$(versionHistory)
versionhistory: createSuccessfulRemoteDataObject$(versionHistory),
_links: {
self: {
href: 'version2-url',
},
},
});
const versions = [version1, version2];
versionHistory.versions = createSuccessfulRemoteDataObject$(createPaginatedList(versions));
const item1 = Object.assign(new Item(), {
const item1 = Object.assign(new Item(), { // is a workspace item
uuid: 'item-identifier-1',
handle: '123456789/1',
version: createSuccessfulRemoteDataObject$(version1)
version: createSuccessfulRemoteDataObject$(version1),
_links: {
self: {
href: '/items/item-identifier-1'
}
}
});
const item2 = Object.assign(new Item(), {
uuid: 'item-identifier-2',
handle: '123456789/2',
version: createSuccessfulRemoteDataObject$(version2)
version: createSuccessfulRemoteDataObject$(version2),
_links: {
self: {
href: '/items/item-identifier-2'
}
}
});
const items = [item1, item2];
version1.item = createSuccessfulRemoteDataObject$(item1);
version2.item = createSuccessfulRemoteDataObject$(item2);
const versionHistoryService = jasmine.createSpyObj('versionHistoryService', {
getVersions: createSuccessfulRemoteDataObject$(createPaginatedList(versions))
const versionHistoryServiceSpy = jasmine.createSpyObj('versionHistoryService', {
getVersions: createSuccessfulRemoteDataObject$(createPaginatedList(versions)),
});
const authenticationServiceSpy = jasmine.createSpyObj('authenticationService', {
isAuthenticated: observableOf(true),
setRedirectUrl: {}
});
const authorizationServiceSpy = jasmine.createSpyObj('authorizationService', ['isAuthorized']);
const workspaceItemDataServiceSpy = jasmine.createSpyObj('workspaceItemDataService', {
findByItem: EMPTY,
});
const workflowItemDataServiceSpy = jasmine.createSpyObj('workflowItemDataService', {
findByItem: EMPTY,
});
const versionServiceSpy = jasmine.createSpyObj('versionService', {
findById: EMPTY,
});
const paginationService = new PaginationServiceStub();
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ItemVersionsComponent, VarDirective],
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
providers: [
{ provide: VersionHistoryDataService, useValue: versionHistoryService },
{ provide: PaginationService, useValue: paginationService }
{provide: PaginationService, useValue: new PaginationServiceStub()},
{provide: FormBuilder, useValue: new FormBuilder()},
{provide: NotificationsService, useValue: new NotificationsServiceStub()},
{provide: AuthService, useValue: authenticationServiceSpy},
{provide: AuthorizationDataService, useValue: authorizationServiceSpy},
{provide: VersionHistoryDataService, useValue: versionHistoryServiceSpy},
{provide: ItemDataService, useValue: {}},
{provide: VersionDataService, useValue: versionServiceSpy},
{provide: WorkspaceitemDataService, useValue: workspaceItemDataServiceSpy},
{provide: WorkflowItemDataService, useValue: workflowItemDataServiceSpy},
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
versionHistoryService = TestBed.inject(VersionHistoryDataService);
authenticationService = TestBed.inject(AuthService);
authorizationService = TestBed.inject(AuthorizationDataService);
workspaceItemDataService = TestBed.inject(WorkspaceitemDataService);
workflowItemDataService = TestBed.inject(WorkflowItemDataService);
versionService = TestBed.inject(VersionDataService);
}));
beforeEach(() => {
fixture = TestBed.createComponent(ItemVersionsComponent);
component = fixture.componentInstance;
component.item = item1;
component.displayActions = true;
fixture.detectChanges();
});
@@ -88,26 +156,29 @@ describe('ItemVersionsComponent', () => {
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);
expect(id.nativeElement.textContent).toContain(version.version.toString());
});
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
// Check if the current version 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`));
it('should add an asterisk to the version of the selected item', () => {
const item = fixture.debugElement.query(By.css(`#version-row-${version.id} .version-row-element-version`));
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);
switch (versionItem.uuid) {
case item1.uuid:
expect(date.nativeElement.textContent.trim()).toEqual('2020-02-01 00:00:00');
break;
case item2.uuid:
expect(date.nativeElement.textContent.trim()).toEqual('2020-02-02 00:00:00');
break;
default:
throw new Error('Unexpected versionItem');
}
});
it(`should display summary ${version.summary} in the correct column for version ${version.id}`, () => {
@@ -115,4 +186,85 @@ describe('ItemVersionsComponent', () => {
expect(summary.nativeElement.textContent).toEqual(version.summary);
});
});
describe('when the user can only delete a version', () => {
beforeAll(waitForAsync(() => {
const canDelete = (featureID: FeatureID, url: string ) => of(featureID === FeatureID.CanDeleteVersion);
authorizationServiceSpy.isAuthorized.and.callFake(canDelete);
}));
it('should not disable the delete button', () => {
const deleteButtons = fixture.debugElement.queryAll(By.css(`.version-row-element-delete`));
deleteButtons.forEach((btn) => {
expect(btn.nativeElement.disabled).toBe(false);
});
});
it('should disable other buttons', () => {
const createButtons = fixture.debugElement.queryAll(By.css(`.version-row-element-create`));
createButtons.forEach((btn) => {
expect(btn.nativeElement.disabled).toBe(true);
});
const editButtons = fixture.debugElement.queryAll(By.css(`.version-row-element-create`));
editButtons.forEach((btn) => {
expect(btn.nativeElement.disabled).toBe(true);
});
});
});
describe('when page is changed', () => {
it('should call getAllVersions', () => {
spyOn(component, 'getAllVersions');
component.onPageChange();
expect(component.getAllVersions).toHaveBeenCalled();
});
});
describe('when onSummarySubmit() is called', () => {
const id = 'version-being-edited-id';
beforeEach(() => {
component.versionBeingEditedId = id;
});
it('should call versionService.findById', () => {
component.onSummarySubmit();
expect(versionService.findById).toHaveBeenCalledWith(id);
});
});
describe('when editing is enabled for an item', () => {
beforeEach(() => {
component.enableVersionEditing(version1);
});
it('should set all variables', () => {
expect(component.versionBeingEditedSummary).toEqual('first version');
expect(component.versionBeingEditedNumber).toEqual(1);
expect(component.versionBeingEditedId).toEqual('1');
});
it('isAnyBeingEdited should be true', () => {
expect(component.isAnyBeingEdited()).toBeTrue();
});
it('isThisBeingEdited should be true for version1', () => {
expect(component.isThisBeingEdited(version1)).toBeTrue();
});
it('isThisBeingEdited should be false for version2', () => {
expect(component.isThisBeingEdited(version2)).toBeFalse();
});
});
describe('when editing is disabled', () => {
beforeEach(() => {
component.disableVersionEditing();
});
it('should unset all variables', () => {
expect(component.versionBeingEditedSummary).toBeUndefined();
expect(component.versionBeingEditedNumber).toBeUndefined();
expect(component.versionBeingEditedId).toBeUndefined();
});
it('isAnyBeingEdited should be false', () => {
expect(component.isAnyBeingEdited()).toBeFalse();
});
it('isThisBeingEdited should be false for all versions', () => {
expect(component.isThisBeingEdited(version1)).toBeFalse();
expect(component.isThisBeingEdited(version2)).toBeFalse();
});
});
});

View File

@@ -2,14 +2,24 @@ import { Component, Input, OnInit } from '@angular/core';
import { Item } from '../../../core/shared/item.model';
import { Version } from '../../../core/shared/version.model';
import { RemoteData } from '../../../core/data/remote-data';
import { BehaviorSubject, combineLatest as observableCombineLatest, Observable } from 'rxjs';
import {
BehaviorSubject,
combineLatest,
combineLatest as observableCombineLatest,
Observable,
of,
Subscription,
} from 'rxjs';
import { VersionHistory } from '../../../core/shared/version-history.model';
import {
getAllSucceededRemoteData,
getAllSucceededRemoteDataPayload,
getFirstCompletedRemoteData,
getFirstSucceededRemoteData,
getFirstSucceededRemoteDataPayload,
getRemoteDataPayload
} from '../../../core/shared/operators';
import { map, startWith, switchMap } from 'rxjs/operators';
import { map, mergeMap, startWith, switchMap, take, tap } from 'rxjs/operators';
import { PaginatedList } from '../../../core/data/paginated-list.model';
import { PaginationComponentOptions } from '../../pagination/pagination-component-options.model';
import { VersionHistoryDataService } from '../../../core/data/version-history-data.service';
@@ -18,16 +28,38 @@ import { AlertType } from '../../alert/aletr-type';
import { followLink } from '../../utils/follow-link-config.model';
import { hasValue, hasValueOperator } from '../../empty.util';
import { PaginationService } from '../../../core/pagination/pagination.service';
import { getItemPageRoute } from '../../../item-page/item-page-routing-paths';
import {
getItemEditVersionhistoryRoute,
getItemPageRoute,
getItemVersionRoute
} from '../../../item-page/item-page-routing-paths';
import { FormBuilder } from '@angular/forms';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { ItemVersionsSummaryModalComponent } from './item-versions-summary-modal/item-versions-summary-modal.component';
import { NotificationsService } from '../../notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
import { ItemVersionsDeleteModalComponent } from './item-versions-delete-modal/item-versions-delete-modal.component';
import { VersionDataService } from '../../../core/data/version-data.service';
import { ItemDataService } from '../../../core/data/item-data.service';
import { Router } from '@angular/router';
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
import { ItemVersionsSharedService } from './item-versions-shared.service';
import { WorkspaceItem } from '../../../core/submission/models/workspaceitem.model';
import { WorkspaceitemDataService } from '../../../core/submission/workspaceitem-data.service';
import { WorkflowItemDataService } from '../../../core/submission/workflowitem-data.service';
@Component({
selector: 'ds-item-versions',
templateUrl: './item-versions.component.html'
templateUrl: './item-versions.component.html',
styleUrls: ['./item-versions.component.scss']
})
/**
* Component listing all available versions of the history the provided item is a part of
*/
export class ItemVersionsComponent implements OnInit {
/**
* The item to display a version history for
*/
@@ -45,6 +77,16 @@ export class ItemVersionsComponent implements OnInit {
*/
@Input() displayTitle = true;
/**
* Whether or not to display the action buttons (delete/create/edit version)
*/
@Input() displayActions: boolean;
/**
* Array of active subscriptions
*/
subs: Subscription[] = [];
/**
* The AlertType enumeration
* @type {AlertType}
@@ -57,14 +99,19 @@ export class ItemVersionsComponent implements OnInit {
versionRD$: Observable<RemoteData<Version>>;
/**
* The item's full version history
* The item's full version history (remote data)
*/
versionHistoryRD$: Observable<RemoteData<VersionHistory>>;
/**
* The item's full version history
*/
versionHistory$: Observable<VersionHistory>;
/**
* The version history's list of versions
*/
versionsRD$: Observable<RemoteData<PaginatedList<Version>>>;
versionsRD$: BehaviorSubject<RemoteData<PaginatedList<Version>>> = new BehaviorSubject<RemoteData<PaginatedList<Version>>>(null);
/**
* Verify if the list of versions has at least one e-person to display
@@ -72,6 +119,12 @@ export class ItemVersionsComponent implements OnInit {
*/
hasEpersons$: Observable<boolean>;
/**
* Verify if there is an inprogress submission in the version history
* Used to disable the "Create version" button
*/
hasDraftVersion$: Observable<boolean>;
/**
* The amount of versions to display per page
*/
@@ -81,17 +134,12 @@ export class ItemVersionsComponent implements OnInit {
* The page options to use for fetching the versions
* Start at page 1 and always use the set page size
*/
options = Object.assign(new PaginationComponentOptions(),{
options = Object.assign(new PaginationComponentOptions(), {
id: 'ivo',
currentPage: 1,
pageSize: this.pageSize
});
/**
* The current page being displayed
*/
currentPage$ = new BehaviorSubject<number>(1);
/**
* The routes to the versions their item pages
* Key: Item ID
@@ -101,9 +149,301 @@ export class ItemVersionsComponent implements OnInit {
[itemId: string]: string
}>;
/**
* The number of the version whose summary is currently being edited
*/
versionBeingEditedNumber: number;
/**
* The id of the version whose summary is currently being edited
*/
versionBeingEditedId: string;
/**
* The summary currently being edited
*/
versionBeingEditedSummary: string;
canCreateVersion$: Observable<boolean>;
createVersionTitle$: Observable<string>;
constructor(private versionHistoryService: VersionHistoryDataService,
private paginationService: PaginationService
) {
private versionService: VersionDataService,
private itemService: ItemDataService,
private paginationService: PaginationService,
private formBuilder: FormBuilder,
private modalService: NgbModal,
private notificationsService: NotificationsService,
private translateService: TranslateService,
private router: Router,
private itemVersionShared: ItemVersionsSharedService,
private authorizationService: AuthorizationDataService,
private workspaceItemDataService: WorkspaceitemDataService,
private workflowItemDataService: WorkflowItemDataService,
) {
}
/**
* True when a version is being edited
* (used to disable buttons for other versions)
*/
isAnyBeingEdited(): boolean {
return this.versionBeingEditedNumber != null;
}
/**
* True if the specified version is being edited
* (used to show input field and to change buttons for specified version)
*/
isThisBeingEdited(version: Version): boolean {
return version?.version === this.versionBeingEditedNumber;
}
/**
* Enables editing for the specified version
*/
enableVersionEditing(version: Version): void {
this.versionBeingEditedSummary = version?.summary;
this.versionBeingEditedNumber = version?.version;
this.versionBeingEditedId = version?.id;
}
/**
* Disables editing for the specified version and discards all pending changes
*/
disableVersionEditing(): void {
this.versionBeingEditedSummary = undefined;
this.versionBeingEditedNumber = undefined;
this.versionBeingEditedId = undefined;
}
/**
* Get the route to the specified version
* @param versionId the ID of the version for which the route will be retrieved
*/
getVersionRoute(versionId: string) {
return getItemVersionRoute(versionId);
}
/**
* Applies changes to version currently being edited
*/
onSummarySubmit() {
const successMessageKey = 'item.version.edit.notification.success';
const failureMessageKey = 'item.version.edit.notification.failure';
this.versionService.findById(this.versionBeingEditedId).pipe(
getFirstSucceededRemoteData(),
switchMap((findRes: RemoteData<Version>) => {
const payload = findRes.payload;
const summary = {summary: this.versionBeingEditedSummary,};
const updatedVersion = Object.assign({}, payload, summary);
return this.versionService.update(updatedVersion).pipe(getFirstCompletedRemoteData<Version>());
}),
).subscribe((updatedVersionRD: RemoteData<Version>) => {
if (updatedVersionRD.hasSucceeded) {
this.notificationsService.success(null, this.translateService.get(successMessageKey, {'version': this.versionBeingEditedNumber}));
this.getAllVersions(this.versionHistory$);
} else {
this.notificationsService.warning(null, this.translateService.get(failureMessageKey, {'version': this.versionBeingEditedNumber}));
}
this.disableVersionEditing();
}
);
}
/**
* Delete the item and get the result of the operation
* @param item
*/
deleteItemAndGetResult$(item: Item): Observable<boolean> {
return this.itemService.delete(item.id).pipe(
getFirstCompletedRemoteData(),
map((deleteItemRes) => deleteItemRes.hasSucceeded),
take(1),
);
}
/**
* Deletes the specified version, notify the success/failure and redirect to latest version
* @param version the version to be deleted
* @param redirectToLatest force the redirect to the latest version in the history
*/
deleteVersion(version: Version, redirectToLatest: boolean): void {
const successMessageKey = 'item.version.delete.notification.success';
const failureMessageKey = 'item.version.delete.notification.failure';
const versionNumber = version.version;
const versionItem$ = version.item;
// Open modal
const activeModal = this.modalService.open(ItemVersionsDeleteModalComponent);
activeModal.componentInstance.versionNumber = version.version;
activeModal.componentInstance.firstVersion = false;
// On modal submit/dismiss
activeModal.result.then(() => {
versionItem$.pipe(
getFirstSucceededRemoteDataPayload<Item>(),
// Retrieve version history and invalidate cache
mergeMap((item: Item) => combineLatest([
of(item),
this.versionHistoryService.getVersionHistoryFromVersion$(version).pipe(
tap((versionHistory: VersionHistory) => {
this.versionHistoryService.invalidateVersionHistoryCache(versionHistory.id);
})
)
])),
// Delete item
mergeMap(([item, versionHistory]: [Item, VersionHistory]) => combineLatest([
this.deleteItemAndGetResult$(item),
of(versionHistory)
])),
// Retrieve new latest version
mergeMap(([deleteItemResult, versionHistory]: [boolean, VersionHistory]) => combineLatest([
of(deleteItemResult),
this.versionHistoryService.getLatestVersionItemFromHistory$(versionHistory).pipe(
tap(() => {
this.getAllVersions(of(versionHistory));
}),
)
])),
).subscribe(([deleteHasSucceeded, newLatestVersionItem]: [boolean, Item]) => {
// Notify operation result and redirect to latest item
if (deleteHasSucceeded) {
this.notificationsService.success(null, this.translateService.get(successMessageKey, {'version': versionNumber}));
} else {
this.notificationsService.error(null, this.translateService.get(failureMessageKey, {'version': versionNumber}));
}
if (redirectToLatest) {
const path = getItemEditVersionhistoryRoute(newLatestVersionItem);
this.router.navigateByUrl(path);
}
});
});
}
/**
* Creates a new version starting from the specified one
* @param version the version from which a new one will be created
*/
createNewVersion(version: Version) {
const versionNumber = version.version;
// Open modal and set current version number
const activeModal = this.modalService.open(ItemVersionsSummaryModalComponent);
activeModal.componentInstance.versionNumber = versionNumber;
// On createVersionEvent emitted create new version and notify
activeModal.componentInstance.createVersionEvent.pipe(
mergeMap((summary: string) => combineLatest([
of(summary),
version.item.pipe(getFirstSucceededRemoteDataPayload())
])),
mergeMap(([summary, item]: [string, Item]) => this.versionHistoryService.createVersion(item._links.self.href, summary)),
// show success/failure notification
tap((newVersionRD: RemoteData<Version>) => {
this.itemVersionShared.notifyCreateNewVersion(newVersionRD);
if (newVersionRD.hasSucceeded) {
const versionHistory$ = this.versionService.getHistoryFromVersion(version).pipe(
tap((versionHistory: VersionHistory) => {
this.itemService.invalidateItemCache(this.item.uuid);
this.versionHistoryService.invalidateVersionHistoryCache(versionHistory.id);
}),
);
this.getAllVersions(versionHistory$);
}
}),
// get workspace item
getFirstSucceededRemoteDataPayload<Version>(),
switchMap((newVersion: Version) => this.itemService.findByHref(newVersion._links.item.href)),
getFirstSucceededRemoteDataPayload<Item>(),
switchMap((newVersionItem: Item) => this.workspaceItemDataService.findByItem(newVersionItem.uuid, true, false)),
getFirstSucceededRemoteDataPayload<WorkspaceItem>(),
).subscribe((wsItem) => {
const wsiId = wsItem.id;
const route = 'workspaceitems/' + wsiId + '/edit';
this.router.navigateByUrl(route);
});
}
/**
* Check is the current user can edit the version summary
* @param version
*/
canEditVersion$(version: Version): Observable<boolean> {
return this.authorizationService.isAuthorized(FeatureID.CanEditVersion, version.self);
}
/**
* Check if the current user can delete the version
* @param version
*/
canDeleteVersion$(version: Version): Observable<boolean> {
return this.authorizationService.isAuthorized(FeatureID.CanDeleteVersion, version.self);
}
/**
* Get all versions for the given version history and store them in versionRD$
* @param versionHistory$
*/
getAllVersions(versionHistory$: Observable<VersionHistory>): void {
const currentPagination = this.paginationService.getCurrentPagination(this.options.id, this.options);
observableCombineLatest([versionHistory$, currentPagination]).pipe(
switchMap(([versionHistory, options]: [VersionHistory, PaginationComponentOptions]) => {
return this.versionHistoryService.getVersions(versionHistory.id,
new PaginatedSearchOptions({pagination: Object.assign({}, options, {currentPage: options.currentPage})}),
false, true, followLink('item'), followLink('eperson'));
}),
getFirstCompletedRemoteData(),
).subscribe((res: RemoteData<PaginatedList<Version>>) => {
this.versionsRD$.next(res);
});
}
/**
* Updates the page
*/
onPageChange() {
this.getAllVersions(this.versionHistory$);
}
/**
* Get the ID of the workspace item, if present, otherwise return undefined
* @param versionItem the item for which retrieve the workspace item id
*/
getWorkspaceId(versionItem): Observable<string> {
return versionItem.pipe(
getFirstSucceededRemoteDataPayload(),
map((item: Item) => item.uuid),
switchMap((itemUuid: string) => this.workspaceItemDataService.findByItem(itemUuid, true)),
getFirstCompletedRemoteData<WorkspaceItem>(),
map((res: RemoteData<WorkspaceItem>) => res?.payload?.id ),
);
}
/**
* Get the ID of the workflow item, if present, otherwise return undefined
* @param versionItem the item for which retrieve the workspace item id
*/
getWorkflowId(versionItem): Observable<string> {
return versionItem.pipe(
getFirstSucceededRemoteDataPayload(),
map((item: Item) => item.uuid),
switchMap((itemUuid: string) => this.workflowItemDataService.findByItem(itemUuid, true)),
getFirstCompletedRemoteData<WorkspaceItem>(),
map((res: RemoteData<WorkspaceItem>) => res?.payload?.id ),
);
}
/**
* redirect to the edit page of the workspace item
* @param id$ the id of the workspace item
*/
editWorkspaceItem(id$: Observable<string>) {
id$.subscribe((id) => {
this.router.navigateByUrl('workspaceitems/' + id + '/edit');
});
}
/**
@@ -116,20 +456,27 @@ export class ItemVersionsComponent implements OnInit {
getAllSucceededRemoteData(),
getRemoteDataPayload(),
hasValueOperator(),
switchMap((version: Version) => version.versionhistory)
switchMap((version: Version) => version.versionhistory),
);
const versionHistory$ = this.versionHistoryRD$.pipe(
getAllSucceededRemoteData(),
getRemoteDataPayload(),
this.versionHistory$ = this.versionHistoryRD$.pipe(
getFirstSucceededRemoteDataPayload(),
hasValueOperator(),
);
const currentPagination = this.paginationService.getCurrentPagination(this.options.id, this.options);
this.versionsRD$ = observableCombineLatest(versionHistory$, currentPagination).pipe(
switchMap(([versionHistory, options]: [VersionHistory, PaginationComponentOptions]) =>
this.versionHistoryService.getVersions(versionHistory.id,
new PaginatedSearchOptions({pagination: Object.assign({}, options, { currentPage: options.currentPage })}),
true, true, followLink('item'), followLink('eperson')))
this.canCreateVersion$ = this.authorizationService.isAuthorized(FeatureID.CanCreateVersion, this.item.self);
// If there is a draft item in the version history the 'Create version' button is disabled and a different tooltip message is shown
this.hasDraftVersion$ = this.versionHistoryRD$.pipe(
getFirstSucceededRemoteDataPayload(),
map((res) => Boolean(res?.draftVersion)),
);
this.createVersionTitle$ = this.hasDraftVersion$.pipe(
take(1),
switchMap((res) => of(res ? 'item.version.history.table.action.hasDraft' : 'item.version.history.table.action.newVersion'))
);
this.getAllVersions(this.versionHistory$);
this.hasEpersons$ = this.versionsRD$.pipe(
getAllSucceededRemoteData(),
getRemoteDataPayload(),
@@ -150,8 +497,15 @@ export class ItemVersionsComponent implements OnInit {
}
ngOnDestroy(): void {
this.cleanupSubscribes();
this.paginationService.clearPagination(this.options.id);
}
/**
* Unsub all subscriptions
*/
cleanupSubscribes() {
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
}
}

View File

@@ -1,4 +1,4 @@
<ds-alert *ngIf="isLatestVersion$ && !(isLatestVersion$ | async)"
<ds-alert *ngIf="showLatestVersionNotice$ && (showLatestVersionNotice$ | async)"
[content]="('item.version.notice' | translate:{ destination: getItemPage(((latestVersion$ | async)?.item | async)?.payload) })"
[dismissible]="false"
[type]="AlertTypeEnum.Warning">

View File

@@ -10,10 +10,13 @@ import { VersionHistoryDataService } from '../../../../core/data/version-history
import { By } from '@angular/platform-browser';
import { createSuccessfulRemoteDataObject$ } from '../../../remote-data.utils';
import { createPaginatedList } from '../../../testing/utils.test';
import { of } from 'rxjs';
import { take } from 'rxjs/operators';
describe('ItemVersionsNoticeComponent', () => {
let component: ItemVersionsNoticeComponent;
let fixture: ComponentFixture<ItemVersionsNoticeComponent>;
let versionHistoryService: VersionHistoryDataService;
const versionHistory = Object.assign(new VersionHistory(), {
id: '1'
@@ -48,19 +51,29 @@ describe('ItemVersionsNoticeComponent', () => {
});
firstVersion.item = createSuccessfulRemoteDataObject$(firstItem);
latestVersion.item = createSuccessfulRemoteDataObject$(latestItem);
const versionHistoryService = jasmine.createSpyObj('versionHistoryService', {
getVersions: createSuccessfulRemoteDataObject$(createPaginatedList(versions))
});
const versionHistoryServiceSpy = jasmine.createSpyObj('versionHistoryService',
['getVersions', 'getLatestVersionFromHistory$', 'isLatest$', ]
);
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ItemVersionsNoticeComponent],
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
providers: [
{ provide: VersionHistoryDataService, useValue: versionHistoryService }
{ provide: VersionHistoryDataService, useValue: versionHistoryServiceSpy }
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
versionHistoryService = TestBed.inject(VersionHistoryDataService);
const isLatestFcn = (version: Version) => of((version.version === latestVersion.version));
versionHistoryServiceSpy.getVersions.and.returnValue(createSuccessfulRemoteDataObject$(createPaginatedList(versions)));
versionHistoryServiceSpy.getLatestVersionFromHistory$.and.returnValue(of(latestVersion));
versionHistoryServiceSpy.isLatest$.and.callFake(isLatestFcn);
}));
describe('when the item is the latest version', () => {
@@ -85,6 +98,19 @@ describe('ItemVersionsNoticeComponent', () => {
});
});
describe('isLatest', () => {
it('firstVersion should not be the latest', () => {
versionHistoryService.isLatest$(firstVersion).pipe(take(1)).subscribe((res) => {
expect(res).toBeFalse();
});
});
it('latestVersion should be the latest', () => {
versionHistoryService.isLatest$(latestVersion).pipe(take(1)).subscribe((res) => {
expect(res).toBeTrue();
});
});
});
function initComponentWithItem(item: Item) {
fixture = TestBed.createComponent(ItemVersionsNoticeComponent);
component = fixture.componentInstance;

View File

@@ -1,15 +1,16 @@
import { Component, Input, OnInit } from '@angular/core';
import { Item } from '../../../../core/shared/item.model';
import { PaginationComponentOptions } from '../../../pagination/pagination-component-options.model';
import { PaginatedSearchOptions } from '../../../search/paginated-search-options.model';
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
import { Observable } from 'rxjs';
import { RemoteData } from '../../../../core/data/remote-data';
import { VersionHistory } from '../../../../core/shared/version-history.model';
import { Version } from '../../../../core/shared/version.model';
import { hasValue, hasValueOperator } from '../../../empty.util';
import { getAllSucceededRemoteData, getRemoteDataPayload } from '../../../../core/shared/operators';
import { filter, map, startWith, switchMap } from 'rxjs/operators';
import { followLink } from '../../../utils/follow-link-config.model';
import {
getAllSucceededRemoteData,
getFirstSucceededRemoteDataPayload,
getRemoteDataPayload
} from '../../../../core/shared/operators';
import { map, startWith, switchMap } from 'rxjs/operators';
import { VersionHistoryDataService } from '../../../../core/data/version-history-data.service';
import { AlertType } from '../../../alert/aletr-type';
import { getItemPageRoute } from '../../../../item-page/item-page-routing-paths';
@@ -47,16 +48,11 @@ export class ItemVersionsNoticeComponent implements OnInit {
* Is the item's version equal to the latest version from the version history?
* This will determine whether or not to display a notice linking to the latest version
*/
isLatestVersion$: Observable<boolean>;
showLatestVersionNotice$: Observable<boolean>;
/**
* Pagination options to fetch a single version on the first page (this is the latest version in the history)
*/
latestVersionOptions = Object.assign(new PaginationComponentOptions(),{
id: 'item-newest-version-options',
currentPage: 1,
pageSize: 1
});
/**
* The AlertType enumeration
@@ -71,7 +67,6 @@ export class ItemVersionsNoticeComponent implements OnInit {
* Initialize the component's observables
*/
ngOnInit(): void {
const latestVersionSearch = new PaginatedSearchOptions({pagination: this.latestVersionOptions});
if (hasValue(this.item.version)) {
this.versionRD$ = this.item.version;
this.versionHistoryRD$ = this.versionRD$.pipe(
@@ -80,25 +75,17 @@ export class ItemVersionsNoticeComponent implements OnInit {
hasValueOperator(),
switchMap((version: Version) => version.versionhistory)
);
const versionHistory$ = this.versionHistoryRD$.pipe(
getAllSucceededRemoteData(),
getRemoteDataPayload(),
);
this.latestVersion$ = versionHistory$.pipe(
switchMap((versionHistory: VersionHistory) =>
this.versionHistoryService.getVersions(versionHistory.id, latestVersionSearch, true, true, followLink('item'))),
getAllSucceededRemoteData(),
getRemoteDataPayload(),
hasValueOperator(),
filter((versions) => versions.page.length > 0),
map((versions) => versions.page[0])
this.latestVersion$ = this.versionHistoryRD$.pipe(
getFirstSucceededRemoteDataPayload(),
switchMap((vh) => this.versionHistoryService.getLatestVersionFromHistory$(vh))
);
this.isLatestVersion$ = observableCombineLatest(
this.versionRD$.pipe(getAllSucceededRemoteData(), getRemoteDataPayload()), this.latestVersion$
).pipe(
map(([itemVersion, latestVersion]: [Version, Version]) => itemVersion.id === latestVersion.id),
startWith(true)
this.showLatestVersionNotice$ = this.versionRD$.pipe(
getFirstSucceededRemoteDataPayload(),
switchMap((version) => this.versionHistoryService.isLatest$(version)),
map((isLatest) => isLatest != null && !isLatest),
startWith(false),
);
}
}

View File

@@ -16,7 +16,7 @@
</div>
</div>
</div>
</div>
</div>
</div>
<ng-content></ng-content>

View File

@@ -211,6 +211,7 @@ import { CollectionSidebarSearchListElementComponent } from './object-list/sideb
import { CommunitySidebarSearchListElementComponent } from './object-list/sidebar-search-list-element/community/community-sidebar-search-list-element.component';
import { AuthorizedCollectionSelectorComponent } from './dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component';
import { DsoPageEditButtonComponent } from './dso-page/dso-page-edit-button/dso-page-edit-button.component';
import { DsoPageVersionButtonComponent } from './dso-page/dso-page-version-button/dso-page-version-button.component';
import { HoverClassDirective } from './hover-class.directive';
import { ValidationSuggestionsComponent } from './input-suggestions/validation-suggestions/validation-suggestions.component';
import { ItemAlertsComponent } from './item/item-alerts/item-alerts.component';
@@ -233,6 +234,8 @@ import { OnClickMenuItemComponent } from './menu/menu-item/onclick-menu-item.com
import { TextMenuItemComponent } from './menu/menu-item/text-menu-item.component';
import { ThemedConfigurationSearchPageComponent } from '../search-page/themed-configuration-search-page.component';
import { SearchNavbarComponent } from '../search-navbar/search-navbar.component';
import { ItemVersionsSummaryModalComponent } from './item/item-versions/item-versions-summary-modal/item-versions-summary-modal.component';
import { ItemVersionsDeleteModalComponent } from './item/item-versions/item-versions-delete-modal/item-versions-delete-modal.component';
import { ScopeSelectorModalComponent } from './search-form/scope-selector-modal/scope-selector-modal.component';
import { BitstreamRequestACopyPageComponent } from './bitstream-request-a-copy-page/bitstream-request-a-copy-page.component';
@@ -462,7 +465,7 @@ const COMPONENTS = [
CollectionSidebarSearchListElementComponent,
CommunitySidebarSearchListElementComponent,
SearchNavbarComponent,
ScopeSelectorModalComponent
ScopeSelectorModalComponent,
];
const ENTRY_COMPONENTS = [
@@ -528,7 +531,7 @@ const ENTRY_COMPONENTS = [
LinkMenuItemComponent,
OnClickMenuItemComponent,
TextMenuItemComponent,
ScopeSelectorModalComponent
ScopeSelectorModalComponent,
];
const SHARED_SEARCH_PAGE_COMPONENTS = [
@@ -540,6 +543,7 @@ const SHARED_ITEM_PAGE_COMPONENTS = [
MetadataFieldWrapperComponent,
MetadataValuesComponent,
DsoPageEditButtonComponent,
DsoPageVersionButtonComponent,
ItemAlertsComponent,
GenericItemPageFieldComponent,
MetadataRepresentationListComponent,
@@ -590,7 +594,9 @@ const DIRECTIVES = [
...COMPONENTS,
...DIRECTIVES,
...SHARED_ITEM_PAGE_COMPONENTS,
...SHARED_SEARCH_PAGE_COMPONENTS
...SHARED_SEARCH_PAGE_COMPONENTS,
ItemVersionsSummaryModalComponent,
ItemVersionsDeleteModalComponent,
],
providers: [
...PROVIDERS

View File

@@ -2061,6 +2061,10 @@
"item.page.return": "Back",
"item.page.version.create": "Create new version",
"item.page.version.hasDraft": "A new version cannot be created because there is an inprogress submission in the version history",
"item.preview.dc.identifier.uri": "Identifier:",
"item.preview.dc.contributor.author": "Authors:",
@@ -2116,6 +2120,8 @@
"item.version.history.selected": "Selected version",
"item.version.history.selected.alert": "You are currently viewing version {{version}} of the item.",
"item.version.history.table.version": "Version",
"item.version.history.table.item": "Item",
@@ -2126,11 +2132,77 @@
"item.version.history.table.summary": "Summary",
"item.version.history.table.workspaceItem": "Workspace item",
"item.version.history.table.workflowItem": "Workflow item",
"item.version.history.table.actions": "Action",
"item.version.history.table.action.editWorkspaceItem": "Edit workspace item",
"item.version.history.table.action.editSummary": "Edit summary",
"item.version.history.table.action.saveSummary": "Save summary edits",
"item.version.history.table.action.discardSummary": "Discard summary edits",
"item.version.history.table.action.newVersion": "Create new version from this one",
"item.version.history.table.action.deleteVersion": "Delete version",
"item.version.history.table.action.hasDraft": "A new version cannot be created because there is an inprogress submission in the version history",
"item.version.notice": "This is not the latest version of this item. The latest version can be found <a href='{{destination}}'>here</a>.",
"item.version.create.modal.header": "New version",
"item.version.create.modal.text": "Create a new version for this item",
"item.version.create.modal.text.startingFrom": "starting from version {{version}}",
"item.version.create.modal.button.confirm": "Create",
"item.version.create.modal.button.confirm.tooltip": "Create new version",
"item.version.create.modal.button.cancel": "Cancel",
"item.version.create.modal.button.cancel.tooltip": "Do not create new version",
"item.version.create.modal.form.summary.label": "Summary",
"item.version.create.modal.form.summary.placeholder": "Insert the summary for the new version",
"item.version.create.notification.success" : "New version has been created with version number {{version}}",
"item.version.create.notification.failure" : "New version has not been created",
"item.version.create.notification.inProgress" : "A new version cannot be created because there is an inprogress submission in the version history",
"item.version.delete.modal.header": "Delete version",
"item.version.delete.modal.text": "Do you want to delete version {{version}}?",
"item.version.delete.modal.button.confirm": "Delete",
"item.version.delete.modal.button.confirm.tooltip": "Delete this version",
"item.version.delete.modal.button.cancel": "Cancel",
"item.version.delete.modal.button.cancel.tooltip": "Do not delete this version",
"item.version.delete.notification.success" : "Version number {{version}} has been deleted",
"item.version.delete.notification.failure" : "Version number {{version}} has not been deleted",
"item.version.edit.notification.success" : "The summary of version number {{version}} has been changed",
"item.version.edit.notification.failure" : "The summary of version number {{version}} has not been changed",
"journal.listelement.badge": "Journal",