mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-10 11:33:04 +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',
|
CanManagePolicies = 'canManagePolicies',
|
||||||
CanMakePrivate = 'canMakePrivate',
|
CanMakePrivate = 'canMakePrivate',
|
||||||
CanMove = 'canMove',
|
CanMove = 'canMove',
|
||||||
|
CanEditVersion = 'canEditVersion',
|
||||||
|
CanDeleteVersion = 'canDeleteVersion',
|
||||||
|
CanCreateVersion = 'canCreateVersion',
|
||||||
}
|
}
|
||||||
|
@@ -31,7 +31,7 @@ describe('ItemDataService', () => {
|
|||||||
},
|
},
|
||||||
removeByHrefSubstring(href: string) {
|
removeByHrefSubstring(href: string) {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
}
|
},
|
||||||
}) as RequestService;
|
}) as RequestService;
|
||||||
const rdbService = getMockRemoteDataBuildService();
|
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
|
* Get the endpoint for browsing items
|
||||||
* (When options.sort.field is empty, the default field to browse by will be 'dc.date.issued')
|
* (When options.sort.field is empty, the default field to browse by will be 'dc.date.issued')
|
||||||
* @param {FindListOptions} options
|
* @param {FindListOptions} options
|
||||||
|
* @param linkPath
|
||||||
* @returns {Observable<string>}
|
* @returns {Observable<string>}
|
||||||
*/
|
*/
|
||||||
public getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): 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}`))
|
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 { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
||||||
import { FindListOptions } from './request.models';
|
import { EMPTY, Observable } from 'rxjs';
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
import { dataService } from '../cache/builders/build-decorators';
|
import { dataService } from '../cache/builders/build-decorators';
|
||||||
import { VERSION } from '../shared/version.resource-type';
|
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
|
* 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> {
|
getHistoryFromVersion(version: Version, useCachedVersionIfAvailable = false, reRequestOnStale = true): Observable<VersionHistory> {
|
||||||
return this.halService.getEndpoint(this.linkPath);
|
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 { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
|
||||||
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
||||||
import { VersionDataService } from './version-data.service';
|
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';
|
const url = 'fake-url';
|
||||||
|
|
||||||
@@ -16,9 +24,97 @@ describe('VersionHistoryDataService', () => {
|
|||||||
let notificationsService: any;
|
let notificationsService: any;
|
||||||
let rdbService: RemoteDataBuildService;
|
let rdbService: RemoteDataBuildService;
|
||||||
let objectCache: ObjectCacheService;
|
let objectCache: ObjectCacheService;
|
||||||
let versionService: VersionDataService;
|
let versionService: SpyObj<VersionDataService>;
|
||||||
let halService: any;
|
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(() => {
|
beforeEach(() => {
|
||||||
createService();
|
createService();
|
||||||
});
|
});
|
||||||
@@ -35,24 +131,70 @@ describe('VersionHistoryDataService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
describe('when getVersions is called', () => {
|
||||||
* Create a VersionHistoryDataService used for testing
|
beforeEach(waitForAsync(() => {
|
||||||
* @param requestEntry$ Supply a requestEntry to be returned by the REST API (optional)
|
service.getVersions(versionHistoryId);
|
||||||
*/
|
}));
|
||||||
function createService(requestEntry$?) {
|
it('findAllByHref should have been called', () => {
|
||||||
requestService = getMockRequestService(requestEntry$);
|
expect(versionService.findAllByHref).toHaveBeenCalled();
|
||||||
rdbService = jasmine.createSpyObj('rdbService', {
|
|
||||||
buildList: jasmine.createSpy('buildList')
|
|
||||||
});
|
});
|
||||||
objectCache = jasmine.createSpyObj('objectCache', {
|
});
|
||||||
remove: jasmine.createSpy('remove')
|
|
||||||
});
|
describe('when getBrowseEndpoint is called', () => {
|
||||||
versionService = jasmine.createSpyObj('objectCache', {
|
it('should return the correct value', () => {
|
||||||
findAllByHref: jasmine.createSpy('findAllByHref')
|
service.getBrowseEndpoint().subscribe((res) => {
|
||||||
});
|
expect(res).toBe(url + '/versionhistories');
|
||||||
halService = new HALEndpointServiceStub(url);
|
});
|
||||||
notificationsService = new NotificationsServiceStub();
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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 { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.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 { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
||||||
import { FindListOptions } from './request.models';
|
import { FindListOptions, PostRequest, RestRequest } from './request.models';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable, of } from 'rxjs';
|
||||||
import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model';
|
import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model';
|
||||||
import { RemoteData } from './remote-data';
|
import { RemoteData } from './remote-data';
|
||||||
import { PaginatedList } from './paginated-list.model';
|
import { PaginatedList } from './paginated-list.model';
|
||||||
import { Version } from '../shared/version.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 { dataService } from '../cache/builders/build-decorators';
|
||||||
import { VERSION_HISTORY } from '../shared/version-history.resource-type';
|
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 { 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
|
* 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);
|
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: {
|
_links: {
|
||||||
self: HALLink;
|
self: HALLink;
|
||||||
versions: HALLink;
|
versions: HALLink;
|
||||||
|
draftVersion: HALLink;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -30,6 +31,24 @@ export class VersionHistory extends DSpaceObject {
|
|||||||
@autoserialize
|
@autoserialize
|
||||||
id: string;
|
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
|
* 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 { RequestService } from '../data/request.service';
|
||||||
import { WorkflowItem } from './models/workflowitem.model';
|
import { WorkflowItem } from './models/workflowitem.model';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
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 { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.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 { RemoteData } from '../data/remote-data';
|
||||||
import { NoContent } from '../shared/NoContent.model';
|
import { NoContent } from '../shared/NoContent.model';
|
||||||
import { getFirstCompletedRemoteData } from '../shared/operators';
|
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.
|
* 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)
|
@dataService(WorkflowItem.type)
|
||||||
export class WorkflowItemDataService extends DataService<WorkflowItem> {
|
export class WorkflowItemDataService extends DataService<WorkflowItem> {
|
||||||
protected linkPath = 'workflowitems';
|
protected linkPath = 'workflowitems';
|
||||||
|
protected searchByItemLinkPath = 'item';
|
||||||
protected responseMsToLive = 10 * 1000;
|
protected responseMsToLive = 10 * 1000;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -86,4 +90,23 @@ export class WorkflowItemDataService extends DataService<WorkflowItem> {
|
|||||||
|
|
||||||
return this.rdbService.buildFromRequestUUID(requestId);
|
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 { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
|
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
|
||||||
import { WorkspaceItem } from './models/workspaceitem.model';
|
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.
|
* 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)
|
@dataService(WorkspaceItem.type)
|
||||||
export class WorkspaceitemDataService extends DataService<WorkspaceItem> {
|
export class WorkspaceitemDataService extends DataService<WorkspaceItem> {
|
||||||
protected linkPath = 'workspaceitems';
|
protected linkPath = 'workspaceitems';
|
||||||
|
protected searchByItemLinkPath = 'item';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected comparator: DSOChangeAnalyzer<WorkspaceItem>,
|
protected comparator: DSOChangeAnalyzer<WorkspaceItem>,
|
||||||
@@ -33,4 +39,22 @@ export class WorkspaceitemDataService extends DataService<WorkspaceItem> {
|
|||||||
super();
|
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="item-metadata">
|
||||||
<div class="button-row top d-flex mb-2">
|
<div class="button-row top d-flex mb-2">
|
||||||
<button class="mr-auto btn btn-success"
|
<button class="mr-auto btn btn-success"
|
||||||
(click)="add()"><i
|
(click)="add()"><i
|
||||||
class="fas fa-plus"></i>
|
class="fas fa-plus"></i>
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.add-button" | translate}}</span>
|
<span class="d-none d-sm-inline"> {{"item.edit.metadata.add-button" | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-warning" *ngIf="isReinstatable() | async"
|
<button class="btn btn-warning" *ngIf="isReinstatable() | async"
|
||||||
(click)="reinstate()"><i
|
(click)="reinstate()"><i
|
||||||
class="fas fa-undo-alt"></i>
|
class="fas fa-undo-alt"></i>
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.reinstate-button" | translate}}</span>
|
<span class="d-none d-sm-inline"> {{"item.edit.metadata.reinstate-button" | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-primary" [disabled]="!(hasChanges() | async) || !(isValid() | async)"
|
<button class="btn btn-primary" [disabled]="!(hasChanges() | async) || !(isValid() | async)"
|
||||||
(click)="submit()"><i
|
(click)="submit()"><i
|
||||||
class="fas fa-save"></i>
|
class="fas fa-save"></i>
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.save-button" | translate}}</span>
|
<span class="d-none d-sm-inline"> {{"item.edit.metadata.save-button" | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-danger" *ngIf="!(isReinstatable() | async)"
|
<button class="btn btn-danger" *ngIf="!(isReinstatable() | async)"
|
||||||
[disabled]="!(hasChanges() | async)"
|
[disabled]="!(hasChanges() | async)"
|
||||||
(click)="discard()"><i
|
(click)="discard()"><i
|
||||||
class="fas fa-times"></i>
|
class="fas fa-times"></i>
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.discard-button" | translate}}</span>
|
<span class="d-none d-sm-inline"> {{"item.edit.metadata.discard-button" | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-responsive table-striped table-bordered" *ngIf="((updates$ | async)| dsObjectValues).length > 0">
|
<table class="table table-responsive table-striped table-bordered"
|
||||||
<tbody>
|
*ngIf="((updates$ | async)| dsObjectValues).length > 0">
|
||||||
<tr>
|
<thead>
|
||||||
<th><span id="fieldName">{{'item.edit.metadata.headers.field' | translate}}</span></th>
|
<tr>
|
||||||
<th><span id="fieldValue">{{'item.edit.metadata.headers.value' | translate}}</span></th>
|
<th><span id="fieldName">{{'item.edit.metadata.headers.field' | translate}}</span></th>
|
||||||
<th class="text-center"><span id="fieldLang">{{'item.edit.metadata.headers.language' | translate}}</span></th>
|
<th><span id="fieldValue">{{'item.edit.metadata.headers.value' | translate}}</span></th>
|
||||||
<th class="text-center">{{'item.edit.metadata.headers.edit' | translate}}</th>
|
<th class="text-center"><span id="fieldLang">{{'item.edit.metadata.headers.language' | translate}}</span></th>
|
||||||
</tr>
|
<th class="text-center">{{'item.edit.metadata.headers.edit' | translate}}</th>
|
||||||
<tr *ngFor="let updateValue of ((updates$ | async)| dsObjectValues); trackBy: trackUpdate"
|
</tr>
|
||||||
ds-edit-in-place-field
|
</thead>
|
||||||
[fieldUpdate]="updateValue || {}"
|
<tbody>
|
||||||
[url]="url"
|
<tr *ngFor="let updateValue of ((updates$ | async)| dsObjectValues); trackBy: trackUpdate"
|
||||||
[ngClass]="{
|
ds-edit-in-place-field
|
||||||
|
[fieldUpdate]="updateValue || {}"
|
||||||
|
[url]="url"
|
||||||
|
[ngClass]="{
|
||||||
'table-warning': updateValue.changeType === 0,
|
'table-warning': updateValue.changeType === 0,
|
||||||
'table-danger': updateValue.changeType === 2,
|
'table-danger': updateValue.changeType === 2,
|
||||||
'table-success': updateValue.changeType === 1
|
'table-success': updateValue.changeType === 1
|
||||||
}">
|
}">
|
||||||
|
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div *ngIf="((updates$ | async)| dsObjectValues).length == 0">
|
<div *ngIf="((updates$ | async)| dsObjectValues).length == 0">
|
||||||
<ds-alert [content]="'item.edit.metadata.empty'" [type]="AlertTypeEnum.Info"></ds-alert>
|
<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>
|
||||||
<div class="button-row bottom">
|
</div>
|
||||||
<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">
|
<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>
|
</div>
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { ItemVersionHistoryComponent } from './item-version-history.component';
|
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 { VarDirective } from '../../../shared/utils/var.directive';
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
@@ -18,12 +18,20 @@ describe('ItemVersionHistoryComponent', () => {
|
|||||||
handle: '123456789/1',
|
handle: '123456789/1',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const activatedRoute = {
|
||||||
|
parent: {
|
||||||
|
parent: {
|
||||||
|
data: observableOf({dso: createSuccessfulRemoteDataObject(item)})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [ItemVersionHistoryComponent, VarDirective],
|
declarations: [ItemVersionHistoryComponent, VarDirective],
|
||||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: ActivatedRoute, useValue: { parent: { data: observableOf({ dso: createSuccessfulRemoteDataObject(item) }) } } }
|
{ provide: ActivatedRoute, useValue: activatedRoute }
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
@@ -30,6 +30,6 @@ export class ItemVersionHistoryComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
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();
|
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) {
|
export function getEntityPageRoute(entityType: string, itemId: string) {
|
||||||
if (isNotEmpty(entityType)) {
|
if (isNotEmpty(entityType)) {
|
||||||
return new URLCombiner('/entities', encodeURIComponent(entityType.toLowerCase()), itemId).toString();
|
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();
|
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_PATH = 'edit';
|
||||||
|
export const ITEM_EDIT_VERSIONHISTORY_PATH = 'versionhistory';
|
||||||
|
export const ITEM_VERSION_PATH = 'version';
|
||||||
export const UPLOAD_BITSTREAM_PATH = 'bitstreams/new';
|
export const UPLOAD_BITSTREAM_PATH = 'bitstreams/new';
|
||||||
|
@@ -3,6 +3,7 @@ import { RouterModule } from '@angular/router';
|
|||||||
import { ItemPageResolver } from './item-page.resolver';
|
import { ItemPageResolver } from './item-page.resolver';
|
||||||
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
||||||
import { ItemBreadcrumbResolver } from '../core/breadcrumbs/item-breadcrumb.resolver';
|
import { ItemBreadcrumbResolver } from '../core/breadcrumbs/item-breadcrumb.resolver';
|
||||||
|
import { VersionResolver } from './version-page/version.resolver';
|
||||||
import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service';
|
import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service';
|
||||||
import { LinkService } from '../core/cache/builders/link.service';
|
import { LinkService } from '../core/cache/builders/link.service';
|
||||||
import { UploadBitstreamComponent } from './bitstreams/upload/upload-bitstream.component';
|
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 { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
|
||||||
import { ThemedItemPageComponent } from './simple/themed-item-page.component';
|
import { ThemedItemPageComponent } from './simple/themed-item-page.component';
|
||||||
import { ThemedFullItemPageComponent } from './full/themed-full-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';
|
import { BitstreamRequestACopyPageComponent } from '../shared/bitstream-request-a-copy-page/bitstream-request-a-copy-page.component';
|
||||||
|
|
||||||
@NgModule({
|
@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,
|
DSOBreadcrumbsService,
|
||||||
LinkService,
|
LinkService,
|
||||||
ItemPageAdministratorGuard,
|
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 { MediaViewerVideoComponent } from './media-viewer/media-viewer-video/media-viewer-video.component';
|
||||||
import { MediaViewerImageComponent } from './media-viewer/media-viewer-image/media-viewer-image.component';
|
import { MediaViewerImageComponent } from './media-viewer/media-viewer-image/media-viewer-image.component';
|
||||||
import { NgxGalleryModule } from '@kolkov/ngx-gallery';
|
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';
|
import { ThemedFileSectionComponent} from './simple/field-components/file-section/themed-file-section.component';
|
||||||
|
|
||||||
const ENTRY_COMPONENTS = [
|
const ENTRY_COMPONENTS = [
|
||||||
@@ -62,7 +64,8 @@ const DECLARATIONS = [
|
|||||||
AbstractIncrementalListComponent,
|
AbstractIncrementalListComponent,
|
||||||
MediaViewerComponent,
|
MediaViewerComponent,
|
||||||
MediaViewerVideoComponent,
|
MediaViewerVideoComponent,
|
||||||
MediaViewerImageComponent
|
MediaViewerImageComponent,
|
||||||
|
VersionPageComponent,
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@@ -74,10 +77,11 @@ const DECLARATIONS = [
|
|||||||
StatisticsModule.forRoot(),
|
StatisticsModule.forRoot(),
|
||||||
JournalEntitiesModule.withEntryComponents(),
|
JournalEntitiesModule.withEntryComponents(),
|
||||||
ResearchEntitiesModule.withEntryComponents(),
|
ResearchEntitiesModule.withEntryComponents(),
|
||||||
NgxGalleryModule,
|
NgxGalleryModule,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
...DECLARATIONS
|
...DECLARATIONS,
|
||||||
|
VersionedItemComponent
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
...DECLARATIONS
|
...DECLARATIONS
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
<ds-item-versions-notice [item]="item"></ds-item-versions-notice>
|
<ds-item-versions-notice [item]="item"></ds-item-versions-notice>
|
||||||
<ds-view-tracker [object]="item"></ds-view-tracker>
|
<ds-view-tracker [object]="item"></ds-view-tracker>
|
||||||
<ds-listable-object-component-loader [object]="item" [viewMode]="viewMode"></ds-listable-object-component-loader>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.item' | translate}}"></ds-error>
|
<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>
|
<ds-metadata-values [mdValues]="object?.allMetadata(['dc.title'])"></ds-metadata-values>
|
||||||
</h2>
|
</h2>
|
||||||
<div class="pl-2">
|
<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>
|
<ds-dso-page-edit-button [pageRoute]="itemPageRoute" [dso]="object" [tooltipMsg]="'item.page.edit'"></ds-dso-page-edit-button>
|
||||||
</div>
|
</div>
|
||||||
</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 { GenericItemPageFieldComponent } from '../../field-components/specific-field/generic/generic-item-page-field.component';
|
||||||
import { createRelationshipsObservable } from '../shared/item.component.spec';
|
import { createRelationshipsObservable } from '../shared/item.component.spec';
|
||||||
import { UntypedItemComponent } from './untyped-item.component';
|
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(), {
|
const mockItem: Item = Object.assign(new Item(), {
|
||||||
bundles: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])),
|
bundles: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])),
|
||||||
@@ -47,13 +53,16 @@ describe('UntypedItemComponent', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot({
|
imports: [
|
||||||
loader: {
|
TranslateModule.forRoot({
|
||||||
provide: TranslateLoader,
|
loader: {
|
||||||
useClass: TranslateLoaderMock
|
provide: TranslateLoader,
|
||||||
}
|
useClass: TranslateLoaderMock
|
||||||
})],
|
}
|
||||||
declarations: [UntypedItemComponent, GenericItemPageFieldComponent, TruncatePipe],
|
}),
|
||||||
|
RouterTestingModule,
|
||||||
|
],
|
||||||
|
declarations: [UntypedItemComponent, GenericItemPageFieldComponent, TruncatePipe ],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: ItemDataService, useValue: {} },
|
{ provide: ItemDataService, useValue: {} },
|
||||||
{ provide: TruncatableService, useValue: {} },
|
{ provide: TruncatableService, useValue: {} },
|
||||||
@@ -68,9 +77,14 @@ describe('UntypedItemComponent', () => {
|
|||||||
{ provide: HttpClient, useValue: {} },
|
{ provide: HttpClient, useValue: {} },
|
||||||
{ provide: DSOChangeAnalyzer, useValue: {} },
|
{ provide: DSOChangeAnalyzer, useValue: {} },
|
||||||
{ provide: DefaultChangeAnalyzer, useValue: {} },
|
{ provide: DefaultChangeAnalyzer, useValue: {} },
|
||||||
|
{ provide: VersionHistoryDataService, useValue: {} },
|
||||||
|
{ provide: VersionDataService, useValue: {} },
|
||||||
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
|
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
|
||||||
|
{ provide: WorkspaceitemDataService, useValue: {} },
|
||||||
|
{ provide: SearchService, useValue: {} },
|
||||||
|
{ provide: ItemDataService, useValue: {} },
|
||||||
|
{ provide: ItemVersionsSharedService, useValue: {} },
|
||||||
],
|
],
|
||||||
|
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).overrideComponent(UntypedItemComponent, {
|
}).overrideComponent(UntypedItemComponent, {
|
||||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||||
import { Item } from '../../../../core/shared/item.model';
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
import { ItemComponent } from '../shared/item.component';
|
|
||||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||||
import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
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
|
* Component that represents a publication Item page
|
||||||
@@ -15,6 +15,6 @@ import { listableObjectComponent } from '../../../../shared/object-collection/sh
|
|||||||
templateUrl: './untyped-item.component.html',
|
templateUrl: './untyped-item.component.html',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
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.
|
* select a collection.
|
||||||
*/
|
*/
|
||||||
openDialog() {
|
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 *ngVar="(versionRD$ | async)?.payload as itemVersion">
|
||||||
<div class="mb-2" *ngIf="versions?.page?.length > 0 || displayWhenEmpty">
|
<div class="mb-2" *ngIf="versions?.page?.length > 0 || displayWhenEmpty">
|
||||||
<h2 *ngIf="displayTitle">{{"item.version.history.head" | translate}}</h2>
|
<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"
|
<ds-pagination *ngIf="versions?.page?.length > 0"
|
||||||
|
(paginationChange)="onPageChange()"
|
||||||
[hideGear]="true"
|
[hideGear]="true"
|
||||||
[hidePagerWhenSinglePage]="true"
|
[hidePagerWhenSinglePage]="true"
|
||||||
[paginationOptions]="options"
|
[paginationOptions]="options"
|
||||||
[pageInfoState]="versions"
|
[pageInfoState]="versions"
|
||||||
[collectionSize]="versions?.totalElements"
|
[collectionSize]="versions?.totalElements"
|
||||||
[retainScrollPosition]="true">
|
[retainScrollPosition]="true">
|
||||||
<table class="table table-striped my-2">
|
<table class="table table-striped table-bordered align-middle my-2">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">{{"item.version.history.table.version" | translate}}</th>
|
<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" *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.date" | translate}}</th>
|
<th scope="col">{{"item.version.history.table.summary" | translate}}</th>
|
||||||
<th scope="col">{{"item.version.history.table.summary" | translate}}</th>
|
</tr>
|
||||||
</tr>
|
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let version of versions?.page" [id]="'version-row-' + version.id">
|
<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-version">
|
||||||
<td class="version-row-element-item">
|
<!-- Get the ID of the workspace/workflow item (`undefined` if they don't exist).
|
||||||
<span *ngVar="(version?.item | async)?.payload as item">
|
Conditionals inside *ngVar are needed in order to avoid useless calls. -->
|
||||||
<a *ngIf="item" [routerLink]="[(itemPageRoutes$ | async)[item?.id]]">{{item?.handle}}</a>
|
<ng-container *ngVar="((hasDraftVersion$ | async) ? getWorkspaceId(version?.item) : undefined) as workspaceId$">
|
||||||
<span *ngIf="version?.id === itemVersion?.id">*</span>
|
<ng-container *ngVar=" ((workspaceId$ | async) ? undefined : getWorkflowId(version?.item)) as workflowId$">
|
||||||
</span>
|
|
||||||
</td>
|
<div class="left-column">
|
||||||
<td *ngIf="(hasEpersons$ | async)" class="version-row-element-editor">
|
|
||||||
|
<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">
|
<span *ngVar="(version?.eperson | async)?.payload as eperson">
|
||||||
<a *ngIf="eperson" [href]="'mailto:' + eperson?.email">{{eperson?.name}}</a>
|
<a *ngIf="eperson" [href]="'mailto:' + eperson?.email">{{eperson?.name}}</a>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="version-row-element-date">{{version?.created}}</td>
|
<td class="version-row-element-date">
|
||||||
<td class="version-row-element-summary">{{version?.summary}}</td>
|
{{version?.created | date : 'yyyy-MM-dd HH:mm:ss'}}
|
||||||
</tr>
|
</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>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div>* {{"item.version.history.selected" | translate}}</div>
|
<div>* {{"item.version.history.selected" | translate}}</div>
|
||||||
</ds-pagination>
|
</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>
|
</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 { By } from '@angular/platform-browser';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../remote-data.utils';
|
||||||
import { createPaginatedList } from '../../testing/utils.test';
|
import { createPaginatedList } from '../../testing/utils.test';
|
||||||
import { PaginationComponentOptions } from '../../pagination/pagination-component-options.model';
|
import { EMPTY, of, of as observableOf } from 'rxjs';
|
||||||
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
|
|
||||||
import { of as observableOf } from 'rxjs';
|
|
||||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||||
import { PaginationServiceStub } from '../../testing/pagination-service.stub';
|
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', () => {
|
describe('ItemVersionsComponent', () => {
|
||||||
let component: ItemVersionsComponent;
|
let component: ItemVersionsComponent;
|
||||||
let fixture: ComponentFixture<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(), {
|
const versionHistory = Object.assign(new VersionHistory(), {
|
||||||
id: '1'
|
id: '1',
|
||||||
|
draftVersion: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const version1 = Object.assign(new Version(), {
|
const version1 = Object.assign(new Version(), {
|
||||||
id: '1',
|
id: '1',
|
||||||
version: 1,
|
version: 1,
|
||||||
created: new Date(2020, 1, 1),
|
created: new Date(2020, 1, 1),
|
||||||
summary: 'first version',
|
summary: 'first version',
|
||||||
versionhistory: createSuccessfulRemoteDataObject$(versionHistory)
|
versionhistory: createSuccessfulRemoteDataObject$(versionHistory),
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'version2-url',
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const version2 = Object.assign(new Version(), {
|
const version2 = Object.assign(new Version(), {
|
||||||
id: '2',
|
id: '2',
|
||||||
version: 2,
|
version: 2,
|
||||||
summary: 'second version',
|
summary: 'second version',
|
||||||
created: new Date(2020, 1, 2),
|
created: new Date(2020, 1, 2),
|
||||||
versionhistory: createSuccessfulRemoteDataObject$(versionHistory)
|
versionhistory: createSuccessfulRemoteDataObject$(versionHistory),
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'version2-url',
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const versions = [version1, version2];
|
const versions = [version1, version2];
|
||||||
versionHistory.versions = createSuccessfulRemoteDataObject$(createPaginatedList(versions));
|
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',
|
uuid: 'item-identifier-1',
|
||||||
handle: '123456789/1',
|
handle: '123456789/1',
|
||||||
version: createSuccessfulRemoteDataObject$(version1)
|
version: createSuccessfulRemoteDataObject$(version1),
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: '/items/item-identifier-1'
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
const item2 = Object.assign(new Item(), {
|
const item2 = Object.assign(new Item(), {
|
||||||
uuid: 'item-identifier-2',
|
uuid: 'item-identifier-2',
|
||||||
handle: '123456789/2',
|
handle: '123456789/2',
|
||||||
version: createSuccessfulRemoteDataObject$(version2)
|
version: createSuccessfulRemoteDataObject$(version2),
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: '/items/item-identifier-2'
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
const items = [item1, item2];
|
const items = [item1, item2];
|
||||||
version1.item = createSuccessfulRemoteDataObject$(item1);
|
version1.item = createSuccessfulRemoteDataObject$(item1);
|
||||||
version2.item = createSuccessfulRemoteDataObject$(item2);
|
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(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [ItemVersionsComponent, VarDirective],
|
declarations: [ItemVersionsComponent, VarDirective],
|
||||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: VersionHistoryDataService, useValue: versionHistoryService },
|
{provide: PaginationService, useValue: new PaginationServiceStub()},
|
||||||
{ provide: PaginationService, useValue: paginationService }
|
{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]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).compileComponents();
|
}).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(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(ItemVersionsComponent);
|
fixture = TestBed.createComponent(ItemVersionsComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
component.item = item1;
|
component.item = item1;
|
||||||
|
component.displayActions = true;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -88,26 +156,29 @@ describe('ItemVersionsComponent', () => {
|
|||||||
|
|
||||||
it(`should display version ${version.version} in the correct column for version ${version.id}`, () => {
|
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`));
|
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}`, () => {
|
// Check if the current version contains an asterisk
|
||||||
const item = fixture.debugElement.query(By.css(`#version-row-${version.id} .version-row-element-item`));
|
|
||||||
expect(item.nativeElement.textContent).toContain(versionItem.handle);
|
|
||||||
});
|
|
||||||
|
|
||||||
// This version's item is equal to the component's item (the selected item)
|
|
||||||
// Check if the handle contains an asterisk
|
|
||||||
if (item1.uuid === versionItem.uuid) {
|
if (item1.uuid === versionItem.uuid) {
|
||||||
it('should add an asterisk to the handle of the selected 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-item`));
|
const item = fixture.debugElement.query(By.css(`#version-row-${version.id} .version-row-element-version`));
|
||||||
expect(item.nativeElement.textContent).toContain('*');
|
expect(item.nativeElement.textContent).toContain('*');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
it(`should display date ${version.created} in the correct column for version ${version.id}`, () => {
|
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`));
|
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}`, () => {
|
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);
|
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 { Item } from '../../../core/shared/item.model';
|
||||||
import { Version } from '../../../core/shared/version.model';
|
import { Version } from '../../../core/shared/version.model';
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
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 { VersionHistory } from '../../../core/shared/version-history.model';
|
||||||
import {
|
import {
|
||||||
getAllSucceededRemoteData,
|
getAllSucceededRemoteData,
|
||||||
getAllSucceededRemoteDataPayload,
|
getAllSucceededRemoteDataPayload,
|
||||||
|
getFirstCompletedRemoteData,
|
||||||
|
getFirstSucceededRemoteData,
|
||||||
|
getFirstSucceededRemoteDataPayload,
|
||||||
getRemoteDataPayload
|
getRemoteDataPayload
|
||||||
} from '../../../core/shared/operators';
|
} 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 { PaginatedList } from '../../../core/data/paginated-list.model';
|
||||||
import { PaginationComponentOptions } from '../../pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../pagination/pagination-component-options.model';
|
||||||
import { VersionHistoryDataService } from '../../../core/data/version-history-data.service';
|
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 { followLink } from '../../utils/follow-link-config.model';
|
||||||
import { hasValue, hasValueOperator } from '../../empty.util';
|
import { hasValue, hasValueOperator } from '../../empty.util';
|
||||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
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({
|
@Component({
|
||||||
selector: 'ds-item-versions',
|
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
|
* Component listing all available versions of the history the provided item is a part of
|
||||||
*/
|
*/
|
||||||
export class ItemVersionsComponent implements OnInit {
|
export class ItemVersionsComponent implements OnInit {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The item to display a version history for
|
* The item to display a version history for
|
||||||
*/
|
*/
|
||||||
@@ -45,6 +77,16 @@ export class ItemVersionsComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
@Input() displayTitle = true;
|
@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
|
* The AlertType enumeration
|
||||||
* @type {AlertType}
|
* @type {AlertType}
|
||||||
@@ -57,14 +99,19 @@ export class ItemVersionsComponent implements OnInit {
|
|||||||
versionRD$: Observable<RemoteData<Version>>;
|
versionRD$: Observable<RemoteData<Version>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The item's full version history
|
* The item's full version history (remote data)
|
||||||
*/
|
*/
|
||||||
versionHistoryRD$: Observable<RemoteData<VersionHistory>>;
|
versionHistoryRD$: Observable<RemoteData<VersionHistory>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The item's full version history
|
||||||
|
*/
|
||||||
|
versionHistory$: Observable<VersionHistory>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The version history's list of versions
|
* 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
|
* 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>;
|
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
|
* 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
|
* The page options to use for fetching the versions
|
||||||
* Start at page 1 and always use the set page size
|
* Start at page 1 and always use the set page size
|
||||||
*/
|
*/
|
||||||
options = Object.assign(new PaginationComponentOptions(),{
|
options = Object.assign(new PaginationComponentOptions(), {
|
||||||
id: 'ivo',
|
id: 'ivo',
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: this.pageSize
|
pageSize: this.pageSize
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* The current page being displayed
|
|
||||||
*/
|
|
||||||
currentPage$ = new BehaviorSubject<number>(1);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The routes to the versions their item pages
|
* The routes to the versions their item pages
|
||||||
* Key: Item ID
|
* Key: Item ID
|
||||||
@@ -101,9 +149,301 @@ export class ItemVersionsComponent implements OnInit {
|
|||||||
[itemId: string]: string
|
[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,
|
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(),
|
getAllSucceededRemoteData(),
|
||||||
getRemoteDataPayload(),
|
getRemoteDataPayload(),
|
||||||
hasValueOperator(),
|
hasValueOperator(),
|
||||||
switchMap((version: Version) => version.versionhistory)
|
switchMap((version: Version) => version.versionhistory),
|
||||||
);
|
);
|
||||||
const versionHistory$ = this.versionHistoryRD$.pipe(
|
this.versionHistory$ = this.versionHistoryRD$.pipe(
|
||||||
getAllSucceededRemoteData(),
|
getFirstSucceededRemoteDataPayload(),
|
||||||
getRemoteDataPayload(),
|
|
||||||
hasValueOperator(),
|
hasValueOperator(),
|
||||||
);
|
);
|
||||||
const currentPagination = this.paginationService.getCurrentPagination(this.options.id, this.options);
|
|
||||||
this.versionsRD$ = observableCombineLatest(versionHistory$, currentPagination).pipe(
|
this.canCreateVersion$ = this.authorizationService.isAuthorized(FeatureID.CanCreateVersion, this.item.self);
|
||||||
switchMap(([versionHistory, options]: [VersionHistory, PaginationComponentOptions]) =>
|
|
||||||
this.versionHistoryService.getVersions(versionHistory.id,
|
// If there is a draft item in the version history the 'Create version' button is disabled and a different tooltip message is shown
|
||||||
new PaginatedSearchOptions({pagination: Object.assign({}, options, { currentPage: options.currentPage })}),
|
this.hasDraftVersion$ = this.versionHistoryRD$.pipe(
|
||||||
true, true, followLink('item'), followLink('eperson')))
|
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(
|
this.hasEpersons$ = this.versionsRD$.pipe(
|
||||||
getAllSucceededRemoteData(),
|
getAllSucceededRemoteData(),
|
||||||
getRemoteDataPayload(),
|
getRemoteDataPayload(),
|
||||||
@@ -150,8 +497,15 @@ export class ItemVersionsComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
|
this.cleanupSubscribes();
|
||||||
this.paginationService.clearPagination(this.options.id);
|
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) })"
|
[content]="('item.version.notice' | translate:{ destination: getItemPage(((latestVersion$ | async)?.item | async)?.payload) })"
|
||||||
[dismissible]="false"
|
[dismissible]="false"
|
||||||
[type]="AlertTypeEnum.Warning">
|
[type]="AlertTypeEnum.Warning">
|
||||||
|
@@ -10,10 +10,13 @@ import { VersionHistoryDataService } from '../../../../core/data/version-history
|
|||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../../remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../../remote-data.utils';
|
||||||
import { createPaginatedList } from '../../../testing/utils.test';
|
import { createPaginatedList } from '../../../testing/utils.test';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
import { take } from 'rxjs/operators';
|
||||||
|
|
||||||
describe('ItemVersionsNoticeComponent', () => {
|
describe('ItemVersionsNoticeComponent', () => {
|
||||||
let component: ItemVersionsNoticeComponent;
|
let component: ItemVersionsNoticeComponent;
|
||||||
let fixture: ComponentFixture<ItemVersionsNoticeComponent>;
|
let fixture: ComponentFixture<ItemVersionsNoticeComponent>;
|
||||||
|
let versionHistoryService: VersionHistoryDataService;
|
||||||
|
|
||||||
const versionHistory = Object.assign(new VersionHistory(), {
|
const versionHistory = Object.assign(new VersionHistory(), {
|
||||||
id: '1'
|
id: '1'
|
||||||
@@ -48,19 +51,29 @@ describe('ItemVersionsNoticeComponent', () => {
|
|||||||
});
|
});
|
||||||
firstVersion.item = createSuccessfulRemoteDataObject$(firstItem);
|
firstVersion.item = createSuccessfulRemoteDataObject$(firstItem);
|
||||||
latestVersion.item = createSuccessfulRemoteDataObject$(latestItem);
|
latestVersion.item = createSuccessfulRemoteDataObject$(latestItem);
|
||||||
const versionHistoryService = jasmine.createSpyObj('versionHistoryService', {
|
|
||||||
getVersions: createSuccessfulRemoteDataObject$(createPaginatedList(versions))
|
const versionHistoryServiceSpy = jasmine.createSpyObj('versionHistoryService',
|
||||||
});
|
['getVersions', 'getLatestVersionFromHistory$', 'isLatest$', ]
|
||||||
|
);
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [ItemVersionsNoticeComponent],
|
declarations: [ItemVersionsNoticeComponent],
|
||||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: VersionHistoryDataService, useValue: versionHistoryService }
|
{ provide: VersionHistoryDataService, useValue: versionHistoryServiceSpy }
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).compileComponents();
|
}).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', () => {
|
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) {
|
function initComponentWithItem(item: Item) {
|
||||||
fixture = TestBed.createComponent(ItemVersionsNoticeComponent);
|
fixture = TestBed.createComponent(ItemVersionsNoticeComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
|
@@ -1,15 +1,16 @@
|
|||||||
import { Component, Input, OnInit } from '@angular/core';
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
import { Item } from '../../../../core/shared/item.model';
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
import { PaginationComponentOptions } from '../../../pagination/pagination-component-options.model';
|
import { Observable } from 'rxjs';
|
||||||
import { PaginatedSearchOptions } from '../../../search/paginated-search-options.model';
|
|
||||||
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
|
||||||
import { RemoteData } from '../../../../core/data/remote-data';
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
import { VersionHistory } from '../../../../core/shared/version-history.model';
|
import { VersionHistory } from '../../../../core/shared/version-history.model';
|
||||||
import { Version } from '../../../../core/shared/version.model';
|
import { Version } from '../../../../core/shared/version.model';
|
||||||
import { hasValue, hasValueOperator } from '../../../empty.util';
|
import { hasValue, hasValueOperator } from '../../../empty.util';
|
||||||
import { getAllSucceededRemoteData, getRemoteDataPayload } from '../../../../core/shared/operators';
|
import {
|
||||||
import { filter, map, startWith, switchMap } from 'rxjs/operators';
|
getAllSucceededRemoteData,
|
||||||
import { followLink } from '../../../utils/follow-link-config.model';
|
getFirstSucceededRemoteDataPayload,
|
||||||
|
getRemoteDataPayload
|
||||||
|
} from '../../../../core/shared/operators';
|
||||||
|
import { map, startWith, switchMap } from 'rxjs/operators';
|
||||||
import { VersionHistoryDataService } from '../../../../core/data/version-history-data.service';
|
import { VersionHistoryDataService } from '../../../../core/data/version-history-data.service';
|
||||||
import { AlertType } from '../../../alert/aletr-type';
|
import { AlertType } from '../../../alert/aletr-type';
|
||||||
import { getItemPageRoute } from '../../../../item-page/item-page-routing-paths';
|
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?
|
* 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
|
* 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)
|
* 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
|
* The AlertType enumeration
|
||||||
@@ -71,7 +67,6 @@ export class ItemVersionsNoticeComponent implements OnInit {
|
|||||||
* Initialize the component's observables
|
* Initialize the component's observables
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
const latestVersionSearch = new PaginatedSearchOptions({pagination: this.latestVersionOptions});
|
|
||||||
if (hasValue(this.item.version)) {
|
if (hasValue(this.item.version)) {
|
||||||
this.versionRD$ = this.item.version;
|
this.versionRD$ = this.item.version;
|
||||||
this.versionHistoryRD$ = this.versionRD$.pipe(
|
this.versionHistoryRD$ = this.versionRD$.pipe(
|
||||||
@@ -80,25 +75,17 @@ export class ItemVersionsNoticeComponent implements OnInit {
|
|||||||
hasValueOperator(),
|
hasValueOperator(),
|
||||||
switchMap((version: Version) => version.versionhistory)
|
switchMap((version: Version) => version.versionhistory)
|
||||||
);
|
);
|
||||||
const versionHistory$ = this.versionHistoryRD$.pipe(
|
|
||||||
getAllSucceededRemoteData(),
|
this.latestVersion$ = this.versionHistoryRD$.pipe(
|
||||||
getRemoteDataPayload(),
|
getFirstSucceededRemoteDataPayload(),
|
||||||
);
|
switchMap((vh) => this.versionHistoryService.getLatestVersionFromHistory$(vh))
|
||||||
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.isLatestVersion$ = observableCombineLatest(
|
this.showLatestVersionNotice$ = this.versionRD$.pipe(
|
||||||
this.versionRD$.pipe(getAllSucceededRemoteData(), getRemoteDataPayload()), this.latestVersion$
|
getFirstSucceededRemoteDataPayload(),
|
||||||
).pipe(
|
switchMap((version) => this.versionHistoryService.isLatest$(version)),
|
||||||
map(([itemVersion, latestVersion]: [Version, Version]) => itemVersion.id === latestVersion.id),
|
map((isLatest) => isLatest != null && !isLatest),
|
||||||
startWith(true)
|
startWith(false),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -16,7 +16,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ng-content></ng-content>
|
<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 { 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 { 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 { 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 { HoverClassDirective } from './hover-class.directive';
|
||||||
import { ValidationSuggestionsComponent } from './input-suggestions/validation-suggestions/validation-suggestions.component';
|
import { ValidationSuggestionsComponent } from './input-suggestions/validation-suggestions/validation-suggestions.component';
|
||||||
import { ItemAlertsComponent } from './item/item-alerts/item-alerts.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 { TextMenuItemComponent } from './menu/menu-item/text-menu-item.component';
|
||||||
import { ThemedConfigurationSearchPageComponent } from '../search-page/themed-configuration-search-page.component';
|
import { ThemedConfigurationSearchPageComponent } from '../search-page/themed-configuration-search-page.component';
|
||||||
import { SearchNavbarComponent } from '../search-navbar/search-navbar.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 { 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';
|
import { BitstreamRequestACopyPageComponent } from './bitstream-request-a-copy-page/bitstream-request-a-copy-page.component';
|
||||||
|
|
||||||
@@ -462,7 +465,7 @@ const COMPONENTS = [
|
|||||||
CollectionSidebarSearchListElementComponent,
|
CollectionSidebarSearchListElementComponent,
|
||||||
CommunitySidebarSearchListElementComponent,
|
CommunitySidebarSearchListElementComponent,
|
||||||
SearchNavbarComponent,
|
SearchNavbarComponent,
|
||||||
ScopeSelectorModalComponent
|
ScopeSelectorModalComponent,
|
||||||
];
|
];
|
||||||
|
|
||||||
const ENTRY_COMPONENTS = [
|
const ENTRY_COMPONENTS = [
|
||||||
@@ -528,7 +531,7 @@ const ENTRY_COMPONENTS = [
|
|||||||
LinkMenuItemComponent,
|
LinkMenuItemComponent,
|
||||||
OnClickMenuItemComponent,
|
OnClickMenuItemComponent,
|
||||||
TextMenuItemComponent,
|
TextMenuItemComponent,
|
||||||
ScopeSelectorModalComponent
|
ScopeSelectorModalComponent,
|
||||||
];
|
];
|
||||||
|
|
||||||
const SHARED_SEARCH_PAGE_COMPONENTS = [
|
const SHARED_SEARCH_PAGE_COMPONENTS = [
|
||||||
@@ -540,6 +543,7 @@ const SHARED_ITEM_PAGE_COMPONENTS = [
|
|||||||
MetadataFieldWrapperComponent,
|
MetadataFieldWrapperComponent,
|
||||||
MetadataValuesComponent,
|
MetadataValuesComponent,
|
||||||
DsoPageEditButtonComponent,
|
DsoPageEditButtonComponent,
|
||||||
|
DsoPageVersionButtonComponent,
|
||||||
ItemAlertsComponent,
|
ItemAlertsComponent,
|
||||||
GenericItemPageFieldComponent,
|
GenericItemPageFieldComponent,
|
||||||
MetadataRepresentationListComponent,
|
MetadataRepresentationListComponent,
|
||||||
@@ -590,7 +594,9 @@ const DIRECTIVES = [
|
|||||||
...COMPONENTS,
|
...COMPONENTS,
|
||||||
...DIRECTIVES,
|
...DIRECTIVES,
|
||||||
...SHARED_ITEM_PAGE_COMPONENTS,
|
...SHARED_ITEM_PAGE_COMPONENTS,
|
||||||
...SHARED_SEARCH_PAGE_COMPONENTS
|
...SHARED_SEARCH_PAGE_COMPONENTS,
|
||||||
|
ItemVersionsSummaryModalComponent,
|
||||||
|
ItemVersionsDeleteModalComponent,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
...PROVIDERS
|
...PROVIDERS
|
||||||
|
@@ -2061,6 +2061,10 @@
|
|||||||
|
|
||||||
"item.page.return": "Back",
|
"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.identifier.uri": "Identifier:",
|
||||||
|
|
||||||
"item.preview.dc.contributor.author": "Authors:",
|
"item.preview.dc.contributor.author": "Authors:",
|
||||||
@@ -2116,6 +2120,8 @@
|
|||||||
|
|
||||||
"item.version.history.selected": "Selected version",
|
"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.version": "Version",
|
||||||
|
|
||||||
"item.version.history.table.item": "Item",
|
"item.version.history.table.item": "Item",
|
||||||
@@ -2126,11 +2132,77 @@
|
|||||||
|
|
||||||
"item.version.history.table.summary": "Summary",
|
"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.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",
|
"journal.listelement.badge": "Journal",
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user