mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge pull request #1318 from 4Science/CST-4499
Create new Item Version (basic Items only)
This commit is contained in:
@@ -22,4 +22,7 @@ export enum FeatureID {
|
||||
CanManagePolicies = 'canManagePolicies',
|
||||
CanMakePrivate = 'canMakePrivate',
|
||||
CanMove = 'canMove',
|
||||
CanEditVersion = 'canEditVersion',
|
||||
CanDeleteVersion = 'canDeleteVersion',
|
||||
CanCreateVersion = 'canCreateVersion',
|
||||
}
|
||||
|
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
181
src/app/core/data/version-data.service.spec.ts
Normal file
181
src/app/core/data/version-data.service.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
});
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
*/
|
||||
|
150
src/app/core/submission/workflowitem-data.service.spec.ts
Normal file
150
src/app/core/submission/workflowitem-data.service.spec.ts
Normal 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);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
150
src/app/core/submission/workspaceitem-data.service.spec.ts
Normal file
150
src/app/core/submission/workspaceitem-data.service.spec.ts
Normal 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);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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"> {{"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"> {{"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"> {{"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"> {{"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"> {{"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"> {{"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"> {{"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"> {{"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>
|
||||
|
@@ -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>
|
||||
|
@@ -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();
|
||||
|
@@ -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>>;
|
||||
}
|
||||
}
|
||||
|
@@ -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';
|
||||
|
@@ -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,
|
||||
]
|
||||
|
||||
})
|
||||
|
@@ -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
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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 }
|
||||
|
@@ -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 {
|
||||
|
||||
}
|
||||
|
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@@ -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);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
@@ -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();
|
||||
});
|
||||
});
|
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
54
src/app/item-page/version-page/version.resolver.ts
Normal file
54
src/app/item-page/version-page/version.resolver.ts
Normal 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$;
|
||||
}
|
||||
}
|
@@ -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() {
|
||||
|
@@ -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>
|
@@ -0,0 +1,3 @@
|
||||
.btn-dark {
|
||||
background-color: var(--ds-admin-sidebar-bg);
|
||||
}
|
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@@ -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)),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@@ -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>
|
@@ -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();
|
||||
});
|
||||
});
|
@@ -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();
|
||||
}
|
||||
|
||||
}
|
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@@ -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')));
|
||||
}
|
||||
|
||||
}
|
@@ -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>
|
@@ -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();
|
||||
});
|
||||
});
|
@@ -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();
|
||||
}
|
||||
|
||||
}
|
@@ -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>* {{"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>
|
||||
|
@@ -0,0 +1,9 @@
|
||||
.left-column {
|
||||
float: left;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.right-column {
|
||||
float: right;
|
||||
text-align: right;
|
||||
}
|
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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">
|
||||
|
@@ -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;
|
||||
|
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -16,7 +16,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ng-content></ng-content>
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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",
|
||||
|
||||
|
Reference in New Issue
Block a user